Completed
Push — new-committers ( 29cb6f...bcba16 )
by Sam
12:18 queued 33s
created

TestRunner::tearDown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * @package framework
4
 * @subpackage testing
5
 */
6
7
/**
8
 * Controller that executes PHPUnit tests.
9
 *
10
 * Alternatively, you can also use the "phpunit" binary directly by
11
 * pointing it to a file or folder containing unit tests.
12
 * See phpunit.dist.xml in the webroot for configuration details.
13
 *
14
 * <h2>URL Options</h2>
15
 * - SkipTests: A comma-separated list of test classes to skip (useful when running dev/tests/all)
16
 *
17
 * See {@link browse()} output for generic usage instructions.
18
 *
19
 * @package framework
20
 * @subpackage testing
21
 */
22
class TestRunner extends Controller {
23
24
	/** @ignore */
25
	private static $default_reporter;
26
27
	private static $url_handlers = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
28
		'' => 'browse',
29
		'coverage/module/$ModuleName' => 'coverageModule',
30
		'coverage/suite/$SuiteName!' => 'coverageSuite',
31
		'coverage/$TestCase!' => 'coverageOnly',
32
		'coverage' => 'coverageAll',
33
		'cleanupdb' => 'cleanupdb',
34
		'module/$ModuleName' => 'module',
35
		'suite/$SuiteName!' => 'suite',
36
		'all' => 'all',
37
		'build' => 'build',
38
		'$TestCase' => 'only'
39
	);
40
41
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
42
		'index',
43
		'browse',
44
		'coverage',
45
		'coverageAll',
46
		'coverageModule',
47
		'coverageSuite',
48
		'coverageOnly',
49
		'cleanupdb',
50
		'module',
51
		'suite',
52
		'all',
53
		'build',
54
		'only'
55
	);
56
57
	/**
58
	 * @var Array Blacklist certain directories for the coverage report.
59
	 * Filepaths are relative to the webroot, without leading slash.
60
	 *
61
	 * @see http://www.phpunit.de/manual/current/en/appendixes.configuration.html
62
	 *      #appendixes.configuration.blacklist-whitelist
63
	 */
64
	static $coverage_filter_dirs = array(
65
		'*/thirdparty',
66
		'*/tests',
67
		'*/lang',
68
	);
69
70
	/**
71
	 * Override the default reporter with a custom configured subclass.
72
	 *
73
	 * @param string $reporter
74
	 */
75
	public static function set_reporter($reporter) {
76
		if (is_string($reporter)) $reporter = new $reporter;
77
		self::$default_reporter = $reporter;
78
	}
79
80
	/**
81
	 * Pushes a class and template manifest instance that include tests onto the
82
	 * top of the loader stacks.
83
	 */
84
	public static function use_test_manifest() {
85
		$flush = false;
86
		if(isset($_GET['flush']) && ($_GET['flush'] === '1' || $_GET['flush'] == 'all')) {
87
			$flush = true;
88
		}
89
90
		$classManifest = new SS_ClassManifest(
91
			BASE_PATH, true, $flush
92
		);
93
94
		SS_ClassLoader::instance()->pushManifest($classManifest, false);
95
		SapphireTest::set_test_class_manifest($classManifest);
96
97
		SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
98
			BASE_PATH, project(), true, $flush
99
		));
100
101
		Config::inst()->pushConfigStaticManifest(new SS_ConfigStaticManifest(
102
			BASE_PATH, true, $flush
103
		));
104
105
		// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
106
		// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
107
		DataObject::reset();
108
	}
109
110
	public function init() {
111
		parent::init();
112
113
		$canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
114
		if(!$canAccess) return Security::permissionFailure($this);
115
116
		if (!self::$default_reporter) self::set_reporter(Director::is_cli() ? 'CliDebugView' : 'DebugView');
117
118
		if(!PhpUnitWrapper::has_php_unit()) {
119
			die("Please install PHPUnit using Composer");
120
		}
121
	}
122
123
	public function Link() {
124
		return Controller::join_links(Director::absoluteBaseURL(), 'dev/tests/');
125
	}
126
127
	/**
128
	 * Run test classes that should be run with every commit.
129
	 * Currently excludes PhpSyntaxTest
130
	 */
131 View Code Duplication
	public function all($request, $coverage = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
132
		self::use_test_manifest();
133
		$tests = ClassInfo::subclassesFor('SapphireTest');
134
		array_shift($tests);
135
		unset($tests['FunctionalTest']);
136
137
		// Remove tests that don't need to be executed every time
138
		unset($tests['PhpSyntaxTest']);
139
140
		foreach($tests as $class => $v) {
141
			$reflection = new ReflectionClass($class);
142
			if(!$reflection->isInstantiable()) unset($tests[$class]);
143
		}
144
145
		$this->runTests($tests, $coverage);
146
	}
147
148
	/**
149
	 * Run test classes that should be run before build - i.e., everything possible.
150
	 */
151 View Code Duplication
	public function build() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
152
		self::use_test_manifest();
153
		$tests = ClassInfo::subclassesFor('SapphireTest');
154
		array_shift($tests);
155
		unset($tests['FunctionalTest']);
156
		foreach($tests as $class => $v) {
157
			$reflection = new ReflectionClass($class);
158
			if(!$reflection->isInstantiable()) unset($tests[$class]);
159
		}
160
161
		$this->runTests($tests);
162
	}
163
164
	/**
165
	 * Browse all enabled test cases in the environment
166
	 */
167
	public function browse() {
168
		self::use_test_manifest();
169
		self::$default_reporter->writeHeader();
170
		self::$default_reporter->writeInfo('Available Tests', false);
171
		if(Director::is_cli()) {
172
			$tests = ClassInfo::subclassesFor('SapphireTest');
173
			$relativeLink = Director::makeRelative($this->Link());
174
			echo "sake {$relativeLink}all: Run all " . count($tests) . " tests\n";
175
			echo "sake {$relativeLink}coverage: Runs all tests and make test coverage report\n";
176
			echo "sake {$relativeLink}module/<modulename>: Runs all tests in a module folder\n";
177
			foreach ($tests as $test) {
0 ignored issues
show
Bug introduced by
The expression $tests of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
178
				echo "sake {$relativeLink}$test: Run $test\n";
179
			}
180
		} else {
181
			echo '<div class="trace">';
182
			$tests = ClassInfo::subclassesFor('SapphireTest');
183
			asort($tests);
184
			echo "<h3><a href=\"" . $this->Link() . "all\">Run all " . count($tests) . " tests</a></h3>";
185
			echo "<h3><a href=\"" . $this->Link() . "coverage\">Runs all tests and make test coverage report</a></h3>";
186
			echo "<hr />";
187
			foreach ($tests as $test) {
188
				echo "<h3><a href=\"" . $this->Link() . "$test\">Run $test</a></h3>";
189
			}
190
			echo '</div>';
191
		}
192
193
		self::$default_reporter->writeFooter();
194
	}
195
196
	/**
197
	 * Run a coverage test across all modules
198
	 */
199
	public function coverageAll($request) {
200
		self::use_test_manifest();
201
		$this->all($request, true);
202
	}
203
204
	/**
205
	 * Run only a single coverage test class or a comma-separated list of tests
206
	 */
207
	public function coverageOnly($request) {
208
		$this->only($request, true);
209
	}
210
211
	/**
212
	 * Run coverage tests for one or more "modules".
213
	 * A module is generally a toplevel folder, e.g. "mysite" or "framework".
214
	 */
215
	public function coverageModule($request) {
216
		$this->module($request, true);
217
	}
218
219
	public function cleanupdb() {
220
		SapphireTest::delete_all_temp_dbs();
221
	}
222
223
	/**
224
	 * Run only a single test class or a comma-separated list of tests
225
	 */
226
	public function only($request, $coverage = false) {
227
		self::use_test_manifest();
228
		if($request->param('TestCase') == 'all') {
229
			$this->all();
0 ignored issues
show
Bug introduced by
The call to all() misses a required argument $request.

This check looks for function calls that miss required arguments.

Loading history...
230
		} else {
231
			$classNames = explode(',', $request->param('TestCase'));
232
			foreach($classNames as $className) {
233
				if(!class_exists($className) || !is_subclass_of($className, 'SapphireTest')) {
234
					user_error("TestRunner::only(): Invalid TestCase '$className', cannot find matching class",
235
						E_USER_ERROR);
236
				}
237
			}
238
239
			$this->runTests($classNames, $coverage);
240
		}
241
	}
242
243
	/**
244
	 * Run tests for one or more "modules".
245
	 * A module is generally a toplevel folder, e.g. "mysite" or "framework".
246
	 */
247
	public function module($request, $coverage = false) {
248
		self::use_test_manifest();
249
		$classNames = array();
250
		$moduleNames = explode(',', $request->param('ModuleName'));
251
252
		$ignored = array('functionaltest', 'phpsyntaxtest');
253
254
		foreach($moduleNames as $moduleName) {
255
			$classNames = array_merge(
256
				$classNames,
257
				$this->getTestsInDirectory($moduleName, $ignored)
258
			);
259
		}
260
261
		$this->runTests($classNames, $coverage);
262
	}
263
264
	/**
265
	 * Find all test classes in a directory and return an array of them.
266
	 * @param string $directory To search in
267
	 * @param array $ignore Ignore these test classes if they are found.
268
	 * @return array
269
	 */
270
	protected function getTestsInDirectory($directory, $ignore = array()) {
271
		$classes = ClassInfo::classes_for_folder($directory);
272
		return $this->filterTestClasses($classes, $ignore);
273
	}
274
275
	/**
276
	 * Find all test classes in a file and return an array of them.
277
	 * @param string $file To search in
278
	 * @param array $ignore Ignore these test classes if they are found.
279
	 * @return array
280
	 */
281
	protected function getTestsInFile($file, $ignore = array()) {
282
		$classes = ClassInfo::classes_for_file($file);
283
		return $this->filterTestClasses($classes, $ignore);
284
	}
285
286
	/**
287
	 * @param array $classes to search in
288
	 * @param array $ignore Ignore these test classes if they are found.
289
	 */
290
	protected function filterTestClasses($classes, $ignore) {
291
		$testClasses = array();
292
		if($classes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $classes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
293
			foreach($classes as $className) {
294
				if(
295
					class_exists($className) &&
296
					is_subclass_of($className, 'SapphireTest') &&
297
					!in_array($className, $ignore)
298
				) {
299
					$testClasses[] = $className;
300
				}
301
			}
302
		}
303
		return $testClasses;
304
	}
305
306
	/**
307
	 * Run tests for a test suite defined in phpunit.xml
308
	 */
309
	public function suite($request, $coverage = false) {
310
		self::use_test_manifest();
311
		$suite = $request->param('SuiteName');
312
		$xmlFile = BASE_PATH.'/phpunit.xml';
313
		if(!is_readable($xmlFile)) {
314
			user_error("TestRunner::suite(): $xmlFile is not readable", E_USER_ERROR);
315
		}
316
		$xml = simplexml_load_file($xmlFile);
317
		$suite = $xml->xpath("//phpunit/testsuite[@name='$suite']");
318
		if(empty($suite)) {
319
			user_error("TestRunner::suite(): couldn't find the $suite testsuite in phpunit.xml");
320
		}
321
		$suite = array_shift($suite);
322
		$classNames = array();
323
		if(isset($suite->directory)) {
324
			foreach($suite->directory as $directory) {
325
				$classNames = array_merge($classNames, $this->getTestsInDirectory($directory));
326
			}
327
		}
328
		if(isset($suite->file)) {
329
			foreach($suite->file as $file) {
330
				$classNames = array_merge($classNames, $this->getTestsInFile($file));
331
			}
332
		}
333
334
		$this->runTests($classNames, $coverage);
335
	}
336
337
	/**
338
	 * Give us some sweet code coverage reports for a particular suite.
339
	 */
340
	public function coverageSuite($request) {
341
		return $this->suite($request, true);
342
	}
343
344
	/**
345
	 * @param array $classList
346
	 * @param boolean $coverage
347
	 */
348
	public function runTests($classList, $coverage = false) {
349
		$startTime = microtime(true);
350
351
		// disable xdebug, as it messes up test execution
352
		if(function_exists('xdebug_disable')) xdebug_disable();
353
354
		ini_set('max_execution_time', 0);
355
356
		$this->setUp();
357
358
		// Optionally skip certain tests
359
		$skipTests = array();
360
		if($this->getRequest()->getVar('SkipTests')) {
361
			$skipTests = explode(',', $this->getRequest()->getVar('SkipTests'));
362
		}
363
364
		$abstractClasses = array();
365
		foreach($classList as $className) {
366
			// Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
367
			class_exists($className);
368
			$reflection = new ReflectionClass($className);
369
			if ($reflection->isAbstract()) {
370
				array_push($abstractClasses, $className);
371
			}
372
		}
373
374
		$classList = array_diff($classList, $skipTests, $abstractClasses);
375
376
		// run tests before outputting anything to the client
377
		$suite = new PHPUnit_Framework_TestSuite();
378
		natcasesort($classList);
379
		foreach($classList as $className) {
380
			// Ensure that the autoloader pulls in the test class, as PHPUnit won't know how to do this.
381
			class_exists($className);
382
			$suite->addTest(new SapphireTestSuite($className));
383
		}
384
385
		// Remove the error handler so that PHPUnit can add its own
386
		restore_error_handler();
387
388
		self::$default_reporter->writeHeader("SilverStripe Test Runner");
389
		if (count($classList) > 1) {
390
			self::$default_reporter->writeInfo("All Tests", "Running test cases: ",implode(", ", $classList));
391
		} elseif (count($classList) == 1) {
392
			self::$default_reporter->writeInfo(reset($classList), '');
393
		} else {
394
			// border case: no tests are available.
395
			self::$default_reporter->writeInfo('', '');
396
		}
397
398
		// perform unit tests (use PhpUnitWrapper or derived versions)
399
		$phpunitwrapper = PhpUnitWrapper::inst();
400
		$phpunitwrapper->setSuite($suite);
401
		$phpunitwrapper->setCoverageStatus($coverage);
402
403
		// Make sure TearDown is called (even in the case of a fatal error)
404
		$self = $this;
405
		register_shutdown_function(function() use ($self) {
406
			$self->tearDown();
407
		});
408
409
		$phpunitwrapper->runTests();
410
411
		// get results of the PhpUnitWrapper class
412
		$reporter = $phpunitwrapper->getReporter();
413
		$results = $phpunitwrapper->getFrameworkTestResults();
414
415
		if(!Director::is_cli()) echo '<div class="trace">';
416
		$reporter->writeResults();
417
418
		$endTime = microtime(true);
419
		if(Director::is_cli()) echo "\n\nTotal time: " . round($endTime-$startTime,3) . " seconds\n";
420
		else echo "<p class=\"total-time\">Total time: " . round($endTime-$startTime,3) . " seconds</p>\n";
421
422
		if(!Director::is_cli()) echo '</div>';
423
424
		// Put the error handlers back
425
		Debug::loadErrorHandlers();
426
427
		if(!Director::is_cli()) self::$default_reporter->writeFooter();
428
429
		$this->tearDown();
430
431
		// Todo: we should figure out how to pass this data back through Director more cleanly
432
		if(Director::is_cli() && ($results->failureCount() + $results->errorCount()) > 0) exit(2);
433
	}
434
435
	public function setUp() {
436
		// The first DB test will sort out the DB, we don't have to
437
		SSViewer::flush_template_cache();
438
	}
439
440
	public function tearDown() {
441
		SapphireTest::kill_temp_db();
442
	}
443
}
444