Skip to content

Commit 7f9e0eb

Browse files
authored
Reuse temporary database in local postgres in fake environment (between specific test runs). (#9084)
1 parent 8e2c3f0 commit 7f9e0eb

File tree

4 files changed

+110
-55
lines changed

4 files changed

+110
-55
lines changed

app/lib/database/database.dart

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@ void registerPrimaryDatabase(PrimaryDatabase database) =>
2424
ss.register(#_primaryDatabase, database);
2525

2626
/// The active primary database service.
27-
PrimaryDatabase? get primaryDatabase =>
27+
PrimaryDatabase? get primaryDatabase => _lookupPrimaryDatabase();
28+
29+
PrimaryDatabase? _lookupPrimaryDatabase() =>
2830
ss.lookup(#_primaryDatabase) as PrimaryDatabase?;
2931

3032
/// Access to the primary database connection and object mapping.
3133
class PrimaryDatabase {
3234
final Pool _pg;
3335
final DatabaseAdapter _adapter;
3436
final Database<PrimarySchema> db;
37+
final Future<void> Function()? _closeFn;
3538

36-
PrimaryDatabase._(this._pg, this._adapter, this.db);
39+
PrimaryDatabase._(this._pg, this._adapter, this.db, this._closeFn);
3740

3841
/// Gets the connection string either from the environment variable or from
3942
/// the secret backend, connects to it and registers the primary database
@@ -43,54 +46,71 @@ class PrimaryDatabase {
4346
// Production is not configured for postgresql yet.
4447
return;
4548
}
46-
var connectionString =
49+
if (_lookupPrimaryDatabase() != null) {
50+
// Already initialized, must be in a local test environment.
51+
assert(activeConfiguration.isFakeOrTest);
52+
return;
53+
}
54+
final connectionString =
4755
envConfig.pubPostgresUrl ??
4856
(await secretBackend.lookup(SecretKey.postgresConnectionString));
4957
if (connectionString == null && activeConfiguration.isStaging) {
5058
// Staging may not have the connection string set yet.
5159
return;
5260
}
61+
final database = await createAndInit(url: connectionString);
62+
registerPrimaryDatabase(database);
63+
ss.registerScopeExitCallback(database.close);
64+
}
65+
66+
/// Creates and initializes a [PrimaryDatabase] instance.
67+
///
68+
/// When [url] is not provided, it will start a new local postgresql instance, or
69+
/// if it detects an existing one, connects to it.
70+
///
71+
/// When NOT running in the AppEngine environment (e.g. testing or local fake),
72+
/// the initilization will create a new database, which will be dropped when the
73+
/// [close] method is called.
74+
static Future<PrimaryDatabase> createAndInit({String? url}) async {
5375
// The scope-specific custom database. We are creating a custom database for
5476
// each test run, in order to provide full isolation, however, this must not
5577
// be used in Appengine.
5678
String? customDb;
57-
if (connectionString == null) {
58-
(connectionString, customDb) = await _startOrUseLocalPostgresInDocker();
79+
if (url == null) {
80+
(url, customDb) = await _startOrUseLocalPostgresInDocker();
5981
}
6082
if (customDb == null && !envConfig.isRunningInAppengine) {
61-
customDb = await _createCustomDatabase(connectionString);
83+
customDb = await _createCustomDatabase(url);
6284
}
6385

86+
final originalUrl = url;
6487
if (customDb != null) {
6588
if (envConfig.isRunningInAppengine) {
6689
throw StateError('Should not use custom database inside AppEngine.');
6790
}
6891

69-
final originalUrl = connectionString;
70-
connectionString = Uri.parse(
71-
connectionString,
72-
).replace(path: customDb).toString();
73-
ss.registerScopeExitCallback(() async {
74-
await _dropCustomDatabase(originalUrl, customDb!);
75-
});
92+
url = Uri.parse(url).replace(path: customDb).toString();
7693
}
7794

78-
final database = await _fromConnectionString(connectionString);
79-
registerPrimaryDatabase(database);
80-
ss.registerScopeExitCallback(database.close);
81-
}
95+
Future<void> closeFn() async {
96+
if (customDb != null) {
97+
await _dropCustomDatabase(originalUrl, customDb);
98+
}
99+
}
82100

83-
static Future<PrimaryDatabase> _fromConnectionString(String value) async {
84-
final pg = Pool.withUrl(value);
101+
final pg = Pool.withUrl(url);
85102
final adapter = DatabaseAdapter.postgres(pg);
86103
final db = Database<PrimarySchema>(adapter, SqlDialect.postgres());
87104
await db.createTables();
88-
return PrimaryDatabase._(pg, adapter, db);
105+
return PrimaryDatabase._(pg, adapter, db, closeFn);
89106
}
90107

91108
Future<void> close() async {
92109
await _adapter.close();
93110
await _pg.close();
111+
if (_closeFn != null) {
112+
await _closeFn();
113+
}
94114
}
95115

96116
@visibleForTesting

app/lib/service/services.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Future<R> withFakeServices<R>({
145145
MemDatastore? datastore,
146146
MemStorage? storage,
147147
FakeCloudCompute? cloudCompute,
148+
PrimaryDatabase? primaryDatabase,
148149
}) async {
149150
if (!envConfig.isRunningLocally) {
150151
throw StateError("Mustn't use fake services inside AppEngine.");
@@ -156,6 +157,9 @@ Future<R> withFakeServices<R>({
156157
register(#appengine.context, FakeClientContext());
157158
registerDbService(DatastoreDB(datastore!));
158159
registerStorageService(RetryEnforcerStorage(storage!));
160+
if (primaryDatabase != null) {
161+
registerPrimaryDatabase(primaryDatabase);
162+
}
159163
IOServer? frontendServer;
160164
IOServer? searchServer;
161165
if (configuration == null) {

app/test/shared/test_services.dart

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:gcloud/db.dart';
1111
import 'package:gcloud/service_scope.dart';
1212
import 'package:meta/meta.dart';
1313
import 'package:pub_dev/account/models.dart';
14+
import 'package:pub_dev/database/database.dart';
1415
import 'package:pub_dev/fake/backend/fake_auth_provider.dart';
1516
import 'package:pub_dev/fake/backend/fake_download_counts.dart';
1617
import 'package:pub_dev/fake/backend/fake_email_sender.dart';
@@ -77,6 +78,28 @@ final class FakeAppengineEnv {
7778
final _storage = MemStorage();
7879
final _datastore = MemDatastore();
7980
final _cloudCompute = FakeCloudCompute();
81+
final PrimaryDatabase _primaryDatabase;
82+
83+
FakeAppengineEnv._(this._primaryDatabase);
84+
85+
/// Initializes, provides and then disposes a fake environment, preserving
86+
/// the backing databases, allowing the use of new runtimes and other dynamic
87+
/// features.
88+
static Future<T> withEnv<T>(
89+
Future<T> Function(FakeAppengineEnv env) fn,
90+
) async {
91+
final database = await PrimaryDatabase.createAndInit();
92+
final env = FakeAppengineEnv._(database);
93+
try {
94+
return await fn(env);
95+
} finally {
96+
await env._dispose();
97+
}
98+
}
99+
100+
Future<void> _dispose() async {
101+
await _primaryDatabase.close();
102+
}
80103

81104
/// Create a service scope with fake services and run [fn] in it.
82105
///
@@ -102,6 +125,7 @@ final class FakeAppengineEnv {
102125
datastore: _datastore,
103126
storage: _storage,
104127
cloudCompute: _cloudCompute,
128+
primaryDatabase: _primaryDatabase,
105129
fn: () async {
106130
registerStaticFileCacheForTest(_staticFileCacheForTesting);
107131

@@ -171,19 +195,19 @@ void testWithProfile(
171195
Iterable<Pattern>? expectedLogMessages,
172196
dynamic skip,
173197
}) {
174-
final env = FakeAppengineEnv();
175-
176198
scopedTest(
177199
name,
178200
() async {
179201
setupDebugEnvBasedLogging();
180-
await env.run(
181-
fn,
182-
testProfile: testProfile ?? defaultTestProfile,
183-
importSource: importSource,
184-
processJobsWithFakeRunners: processJobsWithFakeRunners,
185-
integrityProblem: integrityProblem,
186-
);
202+
await FakeAppengineEnv.withEnv((env) async {
203+
await env.run(
204+
fn,
205+
testProfile: testProfile ?? defaultTestProfile,
206+
importSource: importSource,
207+
processJobsWithFakeRunners: processJobsWithFakeRunners,
208+
integrityProblem: integrityProblem,
209+
);
210+
});
187211
},
188212
expectedLogMessages: expectedLogMessages,
189213
timeout: timeout,

app/test/task/fallback_test.dart

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,45 @@ import '../shared/test_services.dart';
1616
void main() {
1717
group('task fallback test', () {
1818
test('analysis fallback', () async {
19-
final env = FakeAppengineEnv();
20-
await env.run(
21-
testProfile: TestProfile(
22-
generatedPackages: [
23-
GeneratedTestPackage(
24-
name: 'oxygen',
25-
versions: [GeneratedTestVersion(version: '1.0.0')],
26-
),
27-
],
28-
defaultUser: adminAtPubDevEmail,
29-
),
30-
processJobsWithFakeRunners: true,
31-
runtimeVersions: ['2023.08.24'],
32-
() async {
19+
await FakeAppengineEnv.withEnv((env) async {
20+
await env.run(
21+
testProfile: TestProfile(
22+
generatedPackages: [
23+
GeneratedTestPackage(
24+
name: 'oxygen',
25+
versions: [GeneratedTestVersion(version: '1.0.0')],
26+
),
27+
],
28+
defaultUser: adminAtPubDevEmail,
29+
),
30+
processJobsWithFakeRunners: true,
31+
runtimeVersions: ['2023.08.24'],
32+
() async {
33+
final card = await scoreCardBackend.getScoreCardData(
34+
'oxygen',
35+
'1.0.0',
36+
);
37+
expect(card.runtimeVersion, '2023.08.24');
38+
},
39+
);
40+
41+
await env.run(runtimeVersions: ['2023.08.25', '2023.08.24'], () async {
42+
// fallback into accepted runtime works
3343
final card = await scoreCardBackend.getScoreCardData(
3444
'oxygen',
3545
'1.0.0',
3646
);
3747
expect(card.runtimeVersion, '2023.08.24');
38-
},
39-
);
40-
41-
await env.run(runtimeVersions: ['2023.08.25', '2023.08.24'], () async {
42-
// fallback into accepted runtime works
43-
final card = await scoreCardBackend.getScoreCardData('oxygen', '1.0.0');
44-
expect(card.runtimeVersion, '2023.08.24');
45-
});
48+
});
4649

47-
await env.run(runtimeVersions: ['2023.08.26', '2023.08.23'], () async {
48-
// fallback into non-accepted runtime doesn't work
49-
final card = await scoreCardBackend.getScoreCardData('oxygen', '1.0.0');
50-
expect(card.runtimeVersion, '2023.08.26');
50+
await env.run(runtimeVersions: ['2023.08.26', '2023.08.23'], () async {
51+
// fallback into non-accepted runtime doesn't work
52+
final card = await scoreCardBackend.getScoreCardData(
53+
'oxygen',
54+
'1.0.0',
55+
);
56+
expect(card.runtimeVersion, '2023.08.26');
57+
});
5158
});
5259
});
5360
});

0 commit comments

Comments
 (0)