Skip to content

Commit e760559

Browse files
committed
test(Database): fix random-order test execution issues and state leakage under PostgreSQL, MySQL, and OCI8
1 parent ad8526f commit e760559

13 files changed

Lines changed: 170 additions & 38 deletions

File tree

.github/scripts/random-tests-config.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Config
1818
Cookie
1919
# DataCaster
2020
# DataConverter
21-
# Database
21+
Database
2222
# Debug
2323
# Email
2424
# Encryption

system/Database/OCI8/Connection.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ public function connect(bool $persistent = false)
150150
: $func($this->username, $this->password, $this->DSN, $this->charset);
151151
}
152152

153+
public function initialize()
154+
{
155+
parent::initialize();
156+
157+
if ($this->connID) {
158+
$this->simpleQuery("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
159+
$this->simpleQuery("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
160+
$this->simpleQuery("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS'");
161+
}
162+
}
163+
153164
/**
154165
* Close the database connection.
155166
*
@@ -422,7 +433,7 @@ protected function _indexData(string $table): array
422433
$retVal[$row->INDEX_NAME] = new stdClass();
423434
$retVal[$row->INDEX_NAME]->name = $row->INDEX_NAME;
424435
$retVal[$row->INDEX_NAME]->fields = [$row->COLUMN_NAME];
425-
$retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE] ?? 'INDEX';
436+
$retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE ?? ''] ?? 'INDEX';
426437
}
427438

428439
return $retVal;

tests/_support/Config/Registrar.php

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
namespace Tests\Support\Config;
1515

16+
use mysqli;
17+
use PDO;
18+
use Throwable;
19+
1620
/**
1721
* Class Registrar
1822
*
@@ -137,7 +141,63 @@ public static function Database(): array
137141
// so that we can test against multiple databases.
138142
$group = env('DB', 'SQLite3');
139143

140-
$config['tests'] = self::$dbConfig[$group] ?? [];
144+
$dbParams = self::$dbConfig[$group] ?? [];
145+
146+
if (! empty($dbParams) && $group !== 'SQLite3') {
147+
$componentName = '';
148+
149+
foreach ($_SERVER['argv'] ?? [] as $arg) {
150+
if (str_contains($arg, 'tests/system/')) {
151+
$parts = explode('tests/system/', $arg);
152+
if (isset($parts[1])) {
153+
$componentName = explode('/', $parts[1])[0];
154+
break;
155+
}
156+
}
157+
}
158+
159+
if ($componentName !== '') {
160+
$dbParams['database'] = 'test_' . strtolower($componentName);
161+
162+
try {
163+
if ($group === 'MySQLi') {
164+
$conn = @new mysqli(
165+
$dbParams['hostname'],
166+
$dbParams['username'],
167+
$dbParams['password'],
168+
'',
169+
(int) $dbParams['port'],
170+
);
171+
if (! $conn->connect_error) {
172+
$conn->query('CREATE DATABASE IF NOT EXISTS ' . $conn->real_escape_string($dbParams['database']));
173+
$conn->close();
174+
}
175+
} elseif ($group === 'Postgre') {
176+
$dsn = 'pgsql:host=' . $dbParams['hostname'] . ';port=' . $dbParams['port'] . ';user=' . $dbParams['username'] . ';password=' . $dbParams['password'];
177+
$pdo = new PDO($dsn);
178+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
179+
$stmt = $pdo->prepare('SELECT 1 FROM pg_database WHERE datname = ?');
180+
$stmt->execute([$dbParams['database']]);
181+
if (! $stmt->fetchColumn()) {
182+
$pdo->exec('CREATE DATABASE ' . $pdo->quote($dbParams['database']));
183+
}
184+
} elseif ($group === 'SQLSRV') {
185+
$dsn = 'sqlsrv:Server=' . $dbParams['hostname'] . ',' . $dbParams['port'] . ';Encrypt=False;TrustServerCertificate=True';
186+
$pdo = new PDO($dsn, $dbParams['username'], $dbParams['password']);
187+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
188+
$stmt = $pdo->prepare('SELECT 1 FROM sys.databases WHERE name = ?');
189+
$stmt->execute([$dbParams['database']]);
190+
if (! $stmt->fetchColumn()) {
191+
$pdo->exec('CREATE DATABASE [' . str_replace(']', ']]', $dbParams['database']) . ']');
192+
}
193+
}
194+
} catch (Throwable $e) {
195+
// Ignore any error and let the connection fail naturally
196+
}
197+
}
198+
}
199+
200+
$config['tests'] = $dbParams;
141201

142202
return $config;
143203
}

tests/system/Database/Live/ConnectTest.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,28 @@ protected function setUp(): void
4646
$this->group2['DBDriver'] = 'Postgre';
4747
}
4848

49+
protected function tearDown(): void
50+
{
51+
parent::tearDown();
52+
$this->setPrivateProperty(Database::class, 'instances', []);
53+
}
54+
4955
public function testConnectWithMultipleCustomGroups(): void
5056
{
57+
$this->group1['DBPrefix'] = uniqid('g1_', true);
58+
$this->group2['DBPrefix'] = uniqid('g2_', true);
59+
5160
// We should have our test database connection already.
52-
$instances = $this->getPrivateProperty(Database::class, 'instances');
53-
$this->assertCount(1, $instances);
61+
$instances = $this->getPrivateProperty(Database::class, 'instances');
62+
$initialCount = count($instances);
5463

5564
$db1 = Database::connect($this->group1);
5665
$db2 = Database::connect($this->group2);
5766

5867
$this->assertNotSame($db1, $db2);
5968

6069
$instances = $this->getPrivateProperty(Database::class, 'instances');
61-
$this->assertCount(3, $instances);
70+
$this->assertCount($initialCount + 2, $instances);
6271
}
6372

6473
public function testConnectReturnsProvidedConnection(): void

tests/system/Database/Live/ExecuteLogMessageFormatTest.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi
4747
$db->query($sql, [3, 'live', 'Rick']);
4848

4949
$pattern = match ($db->DBDriver) {
50-
'MySQLi' => '/Table \'test\.some_table\' doesn\'t exist/',
50+
'MySQLi' => '/Table \'' . preg_quote($db->database, '/') . '\.some_table\' doesn\'t exist/',
5151
'Postgre' => '/pg_query\(\): Query failed: ERROR: relation "some_table" does not exist/',
5252
'SQLite3' => '/Unable to prepare statement:\s(\d+,\s)?no such table: some_table/',
5353
'OCI8' => '/oci_execute\(\): ORA-00942: table or view "ORACLE"\."SOME_TABLE" does not exist/',
@@ -60,11 +60,18 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi
6060

6161
if ($db->DBDriver === 'Postgre') {
6262
$messageFromLogs = array_slice($messageFromLogs, 2);
63-
} elseif ($db->DBDriver === 'OCI8') {
64-
$messageFromLogs = array_slice($messageFromLogs, 1);
6563
}
6664

67-
$this->assertMatchesRegularExpression('/^in \S+ on line \d+\.$/', array_shift($messageFromLogs));
65+
$inLine = null;
66+
67+
while (($line = array_shift($messageFromLogs)) !== null) {
68+
if (preg_match('/^in \S+ on line \d+\.$/', $line)) {
69+
$inLine = $line;
70+
break;
71+
}
72+
}
73+
74+
$this->assertNotNull($inLine, 'Could not find "in ... on line ..." in log message');
6875

6976
foreach ($messageFromLogs as $line) {
7077
$this->assertMatchesRegularExpression('/^\s*\d* .+(?:\(\d+\))?: \S+(?:(?:\->|::)\S+)?\(.*\)$/', $line);

tests/system/Database/Live/ForgeTest.php

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,64 @@ final class ForgeTest extends CIUnitTestCase
3636
protected $seed = CITestSeeder::class;
3737
private Forge $forge;
3838

39+
private function dropAllMockTables(): void
40+
{
41+
$tablesToDrop = [
42+
'forge_test_invoices',
43+
'forge_test_inv',
44+
'forge_test_users',
45+
'actions',
46+
'forge_test_table',
47+
'test_exists',
48+
'forge_test_attributes',
49+
'forge_array_constraint',
50+
'forge_nullable_table',
51+
'forge_test_1',
52+
'forge_test_two',
53+
'forge_test_three',
54+
'forge_test_four',
55+
'forge_test_modify',
56+
'droptest',
57+
'key_test_users',
58+
'test_stores',
59+
'user2',
60+
'forge_test_table_dummy',
61+
];
62+
63+
foreach ($tablesToDrop as $table) {
64+
$this->forge->dropTable($table, true);
65+
}
66+
}
67+
3968
protected function setUp(): void
4069
{
4170
$this->forge = Database::forge($this->DBGroup);
4271

43-
// when running locally if one of these tables isn't dropped it may cause error
44-
$this->forge->dropTable('forge_test_invoices', true);
45-
$this->forge->dropTable('forge_test_inv', true);
46-
$this->forge->dropTable('forge_test_users', true);
47-
$this->forge->dropTable('actions', true);
72+
$this->dropAllMockTables();
73+
74+
db_connect($this->DBGroup)->resetDataCache();
4875

4976
parent::setUp();
5077
}
5178

79+
protected function tearDown(): void
80+
{
81+
parent::tearDown();
82+
$this->dropAllMockTables();
83+
}
84+
5285
public function testCreateDatabase(): void
5386
{
5487
if ($this->db->DBDriver === 'OCI8') {
5588
$this->markTestSkipped('OCI8 does not support create database.');
5689
}
5790

91+
try {
92+
$this->forge->dropDatabase('test_forge_database');
93+
} catch (DatabaseException) {
94+
// Ignore if doesn't exist
95+
}
96+
5897
$databaseCreated = $this->forge->createDatabase('test_forge_database');
5998

6099
$this->assertTrue($databaseCreated);
@@ -68,14 +107,20 @@ public function testCreateDatabaseWithDots(): void
68107

69108
$dbName = 'test_com.sitedb.web';
70109

110+
try {
111+
$this->forge->dropDatabase($dbName);
112+
} catch (DatabaseException) {
113+
// Ignore if doesn't exist
114+
}
115+
71116
$databaseCreated = $this->forge->createDatabase($dbName);
72117

73118
$this->assertTrue($databaseCreated);
74119

75120
// Checks if tableExists() works.
76121
$config = config(Database::class)->{$this->DBGroup};
77122
$config['database'] = $dbName;
78-
$db = db_connect($config);
123+
$db = db_connect($config, false);
79124
$result = $db->tableExists('not_exist');
80125

81126
$this->assertFalse($result);
@@ -151,6 +196,12 @@ public function testDropDatabase(): void
151196
$this->markTestSkipped('SQLite3 requires file path to drop database');
152197
}
153198

199+
try {
200+
$this->forge->createDatabase('test_forge_database');
201+
} catch (DatabaseException) {
202+
// Ignore if exists
203+
}
204+
154205
$databaseDropped = $this->forge->dropDatabase('test_forge_database');
155206

156207
$this->assertTrue($databaseDropped);

tests/system/Database/Live/GetVersionTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public function testGetVersion(): void
3636
$this->db->connID = false;
3737

3838
$version = $this->db->getVersion();
39-
40-
$this->assertMatchesRegularExpression('/\A\d+(\.\d+)*\z/', $version);
39+
$this->assertMatchesRegularExpression('/\A\d+(\.\d+)*/', $version);
4140
}
4241
}

tests/system/Database/Live/MetadataTest.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,10 @@ public function testListTablesConstrainedByPrefixReturnsOnlyTablesWithMatchingPr
120120

121121
public function testListTablesConstrainedByExtraneousPrefixReturnsOnlyTheExtraneousTable(): void
122122
{
123-
$oldPrefix = '';
123+
$oldPrefix = $this->db->getPrefix();
124124

125125
try {
126126
$this->createExtraneousTable();
127-
128-
$oldPrefix = $this->db->getPrefix();
129127
$this->db->setPrefix('tmp_');
130128

131129
$tables = $this->db->listTables(true);

tests/system/Database/Live/MySQLi/FoundRowsTest.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function testEnableFoundRows(): void
5454
{
5555
$this->tests['foundRows'] = true;
5656

57-
$db1 = Database::connect($this->tests);
57+
$db1 = Database::connect($this->tests, false);
5858

5959
$this->assertTrue($db1->foundRows);
6060
}
@@ -63,7 +63,7 @@ public function testDisableFoundRows(): void
6363
{
6464
$this->tests['foundRows'] = false;
6565

66-
$db1 = Database::connect($this->tests);
66+
$db1 = Database::connect($this->tests, false);
6767

6868
$this->assertFalse($db1->foundRows);
6969
}
@@ -72,7 +72,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithNoChange(): void
7272
{
7373
$this->tests['foundRows'] = true;
7474

75-
$db1 = Database::connect($this->tests);
75+
$db1 = Database::connect($this->tests, false);
7676

7777
$db1->table('db_user')
7878
->set('country', 'US')
@@ -88,7 +88,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithNoChange(): void
8888
{
8989
$this->tests['foundRows'] = false;
9090

91-
$db1 = Database::connect($this->tests);
91+
$db1 = Database::connect($this->tests, false);
9292

9393
$db1->table('db_user')
9494
->set('country', 'US')
@@ -104,7 +104,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithChange(): void
104104
{
105105
$this->tests['foundRows'] = true;
106106

107-
$db1 = Database::connect($this->tests);
107+
$db1 = Database::connect($this->tests, false);
108108

109109
$db1->table('db_user')
110110
->set('country', 'NZ')
@@ -120,7 +120,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithChange(): void
120120
{
121121
$this->tests['foundRows'] = false;
122122

123-
$db1 = Database::connect($this->tests);
123+
$db1 = Database::connect($this->tests, false);
124124

125125
$db1->table('db_user')
126126
->set('country', 'NZ')
@@ -136,7 +136,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithPartialChange(): void
136136
{
137137
$this->tests['foundRows'] = true;
138138

139-
$db1 = Database::connect($this->tests);
139+
$db1 = Database::connect($this->tests, false);
140140

141141
$db1->table('db_user')
142142
->set('name', 'Derek Jones')
@@ -152,7 +152,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithPartialChange(): void
152152
{
153153
$this->tests['foundRows'] = false;
154154

155-
$db1 = Database::connect($this->tests);
155+
$db1 = Database::connect($this->tests, false);
156156

157157
$db1->table('db_user')
158158
->set('name', 'Derek Jones')

0 commit comments

Comments
 (0)