blob: 9d00b4b8bd229fef049915e67096da66767344ec [file]
// 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;
}
}
}
}