1 /*
2 SPDX-FileCopyrightText: 2012 Miha Čančula <miha@noughmad.eu>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "ctestrunjob.h"
8 #include "ctestsuite.h"
9 #include "qttestdelegate.h"
10 #include <debug.h>
11
12 #include <algorithm>
13 #include <interfaces/ilaunchconfiguration.h>
14 #include <interfaces/icore.h>
15 #include <interfaces/itestcontroller.h>
16 #include <interfaces/iruncontroller.h>
17 #include <interfaces/ilauncher.h>
18 #include <interfaces/launchconfigurationtype.h>
19 #include <interfaces/ilaunchmode.h>
20 #include <util/executecompositejob.h>
21 #include <outputview/outputmodel.h>
22
23 #include <KConfigGroup>
24 #include <KLocalizedString>
25
26 using namespace KDevelop;
27
CTestRunJob(CTestSuite * suite,const QStringList & cases,OutputJob::OutputJobVerbosity verbosity,QObject * parent)28 CTestRunJob::CTestRunJob(CTestSuite* suite, const QStringList& cases, OutputJob::OutputJobVerbosity verbosity, QObject* parent)
29 : KJob(parent)
30 , m_suite(suite)
31 , m_cases(cases)
32 , m_job(nullptr)
33 , m_outputModel(nullptr)
34 , m_verbosity(verbosity)
35 {
36 for (const QString& testCase : cases) {
37 m_caseResults[testCase] = TestResult::NotRun;
38 }
39
40 setCapabilities(Killable);
41 }
42
43
createTestJob(const QString & launchModeId,const QStringList & arguments,const QString & workingDirectory)44 static KJob* createTestJob(const QString& launchModeId, const QStringList& arguments, const QString &workingDirectory)
45 {
46 LaunchConfigurationType* type = ICore::self()->runController()->launchConfigurationTypeForId( QStringLiteral("Native Application") );
47 ILaunchMode* mode = ICore::self()->runController()->launchModeForId( launchModeId );
48
49 qCDebug(CMAKE) << "got mode and type:" << type << type->id() << mode << mode->id();
50 Q_ASSERT(type && mode);
51
52 ILauncher* launcher = [type, mode]() {
53 const auto launchers = type->launchers();
54 auto it = std::find_if(launchers.begin(), launchers.end(), [mode](ILauncher *l) {
55 return l->supportedModes().contains(mode->id());
56 });
57 Q_ASSERT(it != launchers.end());
58 return *it;
59 }();
60 Q_ASSERT(launcher);
61
62 auto ilaunch = [type]() {
63 const auto launchConfigurations = ICore::self()->runController()->launchConfigurations();
64 auto it = std::find_if(launchConfigurations.begin(), launchConfigurations.end(),
65 [type](ILaunchConfiguration* l) {
66 return (l->type() == type && l->config().readEntry("ConfiguredByCTest", false));
67 });
68 return it == launchConfigurations.end() ? nullptr : *it;
69 }();
70
71 if (!ilaunch) {
72 ilaunch = ICore::self()->runController()->createLaunchConfiguration( type,
73 qMakePair( mode->id(), launcher->id() ),
74 nullptr, //TODO add project
75 i18n("CTest") );
76 ilaunch->config().writeEntry("ConfiguredByCTest", true);
77 //qCDebug(CMAKE) << "created config, launching";
78 } else {
79 //qCDebug(CMAKE) << "reusing generated config, launching";
80 }
81 if (!workingDirectory.isEmpty())
82 ilaunch->config().writeEntry( "Working Directory", QUrl::fromLocalFile( workingDirectory ) );
83 type->configureLaunchFromCmdLineArguments( ilaunch->config(), arguments );
84 return ICore::self()->runController()->execute(launchModeId, ilaunch);
85 }
86
start()87 void CTestRunJob::start()
88 {
89 // if (!m_suite->cases().isEmpty())
90 // {
91 // TODO: Find a better way of determining whether QTestLib is used by this test
92 // qCDebug(CMAKE) << "Setting a QtTestDelegate";
93 // setDelegate(new QtTestDelegate);
94 // }
95 // setStandardToolView(IOutputView::RunView);
96
97 QStringList arguments = m_cases;
98 if (m_cases.isEmpty() && !m_suite->arguments().isEmpty())
99 {
100 arguments = m_suite->arguments();
101 }
102
103 QStringList cases_selected = arguments;
104 arguments.prepend(m_suite->executable().toLocalFile());
105 const QString workingDirectory = m_suite->properties().value(QStringLiteral("WORKING_DIRECTORY"), QString());
106
107 m_job = createTestJob(QStringLiteral("execute"), arguments, workingDirectory);
108
109 if (auto* cjob = qobject_cast<ExecuteCompositeJob*>(m_job)) {
110 auto* outputJob = cjob->findChild<OutputJob*>();
111 if (outputJob) {
112 outputJob->setVerbosity(m_verbosity);
113
114 QString testName = m_suite->name();
115 QString title;
116 if (cases_selected.count() == 1)
117 title = i18nc("running test %1, %2 test case", "CTest %1: %2", testName, cases_selected.value(0));
118 else
119 title = i18ncp("running test %1, %2 number of test cases", "CTest %2 (%1)", "CTest %2 (%1)",
120 cases_selected.count(), testName);
121
122 outputJob->setTitle(title);
123
124 m_outputModel = qobject_cast<OutputModel*>(outputJob->model());
125 connect(m_outputModel, &QAbstractItemModel::rowsInserted, this, &CTestRunJob::rowsInserted);
126 }
127 }
128 connect(m_job, &KJob::finished, this, &CTestRunJob::processFinished);
129
130 ICore::self()->testController()->notifyTestRunStarted(m_suite, cases_selected);
131 }
132
doKill()133 bool CTestRunJob::doKill()
134 {
135 if (m_job)
136 {
137 m_job->kill();
138 }
139 return true;
140 }
141
processFinished(KJob * job)142 void CTestRunJob::processFinished(KJob* job)
143 {
144 int error = job->error();
145 auto finished = [this,error]() {
146 TestResult result;
147 result.testCaseResults = m_caseResults;
148 if (error == OutputJob::FailedShownError) {
149 result.suiteResult = TestResult::Failed;
150 } else if (error == KJob::NoError) {
151 result.suiteResult = TestResult::Passed;
152 } else {
153 result.suiteResult = TestResult::Error;
154 }
155
156 // in case the job was killed, mark this job as killed as well
157 if (error == KJob::KilledJobError) {
158 setError(KJob::KilledJobError);
159 setErrorText(QStringLiteral("Child job was killed."));
160 }
161
162 qCDebug(CMAKE) << result.suiteResult << result.testCaseResults;
163 ICore::self()->testController()->notifyTestRunFinished(m_suite, result);
164 emitResult();
165 };
166
167 if (m_outputModel)
168 {
169 connect(m_outputModel, &OutputModel::allDone, this, finished, Qt::QueuedConnection);
170 m_outputModel->ensureAllDone();
171 }
172 else
173 {
174 finished();
175 }
176 }
177
rowsInserted(const QModelIndex & parent,int startRow,int endRow)178 void CTestRunJob::rowsInserted(const QModelIndex &parent, int startRow, int endRow)
179 {
180 // This regular expression matches the name of the testcase (whatever between the last "::" and "(", indeed )
181 // For example, from:
182 // PASS : ExpTest::testExp(sum)
183 // matches "testExp"
184 static QRegExp caseRx(QStringLiteral("::([^:]*)\\("), Qt::CaseSensitive, QRegExp::RegExp2);
185 for (int row = startRow; row <= endRow; ++row)
186 {
187 QString line = m_outputModel->data(m_outputModel->index(row, 0, parent), Qt::DisplayRole).toString();
188
189 QString testCase;
190 if (caseRx.indexIn(line) >= 0) {
191 testCase = caseRx.cap(1);
192 }
193
194 TestResult::TestCaseResult prevResult = m_caseResults.value(testCase, TestResult::NotRun);
195 if (prevResult == TestResult::Passed || prevResult == TestResult::NotRun)
196 {
197 TestResult::TestCaseResult result = TestResult::NotRun;
198 const bool expectFail = m_suite->properties().value(QStringLiteral("WILL_FAIL"), QStringLiteral("FALSE")) == QLatin1String("TRUE");
199 if (line.startsWith(QLatin1String("PASS :")))
200 {
201 result = expectFail ? TestResult::UnexpectedPass : TestResult::Passed;
202 }
203 else if (line.startsWith(QLatin1String("FAIL! :")))
204 {
205 result = expectFail ? TestResult::ExpectedFail : TestResult::Failed;
206 }
207 else if (line.startsWith(QLatin1String("XFAIL :")))
208 {
209 result = TestResult::ExpectedFail;
210 }
211 else if (line.startsWith(QLatin1String("XPASS :")))
212 {
213 result = TestResult::UnexpectedPass;
214 }
215 else if (line.startsWith(QLatin1String("SKIP :")))
216 {
217 result = TestResult::Skipped;
218 }
219
220 if (result != TestResult::NotRun)
221 {
222 m_caseResults[testCase] = result;
223 }
224 }
225 }
226 }
227