API Refactor SapphireTest state management into SapphireTestState

API Remove Injector::unregisterAllObjects()
API Remove FakeController
This commit is contained in:
Damian Mooyman 2017-06-13 14:10:59 +12:00
parent f26ae75c6e
commit c66d433977
23 changed files with 496 additions and 322 deletions

9
_config/tests.yml Normal file
View File

@ -0,0 +1,9 @@
---
Name: sapphiretest
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Dev\SapphireTestState:
properties:
States:
flushable: %$SilverStripe\Dev\FlushableTestState
requirements: %$SilverStripe\View\Dev\RequirementsTestState

View File

@ -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);

View File

@ -1340,6 +1340,8 @@ After (`mysite/_config/config.yml`):
#### <a name="overview-general-removed"></a>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`

View File

@ -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));

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;
}
/**

30
src/Core/TestKernel.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace SilverStripe\Core;
/**
* Kernel for running unit tests
*/
class TestKernel extends AppKernel
{
public function __construct($flush = true)
{
parent::__construct($flush);
$this->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;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace SilverStripe\Dev;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Resettable;
/**
* Clears flushable / resettable objects
*/
class FlushableTestState implements TestState
{
/**
* @var bool
*/
protected $flushed = false;
public function setUp(SapphireTest $test)
{
// Reset all resettables
/** @var Resettable $resettable */
foreach (ClassInfo::implementorsOf(Resettable::class) as $resettable) {
$resettable::reset();
}
}
public function tearDown(SapphireTest $test)
{
}
public function setUpOnce($class)
{
if ($this->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)
{
}
}

View File

@ -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);

View File

@ -0,0 +1,70 @@
<?php
namespace SilverStripe\Dev;
use SilverStripe\Core\Injector\Injectable;
class SapphireTestState implements TestState
{
use Injectable;
/**
* @var TestState[]
*/
protected $states = [];
/**
* @return TestState[]
*/
public function getStates()
{
return $this->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);
}
}
}

39
src/Dev/TestState.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace SilverStripe\Dev;
/**
* Helper for resetting, booting, or cleaning up test state.
*
* SapphireTest will detect all implementors of this interface during test execution
*/
interface TestState extends TestOnly
{
/**
* Called on setup
*
* @param SapphireTest $test
*/
public function setUp(SapphireTest $test);
/**
* Called on tear down
*
* @param SapphireTest $test
*/
public function tearDown(SapphireTest $test);
/**
* Called once on setup
*
* @param string $class Class being setup
*/
public function setUpOnce($class);
/**
* Called once on tear down
*
* @param string $class Class being torn down
*/
public function tearDownOnce($class);
}

View File

@ -10,6 +10,7 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
@ -176,14 +177,6 @@ class Member extends DataObject
*/
private static $unique_identifier_field = 'Email';
/**
* Object for validating user's password
*
* @config
* @var PasswordValidator
*/
private static $password_validator = null;
/**
* @config
* The number of days that a password should be valid for.
@ -369,24 +362,32 @@ class Member extends DataObject
/**
* Set a {@link PasswordValidator} object to use to validate member's passwords.
*
* @param PasswordValidator $pv
* @param PasswordValidator $validator
*/
public static function set_password_validator($pv)
public static function set_password_validator(PasswordValidator $validator = null)
{
self::$password_validator = $pv;
// Override existing config
Config::modify()->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));
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace SilverStripe\View\Dev;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\TestState;
use SilverStripe\View\Requirements;
use SilverStripe\View\Requirements_Backend;
/**
* Resets requirements for test state
*/
class RequirementsTestState implements TestState
{
/**
* @var Requirements_Backend
*/
protected $backend = null;
public function setUp(SapphireTest $test)
{
$this->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)
{
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -3,4 +3,3 @@
require __DIR__ . '/bootstrap/init.php';
require __DIR__ . '/bootstrap/cli.php';
require __DIR__ . '/bootstrap/environment.php';
require __DIR__ . '/bootstrap/phpunit.php';

View File

@ -1,10 +0,0 @@
<?php
// Default database settings
global $project;
$project = 'mysite';
global $database;
$database = '';
require_once('conf/ConfigureFromEnv.php');

View File

@ -1,24 +0,0 @@
<?php
// Bootstrap for running SapphireTests
// Connect to database
use SilverStripe\ORM\DB;
require_once __DIR__ . '/../../src/Core/functions.php';
require_once __DIR__ . '/../php/Control/FakeController.php';
// Bootstrap a mock project configuration
require __DIR__ . '/mysite.php';
global $databaseConfig;
DB::connect($databaseConfig);
// Now set a fake REQUEST_URI
$_SERVER['REQUEST_URI'] = BASE_URL;
// Fake a session
$_SESSION = null;
// Remove the error handler so that PHPUnit can add its own
restore_error_handler();

View File

@ -1,28 +0,0 @@
<?php
namespace SilverStripe\Control\Tests;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Controller;
// Fake a current controller. Way harder than it should be
class FakeController extends Controller
{
public function __construct()
{
parent::__construct();
$this->pushCurrent();
$session = new Session(isset($_SESSION) ? $_SESSION : array());
$request = new HTTPRequest('GET', '/');
$request->setSession($session);
$this->setRequest($request);
$this->setResponse(new HTTPResponse());
$this->doInit();
}
}

View File

@ -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);

View File

@ -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'
);

View File

@ -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()