Examples: Read and handle "documentation dependencies"

Some examples are defined in a Qt module (and therefore examples-
manifest), but have dependencies on other modules.

The example-manifests contain a `module` attribute for their
`instructionals` tag, and examples can contain "docdependencies" items
in their meta data, which are comma-separated lists of these module
names. Read the `module` attributes (the meta data is already
generically read) and check if the current set of examples-manifest
contains all required ones for an example. Hide it, if not.

Task-number: QTBUG-120759
Change-Id: Ib8ff80cf8a13965e57b5c4317e5d121c4127278b
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Kai Köhne <kai.koehne@qt.io>
This commit is contained in:
Eike Ziller
2024-01-17 15:08:45 +01:00
parent 716c68ac5a
commit 2865f6eae0
4 changed files with 74 additions and 47 deletions

View File

@@ -299,8 +299,10 @@ ExamplesViewController::ExamplesViewController(ExampleSetModel *exampleSetModel,
updateExamples(); updateExamples();
} }
static bool isValidExampleOrDemo(ExampleItem *item) static std::function<bool(ExampleItem *)> isValidExampleOrDemo(
const QSet<QString> &instructionalsModules)
{ {
return [instructionalsModules](ExampleItem *item) -> bool {
QTC_ASSERT(item, return false); QTC_ASSERT(item, return false);
if (item->type == Tutorial) if (item->type == Tutorial)
return true; return true;
@@ -322,12 +324,28 @@ static bool isValidExampleOrDemo(ExampleItem *item)
} }
if (!ok) { if (!ok) {
item->tags.append(QLatin1String("broken")); item->tags.append(QLatin1String("broken"));
qCDebug(log) << QString::fromLatin1("ERROR: Item \"%1\" broken: %2").arg(item->name, reason); qCDebug(log) << QString::fromLatin1("ERROR: Item \"%1\" broken: %2")
.arg(item->name, reason);
} }
if (item->description.isEmpty()) if (item->description.isEmpty())
qCDebug(log) << QString::fromLatin1("WARNING: Item \"%1\" has no description") qCDebug(log) << QString::fromLatin1("WARNING: Item \"%1\" has no description")
.arg(item->name); .arg(item->name);
// a single docdependencies entry is a string of items concatenated with ','
// the collected meta data can be a list of this
for (const QString &entry : item->metaData.value("docdependencies")) {
const QStringList deps = entry.split(',');
for (const QString &dep : deps) {
if (!instructionalsModules.contains(dep)) {
item->tags.append("unresolvedDependency");
qCDebug(log) << QLatin1String("INFO: Item \"%1\" requires \"%2\"")
.arg(item->name, dep);
ok = false;
break;
}
}
}
return ok || debugExamples(); return ok || debugExamples();
};
} }
// ordered list of "known" categories // ordered list of "known" categories
@@ -365,6 +383,7 @@ void ExamplesViewController::updateExamples()
&demosInstallPath, &demosInstallPath,
&qtVersion, &qtVersion,
m_isExamples); m_isExamples);
QSet<QString> instructionalsModules;
QStringList categoryOrder; QStringList categoryOrder;
QList<ExampleItem *> items; QList<ExampleItem *> items;
for (const QString &exampleSource : sources) { for (const QString &exampleSource : sources) {
@@ -382,10 +401,12 @@ void ExamplesViewController::updateExamples()
<< result.error(); << result.error();
continue; continue;
} }
items += filtered(result->items, isValidExampleOrDemo); instructionalsModules.insert(result->instructionalsModule);
items += result->items;
if (categoryOrder.isEmpty()) if (categoryOrder.isEmpty())
categoryOrder = result->categoryOrder; categoryOrder = result->categoryOrder;
} }
items = filtered(items, isValidExampleOrDemo(instructionalsModules));
if (m_isExamples) { if (m_isExamples) {
if (m_exampleSetModel->selectedQtSupports(Android::Constants::ANDROID_DEVICE_TYPE)) { if (m_exampleSetModel->selectedQtSupports(Android::Constants::ANDROID_DEVICE_TYPE)) {

View File

@@ -283,20 +283,21 @@ expected_str<ParsedExamples> parseExamples(const QByteArray &manifestData,
const bool examples) const bool examples)
{ {
const FilePath path = manifestPath.parentDir(); const FilePath path = manifestPath.parentDir();
QStringList categoryOrder; ParsedExamples result;
QList<ExampleItem *> items;
QXmlStreamReader reader(manifestData); QXmlStreamReader reader(manifestData);
while (!reader.atEnd()) { while (!reader.atEnd()) {
switch (reader.readNext()) { switch (reader.readNext()) {
case QXmlStreamReader::StartElement: case QXmlStreamReader::StartElement:
if (categoryOrder.isEmpty() && reader.name() == QLatin1String("categories")) if (reader.name() == QLatin1String("instructionals"))
categoryOrder = parseCategories(&reader); result.instructionalsModule = reader.attributes().value("module").toString();
else if (result.categoryOrder.isEmpty() && reader.name() == QLatin1String("categories"))
result.categoryOrder = parseCategories(&reader);
else if (examples && reader.name() == QLatin1String("examples")) else if (examples && reader.name() == QLatin1String("examples"))
items += parseExamples(&reader, path, examplesInstallPath); result.items += parseExamples(&reader, path, examplesInstallPath);
else if (examples && reader.name() == QLatin1String("demos")) else if (examples && reader.name() == QLatin1String("demos"))
items += parseDemos(&reader, path, demosInstallPath); result.items += parseDemos(&reader, path, demosInstallPath);
else if (!examples && reader.name() == QLatin1String("tutorials")) else if (!examples && reader.name() == QLatin1String("tutorials"))
items += parseTutorials(&reader, path); result.items += parseTutorials(&reader, path);
break; break;
default: // nothing default: // nothing
break; break;
@@ -304,14 +305,14 @@ expected_str<ParsedExamples> parseExamples(const QByteArray &manifestData,
} }
if (reader.hasError()) { if (reader.hasError()) {
qDeleteAll(items); qDeleteAll(result.items);
return make_unexpected(QString("Could not parse file \"%1\" as XML document: %2:%3: %4") return make_unexpected(QString("Could not parse file \"%1\" as XML document: %2:%3: %4")
.arg(manifestPath.toUserOutput()) .arg(manifestPath.toUserOutput())
.arg(reader.lineNumber()) .arg(reader.lineNumber())
.arg(reader.columnNumber()) .arg(reader.columnNumber())
.arg(reader.errorString())); .arg(reader.errorString()));
} }
return {{items, categoryOrder}}; return result;
} }
static bool sortByHighlightedAndName(ExampleItem *first, ExampleItem *second) static bool sortByHighlightedAndName(ExampleItem *first, ExampleItem *second)

View File

@@ -34,6 +34,7 @@ public:
class QTSUPPORT_TEST_EXPORT ParsedExamples class QTSUPPORT_TEST_EXPORT ParsedExamples
{ {
public: public:
QString instructionalsModule;
QList<ExampleItem *> items; QList<ExampleItem *> items;
QStringList categoryOrder; QStringList categoryOrder;
}; };

View File

@@ -71,6 +71,7 @@ static ExampleItem fetchItem()
void tst_Examples::parsing_data() void tst_Examples::parsing_data()
{ {
QTest::addColumn<QByteArray>("data"); QTest::addColumn<QByteArray>("data");
QTest::addColumn<QString>("instructionalsModule");
QTest::addColumn<bool>("isExamples"); QTest::addColumn<bool>("isExamples");
QTest::addColumn<QString>("name"); QTest::addColumn<QString>("name");
QTest::addColumn<QString>("description"); QTest::addColumn<QString>("description");
@@ -94,7 +95,7 @@ void tst_Examples::parsing_data()
QTest::addRow("example") QTest::addRow("example")
<< QByteArray(R"raw( << QByteArray(R"raw(
<instructionals module="Qt"> <instructionals module="QtGui">
<examples> <examples>
<example docUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/qtwidgets-widgets-analogclock-example.html" <example docUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/qtwidgets-widgets-analogclock-example.html"
imageUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/images/analogclock-example.png" imageUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/images/analogclock-example.png"
@@ -119,8 +120,8 @@ void tst_Examples::parsing_data()
<category>Embedded</category> <category>Embedded</category>
</categories> </categories>
</instructionals> </instructionals>
)raw") << /*isExamples=*/true )raw") << "QtGui"
<< "Analog Clock" << /*isExamples=*/true << "Analog Clock"
<< "The Analog Clock example shows how to draw the contents of a custom widget." << "The Analog Clock example shows how to draw the contents of a custom widget."
<< "qthelp://org.qt-project.qtwidgets.660/qtwidgets/images/analogclock-example.png" << "qthelp://org.qt-project.qtwidgets.660/qtwidgets/images/analogclock-example.png"
<< QStringList{"ios", "widgets"} << QStringList{"ios", "widgets"}
@@ -140,17 +141,18 @@ void tst_Examples::parsing_data()
QTest::addRow("no category, highlighted") QTest::addRow("no category, highlighted")
<< QByteArray(R"raw( << QByteArray(R"raw(
<instructionals module="Qt"> <instructionals module="QtQuick3D">
<examples> <examples>
<example name="No Category, highlighted" <example name="No Category, highlighted"
isHighlighted="true"> isHighlighted="true">
</example> </example>
</examples> </examples>
</instructionals> </instructionals>
)raw") << /*isExamples=*/true )raw") << "QtQuick3D"
<< "No Category, highlighted" << QString() << QString() << QStringList() << /*isExamples=*/true << "No Category, highlighted" << QString() << QString()
<< FilePath("examples") << QString() << FilePaths() << FilePath() << FilePaths() << Example << QStringList() << FilePath("examples") << QString() << FilePaths() << FilePath()
<< /*hasSourceCode=*/false << false << /*isHighlighted=*/true << "" << FilePaths() << Example << /*hasSourceCode=*/false << false << /*isHighlighted=*/true
<< ""
<< "" << QStringList() << MetaData() << QStringList{"Featured"} << QStringList(); << "" << QStringList() << MetaData() << QStringList{"Featured"} << QStringList();
QTest::addRow("tutorial with category") QTest::addRow("tutorial with category")
@@ -172,8 +174,8 @@ void tst_Examples::parsing_data()
</tutorial> </tutorial>
</tutorials> </tutorials>
</instructionals> </instructionals>
)raw") << /*isExamples=*/false )raw") << "Qt"
<< "A tutorial" << /*isExamples=*/false << "A tutorial"
<< "A dummy tutorial." << "A dummy tutorial."
<< ":qtsupport/images/icons/tutorialicon.png" << ":qtsupport/images/icons/tutorialicon.png"
<< QStringList{"qt creator", "build", "compile", "help"} << FilePath() << QStringList{"qt creator", "build", "compile", "help"} << FilePath()
@@ -187,6 +189,7 @@ void tst_Examples::parsing_data()
void tst_Examples::parsing() void tst_Examples::parsing()
{ {
QFETCH(QByteArray, data); QFETCH(QByteArray, data);
QFETCH(QString, instructionalsModule);
QFETCH(bool, isExamples); QFETCH(bool, isExamples);
QFETCH(QStringList, categories); QFETCH(QStringList, categories);
QFETCH(QStringList, categoryOrder); QFETCH(QStringList, categoryOrder);
@@ -198,6 +201,7 @@ void tst_Examples::parsing()
FilePath("demos"), FilePath("demos"),
isExamples); isExamples);
QVERIFY(result); QVERIFY(result);
QCOMPARE(result->instructionalsModule, instructionalsModule);
QCOMPARE(result->categoryOrder, categoryOrder); QCOMPARE(result->categoryOrder, categoryOrder);
QCOMPARE(result->items.size(), 1); QCOMPARE(result->items.size(), 1);
const ExampleItem item = *result->items.at(0); const ExampleItem item = *result->items.at(0);