Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(test): enhance logs and final test report in case of failing tests #563

Merged
merged 2 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions lib/src/cli/cli.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import 'dart:async';

import 'package:collection/collection.dart';
renancaraujo marked this conversation as resolved.
Show resolved Hide resolved
import 'package:glob/glob.dart';
import 'package:lcov_parser/lcov_parser.dart';
import 'package:mason/mason.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:universal_io/io.dart';
import 'package:very_good_cli/src/commands/test/templates/test_optimizer_bundle.dart';
import 'package:very_good_test_runner/very_good_test_runner.dart';
Expand Down
100 changes: 62 additions & 38 deletions lib/src/cli/flutter_cli.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
part of 'cli.dart';

const _testOptimizerFileName = '.test_optimizer.dart';

/// Thrown when `flutter packages get` or `flutter pub get`
/// is executed without a `pubspec.yaml`.
class PubspecNotFound implements Exception {}
Expand Down Expand Up @@ -212,13 +214,13 @@ class Flutter {
'--test-randomize-ordering-seed',
randomSeed
],
if (optimizePerformance) p.join('test', '.test_optimizer.dart')
if (optimizePerformance) p.join('test', _testOptimizerFileName)
],
stdout: stdout ?? noop,
stderr: stderr ?? noop,
).whenComplete(() async {
if (optimizePerformance) {
File(p.join(cwd, 'test', '.test_optimizer.dart'))
File(p.join(cwd, 'test', _testOptimizerFileName))
.delete()
.ignore();
}
Expand Down Expand Up @@ -331,14 +333,15 @@ Future<int> _flutterTest({
final suites = <int, TestSuite>{};
final groups = <int, TestGroup>{};
final tests = <int, Test>{};
final failedTestErrorMessages = <int, String>{};
final failedTestErrorMessages = <String, List<String>>{};

var successCount = 0;
var skipCount = 0;

String computeStats() {
final passingTests = successCount.formatSuccess();
final failingTests = failedTestErrorMessages.length.formatFailure();
final failingTests =
failedTestErrorMessages.values.expand((e) => e).length.formatFailure();
final skippedTests = skipCount.formatSkipped();
final result = [passingTests, failingTests, skippedTests]
..removeWhere((element) => element.isEmpty);
Expand Down Expand Up @@ -385,41 +388,52 @@ Future<int> _flutterTest({
if (event.stackTrace.trim().isNotEmpty) {
stderr('$clearLine${event.stackTrace}');
}

final traceLocation = _getTraceLocation(stackTrace: event.stackTrace);

// When failing to recover the location from the stack trace,
// save a short description of the error
final testErrorDescription = traceLocation ??
event.error.replaceAll('\n', ' ').truncated(_lineLength);

final test = tests[event.testID]!;
final suite = suites[test.suiteID]!;
final prefix = event.isFailure ? '[FAILED]' : '[ERROR]';
failedTestErrorMessages[event.testID] = '$prefix $testErrorDescription';

final optimizationApplied = _isOptimizationApplied(suite);
final topGroupName = _topGroupName(test, groups);
final testPath =
_actualTestPath(optimizationApplied, suite.path!, topGroupName);
final testName =
_actualTestName(optimizationApplied, test.name, topGroupName);
final relativeTestPath = p.relative(testPath, from: cwd);
failedTestErrorMessages[relativeTestPath] = [
...failedTestErrorMessages[relativeTestPath] ?? [],
'$prefix $testName'
];
}

if (event is TestDoneEvent) {
if (event.hidden) return;

final test = tests[event.testID]!;
final suite = suites[test.suiteID]!;
final optimizationApplied = _isOptimizationApplied(suite);
final firstGroupName = _topGroupName(test, groups);
final testPath =
_actualTestPath(optimizationApplied, suite.path!, firstGroupName);
final testName =
_actualTestName(optimizationApplied, test.name, firstGroupName);

if (event.skipped) {
stdout(
'''$clearLine${lightYellow.wrap('${test.name} ${suite.path} (SKIPPED)')}\n''',
'''$clearLine${lightYellow.wrap('$testName $testPath (SKIPPED)')}\n''',
);
skipCount++;
} else if (event.result == TestResult.success) {
successCount++;
} else {
stderr('$clearLine${test.name} ${suite.path} (FAILED)');
stderr('$clearLine$testName $testPath (FAILED)');
}

final timeElapsed = Duration(milliseconds: event.time).formatted();
final stats = computeStats();
final testName = test.name.toSingleLine().truncated(
final truncatedTestName = testName.toSingleLine().truncated(
_lineLength - (timeElapsed.length + stats.length + 2),
);
stdout('''$clearLine$timeElapsed $stats: $testName''');
stdout('''$clearLine$timeElapsed $stats: $truncatedTestName''');
}

if (event is DoneTestEvent) {
Expand All @@ -440,9 +454,15 @@ Future<int> _flutterTest({
final title = styleBold.wrap('Failing Tests:');

final lines = StringBuffer('$clearLine$title\n');
for (final errorMessage in failedTestErrorMessages.values) {
lines.writeln('$clearLine - $errorMessage');
for (final testSuiteErrorMessages
in failedTestErrorMessages.entries) {
lines.writeln('$clearLine - ${testSuiteErrorMessages.key} ');

for (final errorMessage in testSuiteErrorMessages.value) {
lines.writeln('$clearLine \t- $errorMessage');
}
}

stderr(lines.toString());
}
}
Expand All @@ -466,6 +486,29 @@ Future<int> _flutterTest({
return completer.future;
}

bool _isOptimizationApplied(TestSuite suite) =>
suite.path?.contains(_testOptimizerFileName) ?? false;

String? _topGroupName(Test test, Map<int, TestGroup> groups) => test.groupIDs
.map((groupID) => groups[groupID]?.name)
.firstWhereOrNull((groupName) => groupName?.isNotEmpty ?? false);

String _actualTestPath(
bool optimizationApplied,
String path,
String? groupName,
) =>
optimizationApplied
? path.replaceFirst(_testOptimizerFileName, groupName!)
: path;

String _actualTestName(
bool optimizationApplied,
String name,
String? topGroupName,
) =>
optimizationApplied ? name.replaceFirst(topGroupName!, '').trim() : name;

final int _lineLength = () {
try {
return stdout.terminalColumns;
Expand Down Expand Up @@ -518,22 +561,3 @@ extension on String {
String toSingleLine() =>
replaceAll('\n', '').replaceAll(RegExp(r'\s\s+'), ' ');
}

String? _getTraceLocation({
required String stackTrace,
}) {
final trace = Trace.parse(stackTrace);
if (trace.frames.isEmpty) {
return null;
}

final lastFrame = trace.frames.last;

final library = lastFrame.library;
final line = lastFrame.line;
final column = lastFrame.column;

if (line == null) return library;
if (column == null) return '$library:$line';
return '$library:$line:$column';
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final testOptimizerBundle = MasonBundle.fromJson(<String, dynamic>{
{
"path": "test/.test_optimizer.dart",
"data":
"Ly8gR0VORVJBVEVEIENPREUgLSBETyBOT1QgTU9ESUZZIEJZIEhBTkQKLy8gQ29uc2lkZXIgYWRkaW5nIHRoaXMgZmlsZSB0byB5b3VyIC5naXRpZ25vcmUuCgp7eyNpc0ZsdXR0ZXJ9fWltcG9ydCAnZGFydDppbyc7CgppbXBvcnQgJ3BhY2thZ2U6Zmx1dHRlcl90ZXN0L2ZsdXR0ZXJfdGVzdC5kYXJ0JzsKe3svaXNGbHV0dGVyfX17e15pc0ZsdXR0ZXJ9fWltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7e3svaXNGbHV0dGVyfX0KCnt7I3Rlc3RzfX1pbXBvcnQgJ3t7e3BhdGh9fX0nIGFzIHt7aWRlbnRpZmllcn19Owp7ey90ZXN0c319CnZvaWQgbWFpbigpIHsKe3sjaXNGbHV0dGVyfX0gIGdvbGRlbkZpbGVDb21wYXJhdG9yID0gX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yKCk7e3svaXNGbHV0dGVyfX0Ke3sjdGVzdHN9fSAgZ3JvdXAoJ3t7cGF0aC5zbmFrZUNhc2UoKX19JywgKCkgeyB7e2lkZW50aWZpZXJ9fS5tYWluKCk7IH0pOwp7ey90ZXN0c319fQoKe3sjaXNGbHV0dGVyfX0KY2xhc3MgX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yIGV4dGVuZHMgTG9jYWxGaWxlQ29tcGFyYXRvciB7CiAgZmluYWwgTGlzdDxTdHJpbmc+IGdvbGRlbkZpbGVQYXRoczsKCiAgX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yKCkKICAgICAgOiBnb2xkZW5GaWxlUGF0aHMgPSBfZ29sZGVuRmlsZVBhdGhzLAogICAgICAgIHN1cGVyKF90ZXN0RmlsZSk7CgogIHN0YXRpYyBVcmkgZ2V0IF90ZXN0RmlsZSB7CiAgICBmaW5hbCBiYXNlZGlyID0KICAgICAgICAoZ29sZGVuRmlsZUNvbXBhcmF0b3IgYXMgTG9jYWxGaWxlQ29tcGFyYXRvcikuYmFzZWRpci50b1N0cmluZygpOwogICAgcmV0dXJuIFVyaS5wYXJzZSgiJGJhc2VkaXIvLnRlc3Rfb3B0aW1pemVyLmRhcnQiKTsKICB9CgogIHN0YXRpYyBMaXN0PFN0cmluZz4gZ2V0IF9nb2xkZW5GaWxlUGF0aHMgPT4KICAgICAgRGlyZWN0b3J5LmZyb21VcmkoKGdvbGRlbkZpbGVDb21wYXJhdG9yIGFzIExvY2FsRmlsZUNvbXBhcmF0b3IpLmJhc2VkaXIpCiAgICAgICAgICAubGlzdFN5bmMocmVjdXJzaXZlOiB0cnVlLCBmb2xsb3dMaW5rczogdHJ1ZSkKICAgICAgICAgIC53aGVyZVR5cGU8RmlsZT4oKQogICAgICAgICAgLm1hcCgoZmlsZSkgPT4gZmlsZS5wYXRoKQogICAgICAgICAgLndoZXJlKChwYXRoKSA9PiBwYXRoLmVuZHNXaXRoKCcucG5nJykpCiAgICAgICAgICAudG9MaXN0KCk7CgogIEBvdmVycmlkZQogIFVyaSBnZXRUZXN0VXJpKFVyaSBrZXksIGludD8gdmVyc2lvbikgewogICAgZmluYWwga2V5U3RyaW5nID0ga2V5LnBhdGg7CiAgICByZXR1cm4gVXJpLnBhcnNlKGdvbGRlbkZpbGVQYXRocwogICAgICAgIC5zaW5nbGVXaGVyZSgoZ29sZGVuRmlsZVBhdGgpID0+IGdvbGRlbkZpbGVQYXRoLmVuZHNXaXRoKGtleVN0cmluZykpKTsKICB9Cn0Ke3svaXNGbHV0dGVyfX0=",
"Ly8gR0VORVJBVEVEIENPREUgLSBETyBOT1QgTU9ESUZZIEJZIEhBTkQKLy8gQ29uc2lkZXIgYWRkaW5nIHRoaXMgZmlsZSB0byB5b3VyIC5naXRpZ25vcmUuCgp7eyNpc0ZsdXR0ZXJ9fWltcG9ydCAnZGFydDppbyc7CgppbXBvcnQgJ3BhY2thZ2U6Zmx1dHRlcl90ZXN0L2ZsdXR0ZXJfdGVzdC5kYXJ0JzsKe3svaXNGbHV0dGVyfX17e15pc0ZsdXR0ZXJ9fWltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7e3svaXNGbHV0dGVyfX0KCnt7I3Rlc3RzfX1pbXBvcnQgJ3t7e3BhdGh9fX0nIGFzIHt7aWRlbnRpZmllcn19Owp7ey90ZXN0c319CnZvaWQgbWFpbigpIHsKe3sjaXNGbHV0dGVyfX0gIGdvbGRlbkZpbGVDb21wYXJhdG9yID0gX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yKCk7e3svaXNGbHV0dGVyfX0Ke3sjdGVzdHN9fSAgZ3JvdXAoJ3t7e3BhdGh9fX0nLCAoKSB7IHt7aWRlbnRpZmllcn19Lm1haW4oKTsgfSk7Cnt7L3Rlc3RzfX19Cgp7eyNpc0ZsdXR0ZXJ9fQpjbGFzcyBfVGVzdE9wdGltaXphdGlvbkF3YXJlR29sZGVuRmlsZUNvbXBhcmF0b3IgZXh0ZW5kcyBMb2NhbEZpbGVDb21wYXJhdG9yIHsKICBmaW5hbCBMaXN0PFN0cmluZz4gZ29sZGVuRmlsZVBhdGhzOwoKICBfVGVzdE9wdGltaXphdGlvbkF3YXJlR29sZGVuRmlsZUNvbXBhcmF0b3IoKQogICAgICA6IGdvbGRlbkZpbGVQYXRocyA9IF9nb2xkZW5GaWxlUGF0aHMsCiAgICAgICAgc3VwZXIoX3Rlc3RGaWxlKTsKCiAgc3RhdGljIFVyaSBnZXQgX3Rlc3RGaWxlIHsKICAgIGZpbmFsIGJhc2VkaXIgPQogICAgICAgIChnb2xkZW5GaWxlQ29tcGFyYXRvciBhcyBMb2NhbEZpbGVDb21wYXJhdG9yKS5iYXNlZGlyLnRvU3RyaW5nKCk7CiAgICByZXR1cm4gVXJpLnBhcnNlKCIkYmFzZWRpci8udGVzdF9vcHRpbWl6ZXIuZGFydCIpOwogIH0KCiAgc3RhdGljIExpc3Q8U3RyaW5nPiBnZXQgX2dvbGRlbkZpbGVQYXRocyA9PgogICAgICBEaXJlY3RvcnkuZnJvbVVyaSgoZ29sZGVuRmlsZUNvbXBhcmF0b3IgYXMgTG9jYWxGaWxlQ29tcGFyYXRvcikuYmFzZWRpcikKICAgICAgICAgIC5saXN0U3luYyhyZWN1cnNpdmU6IHRydWUsIGZvbGxvd0xpbmtzOiB0cnVlKQogICAgICAgICAgLndoZXJlVHlwZTxGaWxlPigpCiAgICAgICAgICAubWFwKChmaWxlKSA9PiBmaWxlLnBhdGgpCiAgICAgICAgICAud2hlcmUoKHBhdGgpID0+IHBhdGguZW5kc1dpdGgoJy5wbmcnKSkKICAgICAgICAgIC50b0xpc3QoKTsKCiAgQG92ZXJyaWRlCiAgVXJpIGdldFRlc3RVcmkoVXJpIGtleSwgaW50PyB2ZXJzaW9uKSB7CiAgICBmaW5hbCBrZXlTdHJpbmcgPSBrZXkucGF0aDsKICAgIHJldHVybiBVcmkucGFyc2UoZ29sZGVuRmlsZVBhdGhzCiAgICAgICAgLnNpbmdsZVdoZXJlKChnb2xkZW5GaWxlUGF0aCkgPT4gZ29sZGVuRmlsZVBhdGguZW5kc1dpdGgoa2V5U3RyaW5nKSkpOwogIH0KfQp7ey9pc0ZsdXR0ZXJ9fQ==",
"type": "text"
}
],
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ environment:
dependencies:
args: ^2.1.0
cli_completion: ">=0.2.0 <0.4.0"
collection: ^1.17.1
glob: ^2.0.2
lcov_parser: ^0.1.2
mason: ^0.1.0-dev.41
Expand All @@ -22,6 +23,7 @@ dependencies:
usage: ^4.0.2
very_good_test_runner: ^0.1.2


dev_dependencies:
build_runner: ^2.0.0
build_verify: ^3.0.0
Expand Down
Loading