@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Provide isolated, read/write storage fixtures for unit tests

Summary:
- Unit tests can request storage fixtures.
- We build one fixture across all tests in the process, which can quickstart (takes roughly 1s to build, 200ms to destroy for me). This is a one-time cost for running an arbitrary number of fixture-based tests.
- We isolate all the connections inside transactions for each test, so individual tests don't affect one another.

Test Plan: Ran unit tests, which cover the important properties of fixtures.

Reviewers: btrahan, vrana, jungejason, edward

Reviewed By: btrahan

CC: aran, davidreuss

Maniphest Tasks: T140

Differential Revision: https://secure.phabricator.com/D2345

+262 -5
+3
src/__phutil_library_map__.php
··· 453 453 'JavelinViewExampleServerView' => 'applications/uiexample/examples/client', 454 454 'LiskDAO' => 'storage/lisk/dao', 455 455 'LiskEphemeralObjectException' => 'storage/lisk/dao', 456 + 'LiskFixtureTestCase' => 'storage/lisk/dao/__tests__', 456 457 'LiskIsolationTestCase' => 'storage/lisk/dao/__tests__', 457 458 'LiskIsolationTestDAO' => 'storage/lisk/dao/__tests__', 458 459 'LiskIsolationTestDAOException' => 'storage/lisk/dao/__tests__', ··· 896 897 'PhabricatorSortTableExample' => 'applications/uiexample/examples/sorttable', 897 898 'PhabricatorStandardPageView' => 'view/page/standard', 898 899 'PhabricatorStatusController' => 'applications/status/base', 900 + 'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/storage', 899 901 'PhabricatorStorageManagementAPI' => 'infrastructure/setup/storage/management', 900 902 'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/setup/storage/workflow/databases', 901 903 'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/setup/storage/workflow/destroy', ··· 1395 1397 'JavelinReactorExample' => 'PhabricatorUIExample', 1396 1398 'JavelinViewExample' => 'PhabricatorUIExample', 1397 1399 'JavelinViewExampleServerView' => 'AphrontView', 1400 + 'LiskFixtureTestCase' => 'PhabricatorTestCase', 1398 1401 'LiskIsolationTestCase' => 'PhabricatorTestCase', 1399 1402 'LiskIsolationTestDAO' => 'LiskDAO', 1400 1403 'ManiphestAction' => 'PhrictionConstants',
+1 -1
src/applications/base/storage/lisk/PhabricatorLiskDAO.php
··· 72 72 /** 73 73 * @task config 74 74 */ 75 - public static function popStorageNamespace($namespace) { 75 + public static function popStorageNamespace() { 76 76 array_pop(self::$namespaceStack); 77 77 } 78 78
+51
src/infrastructure/testing/fixture/storage/PhabricatorStorageFixtureScopeGuard.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + /** 20 + * Used by unit tests to build storage fixtures. 21 + */ 22 + final class PhabricatorStorageFixtureScopeGuard { 23 + 24 + private $name; 25 + 26 + public function __construct($name) { 27 + $this->name = $name; 28 + 29 + execx( 30 + '%s upgrade --force --namespace %s', 31 + $this->getStorageBinPath(), 32 + $this->name); 33 + 34 + PhabricatorLiskDAO::pushStorageNamespace($name); 35 + } 36 + 37 + public function __destruct() { 38 + PhabricatorLiskDAO::popStorageNamespace(); 39 + 40 + execx( 41 + '%s destroy --force --namespace %s', 42 + $this->getStorageBinPath(), 43 + $this->name); 44 + } 45 + 46 + private function getStorageBinPath() { 47 + $root = dirname(phutil_get_library_root('phabricator')); 48 + return $root.'/bin/storage'; 49 + } 50 + 51 + }
+15
src/infrastructure/testing/fixture/storage/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/base/storage/lisk'); 10 + 11 + phutil_require_module('phutil', 'future/exec'); 12 + phutil_require_module('phutil', 'moduleutils'); 13 + 14 + 15 + phutil_require_source('PhabricatorStorageFixtureScopeGuard.php');
+67 -3
src/infrastructure/testing/testcase/PhabricatorTestCase.php
··· 28 28 * not rely on external resources like databases, and should not produce 29 29 * side effects. 30 30 */ 31 - const PHABRICATOR_TESTCONFIG_ISOLATE_LISK = 'isolate-lisk'; 31 + const PHABRICATOR_TESTCONFIG_ISOLATE_LISK = 'isolate-lisk'; 32 + 33 + /** 34 + * If true, build storage fixtures before running tests, and connect to them 35 + * during test execution. This will impose a performance penalty on test 36 + * execution (currently, it takes roughly one second to build the fixture) 37 + * but allows you to perform tests which require data to be read from storage 38 + * after writes. The fixture is shared across all test cases in this process. 39 + * Defaults to false. 40 + * 41 + * NOTE: All connections to fixture storage open transactions when established 42 + * and roll them back when tests complete. Each test must independently 43 + * write data it relies on; data will not persist across tests. 44 + * 45 + * NOTE: Enabling this implies disabling process isolation. 46 + */ 47 + const PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES = 'storage-fixtures'; 32 48 33 49 private $configuration; 34 50 private $env; 35 51 52 + private static $storageFixtureReferences = 0; 53 + private static $storageFixture; 54 + 36 55 protected function getPhabricatorTestCaseConfiguration() { 37 56 return array(); 38 57 } 39 58 40 59 private function getComputedConfiguration() { 41 - return $this->getPhabricatorTestCaseConfiguration() + array( 42 - self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => true, 60 + $config = $this->getPhabricatorTestCaseConfiguration() + array( 61 + self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => true, 62 + self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => false, 43 63 ); 64 + 65 + if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) { 66 + // Fixtures don't make sense with process isolation. 67 + $config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK] = false; 68 + } 69 + 70 + return $config; 44 71 } 45 72 46 73 protected function willRunTests() { ··· 53 80 LiskDAO::beginIsolateAllLiskEffectsToCurrentProcess(); 54 81 } 55 82 83 + if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) { 84 + ++self::$storageFixtureReferences; 85 + if (!self::$storageFixture) { 86 + self::$storageFixture = $this->newStorageFixture(); 87 + } 88 + } 89 + 56 90 $this->env = PhabricatorEnv::beginScopedEnv(); 57 91 } 58 92 ··· 63 97 LiskDAO::endIsolateAllLiskEffectsToCurrentProcess(); 64 98 } 65 99 100 + if (self::$storageFixture) { 101 + self::$storageFixtureReferences--; 102 + if (!self::$storageFixtureReferences) { 103 + self::$storageFixture = null; 104 + } 105 + } 106 + 66 107 try { 67 108 unset($this->env); 68 109 } catch (Exception $ex) { ··· 70 111 "Some test called PhabricatorEnv::beginScopedEnv(), but is still ". 71 112 "holding a reference to the scoped environment!"); 72 113 } 114 + } 115 + 116 + protected function willRunOneTest($test) { 117 + $config = $this->getComputedConfiguration(); 118 + 119 + if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) { 120 + LiskDAO::beginIsolateAllLiskEffectsToTransactions(); 121 + } 122 + } 123 + 124 + protected function didRunOneTest($test) { 125 + $config = $this->getComputedConfiguration(); 126 + 127 + if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) { 128 + LiskDAO::endIsolateAllLiskEffectsToTransactions(); 129 + } 130 + } 131 + 132 + protected function newStorageFixture() { 133 + $bytes = Filesystem::readRandomCharacters(24); 134 + $name = 'phabricator_unittest_'.$bytes; 135 + 136 + return new PhabricatorStorageFixtureScopeGuard($name); 73 137 } 74 138 75 139 }
+2
src/infrastructure/testing/testcase/__init__.php
··· 9 9 phutil_require_module('arcanist', 'unit/engine/phutil/testcase'); 10 10 11 11 phutil_require_module('phabricator', 'infrastructure/env'); 12 + phutil_require_module('phabricator', 'infrastructure/testing/fixture/storage'); 12 13 phutil_require_module('phabricator', 'storage/lisk/dao'); 13 14 15 + phutil_require_module('phutil', 'filesystem'); 14 16 phutil_require_module('phutil', 'moduleutils'); 15 17 16 18
+50 -1
src/storage/lisk/dao/LiskDAO.php
··· 190 190 191 191 private $__dirtyFields = array(); 192 192 private $__missingFields = array(); 193 - private static $processIsolationLevel = 0; 193 + private static $processIsolationLevel = 0; 194 + private static $transactionIsolationLevel = 0; 194 195 private static $__checkedClasses = array(); 195 196 196 197 private $__ephemeral = false; ··· 809 810 return $connection; 810 811 } 811 812 813 + if (self::shouldIsolateAllLiskEffectsToTransactions()) { 814 + // If we're doing fixture transaction isolation, force the mode to 'w' 815 + // so we always get the same connection for reads and writes, and thus 816 + // can see the writes inside the transaction. 817 + $mode = 'w'; 818 + } 819 + 812 820 // TODO There is currently no protection on 'r' queries against writing 813 821 // or on 'w' queries against reading 814 822 815 823 $connection = $this->getEstablishedConnection($mode); 816 824 if (!$connection) { 817 825 $connection = $this->establishLiveConnection($mode); 826 + if (self::shouldIsolateAllLiskEffectsToTransactions()) { 827 + $connection->openTransaction(); 828 + } 818 829 $this->setEstablishedConnection($mode, $connection); 819 830 } 820 831 ··· 1356 1367 return new AphrontIsolatedDatabaseConnection($config); 1357 1368 } 1358 1369 1370 + /** 1371 + * @task isolate 1372 + */ 1373 + public static function beginIsolateAllLiskEffectsToTransactions() { 1374 + if (self::$transactionIsolationLevel === 0) { 1375 + self::closeAllConnections(); 1376 + } 1377 + self::$transactionIsolationLevel++; 1378 + } 1379 + 1380 + /** 1381 + * @task isolate 1382 + */ 1383 + public static function endIsolateAllLiskEffectsToTransactions() { 1384 + self::$transactionIsolationLevel--; 1385 + if (self::$transactionIsolationLevel < 0) { 1386 + throw new Exception( 1387 + "Lisk transaction isolation level was reduced below 0."); 1388 + } else if (self::$transactionIsolationLevel == 0) { 1389 + foreach (self::$connections as $key => $conn) { 1390 + if ($conn) { 1391 + $conn->killTransaction(); 1392 + } 1393 + } 1394 + self::closeAllConnections(); 1395 + } 1396 + } 1397 + 1398 + /** 1399 + * @task isolate 1400 + */ 1401 + public static function shouldIsolateAllLiskEffectsToTransactions() { 1402 + return (bool)self::$transactionIsolationLevel; 1403 + } 1404 + 1405 + public static function closeAllConnections() { 1406 + self::$connections = array(); 1407 + } 1359 1408 1360 1409 /* -( Utilities )---------------------------------------------------------- */ 1361 1410
+69
src/storage/lisk/dao/__tests__/LiskFixtureTestCase.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class LiskFixtureTestCase extends PhabricatorTestCase { 20 + 21 + public function getPhabricatorTestCaseConfiguration() { 22 + return array( 23 + self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true, 24 + ); 25 + } 26 + 27 + public function testTransactionalIsolation1of2() { 28 + // NOTE: These tests are verifying that data is destroyed between tests. 29 + // If the user from either test persists, the other test will fail. 30 + $this->assertEqual( 31 + 0, 32 + count(id(new PhabricatorUser())->loadAll())); 33 + 34 + id(new PhabricatorUser()) 35 + ->setUserName('alincoln') 36 + ->setRealName('Abraham Lincoln') 37 + ->setEmail('alincoln@example.com') 38 + ->save(); 39 + } 40 + 41 + public function testTransactionalIsolation2of2() { 42 + $this->assertEqual( 43 + 0, 44 + count(id(new PhabricatorUser())->loadAll())); 45 + 46 + id(new PhabricatorUser()) 47 + ->setUserName('ugrant') 48 + ->setRealName('Ulysses S. Grant') 49 + ->setEmail('ugrant@example.com') 50 + ->save(); 51 + } 52 + 53 + public function testFixturesBasicallyWork() { 54 + $this->assertEqual( 55 + 0, 56 + count(id(new PhabricatorUser())->loadAll())); 57 + 58 + id(new PhabricatorUser()) 59 + ->setUserName('gwashington') 60 + ->setRealName('George Washington') 61 + ->setEmail('gwashington@example.com') 62 + ->save(); 63 + 64 + $this->assertEqual( 65 + 1, 66 + count(id(new PhabricatorUser())->loadAll())); 67 + } 68 + 69 + }
+4
src/storage/lisk/dao/__tests__/__init__.php
··· 6 6 7 7 8 8 9 + phutil_require_module('phabricator', 'applications/people/storage/user'); 9 10 phutil_require_module('phabricator', 'applications/phid/storage/phid'); 10 11 phutil_require_module('phabricator', 'infrastructure/testing/testcase'); 11 12 phutil_require_module('phabricator', 'storage/lisk/dao'); 12 13 14 + phutil_require_module('phutil', 'utils'); 13 15 16 + 17 + phutil_require_source('LiskFixtureTestCase.php'); 14 18 phutil_require_source('LiskIsolationTestCase.php'); 15 19 phutil_require_source('LiskIsolationTestDAO.php'); 16 20 phutil_require_source('LiskIsolationTestDAOException.php');