blob: 6b2b71d8606eafd35fda00bb471aecc9cf8db23d [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: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('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', () {
test('does not return builders when run_if does not match any file', () async {
final List<Target> targets = <Target>[
generateTarget(1, runIf: <String>['cde/']),
];
final List<String> files = <String>['abc/cde.py', 'cde/fgh.dart'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.isEmpty, isTrue);
});
test('returns builders when run_if is null', () async {
final List<String> files = <String>['abc/def.py', 'cde/dgh.dart'];
final List<Target> targets = <Target>[generateTarget(1)];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files using full path', () async {
final List<String> files = <String>['abc/cde.py', 'cgh/dhj.dart'];
final List<Target> targets = <Target>[
generateTarget(1, runIf: <String>['abc/cde.py']),
];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with **', () async {
final List<Target> targets = <Target>[
generateTarget(1, runIf: <String>['abc/**']),
];
final List<String> files = <String>['abc/cdf/hj.dart', 'abc/dej.dart'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with ** that contain digits', () async {
final List<Target> targets = <Target>[
generateTarget(
1,
runIf: <String>[
'dev/**',
'packages/flutter/**',
'packages/flutter_driver/**',
'packages/integration_test/**',
'packages/flutter_localizations/**',
'packages/fuchsia_remote_debug_protocol/**',
'packages/flutter_test/**',
'packages/flutter_goldens/**',
'packages/flutter_tools/**',
'bin/**',
'.ci.yaml',
],
),
];
final List<String> files = <String>[
'packages/flutter_localizations/lib/src/l10n/material_es.arb',
'packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb',
];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with * and ** that contains digits', () async {
final List<Target> targets = <Target>[
generateTarget(
1,
runIf: <String>[
'dev/**',
'packages/flutter/**',
'packages/flutter_driver/**',
'packages/integration_test/**',
'packages/flutter_localizations/**/l10n/cupertino*.arb',
'packages/fuchsia_remote_debug_protocol/**',
'packages/flutter_test/**',
'packages/flutter_goldens/**',
'packages/flutter_tools/**',
'bin/**',
'.ci.yaml',
],
),
];
final List<String> files = <String>[
'packages/flutter_localizations/lib/src/l10n/material_es.arb',
'packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb',
'packages/flutter_localizations/lib/src/l10n/cupertino_cy.arb',
];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with * trailing glob', () async {
final List<Target> targets = <Target>[
generateTarget(
1,
runIf: <String>[
'packages/flutter_localizations/**/l10n/*',
],
),
];
final List<String> files = <String>[
'packages/flutter_localizations/lib/src/l10n/material_es.arb',
'packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb',
'packages/flutter_localizations/lib/src/l10n/cupertino_cy.arb',
];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with * trailing glob 2', () async {
final List<Target> targets = <Target>[
generateTarget(
1,
runIf: <String>[
'packages/flutter_localizations/**/l10n/cupertino*',
],
),
];
final List<String> files = <String>[
'packages/flutter_localizations/lib/src/l10n/material_es.arb',
'packages/flutter_localizations/lib/src/l10n/material_en_ZA.arb',
'packages/flutter_localizations/lib/src/l10n/cupertino_cy.arb',
];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, targets);
});
test('returns builders when run_if matches files with ** in the middle', () async {
final List<Target> targets = <Target>[
generateTarget(1, runIf: <String>['abc/**/hj.dart']),
];
final List<String> files = <String>[
'abc/cdf/efg/hj.dart',
'abc/dej.dart',
];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result, [targets[0]]);
});
test('returns builders when run_if matches files with both * and **', () async {
final List<Target> targets = <Target>[
generateTarget(1, runIf: <String>['a/b*c/**']),
];
final List<String> files = <String>['a/baddsc/defg.zz', '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 {
final List<Target> targets = <Target>[
generateTarget(1, runIf: <String>['a/b/']),
generateTarget(2, runIf: <String>['a']),
];
final List<String> files = <String>['a'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.length, 1);
expect(result.single, targets[1]);
});
test('run_if takes precedence over run_if_not', () async {
final List<Target> targets = <Target>[
generateTarget(1, runIf: <String>['a/b/']),
generateTarget(2, runIf: <String>['a'], runIfNot: <String>['a']),
];
final List<String> files = <String>['a'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.length, 1);
expect(result.single, targets[1]);
});
test('no run_if and not run_if_not', () async {
final List<Target> targets = <Target>[
generateTarget(1),
];
final List<String> files = <String>['a'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.length, 1);
expect(result.single, targets[0]);
});
test('run_if_not with matches', () async {
final List<Target> targets = <Target>[
generateTarget(1, runIfNot: ['/a/b/**']),
];
final List<String> files = <String>['/a/b/c/d'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.length, 0);
});
test('run_if_not with no matches', () async {
final List<Target> targets = <Target>[
generateTarget(1, runIfNot: ['/a/b/**']),
];
final List<String> files = <String>['/a/c'];
final List<Target> result = await getTargetsToRun(targets, files);
expect(result.length, 1);
expect(result.single, targets[0]);
});
});
});
group('Fusion Tests', () {
const RetryOptions noRetry = RetryOptions(
maxAttempts: 1,
delayFactor: Duration.zero,
maxDelay: Duration.zero,
);
final goodFlutterRef = (slug: RepositorySlug.full('flutter/flutter'), sha: '1234');
final goodFlauxRef = (slug: RepositorySlug.full('flutter/flaux'), sha: 'abcd');
test('isFusionPR returns false non-flutter repo', () async {
final branchHttpClient = MockClient(
(req) async {
final url = '${req.url}';
if (!url.contains('https://raw.githubusercontent.com/flutter/flutter/DEPS')) {
return http.Response('', HttpStatus.notFound);
}
return http.Response('test', HttpStatus.ok);
},
);
final tester = FusionTester(httpClientProvider: () => branchHttpClient);
final fusion = await tester.isFusionBasedRef(
RepositorySlug('code', 'fu'),
goodFlutterRef.sha,
retryOptions: noRetry,
);
expect(fusion, isFalse);
});
test('isFusionPR returns false for missing DEPS file', () async {
final branchHttpClient = MockClient(
(req) async {
final url = '${req.url}';
if (url.contains('flutter.googlesource.com')) {
return http.Response('', HttpStatus.notFound);
} else if (url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/DEPS',
)) {
return http.Response('', HttpStatus.notFound);
}
return http.Response('test', HttpStatus.ok);
},
);
final tester = FusionTester(httpClientProvider: () => branchHttpClient);
final fusion = await tester.isFusionBasedRef(
goodFlutterRef.slug,
goodFlutterRef.sha,
retryOptions: noRetry,
);
expect(fusion, isFalse);
});
test('isFusionPR returns false for missing engine/src/.gn file', () async {
final branchHttpClient = MockClient(
(req) async {
final url = '${req.url}';
if (url.contains('flutter.googlesource.com')) {
return http.Response('', HttpStatus.notFound);
} else if (url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/engine/src/.gn',
)) {
return http.Response('', HttpStatus.notFound);
}
return http.Response('test', HttpStatus.ok);
},
);
final tester = FusionTester(httpClientProvider: () => branchHttpClient);
final fusion = await tester.isFusionBasedRef(
goodFlutterRef.slug,
goodFlutterRef.sha,
retryOptions: noRetry,
);
expect(fusion, isFalse);
});
test('isFusionPR returns false if required files are empty', () async {
final branchHttpClient = MockClient(
(req) async {
final url = '${req.url}';
if (url.contains('flutter.googlesource.com')) {
return http.Response('', HttpStatus.notFound);
} else if (url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/engine/src/.gn',
) ||
url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/DEPS',
)) {
return http.Response('', HttpStatus.ok);
}
return http.Response('test', HttpStatus.ok);
},
);
final tester = FusionTester(httpClientProvider: () => branchHttpClient);
final fusion = await tester.isFusionBasedRef(
goodFlutterRef.slug,
goodFlutterRef.sha,
retryOptions: noRetry,
);
expect(fusion, isFalse);
});
test('isFusionPR lets non-404 exceptions bubble', () async {
final branchHttpClient = MockClient(
(req) async {
final url = '${req.url}';
if (url.contains('flutter.googlesource.com')) {
return http.Response('', HttpStatus.badRequest);
} else if (url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/engine/src/.gn',
)) {
return http.Response('', HttpStatus.badRequest);
}
return http.Response('test', HttpStatus.ok);
},
);
final tester = FusionTester(httpClientProvider: () => branchHttpClient);
expect(
tester.isFusionBasedRef(
goodFlutterRef.slug,
goodFlutterRef.sha,
retryOptions: noRetry,
),
throwsA(isA<HttpException>()),
);
});
test('isFusionPR returns true whe expected files are present', () async {
final branchHttpClient = MockClient(
(req) async {
final url = '${req.url}';
if (url.contains('flutter.googlesource.com')) {
return http.Response('', HttpStatus.notFound);
} else if (url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/engine/src/.gn',
) ||
url.contains(
'https://raw.githubusercontent.com/flutter/flaux/abcd/engine/src/.gn',
) ||
url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/DEPS',
) ||
url.contains(
'https://raw.githubusercontent.com/flutter/flaux/abcd/DEPS',
)) {
return http.Response('FUSION', HttpStatus.ok);
}
return http.Response('test', HttpStatus.ok);
},
);
for (var request in [goodFlutterRef, goodFlauxRef, goodFlutterRef, goodFlauxRef]) {
final tester = FusionTester(httpClientProvider: () => branchHttpClient);
final fusion = await tester.isFusionBasedRef(
request.slug,
request.sha,
retryOptions: noRetry,
);
expect(fusion, isTrue);
}
});
test('isFusionPR caches results', () async {
final urlCalled = <String, int>{};
final branchHttpClient = MockClient(
(req) async {
final url = '${req.url}';
urlCalled[url] = (urlCalled[url] ?? 0) + 1;
if (url.contains('flutter.googlesource.com')) {
return http.Response('', HttpStatus.notFound);
} else if (url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/engine/src/.gn',
) ||
url.contains(
'https://raw.githubusercontent.com/flutter/flutter/1234/DEPS',
)) {
return http.Response('FUSION', HttpStatus.ok);
}
return http.Response('test', HttpStatus.ok);
},
);
final tester = FusionTester(httpClientProvider: () => branchHttpClient);
for (var request in [goodFlutterRef, goodFlauxRef, goodFlutterRef, goodFlauxRef]) {
final fusion = await tester.isFusionBasedRef(
request.slug,
request.sha,
retryOptions: noRetry,
);
expect(fusion, isTrue);
}
expect(urlCalled['https://raw.githubusercontent.com/flutter/flutter/1234/engine/src/.gn'], 1);
expect(urlCalled['https://raw.githubusercontent.com/flutter/flutter/1234/DEPS'], 1);
expect(urlCalled['https://raw.githubusercontent.com/flutter/flaux/abcd/engine/src/.gn'], 1);
expect(urlCalled['https://raw.githubusercontent.com/flutter/flaux/abcd/DEPS'], 1);
});
});
}