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

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

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

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

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