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

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

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

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

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

1449
            $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1450
1451
            return new PHPUnitFrameworkException(
1452
                sprintf(
1453
                    'Argument #%d%sof %s::%s() must be a %s',
1454
                    $argument,
1455
                    $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1456
                    $stack[1]['class'],
1457
                    $stack[1]['function'],
1458
                    $type
1459
                )
1460
            );
1461
        }
1462
1463
        /**
1464
         * Returns the annotations for this test.
1465
         *
1466
         * @return array
1467
         */
1468
        public function getAnnotations()
1469
        {
1470
            return TestUtil::parseTestMethodAnnotations(
1471
                get_class($this),
1472
                $this->getName(false)
1473
            );
1474
        }
1475
    }
1476
}
1477