Completed
Push — master ( d8776d...afe412 )
by Robbie
46s queued 25s
created

SapphireTest::assertArraySubset()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 23
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 3
nop 4
dl 0
loc 23
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use ArrayAccess;
6
use Exception;
7
use LogicException;
8
use PHPUnit\Framework\Constraint\ArraySubset;
9
use PHPUnit\Framework\Constraint\LogicalNot;
10
use PHPUnit\Framework\ExpectationFailedException;
11
use PHPUnit\Framework\InvalidArgumentException;
12
use PHPUnit\Framework\TestCase;
13
use PHPUnit\Util\InvalidArgumentHelper;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Util\InvalidArgumentHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use SilverStripe\CMS\Controllers\RootURLController;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Controllers\RootURLController was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use SilverStripe\Control\CLIRequestBuilder;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\Cookie;
18
use SilverStripe\Control\Director;
19
use SilverStripe\Control\Email\Email;
20
use SilverStripe\Control\Email\Mailer;
21
use SilverStripe\Control\HTTPApplication;
22
use SilverStripe\Control\HTTPRequest;
23
use SilverStripe\Core\Config\Config;
24
use SilverStripe\Core\Injector\Injector;
25
use SilverStripe\Core\Injector\InjectorLoader;
26
use SilverStripe\Core\Manifest\ClassLoader;
27
use SilverStripe\Core\Manifest\ModuleResourceLoader;
28
use SilverStripe\Dev\Constraint\SSListContains;
29
use SilverStripe\Dev\Constraint\SSListContainsOnly;
30
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems;
31
use SilverStripe\Dev\State\FixtureTestState;
32
use SilverStripe\Dev\State\SapphireTestState;
33
use SilverStripe\i18n\i18n;
34
use SilverStripe\ORM\Connect\TempDatabase;
35
use SilverStripe\ORM\DataObject;
36
use SilverStripe\ORM\FieldType\DBDatetime;
37
use SilverStripe\ORM\FieldType\DBField;
38
use SilverStripe\ORM\SS_List;
39
use SilverStripe\Security\Group;
40
use SilverStripe\Security\IdentityStore;
41
use SilverStripe\Security\Member;
42
use SilverStripe\Security\Permission;
43
use SilverStripe\Security\Security;
44
use SilverStripe\View\SSViewer;
45
46
if (!class_exists(TestCase::class)) {
47
    return;
48
}
49
50
/**
51
 * Test case class for the Sapphire framework.
52
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
53
 * to work with.
54
 *
55
 * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
56
 * in production sites.
57
 */
58
abstract class SapphireTest extends TestCase implements TestOnly
59
{
60
    /**
61
     * Path to fixture data for this test run.
62
     * If passed as an array, multiple fixture files will be loaded.
63
     * Please note that you won't be able to refer with "=>" notation
64
     * between the fixtures, they act independent of each other.
65
     *
66
     * @var string|array
67
     */
68
    protected static $fixture_file = '';
69
70
    /**
71
     * @deprecated 4.0..5.0 Use FixtureTestState instead
72
     * @var FixtureFactory
73
     */
74
    protected $fixtureFactory;
75
76
    /**
77
     * @var Boolean If set to TRUE, this will force a test database to be generated
78
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
79
     * {@link $fixture_file}, which always forces a database build.
80
     *
81
     * @var bool
82
     */
83
    protected $usesDatabase = null;
84
85
    /**
86
     * This test will cleanup its state via transactions.
87
     * If set to false a full schema is forced between tests, but at a performance cost.
88
     *
89
     * @var bool
90
     */
91
    protected $usesTransactions = true;
92
93
    /**
94
     * @var bool
95
     */
96
    protected static $is_running_test = false;
97
98
    /**
99
     * By default, setUp() does not require default records. Pass
100
     * class names in here, and the require/augment default records
101
     * function will be called on them.
102
     *
103
     * @var array
104
     */
105
    protected $requireDefaultRecordsFrom = array();
106
107
    /**
108
     * A list of extensions that can't be applied during the execution of this run.  If they are
109
     * applied, they will be temporarily removed and a database migration called.
110
     *
111
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
112
     * the values are an array of illegal extensions on that class.
113
     *
114
     * Set a class to `*` to remove all extensions (unadvised)
115
     *
116
     * @var array
117
     */
118
    protected static $illegal_extensions = [];
119
120
    /**
121
     * A list of extensions that must be applied during the execution of this run.  If they are
122
     * not applied, they will be temporarily added and a database migration called.
123
     *
124
     * The keys of the are the classes to apply the extensions to, and the values are an array
125
     * of required extensions on that class.
126
     *
127
     * Example:
128
     * <code>
129
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
130
     * </code>
131
     *
132
     * @var array
133
     */
134
    protected static $required_extensions = [];
135
136
    /**
137
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
138
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
139
     * Set it to an array of DataObject subclass names.
140
     *
141
     * @var array
142
     */
143
    protected static $extra_dataobjects = [];
144
145
    /**
146
     * List of class names of {@see Controller} objects to register routes for
147
     * Controllers must implement Link() method
148
     *
149
     * @var array
150
     */
151
    protected static $extra_controllers = [];
152
153
    /**
154
     * We need to disabling backing up of globals to avoid overriding
155
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
156
     *
157
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
158
     */
159
    protected $backupGlobals = false;
160
161
    /**
162
     * State management container for SapphireTest
163
     *
164
     * @var SapphireTestState
165
     */
166
    protected static $state = null;
167
168
    /**
169
     * Temp database helper
170
     *
171
     * @var TempDatabase
172
     */
173
    protected static $tempDB = null;
174
175
    /**
176
     * @return TempDatabase
177
     */
178
    public static function tempDB()
179
    {
180
        if (!static::$tempDB) {
181
            static::$tempDB = TempDatabase::create();
182
        }
183
        return static::$tempDB;
184
    }
185
186
    /**
187
     * Gets illegal extensions for this class
188
     *
189
     * @return array
190
     */
191
    public static function getIllegalExtensions() : array
192
    {
193
        return static::$illegal_extensions;
194
    }
195
196
    /**
197
     * Gets required extensions for this class
198
     *
199
     * @return array
200
     */
201
    public static function getRequiredExtensions() : array
202
    {
203
        return static::$required_extensions;
204
    }
205
206
    /**
207
     * Check if test bootstrapping has been performed. Must not be relied on
208
     * outside of unit tests.
209
     *
210
     * @return bool
211
     */
212
    protected static function is_running_test() : bool
213
    {
214
        return self::$is_running_test;
215
    }
216
217
    /**
218
     * Set test running state
219
     *
220
     * @param bool $bool
221
     */
222
    protected static function set_is_running_test(bool $bool) : void
223
    {
224
        self::$is_running_test = $bool;
225
    }
226
227
    /**
228
     * @return string|string[]
229
     */
230
    public static function get_fixture_file()
231
    {
232
        return static::$fixture_file;
233
    }
234
235
    /**
236
     * @return bool
237
     */
238
    public function getUsesDatabase()
239
    {
240
        return $this->usesDatabase;
241
    }
242
243
    /**
244
     * @return bool
245
     */
246
    public function getUsesTransactions()
247
    {
248
        return $this->usesTransactions;
249
    }
250
251
    /**
252
     * @return array
253
     */
254
    public function getRequireDefaultRecordsFrom()
255
    {
256
        return $this->requireDefaultRecordsFrom;
257
    }
258
259
    /**
260
     * Setup  the test.
261
     * Always sets up in order:
262
     *  - Reset php state
263
     *  - Nest
264
     *  - Custom state helpers
265
     *
266
     * User code should call parent::setUp() before custom setup code
267
     */
268
    protected function setUp() : void
269
    {
270
        if (!defined('FRAMEWORK_PATH')) {
271
            trigger_error(
272
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
273
                E_USER_WARNING
274
            );
275
        }
276
277
        // Call state helpers
278
        static::$state->setUp($this);
279
280
        // i18n needs to be set to the defaults or tests fail
281
        i18n::set_locale(i18n::config()->uninherited('default_locale'));
282
283
        // Set default timezone consistently to avoid NZ-specific dependencies
284
        date_default_timezone_set('UTC');
285
286
        Member::set_password_validator(null);
287
        Cookie::config()->update('report_errors', false);
288
        if (class_exists(RootURLController::class)) {
289
            RootURLController::reset();
290
        }
291
292
        Security::clear_database_is_ready();
293
294
        // Set up test routes
295
        $this->setUpRoutes();
296
297
        $fixtureFiles = $this->getFixturePaths();
298
299
        if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
300
            // Assign fixture factory to deprecated prop in case old tests use it over the getter
301
            /** @var FixtureTestState $fixtureState */
302
            $fixtureState = static::$state->getStateByName('fixtures');
303
            $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Dev\SapphireTest::$fixtureFactory has been deprecated: 4.0..5.0 Use FixtureTestState instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

303
            /** @scrutinizer ignore-deprecated */ $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
Documentation Bug introduced by
It seems like $fixtureState->getFixtureFactory(static::class) can also be of type false. However, the property $fixtureFactory is declared as type SilverStripe\Dev\FixtureFactory. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
304
305
            $this->logInWithPermission('ADMIN');
306
        }
307
308
        // turn off template debugging
309
        SSViewer::config()->update('source_file_comments', false);
310
311
        // Set up the test mailer
312
        Injector::inst()->registerService(new TestMailer(), Mailer::class);
313
        Email::config()->remove('send_all_emails_to');
314
        Email::config()->remove('send_all_emails_from');
315
        Email::config()->remove('cc_all_emails_to');
316
        Email::config()->remove('bcc_all_emails_to');
317
    }
318
319
320
321
    /**
322
     * Helper method to determine if the current test should enable a test database
323
     *
324
     * @param array|string $fixtureFiles
325
     * @return bool
326
     */
327
    protected function shouldSetupDatabaseForCurrentTest($fixtureFiles) : bool
328
    {
329
        $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
330
331
        return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
332
            || $this->currentTestEnablesDatabase();
333
    }
334
335
    /**
336
     * Helper method to check, if the current test uses the database.
337
     * This can be switched on with the annotation "@useDatabase"
338
     *
339
     * @return bool
340
     */
341
    protected function currentTestEnablesDatabase() : bool
342
    {
343
        $annotations = $this->getAnnotations();
344
345
        return array_key_exists('useDatabase', $annotations['method'])
346
            && $annotations['method']['useDatabase'][0] !== 'false';
347
    }
348
349
    /**
350
     * Helper method to check, if the current test uses the database.
351
     * This can be switched on with the annotation "@useDatabase false"
352
     *
353
     * @return bool
354
     */
355
    protected function currentTestDisablesDatabase() : bool
356
    {
357
        $annotations = $this->getAnnotations();
358
359
        return array_key_exists('useDatabase', $annotations['method'])
360
            && $annotations['method']['useDatabase'][0] === 'false';
361
    }
362
363
    /**
364
     * Called once per test case ({@link SapphireTest} subclass).
365
     * This is different to {@link setUp()}, which gets called once
366
     * per method. Useful to initialize expensive operations which
367
     * don't change state for any called method inside the test,
368
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
369
     * for tearing down the state again.
370
     *
371
     * Always sets up in order:
372
     *  - Reset php state
373
     *  - Nest
374
     *  - Custom state helpers
375
     *
376
     * User code should call parent::setUpBeforeClass() before custom setup code
377
     *
378
     * @throws Exception
379
     */
380
    public static function setUpBeforeClass() : void
381
    {
382
        // Start tests
383
        static::start();
384
385
        if (!static::$state) {
386
            throw new Exception('SapphireTest failed to bootstrap!');
387
        }
388
389
        // Call state helpers
390
        static::$state->setUpOnce(static::class);
391
392
        // Build DB if we have objects
393
        if (static::getExtraDataObjects()) {
394
            DataObject::reset();
395
            static::resetDBSchema(true, true);
396
        }
397
    }
398
399
    /**
400
     * tearDown method that's called once per test class rather once per test method.
401
     *
402
     * Always sets up in order:
403
     *  - Custom state helpers
404
     *  - Unnest
405
     *  - Reset php state
406
     *
407
     * User code should call parent::tearDownAfterClass() after custom tear down code
408
     */
409
    public static function tearDownAfterClass() : void
410
    {
411
        // Call state helpers
412
        static::$state->tearDownOnce(static::class);
413
414
        // Reset DB schema
415
        static::resetDBSchema();
416
    }
417
418
    /**
419
     * @deprecated 4.0.0:5.0.0
420
     * @return FixtureFactory|false
421
     */
422
    public function getFixtureFactory() : FixtureFactory
423
    {
424
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
425
        /** @var FixtureTestState $state */
426
        $state = static::$state->getStateByName('fixtures');
427
        return $state->getFixtureFactory(static::class);
428
    }
429
430
    /**
431
     * Sets a new fixture factory
432
     * @deprecated 4.0.0:5.0.0
433
     * @param FixtureFactory $factory
434
     * @return $this
435
     */
436
    public function setFixtureFactory(FixtureFactory $factory) : SapphireTest
437
    {
438
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
439
        /** @var FixtureTestState $state */
440
        $state = static::$state->getStateByName('fixtures');
441
        $state->setFixtureFactory($factory, static::class);
442
        $this->fixtureFactory = $factory;
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Dev\SapphireTest::$fixtureFactory has been deprecated: 4.0..5.0 Use FixtureTestState instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

442
        /** @scrutinizer ignore-deprecated */ $this->fixtureFactory = $factory;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
443
        return $this;
444
    }
445
446
    /**
447
     * Get the ID of an object from the fixture.
448
     *
449
     * @param string $className The data class or table name, as specified in your fixture file.
450
     *                          Parent classes won't work
451
     * @param string $identifier The identifier string, as provided in your fixture file
452
     * @return int
453
     */
454
    protected function idFromFixture(string $className, string $identifier) : int
455
    {
456
        /** @var FixtureTestState $state */
457
        $state = static::$state->getStateByName('fixtures');
458
        $id = $state->getFixtureFactory(static::class)->getId($className, $identifier);
459
460
        if (!$id) {
461
            user_error(sprintf(
462
                "Couldn't find object '%s' (class: %s)",
463
                $identifier,
464
                $className
465
            ), E_USER_ERROR);
466
        }
467
468
        return $id;
469
    }
470
471
    /**
472
     * Return all of the IDs in the fixture of a particular class name.
473
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
474
     *
475
     * @param string $className The data class or table name, as specified in your fixture file
476
     * @return array A map of fixture-identifier => object-id
477
     */
478
    protected function allFixtureIDs(string $className) : array
479
    {
480
        /** @var FixtureTestState $state */
481
        $state = static::$state->getStateByName('fixtures');
482
        return $state->getFixtureFactory(static::class)->getIds($className);
483
    }
484
485
    /**
486
     * Get an object from the fixture.
487
     *
488
     * @param string $className The data class or table name, as specified in your fixture file.
489
     *                          Parent classes won't work
490
     * @param string $identifier The identifier string, as provided in your fixture file
491
     *
492
     * @return DataObject
493
     */
494
    protected function objFromFixture(string $className, string $identifier) : DataObject
495
    {
496
        /** @var FixtureTestState $state */
497
        $state = static::$state->getStateByName('fixtures');
498
        $obj = $state->getFixtureFactory(static::class)->get($className, $identifier);
499
500
        if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
501
            user_error(sprintf(
502
                "Couldn't find object '%s' (class: %s)",
503
                $identifier,
504
                $className
505
            ), E_USER_ERROR);
506
        }
507
508
        return $obj;
509
    }
510
511
    /**
512
     * Load a YAML fixture file into the database.
513
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
514
     * Doesn't clear existing fixtures.
515
     * @deprecated 4.0.0:5.0.0
516
     *
517
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
518
     */
519
    public function loadFixture(string $fixtureFile) : void
520
    {
521
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
522
        $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
523
        $fixture->writeInto($this->getFixtureFactory());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0.0:5.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

523
        $fixture->writeInto(/** @scrutinizer ignore-deprecated */ $this->getFixtureFactory());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
524
    }
525
526
    /**
527
     * Clear all fixtures which were previously loaded through
528
     * {@link loadFixture()}
529
     */
530
    public function clearFixtures() : void
531
    {
532
        /** @var FixtureTestState $state */
533
        $state = static::$state->getStateByName('fixtures');
534
        $state->getFixtureFactory(static::class)->clear();
535
    }
536
537
    /**
538
     * Useful for writing unit tests without hardcoding folder structures.
539
     *
540
     * @return string Absolute path to current class.
541
     */
542
    protected function getCurrentAbsolutePath() : string
543
    {
544
        $filename = ClassLoader::inst()->getItemPath(static::class);
545
        if (!$filename) {
546
            throw new LogicException('getItemPath returned null for ' . static::class
547
                . '. Try adding flush=1 to the test run.');
548
        }
549
        return dirname($filename);
550
    }
551
552
    /**
553
     * @return string File path relative to webroot
554
     */
555
    protected function getCurrentRelativePath() : string
556
    {
557
        $base = Director::baseFolder();
558
        $path = $this->getCurrentAbsolutePath();
559
        if (substr($path, 0, strlen($base)) == $base) {
560
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
561
        }
562
        return $path;
563
    }
564
565
    /**
566
     * Setup  the test.
567
     * Always sets up in order:
568
     *  - Custom state helpers
569
     *  - Unnest
570
     *  - Reset php state
571
     *
572
     * User code should call parent::tearDown() after custom tear down code
573
     */
574
    protected function tearDown() : void
575
    {
576
        // Reset mocked datetime
577
        DBDatetime::clear_mock_now();
578
579
        // Stop the redirection that might have been requested in the test.
580
        // Note: Ideally a clean Controller should be created for each test.
581
        // Now all tests executed in a batch share the same controller.
582
        $controller = Controller::has_curr() ? Controller::curr() : null;
583
        if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
584
            $response->setStatusCode(200);
585
            $response->removeHeader('Location');
586
        }
587
588
        // Call state helpers
589
        static::$state->tearDown($this);
590
    }
591
592
    /**
593
     * Asserts that an array has a specified subset.
594
     *
595
     * @param array|ArrayAccess $subset
596
     * @param array|ArrayAccess $array
597
     *
598
     * @throws ExpectationFailedException
599
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
600
     * @throws \PHPUnit\Framework\Exception
601
     *
602
     * @codeCoverageIgnore
603
     */
604
    public static function assertArraySubset(
605
        $subset,
606
        $array,
607
        bool $checkForObjectIdentity = false,
608
        string $message = ''
609
    ): void {
610
        if (!(\is_array($subset) || $subset instanceof ArrayAccess)) {
0 ignored issues
show
introduced by
$subset is always a sub-type of ArrayAccess.
Loading history...
611
            throw InvalidArgumentException::create(
612
                1,
613
                'array or ArrayAccess'
614
            );
615
        }
616
617
        if (!(\is_array($array) || $array instanceof ArrayAccess)) {
0 ignored issues
show
introduced by
$array is always a sub-type of ArrayAccess.
Loading history...
618
            throw InvalidArgumentException::create(
619
                2,
620
                'array or ArrayAccess'
621
            );
622
        }
623
624
        $constraint = new ArraySubset($subset, $checkForObjectIdentity);
0 ignored issues
show
Bug introduced by
It seems like $subset can also be of type ArrayAccess; however, parameter $subset of PHPUnit\Framework\Constr...aySubset::__construct() does only seem to accept iterable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

624
        $constraint = new ArraySubset(/** @scrutinizer ignore-type */ $subset, $checkForObjectIdentity);
Loading history...
Deprecated Code introduced by
The class PHPUnit\Framework\Constraint\ArraySubset has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/3494 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

624
        $constraint = /** @scrutinizer ignore-deprecated */ new ArraySubset($subset, $checkForObjectIdentity);
Loading history...
625
626
        static::assertThat($array, $constraint, $message);
627
    }
628
629
    public static function assertContains(
630
        $needle,
631
        $haystack,
632
        string $message = '',
633
        bool $ignoreCase = false,
634
        bool $checkForObjectIdentity = true,
635
        bool $checkForNonObjectIdentity = false
636
    ) : void {
637
        if ($haystack instanceof DBField) {
638
            $haystack = (string)$haystack;
639
        }
640
        parent::assertContains(
641
            $needle,
642
            $haystack,
643
            $message,
644
            $ignoreCase,
645
            $checkForObjectIdentity,
646
            $checkForNonObjectIdentity
647
        );
648
    }
649
650
    public static function assertNotContains(
651
        $needle,
652
        $haystack,
653
        string $message = '',
654
        bool $ignoreCase = false,
655
        bool $checkForObjectIdentity = true,
656
        bool $checkForNonObjectIdentity = false
657
    ) : void {
658
        if ($haystack instanceof DBField) {
659
            $haystack = (string)$haystack;
660
        }
661
        parent::assertNotContains(
662
            $needle,
663
            $haystack,
664
            $message,
665
            $ignoreCase,
666
            $checkForObjectIdentity,
667
            $checkForNonObjectIdentity
668
        );
669
    }
670
671
    /**
672
     * Clear the log of emails sent
673
     *
674
     * @return bool True if emails cleared
675
     */
676
    public function clearEmails() : bool
677
    {
678
        /** @var Mailer $mailer */
679
        $mailer = Injector::inst()->get(Mailer::class);
680
        if ($mailer instanceof TestMailer) {
681
            $mailer->clearEmails();
682
            return true;
683
        }
684
        return false;
685
    }
686
687
    /**
688
     * Search for an email that was sent.
689
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
690
     * @param string $to
691
     * @param string $from
692
     * @param string $subject
693
     * @param string $content
694
     * @return array Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
695
     *               'HtmlContent'
696
     */
697
    public static function findEmail($to, $from = null, $subject = null, $content = null) : array
698
    {
699
        /** @var Mailer $mailer */
700
        $mailer = Injector::inst()->get(Mailer::class);
701
        if ($mailer instanceof TestMailer) {
702
            return $mailer->findEmail($to, $from, $subject, $content);
703
        }
704
        return [];
705
    }
706
707
    /**
708
     * Assert that the matching email was sent since the last call to clearEmails()
709
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
710
     *
711
     * @param string $to
712
     * @param string $from
713
     * @param string $subject
714
     * @param string $content
715
     */
716
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null) : void
717
    {
718
        $found = (bool)static::findEmail($to, $from, $subject, $content);
719
720
        $infoParts = '';
721
        $withParts = array();
722
        if ($to) {
723
            $infoParts .= " to '$to'";
724
        }
725
        if ($from) {
726
            $infoParts .= " from '$from'";
727
        }
728
        if ($subject) {
729
            $withParts[] = "subject '$subject'";
730
        }
731
        if ($content) {
732
            $withParts[] = "content '$content'";
733
        }
734
        if ($withParts) {
735
            $infoParts .= ' with ' . implode(' and ', $withParts);
736
        }
737
738
        static::assertTrue(
739
            $found,
740
            "Failed asserting that an email was sent$infoParts."
741
        );
742
    }
743
744
745
    /**
746
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
747
     * pairs.  Each match must correspond to 1 distinct record.
748
     *
749
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
750
     * either pass a single pattern or an array of patterns.
751
     * @param SS_List $list The {@link SS_List} to test.
752
     * @param string $message
753
     *
754
     * Examples
755
     * --------
756
     * Check that $members includes an entry with Email = [email protected]:
757
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
758
     *
759
     * Check that $members includes entries with Email = [email protected] and with
760
     * Email = [email protected]:
761
     *      $this->assertListContains([
762
     *         ['Email' => '[email protected]'],
763
     *         ['Email' => '[email protected]'],
764
     *      ], $members);
765
     */
766
    public static function assertListContains($matches, SS_List $list, $message = '') : void
767
    {
768
        if (!is_array($matches)) {
769
            throw InvalidArgumentHelper::factory(
770
                1,
771
                'array'
772
            );
773
        }
774
775
        static::assertThat(
776
            $list,
777
            new SSListContains(
778
                $matches
779
            ),
780
            $message
781
        );
782
    }
783
784
    /**
785
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
786
     *
787
     * @param array $matches
788
     * @param $dataObjectSet
789
     */
790
    public function assertDOSContains($matches, $dataObjectSet)
791
    {
792
        Deprecation::notice('5.0', 'Use assertListContains() instead');
793
        static::assertListContains($matches, $dataObjectSet);
794
    }
795
796
    /**
797
     * Asserts that no items in a given list appear in the given dataobject list
798
     *
799
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
800
     * either pass a single pattern or an array of patterns.
801
     * @param SS_List $list The {@link SS_List} to test.
802
     * @param string $message
803
     *
804
     * Examples
805
     * --------
806
     * Check that $members doesn't have an entry with Email = [email protected]:
807
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
808
     *
809
     * Check that $members doesn't have entries with Email = [email protected] and with
810
     * Email = [email protected]:
811
     *      $this->assertListNotContains([
812
     *          ['Email' => '[email protected]'],
813
     *          ['Email' => '[email protected]'],
814
     *      ], $members);
815
     */
816
    public static function assertListNotContains($matches, SS_List $list, $message = '') : void
817
    {
818
        if (!is_array($matches)) {
819
            throw InvalidArgumentHelper::factory(
820
                1,
821
                'array'
822
            );
823
        }
824
825
        $constraint =  new LogicalNot(
826
            new SSListContains(
827
                $matches
828
            )
829
        );
830
831
        static::assertThat(
832
            $list,
833
            $constraint,
834
            $message
835
        );
836
    }
837
838
    /**
839
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
840
     *
841
     * @param $matches
842
     * @param $dataObjectSet
843
     */
844
    public static function assertNotDOSContains($matches, $dataObjectSet)
845
    {
846
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
847
        static::assertListNotContains($matches, $dataObjectSet);
848
    }
849
850
    /**
851
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
852
     * key-value pairs.  Each match must correspond to 1 distinct record.
853
     *
854
     * Example
855
     * --------
856
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
857
     * matter:
858
     *     $this->assertListEquals([
859
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
860
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
861
     *      ], $members);
862
     *
863
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
864
     * either pass a single pattern or an array of patterns.
865
     * @param mixed $list The {@link SS_List} to test.
866
     * @param string $message
867
     */
868
    public static function assertListEquals($matches, SS_List $list, $message = '') : void
869
    {
870
        if (!is_array($matches)) {
871
            throw InvalidArgumentHelper::factory(
872
                1,
873
                'array'
874
            );
875
        }
876
877
        static::assertThat(
878
            $list,
879
            new SSListContainsOnly(
880
                $matches
881
            ),
882
            $message
883
        );
884
    }
885
886
    /**
887
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
888
     *
889
     * @param $matches
890
     * @param SS_List $dataObjectSet
891
     */
892
    public function assertDOSEquals($matches, $dataObjectSet)
893
    {
894
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
895
        static::assertListEquals($matches, $dataObjectSet);
896
    }
897
898
899
    /**
900
     * Assert that the every record in the given {@link SS_List} matches the given key-value
901
     * pairs.
902
     *
903
     * Example
904
     * --------
905
     * Check that every entry in $members has a Status of 'Active':
906
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
907
     *
908
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
909
     * @param mixed $list The {@link SS_List} to test.
910
     * @param string $message
911
     */
912
    public static function assertListAllMatch($match, SS_List $list, $message = '') : void
913
    {
914
        if (!is_array($match)) {
915
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
0 ignored issues
show
Bug introduced by
The type SilverStripe\Dev\PHPUnit...l_InvalidArgumentHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
916
                1,
917
                'array'
918
            );
919
        }
920
921
        static::assertThat(
922
            $list,
923
            new SSListContainsOnlyMatchingItems(
924
                $match
925
            ),
926
            $message
927
        );
928
    }
929
930
    /**
931
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
932
     *
933
     * @param $match
934
     * @param SS_List $dataObjectSet
935
     */
936
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
937
    {
938
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
939
        static::assertListAllMatch($match, $dataObjectSet);
940
    }
941
942
    /**
943
     * Removes sequences of repeated whitespace characters from SQL queries
944
     * making them suitable for string comparison
945
     *
946
     * @param string $sql
947
     * @return string The cleaned and normalised SQL string
948
     */
949
    protected static function normaliseSQL($sql) : string
950
    {
951
        return trim(preg_replace('/\s+/m', ' ', $sql));
952
    }
953
954
    /**
955
     * Asserts that two SQL queries are equivalent
956
     *
957
     * @param string $expectedSQL
958
     * @param string $actualSQL
959
     * @param string $message
960
     * @param float|int $delta
961
     * @param integer $maxDepth
962
     * @param boolean $canonicalize
963
     * @param boolean $ignoreCase
964
     */
965
    public static function assertSQLEquals(
966
        $expectedSQL,
967
        $actualSQL,
968
        string $message = '',
969
        int $delta = 0,
970
        int $maxDepth = 10,
971
        bool $canonicalize = false,
972
        bool $ignoreCase = false
973
    ) : void {
974
        // Normalise SQL queries to remove patterns of repeating whitespace
975
        $expectedSQL = static::normaliseSQL($expectedSQL);
976
        $actualSQL = static::normaliseSQL($actualSQL);
977
978
        static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
979
    }
980
981
    /**
982
     * Asserts that a SQL query contains a SQL fragment
983
     *
984
     * @param string $needleSQL
985
     * @param string $haystackSQL
986
     * @param string $message
987
     * @param boolean $ignoreCase
988
     * @param boolean $checkForObjectIdentity
989
     */
990
    public static function assertSQLContains(
991
        $needleSQL,
992
        $haystackSQL,
993
        string $message = '',
994
        bool $ignoreCase = false,
995
        bool $checkForObjectIdentity = true
996
    ) : void {
997
        $needleSQL = static::normaliseSQL($needleSQL);
998
        $haystackSQL = static::normaliseSQL($haystackSQL);
999
1000
        static::assertStringContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
0 ignored issues
show
Unused Code introduced by
The call to PHPUnit\Framework\Assert...tStringContainsString() has too many arguments starting with $ignoreCase. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1000
        static::/** @scrutinizer ignore-call */ 
1001
                assertStringContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1001
    }
1002
1003
    /**
1004
     * Asserts that a SQL query contains a SQL fragment
1005
     *
1006
     * @param string $needleSQL
1007
     * @param string $haystackSQL
1008
     * @param string $message
1009
     * @param boolean $ignoreCase
1010
     * @param boolean $checkForObjectIdentity
1011
     */
1012
    public static function assertSQLNotContains(
1013
        $needleSQL,
1014
        $haystackSQL,
1015
        string $message = '',
1016
        bool $ignoreCase = false,
1017
        bool $checkForObjectIdentity = true
1018
    ) : void {
1019
        $needleSQL = static::normaliseSQL($needleSQL);
1020
        $haystackSQL = static::normaliseSQL($haystackSQL);
1021
1022
        static::assertStringNotContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
0 ignored issues
show
Unused Code introduced by
The call to PHPUnit\Framework\Assert...ringNotContainsString() has too many arguments starting with $ignoreCase. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1022
        static::/** @scrutinizer ignore-call */ 
1023
                assertStringNotContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1023
    }
1024
1025
    /**
1026
     * Start test environment
1027
     */
1028
    public static function start() : void
1029
    {
1030
        if (static::is_running_test()) {
1031
            return;
1032
        }
1033
1034
        // Health check
1035
        if (InjectorLoader::inst()->countManifests()) {
1036
            throw new LogicException('SapphireTest::start() cannot be called within another application');
1037
        }
1038
        static::set_is_running_test(true);
1039
1040
        // Mock request
1041
        $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1042
        $request = CLIRequestBuilder::createFromEnvironment();
1043
1044
        // Test application
1045
        $kernel = new TestKernel(BASE_PATH);
1046
        $app = new HTTPApplication($kernel);
1047
        $flush = array_key_exists('flush', $request->getVars());
1048
1049
        // Custom application
1050
        $app->execute($request, function (HTTPRequest $request) {
1051
            // Start session and execute
1052
            $request->getSession()->init($request);
1053
1054
            // Invalidate classname spec since the test manifest will now pull out new subclasses for each
1055
            // internal class (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1056
            DataObject::reset();
1057
1058
            // Set dummy controller;
1059
            $controller = Controller::create();
1060
            $controller->setRequest($request);
1061
            $controller->pushCurrent();
1062
            $controller->doInit();
1063
        }, $flush);
1064
1065
        // Register state
1066
        static::$state = SapphireTestState::singleton();
1067
        // Register temp DB holder
1068
        static::tempDB();
1069
    }
1070
1071
    /**
1072
     * Reset the testing database's schema, but only if it is active
1073
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1074
     * @param bool $forceCreate Force DB to be created if it doesn't exist
1075
     */
1076
    public static function resetDBSchema(bool $includeExtraDataObjects = false, bool $forceCreate = false) : void
1077
    {
1078
        // Check if DB is active before reset
1079
        if (!static::$tempDB->isUsed()) {
1080
            if (!$forceCreate) {
1081
                return;
1082
            }
1083
            static::$tempDB->build();
1084
        }
1085
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1086
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
1087
    }
1088
1089
    /**
1090
     * A wrapper for automatically performing callbacks as a user with a specific permission
1091
     *
1092
     * @param string|array $permCode
1093
     * @param callable $callback
1094
     * @return mixed
1095
     */
1096
    public function actWithPermission($permCode, callable $callback)
1097
    {
1098
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1099
    }
1100
1101
    /**
1102
     * Create Member and Group objects on demand with specific permission code
1103
     *
1104
     * @param string|array $permCode
1105
     * @return Member
1106
     */
1107
    protected function createMemberWithPermission($permCode) : Member
1108
    {
1109
        if (is_array($permCode)) {
1110
            $permArray = $permCode;
1111
            $permCode = implode('.', $permCode);
1112
        } else {
1113
            $permArray = array($permCode);
1114
        }
1115
1116
        // Check cached member
1117
        if (isset($this->cache_generatedMembers[$permCode])) {
1118
            $member = $this->cache_generatedMembers[$permCode];
1119
        } else {
1120
            // Generate group with these permissions
1121
            $group = Group::create();
1122
            $group->Title = "$permCode group";
1123
            $group->write();
1124
1125
            // Create each individual permission
1126
            foreach ($permArray as $permArrayItem) {
1127
                $permission = Permission::create();
1128
                $permission->Code = $permArrayItem;
1129
                $permission->write();
1130
                $group->Permissions()->add($permission);
1131
            }
1132
1133
            $member = Member::get()->filter([
1134
                'Email' => "[email protected]",
1135
            ])->first();
1136
            if (!$member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1137
                $member = Member::create();
1138
            }
1139
1140
            $member->FirstName = $permCode;
0 ignored issues
show
Bug Best Practice introduced by
The property FirstName does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
1141
            $member->Surname = 'User';
0 ignored issues
show
Bug Best Practice introduced by
The property Surname does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
1142
            $member->Email = "[email protected]";
0 ignored issues
show
Bug Best Practice introduced by
The property Email does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
1143
            $member->write();
1144
            $group->Members()->add($member);
1145
1146
            $this->cache_generatedMembers[$permCode] = $member;
1147
        }
1148
        return $member;
1149
    }
1150
1151
    /**
1152
     * Create a member and group with the given permission code, and log in with it.
1153
     * Returns the member ID.
1154
     *
1155
     * @param string|array $permCode Either a permission, or list of permissions
1156
     * @return int Member ID
1157
     */
1158
    public function logInWithPermission($permCode = 'ADMIN')
1159
    {
1160
        $member = $this->createMemberWithPermission($permCode);
1161
        $this->logInAs($member);
1162
        return $member->ID;
1163
    }
1164
1165
    /**
1166
     * Log in as the given member
1167
     *
1168
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1169
     */
1170
    public function logInAs($member) : void
1171
    {
1172
        if (is_numeric($member)) {
1173
            $member = DataObject::get_by_id(Member::class, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type string; however, parameter $idOrCache of SilverStripe\ORM\DataObject::get_by_id() does only seem to accept boolean|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1173
            $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1174
        } elseif (!is_object($member)) {
1175
            $member = $this->objFromFixture(Member::class, $member);
1176
        }
1177
        Injector::inst()->get(IdentityStore::class)->logIn($member);
1178
    }
1179
1180
    /**
1181
     * Log out the current user
1182
     */
1183
    public function logOut() : void
1184
    {
1185
        /** @var IdentityStore $store */
1186
        $store = Injector::inst()->get(IdentityStore::class);
1187
        $store->logOut();
1188
    }
1189
1190
    /**
1191
     * Cache for logInWithPermission()
1192
     */
1193
    protected $cache_generatedMembers = [];
1194
1195
    /**
1196
     * Test against a theme.
1197
     *
1198
     * @param string $themeBaseDir themes directory
1199
     * @param string $theme Theme name
1200
     * @param callable $callback
1201
     * @throws Exception
1202
     */
1203
    protected function useTestTheme(string $themeBaseDir, string $theme, callable $callback) : void
1204
    {
1205
        Config::nest();
1206
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1207
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1208
        }
1209
        SSViewer::config()->update('theme_enabled', true);
1210
        SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1211
1212
        try {
1213
            $callback();
1214
        } finally {
1215
            Config::unnest();
1216
        }
1217
    }
1218
1219
    /**
1220
     * Get fixture paths for this test
1221
     *
1222
     * @return array List of paths
1223
     */
1224
    protected function getFixturePaths() : array
1225
    {
1226
        $fixtureFile = static::get_fixture_file();
1227
        if (empty($fixtureFile)) {
1228
            return [];
1229
        }
1230
1231
        $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1232
1233
        return array_map(function ($fixtureFilePath) {
1234
            return $this->resolveFixturePath($fixtureFilePath);
1235
        }, $fixtureFiles);
1236
    }
1237
1238
    /**
1239
     * Return all extra objects to scaffold for this test
1240
     * @return array
1241
     */
1242
    public static function getExtraDataObjects()
1243
    {
1244
        return static::$extra_dataobjects;
1245
    }
1246
1247
    /**
1248
     * Get additional controller classes to register routes for
1249
     *
1250
     * @return array
1251
     */
1252
    public static function getExtraControllers() : array
1253
    {
1254
        return static::$extra_controllers;
1255
    }
1256
1257
    /**
1258
     * Map a fixture path to a physical file
1259
     *
1260
     * @param string $fixtureFilePath
1261
     * @return string
1262
     */
1263
    protected function resolveFixturePath(string $fixtureFilePath) : string
1264
    {
1265
        // support loading via composer name path.
1266
        if (strpos($fixtureFilePath, ':') !== false) {
1267
            return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1268
        }
1269
1270
        // Support fixture paths relative to the test class, rather than relative to webroot
1271
        // String checking is faster than file_exists() calls.
1272
        $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1273
        if ($resolvedPath) {
1274
            return $resolvedPath;
1275
        }
1276
1277
        // Check if file exists relative to base dir
1278
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1279
        if ($resolvedPath) {
1280
            return $resolvedPath;
1281
        }
1282
1283
        return $fixtureFilePath;
1284
    }
1285
1286
    protected function setUpRoutes()
1287
    {
1288
        // Get overridden routes
1289
        $rules = $this->getExtraRoutes();
1290
1291
        // Add all other routes
1292
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
1293
            if (!isset($rules[$route])) {
1294
                $rules[$route] = $rule;
1295
            }
1296
        }
1297
1298
        // Add default catch-all rule
1299
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1300
1301
        // Add controller-name auto-routing
1302
        Director::config()->set('rules', $rules);
1303
    }
1304
1305
    /**
1306
     * Get extra routes to merge into Director.rules
1307
     *
1308
     * @return array
1309
     */
1310
    protected function getExtraRoutes()
1311
    {
1312
        $rules = [];
1313
        foreach ($this->getExtraControllers() as $class) {
1314
            $controllerInst = Controller::singleton($class);
1315
            $link = Director::makeRelative($controllerInst->Link());
1316
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1317
            $rules[$route] = $class;
1318
        }
1319
        return $rules;
1320
    }
1321
}
1322