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();
}
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);
if (item->type == Tutorial)
return true;
@@ -322,12 +324,28 @@ static bool isValidExampleOrDemo(ExampleItem *item)
}
if (!ok) {
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())
qCDebug(log) << QString::fromLatin1("WARNING: Item \"%1\" has no description")
.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();
};
}
// ordered list of "known" categories
@@ -365,6 +383,7 @@ void ExamplesViewController::updateExamples()
&demosInstallPath,
&qtVersion,
m_isExamples);
QSet<QString> instructionalsModules;
QStringList categoryOrder;
QList<ExampleItem *> items;
for (const QString &exampleSource : sources) {
@@ -382,10 +401,12 @@ void ExamplesViewController::updateExamples()
<< result.error();
continue;
}
items += filtered(result->items, isValidExampleOrDemo);
instructionalsModules.insert(result->instructionalsModule);
items += result->items;
if (categoryOrder.isEmpty())
categoryOrder = result->categoryOrder;
}
items = filtered(items, isValidExampleOrDemo(instructionalsModules));
if (m_isExamples) {
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 FilePath path = manifestPath.parentDir();
QStringList categoryOrder;
QList<ExampleItem *> items;
ParsedExamples result;
QXmlStreamReader reader(manifestData);
while (!reader.atEnd()) {
switch (reader.readNext()) {
case QXmlStreamReader::StartElement:
if (categoryOrder.isEmpty() && reader.name() == QLatin1String("categories"))
categoryOrder = parseCategories(&reader);
if (reader.name() == QLatin1String("instructionals"))
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"))
items += parseExamples(&reader, path, examplesInstallPath);
result.items += parseExamples(&reader, path, examplesInstallPath);
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"))
items += parseTutorials(&reader, path);
result.items += parseTutorials(&reader, path);
break;
default: // nothing
break;
@@ -304,14 +305,14 @@ expected_str<ParsedExamples> parseExamples(const QByteArray &manifestData,
}
if (reader.hasError()) {
qDeleteAll(items);
qDeleteAll(result.items);
return make_unexpected(QString("Could not parse file \"%1\" as XML document: %2:%3: %4")
.arg(manifestPath.toUserOutput())
.arg(reader.lineNumber())
.arg(reader.columnNumber())
.arg(reader.errorString()));
}
return {{items, categoryOrder}};
return result;
}
static bool sortByHighlightedAndName(ExampleItem *first, ExampleItem *second)

View File

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

View File

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