blob: 0f14269e160bd8ac64f976a5c25d0ae2b3b0c6b4 [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 'package:cocoon_common/guard_status.dart';
import 'package:cocoon_common/rpc_model.dart';
import 'package:cocoon_common/task_status.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_dashboard/service/cocoon.dart';
import 'package:flutter_dashboard/state/presubmit.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../utils/mocks.mocks.dart';
void main() {
late PresubmitState presubmitState;
late MockCocoonService mockCocoonService;
late MockFirebaseAuthService mockAuthService;
setUp(() {
mockCocoonService = MockCocoonService();
mockAuthService = MockFirebaseAuthService();
when(mockAuthService.isAuthenticated).thenReturn(false);
when(mockAuthService.idToken).thenAnswer((_) async => 'fakeToken');
// Default stubs to avoid MissingStubError during auto-fetches
when(
mockCocoonService.fetchPresubmitGuardSummaries(
pr: anyNamed('pr'),
repo: anyNamed('repo'),
owner: anyNamed('owner'),
),
).thenAnswer(
(_) async => const CocoonResponse<List<PresubmitGuardSummary>>.data([]),
);
when(
mockCocoonService.fetchPresubmitGuard(
sha: anyNamed('sha'),
repo: anyNamed('repo'),
owner: anyNamed('owner'),
),
).thenAnswer(
(_) async => const CocoonResponse<PresubmitGuardResponse>.data(null),
);
when(
mockCocoonService.fetchPresubmitJobDetails(
checkRunId: anyNamed('checkRunId'),
jobName: anyNamed('jobName'),
repo: anyNamed('repo'),
owner: anyNamed('owner'),
),
).thenAnswer(
(_) async => const CocoonResponse<List<PresubmitJobResponse>>.data([]),
);
when(
mockCocoonService.fetchCommitStatuses(
repo: anyNamed('repo'),
branch: anyNamed('branch'),
),
).thenAnswer(
(_) async => const CocoonResponse<List<CommitStatus>>.data([]),
);
presubmitState = PresubmitState(
cocoonService: mockCocoonService,
authService: mockAuthService,
);
});
test('PresubmitState initializes with default values', () {
expect(presubmitState.repo, 'flutter');
expect(presubmitState.pr, isNull);
expect(presubmitState.sha, isNull);
expect(presubmitState.isLoading, isFalse);
expect(presubmitState.guardResponse, isNull);
expect(presubmitState.availableSummaries, isEmpty);
});
test(
'PresubmitState update method updates properties and notifies listeners',
() {
var notified = false;
presubmitState.addListener(() => notified = true);
presubmitState.update(repo: 'cocoon', pr: '123', sha: 'abc');
expect(presubmitState.repo, 'cocoon');
expect(presubmitState.pr, '123');
expect(presubmitState.sha, 'abc');
expect(notified, isTrue);
},
);
test(
'PresubmitState fetchAvailableShas updates availableSummaries and notifies listeners',
() async {
const summaries = [
PresubmitGuardSummary(
headSha: 'sha1',
creationTime: 123,
guardStatus: GuardStatus.succeeded,
),
];
when(
mockCocoonService.fetchPresubmitGuardSummaries(
pr: '123',
repo: 'flutter',
),
).thenAnswer(
(_) async =>
const CocoonResponse<List<PresubmitGuardSummary>>.data(summaries),
);
presubmitState.pr = '123';
var notified = false;
presubmitState.addListener(() => notified = true);
await presubmitState.fetchAvailableShas();
expect(presubmitState.availableSummaries, summaries);
expect(notified, isTrue);
},
);
test(
'PresubmitState fetchGuardStatus updates guardResponse and notifies listeners',
() async {
const guardResponse = PresubmitGuardResponse(
prNum: 123,
author: 'dash',
guardStatus: GuardStatus.succeeded,
checkRunId: 456,
stages: [],
);
when(
mockCocoonService.fetchPresubmitGuard(sha: 'sha1', repo: 'flutter'),
).thenAnswer(
(_) async =>
const CocoonResponse<PresubmitGuardResponse>.data(guardResponse),
);
presubmitState.sha = 'sha1';
var notified = false;
presubmitState.addListener(() => notified = true);
await presubmitState.fetchGuardStatus();
expect(presubmitState.guardResponse, guardResponse);
expect(presubmitState.isLoading, isFalse);
expect(notified, isTrue);
},
);
test(
'PresubmitState fetchCheckDetails updates checks and notifies listeners',
() async {
final checks = [
PresubmitJobResponse(
attemptNumber: 1,
jobName: 'check1',
creationTime: 0,
status: TaskStatus.succeeded,
),
];
const guardResponse = PresubmitGuardResponse(
prNum: 123,
author: 'dash',
guardStatus: GuardStatus.succeeded,
checkRunId: 456,
stages: [],
);
presubmitState.setGuardResponseForTest(guardResponse);
when(
mockCocoonService.fetchPresubmitJobDetails(
checkRunId: 456,
jobName: 'check1',
repo: 'flutter',
),
).thenAnswer(
(_) async => CocoonResponse<List<PresubmitJobResponse>>.data(checks),
);
presubmitState.selectJob('check1');
var notified = false;
presubmitState.addListener(() => notified = true);
await presubmitState.fetchJobDetails();
expect(presubmitState.jobs, checks);
expect(notified, isTrue);
},
);
test(
'PresubmitState update triggers notification due to auto-fetch even if values are the same',
() {
presubmitState.update(repo: 'flutter', pr: '123', sha: 'sha1');
var notifiedCount = 0;
presubmitState.addListener(() => notifiedCount++);
presubmitState.update(repo: 'flutter', pr: '123', sha: 'sha1');
expect(notifiedCount, 1);
},
);
test(
'PresubmitState fetchAvailableShas returns early if pr is null',
() async {
await presubmitState.fetchAvailableShas();
verifyNever(
mockCocoonService.fetchPresubmitGuardSummaries(
repo: anyNamed('repo'),
pr: anyNamed('pr'),
),
);
},
);
test(
'PresubmitState fetchAvailableShas defaults sha to latest if sha is null',
() async {
const summaries = [
PresubmitGuardSummary(
headSha: 'latest',
creationTime: 123,
guardStatus: GuardStatus.succeeded,
),
];
when(
mockCocoonService.fetchPresubmitGuardSummaries(
pr: '123',
repo: 'flutter',
),
).thenAnswer(
(_) async =>
const CocoonResponse<List<PresubmitGuardSummary>>.data(summaries),
);
presubmitState.pr = '123';
await presubmitState.fetchAvailableShas();
expect(presubmitState.sha, 'latest');
},
);
test(
'PresubmitState fetchGuardStatus returns early if sha is null',
() async {
await presubmitState.fetchGuardStatus();
verifyNever(
mockCocoonService.fetchPresubmitGuard(
repo: anyNamed('repo'),
sha: anyNamed('sha'),
),
);
},
);
test('PresubmitState refresh timer management', () async {
presubmitState.addListener(() {}); // Trigger timer start
expect(presubmitState.refreshTimer, isNotNull);
presubmitState.dispose();
expect(presubmitState.refreshTimer?.isActive, isFalse);
});
test('PresubmitState pause and resume timer management', () async {
presubmitState.addListener(() {}); // Trigger timer start
expect(presubmitState.refreshTimer, isNotNull);
presubmitState.pause();
expect(presubmitState.refreshTimer, isNull);
presubmitState.resume();
expect(presubmitState.refreshTimer, isNotNull);
});
test(
'PresubmitState refreshes on auth change when becoming authenticated',
() async {
// Create a local state initialized with PR
final localPresubmitState = PresubmitState(
cocoonService: mockCocoonService,
authService: mockAuthService,
);
localPresubmitState.syncUpdate({'pr': '123'});
// Wait for constructor fetch
await Future<void>.delayed(Duration.zero);
clearInteractions(mockCocoonService);
// Now login
when(mockAuthService.isAuthenticated).thenReturn(true);
localPresubmitState.onAuthChanged();
// onAuthChanged triggers _fetchRefreshUpdate -> fetchIfNeeded -> fetchAvailableShas
// which is async. We just need to wait for it to complete.
await localPresubmitState.fetchAvailableShas();
verify(
mockCocoonService.fetchPresubmitGuardSummaries(
repo: anyNamed('repo'),
pr: anyNamed('pr'),
),
).called(greaterThanOrEqualTo(1));
},
);
test(
'PresubmitState refreshes on auth change when becoming unauthenticated',
() async {
when(mockAuthService.isAuthenticated).thenReturn(true);
final localPresubmitState = PresubmitState(
cocoonService: mockCocoonService,
authService: mockAuthService,
);
localPresubmitState.syncUpdate({'pr': '123'});
await localPresubmitState.fetchIfNeeded();
await Future<void>.delayed(Duration.zero);
clearInteractions(mockCocoonService);
when(mockAuthService.isAuthenticated).thenReturn(false);
localPresubmitState.onAuthChanged();
await Future<void>.delayed(Duration.zero);
// Should not refresh when logout
verifyNever(
mockCocoonService.fetchPresubmitGuardSummaries(
repo: anyNamed('repo'),
pr: anyNamed('pr'),
),
);
},
);
test('rerunFailedJob triggers API and updates loading state', () async {
when(
mockCocoonService.rerunFailedJob(
idToken: anyNamed('idToken'),
repo: anyNamed('repo'),
pr: anyNamed('pr'),
jobName: anyNamed('jobName'),
),
).thenAnswer((_) async => const CocoonResponse<void>.data(null));
presubmitState.pr = '123';
final error = await presubmitState.rerunFailedJob('check1');
expect(error, isNull);
verify(
mockCocoonService.rerunFailedJob(
idToken: anyNamed('idToken'),
repo: 'flutter',
pr: 123,
jobName: 'check1',
),
).called(1);
});
test('rerunAllFailedJobs triggers API and updates loading state', () async {
when(
mockCocoonService.rerunAllFailedJobs(
idToken: anyNamed('idToken'),
repo: anyNamed('repo'),
pr: anyNamed('pr'),
),
).thenAnswer((_) async => const CocoonResponse<void>.data(null));
presubmitState.pr = '123';
final error = await presubmitState.rerunAllFailedJobs();
expect(error, isNull);
verify(
mockCocoonService.rerunAllFailedJobs(
idToken: anyNamed('idToken'),
repo: 'flutter',
pr: 123,
),
).called(1);
});
test(
'syncUpdate with null sha resets _lastFetchedPr to force re-fetch when pr is the same',
() async {
const summaries = [
PresubmitGuardSummary(
headSha: 'latest',
creationTime: 123,
guardStatus: GuardStatus.succeeded,
),
];
when(
mockCocoonService.fetchPresubmitGuardSummaries(
pr: '123',
repo: 'flutter',
),
).thenAnswer(
(_) async =>
const CocoonResponse<List<PresubmitGuardSummary>>.data(summaries),
);
presubmitState.update(repo: 'flutter', pr: '123', sha: 'sha1');
expect(presubmitState.sha, 'sha1');
await Future<void>.delayed(Duration.zero);
clearInteractions(mockCocoonService);
presubmitState.update(pr: '123', sha: null);
await Future<void>.delayed(Duration.zero);
verify(
mockCocoonService.fetchPresubmitGuardSummaries(
pr: '123',
repo: 'flutter',
),
).called(1);
expect(presubmitState.sha, 'latest');
},
);
group('canAnalyzeLog', () {
final failedJob = PresubmitJobResponse(
attemptNumber: 1,
jobName: 'check1',
creationTime: 0,
status: TaskStatus.failed,
buildId: Int64.MAX_VALUE.toString(),
);
test('returns false when enableGeminiLogAnalysis is false', () {
const guardResponse = PresubmitGuardResponse(
prNum: 123,
author: 'dash',
guardStatus: GuardStatus.failed,
checkRunId: 456,
stages: [],
enableGeminiLogAnalysis: false,
);
presubmitState.setGuardResponseForTest(guardResponse);
when(mockAuthService.isAuthenticated).thenReturn(true);
expect(presubmitState.canAnalyzeLog(failedJob), isFalse);
});
test(
'returns true when enableGeminiLogAnalysis is true and other conditions met',
() {
const guardResponse = PresubmitGuardResponse(
prNum: 123,
author: 'dash',
guardStatus: GuardStatus.failed,
checkRunId: 456,
stages: [],
enableGeminiLogAnalysis: true,
);
presubmitState.setGuardResponseForTest(guardResponse);
when(mockAuthService.isAuthenticated).thenReturn(true);
expect(presubmitState.canAnalyzeLog(failedJob), isTrue);
},
);
test(
'returns false when enableGeminiLogAnalysis is true but user is not authenticated',
() {
const guardResponse = PresubmitGuardResponse(
prNum: 123,
author: 'dash',
guardStatus: GuardStatus.failed,
checkRunId: 456,
stages: [],
enableGeminiLogAnalysis: true,
);
presubmitState.setGuardResponseForTest(guardResponse);
when(mockAuthService.isAuthenticated).thenReturn(false);
expect(presubmitState.canAnalyzeLog(failedJob), isFalse);
},
);
});
group('query parameters as source of truth', () {
test('filters and job read from and written to query parameters', () {
final state = PresubmitState(
authService: mockAuthService,
cocoonService: mockCocoonService,
);
final guardResponse = const PresubmitGuardResponse(
prNum: 123,
author: 'dash',
guardStatus: GuardStatus.succeeded,
checkRunId: 456,
stages: [
PresubmitGuardStage(
name: 'stage1',
createdAt: 0,
builds: {'Mac my_job': TaskStatus.succeeded},
),
],
);
state.setGuardResponseForTest(guardResponse);
expect(state.selectedStatuses, TaskStatus.values.toSet());
expect(state.jobNameFilter, isNull);
state.updateFilters(
statuses: {TaskStatus.failed, TaskStatus.inProgress},
platforms: {'Mac', 'Linux'},
jobNameFilter: '.*test.*',
);
expect(state.queryParameters['statuses'], 'Failed,In%20Progress');
expect(state.queryParameters['platforms'], 'Mac,Linux');
expect(state.queryParameters['regex'], '.*test.*');
state.syncUpdate({
'statuses': 'Succeeded',
'platforms': 'Mac',
'regex': '.*my_job.*',
'job': 'Mac%20my_job',
});
expect(state.selectedStatuses, {TaskStatus.succeeded});
expect(state.selectedPlatforms, {'Mac'});
expect(state.jobNameFilter, '.*my_job.*');
expect(state.selectedJob, 'Mac my_job');
});
});
}