diff --git a/_config/tests.yml b/_config/tests.yml new file mode 100644 index 000000000..8b40c77e5 --- /dev/null +++ b/_config/tests.yml @@ -0,0 +1,9 @@ +--- +Name: sapphiretest +--- +SilverStripe\Core\Injector\Injector: + SilverStripe\Dev\SapphireTestState: + properties: + States: + flushable: %$SilverStripe\Dev\FlushableTestState + requirements: %$SilverStripe\View\Dev\RequirementsTestState diff --git a/cli-script.php b/cli-script.php index b0246ba25..3b2a702cf 100755 --- a/cli-script.php +++ b/cli-script.php @@ -7,15 +7,14 @@ use SilverStripe\Core\Startup\OutputMiddleware; use SilverStripe\Control\HTTPRequest; require __DIR__ . '/src/includes/cli.php'; -$_SERVER['SCRIPT_FILENAME'] = __FILE__; -chdir(__DIR__); - - require __DIR__ . '/src/includes/autoload.php'; -// Default application +// Build request and detect flush $request = HTTPRequest::createFromEnvironment(); -$kernel = new AppKernel(); +$flush = $request->getVar('flush') || strpos($request->getURL(), 'dev/build') === 0; + +// Default application +$kernel = new AppKernel($flush); $app = new HTTPApplication($kernel); $app->addMiddleware(new OutputMiddleware()); $app->handle($request); diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 4d4408345..be06a4d67 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -1340,6 +1340,8 @@ After (`mysite/_config/config.yml`): #### General and Core Removed API * `CMSMain::buildbrokenlinks()` action is removed. +* `Injector::unregisterAllObjects()` has been removed. Use `unregisterObjects` to unregister + groups of objects limited by type instead. * `SS_Log` class has been removed. Use `Injector::inst()->get(LoggerInterface::class)` instead. * Removed `CMSBatchAction_Delete` * Removed `CMSBatchAction_DeleteFromLive` diff --git a/main.php b/main.php index be8476b4c..8662f7464 100644 --- a/main.php +++ b/main.php @@ -8,9 +8,12 @@ use SilverStripe\Control\HTTPRequest; require __DIR__ . '/src/includes/autoload.php'; -// Default application +// Build request and detect flush $request = HTTPRequest::createFromEnvironment(); -$kernel = new AppKernel(); +$flush = $request->getVar('flush') || strpos($request->getURL(), 'dev/build') === 0; + +// Default application +$kernel = new AppKernel($flush); $app = new HTTPApplication($kernel); $app->addMiddleware(new OutputMiddleware()); $app->addMiddleware(new ErrorControlChainMiddleware($app, $request)); diff --git a/src/Core/AppKernel.php b/src/Core/AppKernel.php index 3e53ccf76..6d39b67de 100644 --- a/src/Core/AppKernel.php +++ b/src/Core/AppKernel.php @@ -27,8 +27,15 @@ use SilverStripe\View\ThemeResourceLoader; class AppKernel extends CoreKernel { - public function __construct() + /** + * @var bool + */ + protected $flush = false; + + public function __construct($flush = false) { + $this->flush = $flush; + // Initialise the dependency injector as soon as possible, as it is // subsequently used by some of the following code $injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class)); @@ -355,28 +362,30 @@ class AppKernel extends CoreKernel ]); } + /** + * @return bool + */ + protected function getIncludeTests() + { + return false; + } + /** * Boot all manifests */ protected function bootManifests() { - // Regenerate the manifest if ?flush is set, or if the database is being built. - // The coupling is a hack, but it removes an annoying bug where new classes - // referenced in _config.php files can be referenced during the build process. - $flush = isset($_GET['flush']) || - trim($_GET['url'], '/') === trim(BASE_URL . '/dev/build', '/'); - // Setup autoloader - $this->getClassLoader()->init(false, $flush); + $this->getClassLoader()->init($this->getIncludeTests(), $this->flush); // Find modules - $this->getModuleLoader()->init(false, $flush); + $this->getModuleLoader()->init($this->getIncludeTests(), $this->flush); // Flush config - if ($flush) { + if ($this->flush) { $config = $this->getConfigLoader()->getManifest(); if ($config instanceof CachedConfigCollection) { - $config->setFlush($flush); + $config->setFlush(true); } } @@ -386,7 +395,7 @@ class AppKernel extends CoreKernel // Find default templates $defaultSet = $this->getThemeResourceLoader()->getSet('$default'); if ($defaultSet instanceof ThemeManifest) { - $defaultSet->init(false, $flush); + $defaultSet->init($this->getIncludeTests(), $this->flush); } } diff --git a/src/Core/Extensible.php b/src/Core/Extensible.php index dc8254267..ca0508e5f 100644 --- a/src/Core/Extensible.php +++ b/src/Core/Extensible.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use SilverStripe\Control\RequestHandler; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; +use SilverStripe\ORM\DataObject; use SilverStripe\View\ViewableData; /** @@ -271,8 +272,8 @@ trait Extensible } Config::modify()->set($class, 'extensions', $config); - // unset singletons to avoid side-effects - Injector::inst()->unregisterAllObjects(); + // Unset singletons + Injector::inst()->unregisterObjects($class); // unset some caches $subclasses = ClassInfo::subclassesFor($class); diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php index 6e0b2bfca..52b76c7c7 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -2,14 +2,15 @@ namespace SilverStripe\Core\Injector; +use ArrayObject; +use InvalidArgumentException; +use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use ReflectionMethod; +use ReflectionObject; +use ReflectionProperty; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; -use ReflectionProperty; -use ArrayObject; -use ReflectionObject; -use ReflectionMethod; -use Psr\Container\ContainerInterface; use SilverStripe\Dev\Deprecation; /** @@ -231,16 +232,10 @@ class Injector implements ContainerInterface protected $nestedFrom = null; /** - * If a user wants to use the injector as a static reference - * - * @param array $config * @return Injector */ - public static function inst($config = null) + public static function inst() { - if (!self::$instance) { - self::$instance = new Injector($config); - } return self::$instance; } @@ -404,7 +399,7 @@ class Injector implements ContainerInterface // make sure the class is set... if (empty($class)) { - throw new \InvalidArgumentException('Missing spec class'); + throw new InvalidArgumentException('Missing spec class'); } $spec['class'] = $class; @@ -651,21 +646,21 @@ class Injector implements ContainerInterface // Format validation if (!is_array($method) || !isset($method[0]) || isset($method[2])) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( "'calls' entries in service definition should be 1 or 2 element arrays." ); } if (!is_string($method[0])) { - throw new \InvalidArgumentException("1st element of a 'calls' entry should be a string"); + throw new InvalidArgumentException("1st element of a 'calls' entry should be a string"); } if (isset($method[1]) && !is_array($method[1])) { - throw new \InvalidArgumentException("2nd element of a 'calls' entry should an arguments array"); + throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array"); } // Check that the method exists and is callable $objectMethod = array($object, $method[0]); if (!is_callable($objectMethod)) { - throw new \InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method"); + throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method"); } // Call it @@ -847,16 +842,18 @@ class Injector implements ContainerInterface * @param object $service The object to register * @param string $replace The name of the object to replace (if different to the * class name of the object to register) + * @return $this */ public function registerService($service, $replace = null) { $registerAt = get_class($service); - if ($replace != null) { + if ($replace !== null) { $registerAt = $replace; } $this->specs[$registerAt] = array('class' => get_class($service)); $this->serviceCache[$registerAt] = $service; + return $this; } /** @@ -864,18 +861,40 @@ class Injector implements ContainerInterface * by the inject * * @param string $name The name to unregister + * @return $this */ public function unregisterNamedObject($name) { unset($this->serviceCache[$name]); + return $this; } /** - * Clear out all objects that are managed by the injetor. + * Clear out objects of one or more types that are managed by the injetor. + * + * @param array|string $types Base class of object (not service name) to remove + * @return $this */ - public function unregisterAllObjects() + public function unregisterObjects($types) { - $this->serviceCache = array('Injector' => $this); + if (!is_array($types)) { + $types = [ $types ]; + } + + // Filter all objects + foreach ($this->serviceCache as $key => $object) { + foreach ($types as $filterClass) { + // Prevent destructive flushing + if (strcasecmp($filterClass, 'object') === 0) { + throw new InvalidArgumentException("Global unregistration is not allowed"); + } + if ($object instanceof $filterClass) { + unset($this->serviceCache[$key]); + break; + } + } + } + return $this; } /** diff --git a/src/Core/TestKernel.php b/src/Core/TestKernel.php new file mode 100644 index 000000000..fd76ce2e6 --- /dev/null +++ b/src/Core/TestKernel.php @@ -0,0 +1,30 @@ +setEnvironment(self::DEV); + } + + /** + * Reset kernel between tests. + * Note: this avoids resetting services (See TestState for service specific reset) + */ + public function reset() + { + $this->setEnvironment(self::DEV); + $this->bootPHP(); + } + + protected function getIncludeTests() + { + return true; + } +} diff --git a/src/Dev/FlushableTestState.php b/src/Dev/FlushableTestState.php new file mode 100644 index 000000000..f1eaaaf2c --- /dev/null +++ b/src/Dev/FlushableTestState.php @@ -0,0 +1,49 @@ +flushed) { + return; + } + $this->flushed = true; + + // Flush all flushable records + /** @var Flushable $class */ + foreach (ClassInfo::implementorsOf(Flushable::class) as $class) { + $class::flush(); + } + } + + public function tearDownOnce($class) + { + } +} diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index 17b2f1788..9db56c892 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -11,37 +11,31 @@ use SilverStripe\Control\Cookie; use SilverStripe\Control\Director; use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Mailer; +use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\Session; -use SilverStripe\Control\Tests\FakeController; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Config\ConfigLoader; -use SilverStripe\Core\Config\CoreConfigFactory; -use SilverStripe\Core\Config\DefaultConfig; use SilverStripe\Core\Extension; -use SilverStripe\Core\Flushable; +use SilverStripe\Core\HTTPApplication; use SilverStripe\Core\Injector\Injector; -use SilverStripe\Core\Manifest\ClassLoader; -use SilverStripe\Core\Manifest\ClassManifest; -use SilverStripe\Core\Resettable; +use SilverStripe\Core\TestKernel; use SilverStripe\i18n\i18n; use SilverStripe\ORM\DataExtension; -use SilverStripe\ORM\SS_List; -use SilverStripe\Security\IdentityStore; -use SilverStripe\Versioned\Versioned; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\SS_List; use SilverStripe\Security\Group; +use SilverStripe\Security\IdentityStore; use SilverStripe\Security\Member; use SilverStripe\Security\Permission; use SilverStripe\Security\Security; -use SilverStripe\View\Requirements; use SilverStripe\View\SSViewer; -use SilverStripe\View\ThemeManifest; -use SilverStripe\View\ThemeResourceLoader; -use Translatable; + +if (!class_exists(PHPUnit_Framework_TestCase::class)) { + return; +} /** * Test case class for the Sapphire framework. @@ -50,12 +44,6 @@ use Translatable; */ class SapphireTest extends PHPUnit_Framework_TestCase { - - /** @config */ - private static $dependencies = array( - 'fixtureFactory' => '%$FixtureFactory', - ); - /** * Path to fixture data for this test run. * If passed as an array, multiple fixture files will be loaded. @@ -77,36 +65,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase * {@link $fixture_file}, which always forces a database build. */ protected $usesDatabase = null; - protected $originalMemberPasswordValidator; - protected $originalRequirements; - protected $originalIsRunningTest; - protected $originalNestedURLsState; - protected $originalMemoryLimit; /** - * @var TestMailer - */ - protected $mailer; - - /** - * Pointer to the manifest that isn't a test manifest - */ - protected static $regular_manifest; - - /** - * @var boolean + * @var bool */ protected static $is_running_test = false; - /** - * @var ClassManifest - */ - protected static $test_class_manifest; - /** * 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 = array(); @@ -173,44 +143,35 @@ class SapphireTest extends PHPUnit_Framework_TestCase protected static $flushedFlushables = false; /** - * Determines if unit tests are currently run, flag set during test bootstrap. - * This is used as a cheap replacement for fully mockable state - * in certain contiditions (e.g. access checks). - * Caution: When set to FALSE, certain controllers might bypass - * access checks, so this is a very security sensitive setting. + * Test application kernel. + * Note: This is always the root kernel. Use Injector to get the current kernel + * if nested. * - * @return boolean + * @var TestKernel + */ + protected static $kernel = null; + + /** + * 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; } - /** - * Set the manifest to be used to look up test classes by helper functions - * - * @param ClassManifest $manifest - */ - public static function set_test_class_manifest($manifest) - { - self::$test_class_manifest = $manifest; - } - - /** - * Return the manifest being used to look up test classes by helper functions - * - * @return ClassManifest - */ - public static function get_test_class_manifest() - { - return self::$test_class_manifest; - } - /** * @return String */ @@ -219,27 +180,30 @@ class SapphireTest extends PHPUnit_Framework_TestCase return static::$fixture_file; } - protected $model; + /** + * @var TestState + */ + protected static $state = null; /** - * State of Versioned before this test is run + * Setup the test. + * Always sets up in order: + * - Reset php state + * - Nest + * - Custom state helpers * - * @var string + * User code should call parent::setUp() before custom setup code */ - protected $originalReadingMode = null; - - protected $originalEnv = null; - protected function setUp() { + self::$kernel->reset(); + //nest config and injector for each test so they are effectively sandboxed per test Config::nest(); Injector::nest(); - $this->originalEnv = Director::get_environment_type(); - if (class_exists(Versioned::class)) { - $this->originalReadingMode = Versioned::get_reading_mode(); - } + // Call state helpers + static::$state->setUp($this); // We cannot run the tests on this abstract class. if (static::class == __CLASS__) { @@ -247,31 +211,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase return; } - // Mark test as being run - $this->originalIsRunningTest = self::$is_running_test; - self::$is_running_test = true; - // i18n needs to be set to the defaults or tests fail i18n::set_locale(i18n::config()->uninherited('default_locale')); // Set default timezone consistently to avoid NZ-specific dependencies date_default_timezone_set('UTC'); - // Remove password validation - $this->originalMemberPasswordValidator = Member::password_validator(); - $this->originalRequirements = Requirements::backend(); Member::set_password_validator(null); Cookie::config()->update('report_errors', false); if (class_exists(RootURLController::class)) { RootURLController::reset(); } - // Reset all resettables - /** @var Resettable $resettable */ - foreach (ClassInfo::implementorsOf(Resettable::class) as $resettable) { - $resettable::reset(); - } - Security::clear_database_is_ready(); // Set up test routes @@ -307,18 +258,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase $this->logInWithPermission("ADMIN"); } - // Preserve memory settings - $this->originalMemoryLimit = ini_get('memory_limit'); - // turn off template debugging SSViewer::config()->update('source_file_comments', false); - // Clear requirements - Requirements::clear(); - // Set up the test mailer - $this->mailer = new TestMailer(); - Injector::inst()->registerService($this->mailer, Mailer::class); + Injector::inst()->registerService(new TestMailer(), Mailer::class); Email::config()->remove('send_all_emails_to'); Email::config()->remove('send_all_emails_from'); Email::config()->remove('cc_all_emails_to'); @@ -332,20 +276,25 @@ class SapphireTest extends PHPUnit_Framework_TestCase * 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 */ public static function setUpBeforeClass() { static::start(); + static::$kernel->reset(); + //nest config and injector for each suite so they are effectively sandboxed Config::nest(); Injector::nest(); $isAltered = false; - if (!Director::isDev()) { - user_error('Tests can only run in "dev" mode', E_USER_ERROR); - } - // Remove any illegal extensions that are present foreach (static::$illegal_extensions as $class => $extensions) { if (!class_exists($class)) { @@ -401,26 +350,30 @@ class SapphireTest extends PHPUnit_Framework_TestCase } // clear singletons, they're caching old extension info // which is used in DatabaseAdmin->doBuild() - Injector::inst()->unregisterAllObjects(); + Injector::inst()->unregisterObjects(DataObject::class); // Set default timezone consistently to avoid NZ-specific dependencies date_default_timezone_set('UTC'); - // Flush all flushable records - $flush = !empty($_GET['flush']); - if (!self::$flushedFlushables && $flush) { - self::$flushedFlushables = true; - foreach (ClassInfo::implementorsOf(Flushable::class) as $class) { - $class::flush(); - } - } + // Call state helpers + static::$state->setUpOnce(static::class); } /** * 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() { + // Call state helpers + static::$state->tearDownOnce(static::class); + // If we have made changes to the extensions present, then migrate the database schema. if (self::$extensions_to_reapply || self::$extensions_to_remove) { // @todo: This isn't strictly necessary to restore extensions, but only to ensure that @@ -451,6 +404,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase if (!empty(self::$extensions_to_reapply) || !empty(self::$extensions_to_remove) || !empty($extraDataObjects)) { static::resetDBSchema(); } + + static::$kernel->reset(); } /** @@ -464,6 +419,12 @@ class SapphireTest extends PHPUnit_Framework_TestCase return $this->fixtureFactory; } + /** + * Sets a new fixture factory + * + * @param FixtureFactory $factory + * @return $this + */ public function setFixtureFactory(FixtureFactory $factory) { $this->fixtureFactory = $factory; @@ -552,11 +513,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase /** * Useful for writing unit tests without hardcoding folder structures. * - * @return String Absolute path to current class. + * @return string Absolute path to current class. */ protected function getCurrentAbsolutePath() { - $filename = self::$test_class_manifest->getItemPath(static::class); + $filename = static::$kernel->getClassLoader()->getItemPath(static::class); if (!$filename) { throw new LogicException("getItemPath returned null for " . static::class); } @@ -564,7 +525,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase } /** - * @return String File path relative to webroot + * @return string File path relative to webroot */ protected function getCurrentRelativePath() { @@ -576,28 +537,17 @@ class SapphireTest extends PHPUnit_Framework_TestCase 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() { - // Preserve memory settings - ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1); - - // Restore email configuration - $this->mailer = null; - - // Restore password validation - if ($this->originalMemberPasswordValidator) { - Member::set_password_validator($this->originalMemberPasswordValidator); - } - - // Restore requirements - if ($this->originalRequirements) { - Requirements::set_backend($this->originalRequirements); - } - - // Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls - self::$is_running_test = $this->originalIsRunningTest; - $this->originalIsRunningTest = null; - // Reset mocked datetime DBDatetime::clear_mock_now(); @@ -610,14 +560,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase $response->removeHeader('Location'); } - Director::set_environment_type($this->originalEnv); - if (class_exists(Versioned::class)) { - Versioned::set_reading_mode($this->originalReadingMode); - } + // Call state helpers + static::$state->setUp($this); //unnest injector / config now that tests are over Injector::unnest(); Config::unnest(); + + // Reset state + self::$kernel->reset(); } public static function assertContains( @@ -650,36 +601,48 @@ class SapphireTest extends PHPUnit_Framework_TestCase /** * Clear the log of emails sent + * + * @return bool True if emails cleared */ public function clearEmails() { - return $this->mailer->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 $to - * @param $from - * @param $subject - * @param $content + * @param string $to + * @param string $from + * @param string $subject + * @param string $content * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles', * 'customHeaders', 'htmlContent', 'inlineImages' */ public function findEmail($to, $from = null, $subject = null, $content = null) { - return $this->mailer->findEmail($to, $from, $subject, $content); + /** @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 $to - * @param $from - * @param $subject - * @param $content - * @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles', - * 'customHeaders', 'htmlContent', inlineImages' + * + * @param string $to + * @param string $from + * @param string $subject + * @param string $content */ public function assertEmailSent($to, $from = null, $subject = null, $content = null) { @@ -1013,43 +976,39 @@ class SapphireTest extends PHPUnit_Framework_TestCase */ public static function start() { - if (!static::is_running_test()) { - new FakeController(); - static::use_test_manifest(); - static::set_is_running_test(true); + if (static::is_running_test()) { + return; } - } + // Health check + if (Injector::inst() || static::$kernel) { + throw new LogicException("SapphireTest::start() cannot be called within another application"); + } + static::set_is_running_test(true); - /** - * Pushes a class and template manifest instance that include tests onto the - * top of the loader stacks. - */ - protected static function use_test_manifest() - { - $flush = !empty($_GET['flush']); - $classManifest = new ClassManifest( - BASE_PATH, - true, - $flush - ); + // Mock request + $session = new Session(isset($_SESSION) ? $_SESSION : array()); + $request = new HTTPRequest('GET', '/'); + $request->setSession($session); - ClassLoader::inst()->pushManifest($classManifest, false); - static::set_test_class_manifest($classManifest); + // Test application + static::$kernel = new TestKernel(); + $app = new HTTPApplication(static::$kernel); - ThemeResourceLoader::inst()->addSet('$default', new ThemeManifest( - BASE_PATH, - project(), - true, - $flush - )); + // Custom application + $app->execute(function () use ($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(); - // Once new class loader is registered, push a new uncached config - $config = CoreConfigFactory::inst()->createCore(); - ConfigLoader::inst()->pushManifest($config); + // Set dummy controller + $controller = Controller::create(); + $controller->setRequest($request); + $controller->pushCurrent(); + $controller->doInit(); + }); - // 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(); + // Register state + static::$state = SapphireTestState::singleton(); } /** @@ -1160,7 +1119,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase DataObject::reset(); // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild() - Injector::inst()->unregisterAllObjects(); + Injector::inst()->unregisterObjects(DataObject::class); $dataClasses = ClassInfo::subclassesFor(DataObject::class); array_shift($dataClasses); diff --git a/src/Dev/SapphireTestState.php b/src/Dev/SapphireTestState.php new file mode 100644 index 000000000..5fa8cead1 --- /dev/null +++ b/src/Dev/SapphireTestState.php @@ -0,0 +1,70 @@ +states; + } + + /** + * @param TestState[] $states + * @return $this + */ + public function setStates(array $states) + { + $this->states = $states; + return $this; + } + + public function setUp(SapphireTest $test) + { + foreach ($this->states as $state) { + $state->setUp($test); + } + } + + public function tearDown(SapphireTest $test) + { + // Tear down in reverse order + /** @var TestState $state */ + foreach (array_reverse($this->states) as $state) { + $state->tearDown($test); + } + } + + public function setUpOnce($class) + { + foreach ($this->states as $state) { + $state->setUpOnce($class); + } + } + + /** + * Called once on tear down + * + * @param string $class Class being torn down + */ + public function tearDownOnce($class) + { + // Tear down in reverse order + /** @var TestState $state */ + foreach (array_reverse($this->states) as $state) { + $state->tearDownOnce($class); + } + } +} diff --git a/src/Dev/TestState.php b/src/Dev/TestState.php new file mode 100644 index 000000000..72ea0f0d8 --- /dev/null +++ b/src/Dev/TestState.php @@ -0,0 +1,39 @@ +remove(Injector::class, PasswordValidator::class); + if ($validator) { + Injector::inst()->registerService($validator, PasswordValidator::class); + } else { + Injector::inst()->unregisterNamedObject(PasswordValidator::class); + } } /** - * Returns the current {@link PasswordValidator} + * Returns the default {@link PasswordValidator} * * @return PasswordValidator */ public static function password_validator() { - return self::$password_validator; + if (Injector::inst()->has(PasswordValidator::class)) { + return Injector::inst()->get(PasswordValidator::class); + } + return null; } - public function isPasswordExpired() { if (!$this->PasswordExpiry) { @@ -1605,16 +1606,17 @@ class Member extends DataObject public function validate() { $valid = parent::validate(); + $validator = static::password_validator(); if (!$this->ID || $this->isChanged('Password')) { - if ($this->Password && self::$password_validator) { - $valid->combineAnd(self::$password_validator->validate($this->Password, $this)); + if ($this->Password && $validator) { + $valid->combineAnd($validator->validate($this->Password, $this)); } } if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) { - if ($this->SetPassword && self::$password_validator) { - $valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this)); + if ($this->SetPassword && $validator) { + $valid->combineAnd($validator->validate($this->SetPassword, $this)); } } diff --git a/src/View/Dev/RequirementsTestState.php b/src/View/Dev/RequirementsTestState.php new file mode 100644 index 000000000..d10dd2505 --- /dev/null +++ b/src/View/Dev/RequirementsTestState.php @@ -0,0 +1,39 @@ +backend = Requirements::backend(); + Requirements::set_backend(Requirements_Backend::create()); + } + + public function tearDown(SapphireTest $test) + { + Requirements::set_backend($this->backend); + } + + public function setUpOnce($class) + { + } + + public function tearDownOnce($class) + { + } +} diff --git a/src/View/Requirements.php b/src/View/Requirements.php index 6a5f8d759..67a4f93b4 100644 --- a/src/View/Requirements.php +++ b/src/View/Requirements.php @@ -2,12 +2,11 @@ namespace SilverStripe\View; +use InvalidArgumentException; +use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Flushable; -use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\Deprecation; -use SilverStripe\Control\HTTPResponse; -use InvalidArgumentException; /** * Requirements tracker for JavaScript and CSS. @@ -104,7 +103,7 @@ class Requirements implements Flushable public static function backend() { if (!self::$backend) { - self::$backend = Injector::inst()->create('SilverStripe\\View\\Requirements_Backend'); + self::$backend = Requirements_Backend::create(); } return self::$backend; } diff --git a/src/View/Requirements_Backend.php b/src/View/Requirements_Backend.php index 3bc6a77df..83ea74dbd 100644 --- a/src/View/Requirements_Backend.php +++ b/src/View/Requirements_Backend.php @@ -2,8 +2,8 @@ namespace SilverStripe\View; -use InvalidArgumentException; use Exception; +use InvalidArgumentException; use SilverStripe\Assets\File; use SilverStripe\Assets\Storage\GeneratedAssetHandler; use SilverStripe\Control\Director; @@ -13,7 +13,6 @@ use SilverStripe\Core\Convert; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Dev\Debug; use SilverStripe\Dev\Deprecation; -use SilverStripe\Dev\SapphireTest; use SilverStripe\i18n\i18n; class Requirements_Backend diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 8e3cc3409..d9b5cece9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -3,4 +3,3 @@ require __DIR__ . '/bootstrap/init.php'; require __DIR__ . '/bootstrap/cli.php'; require __DIR__ . '/bootstrap/environment.php'; -require __DIR__ . '/bootstrap/phpunit.php'; diff --git a/tests/bootstrap/mysite.php b/tests/bootstrap/mysite.php deleted file mode 100644 index 90781792c..000000000 --- a/tests/bootstrap/mysite.php +++ /dev/null @@ -1,10 +0,0 @@ -pushCurrent(); - $session = new Session(isset($_SESSION) ? $_SESSION : array()); - $request = new HTTPRequest('GET', '/'); - $request->setSession($session); - $this->setRequest($request); - $this->setResponse(new HTTPResponse()); - - $this->doInit(); - } -} diff --git a/tests/php/Core/Injector/InjectorTest.php b/tests/php/Core/Injector/InjectorTest.php index c3432867e..620df8875 100644 --- a/tests/php/Core/Injector/InjectorTest.php +++ b/tests/php/Core/Injector/InjectorTest.php @@ -967,7 +967,10 @@ class InjectorTest extends SapphireTest // Test that nested injector values can be overridden Injector::nest(); $this->nestingLevel++; - Injector::inst()->unregisterAllObjects(); + Injector::inst()->unregisterObjects([ + TestStaticInjections::class, + MyParentClass::class, + ]); $newsi = Injector::inst()->get(TestStaticInjections::class); $newsi->backend = new InjectorTest\OriginalRequirementsBackend(); Injector::inst()->registerService($newsi, TestStaticInjections::class); @@ -990,7 +993,10 @@ class InjectorTest extends SapphireTest $this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class)); // Test reset of cache - Injector::inst()->unregisterAllObjects(); + Injector::inst()->unregisterObjects([ + TestStaticInjections::class, + MyParentClass::class, + ]); $si = Injector::inst()->get(TestStaticInjections::class); $this->assertInstanceOf(TestStaticInjections::class, $si); $this->assertInstanceOf(NewRequirementsBackend::class, $si->backend); diff --git a/tests/php/Core/Injector/InjectorTest/TestStaticInjections.php b/tests/php/Core/Injector/InjectorTest/TestStaticInjections.php index bafbf4c83..d3bc7554f 100644 --- a/tests/php/Core/Injector/InjectorTest/TestStaticInjections.php +++ b/tests/php/Core/Injector/InjectorTest/TestStaticInjections.php @@ -6,11 +6,9 @@ use SilverStripe\Dev\TestOnly; class TestStaticInjections implements TestOnly { - public $backend; - /** - * @config -*/ + + /** @config */ private static $dependencies = array( 'backend' => '%$SilverStripe\\Core\\Tests\\Injector\\InjectorTest\\NewRequirementsBackend' ); diff --git a/tests/php/Core/ObjectTest.php b/tests/php/Core/ObjectTest.php index 023896225..93b27f799 100644 --- a/tests/php/Core/ObjectTest.php +++ b/tests/php/Core/ObjectTest.php @@ -2,8 +2,11 @@ namespace SilverStripe\Core\Tests; +use SilverStripe\Control\Controller; use SilverStripe\Core\ClassInfo; +use SilverStripe\Core\Extension; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Tests\ObjectTest\BaseObject; use SilverStripe\Core\Tests\ObjectTest\ExtendTest1; use SilverStripe\Core\Tests\ObjectTest\ExtendTest2; use SilverStripe\Core\Tests\ObjectTest\ExtendTest3; @@ -16,7 +19,6 @@ use SilverStripe\Core\Tests\ObjectTest\MyObject; use SilverStripe\Core\Tests\ObjectTest\MySubObject; use SilverStripe\Core\Tests\ObjectTest\TestExtension; use SilverStripe\Dev\SapphireTest; -use SilverStripe\Control\Controller; use SilverStripe\Versioned\Versioned; /** @@ -30,7 +32,10 @@ class ObjectTest extends SapphireTest protected function setUp() { parent::setUp(); - Injector::inst()->unregisterAllObjects(); + Injector::inst()->unregisterObjects([ + Extension::class, + BaseObject::class, + ]); } public function testHasmethodBehaviour()