42 #include <QDir> |
42 #include <QDir> |
43 #include <QFile> |
43 #include <QFile> |
44 #include <QRegExp> |
44 #include <QRegExp> |
45 #include <QStringList> |
45 #include <QStringList> |
46 #include <QTest> |
46 #include <QTest> |
|
47 #include <QSet> |
|
48 #include <QProcess> |
|
49 #include <QDebug> |
|
50 |
|
51 enum FindSubdirsMode { |
|
52 Flat = 0, |
|
53 Recursive |
|
54 }; |
47 |
55 |
48 class tst_MakeTestSelfTest: public QObject |
56 class tst_MakeTestSelfTest: public QObject |
49 { |
57 { |
50 Q_OBJECT |
58 Q_OBJECT |
51 |
59 |
52 private slots: |
60 private slots: |
|
61 void tests_auto_pro(); |
|
62 |
53 void tests_pro_files(); |
63 void tests_pro_files(); |
54 void tests_pro_files_data(); |
64 void tests_pro_files_data(); |
|
65 |
|
66 void naming_convention(); |
|
67 void naming_convention_data(); |
|
68 |
|
69 private: |
|
70 QStringList find_subdirs(QString const&, FindSubdirsMode, QString const& = QString()); |
|
71 |
|
72 QSet<QString> all_test_classes; |
55 }; |
73 }; |
|
74 |
|
75 bool looks_like_testcase(QString const&,QString*); |
|
76 bool looks_like_subdirs(QString const&); |
|
77 QStringList find_test_class(QString const&); |
|
78 |
|
79 /* |
|
80 Verify that auto.pro only contains other .pro files (and not directories). |
|
81 We enforce this so that we can process every .pro file other than auto.pro |
|
82 independently and get all the tests. |
|
83 If tests were allowed to appear directly in auto.pro, we'd have the problem |
|
84 that we need to somehow run these tests from auto.pro while preventing |
|
85 recursion into the other .pro files. |
|
86 */ |
|
87 void tst_MakeTestSelfTest::tests_auto_pro() |
|
88 { |
|
89 QStringList subdirsList = find_subdirs(SRCDIR "/../auto.pro", Flat); |
|
90 if (QTest::currentTestFailed()) { |
|
91 return; |
|
92 } |
|
93 |
|
94 foreach (QString const& subdir, subdirsList) { |
|
95 QVERIFY2(subdir.endsWith(".pro"), qPrintable(QString( |
|
96 "auto.pro contains a subdir `%1'.\n" |
|
97 "auto.pro must _only_ contain other .pro files, not actual subdirs.\n" |
|
98 "Please move `%1' into some other .pro file referenced by auto.pro." |
|
99 ).arg(subdir))); |
|
100 } |
|
101 } |
56 |
102 |
57 /* Verify that all tests are listed somewhere in one of the autotest .pro files */ |
103 /* Verify that all tests are listed somewhere in one of the autotest .pro files */ |
58 void tst_MakeTestSelfTest::tests_pro_files() |
104 void tst_MakeTestSelfTest::tests_pro_files() |
59 { |
105 { |
60 static QStringList lines; |
106 static QStringList lines; |
104 } |
152 } |
105 QTest::newRow(qPrintable(subdir)) << subdir; |
153 QTest::newRow(qPrintable(subdir)) << subdir; |
106 } |
154 } |
107 } |
155 } |
108 |
156 |
|
157 QString format_list(QStringList const& list) |
|
158 { |
|
159 if (list.count() == 1) { |
|
160 return list.at(0); |
|
161 } |
|
162 return QString("one of (%1)").arg(list.join(", ")); |
|
163 } |
|
164 |
|
165 void tst_MakeTestSelfTest::naming_convention() |
|
166 { |
|
167 QFETCH(QString, subdir); |
|
168 QFETCH(QString, target); |
|
169 |
|
170 QDir dir(SRCDIR "/../" + subdir); |
|
171 |
|
172 QStringList cppfiles = dir.entryList(QStringList() << "*.h" << "*.cpp"); |
|
173 if (cppfiles.isEmpty()) { |
|
174 // Common convention is to have test/test.pro and source files in parent dir |
|
175 if (dir.dirName() == "test") { |
|
176 dir.cdUp(); |
|
177 cppfiles = dir.entryList(QStringList() << "*.h" << "*.cpp"); |
|
178 } |
|
179 |
|
180 if (cppfiles.isEmpty()) { |
|
181 QSKIP("Couldn't locate source files for test", SkipSingle); |
|
182 } |
|
183 } |
|
184 |
|
185 QStringList possible_test_classes; |
|
186 foreach (QString const& file, cppfiles) { |
|
187 possible_test_classes << find_test_class(dir.path() + "/" + file); |
|
188 } |
|
189 |
|
190 if (possible_test_classes.isEmpty()) { |
|
191 QSKIP(qPrintable(QString("Couldn't locate test class in %1").arg(format_list(cppfiles))), SkipSingle); |
|
192 } |
|
193 |
|
194 QVERIFY2(possible_test_classes.contains(target), qPrintable(QString( |
|
195 "TARGET is %1, while test class appears to be %2.\n" |
|
196 "TARGET and test class _must_ match so that all testcase names can be accurately " |
|
197 "determined even if a test fails to compile or run.") |
|
198 .arg(target) |
|
199 .arg(format_list(possible_test_classes)) |
|
200 )); |
|
201 |
|
202 QVERIFY2(!all_test_classes.contains(target), qPrintable(QString( |
|
203 "It looks like there are multiple tests named %1.\n" |
|
204 "This makes it impossible to separate results for these tests.\n" |
|
205 "Please ensure all tests are uniquely named.") |
|
206 .arg(target) |
|
207 )); |
|
208 |
|
209 all_test_classes << target; |
|
210 } |
|
211 |
|
212 void tst_MakeTestSelfTest::naming_convention_data() |
|
213 { |
|
214 QTest::addColumn<QString>("subdir"); |
|
215 QTest::addColumn<QString>("target"); |
|
216 |
|
217 foreach (const QString& subdir, find_subdirs(SRCDIR "/../auto.pro", Recursive)) { |
|
218 if (QFileInfo(SRCDIR "/../" + subdir).isDir()) { |
|
219 QString target; |
|
220 if (looks_like_testcase(SRCDIR "/../" + subdir + "/" + QFileInfo(subdir).baseName() + ".pro", &target)) { |
|
221 QTest::newRow(qPrintable(subdir)) << subdir << target.toLower(); |
|
222 } |
|
223 } |
|
224 } |
|
225 } |
|
226 |
|
227 /* |
|
228 Returns true if a .pro file seems to be for an autotest. |
|
229 Running qmake to figure this out takes too long. |
|
230 */ |
|
231 bool looks_like_testcase(QString const& pro_file, QString* target) |
|
232 { |
|
233 QFile file(pro_file); |
|
234 if (!file.open(QIODevice::ReadOnly)) { |
|
235 return false; |
|
236 } |
|
237 |
|
238 *target = QString(); |
|
239 |
|
240 bool loaded_qttest = false; |
|
241 |
|
242 do { |
|
243 QByteArray line = file.readLine(); |
|
244 if (line.isEmpty()) { |
|
245 break; |
|
246 } |
|
247 |
|
248 line = line.trimmed(); |
|
249 line.replace(' ', ""); |
|
250 |
|
251 if (line == "load(qttest_p4)") { |
|
252 loaded_qttest = true; |
|
253 } |
|
254 |
|
255 if (line.startsWith("TARGET=")) { |
|
256 *target = QString::fromLatin1(line.mid(sizeof("TARGET=")-1)); |
|
257 if (target->contains('/')) { |
|
258 *target = target->right(target->lastIndexOf('/')+1); |
|
259 } |
|
260 } |
|
261 |
|
262 if (loaded_qttest && !target->isEmpty()) { |
|
263 break; |
|
264 } |
|
265 } while(1); |
|
266 |
|
267 if (!loaded_qttest) { |
|
268 return false; |
|
269 } |
|
270 |
|
271 if (!target->isEmpty() && !target->startsWith("tst_")) { |
|
272 return false; |
|
273 } |
|
274 |
|
275 // If no target was set, default to tst_<dirname> |
|
276 if (target->isEmpty()) { |
|
277 *target = "tst_" + QFileInfo(pro_file).baseName(); |
|
278 } |
|
279 |
|
280 return true; |
|
281 } |
|
282 |
|
283 /* |
|
284 Returns true if a .pro file seems to be a subdirs project. |
|
285 Running qmake to figure this out takes too long. |
|
286 */ |
|
287 bool looks_like_subdirs(QString const& pro_file) |
|
288 { |
|
289 QFile file(pro_file); |
|
290 if (!file.open(QIODevice::ReadOnly)) { |
|
291 return false; |
|
292 } |
|
293 |
|
294 do { |
|
295 QByteArray line = file.readLine(); |
|
296 if (line.isEmpty()) { |
|
297 break; |
|
298 } |
|
299 |
|
300 line = line.trimmed(); |
|
301 line.replace(' ', ""); |
|
302 |
|
303 if (line == "TEMPLATE=subdirs") { |
|
304 return true; |
|
305 } |
|
306 } while(1); |
|
307 |
|
308 return false; |
|
309 } |
|
310 |
|
311 /* |
|
312 Returns a list of all subdirs in a given .pro file |
|
313 */ |
|
314 QStringList tst_MakeTestSelfTest::find_subdirs(QString const& pro_file, FindSubdirsMode mode, QString const& prefix) |
|
315 { |
|
316 QStringList out; |
|
317 |
|
318 QByteArray features = qgetenv("QMAKEFEATURES"); |
|
319 |
|
320 if (features.isEmpty()) { |
|
321 features = SRCDIR "/features"; |
|
322 } |
|
323 else { |
|
324 features.prepend(SRCDIR "/features" |
|
325 #ifdef Q_OS_WIN32 |
|
326 ";" |
|
327 #else |
|
328 ":" |
|
329 #endif |
|
330 ); |
|
331 } |
|
332 |
|
333 QStringList args; |
|
334 args << pro_file << "-o" << SRCDIR "/dummy_output" << "CONFIG+=dump_subdirs"; |
|
335 |
|
336 /* Turn on every option there is, to ensure we process every single directory */ |
|
337 args |
|
338 << "QT_CONFIG+=dbus" |
|
339 << "QT_CONFIG+=declarative" |
|
340 << "QT_CONFIG+=egl" |
|
341 << "QT_CONFIG+=multimedia" |
|
342 << "QT_CONFIG+=OdfWriter" |
|
343 << "QT_CONFIG+=opengl" |
|
344 << "QT_CONFIG+=openvg" |
|
345 << "QT_CONFIG+=phonon" |
|
346 << "QT_CONFIG+=private_tests" |
|
347 << "QT_CONFIG+=pulseaudio" |
|
348 << "QT_CONFIG+=qt3support" |
|
349 << "QT_CONFIG+=script" |
|
350 << "QT_CONFIG+=svg" |
|
351 << "QT_CONFIG+=webkit" |
|
352 << "QT_CONFIG+=xmlpatterns" |
|
353 << "CONFIG+=mac" |
|
354 << "CONFIG+=embedded" |
|
355 << "CONFIG+=symbian" |
|
356 ; |
|
357 |
|
358 |
|
359 |
|
360 QString cmd_with_args = QString("qmake %1").arg(args.join(" ")); |
|
361 |
|
362 QProcess proc; |
|
363 |
|
364 proc.setProcessChannelMode(QProcess::MergedChannels); |
|
365 |
|
366 QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); |
|
367 env.insert("QMAKEFEATURES", features); |
|
368 proc.setProcessEnvironment(env); |
|
369 |
|
370 proc.start("qmake", args); |
|
371 if (!proc.waitForStarted(10000)) { |
|
372 QTest::qFail(qPrintable(QString("Failed to run qmake: %1\nCommand: %2") |
|
373 .arg(proc.errorString()) |
|
374 .arg(cmd_with_args)), |
|
375 __FILE__, __LINE__ |
|
376 ); |
|
377 return out; |
|
378 } |
|
379 if (!proc.waitForFinished(30000)) { |
|
380 QTest::qFail(qPrintable(QString("qmake did not finish within 30 seconds\nCommand: %1\nOutput: %2") |
|
381 .arg(proc.errorString()) |
|
382 .arg(cmd_with_args) |
|
383 .arg(QString::fromLocal8Bit(proc.readAll()))), |
|
384 __FILE__, __LINE__ |
|
385 ); |
|
386 return out; |
|
387 } |
|
388 |
|
389 if (proc.exitStatus() != QProcess::NormalExit) { |
|
390 QTest::qFail(qPrintable(QString("qmake crashed\nCommand: %1\nOutput: %2") |
|
391 .arg(cmd_with_args) |
|
392 .arg(QString::fromLocal8Bit(proc.readAll()))), |
|
393 __FILE__, __LINE__ |
|
394 ); |
|
395 return out; |
|
396 } |
|
397 |
|
398 if (proc.exitCode() != 0) { |
|
399 QTest::qFail(qPrintable(QString("qmake exited with code %1\nCommand: %2\nOutput: %3") |
|
400 .arg(proc.exitCode()) |
|
401 .arg(cmd_with_args) |
|
402 .arg(QString::fromLocal8Bit(proc.readAll()))), |
|
403 __FILE__, __LINE__ |
|
404 ); |
|
405 return out; |
|
406 } |
|
407 |
|
408 QList<QByteArray> lines = proc.readAll().split('\n'); |
|
409 if (!lines.count()) { |
|
410 QTest::qFail(qPrintable(QString("qmake seems to have not output anything\nCommand: %1\n") |
|
411 .arg(cmd_with_args)), |
|
412 __FILE__, __LINE__ |
|
413 ); |
|
414 return out; |
|
415 } |
|
416 |
|
417 foreach (QByteArray const& line, lines) { |
|
418 static const QByteArray marker = "Project MESSAGE: subdir: "; |
|
419 if (line.startsWith(marker)) { |
|
420 QString subdir = QString::fromLocal8Bit(line.mid(marker.size()).trimmed()); |
|
421 out << prefix + subdir; |
|
422 |
|
423 if (mode == Flat) { |
|
424 continue; |
|
425 } |
|
426 |
|
427 // Need full path to subdir |
|
428 QString subdir_filepath = subdir; |
|
429 subdir_filepath.prepend(QFileInfo(pro_file).path() + "/"); |
|
430 |
|
431 // Add subdirs recursively |
|
432 if (subdir.endsWith(".pro") && looks_like_subdirs(subdir_filepath)) { |
|
433 // Need full path to .pro file |
|
434 out << find_subdirs(subdir_filepath, mode, prefix); |
|
435 } |
|
436 |
|
437 if (QFileInfo(subdir_filepath).isDir()) { |
|
438 subdir_filepath += "/" + subdir + ".pro"; |
|
439 if (looks_like_subdirs(subdir_filepath)) { |
|
440 out << find_subdirs(subdir_filepath, mode, prefix + subdir + "/"); |
|
441 } |
|
442 } |
|
443 } |
|
444 } |
|
445 |
|
446 return out; |
|
447 } |
|
448 |
|
449 QStringList find_test_class(QString const& filename) |
|
450 { |
|
451 QStringList out; |
|
452 |
|
453 QFile file(filename); |
|
454 if (!file.open(QIODevice::ReadOnly)) { |
|
455 return out; |
|
456 } |
|
457 |
|
458 static char const* klass_indicators[] = { |
|
459 "QTEST_MAIN(", |
|
460 "QTEST_APPLESS_MAIN(", |
|
461 "class", |
|
462 "staticconstcharklass[]=\"", /* hax0r tests which define their own metaobject */ |
|
463 0 |
|
464 }; |
|
465 |
|
466 do { |
|
467 QByteArray line = file.readLine(); |
|
468 if (line.isEmpty()) { |
|
469 break; |
|
470 } |
|
471 |
|
472 line = line.trimmed(); |
|
473 line.replace(' ', ""); |
|
474 |
|
475 for (int i = 0; klass_indicators[i]; ++i) { |
|
476 char const* prefix = klass_indicators[i]; |
|
477 if (!line.startsWith(prefix)) { |
|
478 continue; |
|
479 } |
|
480 QByteArray klass = line.mid(strlen(prefix)); |
|
481 if (!klass.startsWith("tst_")) { |
|
482 continue; |
|
483 } |
|
484 for (int j = 0; j < klass.size(); ++j) { |
|
485 char c = klass[j]; |
|
486 if (c == '_' |
|
487 || (c >= '0' && c <= '9') |
|
488 || (c >= 'A' && c <= 'Z') |
|
489 || (c >= 'a' && c <= 'z')) { |
|
490 continue; |
|
491 } |
|
492 else { |
|
493 klass.truncate(j); |
|
494 break; |
|
495 } |
|
496 } |
|
497 QString klass_str = QString::fromLocal8Bit(klass).toLower(); |
|
498 if (!out.contains(klass_str)) |
|
499 out << klass_str; |
|
500 break; |
|
501 } |
|
502 } while(1); |
|
503 |
|
504 return out; |
|
505 } |
|
506 |
109 QTEST_MAIN(tst_MakeTestSelfTest) |
507 QTEST_MAIN(tst_MakeTestSelfTest) |
110 #include "tst_maketestselftest.moc" |
508 #include "tst_maketestselftest.moc" |