blob: 9082b323b7ef49dd3501ba0db47de98727059a6f [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 'dart:convert';
import 'dart:io';
import 'package:cocoon_service/src/foundation/utils.dart';
import 'package:cocoon_service/src/model/ci_yaml/target.dart';
import 'package:cocoon_service/src/service/logging.dart';
import 'package:cocoon_service/src/service/luci.dart';
import 'package:github/github.dart';
import 'package:googleapis/bigquery/v2.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:logging/logging.dart';
import 'package:retry/retry.dart';
import 'package:test/test.dart';
import '../src/bigquery/fake_tabledata_resource.dart';
import '../src/utilities/entity_generators.dart';
const String branchRegExp = '''
master
flutter-1.1-candidate.1
''';
const String luciBuilders = '''
{
"builders":[
{
"name":"Cocoon",
"repo":"cocoon",
"enabled":true
}, {
"name":"Cocoon2",
"repo":"cocoon",
"enabled":false
}
]
}
''';
void main() {
group('Test utils', () {
const RetryOptions noRetry = RetryOptions(
maxAttempts: 1,
delayFactor: Duration.zero,
maxDelay: Duration.zero,
);
group('githubFileContent', () {
late MockClient branchHttpClient;
test('returns branches', () async {
branchHttpClient = MockClient((_) async => http.Response(branchRegExp, HttpStatus.ok));
final String branches = await githubFileContent(
RepositorySlug('flutter', 'cocoon'),
'branches.txt',
httpClientProvider: () => branchHttpClient,
retryOptions: noRetry,
);
final List<String> branchList = branches.split('\n').map((String branch) => branch.trim()).toList();
branchList.removeWhere((String branch) => branch.isEmpty);
expect(branchList, <String>['master', 'flutter-1.1-candidate.1']);
});
test('retries branches download upon HTTP failure', () async {
int retry = 0;
branchHttpClient = MockClient((_) async {
if (retry++ == 0) {
return http.Response('', HttpStatus.serviceUnavailable);
}
return http.Response(branchRegExp, HttpStatus.ok);
});
final List<LogRecord> records = <LogRecord>[];
log.onRecord.listen((LogRecord record) => records.add(record));
final String branches = await githubFileContent(
RepositorySlug('flutter', 'cocoon'),
'branches.txt',
httpClientProvider: () => branchHttpClient,
retryOptions: const RetryOptions(
maxAttempts: 3,
delayFactor: Duration.zero,
maxDelay: Duration.zero,
),
);
final List<String> branchList = branches.split('\n').map((String branch) => branch.trim()).toList();
branchList.removeWhere((String branch) => branch.isEmpty);
expect(retry, 2);
expect(branchList, <String>['master', 'flutter-1.1-candidate.1']);
expect(records.where((LogRecord record) => record.level == Level.INFO), isNotEmpty);
expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty);
});
test('falls back to git on borg', () async {
branchHttpClient = MockClient((http.Request request) async {
if (request.url.toString() ==
'https://flutter.googlesource.com/mirrors/cocoon/+/ba7fe03781762603a1cdc364f8f5de56a0fdbf5c/.ci.yaml?format=text') {
return http.Response(base64Encode(branchRegExp.codeUnits), HttpStatus.ok);
}
// Mock a GitHub outage
return http.Response('', HttpStatus.serviceUnavailable);
});
final List<LogRecord> records = <LogRecord>[];
log.onRecord.listen((LogRecord record) => records.add(record));
final String branches = await githubFileContent(
RepositorySlug('flutter', 'cocoon'),
'.ci.yaml',
httpClientProvider: () => branchHttpClient,
ref: 'ba7fe03781762603a1cdc364f8f5de56a0fdbf5c',
retryOptions: const RetryOptions(
maxAttempts: 1,
delayFactor: Duration.zero,
maxDelay: Duration.zero,
),
);
final List<String> branchList = branches.split('\n').map((String branch) => branch.trim()).toList();
branchList.removeWhere((String branch) => branch.isEmpty);
expect(branchList, <String>['master', 'flutter-1.1-candidate.1']);
});
test('falls back to git on borg when given sha', () async {
branchHttpClient = MockClient((http.Request request) async {
if (request.url.toString() ==
'https://flutter.googlesource.com/mirrors/cocoon/+/refs/heads/main/.ci.yaml?format=text') {
return http.Response(base64Encode(branchRegExp.codeUnits), HttpStatus.ok);
}
// Mock a GitHub outage
return http.Response('', HttpStatus.serviceUnavailable);
});
final List<LogRecord> records = <LogRecord>[];
log.onRecord.listen((LogRecord record) => records.add(record));
final String branches = await githubFileContent(
RepositorySlug('flutter', 'cocoon'),
'.ci.yaml',
ref: 'main',
httpClientProvider: () => branchHttpClient,
retryOptions: const RetryOptions(
maxAttempts: 1,
delayFactor: Duration.zero,
maxDelay: Duration.zero,
),
);
final List<String> branchList = branches.split('\n').map((String branch) => branch.trim()).toList();
branchList.removeWhere((String branch) => branch.isEmpty);
expect(branchList, <String>['master', 'flutter-1.1-candidate.1']);
});
test('gives up after 6 tries', () async {
int retry = 0;
branchHttpClient = MockClient((_) async {
retry++;
return http.Response('', HttpStatus.serviceUnavailable);
});
final List<LogRecord> records = <LogRecord>[];
log.onRecord.listen((LogRecord record) => records.add(record));
await expectLater(
githubFileContent(
RepositorySlug('flutter', 'cocoon'),
'branches.txt',
httpClientProvider: () => branchHttpClient,
retryOptions: const RetryOptions(
maxAttempts: 3,
delayFactor: Duration.zero,
maxDelay: Duration.zero,
),
),
throwsA(isA<HttpException>()));
// It will request from GitHub 3 times, fallback to GoB, then fail.
expect(retry, 6);
expect(records.where((LogRecord record) => record.level == Level.WARNING), isNotEmpty);
});
});
group('GitHubBackoffCalculator', () {
test('twoSecondLinearBackoff', () {
expect(twoSecondLinearBackoff(0), const Duration(seconds: 2));
expect(twoSecondLinearBackoff(1), const Duration(seconds: 4));
expect(twoSecondLinearBackoff(2), const Duration(seconds: 6));
expect(twoSecondLinearBackoff(3), const Duration(seconds: 8));
});
});
group('repoNameForBuilder', () {
test('Builder config does not exist', () async {
final List<LuciBuilder> builders = <LuciBuilder>[];
final RepositorySlug? result = await repoNameForBuilder(builders, 'DoesNotExist');
expect(result, isNull);
});
test('Builder exists', () async {
final List<LuciBuilder> builders = <LuciBuilder>[
const LuciBuilder(name: 'Cocoon', repo: 'cocoon', flaky: false)
];
final RepositorySlug result = (await repoNameForBuilder(builders, 'Cocoon'))!;
expect(result.fullName, equals('flutter/cocoon'));
});
});
group('bigquery', () {
late FakeTabledataResource tabledataResourceApi;
setUp(() {
tabledataResourceApi = FakeTabledataResource();
});
test('Insert data to bigquery', () async {
await insertBigquery('test', <String, dynamic>{'test': 'test'}, tabledataResourceApi);
final TableDataList tableDataList = await tabledataResourceApi.list('test', 'test', 'test');
expect(tableDataList.totalRows, '1');
});
});
group('getFilteredBuilders', () {
List<String> files;
List<Target> targets;
test('does not return builders when run_if does not match any file', () async {
targets = <Target>[
generateTarget(1, runIf: <String>['d/']),
];
files = <String>['a/b', 'c/d'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.isEmpty, isTrue);
});
test('returns builders when run_if is null', () async {
files = <String>['a/b', 'c/d'];
targets = <Target>[generateTarget(1)];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files', () async {
files = <String>['a/b', 'c/d'];
targets = <Target>[
generateTarget(1, runIf: <String>['a/'])
];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with **', () async {
targets = <Target>[
generateTarget(1, runIf: <String>['a/**']),
];
files = <String>['a/b', 'c/d'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with both * and **', () async {
targets = <Target>[
generateTarget(1, runIf: <String>['a/b*c/**']),
];
files = <String>['a/baddsc/defg', 'c/d'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns correct builders when file and folder share the same name', () async {
targets = <Target>[
generateTarget(1, runIf: <String>['a/b/']),
generateTarget(2, runIf: <String>['a']),
];
files = <String>['a'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.length, 1);
expect(result.single, targets[1]);
});
});
});
}