blob: 97de03de31cb464ae506186069b69dae828b5ef2 [file] [log] [blame]
// Copyright 2020 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cocoon_service/src/model/appengine/commit.dart';
import 'package:cocoon_service/src/model/appengine/task.dart';
import 'package:cocoon_service/src/service/config.dart';
import 'package:cocoon_service/src/service/datastore.dart';
import 'package:gcloud/datastore.dart' as gcloud_datastore;
import 'package:gcloud/db.dart';
import 'package:grpc/grpc.dart';
import 'package:retry/retry.dart';
import 'package:test/test.dart';
import '../src/datastore/fake_config.dart';
import '../src/datastore/fake_datastore.dart';
import '../src/utilities/entity_generators.dart';
class Counter {
int count = 0;
void increase() {
count = count + 1;
}
int value() {
return count;
}
}
void main() {
group('Datastore', () {
late FakeConfig config;
late FakeDatastoreDB db;
late DatastoreService datastoreService;
late Commit commit;
setUp(() {
db = FakeDatastoreDB();
config = FakeConfig(dbValue: db);
datastoreService = DatastoreService(config.db, 5);
commit = Commit(
key: config.db.emptyKey.append(Commit, id: 'abc_master'),
sha: 'abc_master',
repository: Config.flutterSlug.fullName,
branch: 'master',
);
});
group('DatasourceService', () {
setUp(() {});
test('defaultProvider returns a DatasourceService object', () async {
expect(DatastoreService.defaultProvider(config.db), isA<DatastoreService>());
});
test('queryRecentCommits', () async {
for (String branch in <String>['master', 'release']) {
final Commit commit = Commit(
key: config.db.emptyKey.append(Commit, id: 'abc_$branch'),
repository: Config.flutterSlug.fullName,
sha: 'abc_$branch',
branch: branch,
);
config.db.values[commit.key] = commit;
}
// Defaults to master
List<Commit> commits = await datastoreService.queryRecentCommits(slug: Config.flutterSlug).toList();
expect(commits, hasLength(1));
expect(commits[0].branch, equals('master'));
// Explicit branch
commits = await datastoreService.queryRecentCommits(branch: 'release', slug: Config.flutterSlug).toList();
expect(commits, hasLength(1));
expect(commits[0].branch, equals('release'));
});
test('queryRecentCommits with slug', () async {
for (String repo in <String>['flutter', 'engine']) {
final Commit commit = Commit(
key: config.db.emptyKey.append(Commit, id: 'flutter/$repo/main/abc'),
repository: 'flutter/$repo',
sha: 'abc',
branch: 'main',
);
config.db.values[commit.key] = commit;
}
// Only retrieves flutter/flutter
List<Commit> commits =
await datastoreService.queryRecentCommits(slug: Config.flutterSlug, branch: 'main').toList();
expect(commits, hasLength(1));
expect(commits.single.repository, equals('flutter/flutter'));
// Only retrieves flutter/engine
commits = await datastoreService.queryRecentCommits(branch: 'main', slug: Config.engineSlug).toList();
expect(commits, hasLength(1));
expect(commits.single.repository, equals('flutter/engine'));
});
});
test('queryRecentCommits with repo default branch', () async {
for (String branch in <String>['master', 'main']) {
final Commit commit = Commit(
key: config.db.emptyKey.append(Commit, id: 'abc_$branch'),
repository: Config.engineSlug.fullName,
sha: 'abc_$branch',
branch: branch,
);
config.db.values[commit.key] = commit;
}
// Pull from main, not master
final List<Commit> commits = await datastoreService.queryRecentCommits(slug: Config.engineSlug).toList();
expect(commits, hasLength(1));
expect(commits[0].branch, equals('main'));
});
test('queryRecentTasks returns all tasks', () async {
const String branch = 'main';
final Commit commit = Commit(
key: config.db.emptyKey.append(Commit, id: 'abc_$branch'),
repository: Config.engineSlug.fullName,
sha: 'abc_$branch',
branch: branch,
);
const int taskNumber = 2;
for (int i = 0; i < taskNumber; i++) {
final Task task = generateTask(
i,
parent: commit,
);
config.db.values[task.key] = task;
}
config.db.values[commit.key] = commit;
final List<FullTask> datastoreTasks = await datastoreService
.queryRecentTasks(
slug: Config.engineSlug,
)
.toList();
expect(datastoreTasks, hasLength(taskNumber));
});
test('Shard', () async {
// default maxEntityGroups = 5
List<List<Model<dynamic>>> shards = await datastoreService.shard(generateCommits(6));
expect(shards, hasLength(2));
expect(shards[0], hasLength(5));
expect(shards[1], hasLength(1));
// maxEntitygroups = 2
datastoreService = DatastoreService(config.db, 2);
shards = await datastoreService.shard(generateCommits(3));
expect(shards, hasLength(2));
expect(shards[0], hasLength(2));
expect(shards[1], hasLength(1));
// maxEntityGroups = 1
datastoreService = DatastoreService(config.db, 1);
shards = await datastoreService.shard(generateCommits(3));
expect(shards, hasLength(3));
expect(shards[0], hasLength(1));
expect(shards[1], hasLength(1));
expect(shards[2], hasLength(1));
});
test('Insert', () async {
await datastoreService.insert(<Commit>[commit]);
expect(config.db.values[commit.key], equals(commit));
});
test('LookupByKey', () async {
config.db.values[commit.key] = commit;
final List<Commit?> commits = await datastoreService.lookupByKey(<Key<dynamic>>[commit.key]);
expect(commits, hasLength(1));
expect(commits[0], equals(commit));
});
test('LookupByValue', () async {
config.db.values[commit.key] = commit;
final Commit expected = await datastoreService.lookupByValue(commit.key);
expect(expected, equals(commit));
});
test('WithTransaction', () async {
final String? expected = await datastoreService.withTransaction((Transaction transaction) async {
transaction.queueMutations(inserts: <Commit>[commit]);
await transaction.commit();
return 'success';
});
expect(expected, equals('success'));
expect(config.db.values[commit.key], equals(commit));
});
});
group('RunTransactionWithRetry', () {
late RetryOptions retryOptions;
setUp(() {
retryOptions = const RetryOptions(
delayFactor: Duration(milliseconds: 1),
maxDelay: Duration(milliseconds: 2),
maxAttempts: 2,
);
});
test('retriesOnGrpcError', () async {
final Counter counter = Counter();
try {
await runTransactionWithRetries(
() async {
counter.increase();
throw const GrpcError.aborted();
},
retryOptions: retryOptions,
);
} catch (e) {
expect(e, isA<GrpcError>());
}
expect(counter.value(), greaterThan(1));
});
test('retriesTransactionAbortedError', () async {
final Counter counter = Counter();
try {
await runTransactionWithRetries(
() async {
counter.increase();
throw gcloud_datastore.TransactionAbortedError();
},
retryOptions: retryOptions,
);
} catch (e) {
expect(e, isA<gcloud_datastore.TransactionAbortedError>());
}
expect(counter.value(), greaterThan(1));
});
test('DoesNotRetryOnSuccess', () async {
final Counter counter = Counter();
await runTransactionWithRetries(
() async {
counter.increase();
},
retryOptions: retryOptions,
);
expect(counter.value(), equals(1));
});
});
}
/// Helper function to generate fake commits
List<Commit> generateCommits(int i) {
return List<Commit>.generate(i, (int i) => Commit(repository: 'flutter/flutter', sha: '$i'));
}