From 42d5d776038d35237e2ed47e88538a88da114d80 Mon Sep 17 00:00:00 2001 From: Varun Valada Date: Tue, 9 Jul 2024 14:01:55 -0500 Subject: [PATCH] Add frontend changes to display event log --- .../controllers/test_executions/models.py | 2 +- .../test_executions/status_update.py | 4 +- .../test_executions/test_status_update.py | 15 ++-- frontend/lib/models/test_event.dart | 19 +++++ frontend/lib/models/test_execution.dart | 9 ++- frontend/lib/providers/test_events.dart | 15 ++++ frontend/lib/repositories/api_repository.dart | 12 +++ .../test_event_log_expandable.dart | 77 +++++++++++++++++++ .../test_execution_expandable.dart | 21 ++++- .../test_result_filter_expandable.dart | 12 +-- frontend/pubspec.lock | 32 ++++---- 11 files changed, 185 insertions(+), 33 deletions(-) create mode 100644 frontend/lib/models/test_event.dart create mode 100644 frontend/lib/providers/test_events.dart create mode 100644 frontend/lib/ui/artefact_page/test_event_log_expandable.dart diff --git a/backend/test_observer/controllers/test_executions/models.py b/backend/test_observer/controllers/test_executions/models.py index f1cd47ed..ce649d1f 100644 --- a/backend/test_observer/controllers/test_executions/models.py +++ b/backend/test_observer/controllers/test_executions/models.py @@ -155,7 +155,7 @@ class TestResultDTO(BaseModel): "the last one is the oldest one." ), ) - + class RerunRequest(BaseModel): test_execution_ids: set[int] diff --git a/backend/test_observer/controllers/test_executions/status_update.py b/backend/test_observer/controllers/test_executions/status_update.py index 9b8da983..5fad8e45 100644 --- a/backend/test_observer/controllers/test_executions/status_update.py +++ b/backend/test_observer/controllers/test_executions/status_update.py @@ -32,7 +32,9 @@ @router.put("/{id}/status_update") -def put_status_update(id: int, request: StatusUpdateRequest, db: Session = Depends(get_db)): +def put_status_update( + id: int, request: StatusUpdateRequest, db: Session = Depends(get_db) +): test_execution = db.get( TestExecution, id, diff --git a/backend/tests/controllers/test_executions/test_status_update.py b/backend/tests/controllers/test_executions/test_status_update.py index e7121965..7cd714c9 100644 --- a/backend/tests/controllers/test_executions/test_status_update.py +++ b/backend/tests/controllers/test_executions/test_status_update.py @@ -49,7 +49,7 @@ def test_status_updates_stored(test_client: TestClient, generator: DataGenerator "event_name": "job_end", "timestamp": "2015-03-21T11:08:15.859831", "detail": "my_detail_three", - } + }, ], }, ) @@ -66,6 +66,7 @@ def test_status_updates_stored(test_client: TestClient, generator: DataGenerator assert test_execution.test_events[1].detail == "my_detail_two" assert test_execution.status == "ENDED" + def test_status_updates_is_idempotent( test_client: TestClient, generator: DataGenerator ): @@ -98,6 +99,7 @@ def test_status_updates_is_idempotent( ) assert len(test_execution.test_events) == 2 + def test_get_status_update(test_client: TestClient, generator: DataGenerator): artefact = generator.gen_artefact("beta") artefact_build = generator.gen_artefact_build(artefact) @@ -106,7 +108,7 @@ def test_get_status_update(test_client: TestClient, generator: DataGenerator): artefact_build, environment, ci_link="http://localhost" ) - response = test_client.put( + test_client.put( f"/v1/test-executions/{test_execution.id}/status_update", json={ "agent_id": "test_agent", @@ -126,12 +128,14 @@ def test_get_status_update(test_client: TestClient, generator: DataGenerator): "event_name": "job_end", "timestamp": "2015-03-21T11:08:15.859831", "detail": "my_detail_three", - } + }, ], }, ) - get_response = test_client.get(f"/v1/test-executions/{test_execution.id}/status_update") - + get_response = test_client.get( + f"/v1/test-executions/{test_execution.id}/status_update" + ) + assert get_response.status_code == 200 json = get_response.json() assert json[0]["event_name"] == "started_setup" @@ -141,6 +145,7 @@ def test_get_status_update(test_client: TestClient, generator: DataGenerator): assert json[1]["timestamp"] == "2015-03-21T11:08:15.859831" assert json[1]["detail"] == "my_detail_two" + def test_status_updates_invalid_timestamp( test_client: TestClient, generator: DataGenerator ): diff --git a/frontend/lib/models/test_event.dart b/frontend/lib/models/test_event.dart new file mode 100644 index 00000000..8ccfc189 --- /dev/null +++ b/frontend/lib/models/test_event.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:yaru/yaru.dart'; +import 'package:yaru_icons/yaru_icons.dart'; + +part 'test_event.freezed.dart'; +part 'test_event.g.dart'; + +@freezed +class TestEvent with _$TestEvent { + const factory TestEvent({ + @JsonKey(name: 'event_name') required String eventName, + required String timestamp, + required String detail, + }) = _TestEvent; + + factory TestEvent.fromJson(Map json) => + _$TestEventFromJson(json); +} diff --git a/frontend/lib/models/test_execution.dart b/frontend/lib/models/test_execution.dart index e3376ea0..bc05c0bc 100644 --- a/frontend/lib/models/test_execution.dart +++ b/frontend/lib/models/test_execution.dart @@ -50,13 +50,16 @@ enum TestExecutionStatus { @JsonValue('IN_PROGRESS') inProgress, @JsonValue('PASSED') - passed; + passed, + @JsonValue('ENDED') + ended; bool get isCompleted { switch (this) { case notStarted: case inProgress: case notTested: + case ended: return false; case passed: case failed: @@ -76,6 +79,8 @@ enum TestExecutionStatus { return 'Failed'; case notTested: return 'Not Tested'; + case ended: + return 'Ended'; } } @@ -92,6 +97,8 @@ enum TestExecutionStatus { return const Icon(YaruIcons.error, color: YaruColors.red, size: size); case notTested: return const Icon(YaruIcons.information, size: size); + case ended: + return const Icon(YaruIcons.error, color: YaruColors.red, size: size); } } } diff --git a/frontend/lib/providers/test_events.dart b/frontend/lib/providers/test_events.dart new file mode 100644 index 00000000..3cd0a43e --- /dev/null +++ b/frontend/lib/providers/test_events.dart @@ -0,0 +1,15 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../models/test_event.dart'; +import 'api.dart'; + +part 'test_events.g.dart'; + +@riverpod +Future> testEvents( + TestEventsRef ref, + int testExecutionId, +) async { + final api = ref.watch(apiProvider); + return await api.getTestExecutionEvents(testExecutionId); +} diff --git a/frontend/lib/repositories/api_repository.dart b/frontend/lib/repositories/api_repository.dart index 54946ae7..424750b1 100644 --- a/frontend/lib/repositories/api_repository.dart +++ b/frontend/lib/repositories/api_repository.dart @@ -7,6 +7,7 @@ import '../models/family_name.dart'; import '../models/rerun_request.dart'; import '../models/test_execution.dart'; import '../models/test_result.dart'; +import '../models/test_event.dart'; class ApiRepository { final Dio dio; @@ -66,6 +67,17 @@ class ApiRepository { return testResults; } + Future> getTestExecutionEvents(int testExecutionId) async { + final response = + await dio.get('/v1/test-executions/$testExecutionId/status_update'); + final List testEventsJson = response.data; + final testEvents = + testEventsJson.map((json) => TestEvent.fromJson(json)).toList(); + print(testEvents); + return testEvents; + } + + Future> rerunTestExecutions( Set testExecutionIds, ) async { diff --git a/frontend/lib/ui/artefact_page/test_event_log_expandable.dart b/frontend/lib/ui/artefact_page/test_event_log_expandable.dart new file mode 100644 index 00000000..646ed48e --- /dev/null +++ b/frontend/lib/ui/artefact_page/test_event_log_expandable.dart @@ -0,0 +1,77 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:yaru/yaru.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +import '../../models/test_event.dart'; +import '../../providers/test_events.dart'; +import '../spacing.dart'; + +class TestEventLogExpandable extends ConsumerWidget { + const TestEventLogExpandable({ + super.key, + required this.testExecutionId, + required this.initiallyExpanded, + }); + + final int testExecutionId; + final bool initiallyExpanded; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final testEvents = ref.watch(testEventsProvider(testExecutionId)); + + return testEvents.when( + loading: () => const Center(child: YaruCircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text('Error: $error')), + data: (testEvents) { + return ExpansionTile( + controlAffinity: ListTileControlAffinity.leading, + childrenPadding: const EdgeInsets.only(left: Spacing.level4), + shape: const Border(), + title: Text('Event Log'), + initiallyExpanded: this.initiallyExpanded, + children: [ + DataTable( + columns: const [ + DataColumn( + label: Expanded( + child: Text( + 'Event Name', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Timestamp', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Detail', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + ], + rows: testEvents.map((testEvent) => DataRow( + cells: [ + DataCell(Text(testEvent.eventName)), + DataCell(Text(testEvent.timestamp)), + DataCell(Text(testEvent.detail)), + ], + )) + .toList(), + ), + ], + ); + } + ); + } +} diff --git a/frontend/lib/ui/artefact_page/test_execution_expandable.dart b/frontend/lib/ui/artefact_page/test_execution_expandable.dart index 89082610..23816b95 100644 --- a/frontend/lib/ui/artefact_page/test_execution_expandable.dart +++ b/frontend/lib/ui/artefact_page/test_execution_expandable.dart @@ -10,6 +10,7 @@ import '../inline_url_text.dart'; import '../spacing.dart'; import 'test_execution_review.dart'; import 'test_result_filter_expandable.dart'; +import 'test_event_log_expandable.dart'; class TestExecutionExpandable extends ConsumerWidget { const TestExecutionExpandable({super.key, required this.testExecution}); @@ -19,10 +20,14 @@ class TestExecutionExpandable extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { if (!testExecution.status.isCompleted) { - return ListTile( - onTap: () {}, + return ExpansionTile( + controlAffinity: ListTileControlAffinity.leading, + childrenPadding: const EdgeInsets.only(left: Spacing.level4), shape: const Border(), title: _TestExecutionTileTitle(testExecution: testExecution), + children: [ + TestEventLogExpandable(testExecutionId: testExecution.id, initiallyExpanded: true) + ] ); } @@ -31,7 +36,15 @@ class TestExecutionExpandable extends ConsumerWidget { childrenPadding: const EdgeInsets.only(left: Spacing.level4), shape: const Border(), title: _TestExecutionTileTitle(testExecution: testExecution), - children: TestResultStatus.values + children: [ + TestEventLogExpandable(testExecutionId: testExecution.id, initiallyExpanded: false), + ExpansionTile( + controlAffinity: ListTileControlAffinity.leading, + childrenPadding: const EdgeInsets.only(left: Spacing.level4), + shape: const Border(), + title: Text('Test Results'), + initiallyExpanded: true, + children: TestResultStatus.values .map( (status) => TestResultsFilterExpandable( statusToFilterBy: status, @@ -39,6 +52,8 @@ class TestExecutionExpandable extends ConsumerWidget { ), ) .toList(), + ), + ] ); } } diff --git a/frontend/lib/ui/artefact_page/test_result_filter_expandable.dart b/frontend/lib/ui/artefact_page/test_result_filter_expandable.dart index 9c15882e..52a0b024 100644 --- a/frontend/lib/ui/artefact_page/test_result_filter_expandable.dart +++ b/frontend/lib/ui/artefact_page/test_result_filter_expandable.dart @@ -38,8 +38,8 @@ class TestResultsFilterExpandable extends ConsumerWidget { error: (error, stackTrace) => Center(child: Text('Error: $error')), data: (testResults) { final filteredTestResults = testResults - .filter((testResult) => testResult.status == statusToFilterBy) - .toList(); + .filter((testResult) => testResult.status == statusToFilterBy) + .toList(); return ExpansionTile( controlAffinity: ListTileControlAffinity.leading, @@ -50,10 +50,10 @@ class TestResultsFilterExpandable extends ConsumerWidget { style: headerStyle, ), children: filteredTestResults - .map( - (testResult) => TestResultExpandable(testResult: testResult), - ) - .toList(), + .map( + (testResult) => TestResultExpandable(testResult: testResult), + ) + .toList(), ); }, ); diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 372da18e..df3e7f64 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -459,26 +459,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -515,10 +515,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -816,26 +816,26 @@ packages: dependency: "direct dev" description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" time: dependency: transitive description: @@ -944,10 +944,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: