blob: 3578c5612092e9f7a97bbfffeee9b3bbe1962102 [file] [edit]
// Copyright 2019 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:async';
import 'package:cocoon_common/rpc_model.dart';
import 'package:cocoon_common/task_status.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_app_icons/flutter_app_icons_platform_interface.dart';
import 'package:flutter_dashboard/service/cocoon.dart';
import 'package:flutter_dashboard/service/firebase_auth.dart';
import 'package:flutter_dashboard/state/build.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../utils/fake_flutter_app_icons.dart';
import '../utils/generate_commit_for_tests.dart';
import '../utils/generate_task_for_tests.dart';
import '../utils/mocks.dart';
import '../utils/output.dart';
void main() {
const defaultBranch = 'master';
group('BuildState', () {
late MockCocoonService mockCocoonService;
late CommitStatus setupCommitStatus;
setUp(() {
mockCocoonService = MockCocoonService();
setupCommitStatus = _createCommitStatus('setup');
when(
// ignore: discarded_futures
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(dynamic _) async => CocoonResponse<List<CommitStatus>>.data(
<CommitStatus>[setupCommitStatus],
),
);
when(
// ignore: discarded_futures
mockCocoonService.fetchTreeBuildStatus(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(_) async => CocoonResponse<BuildStatusResponse>.data(
BuildStatusResponse(
buildStatus: BuildStatus.success,
failingTasks: [],
),
),
);
// ignore: discarded_futures
when(mockCocoonService.fetchRepos()).thenAnswer(
(_) async =>
const CocoonResponse<List<String>>.data(<String>['flutter']),
);
// ignore: discarded_futures
when(mockCocoonService.fetchFlutterBranches()).thenAnswer(
(_) async => CocoonResponse<List<Branch>>.data([
Branch(channel: defaultBranch, reference: defaultBranch),
]),
);
when(
mockCocoonService.fetchSuppressedTests(repo: anyNamed('repo')),
).thenAnswer(
(_) async => const CocoonResponse<List<SuppressedTest>>.data([]),
);
FlutterAppIconsPlatform.instance = FakeFlutterAppIcons();
});
tearDown(() {
clearInteractions(mockCocoonService);
});
testWidgets('start calls fetch branches', (WidgetTester tester) async {
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
);
void listener() {}
buildState.addListener(listener);
// startFetching immediately starts fetching results
verify(await mockCocoonService.fetchFlutterBranches()).called(1);
buildState.dispose();
});
testWidgets('timer should periodically fetch updates', (
WidgetTester tester,
) async {
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
);
verifyNever(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
void listener() {}
buildState.addListener(listener);
// startFetching immediately starts fetching results
verify(
await mockCocoonService.fetchCommitStatuses(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
verifyNever(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
await tester.pump(buildState.refreshRate! * 2);
verify(
await mockCocoonService.fetchCommitStatuses(
branch: defaultBranch,
repo: 'flutter',
),
).called(2);
buildState.dispose();
});
testWidgets('updateCurrentRepoBranch should make old updates stale', (
WidgetTester tester,
) async {
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
);
verifyNever(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
expect(buildState.statuses, isEmpty);
void listener() {}
// This invokes startFetchUpdates
buildState.addListener(listener);
// startFetching immediately starts fetching results (and returns fake data)
verify(
await mockCocoonService.fetchCommitStatuses(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
verifyNever(
mockCocoonService.fetchCommitStatuses(branch: 'main', repo: 'cocoon'),
);
expect(buildState.statuses, isNotEmpty);
// Start another Timer.periodic async call
await tester.pump();
// Change the repo to Cocoon while a timer is set, and Cocoon is not expected to return data
buildState.updateCurrentRepoBranch('cocoon', 'main');
expect(buildState.statuses, isEmpty);
await untilCalled(
mockCocoonService.fetchCommitStatuses(
branch: defaultBranch,
repo: 'flutter',
),
);
expect(buildState.statuses, isEmpty);
buildState.dispose();
});
test('multiple start updates should not change the timer', () {
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
);
void listener1() {}
buildState.addListener(listener1);
// Another listener shouldn't change the timer.
final refreshTimer = buildState.refreshTimer;
void listener2() {}
buildState.addListener(listener2);
expect(buildState.refreshTimer, equals(refreshTimer));
// Removing a listener shouldn't change the timer.
buildState.removeListener(listener1);
expect(buildState.refreshTimer, equals(refreshTimer));
// Removing both listeners should cancel the timer.
buildState.removeListener(listener2);
expect(buildState.refreshTimer, isNull);
// A new listener now should change the timer.
buildState.addListener(listener1);
expect(buildState.refreshTimer, isNot(isNull));
expect(buildState.refreshTimer, isNot(equals(refreshTimer)));
});
testWidgets('statuses error should not delete previous statuses data', (
WidgetTester tester,
) async {
String? lastError;
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
)..errors.addListener((String message) => lastError = message);
verifyNever(
mockCocoonService.fetchTreeBuildStatus(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
verifyNever(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
void listener() {}
buildState.addListener(listener);
verify(
mockCocoonService.fetchTreeBuildStatus(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
verify(
mockCocoonService.fetchCommitStatuses(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
await tester.pump();
final originalData = buildState.statuses;
verifyNever(
mockCocoonService.fetchTreeBuildStatus(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
verifyNever(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
when(
mockCocoonService.fetchCommitStatuses(
branch: defaultBranch,
repo: 'flutter',
),
).thenAnswer(
(_) => Future<CocoonResponse<List<CommitStatus>>>.value(
const CocoonResponse<List<CommitStatus>>.error(
'error',
statusCode: 500,
),
),
);
await checkOutput(
block: () async {
await tester.pump(buildState.refreshRate);
},
output: <String>[
'An error occurred fetching build statuses from Cocoon: error',
],
);
verify(
await mockCocoonService.fetchTreeBuildStatus(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
verify(
await mockCocoonService.fetchCommitStatuses(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
expect(buildState.statuses, originalData);
expect(lastError, startsWith(BuildState.errorMessageFetchingStatuses));
buildState.dispose();
});
testWidgets(
'build status error should not delete previous build status data',
(WidgetTester tester) async {
String? lastError;
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
)..errors.addListener((String message) => lastError = message);
verifyNever(
mockCocoonService.fetchTreeBuildStatus(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
void listener() {}
buildState.addListener(listener);
await tester.pump();
verify(
mockCocoonService.fetchTreeBuildStatus(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
final originalData = buildState.isTreeBuilding;
verifyNever(
mockCocoonService.fetchTreeBuildStatus(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
verifyNever(
mockCocoonService.fetchTreeBuildStatus(
branch: defaultBranch,
repo: 'flutter',
),
);
when(
mockCocoonService.fetchTreeBuildStatus(
branch: defaultBranch,
repo: 'flutter',
),
).thenAnswer(
(_) => Future<CocoonResponse<BuildStatusResponse>>.value(
const CocoonResponse<BuildStatusResponse>.error(
'error',
statusCode: 500,
),
),
);
await checkOutput(
block: () async {
await tester.pump(buildState.refreshRate);
},
output: <String>[
'An error occurred fetching tree status from Cocoon: error',
],
);
verify(
await mockCocoonService.fetchTreeBuildStatus(
branch: defaultBranch,
repo: 'flutter',
),
).called(1);
expect(buildState.isTreeBuilding, originalData);
expect(
lastError,
startsWith(BuildState.errorMessageFetchingTreeStatus),
);
buildState.dispose();
},
);
testWidgets('fetch more commit statuses appends', (
WidgetTester tester,
) async {
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
);
void listener() {}
buildState.addListener(listener);
await untilCalled(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
expect(buildState.statuses, <CommitStatus?>[setupCommitStatus]);
final statusA = _createCommitStatus('A');
when(
mockCocoonService.fetchCommitStatuses(
lastCommitStatus: captureThat(isNotNull, named: 'lastCommitStatus'),
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(_) async =>
CocoonResponse<List<CommitStatus>>.data(<CommitStatus>[statusA]),
);
await buildState.fetchMoreCommitStatuses();
expect(buildState.statuses, <CommitStatus?>[setupCommitStatus, statusA]);
await tester.pump(buildState.refreshRate);
expect(buildState.statuses, <CommitStatus?>[setupCommitStatus, statusA]);
expect(buildState.moreStatusesExist, true);
buildState.dispose();
});
testWidgets('fetchMoreCommitStatuses returns empty stops fetching more', (
WidgetTester tester,
) async {
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
);
void listener() {}
buildState.addListener(listener);
await untilCalled(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
expect(buildState.statuses, <CommitStatus?>[setupCommitStatus]);
when(
mockCocoonService.fetchCommitStatuses(
lastCommitStatus: captureThat(isNotNull, named: 'lastCommitStatus'),
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(_) async =>
const CocoonResponse<List<CommitStatus>>.data(<CommitStatus>[]),
);
await buildState.fetchMoreCommitStatuses();
expect(buildState.statuses, <CommitStatus?>[setupCommitStatus]);
expect(buildState.moreStatusesExist, false);
buildState.dispose();
});
testWidgets('update branch resets build state data', (
WidgetTester tester,
) async {
// Only return statuses when on master branch
when(
mockCocoonService.fetchCommitStatuses(
branch: 'master',
repo: 'flutter',
),
).thenAnswer(
(_) => Future<CocoonResponse<List<CommitStatus>>>.value(
CocoonResponse<List<CommitStatus>>.data(<CommitStatus>[
setupCommitStatus,
]),
).then((CocoonResponse<List<CommitStatus>> value) => value),
);
// Mark tree green on master, red on dev
when(
mockCocoonService.fetchTreeBuildStatus(
branch: 'master',
repo: 'flutter',
),
).thenAnswer(
(_) => Future<CocoonResponse<BuildStatusResponse>>.value(
CocoonResponse<BuildStatusResponse>.data(
BuildStatusResponse(
buildStatus: BuildStatus.success,
failingTasks: [],
),
),
),
);
when(
mockCocoonService.fetchTreeBuildStatus(branch: 'dev', repo: 'flutter'),
).thenAnswer(
(_) => Future<CocoonResponse<BuildStatusResponse>>.value(
CocoonResponse<BuildStatusResponse>.data(
BuildStatusResponse(
buildStatus: BuildStatus.failure,
failingTasks: ['failing_task_1'],
),
),
),
);
final buildState = BuildState(
authService: MockFirebaseAuthService(),
cocoonService: mockCocoonService,
);
void listener() {}
buildState.addListener(listener);
await untilCalled(
mockCocoonService.fetchCommitStatuses(
branch: 'master',
repo: 'flutter',
),
);
expect(buildState.statuses, isNotEmpty);
expect(buildState.isTreeBuilding, isNotNull);
// With mockito, the fetch requests for data will finish immediately
buildState.updateCurrentRepoBranch('flutter', 'dev');
await tester.pump();
expect(buildState.statuses, isEmpty);
expect(buildState.isTreeBuilding, false);
expect(buildState.moreStatusesExist, true);
buildState.dispose();
});
});
group('refreshGitHubCommits', () {
late MockCocoonService cocoonService;
late MockFirebaseAuthService authService;
setUp(() {
cocoonService = MockCocoonService();
authService = MockFirebaseAuthService();
});
testWidgets('fails fast when !isAuthenticated', (_) async {
when(authService.isAuthenticated).thenReturn(false);
final buildState = BuildState(
authService: authService,
cocoonService: cocoonService,
);
final result = await buildState.refreshGitHubCommits();
expect(result, isFalse);
verifyNever(
cocoonService.vacuumGitHubCommits(
any,
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
);
});
testWidgets(
'clears user when vacuumGitHubCommits fails due to authentication',
(_) async {
const idToken = 'id_token';
when(authService.isAuthenticated).thenReturn(true);
when(authService.idToken).thenAnswer((_) async => idToken);
when(
cocoonService.vacuumGitHubCommits(
idToken,
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(_) async => const CocoonResponse.error('Bad user', statusCode: 401),
);
final buildState = BuildState(
authService: authService,
cocoonService: cocoonService,
);
final result = await buildState.refreshGitHubCommits();
expect(result, isFalse);
verify(authService.clearUser()).called(1);
},
);
testWidgets('returns true when vacuumGitHubCommits succeeds', (_) async {
const idToken = 'id_token';
when(authService.isAuthenticated).thenReturn(true);
when(authService.idToken).thenAnswer((_) async => idToken);
when(
cocoonService.vacuumGitHubCommits(
idToken,
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer((_) async => const CocoonResponse.data(true));
final buildState = BuildState(
authService: authService,
cocoonService: cocoonService,
);
final result = await buildState.refreshGitHubCommits();
expect(result, isTrue);
verifyNever(authService.clearUser());
});
});
group('rerunTask', () {
late MockCocoonService cocoonService;
late MockFirebaseAuthService authService;
final task = generateTaskForTest(status: TaskStatus.failed);
final commit = generateCommitForTest();
setUp(() {
cocoonService = MockCocoonService();
authService = MockFirebaseAuthService();
});
testWidgets('fails fast when !isAuthenticated', (_) async {
when(authService.isAuthenticated).thenReturn(false);
final buildState = BuildState(
authService: authService,
cocoonService: cocoonService,
);
final result = await buildState.rerunTask(task, commit);
expect(result, isFalse);
verifyNever(
cocoonService.rerunTask(
idToken: anyNamed('idToken'),
taskName: anyNamed('taskName'),
commitSha: anyNamed('commitSha'),
repo: anyNamed('repo'),
branch: anyNamed('branch'),
),
);
});
group('when rerunTask fails', () {
const idToken = 'id_token';
late BuildState buildState;
setUp(() {
when(authService.isAuthenticated).thenReturn(true);
when(authService.idToken).thenAnswer((_) async => idToken);
buildState = BuildState(
authService: authService,
cocoonService: cocoonService,
);
});
test('not signed out on most error codes', () async {
when(
cocoonService.rerunTask(
idToken: argThat(equals(idToken), named: 'idToken'),
taskName: argThat(equals(task.builderName), named: 'taskName'),
commitSha: anyNamed('commitSha'),
repo: anyNamed('repo'),
branch: anyNamed('branch'),
),
).thenAnswer(
(_) async => const CocoonResponse<bool>.error(
'Internal server error',
statusCode: 500,
),
);
await expectLater(
buildState.rerunTask(task, commit),
completion(isFalse),
);
verifyNever(authService.clearUser());
});
test('is signed out on 401 unauthorized', () async {
when(
cocoonService.rerunTask(
idToken: argThat(equals(idToken), named: 'idToken'),
taskName: argThat(equals(task.builderName), named: 'taskName'),
commitSha: anyNamed('commitSha'),
repo: anyNamed('repo'),
branch: anyNamed('branch'),
),
).thenAnswer(
(_) async => const CocoonResponse<bool>.error(
'Credentials not valid',
statusCode: 401,
),
);
await expectLater(
buildState.rerunTask(task, commit),
completion(isFalse),
);
verify(authService.clearUser()).called(1);
});
});
testWidgets('returns true when rerunTask succeeds', (_) async {
const idToken = 'id_token';
when(authService.isAuthenticated).thenReturn(true);
when(authService.idToken).thenAnswer((_) async => idToken);
when(
cocoonService.rerunTask(
idToken: argThat(equals(idToken), named: 'idToken'),
taskName: argThat(equals(task.builderName), named: 'taskName'),
commitSha: anyNamed('commitSha'),
repo: anyNamed('repo'),
branch: anyNamed('branch'),
),
).thenAnswer((_) async => const CocoonResponse<bool>.data(true));
final buildState = BuildState(
authService: authService,
cocoonService: cocoonService,
);
final result = await buildState.rerunTask(task, commit);
expect(result, isTrue);
verifyNever(authService.clearUser());
});
});
group('updateTestSuppression', () {
late MockCocoonService cocoonService;
late MockFirebaseAuthService authService;
const idToken = 'id_token';
setUp(() {
cocoonService = MockCocoonService();
authService = MockFirebaseAuthService();
when(authService.isAuthenticated).thenReturn(true);
when(authService.idToken).thenAnswer((_) async => idToken);
when(
cocoonService.fetchSuppressedTests(repo: anyNamed('repo')),
).thenAnswer(
(_) async => const CocoonResponse<List<SuppressedTest>>.data([]),
);
when(
cocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(_) async => const CocoonResponse<List<CommitStatus>>.data([]),
);
when(
cocoonService.fetchTreeBuildStatus(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(_) async => CocoonResponse<BuildStatusResponse>.data(
BuildStatusResponse(
buildStatus: BuildStatus.success,
failingTasks: [],
),
),
);
when(
cocoonService.fetchFlutterBranches(),
).thenAnswer((_) async => const CocoonResponse<List<Branch>>.data([]));
when(
cocoonService.fetchRepos(),
).thenAnswer((_) async => const CocoonResponse<List<String>>.data([]));
});
testWidgets('prepends flutter/ to repository name', (_) async {
when(
cocoonService.updateTestSuppression(
idToken: anyNamed('idToken'),
testName: anyNamed('testName'),
suppress: anyNamed('suppress'),
issueLink: anyNamed('issueLink'),
repo: anyNamed('repo'),
note: anyNamed('note'),
),
).thenAnswer((_) async => const CocoonResponse<bool>.data(true));
final buildState = BuildState(
authService: authService,
cocoonService: cocoonService,
);
// Default repo is flutter
expect(buildState.currentRepo, 'flutter');
await buildState.updateTestSuppression(
testName: 'my_test',
suppress: true,
issueLink: 'link',
);
verify(
cocoonService.updateTestSuppression(
idToken: anyNamed('idToken'),
testName: anyNamed('testName'),
suppress: anyNamed('suppress'),
issueLink: anyNamed('issueLink'),
repo: 'flutter/flutter',
note: anyNamed('note'),
),
).called(1);
verify(cocoonService.fetchSuppressedTests(repo: 'flutter/flutter'));
// Switch to engine
buildState.updateCurrentRepoBranch('engine', 'main');
await buildState.updateTestSuppression(
testName: 'my_test',
suppress: true,
issueLink: 'link',
);
verify(cocoonService.fetchSuppressedTests(repo: 'flutter/engine'));
verify(
cocoonService.updateTestSuppression(
idToken: anyNamed('idToken'),
testName: anyNamed('testName'),
suppress: anyNamed('suppress'),
issueLink: anyNamed('issueLink'),
repo: 'flutter/engine',
note: anyNamed('note'),
),
).called(1);
});
});
testWidgets('sign in functions call notify listener', (
WidgetTester tester,
) async {
final mockSignIn = MockFirebaseAuth();
when(
mockSignIn.authStateChanges(),
).thenAnswer((_) => const Stream<User>.empty());
final cred = MockUserCredential();
when(cred.user).thenReturn(null);
when(mockSignIn.signInWithPopup(any)).thenAnswer((_) async => cred);
when(mockSignIn.signOut()).thenAnswer((_) async {});
final mockCocoonService = MockCocoonService();
when(
mockCocoonService.fetchFlutterBranches(),
).thenAnswer((_) => Completer<CocoonResponse<List<Branch>>>().future);
when(
mockCocoonService.fetchCommitStatuses(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer((_) => Completer<CocoonResponse<List<CommitStatus>>>().future);
when(
mockCocoonService.fetchRepos(),
).thenAnswer((_) => Completer<CocoonResponse<List<String>>>().future);
when(
mockCocoonService.fetchTreeBuildStatus(
branch: anyNamed('branch'),
repo: anyNamed('repo'),
),
).thenAnswer(
(_) => Completer<CocoonResponse<BuildStatusResponse>>().future,
);
final signInService = FirebaseAuthService(auth: mockSignIn);
final buildState = BuildState(
cocoonService: mockCocoonService,
authService: signInService,
);
var callCount = 0;
buildState.addListener(() => callCount += 1);
await tester.pump(const Duration(seconds: 5));
await signInService.signInWithGoogle();
expect(callCount, 1);
await signInService.signOut();
expect(callCount, 2);
buildState.dispose();
});
}
CommitStatus _createCommitStatus(
String keyValue, {
String branch = 'master',
String repo = 'flutter',
}) {
return CommitStatus(
commit: generateCommitForTest(
author: keyValue,
repository: 'flutter/$repo',
branch: branch,
),
tasks: [],
);
}