From 78dd43ed9605f3905f9a0edaea0eeb50064765c0 Mon Sep 17 00:00:00 2001 From: Serge Latyntcev Date: Tue, 18 Dec 2018 14:24:59 +1300 Subject: [PATCH 1/7] ADD / TestSessionState initial implementation TestSessionState model initial implementation TestSessionEnvironment to initialize the state for every scenario and provide API for the clients to use it TestSessionHTTPMiddleware to keep the state fields up to date --- README.md | 5 +++++ src/TestSessionEnvironment.php | 33 +++++++++++++++++++++++++++++++ src/TestSessionHTTPMiddleware.php | 29 +++++++++++++++++++++++++++ src/TestSessionState.php | 33 +++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 src/TestSessionState.php diff --git a/README.md b/README.md index 15fae1f..d8d9cb9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ is a random token stored in the browser session, in order to make the test session specific to the executing browser, and allow multiple people using their own test session in the same webroot. +The module also keeps some metadata about the session state in the database, +so that it may be available for the clients as well. +E.g. the silverstripe-behat-extension may use it through this module APIs, +allowing us to introduce some grey-box testing techniques. + The module also serves as an initializer for the [SilverStripe Behat Extension](https://github.com/silverstripe-labs/silverstripe-behat-extension/). It is required for Behat because the Behat CLI test runner needs to persist diff --git a/src/TestSessionEnvironment.php b/src/TestSessionEnvironment.php index 6e62a7d..9ebd0cc 100644 --- a/src/TestSessionEnvironment.php +++ b/src/TestSessionEnvironment.php @@ -335,6 +335,8 @@ class TestSessionEnvironment // Connect to the new database, overwriting the old DB connection (if any) DB::connect($databaseConfig); } + + TestSessionState::create()->write(); // initialize the session state } // Mailer @@ -363,6 +365,7 @@ class TestSessionEnvironment } $this->saveState($state); + $this->extend('onAfterApplyState'); } @@ -558,4 +561,34 @@ class TestSessionEnvironment { return PUBLIC_PATH . DIRECTORY_SEPARATOR . 'assets_backup'; } + + + /** + * Wait for pending requests + * + * @param int $await Time to wait (in ms) after the last response (to allow the browser react) + * @param int $timeout For how long (in ms) do we wait before giving up + * + * @return bool Whether there are no more pending requests + */ + public function waitForPendingRequests($await=700, $timeout=10000) + { + $timeout = microtime(true) * 10000 + $timeout; + $interval = $await < 300 ? 300 : $await; + do { + $model = TestSessionState::get()->byID(1); + + $pendingRequests = $model->PendingRequests > 0; + $lastRequestAwait = $model->LastResponseTimestamp + $await > microtime(true) * 10000; + + $pending = $pendingRequests || $lastRequestAwait; + + if ($timeout < microtime(true) * 10000) { + // timed out + return false; + } + } while ($pending && (usleep($interval * 1000) || true)); + + return true; + } } diff --git a/src/TestSessionHTTPMiddleware.php b/src/TestSessionHTTPMiddleware.php index c75bc38..42e316b 100644 --- a/src/TestSessionHTTPMiddleware.php +++ b/src/TestSessionHTTPMiddleware.php @@ -8,8 +8,10 @@ use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\Middleware\HTTPMiddleware; use SilverStripe\Core\Injector\Injector; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBDatetime; +use SilverStripe\ORM\Queries\SQLUpdate; /** * Sets state previously initialized through {@link TestSessionController}. @@ -39,15 +41,42 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware // Load test state $this->loadTestState($request); + $this->incrementModelState(); // Call with safe teardown try { return $delegate($request); } finally { $this->restoreTestState($request); + $this->decrementModelState(); } } + protected function incrementModelState() { + $schema = DataObject::getSchema(); + + $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(TestSessionState::class))) + ->addWhere(['ID' => 1]) + ->addAssignments([ + 'PendingRequests' => [ '"PendingRequests" + 1' => [] ] + ]); + + $update->execute(); + } + + protected function decrementModelState() { + $schema = DataObject::getSchema(); + + $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(TestSessionState::class))) + ->addWhere(['ID' => 1]) + ->addAssignments([ + '"PendingRequests"' => [ '"PendingRequests" - 1' => [] ], + '"LastResponseTimestamp"' => microtime(true) * 10000 + ]); + + $update->execute(); + } + /** * Load test state from environment into "real" environment * diff --git a/src/TestSessionState.php b/src/TestSessionState.php new file mode 100644 index 0000000..21c29ed --- /dev/null +++ b/src/TestSessionState.php @@ -0,0 +1,33 @@ + 'Int', + + /** + * The microtime stamp of the last response + * made by the server. + * (well, actually that's rather TestSessionMiddleware) + */ + 'LastResponseTimestamp' => 'Decimal(14, 0)' + ]; +} From 0c078e5027cf33af72355720b68ee295eaa1f963 Mon Sep 17 00:00:00 2001 From: Serge Latyntcev Date: Tue, 8 Jan 2019 16:36:20 +1300 Subject: [PATCH 2/7] TestSessionState implementation refinement; Move increment/decrement methods to TestSessionState class, fix some documentation, fix some code style and readability issues --- src/TestSessionEnvironment.php | 14 +++++--- src/TestSessionHTTPMiddleware.php | 31 ++---------------- src/TestSessionState.php | 53 +++++++++++++++++++++++-------- 3 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/TestSessionEnvironment.php b/src/TestSessionEnvironment.php index 9ebd0cc..9ffbe16 100644 --- a/src/TestSessionEnvironment.php +++ b/src/TestSessionEnvironment.php @@ -571,19 +571,23 @@ class TestSessionEnvironment * * @return bool Whether there are no more pending requests */ - public function waitForPendingRequests($await=700, $timeout=10000) + public function waitForPendingRequests($await = 700, $timeout = 10000) { - $timeout = microtime(true) * 10000 + $timeout; - $interval = $await < 300 ? 300 : $await; + $now = static function () { + return microtime(true) * 10000; + }; + + $timeout = $now() + $timeout; + $interval = max(300, $await); do { $model = TestSessionState::get()->byID(1); $pendingRequests = $model->PendingRequests > 0; - $lastRequestAwait = $model->LastResponseTimestamp + $await > microtime(true) * 10000; + $lastRequestAwait = ($model->LastResponseTimestamp + $await) > $now(); $pending = $pendingRequests || $lastRequestAwait; - if ($timeout < microtime(true) * 10000) { + if ($timeout < $now()) { // timed out return false; } diff --git a/src/TestSessionHTTPMiddleware.php b/src/TestSessionHTTPMiddleware.php index 42e316b..ac8be5c 100644 --- a/src/TestSessionHTTPMiddleware.php +++ b/src/TestSessionHTTPMiddleware.php @@ -8,10 +8,8 @@ use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\Middleware\HTTPMiddleware; use SilverStripe\Core\Injector\Injector; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBDatetime; -use SilverStripe\ORM\Queries\SQLUpdate; /** * Sets state previously initialized through {@link TestSessionController}. @@ -41,42 +39,17 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware // Load test state $this->loadTestState($request); - $this->incrementModelState(); + TestSessionState::incrementState(); // Call with safe teardown try { return $delegate($request); } finally { $this->restoreTestState($request); - $this->decrementModelState(); + TestSessionState::decrementState(); } } - protected function incrementModelState() { - $schema = DataObject::getSchema(); - - $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(TestSessionState::class))) - ->addWhere(['ID' => 1]) - ->addAssignments([ - 'PendingRequests' => [ '"PendingRequests" + 1' => [] ] - ]); - - $update->execute(); - } - - protected function decrementModelState() { - $schema = DataObject::getSchema(); - - $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(TestSessionState::class))) - ->addWhere(['ID' => 1]) - ->addAssignments([ - '"PendingRequests"' => [ '"PendingRequests" - 1' => [] ], - '"LastResponseTimestamp"' => microtime(true) * 10000 - ]); - - $update->execute(); - } - /** * Load test state from environment into "real" environment * diff --git a/src/TestSessionState.php b/src/TestSessionState.php index 21c29ed..3755392 100644 --- a/src/TestSessionState.php +++ b/src/TestSessionState.php @@ -3,7 +3,7 @@ namespace SilverStripe\TestSession; use SilverStripe\ORM\DataObject; - +use SilverStripe\ORM\Queries\SQLUpdate; /** * The session state keeps some metadata about the current test session. @@ -12,22 +12,49 @@ use SilverStripe\ORM\DataObject; * * The client side (Behat) must not use this class straightforwardly, but rather * rely on the API of {@see TestSessionEnvironment} or {@see TestSessionController}. + * + * @property int PendingRequests keeps information about how many requests are in progress + * @property float LastResponseTimestamp microtime of the last response made by the server */ class TestSessionState extends DataObject { - private static $db = [ - /** - * Pending requests to keep information - * about how many requests are in progress - * on the server - */ - 'PendingRequests' => 'Int', + private static $table_name = 'TestSessionState'; - /** - * The microtime stamp of the last response - * made by the server. - * (well, actually that's rather TestSessionMiddleware) - */ + private static $db = [ + 'PendingRequests' => 'Int', 'LastResponseTimestamp' => 'Decimal(14, 0)' ]; + + /** + * Increments TestSessionState.PendingRequests number by 1 + * to indicate we have one more request in progress + */ + public static function incrementState() + { + $schema = DataObject::getSchema(); + + $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(self::class))) + ->addWhere(['ID' => 1]) + ->assignSQL('"PendingRequests"', '"PendingRequests" + 1'); + + $update->execute(); + } + + /** + * Decrements TestSessionState.PendingRequests number by 1 + * to indicate we have one more request in progress. + * Also updates TestSessionState.LastResponseTimestamp + * to the current timestamp. + */ + public static function decrementState() + { + $schema = DataObject::getSchema(); + + $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(self::class))) + ->addWhere(['ID' => 1]) + ->assignSQL('"PendingRequests"', '"PendingRequests" - 1') + ->assign('"LastResponseTimestamp"', microtime(true) * 10000); + + $update->execute(); + } } From 32c8e6a3b171c1cb0727a9f2464c42acbe839579 Mon Sep 17 00:00:00 2001 From: Serge Latyntcev Date: Thu, 10 Jan 2019 11:48:59 +1300 Subject: [PATCH 3/7] Fix TestSessionState and TestSessionEnvironment Fixing a bug that makes it only wait for pending requests, but not for some time after the last response --- src/TestSessionEnvironment.php | 20 +++++++++----------- src/TestSessionState.php | 12 +++++++++++- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/TestSessionEnvironment.php b/src/TestSessionEnvironment.php index 9ffbe16..89443dc 100644 --- a/src/TestSessionEnvironment.php +++ b/src/TestSessionEnvironment.php @@ -573,24 +573,22 @@ class TestSessionEnvironment */ public function waitForPendingRequests($await = 700, $timeout = 10000) { - $now = static function () { - return microtime(true) * 10000; - }; - - $timeout = $now() + $timeout; + $timeout = TestSessionState::microtime() + $timeout; $interval = max(300, $await); + do { + $now = TestSessionState::microtime(); + + if ($timeout < $now) { + return false; + } + $model = TestSessionState::get()->byID(1); $pendingRequests = $model->PendingRequests > 0; - $lastRequestAwait = ($model->LastResponseTimestamp + $await) > $now(); + $lastRequestAwait = ($model->LastResponseTimestamp + $await) > $now; $pending = $pendingRequests || $lastRequestAwait; - - if ($timeout < $now()) { - // timed out - return false; - } } while ($pending && (usleep($interval * 1000) || true)); return true; diff --git a/src/TestSessionState.php b/src/TestSessionState.php index 3755392..1afd494 100644 --- a/src/TestSessionState.php +++ b/src/TestSessionState.php @@ -53,8 +53,18 @@ class TestSessionState extends DataObject $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(self::class))) ->addWhere(['ID' => 1]) ->assignSQL('"PendingRequests"', '"PendingRequests" - 1') - ->assign('"LastResponseTimestamp"', microtime(true) * 10000); + ->assign('"LastResponseTimestamp"', self::microtime()); $update->execute(); } + + /** + * Returns unix timestamp in milliseconds + * + * @return float milliseconds since 1970 + */ + public static function microtime() + { + return round(microtime(true) * 1000); + } } From f54baefb5a04bd1f8742e0a7acf510eb6e8a79fc Mon Sep 17 00:00:00 2001 From: Serge Latyntcev Date: Thu, 10 Jan 2019 15:30:39 +1300 Subject: [PATCH 4/7] Rename TestSessionState::microtime to millitime --- src/TestSessionEnvironment.php | 4 ++-- src/TestSessionState.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TestSessionEnvironment.php b/src/TestSessionEnvironment.php index 89443dc..f6fcb09 100644 --- a/src/TestSessionEnvironment.php +++ b/src/TestSessionEnvironment.php @@ -573,11 +573,11 @@ class TestSessionEnvironment */ public function waitForPendingRequests($await = 700, $timeout = 10000) { - $timeout = TestSessionState::microtime() + $timeout; + $timeout = TestSessionState::millitime() + $timeout; $interval = max(300, $await); do { - $now = TestSessionState::microtime(); + $now = TestSessionState::millitime(); if ($timeout < $now) { return false; diff --git a/src/TestSessionState.php b/src/TestSessionState.php index 1afd494..7cc3835 100644 --- a/src/TestSessionState.php +++ b/src/TestSessionState.php @@ -53,7 +53,7 @@ class TestSessionState extends DataObject $update = SQLUpdate::create(sprintf('"%s"', $schema->tableName(self::class))) ->addWhere(['ID' => 1]) ->assignSQL('"PendingRequests"', '"PendingRequests" - 1') - ->assign('"LastResponseTimestamp"', self::microtime()); + ->assign('"LastResponseTimestamp"', self::millitime()); $update->execute(); } @@ -63,7 +63,7 @@ class TestSessionState extends DataObject * * @return float milliseconds since 1970 */ - public static function microtime() + public static function millitime() { return round(microtime(true) * 1000); } From e957d1e0fd5697d8fe30e13d968f1e394efb0ec3 Mon Sep 17 00:00:00 2001 From: UndefinedOffset Date: Thu, 24 Jan 2019 15:22:29 -0400 Subject: [PATCH 5/7] BUGFIX: Fixed issue where the incorrect database connection could be made when using a stubfile (fixes #60) --- src/TestSessionEnvironment.php | 54 +++++++++++++++++++------------ src/TestSessionHTTPMiddleware.php | 6 ++-- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/TestSessionEnvironment.php b/src/TestSessionEnvironment.php index f6fcb09..fe42208 100644 --- a/src/TestSessionEnvironment.php +++ b/src/TestSessionEnvironment.php @@ -284,26 +284,7 @@ class TestSessionEnvironment } // ensure we have a connection to the database - if (isset($state->database) && $state->database) { - if (!DB::get_conn()) { - // 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::get_conn()->getSelectedDatabase(); - if (isset($state->database) && $db != $state->database) { - $this->oldDatabaseName = $databaseConfig['database']; - $databaseConfig['database'] = $state->database; - DB::connect($databaseConfig); - } - } - } + $this->connectToDatabase($state); // Database if (!$this->isRunningTests()) { @@ -562,6 +543,39 @@ class TestSessionEnvironment return PUBLIC_PATH . DIRECTORY_SEPARATOR . 'assets_backup'; } + /** + * Ensure that there is a connection to the database + * + * @param mixed $state + */ + public function connectToDatabase($state = null) { + if ($state == null) { + $state = $this->getState(); + } + + $databaseConfig = DB::getConfig(); + + if (isset($state->database) && $state->database) { + if (!DB::get_conn()) { + // 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::get_conn()->getSelectedDatabase(); + if (isset($state->database) && $db != $state->database) { + $this->oldDatabaseName = $databaseConfig['database']; + $databaseConfig['database'] = $state->database; + DB::connect($databaseConfig); + } + } + } + } /** * Wait for pending requests diff --git a/src/TestSessionHTTPMiddleware.php b/src/TestSessionHTTPMiddleware.php index ac8be5c..6dd69e0 100644 --- a/src/TestSessionHTTPMiddleware.php +++ b/src/TestSessionHTTPMiddleware.php @@ -80,10 +80,8 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware $file = $testState->stubfile; if (!Director::isLive() && $file && file_exists($file)) { // Connect to the database so the included code can interact with it - $databaseConfig = DB::getConfig(); - if ($databaseConfig) { - DB::connect($databaseConfig); - } + $this->testSessionEnvironment->connectToDatabase(); + include_once($file); } } From 075d960e5d33ffe024eb9aeff0abc09117394a82 Mon Sep 17 00:00:00 2001 From: Bernard Hamlin Date: Wed, 13 Mar 2019 09:15:10 +1300 Subject: [PATCH 6/7] Connect to test database on session load --- src/TestSessionHTTPMiddleware.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TestSessionHTTPMiddleware.php b/src/TestSessionHTTPMiddleware.php index 6dd69e0..c031558 100644 --- a/src/TestSessionHTTPMiddleware.php +++ b/src/TestSessionHTTPMiddleware.php @@ -71,6 +71,9 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware Email::config()->set("send_all_emails_to", null); } + // Connect to the test session database + $this->testSessionEnvironment->connectToDatabase(); + // 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 @@ -79,9 +82,6 @@ class TestSessionHTTPMiddleware implements HTTPMiddleware 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 - $this->testSessionEnvironment->connectToDatabase(); - include_once($file); } } From 81c241741458997c4ef32e0b59a7f3335094df40 Mon Sep 17 00:00:00 2001 From: pjayme Date: Wed, 8 May 2019 12:05:28 +1200 Subject: [PATCH 7/7] updated route config for testsession endpoint --- _config/routes.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/_config/routes.yml b/_config/routes.yml index a5b80c2..2138868 100644 --- a/_config/routes.yml +++ b/_config/routes.yml @@ -1,6 +1,7 @@ --- Name: testsessionroutes --- -SilverStripe\Control\Director: - rules: - dev/testsession: SilverStripe\TestSession\TestSessionController +SilverStripe\Dev\DevelopmentAdmin: + registered_controllers: + testsession: + controller: SilverStripe\TestSession\TestSessionController