Passed
Pull Request — 4 (#10028)
by Steve
07:08 queued 16s
created

SapphireTest::createPHPUnitFrameworkException()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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