diff --git a/src/Dev/Constraint/SSListContains.php b/src/Dev/Constraint/SSListContains.php index 51eff1785b3..73618484e51 100644 --- a/src/Dev/Constraint/SSListContains.php +++ b/src/Dev/Constraint/SSListContains.php @@ -2,14 +2,16 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; use SilverStripe\Dev\SSListExporter; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\SS_List; use SilverStripe\View\ViewableData; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -17,7 +19,7 @@ * Constraint for checking if a SS_List contains items matching the given * key-value pairs. */ -class SSListContains extends PHPUnit_Framework_Constraint implements TestOnly +class SSListContains extends Constraint implements TestOnly { /** * @var array @@ -31,9 +33,8 @@ class SSListContains extends PHPUnit_Framework_Constraint implements TestOnly */ protected $hasLeftoverItems = false; - public function __construct($matches) + public function __construct(array $matches) { - parent::__construct(); $this->exporter = new SSListExporter(); $this->matches = $matches; @@ -55,9 +56,9 @@ public function __construct($matches) * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?bool { $success = true; @@ -86,7 +87,7 @@ public function evaluate($other, $description = '', $returnResult = false) * @param ViewableData $item * @return bool */ - protected function checkIfItemEvaluatesRemainingMatches(ViewableData $item) + protected function checkIfItemEvaluatesRemainingMatches(ViewableData $item): bool { $success = false; foreach ($this->matches as $key => $match) { @@ -107,7 +108,7 @@ protected function checkIfItemEvaluatesRemainingMatches(ViewableData $item) * * @return string */ - public function toString() + public function toString(): string { $matchToString = function ($key, $value) { return ' "' . $key . '" is "' . $value . '"'; @@ -132,7 +133,10 @@ public function toString() return $this->getStubForToString() . $allMatchesAsString; } - protected function getStubForToString() + /** + * @return string + */ + protected function getStubForToString(): string { return ' contains an item matching '; } diff --git a/src/Dev/Constraint/SSListContainsOnly.php b/src/Dev/Constraint/SSListContainsOnly.php index 0be563ac9a6..9b0eb1da3ec 100644 --- a/src/Dev/Constraint/SSListContainsOnly.php +++ b/src/Dev/Constraint/SSListContainsOnly.php @@ -2,12 +2,14 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\SS_List; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -40,9 +42,9 @@ class SSListContainsOnly extends SSListContains implements TestOnly * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?null { $success = true; @@ -71,7 +73,7 @@ public function evaluate($other, $description = '', $returnResult = false) return null; } - protected function getStubForToString() + protected function getStubForToString(): string { return $this->itemNotMatching ? parent::getStubForToString() diff --git a/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php b/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php index d17ec07e09b..248840f4a40 100644 --- a/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php +++ b/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php @@ -2,13 +2,15 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; use SilverStripe\Dev\SSListExporter; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\SS_List; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -16,7 +18,7 @@ * Constraint for checking if every item in a SS_List matches a given match, * e.g. every Member has isActive set to true */ -class SSListContainsOnlyMatchingItems extends PHPUnit_Framework_Constraint implements TestOnly +class SSListContainsOnlyMatchingItems extends Constraint implements TestOnly { /** * @var array @@ -30,7 +32,6 @@ class SSListContainsOnlyMatchingItems extends PHPUnit_Framework_Constraint imple public function __construct($match) { - parent::__construct(); $this->exporter = new SSListExporter(); $this->constraint = new ViewableDataContains($match); @@ -53,9 +54,9 @@ public function __construct($match) * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?bool { $success = true; @@ -82,7 +83,7 @@ public function evaluate($other, $description = '', $returnResult = false) * * @return string */ - public function toString() + public function toString(): string { return 'contains only Objects where "' . key($this->match) . '" is "' . current($this->match) . '"'; } diff --git a/src/Dev/Constraint/ViewableDataContains.php b/src/Dev/Constraint/ViewableDataContains.php index 6665c0f46fb..9e32cab8c70 100644 --- a/src/Dev/Constraint/ViewableDataContains.php +++ b/src/Dev/Constraint/ViewableDataContains.php @@ -2,13 +2,16 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; -use PHPUnit_Util_InvalidArgumentHelper; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; +// use PHPUnit_Util_InvalidArgumentHelper; use SilverStripe\Dev\TestOnly; use SilverStripe\View\ViewableData; +use SilverStripe\Dev\SapphireTest; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -16,7 +19,7 @@ * Constraint for checking if a ViewableData (e.g. ArrayData or any DataObject) contains fields matching the given * key-value pairs. */ -class ViewableDataContains extends PHPUnit_Framework_Constraint implements TestOnly +class ViewableDataContains extends Constraint implements TestOnly { /** * @var array @@ -27,12 +30,10 @@ class ViewableDataContains extends PHPUnit_Framework_Constraint implements TestO * ViewableDataContains constructor. * @param array $match */ - public function __construct($match) + public function __construct(array $match) { - parent::__construct(); - if (!is_array($match)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( + throw SapphireTest::createPHPUnitFrameworkException( 1, 'array' ); @@ -57,9 +58,9 @@ public function __construct($match) * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?bool { $success = true; @@ -89,7 +90,7 @@ public function evaluate($other, $description = '', $returnResult = false) * * @return string */ - public function toString() + public function toString(): string { return 'contains only Objects where "' . key($this->match) . '" is "' . current($this->match) . '"'; } diff --git a/src/Dev/FunctionalTest.php b/src/Dev/FunctionalTest.php index 0d9e7459d6a..5d9a9758f07 100644 --- a/src/Dev/FunctionalTest.php +++ b/src/Dev/FunctionalTest.php @@ -2,7 +2,8 @@ namespace SilverStripe\Dev; -use PHPUnit_Framework_AssertionFailedError; +// use PHPUnit_Framework_AssertionFailedError; +use PHPUnit\Framework\AssertionFailedError; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\Session; @@ -81,7 +82,7 @@ public function session() return $this->mainSession->session(); } - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -113,7 +114,7 @@ protected function setUp() SecurityToken::disable(); } - protected function tearDown() + protected function tearDown(): void { SecurityToken::enable(); unset($this->mainSession); @@ -267,7 +268,7 @@ public function cssParser() * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' * @param array|string $expectedMatches The content of at least one of the matched tags * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError + * @throws AssertionFailedError */ public function assertPartialMatchBySelector($selector, $expectedMatches, $message = null) { @@ -304,7 +305,7 @@ public function assertPartialMatchBySelector($selector, $expectedMatches, $messa * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' * @param array|string $expectedMatches The content of *all* matching tags as an array * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError + * @throws AssertionFailedError */ public function assertExactMatchBySelector($selector, $expectedMatches, $message = null) { @@ -339,7 +340,7 @@ public function assertExactMatchBySelector($selector, $expectedMatches, $message * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' * @param array|string $expectedMatches The content of at least one of the matched tags * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError + * @throws AssertionFailedError */ public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null) { @@ -377,7 +378,7 @@ public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $m * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' * @param array|string $expectedMatches The content of *all* matched tags as an array * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError + * @throws AssertionFailedError */ public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null) { diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index fb987006b55..8027b9a7acf 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -3,10 +3,16 @@ namespace SilverStripe\Dev; use Exception; +use InvalidArgumentException; use LogicException; -use PHPUnit_Framework_Constraint_Not; -use PHPUnit_Framework_TestCase; -use PHPUnit_Util_InvalidArgumentHelper; + +// use PHPUnit_Framework_Constraint_Not; +use PHPUnit\Framework\Constraint\LogicalNot; +// use PHPUnit_Framework_TestCase; +use PHPUnit\Framework\TestCase; +// use PHPUnit_Util_InvalidArgumentHelper; +use PHPUnit\Framework\Exception as PHPUnitFrameworkException; +use PHPUnit\Util\Test as TestUtil; use SilverStripe\CMS\Controllers\RootURLController; use SilverStripe\Control\CLIRequestBuilder; use SilverStripe\Control\Controller; @@ -39,7 +45,7 @@ use SilverStripe\Security\Security; use SilverStripe\View\SSViewer; -if (!class_exists(PHPUnit_Framework_TestCase::class)) { +if (!class_exists(TestCase::class)) { return; } @@ -51,7 +57,7 @@ * This class should not be used anywhere outside of unit tests, as phpunit may not be installed * in production sites. */ -class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly +class SapphireTest implements TestOnly { /** * Path to fixture data for this test run. @@ -400,7 +406,7 @@ protected function currentTestDisablesDatabase() * * @throws Exception */ - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { // Start tests static::start(); @@ -429,7 +435,7 @@ public static function setUpBeforeClass() * * User code should call parent::tearDownAfterClass() after custom tear down code */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { // Call state helpers static::$state->tearDownOnce(static::class); @@ -592,7 +598,7 @@ protected function getCurrentRelativePath() * * User code should call parent::tearDown() after custom tear down code */ - protected function tearDown() + protected function tearDown(): void { // Reset mocked datetime if (class_exists(DBDatetime::class)) { @@ -621,11 +627,36 @@ public static function assertContains( $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false - ) { + ):void { if ($haystack instanceof DBField) { $haystack = (string)$haystack; } - parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity); + if (is_iterable($haystack)) { + $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity; + if ($strict) { + parent::assertContains($needle, $haystack, $message); + } else { + parent::assertContainsEquals($needle, $haystack, $message); + } + } else { + static::assertContainsNonIterable($needle, $haystack, $message, $ignoreCase); + } + } + + public static function assertContainsNonIterable( + $needle, + $haystack, + $message = '', + $ignoreCase = false + ):void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if ($ignoreCase) { + parent::assertStringContainsStringIgnoringCase($needle, $haystack, $message); + } else { + parent::assertStringContainsString($needle, $haystack, $message); + } } public static function assertNotContains( @@ -635,11 +666,36 @@ public static function assertNotContains( $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false - ) { + ):void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if (is_iterable($haystack)) { + $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity; + if ($strict) { + parent::assertNotContains($needle, $haystack, $message); + } else { + parent::assertNotContainsEquals($needle, $haystack, $message); + } + } else { + static::assertNotContainsNonIterable($needle, $haystack, $message, $ignoreCase); + } + } + + protected static function assertNotContainsNonIterable( + $needle, + $haystack, + $message = '', + $ignoreCase = false + ):void { if ($haystack instanceof DBField) { $haystack = (string)$haystack; } - parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity); + if ($ignoreCase) { + parent::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message); + } else { + parent::assertStringNotContainsString($needle, $haystack, $message); + } } /** @@ -740,7 +796,7 @@ public static function assertEmailSent($to, $from = null, $subject = null, $cont public static function assertListContains($matches, SS_List $list, $message = '') { if (!is_array($matches)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( + throw self::createPHPUnitFrameworkException( 1, 'array' ); @@ -790,13 +846,13 @@ public function assertDOSContains($matches, $dataObjectSet) public static function assertListNotContains($matches, SS_List $list, $message = '') { if (!is_array($matches)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( + throw self::createPHPUnitFrameworkException( 1, 'array' ); } - $constraint = new PHPUnit_Framework_Constraint_Not( + $constraint = new LogicalNot( new SSListContains( $matches ) @@ -842,7 +898,7 @@ public static function assertNotDOSContains($matches, $dataObjectSet) public static function assertListEquals($matches, SS_List $list, $message = '') { if (!is_array($matches)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( + throw self::createPHPUnitFrameworkException( 1, 'array' ); @@ -886,7 +942,7 @@ public function assertDOSEquals($matches, $dataObjectSet) public static function assertListAllMatch($match, SS_List $list, $message = '') { if (!is_array($match)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( + throw self::createPHPUnitFrameworkException( 1, 'array' ); @@ -970,8 +1026,11 @@ public static function assertSQLContains( ) { $needleSQL = static::normaliseSQL($needleSQL); $haystackSQL = static::normaliseSQL($haystackSQL); - - static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + if (is_iterable($haystackSQL)) { + return static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } else { + return static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); + } } /** @@ -992,8 +1051,11 @@ public static function assertSQLNotContains( ) { $needleSQL = static::normaliseSQL($needleSQL); $haystackSQL = static::normaliseSQL($haystackSQL); - - static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + if (is_iterable($haystackSQL)) { + return static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } else { + return static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); + } } /** @@ -1316,4 +1378,42 @@ protected function getExtraRoutes() } return $rules; } + + // === REIMPLEMENTATION METHODS THAT EXISTED IN SAPPHIRE_TEST 5 === + + /** + * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory() + * + * @param $argument + * @param $type + * @param $value + */ + public static function createPHPUnitFrameworkException($argument, $type, $value = null) + { + $stack = debug_backtrace(false); + + return new PHPUnitFrameworkException( + sprintf( + 'Argument #%d%sof %s::%s() must be a %s', + $argument, + $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ', + $stack[1]['class'], + $stack[1]['function'], + $type + ) + ); + } + + /** + * Returns the annotations for this test. + * + * @return array + */ + public function getAnnotations() + { + return TestUtil::parseTestMethodAnnotations( + get_class($this), + $this->getName(false) + ); + } } diff --git a/src/Dev/SapphireTestInner.php b/src/Dev/SapphireTestInner.php new file mode 100644 index 00000000000..c1d2bf776e7 --- /dev/null +++ b/src/Dev/SapphireTestInner.php @@ -0,0 +1,1419 @@ +" notation + * between the fixtures, they act independent of each other. + * + * @var string|array + */ + protected static $fixture_file = null; + + /** + * @deprecated 4.0..5.0 Use FixtureTestState instead + * @var FixtureFactory + */ + protected $fixtureFactory; + + /** + * @var Boolean If set to TRUE, this will force a test database to be generated + * in {@link setUp()}. Note that this flag is overruled by the presence of a + * {@link $fixture_file}, which always forces a database build. + * + * @var bool + */ + protected $usesDatabase = null; + + /** + * This test will cleanup its state via transactions. + * If set to false a full schema is forced between tests, but at a performance cost. + * + * @var bool + */ + protected $usesTransactions = true; + + /** + * @var bool + */ + protected static $is_running_test = false; + + /** + * By default, setUp() does not require default records. Pass + * class names in here, and the require/augment default records + * function will be called on them. + * + * @var array + */ + protected $requireDefaultRecordsFrom = []; + + /** + * A list of extensions that can't be applied during the execution of this run. If they are + * applied, they will be temporarily removed and a database migration called. + * + * The keys of the are the classes that the extensions can't be applied the extensions to, and + * the values are an array of illegal extensions on that class. + * + * Set a class to `*` to remove all extensions (unadvised) + * + * @var array + */ + protected static $illegal_extensions = []; + + /** + * A list of extensions that must be applied during the execution of this run. If they are + * not applied, they will be temporarily added and a database migration called. + * + * The keys of the are the classes to apply the extensions to, and the values are an array + * of required extensions on that class. + * + * Example: + * + * array("MyTreeDataObject" => array("Versioned", "Hierarchy")) + * + * + * @var array + */ + protected static $required_extensions = []; + + /** + * By default, the test database won't contain any DataObjects that have the interface TestOnly. + * This variable lets you define additional TestOnly DataObjects to set up for this test. + * Set it to an array of DataObject subclass names. + * + * @var array + */ + protected static $extra_dataobjects = []; + + /** + * List of class names of {@see Controller} objects to register routes for + * Controllers must implement Link() method + * + * @var array + */ + protected static $extra_controllers = []; + + /** + * We need to disabling backing up of globals to avoid overriding + * the few globals SilverStripe relies on, like $lang for the i18n subsystem. + * + * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html + */ + protected $backupGlobals = false; + + /** + * State management container for SapphireTest + * + * @var SapphireTestState + */ + protected static $state = null; + + /** + * Temp database helper + * + * @var TempDatabase + */ + protected static $tempDB = null; + + /** + * @return TempDatabase + */ + public static function tempDB() + { + if (!class_exists(TempDatabase::class)) { + return null; + } + + if (!static::$tempDB) { + static::$tempDB = TempDatabase::create(); + } + return static::$tempDB; + } + + /** + * Gets illegal extensions for this class + * + * @return array + */ + public static function getIllegalExtensions() + { + return static::$illegal_extensions; + } + + /** + * Gets required extensions for this class + * + * @return array + */ + public static function getRequiredExtensions() + { + return static::$required_extensions; + } + + /** + * Check if test bootstrapping has been performed. Must not be relied on + * outside of unit tests. + * + * @return bool + */ + protected static function is_running_test() + { + return self::$is_running_test; + } + + /** + * Set test running state + * + * @param bool $bool + */ + protected static function set_is_running_test($bool) + { + self::$is_running_test = $bool; + } + + /** + * @return String + */ + public static function get_fixture_file() + { + return static::$fixture_file; + } + + /** + * @return bool + */ + public function getUsesDatabase() + { + return $this->usesDatabase; + } + + /** + * @return bool + */ + public function getUsesTransactions() + { + return $this->usesTransactions; + } + + /** + * @return array + */ + public function getRequireDefaultRecordsFrom() + { + return $this->requireDefaultRecordsFrom; + } + + /** + * Setup the test. + * Always sets up in order: + * - Reset php state + * - Nest + * - Custom state helpers + * + * User code should call parent::setUp() before custom setup code + */ + protected function setUp() + { + if (!defined('FRAMEWORK_PATH')) { + trigger_error( + 'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?', + E_USER_WARNING + ); + } + + // Call state helpers + static::$state->setUp($this); + + // We cannot run the tests on this abstract class. + if (static::class == __CLASS__) { + $this->markTestSkipped(sprintf('Skipping %s ', static::class)); + return; + } + + // i18n needs to be set to the defaults or tests fail + if (class_exists(i18n::class)) { + i18n::set_locale(i18n::config()->uninherited('default_locale')); + } + + // Set default timezone consistently to avoid NZ-specific dependencies + date_default_timezone_set('UTC'); + + if (class_exists(Member::class)) { + Member::set_password_validator(null); + } + + if (class_exists(Cookie::class)) { + Cookie::config()->update('report_errors', false); + } + + if (class_exists(RootURLController::class)) { + RootURLController::reset(); + } + + if (class_exists(Security::class)) { + Security::clear_database_is_ready(); + } + + // Set up test routes + $this->setUpRoutes(); + + $fixtureFiles = $this->getFixturePaths(); + + if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) { + // Assign fixture factory to deprecated prop in case old tests use it over the getter + /** @var FixtureTestState $fixtureState */ + $fixtureState = static::$state->getStateByName('fixtures'); + $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class); + + $this->logInWithPermission('ADMIN'); + } + + // turn off template debugging + if (class_exists(SSViewer::class)) { + SSViewer::config()->update('source_file_comments', false); + } + + // Set up the test mailer + if (class_exists(TestMailer::class)) { + Injector::inst()->registerService(new TestMailer(), Mailer::class); + } + + if (class_exists(Email::class)) { + Email::config()->remove('send_all_emails_to'); + Email::config()->remove('send_all_emails_from'); + Email::config()->remove('cc_all_emails_to'); + Email::config()->remove('bcc_all_emails_to'); + } + } + + + + /** + * Helper method to determine if the current test should enable a test database + * + * @param $fixtureFiles + * @return bool + */ + protected function shouldSetupDatabaseForCurrentTest($fixtureFiles) + { + $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase; + + return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase()) + || $this->currentTestEnablesDatabase(); + } + + /** + * Helper method to check, if the current test uses the database. + * This can be switched on with the annotation "@useDatabase" + * + * @return bool + */ + protected function currentTestEnablesDatabase() + { + $annotations = $this->getAnnotations(); + + return array_key_exists('useDatabase', $annotations['method']) + && $annotations['method']['useDatabase'][0] !== 'false'; + } + + /** + * Helper method to check, if the current test uses the database. + * This can be switched on with the annotation "@useDatabase false" + * + * @return bool + */ + protected function currentTestDisablesDatabase() + { + $annotations = $this->getAnnotations(); + + return array_key_exists('useDatabase', $annotations['method']) + && $annotations['method']['useDatabase'][0] === 'false'; + } + + /** + * Called once per test case ({@link SapphireTest} subclass). + * This is different to {@link setUp()}, which gets called once + * per method. Useful to initialize expensive operations which + * don't change state for any called method inside the test, + * e.g. dynamically adding an extension. See {@link teardownAfterClass()} + * for tearing down the state again. + * + * Always sets up in order: + * - Reset php state + * - Nest + * - Custom state helpers + * + * User code should call parent::setUpBeforeClass() before custom setup code + * + * @throws Exception + */ + public static function setUpBeforeClass(): void + { + // Start tests + static::start(); + + if (!static::$state) { + throw new Exception('SapphireTest failed to bootstrap!'); + } + + // Call state helpers + static::$state->setUpOnce(static::class); + + // Build DB if we have objects + if (class_exists(DataObject::class) && static::getExtraDataObjects()) { + DataObject::reset(); + static::resetDBSchema(true, true); + } + } + + /** + * tearDown method that's called once per test class rather once per test method. + * + * Always sets up in order: + * - Custom state helpers + * - Unnest + * - Reset php state + * + * User code should call parent::tearDownAfterClass() after custom tear down code + */ + public static function tearDownAfterClass(): void + { + // Call state helpers + static::$state->tearDownOnce(static::class); + + // Reset DB schema + static::resetDBSchema(); + } + + /** + * @deprecated 4.0.0:5.0.0 + * @return FixtureFactory|false + */ + public function getFixtureFactory() + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + return $state->getFixtureFactory(static::class); + } + + /** + * Sets a new fixture factory + * @deprecated 4.0.0:5.0.0 + * @param FixtureFactory $factory + * @return $this + */ + public function setFixtureFactory(FixtureFactory $factory) + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $state->setFixtureFactory($factory, static::class); + $this->fixtureFactory = $factory; + return $this; + } + + /** + * Get the ID of an object from the fixture. + * + * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work + * @param string $identifier The identifier string, as provided in your fixture file + * @return int + */ + protected function idFromFixture($className, $identifier) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $id = $state->getFixtureFactory(static::class)->getId($className, $identifier); + + if (!$id) { + throw new \InvalidArgumentException(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + )); + } + + return $id; + } + + /** + * Return all of the IDs in the fixture of a particular class name. + * Will collate all IDs form all fixtures if multiple fixtures are provided. + * + * @param string $className The data class or table name, as specified in your fixture file + * @return array A map of fixture-identifier => object-id + */ + protected function allFixtureIDs($className) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + return $state->getFixtureFactory(static::class)->getIds($className); + } + + /** + * Get an object from the fixture. + * + * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work + * @param string $identifier The identifier string, as provided in your fixture file + * + * @return DataObject + */ + protected function objFromFixture($className, $identifier) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $obj = $state->getFixtureFactory(static::class)->get($className, $identifier); + + if (!$obj) { + throw new \InvalidArgumentException(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + )); + } + + return $obj; + } + + /** + * Load a YAML fixture file into the database. + * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. + * Doesn't clear existing fixtures. + * @deprecated 4.0.0:5.0.0 + * + * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir + */ + public function loadFixture($fixtureFile) + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile); + $fixture->writeInto($this->getFixtureFactory()); + } + + /** + * Clear all fixtures which were previously loaded through + * {@link loadFixture()} + */ + public function clearFixtures() + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $state->getFixtureFactory(static::class)->clear(); + } + + /** + * Useful for writing unit tests without hardcoding folder structures. + * + * @return string Absolute path to current class. + */ + protected function getCurrentAbsolutePath() + { + $filename = ClassLoader::inst()->getItemPath(static::class); + if (!$filename) { + throw new LogicException('getItemPath returned null for ' . static::class + . '. Try adding flush=1 to the test run.'); + } + return dirname($filename); + } + + /** + * @return string File path relative to webroot + */ + protected function getCurrentRelativePath() + { + $base = Director::baseFolder(); + $path = $this->getCurrentAbsolutePath(); + if (substr($path, 0, strlen($base)) == $base) { + $path = preg_replace('/^\/*/', '', substr($path, strlen($base))); + } + return $path; + } + + /** + * Setup the test. + * Always sets up in order: + * - Custom state helpers + * - Unnest + * - Reset php state + * + * User code should call parent::tearDown() after custom tear down code + */ + protected function tearDown(): void + { + // Reset mocked datetime + if (class_exists(DBDatetime::class)) { + DBDatetime::clear_mock_now(); + } + + // Stop the redirection that might have been requested in the test. + // Note: Ideally a clean Controller should be created for each test. + // Now all tests executed in a batch share the same controller. + if (class_exists(Controller::class)) { + $controller = Controller::has_curr() ? Controller::curr() : null; + if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) { + $response->setStatusCode(200); + $response->removeHeader('Location'); + } + } + + // Call state helpers + static::$state->tearDown($this); + } + + public static function assertContains( + $needle, + $haystack, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true, + $checkForNonObjectIdentity = false + ):void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if (is_iterable($haystack)) { + $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity; + if ($strict) { + parent::assertContains($needle, $haystack, $message); + } else { + parent::assertContainsEquals($needle, $haystack, $message); + } + } else { + static::assertContainsNonIterable($needle, $haystack, $message, $ignoreCase); + } + } + + public static function assertContainsNonIterable( + $needle, + $haystack, + $message = '', + $ignoreCase = false + ):void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if ($ignoreCase) { + parent::assertStringContainsStringIgnoringCase($needle, $haystack, $message); + } else { + parent::assertStringContainsString($needle, $haystack, $message); + } + } + + public static function assertNotContains( + $needle, + $haystack, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true, + $checkForNonObjectIdentity = false + ):void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if (is_iterable($haystack)) { + $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity; + if ($strict) { + parent::assertNotContains($needle, $haystack, $message); + } else { + parent::assertNotContainsEquals($needle, $haystack, $message); + } + } else { + static::assertNotContainsNonIterable($needle, $haystack, $message, $ignoreCase); + } + } + + protected static function assertNotContainsNonIterable( + $needle, + $haystack, + $message = '', + $ignoreCase = false + ):void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if ($ignoreCase) { + parent::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message); + } else { + parent::assertStringNotContainsString($needle, $haystack, $message); + } + } + + /** + * Clear the log of emails sent + * + * @return bool True if emails cleared + */ + public function clearEmails() + { + /** @var Mailer $mailer */ + $mailer = Injector::inst()->get(Mailer::class); + if ($mailer instanceof TestMailer) { + $mailer->clearEmails(); + return true; + } + return false; + } + + /** + * Search for an email that was sent. + * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. + * @param string $to + * @param string $from + * @param string $subject + * @param string $content + * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', + * 'HtmlContent' + */ + public static function findEmail($to, $from = null, $subject = null, $content = null) + { + /** @var Mailer $mailer */ + $mailer = Injector::inst()->get(Mailer::class); + if ($mailer instanceof TestMailer) { + return $mailer->findEmail($to, $from, $subject, $content); + } + return null; + } + + /** + * Assert that the matching email was sent since the last call to clearEmails() + * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. + * + * @param string $to + * @param string $from + * @param string $subject + * @param string $content + */ + public static function assertEmailSent($to, $from = null, $subject = null, $content = null) + { + $found = (bool)static::findEmail($to, $from, $subject, $content); + + $infoParts = ''; + $withParts = []; + if ($to) { + $infoParts .= " to '$to'"; + } + if ($from) { + $infoParts .= " from '$from'"; + } + if ($subject) { + $withParts[] = "subject '$subject'"; + } + if ($content) { + $withParts[] = "content '$content'"; + } + if ($withParts) { + $infoParts .= ' with ' . implode(' and ', $withParts); + } + + static::assertTrue( + $found, + "Failed asserting that an email was sent$infoParts." + ); + } + + + /** + * Assert that the given {@link SS_List} includes DataObjects matching the given key-value + * pairs. Each match must correspond to 1 distinct record. + * + * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param SS_List $list The {@link SS_List} to test. + * @param string $message + * + * Examples + * -------- + * Check that $members includes an entry with Email = sam@example.com: + * $this->assertListContains(['Email' => '...@example.com'], $members); + * + * Check that $members includes entries with Email = sam@example.com and with + * Email = ingo@example.com: + * $this->assertListContains([ + * ['Email' => '...@example.com'], + * ['Email' => 'i...@example.com'], + * ], $members); + */ + public static function assertListContains($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } + + static::assertThat( + $list, + new SSListContains( + $matches + ), + $message + ); + } + + /** + * @deprecated 4.0.0:5.0.0 Use assertListContains() instead + * + * @param $matches + * @param $dataObjectSet + */ + public function assertDOSContains($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListContains() instead'); + return static::assertListContains($matches, $dataObjectSet); + } + + /** + * Asserts that no items in a given list appear in the given dataobject list + * + * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param SS_List $list The {@link SS_List} to test. + * @param string $message + * + * Examples + * -------- + * Check that $members doesn't have an entry with Email = sam@example.com: + * $this->assertListNotContains(['Email' => '...@example.com'], $members); + * + * Check that $members doesn't have entries with Email = sam@example.com and with + * Email = ingo@example.com: + * $this->assertListNotContains([ + * ['Email' => '...@example.com'], + * ['Email' => 'i...@example.com'], + * ], $members); + */ + public static function assertListNotContains($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } + + $constraint = new LogicalNot( + new SSListContains( + $matches + ) + ); + + static::assertThat( + $list, + $constraint, + $message + ); + } + + /** + * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead + * + * @param $matches + * @param $dataObjectSet + */ + public static function assertNotDOSContains($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListNotContains() instead'); + return static::assertListNotContains($matches, $dataObjectSet); + } + + /** + * Assert that the given {@link SS_List} includes only DataObjects matching the given + * key-value pairs. Each match must correspond to 1 distinct record. + * + * Example + * -------- + * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't + * matter: + * $this->assertListEquals([ + * ['FirstName' =>'Sam', 'Surname' => 'Minnee'], + * ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], + * ], $members); + * + * @param mixed $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param mixed $list The {@link SS_List} to test. + * @param string $message + */ + public static function assertListEquals($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } + + static::assertThat( + $list, + new SSListContainsOnly( + $matches + ), + $message + ); + } + + /** + * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead + * + * @param $matches + * @param SS_List $dataObjectSet + */ + public function assertDOSEquals($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListEquals() instead'); + return static::assertListEquals($matches, $dataObjectSet); + } + + + /** + * Assert that the every record in the given {@link SS_List} matches the given key-value + * pairs. + * + * Example + * -------- + * Check that every entry in $members has a Status of 'Active': + * $this->assertListAllMatch(['Status' => 'Active'], $members); + * + * @param mixed $match The pattern to match. The pattern is a map of key-value pairs. + * @param mixed $list The {@link SS_List} to test. + * @param string $message + */ + public static function assertListAllMatch($match, SS_List $list, $message = '') + { + if (!is_array($match)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } + + static::assertThat( + $list, + new SSListContainsOnlyMatchingItems( + $match + ), + $message + ); + } + + /** + * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead + * + * @param $match + * @param SS_List $dataObjectSet + */ + public function assertDOSAllMatch($match, SS_List $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListAllMatch() instead'); + return static::assertListAllMatch($match, $dataObjectSet); + } + + /** + * Removes sequences of repeated whitespace characters from SQL queries + * making them suitable for string comparison + * + * @param string $sql + * @return string The cleaned and normalised SQL string + */ + protected static function normaliseSQL($sql) + { + return trim(preg_replace('/\s+/m', ' ', $sql)); + } + + /** + * Asserts that two SQL queries are equivalent + * + * @param string $expectedSQL + * @param string $actualSQL + * @param string $message + * @param float|int $delta + * @param integer $maxDepth + * @param boolean $canonicalize + * @param boolean $ignoreCase + */ + public static function assertSQLEquals( + $expectedSQL, + $actualSQL, + $message = '', + $delta = 0, + $maxDepth = 10, + $canonicalize = false, + $ignoreCase = false + ) { + // Normalise SQL queries to remove patterns of repeating whitespace + $expectedSQL = static::normaliseSQL($expectedSQL); + $actualSQL = static::normaliseSQL($actualSQL); + + static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); + } + + /** + * Asserts that a SQL query contains a SQL fragment + * + * @param string $needleSQL + * @param string $haystackSQL + * @param string $message + * @param boolean $ignoreCase + * @param boolean $checkForObjectIdentity + */ + public static function assertSQLContains( + $needleSQL, + $haystackSQL, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true + ) { + $needleSQL = static::normaliseSQL($needleSQL); + $haystackSQL = static::normaliseSQL($haystackSQL); + if (is_iterable($haystackSQL)) { + return static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } else { + return static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); + } + } + + /** + * Asserts that a SQL query contains a SQL fragment + * + * @param string $needleSQL + * @param string $haystackSQL + * @param string $message + * @param boolean $ignoreCase + * @param boolean $checkForObjectIdentity + */ + public static function assertSQLNotContains( + $needleSQL, + $haystackSQL, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true + ) { + $needleSQL = static::normaliseSQL($needleSQL); + $haystackSQL = static::normaliseSQL($haystackSQL); + if (is_iterable($haystackSQL)) { + return static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } else { + return static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); + } + } + + /** + * Start test environment + */ + public static function start() + { + if (static::is_running_test()) { + return; + } + + // Health check + if (InjectorLoader::inst()->countManifests()) { + throw new LogicException('SapphireTest::start() cannot be called within another application'); + } + static::set_is_running_test(true); + + // Test application + $kernel = new TestKernel(BASE_PATH); + + if (class_exists(HTTPApplication::class)) { + // Mock request + $_SERVER['argv'] = ['vendor/bin/phpunit', '/']; + $request = CLIRequestBuilder::createFromEnvironment(); + + $app = new HTTPApplication($kernel); + $flush = array_key_exists('flush', $request->getVars()); + + // Custom application + $res = $app->execute($request, function (HTTPRequest $request) { + // Start session and execute + $request->getSession()->init($request); + + // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class + // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) + DataObject::reset(); + + // Set dummy controller; + $controller = Controller::create(); + $controller->setRequest($request); + $controller->pushCurrent(); + $controller->doInit(); + }, $flush); + + if ($res && $res->isError()) { + throw new LogicException($res->getBody()); + } + } else { + // Allow flush from the command line in the absence of HTTPApplication's special sauce + $flush = false; + foreach ($_SERVER['argv'] as $arg) { + if (preg_match('/^(--)?flush(=1)?$/', $arg)) { + $flush = true; + } + } + $kernel->boot($flush); + } + + // Register state + static::$state = SapphireTestState::singleton(); + // Register temp DB holder + static::tempDB(); + } + + /** + * Reset the testing database's schema, but only if it is active + * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included + * @param bool $forceCreate Force DB to be created if it doesn't exist + */ + public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false) + { + if (!static::$tempDB) { + return; + } + + // Check if DB is active before reset + if (!static::$tempDB->isUsed()) { + if (!$forceCreate) { + return; + } + static::$tempDB->build(); + } + $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : []; + static::$tempDB->resetDBSchema((array)$extraDataObjects); + } + + /** + * A wrapper for automatically performing callbacks as a user with a specific permission + * + * @param string|array $permCode + * @param callable $callback + * @return mixed + */ + public function actWithPermission($permCode, $callback) + { + return Member::actAs($this->createMemberWithPermission($permCode), $callback); + } + + /** + * Create Member and Group objects on demand with specific permission code + * + * @param string|array $permCode + * @return Member + */ + protected function createMemberWithPermission($permCode) + { + if (is_array($permCode)) { + $permArray = $permCode; + $permCode = implode('.', $permCode); + } else { + $permArray = [$permCode]; + } + + // Check cached member + if (isset($this->cache_generatedMembers[$permCode])) { + $member = $this->cache_generatedMembers[$permCode]; + } else { + // Generate group with these permissions + $group = Group::create(); + $group->Title = "$permCode group"; + $group->write(); + + // Create each individual permission + foreach ($permArray as $permArrayItem) { + $permission = Permission::create(); + $permission->Code = $permArrayItem; + $permission->write(); + $group->Permissions()->add($permission); + } + + $member = Member::get()->filter([ + 'Email' => "$permCode@example.org", + ])->first(); + if (!$member) { + $member = Member::create(); + } + + $member->FirstName = $permCode; + $member->Surname = 'User'; + $member->Email = "$permCode@example.org"; + $member->write(); + $group->Members()->add($member); + + $this->cache_generatedMembers[$permCode] = $member; + } + return $member; + } + + /** + * Create a member and group with the given permission code, and log in with it. + * Returns the member ID. + * + * @param string|array $permCode Either a permission, or list of permissions + * @return int Member ID + */ + public function logInWithPermission($permCode = 'ADMIN') + { + $member = $this->createMemberWithPermission($permCode); + $this->logInAs($member); + return $member->ID; + } + + /** + * Log in as the given member + * + * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in + */ + public function logInAs($member) + { + if (is_numeric($member)) { + $member = DataObject::get_by_id(Member::class, $member); + } elseif (!is_object($member)) { + $member = $this->objFromFixture(Member::class, $member); + } + Injector::inst()->get(IdentityStore::class)->logIn($member); + } + + /** + * Log out the current user + */ + public function logOut() + { + /** @var IdentityStore $store */ + $store = Injector::inst()->get(IdentityStore::class); + $store->logOut(); + } + + /** + * Cache for logInWithPermission() + */ + protected $cache_generatedMembers = []; + + /** + * Test against a theme. + * + * @param string $themeBaseDir themes directory + * @param string $theme Theme name + * @param callable $callback + * @throws Exception + */ + protected function useTestTheme($themeBaseDir, $theme, $callback) + { + Config::nest(); + if (strpos($themeBaseDir, BASE_PATH) === 0) { + $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH)); + } + SSViewer::config()->update('theme_enabled', true); + SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']); + + try { + $callback(); + } finally { + Config::unnest(); + } + } + + /** + * Get fixture paths for this test + * + * @return array List of paths + */ + protected function getFixturePaths() + { + $fixtureFile = static::get_fixture_file(); + if (empty($fixtureFile)) { + return []; + } + + $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile]; + + return array_map(function ($fixtureFilePath) { + return $this->resolveFixturePath($fixtureFilePath); + }, $fixtureFiles); + } + + /** + * Return all extra objects to scaffold for this test + * @return array + */ + public static function getExtraDataObjects() + { + return static::$extra_dataobjects; + } + + /** + * Get additional controller classes to register routes for + * + * @return array + */ + public static function getExtraControllers() + { + return static::$extra_controllers; + } + + /** + * Map a fixture path to a physical file + * + * @param string $fixtureFilePath + * @return string + */ + protected function resolveFixturePath($fixtureFilePath) + { + // support loading via composer name path. + if (strpos($fixtureFilePath, ':') !== false) { + return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath); + } + + // Support fixture paths relative to the test class, rather than relative to webroot + // String checking is faster than file_exists() calls. + $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath); + if ($resolvedPath) { + return $resolvedPath; + } + + // Check if file exists relative to base dir + $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath); + if ($resolvedPath) { + return $resolvedPath; + } + + return $fixtureFilePath; + } + + protected function setUpRoutes() + { + if (!class_exists(Director::class)) { + return; + } + + // Get overridden routes + $rules = $this->getExtraRoutes(); + + // Add all other routes + foreach (Director::config()->uninherited('rules') as $route => $rule) { + if (!isset($rules[$route])) { + $rules[$route] = $rule; + } + } + + // Add default catch-all rule + $rules['$Controller//$Action/$ID/$OtherID'] = '*'; + + // Add controller-name auto-routing + Director::config()->set('rules', $rules); + } + + /** + * Get extra routes to merge into Director.rules + * + * @return array + */ + protected function getExtraRoutes() + { + $rules = []; + foreach ($this->getExtraControllers() as $class) { + $controllerInst = Controller::singleton($class); + $link = Director::makeRelative($controllerInst->Link()); + $route = rtrim($link, '/') . '//$Action/$ID/$OtherID'; + $rules[$route] = $class; + } + return $rules; + } + + // === REIMPLEMENTATION METHODS THAT EXISTED IN SAPPHIRE_TEST 5 === + + /** + * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory() + * + * @param $argument + * @param $type + * @param $value + */ + public static function createPHPUnitFrameworkException($argument, $type, $value = null) + { + $stack = debug_backtrace(false); + + return new PHPUnitFrameworkException( + sprintf( + 'Argument #%d%sof %s::%s() must be a %s', + $argument, + $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ', + $stack[1]['class'], + $stack[1]['function'], + $type + ) + ); + } + + /** + * Returns the annotations for this test. + * + * @return array + */ + public function getAnnotations() + { + return TestUtil::parseTestMethodAnnotations( + get_class($this), + $this->getName(false) + ); + } +}