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; use SilverStripe\Control\HTTPRequest;
require __DIR__ . '/src/includes/cli.php'; require __DIR__ . '/src/includes/cli.php';
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
chdir(__DIR__);
require __DIR__ . '/src/includes/autoload.php'; require __DIR__ . '/src/includes/autoload.php';
// Default application // Build request and detect flush
$request = HTTPRequest::createFromEnvironment(); $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 = new HTTPApplication($kernel);
$app->addMiddleware(new OutputMiddleware()); $app->addMiddleware(new OutputMiddleware());
$app->handle($request); $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 #### <a name="overview-general-removed"></a>General and Core Removed API
* `CMSMain::buildbrokenlinks()` action is removed. * `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. * `SS_Log` class has been removed. Use `Injector::inst()->get(LoggerInterface::class)` instead.
* Removed `CMSBatchAction_Delete` * Removed `CMSBatchAction_Delete`
* Removed `CMSBatchAction_DeleteFromLive` * Removed `CMSBatchAction_DeleteFromLive`

View File

@ -8,9 +8,12 @@ use SilverStripe\Control\HTTPRequest;
require __DIR__ . '/src/includes/autoload.php'; require __DIR__ . '/src/includes/autoload.php';
// Default application // Build request and detect flush
$request = HTTPRequest::createFromEnvironment(); $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 = new HTTPApplication($kernel);
$app->addMiddleware(new OutputMiddleware()); $app->addMiddleware(new OutputMiddleware());
$app->addMiddleware(new ErrorControlChainMiddleware($app, $request)); $app->addMiddleware(new ErrorControlChainMiddleware($app, $request));

View File

@ -27,8 +27,15 @@ use SilverStripe\View\ThemeResourceLoader;
class AppKernel extends CoreKernel 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 // Initialise the dependency injector as soon as possible, as it is
// subsequently used by some of the following code // subsequently used by some of the following code
$injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class)); $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 * Boot all manifests
*/ */
protected function bootManifests() 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 // Setup autoloader
$this->getClassLoader()->init(false, $flush); $this->getClassLoader()->init($this->getIncludeTests(), $this->flush);
// Find modules // Find modules
$this->getModuleLoader()->init(false, $flush); $this->getModuleLoader()->init($this->getIncludeTests(), $this->flush);
// Flush config // Flush config
if ($flush) { if ($this->flush) {
$config = $this->getConfigLoader()->getManifest(); $config = $this->getConfigLoader()->getManifest();
if ($config instanceof CachedConfigCollection) { if ($config instanceof CachedConfigCollection) {
$config->setFlush($flush); $config->setFlush(true);
} }
} }
@ -386,7 +395,7 @@ class AppKernel extends CoreKernel
// Find default templates // Find default templates
$defaultSet = $this->getThemeResourceLoader()->getSet('$default'); $defaultSet = $this->getThemeResourceLoader()->getSet('$default');
if ($defaultSet instanceof ThemeManifest) { 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\Control\RequestHandler;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
/** /**
@ -271,8 +272,8 @@ trait Extensible
} }
Config::modify()->set($class, 'extensions', $config); Config::modify()->set($class, 'extensions', $config);
// unset singletons to avoid side-effects // Unset singletons
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects($class);
// unset some caches // unset some caches
$subclasses = ClassInfo::subclassesFor($class); $subclasses = ClassInfo::subclassesFor($class);

View File

@ -2,14 +2,15 @@
namespace SilverStripe\Core\Injector; namespace SilverStripe\Core\Injector;
use ArrayObject;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
use ReflectionMethod;
use ReflectionObject;
use ReflectionProperty;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use ReflectionProperty;
use ArrayObject;
use ReflectionObject;
use ReflectionMethod;
use Psr\Container\ContainerInterface;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
/** /**
@ -231,16 +232,10 @@ class Injector implements ContainerInterface
protected $nestedFrom = null; protected $nestedFrom = null;
/** /**
* If a user wants to use the injector as a static reference
*
* @param array $config
* @return Injector * @return Injector
*/ */
public static function inst($config = null) public static function inst()
{ {
if (!self::$instance) {
self::$instance = new Injector($config);
}
return self::$instance; return self::$instance;
} }
@ -404,7 +399,7 @@ class Injector implements ContainerInterface
// make sure the class is set... // make sure the class is set...
if (empty($class)) { if (empty($class)) {
throw new \InvalidArgumentException('Missing spec class'); throw new InvalidArgumentException('Missing spec class');
} }
$spec['class'] = $class; $spec['class'] = $class;
@ -651,21 +646,21 @@ class Injector implements ContainerInterface
// Format validation // Format validation
if (!is_array($method) || !isset($method[0]) || isset($method[2])) { 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." "'calls' entries in service definition should be 1 or 2 element arrays."
); );
} }
if (!is_string($method[0])) { 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])) { 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 // Check that the method exists and is callable
$objectMethod = array($object, $method[0]); $objectMethod = array($object, $method[0]);
if (!is_callable($objectMethod)) { 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 // Call it
@ -847,16 +842,18 @@ class Injector implements ContainerInterface
* @param object $service The object to register * @param object $service The object to register
* @param string $replace The name of the object to replace (if different to the * @param string $replace The name of the object to replace (if different to the
* class name of the object to register) * class name of the object to register)
* @return $this
*/ */
public function registerService($service, $replace = null) public function registerService($service, $replace = null)
{ {
$registerAt = get_class($service); $registerAt = get_class($service);
if ($replace != null) { if ($replace !== null) {
$registerAt = $replace; $registerAt = $replace;
} }
$this->specs[$registerAt] = array('class' => get_class($service)); $this->specs[$registerAt] = array('class' => get_class($service));
$this->serviceCache[$registerAt] = $service; $this->serviceCache[$registerAt] = $service;
return $this;
} }
/** /**
@ -864,18 +861,40 @@ class Injector implements ContainerInterface
* by the inject * by the inject
* *
* @param string $name The name to unregister * @param string $name The name to unregister
* @return $this
*/ */
public function unregisterNamedObject($name) public function unregisterNamedObject($name)
{ {
unset($this->serviceCache[$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\Director;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\Tests\FakeController;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; 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\Extension;
use SilverStripe\Core\Flushable; use SilverStripe\Core\HTTPApplication;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\TestKernel;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Resettable;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\View\Requirements;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\ThemeManifest;
use SilverStripe\View\ThemeResourceLoader; if (!class_exists(PHPUnit_Framework_TestCase::class)) {
use Translatable; return;
}
/** /**
* Test case class for the Sapphire framework. * Test case class for the Sapphire framework.
@ -50,12 +44,6 @@ use Translatable;
*/ */
class SapphireTest extends PHPUnit_Framework_TestCase class SapphireTest extends PHPUnit_Framework_TestCase
{ {
/** @config */
private static $dependencies = array(
'fixtureFactory' => '%$FixtureFactory',
);
/** /**
* Path to fixture data for this test run. * Path to fixture data for this test run.
* If passed as an array, multiple fixture files will be loaded. * 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. * {@link $fixture_file}, which always forces a database build.
*/ */
protected $usesDatabase = null; protected $usesDatabase = null;
protected $originalMemberPasswordValidator;
protected $originalRequirements;
protected $originalIsRunningTest;
protected $originalNestedURLsState;
protected $originalMemoryLimit;
/** /**
* @var TestMailer * @var bool
*/
protected $mailer;
/**
* Pointer to the manifest that isn't a test manifest
*/
protected static $regular_manifest;
/**
* @var boolean
*/ */
protected static $is_running_test = false; protected static $is_running_test = false;
/**
* @var ClassManifest
*/
protected static $test_class_manifest;
/** /**
* By default, setUp() does not require default records. Pass * By default, setUp() does not require default records. Pass
* class names in here, and the require/augment default records * class names in here, and the require/augment default records
* function will be called on them. * function will be called on them.
*
* @var array
*/ */
protected $requireDefaultRecordsFrom = array(); protected $requireDefaultRecordsFrom = array();
@ -173,44 +143,35 @@ class SapphireTest extends PHPUnit_Framework_TestCase
protected static $flushedFlushables = false; protected static $flushedFlushables = false;
/** /**
* Determines if unit tests are currently run, flag set during test bootstrap. * Test application kernel.
* This is used as a cheap replacement for fully mockable state * Note: This is always the root kernel. Use Injector to get the current kernel
* in certain contiditions (e.g. access checks). * if nested.
* Caution: When set to FALSE, certain controllers might bypass
* access checks, so this is a very security sensitive setting.
* *
* @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() protected static function is_running_test()
{ {
return self::$is_running_test; return self::$is_running_test;
} }
/**
* Set test running state
*
* @param bool $bool
*/
protected static function set_is_running_test($bool) protected static function set_is_running_test($bool)
{ {
self::$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 * @return String
*/ */
@ -219,27 +180,30 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return static::$fixture_file; 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() protected function setUp()
{ {
self::$kernel->reset();
//nest config and injector for each test so they are effectively sandboxed per test //nest config and injector for each test so they are effectively sandboxed per test
Config::nest(); Config::nest();
Injector::nest(); Injector::nest();
$this->originalEnv = Director::get_environment_type(); // Call state helpers
if (class_exists(Versioned::class)) { static::$state->setUp($this);
$this->originalReadingMode = Versioned::get_reading_mode();
}
// We cannot run the tests on this abstract class. // We cannot run the tests on this abstract class.
if (static::class == __CLASS__) { if (static::class == __CLASS__) {
@ -247,31 +211,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return; 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 needs to be set to the defaults or tests fail
i18n::set_locale(i18n::config()->uninherited('default_locale')); i18n::set_locale(i18n::config()->uninherited('default_locale'));
// Set default timezone consistently to avoid NZ-specific dependencies // Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
// Remove password validation
$this->originalMemberPasswordValidator = Member::password_validator();
$this->originalRequirements = Requirements::backend();
Member::set_password_validator(null); Member::set_password_validator(null);
Cookie::config()->update('report_errors', false); Cookie::config()->update('report_errors', false);
if (class_exists(RootURLController::class)) { if (class_exists(RootURLController::class)) {
RootURLController::reset(); RootURLController::reset();
} }
// Reset all resettables
/** @var Resettable $resettable */
foreach (ClassInfo::implementorsOf(Resettable::class) as $resettable) {
$resettable::reset();
}
Security::clear_database_is_ready(); Security::clear_database_is_ready();
// Set up test routes // Set up test routes
@ -307,18 +258,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$this->logInWithPermission("ADMIN"); $this->logInWithPermission("ADMIN");
} }
// Preserve memory settings
$this->originalMemoryLimit = ini_get('memory_limit');
// turn off template debugging // turn off template debugging
SSViewer::config()->update('source_file_comments', false); SSViewer::config()->update('source_file_comments', false);
// Clear requirements
Requirements::clear();
// Set up the test mailer // Set up the test mailer
$this->mailer = new TestMailer(); Injector::inst()->registerService(new TestMailer(), Mailer::class);
Injector::inst()->registerService($this->mailer, Mailer::class);
Email::config()->remove('send_all_emails_to'); Email::config()->remove('send_all_emails_to');
Email::config()->remove('send_all_emails_from'); Email::config()->remove('send_all_emails_from');
Email::config()->remove('cc_all_emails_to'); 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, * don't change state for any called method inside the test,
* e.g. dynamically adding an extension. See {@link teardownAfterClass()} * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
* for tearing down the state again. * 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() public static function setUpBeforeClass()
{ {
static::start(); static::start();
static::$kernel->reset();
//nest config and injector for each suite so they are effectively sandboxed //nest config and injector for each suite so they are effectively sandboxed
Config::nest(); Config::nest();
Injector::nest(); Injector::nest();
$isAltered = false; $isAltered = false;
if (!Director::isDev()) {
user_error('Tests can only run in "dev" mode', E_USER_ERROR);
}
// Remove any illegal extensions that are present // Remove any illegal extensions that are present
foreach (static::$illegal_extensions as $class => $extensions) { foreach (static::$illegal_extensions as $class => $extensions) {
if (!class_exists($class)) { if (!class_exists($class)) {
@ -401,26 +350,30 @@ class SapphireTest extends PHPUnit_Framework_TestCase
} }
// clear singletons, they're caching old extension info // clear singletons, they're caching old extension info
// which is used in DatabaseAdmin->doBuild() // which is used in DatabaseAdmin->doBuild()
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects(DataObject::class);
// Set default timezone consistently to avoid NZ-specific dependencies // Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
// Flush all flushable records // Call state helpers
$flush = !empty($_GET['flush']); static::$state->setUpOnce(static::class);
if (!self::$flushedFlushables && $flush) {
self::$flushedFlushables = true;
foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
$class::flush();
}
}
} }
/** /**
* tearDown method that's called once per test class rather once per test method. * 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() 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 we have made changes to the extensions present, then migrate the database schema.
if (self::$extensions_to_reapply || self::$extensions_to_remove) { if (self::$extensions_to_reapply || self::$extensions_to_remove) {
// @todo: This isn't strictly necessary to restore extensions, but only to ensure that // @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)) { if (!empty(self::$extensions_to_reapply) || !empty(self::$extensions_to_remove) || !empty($extraDataObjects)) {
static::resetDBSchema(); static::resetDBSchema();
} }
static::$kernel->reset();
} }
/** /**
@ -464,6 +419,12 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return $this->fixtureFactory; return $this->fixtureFactory;
} }
/**
* Sets a new fixture factory
*
* @param FixtureFactory $factory
* @return $this
*/
public function setFixtureFactory(FixtureFactory $factory) public function setFixtureFactory(FixtureFactory $factory)
{ {
$this->fixtureFactory = $factory; $this->fixtureFactory = $factory;
@ -552,11 +513,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase
/** /**
* Useful for writing unit tests without hardcoding folder structures. * 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() protected function getCurrentAbsolutePath()
{ {
$filename = self::$test_class_manifest->getItemPath(static::class); $filename = static::$kernel->getClassLoader()->getItemPath(static::class);
if (!$filename) { if (!$filename) {
throw new LogicException("getItemPath returned null for " . static::class); 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() protected function getCurrentRelativePath()
{ {
@ -576,28 +537,17 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return $path; 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() 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 // Reset mocked datetime
DBDatetime::clear_mock_now(); DBDatetime::clear_mock_now();
@ -610,14 +560,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$response->removeHeader('Location'); $response->removeHeader('Location');
} }
Director::set_environment_type($this->originalEnv); // Call state helpers
if (class_exists(Versioned::class)) { static::$state->setUp($this);
Versioned::set_reading_mode($this->originalReadingMode);
}
//unnest injector / config now that tests are over //unnest injector / config now that tests are over
Injector::unnest(); Injector::unnest();
Config::unnest(); Config::unnest();
// Reset state
self::$kernel->reset();
} }
public static function assertContains( public static function assertContains(
@ -650,36 +601,48 @@ class SapphireTest extends PHPUnit_Framework_TestCase
/** /**
* Clear the log of emails sent * Clear the log of emails sent
*
* @return bool True if emails cleared
*/ */
public function clearEmails() 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. * 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. * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
* @param $to * @param string $to
* @param $from * @param string $from
* @param $subject * @param string $subject
* @param $content * @param string $content
* @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles', * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
* 'customHeaders', 'htmlContent', 'inlineImages' * 'customHeaders', 'htmlContent', 'inlineImages'
*/ */
public function findEmail($to, $from = null, $subject = null, $content = null) 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() * 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. * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
* @param $to *
* @param $from * @param string $to
* @param $subject * @param string $from
* @param $content * @param string $subject
* @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles', * @param string $content
* 'customHeaders', 'htmlContent', inlineImages'
*/ */
public function assertEmailSent($to, $from = null, $subject = null, $content = null) public function assertEmailSent($to, $from = null, $subject = null, $content = null)
{ {
@ -1013,43 +976,39 @@ class SapphireTest extends PHPUnit_Framework_TestCase
*/ */
public static function start() public static function start()
{ {
if (!static::is_running_test()) { if (static::is_running_test()) {
new FakeController(); return;
static::use_test_manifest();
static::set_is_running_test(true);
} }
} // Health check
if (Injector::inst() || static::$kernel) {
throw new LogicException("SapphireTest::start() cannot be called within another application");
}
static::set_is_running_test(true);
/** // Mock request
* Pushes a class and template manifest instance that include tests onto the $session = new Session(isset($_SESSION) ? $_SESSION : array());
* top of the loader stacks. $request = new HTTPRequest('GET', '/');
*/ $request->setSession($session);
protected static function use_test_manifest()
{
$flush = !empty($_GET['flush']);
$classManifest = new ClassManifest(
BASE_PATH,
true,
$flush
);
ClassLoader::inst()->pushManifest($classManifest, false); // Test application
static::set_test_class_manifest($classManifest); static::$kernel = new TestKernel();
$app = new HTTPApplication(static::$kernel);
ThemeResourceLoader::inst()->addSet('$default', new ThemeManifest( // Custom application
BASE_PATH, $app->execute(function () use ($request) {
project(), // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
true, // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
$flush DataObject::reset();
));
// Once new class loader is registered, push a new uncached config // Set dummy controller
$config = CoreConfigFactory::inst()->createCore(); $controller = Controller::create();
ConfigLoader::inst()->pushManifest($config); $controller->setRequest($request);
$controller->pushCurrent();
$controller->doInit();
});
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class // Register state
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) static::$state = SapphireTestState::singleton();
DataObject::reset();
} }
/** /**
@ -1160,7 +1119,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
DataObject::reset(); DataObject::reset();
// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild() // 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); $dataClasses = ClassInfo::subclassesFor(DataObject::class);
array_shift($dataClasses); 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\Director;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Email\Mailer;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
@ -176,14 +177,6 @@ class Member extends DataObject
*/ */
private static $unique_identifier_field = 'Email'; private static $unique_identifier_field = 'Email';
/**
* Object for validating user's password
*
* @config
* @var PasswordValidator
*/
private static $password_validator = null;
/** /**
* @config * @config
* The number of days that a password should be valid for. * 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. * 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 * @return PasswordValidator
*/ */
public static function password_validator() 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() public function isPasswordExpired()
{ {
if (!$this->PasswordExpiry) { if (!$this->PasswordExpiry) {
@ -1605,16 +1606,17 @@ class Member extends DataObject
public function validate() public function validate()
{ {
$valid = parent::validate(); $valid = parent::validate();
$validator = static::password_validator();
if (!$this->ID || $this->isChanged('Password')) { if (!$this->ID || $this->isChanged('Password')) {
if ($this->Password && self::$password_validator) { if ($this->Password && $validator) {
$valid->combineAnd(self::$password_validator->validate($this->Password, $this)); $valid->combineAnd($validator->validate($this->Password, $this));
} }
} }
if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) { if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) {
if ($this->SetPassword && self::$password_validator) { if ($this->SetPassword && $validator) {
$valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this)); $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; namespace SilverStripe\View;
use InvalidArgumentException;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Flushable; use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Control\HTTPResponse;
use InvalidArgumentException;
/** /**
* Requirements tracker for JavaScript and CSS. * Requirements tracker for JavaScript and CSS.
@ -104,7 +103,7 @@ class Requirements implements Flushable
public static function backend() public static function backend()
{ {
if (!self::$backend) { if (!self::$backend) {
self::$backend = Injector::inst()->create('SilverStripe\\View\\Requirements_Backend'); self::$backend = Requirements_Backend::create();
} }
return self::$backend; return self::$backend;
} }

View File

@ -2,8 +2,8 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use InvalidArgumentException;
use Exception; use Exception;
use InvalidArgumentException;
use SilverStripe\Assets\File; use SilverStripe\Assets\File;
use SilverStripe\Assets\Storage\GeneratedAssetHandler; use SilverStripe\Assets\Storage\GeneratedAssetHandler;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
@ -13,7 +13,6 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
class Requirements_Backend class Requirements_Backend

View File

@ -3,4 +3,3 @@
require __DIR__ . '/bootstrap/init.php'; require __DIR__ . '/bootstrap/init.php';
require __DIR__ . '/bootstrap/cli.php'; require __DIR__ . '/bootstrap/cli.php';
require __DIR__ . '/bootstrap/environment.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 // Test that nested injector values can be overridden
Injector::nest(); Injector::nest();
$this->nestingLevel++; $this->nestingLevel++;
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects([
TestStaticInjections::class,
MyParentClass::class,
]);
$newsi = Injector::inst()->get(TestStaticInjections::class); $newsi = Injector::inst()->get(TestStaticInjections::class);
$newsi->backend = new InjectorTest\OriginalRequirementsBackend(); $newsi->backend = new InjectorTest\OriginalRequirementsBackend();
Injector::inst()->registerService($newsi, TestStaticInjections::class); Injector::inst()->registerService($newsi, TestStaticInjections::class);
@ -990,7 +993,10 @@ class InjectorTest extends SapphireTest
$this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class)); $this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class));
// Test reset of cache // Test reset of cache
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects([
TestStaticInjections::class,
MyParentClass::class,
]);
$si = Injector::inst()->get(TestStaticInjections::class); $si = Injector::inst()->get(TestStaticInjections::class);
$this->assertInstanceOf(TestStaticInjections::class, $si); $this->assertInstanceOf(TestStaticInjections::class, $si);
$this->assertInstanceOf(NewRequirementsBackend::class, $si->backend); $this->assertInstanceOf(NewRequirementsBackend::class, $si->backend);

View File

@ -6,11 +6,9 @@ use SilverStripe\Dev\TestOnly;
class TestStaticInjections implements TestOnly class TestStaticInjections implements TestOnly
{ {
public $backend; public $backend;
/**
* @config /** @config */
*/
private static $dependencies = array( private static $dependencies = array(
'backend' => '%$SilverStripe\\Core\\Tests\\Injector\\InjectorTest\\NewRequirementsBackend' 'backend' => '%$SilverStripe\\Core\\Tests\\Injector\\InjectorTest\\NewRequirementsBackend'
); );

View File

@ -2,8 +2,11 @@
namespace SilverStripe\Core\Tests; namespace SilverStripe\Core\Tests;
use SilverStripe\Control\Controller;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Extension;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Tests\ObjectTest\BaseObject;
use SilverStripe\Core\Tests\ObjectTest\ExtendTest1; use SilverStripe\Core\Tests\ObjectTest\ExtendTest1;
use SilverStripe\Core\Tests\ObjectTest\ExtendTest2; use SilverStripe\Core\Tests\ObjectTest\ExtendTest2;
use SilverStripe\Core\Tests\ObjectTest\ExtendTest3; 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\MySubObject;
use SilverStripe\Core\Tests\ObjectTest\TestExtension; use SilverStripe\Core\Tests\ObjectTest\TestExtension;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
/** /**
@ -30,7 +32,10 @@ class ObjectTest extends SapphireTest
protected function setUp() protected function setUp()
{ {
parent::setUp(); parent::setUp();
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects([
Extension::class,
BaseObject::class,
]);
} }
public function testHasmethodBehaviour() public function testHasmethodBehaviour()