PhpUnitTestCase::getClassDependendFiles()   C
last analyzed

Complexity

Conditions 7
Paths 9

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 9
nop 2
dl 0
loc 25
rs 6.7272
c 0
b 0
f 0
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()
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...
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()
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...
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;
0 ignored issues
show
Unused Code introduced by
$flag is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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();
0 ignored issues
show
Unused Code introduced by
$file is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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();
0 ignored issues
show
Unused Code introduced by
$file is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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)
0 ignored issues
show
Coding Style introduced by
prepareTemplate uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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