Passed
Pull Request — 4 (#10028)
by Steve
07:55
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
9
// use PHPUnit_Framework_Constraint_Not;
10
use PHPUnit\Framework\Constraint\LogicalNot;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Framework\Constraint\LogicalNot 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...
11
// use PHPUnit_Framework_TestCase;
12
use PHPUnit\Framework\TestCase;
13
// use PHPUnit_Util_InvalidArgumentHelper;
14
use PHPUnit\Framework\Exception as PHPUnitFrameworkException;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Framework\Exception was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

549
        $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...
550
    }
551
552
    /**
553
     * Clear all fixtures which were previously loaded through
554
     * {@link loadFixture()}
555
     */
556
    public function clearFixtures()
557
    {
558
        /** @var FixtureTestState $state */
559
        $state = static::$state->getStateByName('fixtures');
560
        $state->getFixtureFactory(static::class)->clear();
561
    }
562
563
    /**
564
     * Useful for writing unit tests without hardcoding folder structures.
565
     *
566
     * @return string Absolute path to current class.
567
     */
568
    protected function getCurrentAbsolutePath()
569
    {
570
        $filename = ClassLoader::inst()->getItemPath(static::class);
571
        if (!$filename) {
572
            throw new LogicException('getItemPath returned null for ' . static::class
573
                . '. Try adding flush=1 to the test run.');
574
        }
575
        return dirname($filename);
576
    }
577
578
    /**
579
     * @return string File path relative to webroot
580
     */
581
    protected function getCurrentRelativePath()
582
    {
583
        $base = Director::baseFolder();
584
        $path = $this->getCurrentAbsolutePath();
585
        if (substr($path, 0, strlen($base)) == $base) {
586
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
587
        }
588
        return $path;
589
    }
590
591
    /**
592
     * Setup  the test.
593
     * Always sets up in order:
594
     *  - Custom state helpers
595
     *  - Unnest
596
     *  - Reset php state
597
     *
598
     * User code should call parent::tearDown() after custom tear down code
599
     */
600
    protected function tearDown(): void
601
    {
602
        // Reset mocked datetime
603
        if (class_exists(DBDatetime::class)) {
604
            DBDatetime::clear_mock_now();
605
        }
606
607
        // Stop the redirection that might have been requested in the test.
608
        // Note: Ideally a clean Controller should be created for each test.
609
        // Now all tests executed in a batch share the same controller.
610
        if (class_exists(Controller::class)) {
611
            $controller = Controller::has_curr() ? Controller::curr() : null;
612
            if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
613
                $response->setStatusCode(200);
614
                $response->removeHeader('Location');
615
            }
616
        }
617
618
        // Call state helpers
619
        static::$state->tearDown($this);
620
    }
621
622
    public static function assertContains(
623
        $needle,
624
        $haystack,
625
        $message = '',
626
        $ignoreCase = false,
627
        $checkForObjectIdentity = true,
628
        $checkForNonObjectIdentity = false
629
    ): void {
630
        if ($haystack instanceof DBField) {
631
            $haystack = (string)$haystack;
632
        }
633
        if (is_iterable($haystack)) {
634
            $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity;
635
            if ($strict) {
636
                parent::assertContains($needle, $haystack, $message);
637
            } else {
638
                parent::assertContainsEquals($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertContainsEquals() does not exist on PHPUnit\Framework\TestCase. Did you maybe mean assertContains()? ( Ignorable by Annotation )

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

638
                parent::/** @scrutinizer ignore-call */ 
639
                        assertContainsEquals($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
639
            }
640
        } else {
641
            static::assertContainsNonIterable($needle, $haystack, $message, $ignoreCase);
642
        }
643
    }
644
645
    public static function assertContainsNonIterable(
646
        $needle,
647
        $haystack,
648
        $message = '',
649
        $ignoreCase = false
650
    ): void {
651
        if ($haystack instanceof DBField) {
652
            $haystack = (string)$haystack;
653
        }
654
        if ($ignoreCase) {
655
            parent::assertStringContainsStringIgnoringCase($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringContainsStringIgnoringCase() does not exist on PHPUnit\Framework\TestCase. ( Ignorable by Annotation )

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

655
            parent::/** @scrutinizer ignore-call */ 
656
                    assertStringContainsStringIgnoringCase($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
656
        } else {
657
            parent::assertStringContainsString($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringContainsString() does not exist on PHPUnit\Framework\TestCase. ( Ignorable by Annotation )

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

657
            parent::/** @scrutinizer ignore-call */ 
658
                    assertStringContainsString($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
658
        }
659
    }
660
661
    public static function assertNotContains(
662
        $needle,
663
        $haystack,
664
        $message = '',
665
        $ignoreCase = false,
666
        $checkForObjectIdentity = true,
667
        $checkForNonObjectIdentity = false
668
    ): void {
669
        if ($haystack instanceof DBField) {
670
            $haystack = (string)$haystack;
671
        }
672
        if (is_iterable($haystack)) {
673
            $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity;
674
            if ($strict) {
675
                parent::assertNotContains($needle, $haystack, $message);
676
            } else {
677
                parent::assertNotContainsEquals($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertNotContainsEquals() does not exist on PHPUnit\Framework\TestCase. Did you maybe mean assertNotContains()? ( Ignorable by Annotation )

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

677
                parent::/** @scrutinizer ignore-call */ 
678
                        assertNotContainsEquals($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
678
            }
679
        } else {
680
            static::assertNotContainsNonIterable($needle, $haystack, $message, $ignoreCase);
681
        }
682
    }
683
684
    protected static function assertNotContainsNonIterable(
685
        $needle,
686
        $haystack,
687
        $message = '',
688
        $ignoreCase = false
689
    ): void {
690
        if ($haystack instanceof DBField) {
691
            $haystack = (string)$haystack;
692
        }
693
        if ($ignoreCase) {
694
            parent::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringNotContainsStringIgnoringCase() does not exist on PHPUnit\Framework\TestCase. ( Ignorable by Annotation )

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

694
            parent::/** @scrutinizer ignore-call */ 
695
                    assertStringNotContainsStringIgnoringCase($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
695
        } else {
696
            parent::assertStringNotContainsString($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringNotContainsString() does not exist on PHPUnit\Framework\TestCase. Did you maybe mean assertStringNotMatchesFormat()? ( Ignorable by Annotation )

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

696
            parent::/** @scrutinizer ignore-call */ 
697
                    assertStringNotContainsString($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
697
        }
698
    }
699
700
    /**
701
     * Clear the log of emails sent
702
     *
703
     * @return bool True if emails cleared
704
     */
705
    public function clearEmails()
706
    {
707
        /** @var Mailer $mailer */
708
        $mailer = Injector::inst()->get(Mailer::class);
709
        if ($mailer instanceof TestMailer) {
710
            $mailer->clearEmails();
711
            return true;
712
        }
713
        return false;
714
    }
715
716
    /**
717
     * Search for an email that was sent.
718
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
719
     * @param string $to
720
     * @param string $from
721
     * @param string $subject
722
     * @param string $content
723
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
724
     *               'HtmlContent'
725
     */
726
    public static function findEmail($to, $from = null, $subject = null, $content = null)
727
    {
728
        /** @var Mailer $mailer */
729
        $mailer = Injector::inst()->get(Mailer::class);
730
        if ($mailer instanceof TestMailer) {
731
            return $mailer->findEmail($to, $from, $subject, $content);
732
        }
733
        return null;
734
    }
735
736
    /**
737
     * Assert that the matching email was sent since the last call to clearEmails()
738
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
739
     *
740
     * @param string $to
741
     * @param string $from
742
     * @param string $subject
743
     * @param string $content
744
     */
745
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
746
    {
747
        $found = (bool)static::findEmail($to, $from, $subject, $content);
748
749
        $infoParts = '';
750
        $withParts = [];
751
        if ($to) {
752
            $infoParts .= " to '$to'";
753
        }
754
        if ($from) {
755
            $infoParts .= " from '$from'";
756
        }
757
        if ($subject) {
758
            $withParts[] = "subject '$subject'";
759
        }
760
        if ($content) {
761
            $withParts[] = "content '$content'";
762
        }
763
        if ($withParts) {
764
            $infoParts .= ' with ' . implode(' and ', $withParts);
765
        }
766
767
        static::assertTrue(
768
            $found,
769
            "Failed asserting that an email was sent$infoParts."
770
        );
771
    }
772
773
774
    /**
775
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
776
     * pairs.  Each match must correspond to 1 distinct record.
777
     *
778
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
779
     * either pass a single pattern or an array of patterns.
780
     * @param SS_List $list The {@link SS_List} to test.
781
     * @param string $message
782
     *
783
     * Examples
784
     * --------
785
     * Check that $members includes an entry with Email = [email protected]:
786
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
787
     *
788
     * Check that $members includes entries with Email = [email protected] and with
789
     * Email = [email protected]:
790
     *      $this->assertListContains([
791
     *         ['Email' => '[email protected]'],
792
     *         ['Email' => '[email protected]'],
793
     *      ], $members);
794
     */
795
    public static function assertListContains($matches, SS_List $list, $message = '')
796
    {
797
        if (!is_array($matches)) {
798
            throw self::createPHPUnitFrameworkException(
799
                1,
800
                'array'
801
            );
802
        }
803
804
        static::assertThat(
805
            $list,
806
            new SSListContains(
807
                $matches
808
            ),
809
            $message
810
        );
811
    }
812
813
    /**
814
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
815
     *
816
     * @param $matches
817
     * @param $dataObjectSet
818
     */
819
    public function assertDOSContains($matches, $dataObjectSet)
820
    {
821
        Deprecation::notice('5.0', 'Use assertListContains() instead');
822
        static::assertListContains($matches, $dataObjectSet);
823
    }
824
825
    /**
826
     * Asserts that no items in a given list appear in the given dataobject list
827
     *
828
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
829
     * either pass a single pattern or an array of patterns.
830
     * @param SS_List $list The {@link SS_List} to test.
831
     * @param string $message
832
     *
833
     * Examples
834
     * --------
835
     * Check that $members doesn't have an entry with Email = [email protected]:
836
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
837
     *
838
     * Check that $members doesn't have entries with Email = [email protected] and with
839
     * Email = [email protected]:
840
     *      $this->assertListNotContains([
841
     *          ['Email' => '[email protected]'],
842
     *          ['Email' => '[email protected]'],
843
     *      ], $members);
844
     */
845
    public static function assertListNotContains($matches, SS_List $list, $message = '')
846
    {
847
        if (!is_array($matches)) {
848
            throw self::createPHPUnitFrameworkException(
849
                1,
850
                'array'
851
            );
852
        }
853
854
        $constraint =  new LogicalNot(
855
            new SSListContains(
856
                $matches
857
            )
858
        );
859
860
        static::assertThat(
861
            $list,
862
            $constraint,
863
            $message
864
        );
865
    }
866
867
    /**
868
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
869
     *
870
     * @param $matches
871
     * @param $dataObjectSet
872
     */
873
    public static function assertNotDOSContains($matches, $dataObjectSet)
874
    {
875
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
876
        static::assertListNotContains($matches, $dataObjectSet);
877
    }
878
879
    /**
880
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
881
     * key-value pairs.  Each match must correspond to 1 distinct record.
882
     *
883
     * Example
884
     * --------
885
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
886
     * matter:
887
     *     $this->assertListEquals([
888
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
889
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
890
     *      ], $members);
891
     *
892
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
893
     * either pass a single pattern or an array of patterns.
894
     * @param mixed $list The {@link SS_List} to test.
895
     * @param string $message
896
     */
897
    public static function assertListEquals($matches, SS_List $list, $message = '')
898
    {
899
        if (!is_array($matches)) {
900
            throw self::createPHPUnitFrameworkException(
901
                1,
902
                'array'
903
            );
904
        }
905
906
        static::assertThat(
907
            $list,
908
            new SSListContainsOnly(
909
                $matches
910
            ),
911
            $message
912
        );
913
    }
914
915
    /**
916
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
917
     *
918
     * @param $matches
919
     * @param SS_List $dataObjectSet
920
     */
921
    public function assertDOSEquals($matches, $dataObjectSet)
922
    {
923
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
924
        static::assertListEquals($matches, $dataObjectSet);
925
    }
926
927
928
    /**
929
     * Assert that the every record in the given {@link SS_List} matches the given key-value
930
     * pairs.
931
     *
932
     * Example
933
     * --------
934
     * Check that every entry in $members has a Status of 'Active':
935
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
936
     *
937
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
938
     * @param mixed $list The {@link SS_List} to test.
939
     * @param string $message
940
     */
941
    public static function assertListAllMatch($match, SS_List $list, $message = '')
942
    {
943
        if (!is_array($match)) {
944
            throw self::createPHPUnitFrameworkException(
945
                1,
946
                'array'
947
            );
948
        }
949
950
        static::assertThat(
951
            $list,
952
            new SSListContainsOnlyMatchingItems(
953
                $match
954
            ),
955
            $message
956
        );
957
    }
958
959
    /**
960
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
961
     *
962
     * @param $match
963
     * @param SS_List $dataObjectSet
964
     */
965
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
966
    {
967
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
968
        static::assertListAllMatch($match, $dataObjectSet);
969
    }
970
971
    /**
972
     * Removes sequences of repeated whitespace characters from SQL queries
973
     * making them suitable for string comparison
974
     *
975
     * @param string $sql
976
     * @return string The cleaned and normalised SQL string
977
     */
978
    protected static function normaliseSQL($sql)
979
    {
980
        return trim(preg_replace('/\s+/m', ' ', $sql));
981
    }
982
983
    /**
984
     * Asserts that two SQL queries are equivalent
985
     *
986
     * @param string $expectedSQL
987
     * @param string $actualSQL
988
     * @param string $message
989
     * @param float|int $delta
990
     * @param integer $maxDepth
991
     * @param boolean $canonicalize
992
     * @param boolean $ignoreCase
993
     */
994
    public static function assertSQLEquals(
995
        $expectedSQL,
996
        $actualSQL,
997
        $message = ''
998
    ) {
999
        // Normalise SQL queries to remove patterns of repeating whitespace
1000
        $expectedSQL = static::normaliseSQL($expectedSQL);
1001
        $actualSQL = static::normaliseSQL($actualSQL);
1002
1003
        static::assertEquals($expectedSQL, $actualSQL, $message);
1004
    }
1005
1006
    /**
1007
     * Asserts that a SQL query contains a SQL fragment
1008
     *
1009
     * @param string $needleSQL
1010
     * @param string $haystackSQL
1011
     * @param string $message
1012
     * @param boolean $ignoreCase
1013
     * @param boolean $checkForObjectIdentity
1014
     */
1015
    public static function assertSQLContains(
1016
        $needleSQL,
1017
        $haystackSQL,
1018
        $message = '',
1019
        $ignoreCase = false,
1020
        $checkForObjectIdentity = true
1021
    ) {
1022
        $needleSQL = static::normaliseSQL($needleSQL);
1023
        $haystackSQL = static::normaliseSQL($haystackSQL);
1024
        if (is_iterable($haystackSQL)) {
1025
            /** @var iterable $iterableHaystackSQL */
1026
            $iterableHaystackSQL = $haystackSQL;
1027
            static::assertContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1028
        } else {
1029
            static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
1030
        }
1031
    }
1032
1033
    /**
1034
     * Asserts that a SQL query contains a SQL fragment
1035
     *
1036
     * @param string $needleSQL
1037
     * @param string $haystackSQL
1038
     * @param string $message
1039
     * @param boolean $ignoreCase
1040
     * @param boolean $checkForObjectIdentity
1041
     */
1042
    public static function assertSQLNotContains(
1043
        $needleSQL,
1044
        $haystackSQL,
1045
        $message = '',
1046
        $ignoreCase = false,
1047
        $checkForObjectIdentity = true
1048
    ) {
1049
        $needleSQL = static::normaliseSQL($needleSQL);
1050
        $haystackSQL = static::normaliseSQL($haystackSQL);
1051
        if (is_iterable($haystackSQL)) {
1052
            /** @var iterable $iterableHaystackSQL */
1053
            $iterableHaystackSQL = $haystackSQL;
1054
            static::assertNotContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1055
        } else {
1056
            static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
1057
        }
1058
    }
1059
1060
    /**
1061
     * Start test environment
1062
     */
1063
    public static function start()
1064
    {
1065
        if (static::is_running_test()) {
1066
            return;
1067
        }
1068
1069
        // Health check
1070
        if (InjectorLoader::inst()->countManifests()) {
1071
            throw new LogicException('SapphireTest::start() cannot be called within another application');
1072
        }
1073
        static::set_is_running_test(true);
1074
1075
        // Test application
1076
        $kernel = new TestKernel(BASE_PATH);
1077
1078
        if (class_exists(HTTPApplication::class)) {
1079
            // Mock request
1080
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1081
            $request = CLIRequestBuilder::createFromEnvironment();
1082
1083
            $app = new HTTPApplication($kernel);
1084
            $flush = array_key_exists('flush', $request->getVars());
1085
1086
            // Custom application
1087
            $res = $app->execute($request, function (HTTPRequest $request) {
1088
                // Start session and execute
1089
                $request->getSession()->init($request);
1090
1091
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1092
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1093
                DataObject::reset();
1094
1095
                // Set dummy controller;
1096
                $controller = Controller::create();
1097
                $controller->setRequest($request);
1098
                $controller->pushCurrent();
1099
                $controller->doInit();
1100
            }, $flush);
1101
1102
            if ($res && $res->isError()) {
1103
                throw new LogicException($res->getBody());
1104
            }
1105
        } else {
1106
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
1107
            $flush = false;
1108
            foreach ($_SERVER['argv'] as $arg) {
1109
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
1110
                    $flush = true;
1111
                }
1112
            }
1113
            $kernel->boot($flush);
1114
        }
1115
1116
        // Register state
1117
        static::$state = SapphireTestState::singleton();
1118
        // Register temp DB holder
1119
        static::tempDB();
1120
    }
1121
1122
    /**
1123
     * Reset the testing database's schema, but only if it is active
1124
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1125
     * @param bool $forceCreate Force DB to be created if it doesn't exist
1126
     */
1127
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
1128
    {
1129
        if (!static::$tempDB) {
1130
            return;
1131
        }
1132
1133
        // Check if DB is active before reset
1134
        if (!static::$tempDB->isUsed()) {
1135
            if (!$forceCreate) {
1136
                return;
1137
            }
1138
            static::$tempDB->build();
1139
        }
1140
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1141
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
1142
    }
1143
1144
    /**
1145
     * A wrapper for automatically performing callbacks as a user with a specific permission
1146
     *
1147
     * @param string|array $permCode
1148
     * @param callable $callback
1149
     * @return mixed
1150
     */
1151
    public function actWithPermission($permCode, $callback)
1152
    {
1153
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1154
    }
1155
1156
    /**
1157
     * Create Member and Group objects on demand with specific permission code
1158
     *
1159
     * @param string|array $permCode
1160
     * @return Member
1161
     */
1162
    protected function createMemberWithPermission($permCode)
1163
    {
1164
        if (is_array($permCode)) {
1165
            $permArray = $permCode;
1166
            $permCode = implode('.', $permCode);
1167
        } else {
1168
            $permArray = [$permCode];
1169
        }
1170
1171
        // Check cached member
1172
        if (isset($this->cache_generatedMembers[$permCode])) {
1173
            $member = $this->cache_generatedMembers[$permCode];
1174
        } else {
1175
            // Generate group with these permissions
1176
            $group = Group::create();
1177
            $group->Title = "$permCode group";
1178
            $group->write();
1179
1180
            // Create each individual permission
1181
            foreach ($permArray as $permArrayItem) {
1182
                $permission = Permission::create();
1183
                $permission->Code = $permArrayItem;
1184
                $permission->write();
1185
                $group->Permissions()->add($permission);
1186
            }
1187
1188
            $member = Member::get()->filter([
1189
                'Email' => "[email protected]",
1190
            ])->first();
1191
            if (!$member) {
1192
                $member = Member::create();
1193
            }
1194
1195
            $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...
1196
            $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...
1197
            $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...
1198
            $member->write();
1199
            $group->Members()->add($member);
1200
1201
            $this->cache_generatedMembers[$permCode] = $member;
1202
        }
1203
        return $member;
1204
    }
1205
1206
    /**
1207
     * Create a member and group with the given permission code, and log in with it.
1208
     * Returns the member ID.
1209
     *
1210
     * @param string|array $permCode Either a permission, or list of permissions
1211
     * @return int Member ID
1212
     */
1213
    public function logInWithPermission($permCode = 'ADMIN')
1214
    {
1215
        $member = $this->createMemberWithPermission($permCode);
1216
        $this->logInAs($member);
1217
        return $member->ID;
1218
    }
1219
1220
    /**
1221
     * Log in as the given member
1222
     *
1223
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1224
     */
1225
    public function logInAs($member)
1226
    {
1227
        if (is_numeric($member)) {
1228
            $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

1228
            $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1229
        } elseif (!is_object($member)) {
1230
            $member = $this->objFromFixture(Member::class, $member);
1231
        }
1232
        Injector::inst()->get(IdentityStore::class)->logIn($member);
1233
    }
1234
1235
    /**
1236
     * Log out the current user
1237
     */
1238
    public function logOut()
1239
    {
1240
        /** @var IdentityStore $store */
1241
        $store = Injector::inst()->get(IdentityStore::class);
1242
        $store->logOut();
1243
    }
1244
1245
    /**
1246
     * Cache for logInWithPermission()
1247
     */
1248
    protected $cache_generatedMembers = [];
1249
1250
    /**
1251
     * Test against a theme.
1252
     *
1253
     * @param string $themeBaseDir themes directory
1254
     * @param string $theme Theme name
1255
     * @param callable $callback
1256
     * @throws Exception
1257
     */
1258
    protected function useTestTheme($themeBaseDir, $theme, $callback)
1259
    {
1260
        Config::nest();
1261
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1262
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1263
        }
1264
        SSViewer::config()->update('theme_enabled', true);
1265
        SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1266
1267
        try {
1268
            $callback();
1269
        } finally {
1270
            Config::unnest();
1271
        }
1272
    }
1273
1274
    /**
1275
     * Get fixture paths for this test
1276
     *
1277
     * @return array List of paths
1278
     */
1279
    protected function getFixturePaths()
1280
    {
1281
        $fixtureFile = static::get_fixture_file();
1282
        if (empty($fixtureFile)) {
1283
            return [];
1284
        }
1285
1286
        $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1287
1288
        return array_map(function ($fixtureFilePath) {
1289
            return $this->resolveFixturePath($fixtureFilePath);
1290
        }, $fixtureFiles);
1291
    }
1292
1293
    /**
1294
     * Return all extra objects to scaffold for this test
1295
     * @return array
1296
     */
1297
    public static function getExtraDataObjects()
1298
    {
1299
        return static::$extra_dataobjects;
1300
    }
1301
1302
    /**
1303
     * Get additional controller classes to register routes for
1304
     *
1305
     * @return array
1306
     */
1307
    public static function getExtraControllers()
1308
    {
1309
        return static::$extra_controllers;
1310
    }
1311
1312
    /**
1313
     * Map a fixture path to a physical file
1314
     *
1315
     * @param string $fixtureFilePath
1316
     * @return string
1317
     */
1318
    protected function resolveFixturePath($fixtureFilePath)
1319
    {
1320
        // support loading via composer name path.
1321
        if (strpos($fixtureFilePath, ':') !== false) {
1322
            return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1323
        }
1324
1325
        // Support fixture paths relative to the test class, rather than relative to webroot
1326
        // String checking is faster than file_exists() calls.
1327
        $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1328
        if ($resolvedPath) {
1329
            return $resolvedPath;
1330
        }
1331
1332
        // Check if file exists relative to base dir
1333
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1334
        if ($resolvedPath) {
1335
            return $resolvedPath;
1336
        }
1337
1338
        return $fixtureFilePath;
1339
    }
1340
1341
    protected function setUpRoutes()
1342
    {
1343
        if (!class_exists(Director::class)) {
1344
            return;
1345
        }
1346
1347
        // Get overridden routes
1348
        $rules = $this->getExtraRoutes();
1349
1350
        // Add all other routes
1351
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
1352
            if (!isset($rules[$route])) {
1353
                $rules[$route] = $rule;
1354
            }
1355
        }
1356
1357
        // Add default catch-all rule
1358
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1359
1360
        // Add controller-name auto-routing
1361
        Director::config()->set('rules', $rules);
1362
    }
1363
1364
    /**
1365
     * Get extra routes to merge into Director.rules
1366
     *
1367
     * @return array
1368
     */
1369
    protected function getExtraRoutes()
1370
    {
1371
        $rules = [];
1372
        foreach ($this->getExtraControllers() as $class) {
1373
            $controllerInst = Controller::singleton($class);
1374
            $link = Director::makeRelative($controllerInst->Link());
1375
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1376
            $rules[$route] = $class;
1377
        }
1378
        return $rules;
1379
    }
1380
1381
    // === REIMPLEMENTATION METHODS THAT EXISTED IN SAPPHIRE_TEST 5 ===
1382
1383
    /**
1384
     * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory()
1385
     *
1386
     * @param $argument
1387
     * @param $type
1388
     * @param $value
1389
     */
1390
    public static function createPHPUnitFrameworkException($argument, $type, $value = null)
1391
    {
1392
        $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

1392
        $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1393
1394
        return new PHPUnitFrameworkException(
1395
            sprintf(
1396
                'Argument #%d%sof %s::%s() must be a %s',
1397
                $argument,
1398
                $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1399
                $stack[1]['class'],
1400
                $stack[1]['function'],
1401
                $type
1402
            )
1403
        );
1404
    }
1405
1406
    /**
1407
     * Returns the annotations for this test.
1408
     *
1409
     * @return array
1410
     */
1411
    public function getAnnotations()
1412
    {
1413
        return TestUtil::parseTestMethodAnnotations(
1414
            get_class($this),
1415
            $this->getName(false)
1416
        );
1417
    }
1418
}
1419