Passed
Pull Request — 4 (#10028)
by Steve
09:35
created

SapphireTest::assertContainsNonIterable()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 4
nop 4
dl 0
loc 13
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use Exception;
6
use InvalidArgumentException;
7
use LogicException;
8
use PHPUnit\Framework\Constraint\LogicalNot;
9
use PHPUnit\Framework\Constraint\IsEqualCanonicalizing;
10
use PHPUnit\Framework\TestCase;
11
use PHPUnit\Framework\Exception as PHPUnitFrameworkException;
12
use PHPUnit\Util\Test as TestUtil;
13
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...
14
use SilverStripe\Control\CLIRequestBuilder;
15
use SilverStripe\Control\Controller;
16
use SilverStripe\Control\Cookie;
17
use SilverStripe\Control\Director;
18
use SilverStripe\Control\Email\Email;
19
use SilverStripe\Control\Email\Mailer;
20
use SilverStripe\Control\HTTPApplication;
21
use SilverStripe\Control\HTTPRequest;
22
use SilverStripe\Core\Config\Config;
23
use SilverStripe\Core\Injector\Injector;
24
use SilverStripe\Core\Injector\InjectorLoader;
25
use SilverStripe\Core\Manifest\ClassLoader;
26
use SilverStripe\Core\Manifest\ModuleResourceLoader;
27
use SilverStripe\Dev\Constraint\SSListContains;
28
use SilverStripe\Dev\Constraint\SSListContainsOnly;
29
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems;
30
use SilverStripe\Dev\State\FixtureTestState;
31
use SilverStripe\Dev\State\SapphireTestState;
32
use SilverStripe\i18n\i18n;
33
use SilverStripe\ORM\Connect\TempDatabase;
34
use SilverStripe\ORM\DataObject;
35
use SilverStripe\ORM\FieldType\DBDatetime;
36
use SilverStripe\ORM\FieldType\DBField;
37
use SilverStripe\ORM\SS_List;
38
use SilverStripe\Security\Group;
39
use SilverStripe\Security\IdentityStore;
40
use SilverStripe\Security\Member;
41
use SilverStripe\Security\Permission;
42
use SilverStripe\Security\Security;
43
use SilverStripe\View\SSViewer;
44
45
/* -------------------------------------------------
46
 *
47
 * This version of SapphireTest is for phpunit 9
48
 * If using phpunit 5, see _legacy/SapphireTest.php
49
 * phpunit 6, 7 and 8 are not supported
50
 *
51
 * phpunit 5 is not compatible with php 8, though aminimum versin of php 7.3 is required
52
 * for phpunit 9.  As we're still supporting php 7.1 we need to support both versions for a while
53
 *
54
 * Once php 7.3 is the minimum version required by framework, the _legacy versions of SapphireTest
55
 * and FunctionalTest should be removed, along with the if(class_exists()) check on this file
56
 *
57
 * IsEqualCanonicalizing::class is a new class added in phpunit 9, testing that this class exists
58
 * to ensure that we're not using a a prior, incompatible version of php
59
 *
60
 * Using `if (class_exists())` rather than `if (!class_exists()) { return; }` so that
61
 * the class is indented and the phpunit 5 version in _legacy/SapphireTest.php is used
62
 * for the function signature by the PHP interpreter.  This is desirable because the phpunit5
63
 * version has `setUp()` with no return type while the phpunit 9 version has `setUp(): void`
64
 * Return type covariance allows more specific return types to be defined, while contravariance
65
 * of return types of more abstract return types is not supported
66
 *
67
 * -------------------------------------------------
68
 */
69
if (class_exists(IsEqualCanonicalizing::class)) {
70
71
    /**
72
     * Test case class for the Sapphire framework.
73
     * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
74
     * to work with.
75
     *
76
     * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
77
     * in production sites.
78
     */
79
    class SapphireTest extends TestCase implements TestOnly
80
    {
81
        /**
82
         * Path to fixture data for this test run.
83
         * If passed as an array, multiple fixture files will be loaded.
84
         * Please note that you won't be able to refer with "=>" notation
85
         * between the fixtures, they act independent of each other.
86
         *
87
         * @var string|array
88
         */
89
        protected static $fixture_file = null;
90
91
        /**
92
         * @deprecated 4.0..5.0 Use FixtureTestState instead
93
         * @var FixtureFactory
94
         */
95
        protected $fixtureFactory;
96
97
        /**
98
         * @var Boolean If set to TRUE, this will force a test database to be generated
99
         * in {@link setUp()}. Note that this flag is overruled by the presence of a
100
         * {@link $fixture_file}, which always forces a database build.
101
         *
102
         * @var bool
103
         */
104
        protected $usesDatabase = null;
105
106
        /**
107
         * This test will cleanup its state via transactions.
108
         * If set to false a full schema is forced between tests, but at a performance cost.
109
         *
110
         * @var bool
111
         */
112
        protected $usesTransactions = true;
113
114
        /**
115
         * @var bool
116
         */
117
        protected static $is_running_test = false;
118
119
        /**
120
         * By default, setUp() does not require default records. Pass
121
         * class names in here, and the require/augment default records
122
         * function will be called on them.
123
         *
124
         * @var array
125
         */
126
        protected $requireDefaultRecordsFrom = [];
127
128
        /**
129
         * A list of extensions that can't be applied during the execution of this run.  If they are
130
         * applied, they will be temporarily removed and a database migration called.
131
         *
132
         * The keys of the are the classes that the extensions can't be applied the extensions to, and
133
         * the values are an array of illegal extensions on that class.
134
         *
135
         * Set a class to `*` to remove all extensions (unadvised)
136
         *
137
         * @var array
138
         */
139
        protected static $illegal_extensions = [];
140
141
        /**
142
         * A list of extensions that must be applied during the execution of this run.  If they are
143
         * not applied, they will be temporarily added and a database migration called.
144
         *
145
         * The keys of the are the classes to apply the extensions to, and the values are an array
146
         * of required extensions on that class.
147
         *
148
         * Example:
149
         * <code>
150
         * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
151
         * </code>
152
         *
153
         * @var array
154
         */
155
        protected static $required_extensions = [];
156
157
        /**
158
         * By default, the test database won't contain any DataObjects that have the interface TestOnly.
159
         * This variable lets you define additional TestOnly DataObjects to set up for this test.
160
         * Set it to an array of DataObject subclass names.
161
         *
162
         * @var array
163
         */
164
        protected static $extra_dataobjects = [];
165
166
        /**
167
         * List of class names of {@see Controller} objects to register routes for
168
         * Controllers must implement Link() method
169
         *
170
         * @var array
171
         */
172
        protected static $extra_controllers = [];
173
174
        /**
175
         * We need to disabling backing up of globals to avoid overriding
176
         * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
177
         *
178
         * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
179
         */
180
        protected $backupGlobals = false;
181
182
        /**
183
         * State management container for SapphireTest
184
         *
185
         * @var SapphireTestState
186
         */
187
        protected static $state = null;
188
189
        /**
190
         * Temp database helper
191
         *
192
         * @var TempDatabase
193
         */
194
        protected static $tempDB = null;
195
196
        /**
197
         * @return TempDatabase
198
         */
199
        public static function tempDB()
200
        {
201
            if (!class_exists(TempDatabase::class)) {
202
                return null;
203
            }
204
205
            if (!static::$tempDB) {
206
                static::$tempDB = TempDatabase::create();
207
            }
208
            return static::$tempDB;
209
        }
210
211
        /**
212
         * Gets illegal extensions for this class
213
         *
214
         * @return array
215
         */
216
        public static function getIllegalExtensions()
217
        {
218
            return static::$illegal_extensions;
219
        }
220
221
        /**
222
         * Gets required extensions for this class
223
         *
224
         * @return array
225
         */
226
        public static function getRequiredExtensions()
227
        {
228
            return static::$required_extensions;
229
        }
230
231
        /**
232
         * Check if test bootstrapping has been performed. Must not be relied on
233
         * outside of unit tests.
234
         *
235
         * @return bool
236
         */
237
        protected static function is_running_test()
238
        {
239
            return self::$is_running_test;
240
        }
241
242
        /**
243
         * Set test running state
244
         *
245
         * @param bool $bool
246
         */
247
        protected static function set_is_running_test($bool)
248
        {
249
            self::$is_running_test = $bool;
250
        }
251
252
        /**
253
         * @return String
254
         */
255
        public static function get_fixture_file()
256
        {
257
            return static::$fixture_file;
258
        }
259
260
        /**
261
         * @return bool
262
         */
263
        public function getUsesDatabase()
264
        {
265
            return $this->usesDatabase;
266
        }
267
268
        /**
269
         * @return bool
270
         */
271
        public function getUsesTransactions()
272
        {
273
            return $this->usesTransactions;
274
        }
275
276
        /**
277
         * @return array
278
         */
279
        public function getRequireDefaultRecordsFrom()
280
        {
281
            return $this->requireDefaultRecordsFrom;
282
        }
283
284
        /**
285
         * Setup  the test.
286
         * Always sets up in order:
287
         *  - Reset php state
288
         *  - Nest
289
         *  - Custom state helpers
290
         *
291
         * User code should call parent::setUp() before custom setup code
292
         */
293
        protected function setUp(): void
294
        {
295
            if (!defined('FRAMEWORK_PATH')) {
296
                trigger_error(
297
                    'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
298
                    E_USER_WARNING
299
                );
300
            }
301
302
            // Call state helpers
303
            static::$state->setUp($this);
304
305
            // We cannot run the tests on this abstract class.
306
            if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
307
                $this->markTestSkipped(sprintf('Skipping %s ', static::class));
308
            }
309
310
            // i18n needs to be set to the defaults or tests fail
311
            if (class_exists(i18n::class)) {
312
                i18n::set_locale(i18n::config()->uninherited('default_locale'));
313
            }
314
315
            // Set default timezone consistently to avoid NZ-specific dependencies
316
            date_default_timezone_set('UTC');
317
318
            if (class_exists(Member::class)) {
319
                Member::set_password_validator(null);
320
            }
321
322
            if (class_exists(Cookie::class)) {
323
                Cookie::config()->update('report_errors', false);
324
            }
325
326
            if (class_exists(RootURLController::class)) {
327
                RootURLController::reset();
328
            }
329
330
            if (class_exists(Security::class)) {
331
                Security::clear_database_is_ready();
332
            }
333
334
            // Set up test routes
335
            $this->setUpRoutes();
336
337
            $fixtureFiles = $this->getFixturePaths();
338
339
            if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
340
                // Assign fixture factory to deprecated prop in case old tests use it over the getter
341
                /** @var FixtureTestState $fixtureState */
342
                $fixtureState = static::$state->getStateByName('fixtures');
343
                $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
344
345
                $this->logInWithPermission('ADMIN');
346
            }
347
348
            // turn off template debugging
349
            if (class_exists(SSViewer::class)) {
350
                SSViewer::config()->update('source_file_comments', false);
351
            }
352
353
            // Set up the test mailer
354
            if (class_exists(TestMailer::class)) {
355
                Injector::inst()->registerService(new TestMailer(), Mailer::class);
356
            }
357
358
            if (class_exists(Email::class)) {
359
                Email::config()->remove('send_all_emails_to');
360
                Email::config()->remove('send_all_emails_from');
361
                Email::config()->remove('cc_all_emails_to');
362
                Email::config()->remove('bcc_all_emails_to');
363
            }
364
        }
365
366
367
        /**
368
         * Helper method to determine if the current test should enable a test database
369
         *
370
         * @param $fixtureFiles
371
         * @return bool
372
         */
373
        protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
374
        {
375
            $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
376
377
            return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
378
                || $this->currentTestEnablesDatabase();
379
        }
380
381
        /**
382
         * Helper method to check, if the current test uses the database.
383
         * This can be switched on with the annotation "@useDatabase"
384
         *
385
         * @return bool
386
         */
387
        protected function currentTestEnablesDatabase()
388
        {
389
            $annotations = $this->getAnnotations();
390
391
            return array_key_exists('useDatabase', $annotations['method'])
392
                && $annotations['method']['useDatabase'][0] !== 'false';
393
        }
394
395
        /**
396
         * Helper method to check, if the current test uses the database.
397
         * This can be switched on with the annotation "@useDatabase false"
398
         *
399
         * @return bool
400
         */
401
        protected function currentTestDisablesDatabase()
402
        {
403
            $annotations = $this->getAnnotations();
404
405
            return array_key_exists('useDatabase', $annotations['method'])
406
                && $annotations['method']['useDatabase'][0] === 'false';
407
        }
408
409
        /**
410
         * Called once per test case ({@link SapphireTest} subclass).
411
         * This is different to {@link setUp()}, which gets called once
412
         * per method. Useful to initialize expensive operations which
413
         * don't change state for any called method inside the test,
414
         * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
415
         * for tearing down the state again.
416
         *
417
         * Always sets up in order:
418
         *  - Reset php state
419
         *  - Nest
420
         *  - Custom state helpers
421
         *
422
         * User code should call parent::setUpBeforeClass() before custom setup code
423
         *
424
         * @throws Exception
425
         */
426
        public static function setUpBeforeClass(): void
427
        {
428
            // Start tests
429
            static::start();
430
431
            if (!static::$state) {
432
                throw new Exception('SapphireTest failed to bootstrap!');
433
            }
434
435
            // Call state helpers
436
            static::$state->setUpOnce(static::class);
437
438
            // Build DB if we have objects
439
            if (class_exists(DataObject::class) && static::getExtraDataObjects()) {
440
                DataObject::reset();
441
                static::resetDBSchema(true, true);
442
            }
443
        }
444
445
        /**
446
         * tearDown method that's called once per test class rather once per test method.
447
         *
448
         * Always sets up in order:
449
         *  - Custom state helpers
450
         *  - Unnest
451
         *  - Reset php state
452
         *
453
         * User code should call parent::tearDownAfterClass() after custom tear down code
454
         */
455
        public static function tearDownAfterClass(): void
456
        {
457
            // Call state helpers
458
            static::$state->tearDownOnce(static::class);
459
460
            // Reset DB schema
461
            static::resetDBSchema();
462
        }
463
464
        /**
465
         * @return FixtureFactory|false
466
         * @deprecated 4.0.0:5.0.0
467
         */
468
        public function getFixtureFactory()
469
        {
470
            Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
471
            /** @var FixtureTestState $state */
472
            $state = static::$state->getStateByName('fixtures');
473
            return $state->getFixtureFactory(static::class);
474
        }
475
476
        /**
477
         * Sets a new fixture factory
478
         * @param FixtureFactory $factory
479
         * @return $this
480
         * @deprecated 4.0.0:5.0.0
481
         */
482
        public function setFixtureFactory(FixtureFactory $factory)
483
        {
484
            Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
485
            /** @var FixtureTestState $state */
486
            $state = static::$state->getStateByName('fixtures');
487
            $state->setFixtureFactory($factory, static::class);
488
            $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

488
            /** @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...
489
            return $this;
490
        }
491
492
        /**
493
         * Get the ID of an object from the fixture.
494
         *
495
         * @param string $className The data class or table name, as specified in your fixture file.  Parent classes won't work
496
         * @param string $identifier The identifier string, as provided in your fixture file
497
         * @return int
498
         */
499
        protected function idFromFixture($className, $identifier)
500
        {
501
            /** @var FixtureTestState $state */
502
            $state = static::$state->getStateByName('fixtures');
503
            $id = $state->getFixtureFactory(static::class)->getId($className, $identifier);
504
505
            if (!$id) {
506
                throw new InvalidArgumentException(sprintf(
507
                    "Couldn't find object '%s' (class: %s)",
508
                    $identifier,
509
                    $className
510
                ));
511
            }
512
513
            return $id;
514
        }
515
516
        /**
517
         * Return all of the IDs in the fixture of a particular class name.
518
         * Will collate all IDs form all fixtures if multiple fixtures are provided.
519
         *
520
         * @param string $className The data class or table name, as specified in your fixture file
521
         * @return array A map of fixture-identifier => object-id
522
         */
523
        protected function allFixtureIDs($className)
524
        {
525
            /** @var FixtureTestState $state */
526
            $state = static::$state->getStateByName('fixtures');
527
            return $state->getFixtureFactory(static::class)->getIds($className);
528
        }
529
530
        /**
531
         * Get an object from the fixture.
532
         *
533
         * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work
534
         * @param string $identifier The identifier string, as provided in your fixture file
535
         *
536
         * @return DataObject
537
         */
538
        protected function objFromFixture($className, $identifier)
539
        {
540
            /** @var FixtureTestState $state */
541
            $state = static::$state->getStateByName('fixtures');
542
            $obj = $state->getFixtureFactory(static::class)->get($className, $identifier);
543
544
            if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
545
                throw new InvalidArgumentException(sprintf(
546
                    "Couldn't find object '%s' (class: %s)",
547
                    $identifier,
548
                    $className
549
                ));
550
            }
551
552
            return $obj;
553
        }
554
555
        /**
556
         * Load a YAML fixture file into the database.
557
         * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
558
         * Doesn't clear existing fixtures.
559
         * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
560
         * @deprecated 4.0.0:5.0.0
561
         *
562
         */
563
        public function loadFixture($fixtureFile)
564
        {
565
            Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
566
            $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
567
            $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

567
            $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...
568
        }
569
570
        /**
571
         * Clear all fixtures which were previously loaded through
572
         * {@link loadFixture()}
573
         */
574
        public function clearFixtures()
575
        {
576
            /** @var FixtureTestState $state */
577
            $state = static::$state->getStateByName('fixtures');
578
            $state->getFixtureFactory(static::class)->clear();
579
        }
580
581
        /**
582
         * Useful for writing unit tests without hardcoding folder structures.
583
         *
584
         * @return string Absolute path to current class.
585
         */
586
        protected function getCurrentAbsolutePath()
587
        {
588
            $filename = ClassLoader::inst()->getItemPath(static::class);
589
            if (!$filename) {
590
                throw new LogicException('getItemPath returned null for ' . static::class
591
                    . '. Try adding flush=1 to the test run.');
592
            }
593
            return dirname($filename);
594
        }
595
596
        /**
597
         * @return string File path relative to webroot
598
         */
599
        protected function getCurrentRelativePath()
600
        {
601
            $base = Director::baseFolder();
602
            $path = $this->getCurrentAbsolutePath();
603
            if (substr($path, 0, strlen($base)) == $base) {
604
                $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
605
            }
606
            return $path;
607
        }
608
609
        /**
610
         * Setup  the test.
611
         * Always sets up in order:
612
         *  - Custom state helpers
613
         *  - Unnest
614
         *  - Reset php state
615
         *
616
         * User code should call parent::tearDown() after custom tear down code
617
         */
618
        protected function tearDown(): void
619
        {
620
            // Reset mocked datetime
621
            if (class_exists(DBDatetime::class)) {
622
                DBDatetime::clear_mock_now();
623
            }
624
625
            // Stop the redirection that might have been requested in the test.
626
            // Note: Ideally a clean Controller should be created for each test.
627
            // Now all tests executed in a batch share the same controller.
628
            if (class_exists(Controller::class)) {
629
                $controller = Controller::has_curr() ? Controller::curr() : null;
630
                if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
631
                    $response->setStatusCode(200);
632
                    $response->removeHeader('Location');
633
                }
634
            }
635
636
            // Call state helpers
637
            static::$state->tearDown($this);
638
        }
639
640
        public static function assertContains(
641
            $needle,
642
            $haystack,
643
            $message = '',
644
            $ignoreCase = false,
645
            $checkForObjectIdentity = true,
646
            $checkForNonObjectIdentity = false
647
        ): void {
648
            if ($haystack instanceof DBField) {
649
                $haystack = (string)$haystack;
650
            }
651
            if (is_iterable($haystack)) {
652
                $strict = is_object($needle) ? $checkForObjectIdentity : $checkForNonObjectIdentity;
653
                if ($strict) {
654
                    parent::assertContains($needle, $haystack, $message);
655
                } else {
656
                    parent::assertContainsEquals($needle, $haystack, $message);
657
                }
658
            } else {
659
                static::assertContainsNonIterable($needle, $haystack, $message, $ignoreCase);
660
            }
661
        }
662
663
        public static function assertContainsNonIterable(
664
            $needle,
665
            $haystack,
666
            $message = '',
667
            $ignoreCase = false
668
        ): void {
669
            if ($haystack instanceof DBField) {
670
                $haystack = (string)$haystack;
671
            }
672
            if ($ignoreCase) {
673
                parent::assertStringContainsStringIgnoringCase($needle, $haystack, $message);
674
            } else {
675
                parent::assertStringContainsString($needle, $haystack, $message);
676
            }
677
        }
678
679
        public static function assertNotContains(
680
            $needle,
681
            $haystack,
682
            $message = '',
683
            $ignoreCase = false,
684
            $checkForObjectIdentity = true,
685
            $checkForNonObjectIdentity = false
686
        ): void {
687
            if ($haystack instanceof DBField) {
688
                $haystack = (string)$haystack;
689
            }
690
            if (is_iterable($haystack)) {
691
                $strict = is_object($needle) ? $checkForObjectIdentity : $checkForNonObjectIdentity;
692
                if ($strict) {
693
                    parent::assertNotContains($needle, $haystack, $message);
694
                } else {
695
                    parent::assertNotContainsEquals($needle, $haystack, $message);
696
                }
697
            } else {
698
                static::assertNotContainsNonIterable($needle, $haystack, $message, $ignoreCase);
699
            }
700
        }
701
702
        protected static function assertNotContainsNonIterable(
703
            $needle,
704
            $haystack,
705
            $message = '',
706
            $ignoreCase = false
707
        ): void {
708
            if ($haystack instanceof DBField) {
709
                $haystack = (string)$haystack;
710
            }
711
            if ($ignoreCase) {
712
                parent::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message);
713
            } else {
714
                parent::assertStringNotContainsString($needle, $haystack, $message);
715
            }
716
        }
717
718
        /**
719
         * Backwards compatibility for core tests
720
         */
721
        public static function assertInternalType($expected, $actual, $message = '')
722
        {
723
            switch ($expected) {
724
                case 'numeric':
725
                    static::assertIsNumeric($actual, $message);
726
                    return;
727
                case 'integer':
728
                case 'int':
729
                    static::assertIsInt($actual, $message);
730
                    return;
731
                case 'double':
732
                case 'float':
733
                case 'real':
734
                    static::assertIsFloat($actual, $message);
735
                    return;
736
                case 'string':
737
                    static::assertIsString($actual, $message);
738
                    return;
739
                case 'boolean':
740
                case 'bool':
741
                    static::assertIsBool($actual, $message);
742
                    return;
743
                case 'null':
744
                    static::assertTrue(is_null($actual), $message);
745
                    return;
746
                case 'array':
747
                    static::assertIsArray($actual, $message);
748
                    return;
749
                case 'object':
750
                    static::assertIsObject($actual, $message);
751
                    return;
752
                case 'resource':
753
                    static::assertIsResource($actual, $message);
754
                    return;
755
                case 'resource (closed)':
756
                    static::assertIsClosedResource($actual, $message);
757
                    return;
758
                case 'scalar':
759
                    static::assertIsScalar($actual, $message);
760
                    return;
761
                case 'callable':
762
                    static::assertIsCallable($actual, $message);
763
                    return;
764
                case 'iterable':
765
                    static::assertIsIterable($actual, $message);
766
                    return;
767
                default:
768
                    return false;
769
            }
770
        }
771
772
        /**
773
         * Clear the log of emails sent
774
         *
775
         * @return bool True if emails cleared
776
         */
777
        public function clearEmails()
778
        {
779
            /** @var Mailer $mailer */
780
            $mailer = Injector::inst()->get(Mailer::class);
781
            if ($mailer instanceof TestMailer) {
782
                $mailer->clearEmails();
783
                return true;
784
            }
785
            return false;
786
        }
787
788
        /**
789
         * Search for an email that was sent.
790
         * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
791
         * @param string $to
792
         * @param string $from
793
         * @param string $subject
794
         * @param string $content
795
         * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
796
         *               'HtmlContent'
797
         */
798
        public static function findEmail($to, $from = null, $subject = null, $content = null)
799
        {
800
            /** @var Mailer $mailer */
801
            $mailer = Injector::inst()->get(Mailer::class);
802
            if ($mailer instanceof TestMailer) {
803
                return $mailer->findEmail($to, $from, $subject, $content);
804
            }
805
            return null;
806
        }
807
808
        /**
809
         * Assert that the matching email was sent since the last call to clearEmails()
810
         * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
811
         *
812
         * @param string $to
813
         * @param string $from
814
         * @param string $subject
815
         * @param string $content
816
         */
817
        public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
818
        {
819
            $found = (bool)static::findEmail($to, $from, $subject, $content);
820
821
            $infoParts = '';
822
            $withParts = [];
823
            if ($to) {
824
                $infoParts .= " to '$to'";
825
            }
826
            if ($from) {
827
                $infoParts .= " from '$from'";
828
            }
829
            if ($subject) {
830
                $withParts[] = "subject '$subject'";
831
            }
832
            if ($content) {
833
                $withParts[] = "content '$content'";
834
            }
835
            if ($withParts) {
836
                $infoParts .= ' with ' . implode(' and ', $withParts);
837
            }
838
839
            static::assertTrue(
840
                $found,
841
                "Failed asserting that an email was sent$infoParts."
842
            );
843
        }
844
845
846
        /**
847
         * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
848
         * pairs.  Each match must correspond to 1 distinct record.
849
         *
850
         * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
851
         * either pass a single pattern or an array of patterns.
852
         * @param SS_List $list The {@link SS_List} to test.
853
         * @param string $message
854
         *
855
         * Examples
856
         * --------
857
         * Check that $members includes an entry with Email = [email protected]:
858
         *      $this->assertListContains(['Email' => '[email protected]'], $members);
859
         *
860
         * Check that $members includes entries with Email = [email protected] and with
861
         * Email = [email protected]:
862
         *      $this->assertListContains([
863
         *         ['Email' => '[email protected]'],
864
         *         ['Email' => '[email protected]'],
865
         *      ], $members);
866
         */
867
        public static function assertListContains($matches, SS_List $list, $message = '')
868
        {
869
            if (!is_array($matches)) {
870
                throw self::createPHPUnitFrameworkException(
871
                    1,
872
                    'array'
873
                );
874
            }
875
876
            static::assertThat(
877
                $list,
878
                new SSListContains(
879
                    $matches
880
                ),
881
                $message
882
            );
883
        }
884
885
        /**
886
         * @param $matches
887
         * @param $dataObjectSet
888
         * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
889
         *
890
         */
891
        public function assertDOSContains($matches, $dataObjectSet)
892
        {
893
            Deprecation::notice('5.0', 'Use assertListContains() instead');
894
            static::assertListContains($matches, $dataObjectSet);
895
        }
896
897
        /**
898
         * Asserts that no items in a given list appear in the given dataobject list
899
         *
900
         * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
901
         * either pass a single pattern or an array of patterns.
902
         * @param SS_List $list The {@link SS_List} to test.
903
         * @param string $message
904
         *
905
         * Examples
906
         * --------
907
         * Check that $members doesn't have an entry with Email = [email protected]:
908
         *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
909
         *
910
         * Check that $members doesn't have entries with Email = [email protected] and with
911
         * Email = [email protected]:
912
         *      $this->assertListNotContains([
913
         *          ['Email' => '[email protected]'],
914
         *          ['Email' => '[email protected]'],
915
         *      ], $members);
916
         */
917
        public static function assertListNotContains($matches, SS_List $list, $message = '')
918
        {
919
            if (!is_array($matches)) {
920
                throw self::createPHPUnitFrameworkException(
921
                    1,
922
                    'array'
923
                );
924
            }
925
926
            $constraint = new LogicalNot(
927
                new SSListContains(
928
                    $matches
929
                )
930
            );
931
932
            static::assertThat(
933
                $list,
934
                $constraint,
935
                $message
936
            );
937
        }
938
939
        /**
940
         * @param $matches
941
         * @param $dataObjectSet
942
         * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
943
         *
944
         */
945
        public static function assertNotDOSContains($matches, $dataObjectSet)
946
        {
947
            Deprecation::notice('5.0', 'Use assertListNotContains() instead');
948
            static::assertListNotContains($matches, $dataObjectSet);
949
        }
950
951
        /**
952
         * Assert that the given {@link SS_List} includes only DataObjects matching the given
953
         * key-value pairs.  Each match must correspond to 1 distinct record.
954
         *
955
         * Example
956
         * --------
957
         * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
958
         * matter:
959
         *     $this->assertListEquals([
960
         *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
961
         *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
962
         *      ], $members);
963
         *
964
         * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
965
         * either pass a single pattern or an array of patterns.
966
         * @param mixed $list The {@link SS_List} to test.
967
         * @param string $message
968
         */
969
        public static function assertListEquals($matches, SS_List $list, $message = '')
970
        {
971
            if (!is_array($matches)) {
972
                throw self::createPHPUnitFrameworkException(
973
                    1,
974
                    'array'
975
                );
976
            }
977
978
            static::assertThat(
979
                $list,
980
                new SSListContainsOnly(
981
                    $matches
982
                ),
983
                $message
984
            );
985
        }
986
987
        /**
988
         * @param $matches
989
         * @param SS_List $dataObjectSet
990
         * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
991
         *
992
         */
993
        public function assertDOSEquals($matches, $dataObjectSet)
994
        {
995
            Deprecation::notice('5.0', 'Use assertListEquals() instead');
996
            static::assertListEquals($matches, $dataObjectSet);
997
        }
998
999
1000
        /**
1001
         * Assert that the every record in the given {@link SS_List} matches the given key-value
1002
         * pairs.
1003
         *
1004
         * Example
1005
         * --------
1006
         * Check that every entry in $members has a Status of 'Active':
1007
         *     $this->assertListAllMatch(['Status' => 'Active'], $members);
1008
         *
1009
         * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
1010
         * @param mixed $list The {@link SS_List} to test.
1011
         * @param string $message
1012
         */
1013
        public static function assertListAllMatch($match, SS_List $list, $message = '')
1014
        {
1015
            if (!is_array($match)) {
1016
                throw self::createPHPUnitFrameworkException(
1017
                    1,
1018
                    'array'
1019
                );
1020
            }
1021
1022
            static::assertThat(
1023
                $list,
1024
                new SSListContainsOnlyMatchingItems(
1025
                    $match
1026
                ),
1027
                $message
1028
            );
1029
        }
1030
1031
        /**
1032
         * @param $match
1033
         * @param SS_List $dataObjectSet
1034
         * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
1035
         *
1036
         */
1037
        public function assertDOSAllMatch($match, SS_List $dataObjectSet)
1038
        {
1039
            Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
1040
            static::assertListAllMatch($match, $dataObjectSet);
1041
        }
1042
1043
        /**
1044
         * Removes sequences of repeated whitespace characters from SQL queries
1045
         * making them suitable for string comparison
1046
         *
1047
         * @param string $sql
1048
         * @return string The cleaned and normalised SQL string
1049
         */
1050
        protected static function normaliseSQL($sql)
1051
        {
1052
            return trim(preg_replace('/\s+/m', ' ', $sql));
1053
        }
1054
1055
        /**
1056
         * Asserts that two SQL queries are equivalent
1057
         *
1058
         * @param string $expectedSQL
1059
         * @param string $actualSQL
1060
         * @param string $message
1061
         * @param float|int $delta
1062
         * @param integer $maxDepth
1063
         * @param boolean $canonicalize
1064
         * @param boolean $ignoreCase
1065
         */
1066
        public static function assertSQLEquals(
1067
            $expectedSQL,
1068
            $actualSQL,
1069
            $message = ''
1070
        ) {
1071
            // Normalise SQL queries to remove patterns of repeating whitespace
1072
            $expectedSQL = static::normaliseSQL($expectedSQL);
1073
            $actualSQL = static::normaliseSQL($actualSQL);
1074
1075
            static::assertEquals($expectedSQL, $actualSQL, $message);
1076
        }
1077
1078
        /**
1079
         * Asserts that a SQL query contains a SQL fragment
1080
         *
1081
         * @param string $needleSQL
1082
         * @param string $haystackSQL
1083
         * @param string $message
1084
         * @param boolean $ignoreCase
1085
         * @param boolean $checkForObjectIdentity
1086
         */
1087
        public static function assertSQLContains(
1088
            $needleSQL,
1089
            $haystackSQL,
1090
            $message = '',
1091
            $ignoreCase = false,
1092
            $checkForObjectIdentity = true
1093
        ) {
1094
            $needleSQL = static::normaliseSQL($needleSQL);
1095
            $haystackSQL = static::normaliseSQL($haystackSQL);
1096
            if (is_iterable($haystackSQL)) {
1097
                /** @var iterable $iterableHaystackSQL */
1098
                $iterableHaystackSQL = $haystackSQL;
1099
                static::assertContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1100
            } else {
1101
                static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
1102
            }
1103
        }
1104
1105
        /**
1106
         * Asserts that a SQL query contains a SQL fragment
1107
         *
1108
         * @param string $needleSQL
1109
         * @param string $haystackSQL
1110
         * @param string $message
1111
         * @param boolean $ignoreCase
1112
         * @param boolean $checkForObjectIdentity
1113
         */
1114
        public static function assertSQLNotContains(
1115
            $needleSQL,
1116
            $haystackSQL,
1117
            $message = '',
1118
            $ignoreCase = false,
1119
            $checkForObjectIdentity = true
1120
        ) {
1121
            $needleSQL = static::normaliseSQL($needleSQL);
1122
            $haystackSQL = static::normaliseSQL($haystackSQL);
1123
            if (is_iterable($haystackSQL)) {
1124
                /** @var iterable $iterableHaystackSQL */
1125
                $iterableHaystackSQL = $haystackSQL;
1126
                static::assertNotContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1127
            } else {
1128
                static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
1129
            }
1130
        }
1131
1132
        /**
1133
         * Start test environment
1134
         */
1135
        public static function start()
1136
        {
1137
            if (static::is_running_test()) {
1138
                return;
1139
            }
1140
1141
            // Health check
1142
            if (InjectorLoader::inst()->countManifests()) {
1143
                throw new LogicException('SapphireTest::start() cannot be called within another application');
1144
            }
1145
            static::set_is_running_test(true);
1146
1147
            // Test application
1148
            $kernel = new TestKernel(BASE_PATH);
1149
1150
            if (class_exists(HTTPApplication::class)) {
1151
                // Mock request
1152
                $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1153
                $request = CLIRequestBuilder::createFromEnvironment();
1154
1155
                $app = new HTTPApplication($kernel);
1156
                $flush = array_key_exists('flush', $request->getVars());
1157
1158
                // Custom application
1159
                $res = $app->execute($request, function (HTTPRequest $request) {
1160
                    // Start session and execute
1161
                    $request->getSession()->init($request);
1162
1163
                    // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1164
                    // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1165
                    DataObject::reset();
1166
1167
                    // Set dummy controller;
1168
                    $controller = Controller::create();
1169
                    $controller->setRequest($request);
1170
                    $controller->pushCurrent();
1171
                    $controller->doInit();
1172
                }, $flush);
1173
1174
                if ($res && $res->isError()) {
1175
                    throw new LogicException($res->getBody());
1176
                }
1177
            } else {
1178
                // Allow flush from the command line in the absence of HTTPApplication's special sauce
1179
                $flush = false;
1180
                foreach ($_SERVER['argv'] as $arg) {
1181
                    if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
1182
                        $flush = true;
1183
                    }
1184
                }
1185
                $kernel->boot($flush);
1186
            }
1187
1188
            // Register state
1189
            static::$state = SapphireTestState::singleton();
1190
            // Register temp DB holder
1191
            static::tempDB();
1192
        }
1193
1194
        /**
1195
         * Reset the testing database's schema, but only if it is active
1196
         * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1197
         * @param bool $forceCreate Force DB to be created if it doesn't exist
1198
         */
1199
        public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
1200
        {
1201
            if (!static::$tempDB) {
1202
                return;
1203
            }
1204
1205
            // Check if DB is active before reset
1206
            if (!static::$tempDB->isUsed()) {
1207
                if (!$forceCreate) {
1208
                    return;
1209
                }
1210
                static::$tempDB->build();
1211
            }
1212
            $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1213
            static::$tempDB->resetDBSchema((array)$extraDataObjects);
1214
        }
1215
1216
        /**
1217
         * A wrapper for automatically performing callbacks as a user with a specific permission
1218
         *
1219
         * @param string|array $permCode
1220
         * @param callable $callback
1221
         * @return mixed
1222
         */
1223
        public function actWithPermission($permCode, $callback)
1224
        {
1225
            return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1226
        }
1227
1228
        /**
1229
         * Create Member and Group objects on demand with specific permission code
1230
         *
1231
         * @param string|array $permCode
1232
         * @return Member
1233
         */
1234
        protected function createMemberWithPermission($permCode)
1235
        {
1236
            if (is_array($permCode)) {
1237
                $permArray = $permCode;
1238
                $permCode = implode('.', $permCode);
1239
            } else {
1240
                $permArray = [$permCode];
1241
            }
1242
1243
            // Check cached member
1244
            if (isset($this->cache_generatedMembers[$permCode])) {
1245
                $member = $this->cache_generatedMembers[$permCode];
1246
            } else {
1247
                // Generate group with these permissions
1248
                $group = Group::create();
1249
                $group->Title = "$permCode group";
1250
                $group->write();
1251
1252
                // Create each individual permission
1253
                foreach ($permArray as $permArrayItem) {
1254
                    $permission = Permission::create();
1255
                    $permission->Code = $permArrayItem;
1256
                    $permission->write();
1257
                    $group->Permissions()->add($permission);
1258
                }
1259
1260
                $member = Member::get()->filter([
1261
                    'Email' => "[email protected]",
1262
                ])->first();
1263
                if (!$member) {
1264
                    $member = Member::create();
1265
                }
1266
1267
                $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...
1268
                $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...
1269
                $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...
1270
                $member->write();
1271
                $group->Members()->add($member);
1272
1273
                $this->cache_generatedMembers[$permCode] = $member;
1274
            }
1275
            return $member;
1276
        }
1277
1278
        /**
1279
         * Create a member and group with the given permission code, and log in with it.
1280
         * Returns the member ID.
1281
         *
1282
         * @param string|array $permCode Either a permission, or list of permissions
1283
         * @return int Member ID
1284
         */
1285
        public function logInWithPermission($permCode = 'ADMIN')
1286
        {
1287
            $member = $this->createMemberWithPermission($permCode);
1288
            $this->logInAs($member);
1289
            return $member->ID;
1290
        }
1291
1292
        /**
1293
         * Log in as the given member
1294
         *
1295
         * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1296
         */
1297
        public function logInAs($member)
1298
        {
1299
            if (is_numeric($member)) {
1300
                $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

1300
                $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1301
            } elseif (!is_object($member)) {
1302
                $member = $this->objFromFixture(Member::class, $member);
1303
            }
1304
            Injector::inst()->get(IdentityStore::class)->logIn($member);
1305
        }
1306
1307
        /**
1308
         * Log out the current user
1309
         */
1310
        public function logOut()
1311
        {
1312
            /** @var IdentityStore $store */
1313
            $store = Injector::inst()->get(IdentityStore::class);
1314
            $store->logOut();
1315
        }
1316
1317
        /**
1318
         * Cache for logInWithPermission()
1319
         */
1320
        protected $cache_generatedMembers = [];
1321
1322
        /**
1323
         * Test against a theme.
1324
         *
1325
         * @param string $themeBaseDir themes directory
1326
         * @param string $theme Theme name
1327
         * @param callable $callback
1328
         * @throws Exception
1329
         */
1330
        protected function useTestTheme($themeBaseDir, $theme, $callback)
1331
        {
1332
            Config::nest();
1333
            if (strpos($themeBaseDir, BASE_PATH) === 0) {
1334
                $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1335
            }
1336
            SSViewer::config()->update('theme_enabled', true);
1337
            SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1338
1339
            try {
1340
                $callback();
1341
            } finally {
1342
                Config::unnest();
1343
            }
1344
        }
1345
1346
        /**
1347
         * Get fixture paths for this test
1348
         *
1349
         * @return array List of paths
1350
         */
1351
        protected function getFixturePaths()
1352
        {
1353
            $fixtureFile = static::get_fixture_file();
1354
            if (empty($fixtureFile)) {
1355
                return [];
1356
            }
1357
1358
            $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1359
1360
            return array_map(function ($fixtureFilePath) {
1361
                return $this->resolveFixturePath($fixtureFilePath);
1362
            }, $fixtureFiles);
1363
        }
1364
1365
        /**
1366
         * Return all extra objects to scaffold for this test
1367
         * @return array
1368
         */
1369
        public static function getExtraDataObjects()
1370
        {
1371
            return static::$extra_dataobjects;
1372
        }
1373
1374
        /**
1375
         * Get additional controller classes to register routes for
1376
         *
1377
         * @return array
1378
         */
1379
        public static function getExtraControllers()
1380
        {
1381
            return static::$extra_controllers;
1382
        }
1383
1384
        /**
1385
         * Map a fixture path to a physical file
1386
         *
1387
         * @param string $fixtureFilePath
1388
         * @return string
1389
         */
1390
        protected function resolveFixturePath($fixtureFilePath)
1391
        {
1392
            // support loading via composer name path.
1393
            if (strpos($fixtureFilePath, ':') !== false) {
1394
                return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1395
            }
1396
1397
            // Support fixture paths relative to the test class, rather than relative to webroot
1398
            // String checking is faster than file_exists() calls.
1399
            $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1400
            if ($resolvedPath) {
1401
                return $resolvedPath;
1402
            }
1403
1404
            // Check if file exists relative to base dir
1405
            $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1406
            if ($resolvedPath) {
1407
                return $resolvedPath;
1408
            }
1409
1410
            return $fixtureFilePath;
1411
        }
1412
1413
        protected function setUpRoutes()
1414
        {
1415
            if (!class_exists(Director::class)) {
1416
                return;
1417
            }
1418
1419
            // Get overridden routes
1420
            $rules = $this->getExtraRoutes();
1421
1422
            // Add all other routes
1423
            foreach (Director::config()->uninherited('rules') as $route => $rule) {
1424
                if (!isset($rules[$route])) {
1425
                    $rules[$route] = $rule;
1426
                }
1427
            }
1428
1429
            // Add default catch-all rule
1430
            $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1431
1432
            // Add controller-name auto-routing
1433
            Director::config()->set('rules', $rules);
1434
        }
1435
1436
        /**
1437
         * Get extra routes to merge into Director.rules
1438
         *
1439
         * @return array
1440
         */
1441
        protected function getExtraRoutes()
1442
        {
1443
            $rules = [];
1444
            foreach ($this->getExtraControllers() as $class) {
1445
                $controllerInst = Controller::singleton($class);
1446
                $link = Director::makeRelative($controllerInst->Link());
1447
                $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1448
                $rules[$route] = $class;
1449
            }
1450
            return $rules;
1451
        }
1452
1453
        /**
1454
         * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory()
1455
         *
1456
         * @param $argument
1457
         * @param $type
1458
         * @param $value
1459
         */
1460
        public static function createPHPUnitFrameworkException($argument, $type, $value = null)
1461
        {
1462
            $stack = debug_backtrace(false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $options of debug_backtrace(). ( Ignorable by Annotation )

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

1462
            $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1463
1464
            return new PHPUnitFrameworkException(
1465
                sprintf(
1466
                    'Argument #%d%sof %s::%s() must be a %s',
1467
                    $argument,
1468
                    $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1469
                    $stack[1]['class'],
1470
                    $stack[1]['function'],
1471
                    $type
1472
                )
1473
            );
1474
        }
1475
1476
        /**
1477
         * Returns the annotations for this test.
1478
         *
1479
         * @return array
1480
         */
1481
        public function getAnnotations()
1482
        {
1483
            return TestUtil::parseTestMethodAnnotations(
1484
                get_class($this),
1485
                $this->getName(false)
1486
            );
1487
        }
1488
    }
1489
}
1490