1
|
|
|
<?php |
2
|
|
|
namespace Agavi\Testing; |
3
|
|
|
|
4
|
|
|
// +---------------------------------------------------------------------------+ |
5
|
|
|
// | This file is part of the Agavi package. | |
6
|
|
|
// | Copyright (c) 2005-2011 the Agavi Project. | |
7
|
|
|
// | | |
8
|
|
|
// | For the full copyright and license information, please view the LICENSE | |
9
|
|
|
// | file that was distributed with this source code. You can also view the | |
10
|
|
|
// | LICENSE file online at http://www.agavi.org/LICENSE.txt | |
11
|
|
|
// | vi: set noexpandtab: | |
12
|
|
|
// | Local Variables: | |
13
|
|
|
// | indent-tabs-mode: t | |
14
|
|
|
// | End: | |
15
|
|
|
// +---------------------------------------------------------------------------+ |
16
|
|
|
use Agavi\Config\Config; |
17
|
|
|
use Agavi\Util\Toolkit; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* PhpUnitTestCase is the base class for all Agavi Testcases. |
21
|
|
|
* |
22
|
|
|
* |
23
|
|
|
* @package agavi |
24
|
|
|
* @subpackage testing |
25
|
|
|
* |
26
|
|
|
* @author Felix Gilcher <[email protected]> |
27
|
|
|
* @copyright The Agavi Project |
28
|
|
|
* |
29
|
|
|
* @since 1.0.0 |
30
|
|
|
* |
31
|
|
|
* @version $Id$ |
32
|
|
|
*/ |
33
|
|
|
abstract class PhpUnitTestCase extends \PHPUnit\Framework\TestCase |
34
|
|
|
{ |
35
|
|
|
/** |
36
|
|
|
* @var string the name of the environment to bootstrap in isolated tests. |
37
|
|
|
*/ |
38
|
|
|
protected $isolationEnvironment; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var string the name of the default context to use in isolated tests. |
42
|
|
|
*/ |
43
|
|
|
protected $isolationDefaultContext; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var bool if the cache in the isolated process should be cleared |
47
|
|
|
*/ |
48
|
|
|
protected $clearIsolationCache = false; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var string store the dataName since we can't access it from PHPUnit_Framework_TestCase. |
52
|
|
|
*/ |
53
|
|
|
protected $myDataName; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Constructs a test case with the given name. |
57
|
|
|
* |
58
|
|
|
* @param string |
59
|
|
|
* @param array |
60
|
|
|
* @param string |
61
|
|
|
* |
62
|
|
|
* @since 1.1.0 |
63
|
|
|
*/ |
64
|
|
|
public function __construct($name = null, array $data = array(), $dataName = '') |
65
|
|
|
{ |
66
|
|
|
parent::__construct($name, $data, $dataName); |
67
|
|
|
$this->myDataName = $dataName; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* set the environment to bootstrap in isolated tests |
73
|
|
|
* |
74
|
|
|
* @param string $environmentName the name of the environment |
75
|
|
|
* |
76
|
|
|
* @author Felix Gilcher <[email protected]> |
77
|
|
|
* |
78
|
|
|
* @since 1.0.0 |
79
|
|
|
*/ |
80
|
|
|
public function setIsolationEnvironment($environmentName) |
81
|
|
|
{ |
82
|
|
|
$this->isolationEnvironment = $environmentName; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* get the environment to bootstrap in isolated tests |
88
|
|
|
* |
89
|
|
|
* @return string the name of the isolation environment |
90
|
|
|
* |
91
|
|
|
* @author Felix Gilcher <[email protected]> |
92
|
|
|
* |
93
|
|
|
* @since 1.0.2 |
94
|
|
|
*/ |
95
|
|
View Code Duplication |
public function getIsolationEnvironment() |
|
|
|
|
96
|
|
|
{ |
97
|
|
|
$environmentName = null; |
98
|
|
|
|
99
|
|
|
$annotations = $this->getAnnotations(); |
100
|
|
|
|
101
|
|
|
if (!empty($annotations['method']['agaviIsolationEnvironment'])) { |
102
|
|
|
$environmentName = $annotations['method']['agaviIsolationEnvironment'][0]; |
103
|
|
|
} elseif (!empty($annotations['class']['agaviIsolationEnvironment'])) { |
104
|
|
|
$environmentName = $annotations['class']['agaviIsolationEnvironment'][0]; |
105
|
|
|
} elseif (!empty($this->isolationEnvironment)) { |
106
|
|
|
$environmentName = $this->isolationEnvironment; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
return $environmentName; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* set the default context to use in isolated tests |
115
|
|
|
* |
116
|
|
|
* @param string $contextName the name of the context |
117
|
|
|
* |
118
|
|
|
* @author Felix Gilcher <[email protected]> |
119
|
|
|
* |
120
|
|
|
* @since 1.0.2 |
121
|
|
|
*/ |
122
|
|
|
public function setIsolationDefaultContext($contextName) |
123
|
|
|
{ |
124
|
|
|
$this->isolationDefaultContext = $contextName; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* get the default context to use in isolated tests |
130
|
|
|
* |
131
|
|
|
* @return string the default context to use in isolated tests |
132
|
|
|
* |
133
|
|
|
* @author Felix Gilcher <[email protected]> |
134
|
|
|
* |
135
|
|
|
* @since 1.0.2 |
136
|
|
|
*/ |
137
|
|
View Code Duplication |
public function getIsolationDefaultContext() |
|
|
|
|
138
|
|
|
{ |
139
|
|
|
$ctxName = null; |
140
|
|
|
|
141
|
|
|
$annotations = $this->getAnnotations(); |
142
|
|
|
|
143
|
|
|
if (!empty($annotations['method']['agaviIsolationDefaultContext'])) { |
144
|
|
|
$ctxName = $annotations['method']['agaviIsolationDefaultContext'][0]; |
145
|
|
|
} elseif (!empty($annotations['class']['agaviIsolationDefaultContext'])) { |
146
|
|
|
$ctxName = $annotations['class']['agaviIsolationDefaultContext'][0]; |
147
|
|
|
} elseif (!empty($this->isolationDefaultContext)) { |
148
|
|
|
$ctxName = $this->isolationDefaultContext; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
return $ctxName; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* set whether the cache should be cleared for the isolated subprocess |
157
|
|
|
* |
158
|
|
|
* @param bool true if the cache should be cleared |
159
|
|
|
* |
160
|
|
|
* @author Felix Gilcher <[email protected]> |
161
|
|
|
* |
162
|
|
|
* @since 1.0.2 |
163
|
|
|
*/ |
164
|
|
|
public function setClearCache($flag) |
165
|
|
|
{ |
166
|
|
|
$this->clearIsolationCache = (bool)$flag; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* check whether to clear the cache in isolated tests |
172
|
|
|
* |
173
|
|
|
* @return bool true if the cache is cleared in isolated tests |
174
|
|
|
* |
175
|
|
|
* @author Felix Gilcher <[email protected]> |
176
|
|
|
* |
177
|
|
|
* @since 1.0.2 |
178
|
|
|
*/ |
179
|
|
|
public function getClearCache() |
180
|
|
|
{ |
181
|
|
|
$flag = null; |
|
|
|
|
182
|
|
|
|
183
|
|
|
$annotations = $this->getAnnotations(); |
184
|
|
|
|
185
|
|
|
if (!empty($annotations['method']['agaviClearIsolationCache'])) { |
186
|
|
|
file_put_contents('/tmp/cclean.txt', 'SETTING CLEARCACHE ' . $this->getName() . "\r\n", FILE_APPEND); |
187
|
|
|
$flag = true; |
188
|
|
|
} elseif (!empty($annotations['class']['agaviClearIsolationCache'])) { |
189
|
|
|
$flag = true; |
190
|
|
|
} else { |
191
|
|
|
$flag = $this->clearIsolationCache; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
return $flag; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Retrieve the classes and defining files the given class depends on (including the given class) |
199
|
|
|
* |
200
|
|
|
* @param \ReflectionClass $reflectionClass The class to get the dependend classes for. |
201
|
|
|
* @param callable $isBlacklisted A callback function which takes a file name as argument |
202
|
|
|
* and returns whether the file is blacklisted. |
203
|
|
|
* |
204
|
|
|
* @return string[] An array containing class names as keys and path to the |
205
|
|
|
* file's defining class as value. |
206
|
|
|
* |
207
|
|
|
* @author Dominik del Bondio <[email protected]> |
208
|
|
|
* @since 1.1.0 |
209
|
|
|
*/ |
210
|
|
|
private function getClassDependendFiles(\ReflectionClass $reflectionClass, $isBlacklisted) |
211
|
|
|
{ |
212
|
|
|
$requires = array(); |
213
|
|
|
|
214
|
|
|
while ($reflectionClass) { |
215
|
|
|
$file = $reflectionClass->getFileName(); |
216
|
|
|
// we don't care for duplicates since we're using require_once anyways |
217
|
|
|
if (!$isBlacklisted($file) && is_file($file)) { |
218
|
|
|
$requires[$reflectionClass->getName()] = $file; |
219
|
|
|
} |
220
|
|
|
foreach ($reflectionClass->getInterfaces() as $interface) { |
221
|
|
|
$file = $interface->getFileName(); |
|
|
|
|
222
|
|
|
$requires = array_merge($requires, $this->getClassDependendFiles($interface, $isBlacklisted)); |
223
|
|
|
} |
224
|
|
|
if (is_callable(array($reflectionClass, 'getTraits'))) { |
225
|
|
|
// FIXME: remove check after bumping php requirement to 5.4 |
226
|
|
|
foreach ($reflectionClass->getTraits() as $trait) { |
227
|
|
|
$file = $trait->getFileName(); |
|
|
|
|
228
|
|
|
$requires = array_merge($requires, $this->getClassDependendFiles($trait, $isBlacklisted)); |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
$reflectionClass = $reflectionClass->getParentClass(); |
232
|
|
|
} |
233
|
|
|
return $requires; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* Get the dependend classes of this test. |
238
|
|
|
* |
239
|
|
|
* @return string[] An array containing class names as keys and path to the |
240
|
|
|
* file's defining class as value. |
241
|
|
|
* |
242
|
|
|
* @author Dominik del Bondio <[email protected]> |
243
|
|
|
* @since 1.1.0 |
244
|
|
|
*/ |
245
|
|
|
private function getDependendClasses() |
246
|
|
|
{ |
247
|
|
|
// We need to collect the dependend classes in case there is a test which |
248
|
|
|
// has set @agaviBootstrap to off. That results in the Agavi autoloader not |
249
|
|
|
// being started and if the test class depends on any files from Agavi (like |
250
|
|
|
// PhpUnitTestCase) it would not be loaded when the test is instantiated |
251
|
|
|
|
252
|
|
|
$classesInTest = array(); |
253
|
|
|
$reflectionClass = new \ReflectionClass(get_class($this)); |
254
|
|
|
$testFile = $reflectionClass->getFileName(); |
255
|
|
|
|
256
|
|
|
$getDeclaredFuncs = array('get_declared_classes', 'get_declared_interfaces'); |
257
|
|
|
if (version_compare(PHP_VERSION, '5.4', '>=')) { |
258
|
|
|
$getDeclaredFuncs[] = 'get_declared_traits'; |
259
|
|
|
} |
260
|
|
|
foreach ($getDeclaredFuncs as $getDeclaredFunc) { |
261
|
|
|
foreach ($getDeclaredFunc() as $name) { |
262
|
|
|
$reflectionClass = new \ReflectionClass($name); |
263
|
|
|
if ($testFile === $reflectionClass->getFileName()) { |
264
|
|
|
$classesInTest[] = $name; |
265
|
|
|
} |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
// FIXME: added by phpunit 4.x |
270
|
|
|
if (class_exists('\PHPUnit_Util_Blacklist')) { |
271
|
|
|
$blacklist = new \PHPUnit\Util\Blacklist; |
272
|
|
|
$isBlacklisted = function ($file) use ($testFile, $blacklist) { |
273
|
|
|
return $file === $testFile || $blacklist->isBlacklisted($file); |
274
|
|
|
}; |
275
|
|
|
} elseif (is_callable(array('\PHPUnit_Util_GlobalState', 'phpunitFiles'))) { |
276
|
|
|
$blacklist = \PHPUnit_Util_GlobalState::phpunitFiles(); |
277
|
|
|
$isBlacklisted = function ($file) use ($testFile, $blacklist) { |
278
|
|
|
return $file === $testFile || isset($blacklist[$file]); |
279
|
|
|
}; |
280
|
|
|
} else { |
281
|
|
|
$isBlacklisted = function ($file) use ($testFile) { |
282
|
|
|
return $file === $testFile; |
283
|
|
|
}; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
$classesToFile = array('Testing' => realpath(__DIR__ . '/Testing.class.php')); |
287
|
|
|
foreach ($classesInTest as $className) { |
288
|
|
|
$classesToFile = array_merge( |
289
|
|
|
$classesToFile, |
290
|
|
|
$this->getClassDependendFiles(new \ReflectionClass($className), $isBlacklisted) |
291
|
|
|
); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
return $classesToFile; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Performs custom preparations on the process isolation template. |
299
|
|
|
* |
300
|
|
|
* @param \Text_Template $template |
301
|
|
|
* |
302
|
|
|
* @author Felix Gilcher <[email protected]> |
303
|
|
|
* @since 1.0.2 |
304
|
|
|
*/ |
305
|
|
|
protected function prepareTemplate(\Text_Template $template) |
|
|
|
|
306
|
|
|
{ |
307
|
|
|
parent::prepareTemplate($template); |
308
|
|
|
|
309
|
|
|
// FIXME: workaround for php unit bug (https://github.com/sebastianbergmann/phpunit/pull/1338) |
310
|
|
|
$template->setVar(array( |
311
|
|
|
'dataName' => "'.(" . var_export($this->myDataName, true) . ").'" |
312
|
|
|
)); |
313
|
|
|
|
314
|
|
|
// FIXME: if we have full composer autoloading we can remove this |
315
|
|
|
// we need to restore the included files even without global state, since otherwise |
316
|
|
|
// the agavi test class files would be missing. |
317
|
|
|
// We can't write include()s directly since Agavi possibly get's bootstrapped later |
318
|
|
|
// in the process (but before the test instance is created) and if we'd load any |
319
|
|
|
// files which are being loaded by the bootstrap process chaos would ensue since |
320
|
|
|
// the bootstrap process uses plain include()s without _once |
321
|
|
|
$fileAutoloader = sprintf(' |
322
|
|
|
spl_autoload_register(function($name) { |
323
|
|
|
$classMap = %s; |
324
|
|
|
if(isset($classMap[$name])) { |
325
|
|
|
include($classMap[$name]); |
326
|
|
|
} |
327
|
|
|
}); |
328
|
|
|
', var_export($this->getDependendClasses(), true)); |
329
|
|
|
|
330
|
|
|
// these constants are either used by out bootstrap wrapper script |
331
|
|
|
// (AGAVI_TESTING_ORIGINAL_PHPUNIT_BOOTSTRAP) or can be used by the user's |
332
|
|
|
// bootstrap script (AGAVI_TESTING_IN_SEPARATE_PROCESS) |
333
|
|
|
$constants = sprintf(' |
334
|
|
|
define("AGAVI_TESTING_IN_SEPARATE_PROCESS", true); |
335
|
|
|
define("AGAVI_TESTING_ORIGINAL_PHPUNIT_BOOTSTRAP", %s); |
336
|
|
|
', |
337
|
|
|
var_export(isset($GLOBALS["__PHPUNIT_BOOTSTRAP"]) ? $GLOBALS["__PHPUNIT_BOOTSTRAP"] : null, true) |
338
|
|
|
); |
339
|
|
|
|
340
|
|
|
|
341
|
|
|
$isolatedTestSettings = array( |
342
|
|
|
'environment' => $this->getIsolationEnvironment(), |
343
|
|
|
'defaultContext' => $this->getIsolationDefaultContext(), |
344
|
|
|
'clearCache' => $this->getClearCache(), |
345
|
|
|
'bootstrap' => $this->doBootstrap(), |
346
|
|
|
); |
347
|
|
|
$globals = sprintf(' |
348
|
|
|
$GLOBALS["AGAVI_TESTING_CONFIG"] = %s; |
349
|
|
|
$GLOBALS["AGAVI_TESTING_ISOLATED_TEST_SETTINGS"] = %s; |
350
|
|
|
$GLOBALS["__PHPUNIT_BOOTSTRAP"] = %s; |
351
|
|
|
', |
352
|
|
|
var_export(Config::toArray(), true), |
353
|
|
|
var_export($isolatedTestSettings, true), |
354
|
|
|
var_export(__DIR__ . '/scripts/IsolatedBootstrap.php', true) |
355
|
|
|
); |
356
|
|
|
|
357
|
|
|
if (!$this->preserveGlobalState) { |
358
|
|
|
$template->setVar(array( |
359
|
|
|
'included_files' => $fileAutoloader, |
360
|
|
|
'constants' => $constants, |
361
|
|
|
'globals' => $globals, |
362
|
|
|
)); |
363
|
|
|
} else { |
364
|
|
|
// HACK: oh great, text/template doesn't expose the already set variables, but we need to modify |
365
|
|
|
// them instead of overwriting them. So let's use the reflection to the rescue here. |
366
|
|
|
$reflected = new \ReflectionObject($template); |
367
|
|
|
$property = $reflected->getProperty('values'); |
368
|
|
|
$property->setAccessible(true); |
369
|
|
|
$oldVars = $property->getValue($template); |
370
|
|
|
$template->setVar(array( |
371
|
|
|
'included_files' => $fileAutoloader, |
372
|
|
|
'constants' => $oldVars['constants'] . PHP_EOL . $constants, |
373
|
|
|
'globals' => $oldVars['globals'] . PHP_EOL . $globals, |
374
|
|
|
)); |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Whether or not an agavi bootstrap should be done in isolation. |
380
|
|
|
* |
381
|
|
|
* @return boolean true if agavi should be bootstrapped |
382
|
|
|
* |
383
|
|
|
* @author Felix Gilcher <[email protected]> |
384
|
|
|
* |
385
|
|
|
* @since 1.0.2 |
386
|
|
|
*/ |
387
|
|
|
protected function doBootstrap() |
388
|
|
|
{ |
389
|
|
|
$flag = true; |
390
|
|
|
|
391
|
|
|
$annotations = $this->getAnnotations(); |
392
|
|
|
if (!empty($annotations['method']['agaviBootstrap'])) { |
393
|
|
|
$flag = Toolkit::literalize($annotations['method']['agaviBootstrap'][0]); |
394
|
|
|
} elseif (!empty($annotations['class']['agaviBootstrap'])) { |
395
|
|
|
$flag = Toolkit::literalize($annotations['class']['agaviBootstrap'][0]); |
396
|
|
|
} |
397
|
|
|
return $flag; |
398
|
|
|
} |
399
|
|
|
} |
400
|
|
|
|
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.