Passed
Pull Request — 4 (#10028)
by Steve
07:43
created

SapphireTest::createPHPUnitFrameworkException()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 12
rs 9.9666
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 SilverStripe\CMS\Controllers\RootURLController;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Controllers\RootURLController was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
16
use SilverStripe\Control\CLIRequestBuilder;
17
use SilverStripe\Control\Controller;
18
use SilverStripe\Control\Cookie;
19
use SilverStripe\Control\Director;
20
use SilverStripe\Control\Email\Email;
21
use SilverStripe\Control\Email\Mailer;
22
use SilverStripe\Control\HTTPApplication;
23
use SilverStripe\Control\HTTPRequest;
24
use SilverStripe\Core\Config\Config;
25
use SilverStripe\Core\Injector\Injector;
26
use SilverStripe\Core\Injector\InjectorLoader;
27
use SilverStripe\Core\Manifest\ClassLoader;
28
use SilverStripe\Core\Manifest\ModuleResourceLoader;
29
use SilverStripe\Dev\Constraint\SSListContains;
30
use SilverStripe\Dev\Constraint\SSListContainsOnly;
31
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems;
32
use SilverStripe\Dev\State\FixtureTestState;
33
use SilverStripe\Dev\State\SapphireTestState;
34
use SilverStripe\i18n\i18n;
35
use SilverStripe\ORM\Connect\TempDatabase;
36
use SilverStripe\ORM\DataObject;
37
use SilverStripe\ORM\FieldType\DBDatetime;
38
use SilverStripe\ORM\FieldType\DBField;
39
use SilverStripe\ORM\SS_List;
40
use SilverStripe\Security\Group;
41
use SilverStripe\Security\IdentityStore;
42
use SilverStripe\Security\Member;
43
use SilverStripe\Security\Permission;
44
use SilverStripe\Security\Security;
45
use SilverStripe\View\SSViewer;
46
47
if (!class_exists(PHPUnit_Framework_TestCase::class)) {
0 ignored issues
show
Bug introduced by
The type SilverStripe\Dev\PHPUnit_Framework_TestCase was not found. Did you mean PHPUnit_Framework_TestCase? If so, make sure to prefix the type with \.
Loading history...
48
    return;
49
}
50
51
/**
52
 * Test case class for the Sapphire framework.
53
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
54
 * to work with.
55
 *
56
 * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
57
 * in production sites.
58
 */
59
class SapphireTest extends TestCase implements TestOnly
60
{
61
    /**
62
     * Path to fixture data for this test run.
63
     * If passed as an array, multiple fixture files will be loaded.
64
     * Please note that you won't be able to refer with "=>" notation
65
     * between the fixtures, they act independent of each other.
66
     *
67
     * @var string|array
68
     */
69
    protected static $fixture_file = null;
70
71
    /**
72
     * @deprecated 4.0..5.0 Use FixtureTestState instead
73
     * @var FixtureFactory
74
     */
75
    protected $fixtureFactory;
76
77
    /**
78
     * @var Boolean If set to TRUE, this will force a test database to be generated
79
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
80
     * {@link $fixture_file}, which always forces a database build.
81
     *
82
     * @var bool
83
     */
84
    protected $usesDatabase = null;
85
86
    /**
87
     * This test will cleanup its state via transactions.
88
     * If set to false a full schema is forced between tests, but at a performance cost.
89
     *
90
     * @var bool
91
     */
92
    protected $usesTransactions = true;
93
94
    /**
95
     * @var bool
96
     */
97
    protected static $is_running_test = false;
98
99
    /**
100
     * By default, setUp() does not require default records. Pass
101
     * class names in here, and the require/augment default records
102
     * function will be called on them.
103
     *
104
     * @var array
105
     */
106
    protected $requireDefaultRecordsFrom = [];
107
108
    /**
109
     * A list of extensions that can't be applied during the execution of this run.  If they are
110
     * applied, they will be temporarily removed and a database migration called.
111
     *
112
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
113
     * the values are an array of illegal extensions on that class.
114
     *
115
     * Set a class to `*` to remove all extensions (unadvised)
116
     *
117
     * @var array
118
     */
119
    protected static $illegal_extensions = [];
120
121
    /**
122
     * A list of extensions that must be applied during the execution of this run.  If they are
123
     * not applied, they will be temporarily added and a database migration called.
124
     *
125
     * The keys of the are the classes to apply the extensions to, and the values are an array
126
     * of required extensions on that class.
127
     *
128
     * Example:
129
     * <code>
130
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
131
     * </code>
132
     *
133
     * @var array
134
     */
135
    protected static $required_extensions = [];
136
137
    /**
138
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
139
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
140
     * Set it to an array of DataObject subclass names.
141
     *
142
     * @var array
143
     */
144
    protected static $extra_dataobjects = [];
145
146
    /**
147
     * List of class names of {@see Controller} objects to register routes for
148
     * Controllers must implement Link() method
149
     *
150
     * @var array
151
     */
152
    protected static $extra_controllers = [];
153
154
    /**
155
     * We need to disabling backing up of globals to avoid overriding
156
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
157
     *
158
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
159
     */
160
    protected $backupGlobals = false;
161
162
    /**
163
     * State management container for SapphireTest
164
     *
165
     * @var SapphireTestState
166
     */
167
    protected static $state = null;
168
169
    /**
170
     * Temp database helper
171
     *
172
     * @var TempDatabase
173
     */
174
    protected static $tempDB = null;
175
176
    /**
177
     * @return TempDatabase
178
     */
179
    public static function tempDB()
180
    {
181
        if (!class_exists(TempDatabase::class)) {
182
            return null;
183
        }
184
185
        if (!static::$tempDB) {
186
            static::$tempDB = TempDatabase::create();
187
        }
188
        return static::$tempDB;
189
    }
190
191
    /**
192
     * Gets illegal extensions for this class
193
     *
194
     * @return array
195
     */
196
    public static function getIllegalExtensions()
197
    {
198
        return static::$illegal_extensions;
199
    }
200
201
    /**
202
     * Gets required extensions for this class
203
     *
204
     * @return array
205
     */
206
    public static function getRequiredExtensions()
207
    {
208
        return static::$required_extensions;
209
    }
210
211
    /**
212
     * Check if test bootstrapping has been performed. Must not be relied on
213
     * outside of unit tests.
214
     *
215
     * @return bool
216
     */
217
    protected static function is_running_test()
218
    {
219
        return self::$is_running_test;
220
    }
221
222
    /**
223
     * Set test running state
224
     *
225
     * @param bool $bool
226
     */
227
    protected static function set_is_running_test($bool)
228
    {
229
        self::$is_running_test = $bool;
230
    }
231
232
    /**
233
     * @return String
234
     */
235
    public static function get_fixture_file()
236
    {
237
        return static::$fixture_file;
238
    }
239
240
    /**
241
     * @return bool
242
     */
243
    public function getUsesDatabase()
244
    {
245
        return $this->usesDatabase;
246
    }
247
248
    /**
249
     * @return bool
250
     */
251
    public function getUsesTransactions()
252
    {
253
        return $this->usesTransactions;
254
    }
255
256
    /**
257
     * @return array
258
     */
259
    public function getRequireDefaultRecordsFrom()
260
    {
261
        return $this->requireDefaultRecordsFrom;
262
    }
263
264
    /**
265
     * Setup  the test.
266
     * Always sets up in order:
267
     *  - Reset php state
268
     *  - Nest
269
     *  - Custom state helpers
270
     *
271
     * User code should call parent::setUp() before custom setup code
272
     */
273
    protected function setUp()
274
    {
275
        if (!defined('FRAMEWORK_PATH')) {
276
            trigger_error(
277
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
278
                E_USER_WARNING
279
            );
280
        }
281
282
        // Call state helpers
283
        static::$state->setUp($this);
284
285
        // We cannot run the tests on this abstract class.
286
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
287
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
288
            return;
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()
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()
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()
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
    ) {
630
        if ($haystack instanceof DBField) {
631
            $haystack = (string)$haystack;
632
        }
633
        parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
634
    }
635
636
    public static function assertNotContains(
637
        $needle,
638
        $haystack,
639
        $message = '',
640
        $ignoreCase = false,
641
        $checkForObjectIdentity = true,
642
        $checkForNonObjectIdentity = false
643
    ) {
644
        if ($haystack instanceof DBField) {
645
            $haystack = (string)$haystack;
646
        }
647
        parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
648
    }
649
650
    /**
651
     * Clear the log of emails sent
652
     *
653
     * @return bool True if emails cleared
654
     */
655
    public function clearEmails()
656
    {
657
        /** @var Mailer $mailer */
658
        $mailer = Injector::inst()->get(Mailer::class);
659
        if ($mailer instanceof TestMailer) {
660
            $mailer->clearEmails();
661
            return true;
662
        }
663
        return false;
664
    }
665
666
    /**
667
     * Search for an email that was sent.
668
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
669
     * @param string $to
670
     * @param string $from
671
     * @param string $subject
672
     * @param string $content
673
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
674
     *               'HtmlContent'
675
     */
676
    public static function findEmail($to, $from = null, $subject = null, $content = null)
677
    {
678
        /** @var Mailer $mailer */
679
        $mailer = Injector::inst()->get(Mailer::class);
680
        if ($mailer instanceof TestMailer) {
681
            return $mailer->findEmail($to, $from, $subject, $content);
682
        }
683
        return null;
684
    }
685
686
    /**
687
     * Assert that the matching email was sent since the last call to clearEmails()
688
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
689
     *
690
     * @param string $to
691
     * @param string $from
692
     * @param string $subject
693
     * @param string $content
694
     */
695
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
696
    {
697
        $found = (bool)static::findEmail($to, $from, $subject, $content);
698
699
        $infoParts = '';
700
        $withParts = [];
701
        if ($to) {
702
            $infoParts .= " to '$to'";
703
        }
704
        if ($from) {
705
            $infoParts .= " from '$from'";
706
        }
707
        if ($subject) {
708
            $withParts[] = "subject '$subject'";
709
        }
710
        if ($content) {
711
            $withParts[] = "content '$content'";
712
        }
713
        if ($withParts) {
714
            $infoParts .= ' with ' . implode(' and ', $withParts);
715
        }
716
717
        static::assertTrue(
718
            $found,
719
            "Failed asserting that an email was sent$infoParts."
720
        );
721
    }
722
723
724
    /**
725
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
726
     * pairs.  Each match must correspond to 1 distinct record.
727
     *
728
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
729
     * either pass a single pattern or an array of patterns.
730
     * @param SS_List $list The {@link SS_List} to test.
731
     * @param string $message
732
     *
733
     * Examples
734
     * --------
735
     * Check that $members includes an entry with Email = [email protected]:
736
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
737
     *
738
     * Check that $members includes entries with Email = [email protected] and with
739
     * Email = [email protected]:
740
     *      $this->assertListContains([
741
     *         ['Email' => '[email protected]'],
742
     *         ['Email' => '[email protected]'],
743
     *      ], $members);
744
     */
745
    public static function assertListContains($matches, SS_List $list, $message = '')
746
    {
747
        if (!is_array($matches)) {
748
            throw self::createPHPUnitFrameworkException(
749
                1,
750
                'array'
751
            );
752
        }
753
754
        static::assertThat(
755
            $list,
756
            new SSListContains(
757
                $matches
758
            ),
759
            $message
760
        );
761
    }
762
763
    /**
764
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
765
     *
766
     * @param $matches
767
     * @param $dataObjectSet
768
     */
769
    public function assertDOSContains($matches, $dataObjectSet)
770
    {
771
        Deprecation::notice('5.0', 'Use assertListContains() instead');
772
        return static::assertListContains($matches, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListContai...atches, $dataObjectSet) targeting SilverStripe\Dev\Sapphir...t::assertListContains() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
773
    }
774
775
    /**
776
     * Asserts that no items in a given list appear in the given dataobject list
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 doesn't have an entry with Email = [email protected]:
786
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
787
     *
788
     * Check that $members doesn't have entries with Email = [email protected] and with
789
     * Email = [email protected]:
790
     *      $this->assertListNotContains([
791
     *          ['Email' => '[email protected]'],
792
     *          ['Email' => '[email protected]'],
793
     *      ], $members);
794
     */
795
    public static function assertListNotContains($matches, SS_List $list, $message = '')
796
    {
797
        if (!is_array($matches)) {
798
            throw self::createPHPUnitFrameworkException(
799
                1,
800
                'array'
801
            );
802
        }
803
804
        $constraint =  new LogicalNot(
805
            new SSListContains(
806
                $matches
807
            )
808
        );
809
810
        static::assertThat(
811
            $list,
812
            $constraint,
813
            $message
814
        );
815
    }
816
817
    /**
818
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
819
     *
820
     * @param $matches
821
     * @param $dataObjectSet
822
     */
823
    public static function assertNotDOSContains($matches, $dataObjectSet)
824
    {
825
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
826
        return static::assertListNotContains($matches, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListNotCon...atches, $dataObjectSet) targeting SilverStripe\Dev\Sapphir...assertListNotContains() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
827
    }
828
829
    /**
830
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
831
     * key-value pairs.  Each match must correspond to 1 distinct record.
832
     *
833
     * Example
834
     * --------
835
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
836
     * matter:
837
     *     $this->assertListEquals([
838
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
839
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
840
     *      ], $members);
841
     *
842
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
843
     * either pass a single pattern or an array of patterns.
844
     * @param mixed $list The {@link SS_List} to test.
845
     * @param string $message
846
     */
847
    public static function assertListEquals($matches, SS_List $list, $message = '')
848
    {
849
        if (!is_array($matches)) {
850
            throw self::createPHPUnitFrameworkException(
851
                1,
852
                'array'
853
            );
854
        }
855
856
        static::assertThat(
857
            $list,
858
            new SSListContainsOnly(
859
                $matches
860
            ),
861
            $message
862
        );
863
    }
864
865
    /**
866
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
867
     *
868
     * @param $matches
869
     * @param SS_List $dataObjectSet
870
     */
871
    public function assertDOSEquals($matches, $dataObjectSet)
872
    {
873
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
874
        return static::assertListEquals($matches, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListEquals($matches, $dataObjectSet) targeting SilverStripe\Dev\SapphireTest::assertListEquals() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
875
    }
876
877
878
    /**
879
     * Assert that the every record in the given {@link SS_List} matches the given key-value
880
     * pairs.
881
     *
882
     * Example
883
     * --------
884
     * Check that every entry in $members has a Status of 'Active':
885
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
886
     *
887
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
888
     * @param mixed $list The {@link SS_List} to test.
889
     * @param string $message
890
     */
891
    public static function assertListAllMatch($match, SS_List $list, $message = '')
892
    {
893
        if (!is_array($match)) {
894
            throw self::createPHPUnitFrameworkException(
895
                1,
896
                'array'
897
            );
898
        }
899
900
        static::assertThat(
901
            $list,
902
            new SSListContainsOnlyMatchingItems(
903
                $match
904
            ),
905
            $message
906
        );
907
    }
908
909
    /**
910
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
911
     *
912
     * @param $match
913
     * @param SS_List $dataObjectSet
914
     */
915
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
916
    {
917
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
918
        return static::assertListAllMatch($match, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListAllMatch($match, $dataObjectSet) targeting SilverStripe\Dev\Sapphir...t::assertListAllMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
919
    }
920
921
    /**
922
     * Removes sequences of repeated whitespace characters from SQL queries
923
     * making them suitable for string comparison
924
     *
925
     * @param string $sql
926
     * @return string The cleaned and normalised SQL string
927
     */
928
    protected static function normaliseSQL($sql)
929
    {
930
        return trim(preg_replace('/\s+/m', ' ', $sql));
931
    }
932
933
    /**
934
     * Asserts that two SQL queries are equivalent
935
     *
936
     * @param string $expectedSQL
937
     * @param string $actualSQL
938
     * @param string $message
939
     * @param float|int $delta
940
     * @param integer $maxDepth
941
     * @param boolean $canonicalize
942
     * @param boolean $ignoreCase
943
     */
944
    public static function assertSQLEquals(
945
        $expectedSQL,
946
        $actualSQL,
947
        $message = '',
948
        $delta = 0,
949
        $maxDepth = 10,
950
        $canonicalize = false,
951
        $ignoreCase = false
952
    ) {
953
        // Normalise SQL queries to remove patterns of repeating whitespace
954
        $expectedSQL = static::normaliseSQL($expectedSQL);
955
        $actualSQL = static::normaliseSQL($actualSQL);
956
957
        static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
958
    }
959
960
    /**
961
     * Asserts that a SQL query contains a SQL fragment
962
     *
963
     * @param string $needleSQL
964
     * @param string $haystackSQL
965
     * @param string $message
966
     * @param boolean $ignoreCase
967
     * @param boolean $checkForObjectIdentity
968
     */
969
    public static function assertSQLContains(
970
        $needleSQL,
971
        $haystackSQL,
972
        $message = '',
973
        $ignoreCase = false,
974
        $checkForObjectIdentity = true
975
    ) {
976
        $needleSQL = static::normaliseSQL($needleSQL);
977
        $haystackSQL = static::normaliseSQL($haystackSQL);
978
979
        static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
980
    }
981
982
    /**
983
     * Asserts that a SQL query contains a SQL fragment
984
     *
985
     * @param string $needleSQL
986
     * @param string $haystackSQL
987
     * @param string $message
988
     * @param boolean $ignoreCase
989
     * @param boolean $checkForObjectIdentity
990
     */
991
    public static function assertSQLNotContains(
992
        $needleSQL,
993
        $haystackSQL,
994
        $message = '',
995
        $ignoreCase = false,
996
        $checkForObjectIdentity = true
997
    ) {
998
        $needleSQL = static::normaliseSQL($needleSQL);
999
        $haystackSQL = static::normaliseSQL($haystackSQL);
1000
1001
        static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1002
    }
1003
1004
    /**
1005
     * Start test environment
1006
     */
1007
    public static function start()
1008
    {
1009
        if (static::is_running_test()) {
1010
            return;
1011
        }
1012
1013
        // Health check
1014
        if (InjectorLoader::inst()->countManifests()) {
1015
            throw new LogicException('SapphireTest::start() cannot be called within another application');
1016
        }
1017
        static::set_is_running_test(true);
1018
1019
        // Test application
1020
        $kernel = new TestKernel(BASE_PATH);
1021
1022
        if (class_exists(HTTPApplication::class)) {
1023
            // Mock request
1024
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1025
            $request = CLIRequestBuilder::createFromEnvironment();
1026
1027
            $app = new HTTPApplication($kernel);
1028
            $flush = array_key_exists('flush', $request->getVars());
1029
1030
            // Custom application
1031
            $res = $app->execute($request, function (HTTPRequest $request) {
1032
                // Start session and execute
1033
                $request->getSession()->init($request);
1034
1035
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1036
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1037
                DataObject::reset();
1038
1039
                // Set dummy controller;
1040
                $controller = Controller::create();
1041
                $controller->setRequest($request);
1042
                $controller->pushCurrent();
1043
                $controller->doInit();
1044
            }, $flush);
1045
1046
            if ($res && $res->isError()) {
1047
                throw new LogicException($res->getBody());
1048
            }
1049
        } else {
1050
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
1051
            $flush = false;
1052
            foreach ($_SERVER['argv'] as $arg) {
1053
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
1054
                    $flush = true;
1055
                }
1056
            }
1057
            $kernel->boot($flush);
1058
        }
1059
1060
        // Register state
1061
        static::$state = SapphireTestState::singleton();
1062
        // Register temp DB holder
1063
        static::tempDB();
1064
    }
1065
1066
    /**
1067
     * Reset the testing database's schema, but only if it is active
1068
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1069
     * @param bool $forceCreate Force DB to be created if it doesn't exist
1070
     */
1071
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
1072
    {
1073
        if (!static::$tempDB) {
1074
            return;
1075
        }
1076
1077
        // Check if DB is active before reset
1078
        if (!static::$tempDB->isUsed()) {
1079
            if (!$forceCreate) {
1080
                return;
1081
            }
1082
            static::$tempDB->build();
1083
        }
1084
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1085
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
1086
    }
1087
1088
    /**
1089
     * A wrapper for automatically performing callbacks as a user with a specific permission
1090
     *
1091
     * @param string|array $permCode
1092
     * @param callable $callback
1093
     * @return mixed
1094
     */
1095
    public function actWithPermission($permCode, $callback)
1096
    {
1097
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1098
    }
1099
1100
    /**
1101
     * Create Member and Group objects on demand with specific permission code
1102
     *
1103
     * @param string|array $permCode
1104
     * @return Member
1105
     */
1106
    protected function createMemberWithPermission($permCode)
1107
    {
1108
        if (is_array($permCode)) {
1109
            $permArray = $permCode;
1110
            $permCode = implode('.', $permCode);
1111
        } else {
1112
            $permArray = [$permCode];
1113
        }
1114
1115
        // Check cached member
1116
        if (isset($this->cache_generatedMembers[$permCode])) {
1117
            $member = $this->cache_generatedMembers[$permCode];
1118
        } else {
1119
            // Generate group with these permissions
1120
            $group = Group::create();
1121
            $group->Title = "$permCode group";
1122
            $group->write();
1123
1124
            // Create each individual permission
1125
            foreach ($permArray as $permArrayItem) {
1126
                $permission = Permission::create();
1127
                $permission->Code = $permArrayItem;
1128
                $permission->write();
1129
                $group->Permissions()->add($permission);
1130
            }
1131
1132
            $member = Member::get()->filter([
1133
                'Email' => "[email protected]",
1134
            ])->first();
1135
            if (!$member) {
1136
                $member = Member::create();
1137
            }
1138
1139
            $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...
1140
            $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...
1141
            $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...
1142
            $member->write();
1143
            $group->Members()->add($member);
1144
1145
            $this->cache_generatedMembers[$permCode] = $member;
1146
        }
1147
        return $member;
1148
    }
1149
1150
    /**
1151
     * Create a member and group with the given permission code, and log in with it.
1152
     * Returns the member ID.
1153
     *
1154
     * @param string|array $permCode Either a permission, or list of permissions
1155
     * @return int Member ID
1156
     */
1157
    public function logInWithPermission($permCode = 'ADMIN')
1158
    {
1159
        $member = $this->createMemberWithPermission($permCode);
1160
        $this->logInAs($member);
1161
        return $member->ID;
1162
    }
1163
1164
    /**
1165
     * Log in as the given member
1166
     *
1167
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1168
     */
1169
    public function logInAs($member)
1170
    {
1171
        if (is_numeric($member)) {
1172
            $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

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

1334
        $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1335
1336
        return new PHPUnitFrameworkException(
1337
            sprintf(
1338
                'Argument #%d%sof %s::%s() must be a %s',
1339
                $argument,
1340
                $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1341
                $stack[1]['class'],
1342
                $stack[1]['function'],
1343
                $type
1344
            )
1345
        );
1346
    }
1347
}
1348