diff --git a/app_flutter/lib/main.dart b/app_flutter/lib/main.dart index 56c04ecc35..a6ac6dd8c5 100644 --- a/app_flutter/lib/main.dart +++ b/app_flutter/lib/main.dart @@ -40,9 +40,12 @@ class _BuildDashboardPageState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); return Scaffold( appBar: AppBar( title: Text('Flutter Build Dashboard v2'), + backgroundColor: + buildState.isTreeBuilding ? theme.primaryColor : theme.errorColor, ), body: Column( children: [ diff --git a/app_flutter/lib/service/appengine_cocoon.dart b/app_flutter/lib/service/appengine_cocoon.dart index 138d08bfcd..4fec3d4705 100644 --- a/app_flutter/lib/service/appengine_cocoon.dart +++ b/app_flutter/lib/service/appengine_cocoon.dart @@ -46,6 +46,48 @@ class AppEngineCocoonService implements CocoonService { return _commitStatusesFromJson(jsonResponse['Statuses']); } + @override + Future fetchTreeBuildStatus() async { + /// This endpoint returns JSON {AnticipatedBuildStatus: [BuildStatus]} + http.Response response = + await _client.get('$_baseApiUrl/public/build-status'); + + if (response.statusCode != HttpStatus.ok) { + throw HttpException( + '$_baseApiUrl/public/build-status returned ${response.statusCode}'); + } + + Map jsonResponse = jsonDecode(response.body); + + if (!_isBuildStatusResponseValid(jsonResponse)) { + throw HttpException( + '$_baseApiUrl/public/build-status had a malformed response'); + } + + return jsonResponse['AnticipatedBuildStatus'] == "Succeeded"; + } + + /// Check if [Map] follows the format for build-status. + /// + /// Template of the response it should receive: + /// ```json + /// { + /// "AnticipatedBuildStatus": "Succeeded"|"Failed" + /// } + /// ``` + bool _isBuildStatusResponseValid(Map response) { + if (!response.containsKey('AnticipatedBuildStatus')) { + return false; + } + + String treeBuildStatus = response['AnticipatedBuildStatus']; + if (treeBuildStatus != 'Failed' && treeBuildStatus != 'Succeeded') { + return false; + } + + return true; + } + List _commitStatusesFromJson(List jsonCommitStatuses) { assert(jsonCommitStatuses != null); // TODO(chillers): Remove adapter code to just use proto fromJson method. https://github.com/flutter/cocoon/issues/441 diff --git a/app_flutter/lib/service/cocoon.dart b/app_flutter/lib/service/cocoon.dart index 3f9df8aecb..872e8872e1 100644 --- a/app_flutter/lib/service/cocoon.dart +++ b/app_flutter/lib/service/cocoon.dart @@ -31,4 +31,7 @@ abstract class CocoonService { /// /// TODO(chillers): Make configurable to get range of commits Future> fetchCommitStatuses(); + + /// Gets the current build status of flutter/flutter. + Future fetchTreeBuildStatus(); } diff --git a/app_flutter/lib/service/fake_cocoon.dart b/app_flutter/lib/service/fake_cocoon.dart index 4e2fb67243..6ce77a2273 100644 --- a/app_flutter/lib/service/fake_cocoon.dart +++ b/app_flutter/lib/service/fake_cocoon.dart @@ -18,6 +18,11 @@ class FakeCocoonService implements CocoonService { return Future.value(_createFakeCommitStatuses()); } + @override + Future fetchTreeBuildStatus() async { + return true; + } + List _createFakeCommitStatuses() { List stats = []; diff --git a/app_flutter/lib/state/flutter_build.dart b/app_flutter/lib/state/flutter_build.dart index c663a66947..b302474ca8 100644 --- a/app_flutter/lib/state/flutter_build.dart +++ b/app_flutter/lib/state/flutter_build.dart @@ -26,6 +26,10 @@ class FlutterBuildState extends ChangeNotifier { /// The current status of the commits loaded. List statuses = []; + /// Whether or not flutter/flutter currently passes tests. + bool get isTreeBuilding => _isTreeBuilding; + bool _isTreeBuilding = true; + /// Creates a new [FlutterBuildState]. /// /// If [CocoonService] is not specified, a new [CocoonService] instance is created. @@ -39,13 +43,24 @@ class FlutterBuildState extends ChangeNotifier { return; } + /// [Timer.periodic] does not necessarily run at the start of the timer. + _fetchBuildStatusUpdate(); + refreshTimer = Timer.periodic(refreshRate, (t) => _fetchBuildStatusUpdate()); } /// Request the latest [statuses] from [CocoonService]. void _fetchBuildStatusUpdate() async { - statuses = await _cocoonService.fetchCommitStatuses(); + await Future.wait([ + _cocoonService + .fetchCommitStatuses() + .then((commitStatuses) => statuses = commitStatuses), + _cocoonService + .fetchTreeBuildStatus() + .then((treeStatus) => _isTreeBuilding = treeStatus), + ]); + notifyListeners(); } diff --git a/app_flutter/test/service/appengine_cocoon_test.dart b/app_flutter/test/service/appengine_cocoon_test.dart index 2811d53e58..2582c34528 100644 --- a/app_flutter/test/service/appengine_cocoon_test.dart +++ b/app_flutter/test/service/appengine_cocoon_test.dart @@ -63,8 +63,20 @@ const String jsonGetStatsResponse = """ } """; +const String jsonBuildStatusTrueResponse = """ + { + "AnticipatedBuildStatus": "Succeeded" + } +"""; + +const String jsonBuildStatusFalseResponse = """ + { + "AnticipatedBuildStatus": "Failed" + } +"""; + void main() { - group('AppEngine CocoonService', () { + group('AppEngine CocoonService fetchCommitStatus ', () { AppEngineCocoonService service; setUp(() async { @@ -123,4 +135,48 @@ void main() { expect(service.fetchCommitStatuses(), throwsException); }); }); + + group('AppEngine CocoonService fetchTreeBuildStatus ', () { + AppEngineCocoonService service; + + setUp(() async { + service = AppEngineCocoonService(client: MockClient((request) async { + return Response(jsonBuildStatusTrueResponse, 200); + })); + }); + + test('should return bool', () { + expect(service.fetchTreeBuildStatus(), TypeMatcher>()); + }); + + test('should return true when given Succeeded', () async { + bool treeBuildStatus = await service.fetchTreeBuildStatus(); + + expect(treeBuildStatus, true); + }); + + test('should return false when given Failed', () async { + service = AppEngineCocoonService(client: MockClient((request) async { + return Response(jsonBuildStatusFalseResponse, 200); + })); + + bool treeBuildStatus = await service.fetchTreeBuildStatus(); + + expect(treeBuildStatus, false); + }); + + test('should throw exception if given non-200 response', () { + service = AppEngineCocoonService( + client: MockClient((request) async => Response('', 404))); + + expect(service.fetchTreeBuildStatus(), throwsException); + }); + + test('should throw exception if given bad response', () { + service = AppEngineCocoonService( + client: MockClient((request) async => Response('bad', 200))); + + expect(service.fetchTreeBuildStatus(), throwsException); + }); + }); } diff --git a/app_flutter/test/state/flutter_build_test.dart b/app_flutter/test/state/flutter_build_test.dart index 9599b43445..f7c13e3926 100644 --- a/app_flutter/test/state/flutter_build_test.dart +++ b/app_flutter/test/state/flutter_build_test.dart @@ -18,27 +18,31 @@ void main() { setUp(() { mockService = MockCocoonService(); buildState = FlutterBuildState(cocoonService: mockService); + + when(mockService.fetchCommitStatuses()) + .thenAnswer((_) => Future.value([])); + when(mockService.fetchTreeBuildStatus()) + .thenAnswer((_) => Future.value(true)); }); testWidgets('timer should periodically fetch updates', (WidgetTester tester) async { buildState.startFetchingBuildStateUpdates(); - verifyZeroInteractions(mockService); + + // startFetching immediately starts fetching results + verify(mockService.fetchCommitStatuses()).called(1); // Periodic timers don't necessarily run at the same time in each interval. // We double the refreshRate to gurantee a call would have been made. await tester.pump(buildState.refreshRate * 2); - verify(mockService.fetchCommitStatuses()).called(greaterThan(0)); - - await tester.pump(buildState.refreshRate * 2); - // Now we check to see if another call had been made verify(mockService.fetchCommitStatuses()).called(greaterThan(1)); // Tear down fails to cancel the timer before the test is over buildState.dispose(); }); - test('multiple start updates should not change the timer', () { + testWidgets('multiple start updates should not change the timer', + (WidgetTester tester) async { buildState.startFetchingBuildStateUpdates(); Timer refreshTimer = buildState.refreshTimer; @@ -47,6 +51,10 @@ void main() { expect(refreshTimer, equals(buildState.refreshTimer)); + // Since startFetching sends out requests on start, we need to wait + // for them to finish before disposing of the state. + await tester.pumpAndSettle(); + // Tear down fails to cancel the timer before the test is over buildState.dispose(); });