diff --git a/src/plugins/autotest/testcodeparser.cpp b/src/plugins/autotest/testcodeparser.cpp index 9f60f302e0d..7e671d972f0 100644 --- a/src/plugins/autotest/testcodeparser.cpp +++ b/src/plugins/autotest/testcodeparser.cpp @@ -25,10 +25,13 @@ #include "autotestconstants.h" #include "autotest_utils.h" +#include "autotestplugin.h" #include "testcodeparser.h" #include "testframeworkmanager.h" +#include "testsettings.h" #include +#include #include #include #include @@ -160,6 +163,28 @@ void TestCodeParser::updateTestTree() scanForTests(); } +static QStringList filterFiles(const QString &projectDir, const QStringList &files) +{ + const QSharedPointer &settings = AutotestPlugin::instance()->settings(); + const QSet &filters = settings->whiteListFilters.toSet(); // avoid duplicates + if (!settings->filterScan || filters.isEmpty()) + return files; + QStringList finalResult; + for (const QString &file : files) { + // apply filter only below project directory if file is part of a project + const QString &fileToProcess = file.startsWith(projectDir) + ? file.mid(projectDir.size()) + : file; + for (const QString &filter : filters) { + if (fileToProcess.contains(filter)) { + finalResult.push_back(file); + break; + } + } + } + return finalResult; +} + // used internally to indicate a parse that failed due to having triggered a parse for a file that // is not (yet) part of the CppModelManager's snapshot static bool parsingHasFailed; @@ -302,9 +327,12 @@ void TestCodeParser::scanForTests(const QStringList &fileList) m_reparseTimerTimedOut = false; m_postponedFiles.clear(); bool isFullParse = fileList.isEmpty(); + ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject(); + if (!project) + return; QStringList list; if (isFullParse) { - list = ProjectExplorer::SessionManager::startupProject()->files(ProjectExplorer::Project::SourceFiles); + list = project->files(ProjectExplorer::Project::SourceFiles); if (list.isEmpty()) { // at least project file should be there, but might happen if parsing current project // takes too long, especially when opening sessions holding multiple projects @@ -333,6 +361,17 @@ void TestCodeParser::scanForTests(const QStringList &fileList) m_model->markForRemoval(filePath); } + list = filterFiles(project->projectDirectory().toString(), list); + if (list.isEmpty()) { + if (isFullParse) { + Core::MessageManager::instance()->write( + tr("AutoTest Plugin WARNING: No files left after filtering test scan " + "folders. Check test filter settings."), + Core::MessageManager::Flash); + } + onFinished(); + return; + } qCDebug(LOG) << QDateTime::currentDateTime().toString("hh:mm:ss.zzz") << "StartParsing"; foreach (ITestParser *parser, m_testCodeParsers) parser->init(list); diff --git a/src/plugins/autotest/testsettings.cpp b/src/plugins/autotest/testsettings.cpp index c3f0b1bda00..0b24390b958 100644 --- a/src/plugins/autotest/testsettings.cpp +++ b/src/plugins/autotest/testsettings.cpp @@ -39,6 +39,8 @@ static const char omitInternalKey[] = "OmitInternal"; static const char omitRunConfigWarnKey[] = "OmitRCWarnings"; static const char limitResultOutputKey[] = "LimitResultOutput"; static const char autoScrollKey[] = "AutoScrollResults"; +static const char filterScanKey[] = "FilterScan"; +static const char filtersKey[] = "WhiteListFilters"; static const int defaultTimeout = 60000; @@ -55,6 +57,8 @@ void TestSettings::toSettings(QSettings *s) const s->setValue(omitRunConfigWarnKey, omitRunConfigWarn); s->setValue(limitResultOutputKey, limitResultOutput); s->setValue(autoScrollKey, autoScroll); + s->setValue(filterScanKey, filterScan); + s->setValue(filtersKey, whiteListFilters); // store frameworks and their current active state for (const Core::Id &id : frameworks.keys()) s->setValue(QLatin1String(id.name()), frameworks.value(id)); @@ -69,6 +73,8 @@ void TestSettings::fromSettings(QSettings *s) omitRunConfigWarn = s->value(omitRunConfigWarnKey, false).toBool(); limitResultOutput = s->value(limitResultOutputKey, true).toBool(); autoScroll = s->value(autoScrollKey, true).toBool(); + filterScan = s->value(filterScanKey, false).toBool(); + whiteListFilters = s->value(filtersKey, QStringList()).toStringList(); // try to get settings for registered frameworks TestFrameworkManager *frameworkManager = TestFrameworkManager::instance(); const QList ®istered = frameworkManager->registeredFrameworkIds(); diff --git a/src/plugins/autotest/testsettings.h b/src/plugins/autotest/testsettings.h index 5d0f6400f54..b3e0893b9d0 100644 --- a/src/plugins/autotest/testsettings.h +++ b/src/plugins/autotest/testsettings.h @@ -47,7 +47,9 @@ struct TestSettings bool omitRunConfigWarn = false; bool limitResultOutput = true; bool autoScroll = true; + bool filterScan = false; QHash frameworks; + QStringList whiteListFilters; }; } // namespace Internal diff --git a/src/plugins/autotest/testsettingspage.cpp b/src/plugins/autotest/testsettingspage.cpp index 3006eee7a3f..c41b49a7135 100644 --- a/src/plugins/autotest/testsettingspage.cpp +++ b/src/plugins/autotest/testsettingspage.cpp @@ -24,18 +24,97 @@ ****************************************************************************/ #include "autotestconstants.h" +#include "testcodeparser.h" #include "testframeworkmanager.h" #include "testsettingspage.h" #include "testsettings.h" #include "testtreemodel.h" #include - +#include +#include #include +#include +#include +#include + namespace Autotest { namespace Internal { +class TestFilterDialog : public QDialog +{ +public: + explicit TestFilterDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); + QString filterPath() const; + void setDetailsText(const QString &details) { m_details->setText(details); } + void setDefaultFilterPath(const QString &defaultPath); +private: + static bool validate(Utils::FancyLineEdit *edit, QString * /*errormessage*/); + QLabel *m_details; + Utils::FancyLineEdit *m_lineEdit; + QString m_defaultPath; +}; + +TestFilterDialog::TestFilterDialog(QWidget *parent, Qt::WindowFlags f) + : QDialog(parent, f), + m_details(new QLabel), + m_lineEdit(new Utils::FancyLineEdit) +{ + setModal(true); + auto layout = new QVBoxLayout(this); + layout->setSizeConstraint(QLayout::SetFixedSize); + layout->addWidget(m_details); + m_lineEdit->setValidationFunction(&validate); + layout->addWidget(m_lineEdit); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + auto okButton = buttonBox->button(QDialogButtonBox::Ok); + auto cancelButton = buttonBox->button(QDialogButtonBox::Cancel); + okButton->setEnabled(false); + layout->addWidget(buttonBox); + setLayout(layout); + connect(m_lineEdit, &Utils::FancyLineEdit::validChanged, okButton, &QPushButton::setEnabled); + connect(okButton, &QPushButton::clicked, this, &QDialog::accept); + connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); +} + +QString TestFilterDialog::filterPath() const +{ + static const QRegExp repetition("//+"); + QString path = m_lineEdit->isValid() ? m_lineEdit->text() : m_defaultPath; + path.replace('\\', '/'); // turn windows separators into forward slashes + path.replace(repetition, "/"); // avoid duplicated separators + if (!path.startsWith('/')) + path.prepend('/'); + if (!path.endsWith('/')) + path.append('/'); + if (path.length() <= 2) // after surrounding with '/' this should be > 2 if valid + return QString(); // empty string marks invalid filter + return path; +} + +void TestFilterDialog::setDefaultFilterPath(const QString &defaultPath) +{ + m_lineEdit->setText(defaultPath); + if (m_lineEdit->isValid()) + m_defaultPath = defaultPath; + else + m_lineEdit->setText(m_defaultPath); +} + +bool TestFilterDialog::validate(Utils::FancyLineEdit *edit, QString *) +{ + if (!edit) + return false; + const QString &value = edit->text(); + if (value.isEmpty()) + return false; + // should we distinguish between Windows and UNIX? + static const QRegExp valid("(\\|/)?([^?*:;\"\'|<>\t\b]+(\\|/)?)+"); + return valid.exactMatch(value); +} +/**************************************************************************************************/ + TestSettingsWidget::TestSettingsWidget(QWidget *parent) : QWidget(parent) { @@ -49,6 +128,16 @@ TestSettingsWidget::TestSettingsWidget(QWidget *parent) "having at least one active test framework.")); connect(m_ui.frameworkListWidget, &QListWidget::itemChanged, this, &TestSettingsWidget::onFrameworkItemChanged); + connect(m_ui.addFilter, &QPushButton::clicked, this, &TestSettingsWidget::onAddFilterClicked); + connect(m_ui.editFilter, &QPushButton::clicked, this, &TestSettingsWidget::onEditFilterClicked); + connect(m_ui.filterTreeWidget, &QTreeWidget::activated, + this, &TestSettingsWidget::onEditFilterClicked); + connect(m_ui.removeFilter, &QPushButton::clicked, + this, &TestSettingsWidget::onRemoveFilterClicked); + connect(m_ui.filterTreeWidget, &QTreeWidget::itemSelectionChanged, [this] () { + m_ui.editFilter->setEnabled(true); + m_ui.removeFilter->setEnabled(true); + }); } void TestSettingsWidget::setSettings(const TestSettings &settings) @@ -58,7 +147,9 @@ void TestSettingsWidget::setSettings(const TestSettings &settings) m_ui.omitRunConfigWarnCB->setChecked(settings.omitRunConfigWarn); m_ui.limitResultOutputCB->setChecked(settings.limitResultOutput); m_ui.autoScrollCB->setChecked(settings.autoScroll); + m_ui.filterGroupBox->setChecked(settings.filterScan); populateFrameworksListWidget(settings.frameworks); + populateFiltersWidget(settings.whiteListFilters); } TestSettings TestSettingsWidget::settings() const @@ -69,7 +160,9 @@ TestSettings TestSettingsWidget::settings() const result.omitRunConfigWarn = m_ui.omitRunConfigWarnCB->isChecked(); result.limitResultOutput = m_ui.limitResultOutputCB->isChecked(); result.autoScroll = m_ui.autoScrollCB->isChecked(); + result.filterScan = m_ui.filterGroupBox->isChecked(); result.frameworks = frameworks(); + result.whiteListFilters = filters(); return result; } @@ -87,6 +180,12 @@ void TestSettingsWidget::populateFrameworksListWidget(const QHash TestSettingsWidget::frameworks() const { const int itemCount = m_ui.frameworkListWidget->count(); @@ -100,6 +199,16 @@ QHash TestSettingsWidget::frameworks() const return frameworks; } +QStringList TestSettingsWidget::filters() const +{ + QStringList result; + if (QAbstractItemModel *model = m_ui.filterTreeWidget->model()) { + for (int row = 0, count = model->rowCount(); row < count; ++row) + result.append(model->index(row, 0).data().toString()); + } + return result; +} + void TestSettingsWidget::onFrameworkItemChanged() { for (int row = 0, count = m_ui.frameworkListWidget->count(); row < count; ++row) { @@ -113,6 +222,45 @@ void TestSettingsWidget::onFrameworkItemChanged() m_ui.frameworksWarnIcon->setVisible(true); } +void TestSettingsWidget::onAddFilterClicked() +{ + TestFilterDialog dialog; + dialog.setWindowTitle(tr("Add Filter")); + dialog.setDetailsText(tr("

Specify a filter expression to be added to the list of filters." + "
Wildcards are not supported.

")); + if (dialog.exec() == QDialog::Accepted) { + const QString &filter = dialog.filterPath(); + if (!filter.isEmpty()) + new QTreeWidgetItem(m_ui.filterTreeWidget, {filter} ); + } +} + +void TestSettingsWidget::onEditFilterClicked() +{ + const QList &selected = m_ui.filterTreeWidget->selectedItems(); + QTC_ASSERT(selected.size() == 1, return); + const QString &oldFilter = selected.first()->data(0, Qt::DisplayRole).toString(); + + TestFilterDialog dialog; + dialog.setWindowTitle(tr("Edit Filter")); + dialog.setDetailsText(tr("

Specify a filter expression that will replace \"%1\"." + "
Wildcards are not supported.

").arg(oldFilter)); + dialog.setDefaultFilterPath(oldFilter); + if (dialog.exec() == QDialog::Accepted) { + const QString &edited = dialog.filterPath(); + if (!edited.isEmpty() && edited != oldFilter) + selected.first()->setData(0, Qt::DisplayRole, edited); + } +} + +void TestSettingsWidget::onRemoveFilterClicked() +{ + const QList &selected = m_ui.filterTreeWidget->selectedItems(); + QTC_ASSERT(selected.size() == 1, return); + m_ui.filterTreeWidget->removeItemWidget(selected.first(), 0); + delete selected.first(); +} + TestSettingsPage::TestSettingsPage(const QSharedPointer &settings) : m_settings(settings), m_widget(0) { @@ -142,11 +290,14 @@ void TestSettingsPage::apply() return; const TestSettings newSettings = m_widget->settings(); bool frameworkSyncNecessary = newSettings.frameworks != m_settings->frameworks; + bool forceReparse = newSettings.whiteListFilters.toSet() != m_settings->whiteListFilters.toSet(); *m_settings = newSettings; m_settings->toSettings(Core::ICore::settings()); TestFrameworkManager::instance()->activateFrameworksFromSettings(m_settings); if (frameworkSyncNecessary) TestTreeModel::instance()->syncTestFrameworks(); + else if (forceReparse) + TestTreeModel::instance()->parser()->emitUpdateTestTree(); } } // namespace Internal diff --git a/src/plugins/autotest/testsettingspage.h b/src/plugins/autotest/testsettingspage.h index 7cc55833594..6ee67154c65 100644 --- a/src/plugins/autotest/testsettingspage.h +++ b/src/plugins/autotest/testsettingspage.h @@ -47,8 +47,13 @@ public: private: void populateFrameworksListWidget(const QHash &frameworks); + void populateFiltersWidget(const QStringList &filters); QHash frameworks() const; + QStringList filters() const; void onFrameworkItemChanged(); + void onAddFilterClicked(); + void onEditFilterClicked(); + void onRemoveFilterClicked(); Ui::TestSettingsPage m_ui; }; diff --git a/src/plugins/autotest/testsettingspage.ui b/src/plugins/autotest/testsettingspage.ui index 4e4a39cfb49..9ab4a5951f7 100644 --- a/src/plugins/autotest/testsettingspage.ui +++ b/src/plugins/autotest/testsettingspage.ui @@ -7,7 +7,7 @@ 0 0 585 - 324 + 373 @@ -201,7 +201,7 @@ 20 - 40 + 20 @@ -210,6 +210,115 @@ + + + + true + + + Global Filters + + + true + + + false + + + + + + Filters used on directories when scanning for tests.<br/>If filtering is enabled, only directories that match any of the filters will be scanned. + + + true + + + true + + + + 1 + + + + + + + + + + Add... + + + + + + + false + + + Edit... + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + false + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + +