| // Copyright 2014 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'; | |
| class TestSpecs { | |
| TestSpecs({required this.path, required this.startTime}); | |
| final String path; | |
| int startTime; | |
| int? _endTime; | |
| int get milliseconds => endTime - startTime; | |
| set endTime(int value) { | |
| _endTime = value; | |
| } | |
| int get endTime => _endTime ?? 0; | |
| String toJson() { | |
| return json.encode(<String, String>{'path': path, 'runtime': milliseconds.toString()}); | |
| } | |
| } | |
| /// The parsed result of a single test case as reported by the `dart test` | |
| /// JSON file reporter. | |
| class TestResult { | |
| TestResult({required this.name, required this.suiteID, required this.startTime}); | |
| /// The full name of the test (including any group prefixes). | |
| final String name; | |
| /// The id of the suite (test file) that this test belongs to. | |
| final int suiteID; | |
| /// The time (in milliseconds, relative to the start of the run) at which the | |
| /// test started. | |
| final int startTime; | |
| /// The time (in milliseconds, relative to the start of the run) at which the | |
| /// test finished, or null if it never finished. | |
| int? endTime; | |
| /// The raw result reported by `dart test`: one of `success`, `failure`, or | |
| /// `error`. | |
| String result = 'success'; | |
| /// Whether the test was skipped. | |
| bool skipped = false; | |
| /// Whether the test is a "hidden" bookkeeping test (for example, the | |
| /// synthetic "loading <suite>" test that `dart test` emits for each suite). | |
| /// | |
| /// Hidden tests are excluded from the reported results. | |
| bool hidden = false; | |
| /// The duration of the test, in seconds. | |
| double get seconds => ((endTime ?? startTime) - startTime) / 1000.0; | |
| /// Maps the `dart test` [result]/[skipped] to a `PASS`/`FAIL`/`SKIP` result | |
| /// type. | |
| String get actual => switch (this) { | |
| TestResult(skipped: true) => 'SKIP', | |
| TestResult(result: 'success') => 'PASS', | |
| _ => 'FAIL', | |
| }; | |
| /// The expected result type for this test. | |
| /// | |
| /// `dart test` has no concept of expected failures, so every non-skipped | |
| /// test is expected to pass. | |
| String get expected => skipped ? 'SKIP' : 'PASS'; | |
| } | |
| class TestFileReporterResults { | |
| TestFileReporterResults._({ | |
| required this.allTestSpecs, | |
| required this.testResults, | |
| required this.hasFailedTests, | |
| required this.errors, | |
| }); | |
| /// Intended to parse the output file of `dart test --file-reporter json:file_name | |
| factory TestFileReporterResults.fromFile(File metrics) { | |
| if (!metrics.existsSync()) { | |
| throw Exception('${metrics.path} does not exist'); | |
| } | |
| final testSpecs = <int, TestSpecs>{}; | |
| final testResults = <int, TestResult>{}; | |
| var hasFailedTests = true; | |
| final errors = <String>[]; | |
| for (final String metric in metrics.readAsLinesSync()) { | |
| /// Using print within a test adds the printed content to the json file report | |
| /// as \u0000 making the file parsing step fail. The content of the json file | |
| /// is expected to be a json dictionary per line and the following line removes | |
| /// all the additional content at the beginning of the line until it finds the | |
| /// first opening curly bracket. | |
| // TODO(godofredoc): remove when https://github.com/flutter/flutter/issues/145553 is fixed. | |
| final String sanitizedMetric = metric.replaceAll(RegExp(r'$.*{'), '{'); | |
| final entry = json.decode(sanitizedMetric) as Map<String, Object?>; | |
| switch (entry) { | |
| case {'suite': final Map<String, Object?> suite, 'time': final int time}: | |
| addTestSpec(suite, time, testSpecs); | |
| case {'type': 'group', 'group': {'suiteID': final int suiteID}, 'time': final int time} | |
| when testSpecs.containsKey(suiteID): | |
| addMetricDone(suiteID, time, testSpecs); | |
| case {'type': 'testStart', 'test': final Map<String, Object?> test, 'time': final int time}: | |
| addTestStart(test, time, testResults); | |
| case {'type': 'testDone'}: | |
| addTestDone(entry, testResults); | |
| case {'error': final Object? error}: | |
| final String stackTrace = entry['stackTrace'] as String? ?? ''; | |
| errors.add('$error\n $stackTrace'); | |
| case {'success': true}: | |
| hasFailedTests = false; | |
| } | |
| } | |
| return TestFileReporterResults._( | |
| allTestSpecs: testSpecs, | |
| testResults: testResults, | |
| hasFailedTests: hasFailedTests, | |
| errors: errors, | |
| ); | |
| } | |
| final Map<int, TestSpecs> allTestSpecs; | |
| final Map<int, TestResult> testResults; | |
| final bool hasFailedTests; | |
| final List<String> errors; | |
| static void addTestSpec(Map<String, Object?> suite, int time, Map<int, TestSpecs> allTestSpecs) { | |
| if (suite case {'id': final int id, 'path': final String path}) { | |
| allTestSpecs[id] = TestSpecs(path: path, startTime: time); | |
| } | |
| } | |
| static void addMetricDone(int suiteID, int time, Map<int, TestSpecs> allTestSpecs) { | |
| allTestSpecs[suiteID]?.endTime = time; | |
| } | |
| static bool isMetricDone(Map<String, Object?> entry, Map<int, TestSpecs> allTestSpecs) => | |
| switch (entry) { | |
| {'type': 'group', 'group': {'suiteID': final int suiteID}} => allTestSpecs.containsKey( | |
| suiteID, | |
| ), | |
| _ => false, | |
| }; | |
| static void addTestStart(Map<String, Object?> test, int time, Map<int, TestResult> testResults) { | |
| if (test case {'id': final int id, 'name': final String name, 'suiteID': final int suiteID}) { | |
| testResults[id] = TestResult(name: name, suiteID: suiteID, startTime: time); | |
| } | |
| } | |
| static void addTestDone(Map<String, Object?> entry, Map<int, TestResult> testResults) { | |
| if (entry case {'testID': final int testID, 'time': final int time}) { | |
| if (testResults[testID] case final testResult?) { | |
| testResult | |
| ..endTime = time | |
| ..result = entry['result'] as String? ?? testResult.result | |
| ..skipped = entry['skipped'] as bool? ?? false | |
| ..hidden = entry['hidden'] as bool? ?? false; | |
| } | |
| } | |
| } | |
| } |