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