mirror of
https://github.com/silverstripe/silverstripe-testsession
synced 2024-10-22 14:06:00 +02:00
Refactor testsession module to use file-based session state storage.
This is a major refactoring of the testsession module to use a persistent file storage instead of using $_SESSION storage. The primary reason for this is for out-of-band tests (e.g. simplifying Behat tests, and testing modules like silverstripe-resque (https://github.com/stojg/silverstripe-resque)). Testing the silverstripe-resque module without this is impossible as the PHP code running the job has been started and loaded into memory long before you started a testsession. By default, this will create a TESTS_RUNNING.json file in your webroot, which means that tests need to be run as a user who has permission to create files there. In practice, this means your webroot needs to be owned by your webserver user. The reason we store the file here is that it will show up as a changed file in version control, so it’s more prominent if developers can’t figure out why there are issues with database content. API CHANGES: - Add persistent file storage (using webroot/TESTS_RUNNING.json) as a base. - Update TestSessionController to use new TestSessionEnvironment class. - Moved extension points from TestSessionController to TestSessionEnvironment. - Moved loadFixtureIntoDb from TestSessionController to TestSessionEnvironment. - Moved setState from TestSessionController to TestSessionEnvironment. - Deprecated the use of TestSessionController::setState() FIXES: - Fixes TestSessionRequestFilter to use new TestSessionEnvironment instead of $_SESSION. MINOR: - Renamed TestSesssionRequestFilter.php to fix spelling error (three ’S’s) - Class did not need renaming, just the file itself.
This commit is contained in:
parent
1f67061901
commit
42be37db53
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
// Determine whether there is a testsession currently running, and if so - setup the persistent details for it.
|
||||
Injector::inst()->get('TestSessionEnvironment')->loadFromFile();
|
@ -8,4 +8,9 @@ Injector:
|
||||
- '%$TestSessionRequestFilter'
|
||||
Member:
|
||||
extensions:
|
||||
- TestSessionMemberExtension
|
||||
- TestSessionMemberExtension
|
||||
---
|
||||
Name: testsession-setup
|
||||
---
|
||||
TestSessionEnvironment:
|
||||
test_state_file: TESTS_RUNNING.json
|
@ -41,7 +41,7 @@ class TestSessionController extends Controller {
|
||||
}
|
||||
|
||||
public function index() {
|
||||
if(Session::get('testsession.started')) {
|
||||
if(Injector::inst()->get('TestSessionEnvironment')->isRunningTests()) {
|
||||
return $this->renderWith('TestSession_inprogress');
|
||||
} else {
|
||||
return $this->renderWith('TestSession_start');
|
||||
@ -49,13 +49,29 @@ class TestSessionController extends Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a test session.
|
||||
* Start a test session. If you wish to extend how the test session is started (and add additional test state),
|
||||
* then take a look at {@link TestSessionEnvironment::startTestSession()} and
|
||||
* {@link TestSessionEnvironment::applyState()} to see the extension points.
|
||||
*/
|
||||
public function start() {
|
||||
$this->extend('onBeforeStart');
|
||||
$params = $this->request->requestVars();
|
||||
$this->setState($params);
|
||||
$this->extend('onAfterStart');
|
||||
|
||||
// Convert datetime from form object into a single string
|
||||
$params = $this->fixDatetimeFormField($params);
|
||||
|
||||
// Remove unnecessary items of form-specific data from being saved in the test session
|
||||
$params = array_diff_key(
|
||||
$params,
|
||||
array(
|
||||
'action_set' => true,
|
||||
'action_start' => true,
|
||||
'SecurityID' => true,
|
||||
'url' => true,
|
||||
'flush' => true,
|
||||
)
|
||||
);
|
||||
|
||||
Injector::inst()->get('TestSessionEnvironment')->startTestSession($params);
|
||||
|
||||
return $this->renderWith('TestSession_inprogress');
|
||||
}
|
||||
@ -111,6 +127,8 @@ class TestSessionController extends Controller {
|
||||
}
|
||||
|
||||
protected function getBaseFields() {
|
||||
$testState = Injector::inst()->get('TestSessionEnvironment')->getState();
|
||||
|
||||
$fields = new FieldList(
|
||||
$textfield = new TextField('fixture', 'Fixture YAML file path'),
|
||||
$datetimeField = new DatetimeField('datetime', 'Custom date'),
|
||||
@ -124,7 +142,7 @@ class TestSessionController extends Controller {
|
||||
$datetimeField->getTimeField()
|
||||
->setConfig('timeformat', 'HH:mm:ss')
|
||||
->setAttribute('placeholder', 'Time (HH:mm:ss)');
|
||||
$datetimeField->setValue(Session::get('testsession.datetime'));
|
||||
$datetimeField->setValue((isset($testState->datetime) ? $testState->datetime : null));
|
||||
|
||||
$this->extend('updateBaseFields', $fields);
|
||||
|
||||
@ -132,32 +150,46 @@ class TestSessionController extends Controller {
|
||||
}
|
||||
|
||||
public function DatabaseName() {
|
||||
// Workaround for bug in Cookie::get(), fixed in 3.1-rc1
|
||||
if(self::$alternative_database_name != -1) {
|
||||
return self::$alternative_database_name;
|
||||
} else if ($dbname = DB::get_alternative_database_name()) {
|
||||
return $dbname;
|
||||
} else {
|
||||
$db = DB::getConn();
|
||||
if(method_exists($db, 'currentDatabase')) return $db->currentDatabase();
|
||||
}
|
||||
$db = DB::getConn();
|
||||
if(method_exists($db, 'currentDatabase')) return $db->currentDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an in-progress {@link TestSessionEnvironment} object with new details. This could be loading in new
|
||||
* fixtures, setting the mocked date to another value etc.
|
||||
*
|
||||
* @return HTMLText Rendered Template
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function set() {
|
||||
if(!Session::get('testsession.started')) {
|
||||
if(!Injector::inst()->get('TestSessionEnvironment')->isRunningTests()) {
|
||||
throw new LogicException("No test session in progress.");
|
||||
}
|
||||
|
||||
$params = $this->request->requestVars();
|
||||
$this->extend('onBeforeSet', $params);
|
||||
$this->setState($params);
|
||||
$this->extend('onAfterSet');
|
||||
|
||||
// Convert datetime from form object into a single string
|
||||
$params = $this->fixDatetimeFormField($params);
|
||||
|
||||
// Remove unnecessary items of form-specific data from being saved in the test session
|
||||
$params = array_diff_key(
|
||||
$params,
|
||||
array(
|
||||
'action_set' => true,
|
||||
'action_start' => true,
|
||||
'SecurityID' => true,
|
||||
'url' => true,
|
||||
'flush' => true,
|
||||
)
|
||||
);
|
||||
|
||||
Injector::inst()->get('TestSessionEnvironment')->updateTestSession($params);
|
||||
|
||||
return $this->renderWith('TestSession_inprogress');
|
||||
}
|
||||
|
||||
public function clear() {
|
||||
if(!Session::get('testsession.started')) {
|
||||
if(!Injector::inst()->get('TestSessionEnvironment')->isRunningTests()) {
|
||||
throw new LogicException("No test session in progress.");
|
||||
}
|
||||
|
||||
@ -175,50 +207,22 @@ class TestSessionController extends Controller {
|
||||
|
||||
return "Cleared database and test state";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* As with {@link self::start()}, if you want to extend the functionality of this, then look at
|
||||
* {@link TestSessionEnvironent::endTestSession()} as the extension points have moved to there now that the logic
|
||||
* is there.
|
||||
*/
|
||||
public function end() {
|
||||
if(!Session::get('testsession.started')) {
|
||||
if(!Injector::inst()->get('TestSessionEnvironment')->isRunningTests()) {
|
||||
throw new LogicException("No test session in progress.");
|
||||
}
|
||||
|
||||
$this->extend('onBeforeEnd');
|
||||
|
||||
if(SapphireTest::using_temp_db()) {
|
||||
SapphireTest::kill_temp_db();
|
||||
DB::set_alternative_database_name(null);
|
||||
// Workaround for bug in Cookie::get(), fixed in 3.1-rc1
|
||||
self::$alternative_database_name = null;
|
||||
}
|
||||
|
||||
Session::clear('testsession');
|
||||
|
||||
$this->extend('onAfterEnd');
|
||||
Injector::inst()->get('TestSessionEnvironment')->endTestSession();
|
||||
|
||||
return $this->renderWith('TestSession_end');
|
||||
}
|
||||
|
||||
protected function loadFixtureIntoDb($fixtureFile) {
|
||||
$realFile = realpath(BASE_PATH.'/'.$fixtureFile);
|
||||
$baseDir = realpath(Director::baseFolder());
|
||||
if(!$realFile || !file_exists($realFile)) {
|
||||
throw new LogicException("Fixture file doesn't exist");
|
||||
} else if(substr($realFile,0,strlen($baseDir)) != $baseDir) {
|
||||
throw new LogicException("Fixture file must be inside $baseDir");
|
||||
} else if(substr($realFile,-4) != '.yml') {
|
||||
throw new LogicException("Fixture file must be a .yml file");
|
||||
} else if(!preg_match('/^([^\/.][^\/]+)\/tests\//', $fixtureFile)) {
|
||||
throw new LogicException("Fixture file must be inside the tests subfolder of one of your modules.");
|
||||
}
|
||||
|
||||
$factory = Injector::inst()->create('FixtureFactory');
|
||||
$fixture = Injector::inst()->create('YamlFixture', $fixtureFile);
|
||||
$fixture->writeInto($factory);
|
||||
|
||||
Session::add_to_array('testsession.fixtures', $fixtureFile);
|
||||
|
||||
return $fixture;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
@ -227,131 +231,19 @@ class TestSessionController extends Controller {
|
||||
}
|
||||
|
||||
public function setState($data) {
|
||||
// Filter keys
|
||||
$data = array_diff_key(
|
||||
$data,
|
||||
array(
|
||||
'action_set' => true,
|
||||
'action_start' => true,
|
||||
'SecurityID' => true,
|
||||
'url' => true,
|
||||
'flush' => true,
|
||||
)
|
||||
);
|
||||
|
||||
// Database
|
||||
if(
|
||||
!Session::get('testsession.started')
|
||||
&& (@$data['createDatabase'] || @$data['database'])
|
||||
) {
|
||||
$dbName = (isset($data['database'])) ? $data['database'] : null;
|
||||
if($dbName) {
|
||||
$dbExists = (bool)DB::query(
|
||||
sprintf("SHOW DATABASES LIKE '%s'", Convert::raw2sql($dbName))
|
||||
)->value();
|
||||
} else {
|
||||
$dbExists = false;
|
||||
}
|
||||
if(!$dbExists) {
|
||||
// Create a new one with a randomized name
|
||||
$dbName = SapphireTest::create_temp_db();
|
||||
}
|
||||
|
||||
// Set existing one, assumes it already has been created
|
||||
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
|
||||
$pattern = strtolower(sprintf('#^%stmpdb\d{7}#', $prefix));
|
||||
if(!preg_match($pattern, $dbName)) {
|
||||
throw new InvalidArgumentException("Invalid database name format");
|
||||
}
|
||||
DB::set_alternative_database_name($dbName);
|
||||
// Workaround for bug in Cookie::get(), fixed in 3.1-rc1
|
||||
self::$alternative_database_name = $dbName;
|
||||
|
||||
// Database name is set in cookie (next request), ensure its available on this request already
|
||||
global $databaseConfig;
|
||||
DB::connect(array_merge($databaseConfig, array('database' => $dbName)));
|
||||
if(isset($data['database'])) unset($data['database']);
|
||||
|
||||
// Import database template if required
|
||||
if(isset($data['createDatabaseTemplate']) && $data['createDatabaseTemplate']) {
|
||||
$sql = file_get_contents($data['createDatabaseTemplate']);
|
||||
// Split into individual query commands, removing comments
|
||||
$sqlCmds = array_filter(
|
||||
preg_split('/\s*;\s*/',
|
||||
preg_replace(array('/^$\n/m', '/^(\/|#).*$\n/m'), '', $sql)
|
||||
)
|
||||
);
|
||||
|
||||
// Execute each query
|
||||
foreach($sqlCmds as $sqlCmd) {
|
||||
DB::query($sqlCmd);
|
||||
}
|
||||
|
||||
// In case the dump involved CREATE TABLE commands, we need to ensure
|
||||
// the schema is still up to date
|
||||
$dbAdmin = new DatabaseAdmin();
|
||||
$dbAdmin->doBuild(true /*quiet*/, false /*populate*/);
|
||||
}
|
||||
}
|
||||
|
||||
// Fixtures
|
||||
$fixtureFile = (isset($data['fixture'])) ? $data['fixture'] : null;
|
||||
if($fixtureFile) {
|
||||
$this->loadFixtureIntoDb($fixtureFile);
|
||||
unset($data['fixture']);
|
||||
}
|
||||
|
||||
// Mailer
|
||||
$mailer = (isset($data['mailer'])) ? $data['mailer'] : null;
|
||||
if($mailer) {
|
||||
if(!class_exists($mailer) || !is_subclass_of($mailer, 'Mailer')) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Class "%s" is not a valid class, or subclass of Mailer',
|
||||
$mailer
|
||||
));
|
||||
}
|
||||
Session::set('testsession.mailer', $mailer);
|
||||
unset($data['mailer']);
|
||||
}
|
||||
|
||||
// Date and time
|
||||
if(@$data['datetime']['date']) {
|
||||
require_once 'Zend/Date.php';
|
||||
// Convert DatetimeField format
|
||||
$datetime = $data['datetime']['date'];
|
||||
$datetime .= ' ';
|
||||
$datetime .= (@$data['datetime']['time']) ? $data['datetime']['time'] : '00:00:00';
|
||||
if(!Zend_Date::isDate($datetime, 'yyyy-MM-dd HH:mm:ss')) {
|
||||
throw new LogicException(sprintf(
|
||||
'Invalid date format "%s", use yyyy-MM-dd HH:mm:ss',
|
||||
$datetime
|
||||
));
|
||||
}
|
||||
Session::set('testsession.datetime', $datetime);
|
||||
unset($data['datetime']);
|
||||
} else {
|
||||
unset($data['datetime']);
|
||||
}
|
||||
|
||||
// Set all other keys without special handling
|
||||
if($data) foreach($data as $k => $v) {
|
||||
Session::set('testsession.' . $k, $v);
|
||||
}
|
||||
|
||||
Session::set('testsession.started', true);
|
||||
Deprecation::notice('3.1', 'TestSessionController::setState() is no longer used, please use '
|
||||
. 'TestSessionEnvironment instead.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function getState() {
|
||||
$stateObj = Injector::inst()->get('TestSessionEnvironment')->getState();
|
||||
$state = array();
|
||||
$state[] = new ArrayData(array(
|
||||
'Name' => 'Database',
|
||||
'Value' => DB::getConn()->currentDatabase(),
|
||||
));
|
||||
$sessionStates = Session::get('testsession');
|
||||
if($sessionStates) foreach($sessionStates as $k => $v) {
|
||||
|
||||
// Convert the stdObject of state into ArrayData
|
||||
foreach($stateObj as $k => $v) {
|
||||
$state[] = new ArrayData(array(
|
||||
'Name' => $k,
|
||||
'Value' => var_export($v, true)
|
||||
@ -391,4 +283,22 @@ class TestSessionController extends Controller {
|
||||
return $templates;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* @param $params array The form fields as passed through from ->start() or ->set()
|
||||
* @return array The form fields, after fixing the datetime field if necessary
|
||||
*/
|
||||
private function fixDatetimeFormField($params) {
|
||||
if(isset($params['datetime']) && is_array($params['datetime']) && !empty($params['datetime']['date'])) {
|
||||
// Convert DatetimeField format from array into string
|
||||
$datetime = $params['datetime']['date'];
|
||||
$datetime .= ' ';
|
||||
$datetime .= (@$params['datetime']['time']) ? $params['datetime']['time'] : '00:00:00';
|
||||
$params['datetime'] = $datetime;
|
||||
} else if(isset($params['datetime']) && empty($params['datetime']['date'])) {
|
||||
unset($params['datetime']); // No datetime, so remove the param entirely
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
}
|
338
code/TestSessionEnvironment.php
Normal file
338
code/TestSessionEnvironment.php
Normal file
@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class TestSessionEnvironment
|
||||
* Abstracts out how testing sessions are started, run, and finished. This should ensure that test sessions details are
|
||||
* enforced across multiple separate requests (for example: behat CLI starts a testsession, then opens a web browser -
|
||||
* the web browser should know nothing about the test session, and shouldn't need to visit dev/testsession/start itself
|
||||
* as it will be loaded from this class). Additionally, Resque workers etc. should also not need to know about it
|
||||
* (although in that case they do need to poll for changes to testsession, as they are a long-lived process that is
|
||||
* generally started much earlier than the test session is created).
|
||||
*
|
||||
* Information here is currently stored on the filesystem - in the webroot, as it's the only persistent place to store
|
||||
* this detail.
|
||||
*/
|
||||
class TestSessionEnvironment extends Object {
|
||||
/**
|
||||
* @var stdClass Test session state. For a valid test session to exist, this needs to contain at least:
|
||||
* - database: The alternate database name that is being used for this test session (e.g. ss_tmpdb_1234567)
|
||||
* It can optionally contain other details that should be passed through many separate requests:
|
||||
* - datetime: Mocked SS_DateTime ({@see TestSessionRequestFilter})
|
||||
* - mailer: Mocked Email sender ({@see TestSessionRequestFilter})
|
||||
* - stubfile: Path to PHP stub file for setup ({@see TestSessionRequestFilter})
|
||||
* Extensions of TestSessionEnvironment can add extra fields in here to be saved and restored on each request.
|
||||
*/
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* @var string The original database name, before we overrode it with our tmpdb.
|
||||
*
|
||||
* Used in {@link self::resetDatabaseName()} when we want to restore the normal DB connection.
|
||||
*/
|
||||
private $oldDatabaseName;
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var string Path (from web-root) to the test state file that indicates a testsession is in progress.
|
||||
* Defaults to value stored in testsession/_config/_config.yml
|
||||
*/
|
||||
private static $test_state_file;
|
||||
|
||||
/**
|
||||
* @var TestSessionEnvironment A singleton of this TestSessionEnvironment, for use with ::inst()
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
public static function inst() {
|
||||
if (!self::$instance) {
|
||||
self::$instance = new TestSessionEnvironment();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for the existence of the file specified by $this->test_state_file
|
||||
*/
|
||||
public function isRunningTests() {
|
||||
return(file_exists(Director::getAbsFile($this->config()->test_state_file)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temp database, sets up any extra requirements, and writes the state file. The database will be
|
||||
* connected to as part of {@link self::applyState()}, so if you're continuing script execution after calling this
|
||||
* method, be aware that the database will be different - so various things may break (e.g. administrator logins
|
||||
* using the SS_DEFAULT_USERNAME / SS_DEFAULT_PASSWORD constants).
|
||||
*
|
||||
* If something isn't explicitly handled here, and needs special handling, then it should be taken care of by an
|
||||
* extension to TestSessionEnvironment. You can either extend onBeforeStartTestSession() or
|
||||
* onAfterStartTestSession(). Alternatively, for more fine-grained control, you can also extend
|
||||
* onBeforeApplyState() and onAfterApplyState(). See the {@link self::applyState()} method for more.
|
||||
*
|
||||
* @param array $state An array of test state options to write.
|
||||
*/
|
||||
public function startTestSession($state) {
|
||||
$this->extend('onBeforeStartTestSession', $state);
|
||||
|
||||
// Convert to JSON and back so we can share the appleState() code between this and ->loadFromFile()
|
||||
$jason = json_encode($state, JSON_FORCE_OBJECT);
|
||||
$state = json_decode($jason);
|
||||
|
||||
$this->applyState($state);
|
||||
$this->persistState();
|
||||
|
||||
$this->extend('onAfterStartTestSession');
|
||||
}
|
||||
|
||||
public function updateTestSession($state) {
|
||||
$this->extend('onBeforeUpdateTestSession', $state);
|
||||
|
||||
// Convert to JSON and back so we can share the appleState() code between this and ->loadFromFile()
|
||||
$jason = json_encode($state, JSON_FORCE_OBJECT);
|
||||
$state = json_decode($jason);
|
||||
|
||||
$this->applyState($state);
|
||||
$this->persistState();
|
||||
|
||||
$this->extend('onAfterUpdateTestSession');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes the database has already been created in startTestSession(), as this method can be called from
|
||||
* _config.php where we don't yet have a DB connection.
|
||||
*
|
||||
* Does not persist the state to the filesystem, {@see self::persistState()}.
|
||||
*
|
||||
* You can extend this by creating an Extension object and implementing either onBeforeApplyState() or
|
||||
* onAfterApplyState() to add your own test state handling in.
|
||||
*
|
||||
* @throws LogicException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function applyState($state) {
|
||||
global $databaseConfig;
|
||||
|
||||
$this->extend('onBeforeApplyState', $state);
|
||||
|
||||
// Load existing state from $this->state into $state, if there is any
|
||||
if($this->state) {
|
||||
foreach($this->state as $k => $v) {
|
||||
if(!isset($state->$k)) $state->$k = $v; // Don't overwrite stuff in $state, as that's the new state
|
||||
}
|
||||
}
|
||||
|
||||
if(!DB::getConn()) {
|
||||
// No connection, so try and connect to tmpdb if it exists
|
||||
if(isset($state->database)) {
|
||||
$this->oldDatabaseName = $databaseConfig['database'];
|
||||
$databaseConfig['database'] = $state->database;
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
DB::connect($databaseConfig);
|
||||
} else {
|
||||
// We've already connected to the database, do a fast check to see what database we're currently using
|
||||
$db = DB::query("SELECT DATABASE()")->value();
|
||||
if(isset($state->database) && $db != $state->database) {
|
||||
$this->oldDatabaseName = $databaseConfig['database'];
|
||||
$databaseConfig['database'] = $state->database;
|
||||
DB::connect($databaseConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Database
|
||||
if(!$this->isRunningTests() && (@$state->createDatabase || @$state->database)) {
|
||||
$dbName = (isset($state->database)) ? $state->database : null;
|
||||
|
||||
if($dbName) {
|
||||
$dbExists = (bool)DB::query(
|
||||
sprintf("SHOW DATABASES LIKE '%s'", Convert::raw2sql($dbName))
|
||||
)->value();
|
||||
} else {
|
||||
$dbExists = false;
|
||||
}
|
||||
|
||||
if(!$dbExists) {
|
||||
// Create a new one with a randomized name
|
||||
$dbName = SapphireTest::create_temp_db();
|
||||
|
||||
$state->database = $dbName; // In case it's changed by the call to SapphireTest::create_temp_db();
|
||||
|
||||
// Set existing one, assumes it already has been created
|
||||
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
|
||||
$pattern = strtolower(sprintf('#^%stmpdb\d{7}#', $prefix));
|
||||
if(!preg_match($pattern, $dbName)) {
|
||||
throw new InvalidArgumentException("Invalid database name format");
|
||||
}
|
||||
|
||||
$this->oldDatabaseName = $databaseConfig['database'];
|
||||
$databaseConfig['database'] = $dbName; // Instead of calling DB::set_alternative_db_name();
|
||||
|
||||
// Connect to the new database, overwriting the old DB connection (if any)
|
||||
DB::connect($databaseConfig);
|
||||
}
|
||||
|
||||
// Import database template if required
|
||||
if(isset($state->createDatabaseTemplate) && $state->createDatabaseTemplate) {
|
||||
$sql = file_get_contents($state->createDatabaseTemplate);
|
||||
// Split into individual query commands, removing comments
|
||||
$sqlCmds = array_filter(
|
||||
preg_split('/\s*;\s*/',
|
||||
preg_replace(array('/^$\n/m', '/^(\/|#).*$\n/m'), '', $sql)
|
||||
)
|
||||
);
|
||||
|
||||
// Execute each query
|
||||
foreach($sqlCmds as $sqlCmd) {
|
||||
DB::query($sqlCmd);
|
||||
}
|
||||
|
||||
// In case the dump involved CREATE TABLE commands, we need to ensure
|
||||
// the schema is still up to date
|
||||
$dbAdmin = new DatabaseAdmin();
|
||||
$dbAdmin->doBuild(true /*quiet*/, false /*populate*/);
|
||||
}
|
||||
|
||||
if(isset($state->createDatabase)) unset($state->createDatabase);
|
||||
}
|
||||
|
||||
// Fixtures
|
||||
$fixtureFile = (isset($state->fixture)) ? $state->fixture : null;
|
||||
if($fixtureFile) {
|
||||
$this->loadFixtureIntoDb($fixtureFile);
|
||||
unset($state->fixture); // Only insert the fixture(s) once, not every time we call this method
|
||||
}
|
||||
|
||||
// Mailer
|
||||
$mailer = (isset($state->mailer)) ? $state->mailer : null;
|
||||
if($mailer) {
|
||||
if(!class_exists($mailer) || !is_subclass_of($mailer, 'Mailer')) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Class "%s" is not a valid class, or subclass of Mailer',
|
||||
$mailer
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Date and time
|
||||
if(isset($state->datetime)) {
|
||||
require_once 'Zend/Date.php';
|
||||
// Convert DatetimeField format
|
||||
if(!Zend_Date::isDate($state->datetime, 'yyyy-MM-dd HH:mm:ss')) {
|
||||
throw new LogicException(sprintf(
|
||||
'Invalid date format "%s", use yyyy-MM-dd HH:mm:ss',
|
||||
$state->datetime
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$this->state = $state;
|
||||
|
||||
$this->extend('onAfterApplyState');
|
||||
}
|
||||
|
||||
public function loadFromFile() {
|
||||
if($this->isRunningTests()) {
|
||||
try {
|
||||
$contents = file_get_contents(Director::getAbsFile($this->config()->test_state_file));
|
||||
$jason = json_decode($contents);
|
||||
|
||||
if(!isset($jason->database)) {
|
||||
throw new \LogicException('The test session file ('
|
||||
. Director::getAbsFile($this->config()->test_state_file) . ') doesn\'t contain a database name.');
|
||||
}
|
||||
|
||||
$this->applyState($jason);
|
||||
} catch(Exception $e) {
|
||||
throw new \Exception("A test session appears to be in progress, but we can't retrieve the details. "
|
||||
. "Try removing the " . Director::getAbsFile($this->config()->test_state_file) . " file. Inner "
|
||||
. "error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes $this->state JSON object into the $this->config()->test_state_file file.
|
||||
*/
|
||||
private function persistState() {
|
||||
file_put_contents(Director::getAbsFile($this->config()->test_state_file), json_encode($this->state));
|
||||
}
|
||||
|
||||
private function removeStateFile() {
|
||||
if(file_exists(Director::getAbsFile($this->config()->test_state_file))) {
|
||||
if(!unlink(Director::getAbsFile($this->config()->test_state_file))) {
|
||||
throw new \Exception('Unable to remove the testsession state file, please remove it manually. File '
|
||||
. 'path: ' . Director::getAbsFile($this->config()->test_state_file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the test session state by restoring the normal database connect (for the rest of this request, if any)
|
||||
* and removes the {@link self::$test_state_file} so that future requests don't use this test state.
|
||||
*
|
||||
* Can be extended by implementing either onBeforeEndTestSession() or onAfterEndTestSession().
|
||||
*
|
||||
* This should implement itself cleanly in case it is called twice (e.g. don't throw errors when the state file
|
||||
* doesn't exist anymore because it's already been cleaned up etc.) This is because during behat test runs where
|
||||
* a queueing system (e.g. silverstripe-resque) is used, the behat module may call this first, and then the forked
|
||||
* worker will call it as well - but there is only one state file that is created.
|
||||
*/
|
||||
public function endTestSession() {
|
||||
$this->extend('onBeforeEndTestSession');
|
||||
|
||||
if(SapphireTest::using_temp_db()) {
|
||||
$this->resetDatabaseName();
|
||||
}
|
||||
|
||||
$this->removeStateFile();
|
||||
|
||||
$this->extend('onAfterEndTestSession');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a YAML fixture into the database as part of the {@link TestSessionController}.
|
||||
*
|
||||
* @param string $fixtureFile The .yml file to load
|
||||
* @return FixtureFactory The loaded fixture
|
||||
* @throws LogicException
|
||||
*/
|
||||
protected function loadFixtureIntoDb($fixtureFile) {
|
||||
$realFile = realpath(BASE_PATH.'/'.$fixtureFile);
|
||||
$baseDir = realpath(Director::baseFolder());
|
||||
if(!$realFile || !file_exists($realFile)) {
|
||||
throw new LogicException("Fixture file doesn't exist");
|
||||
} else if(substr($realFile,0,strlen($baseDir)) != $baseDir) {
|
||||
throw new LogicException("Fixture file must be inside $baseDir");
|
||||
} else if(substr($realFile,-4) != '.yml') {
|
||||
throw new LogicException("Fixture file must be a .yml file");
|
||||
} else if(!preg_match('/^([^\/.][^\/]+)\/tests\//', $fixtureFile)) {
|
||||
throw new LogicException("Fixture file must be inside the tests subfolder of one of your modules.");
|
||||
}
|
||||
|
||||
$factory = Injector::inst()->create('FixtureFactory');
|
||||
$fixture = Injector::inst()->create('YamlFixture', $fixtureFile);
|
||||
$fixture->writeInto($factory);
|
||||
|
||||
$this->state->fixtures[] = $fixtureFile;
|
||||
|
||||
return $fixture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the database connection to use the original database. Called by {@link self::endTestSession()}.
|
||||
*/
|
||||
public function resetDatabaseName() {
|
||||
global $databaseConfig;
|
||||
|
||||
$databaseConfig['database'] = $this->oldDatabaseName;
|
||||
|
||||
DB::connect($databaseConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass Data as taken from the JSON object in {@link self::loadFromFile()}
|
||||
*/
|
||||
public function getState() {
|
||||
return $this->state;
|
||||
}
|
||||
}
|
42
code/TestSessionRequestFilter.php
Normal file
42
code/TestSessionRequestFilter.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Sets state previously initialized through {@link TestSessionController}.
|
||||
*/
|
||||
class TestSessionRequestFilter {
|
||||
|
||||
public function preRequest($req, $session, $model) {
|
||||
if(!Injector::inst()->get('TestSessionEnvironment')->isRunningTests()) return;
|
||||
|
||||
$testState = Injector::inst()->get('TestSessionEnvironment')->getState();
|
||||
|
||||
// Date and time
|
||||
if(isset($testState->datetime)) {
|
||||
SS_Datetime::set_mock_now($testState->datetime);
|
||||
}
|
||||
|
||||
// Register mailer
|
||||
if(isset($testState->mailer)) {
|
||||
$mailer = $testState->mailer;
|
||||
Email::set_mailer(new $mailer());
|
||||
Config::inst()->update("Email","send_all_emails_to", null);
|
||||
}
|
||||
|
||||
// Allows inclusion of a PHP file, usually with procedural commands
|
||||
// to set up required test state. The file can be generated
|
||||
// through {@link TestSessionStubCodeWriter}, and the session state
|
||||
// set through {@link TestSessionController->set()} and the
|
||||
// 'testsession.stubfile' state parameter.
|
||||
if(isset($testState->stubfile)) {
|
||||
$file = $testState->stubfile;
|
||||
if(!Director::isLive() && $file && file_exists($file)) {
|
||||
// Connect to the database so the included code can interact with it
|
||||
global $databaseConfig;
|
||||
if ($databaseConfig) DB::connect($databaseConfig);
|
||||
include_once($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function postRequest() {
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Sets state previously initialized through {@link TestSessionController}.
|
||||
*/
|
||||
class TestSessionRequestFilter {
|
||||
|
||||
public function preRequest($req, $session, $model) {
|
||||
if(!$session->inst_get('testsession.started')) return;
|
||||
|
||||
// Date and time
|
||||
if($datetime = $session->inst_get('testsession.datetime')) {
|
||||
SS_Datetime::set_mock_now($datetime);
|
||||
}
|
||||
|
||||
// Register mailer
|
||||
if($mailer = $session->inst_get('testsession.mailer')) {
|
||||
Email::set_mailer(new $mailer());
|
||||
Config::inst()->update("Email","send_all_emails_to", null);
|
||||
}
|
||||
|
||||
// Allows inclusion of a PHP file, usually with procedural commands
|
||||
// to set up required test state. The file can be generated
|
||||
// through {@link TestSessionStubCodeWriter}, and the session state
|
||||
// set through {@link TestSessionController->set()} and the
|
||||
// 'testsessio.stubfile' state parameter.
|
||||
$file = $session->inst_get('testsession.stubfile');
|
||||
if(!Director::isLive() && $file && file_exists($file)) {
|
||||
// Connect to the database so the included code can interact with it
|
||||
global $databaseConfig;
|
||||
if ($databaseConfig) DB::connect($databaseConfig);
|
||||
include_once($file);
|
||||
}
|
||||
}
|
||||
|
||||
public function postRequest() {
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user