Skip to content

Commit

Permalink
feat(test): enhance logs and final test report in case of failing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Abdelaziz GACEMI committed Feb 17, 2023
1 parent ff31511 commit 1551929
Show file tree
Hide file tree
Showing 7 changed files with 1,241 additions and 1,090 deletions.
2 changes: 1 addition & 1 deletion bricks/test_optimizer/__brick__/test/.test_optimizer.dart

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';
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 @@ -207,14 +209,14 @@ 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(() {
if (optimizePerformance) {
File(p.join(cwd, 'test', '.test_optimizer.dart')).delete().ignore();
File(p.join(cwd, 'test', _testOptimizerFileName)).delete().ignore();
}
});
},
Expand Down Expand Up @@ -321,14 +323,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 @@ -375,41 +378,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 @@ -430,9 +444,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 @@ -456,6 +476,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 @@ -508,22 +551,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';
}
2 changes: 1 addition & 1 deletion lib/src/commands/test/templates/test_optimizer_bundle.dart
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":
"Ly8gR0VORVJBVEVEIENPREUgLSBETyBOT1QgTU9ESUZZIEJZIEhBTkQKLy8gQ29uc2lkZXIgYWRkaW5nIHRoaXMgZmlsZSB0byB5b3VyIC5naXRpZ25vcmUuCgp7eyNpc0ZsdXR0ZXJ9fWltcG9ydCAnZGFydDppbyc7CgppbXBvcnQgJ3BhY2thZ2U6Zmx1dHRlcl90ZXN0L2ZsdXR0ZXJfdGVzdC5kYXJ0JzsKe3svaXNGbHV0dGVyfX17e15pc0ZsdXR0ZXJ9fWltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7e3svaXNGbHV0dGVyfX0KCnt7I3Rlc3RzfX1pbXBvcnQgJ3t7ey59fX0nIGFzIHt7I3NuYWtlQ2FzZX19e3t7Ln19fXt7L3NuYWtlQ2FzZX19Owp7ey90ZXN0c319CnZvaWQgbWFpbigpIHsKe3sjaXNGbHV0dGVyfX0gIGdvbGRlbkZpbGVDb21wYXJhdG9yID0gX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yKCk7e3svaXNGbHV0dGVyfX0Ke3sjdGVzdHN9fSAgZ3JvdXAoJ3t7I3NuYWtlQ2FzZX19e3t7Ln19fXt7L3NuYWtlQ2FzZX19JywgKCkgeyB7eyNzbmFrZUNhc2V9fXt7ey59fX17ey9zbmFrZUNhc2V9fS5tYWluKCk7IH0pOwp7ey90ZXN0c319fQoKe3sjaXNGbHV0dGVyfX0KY2xhc3MgX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yIGV4dGVuZHMgTG9jYWxGaWxlQ29tcGFyYXRvciB7CiAgZmluYWwgTGlzdDxTdHJpbmc+IGdvbGRlbkZpbGVQYXRoczsKCiAgX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yKCkKICAgICAgOiBnb2xkZW5GaWxlUGF0aHMgPSBfZ29sZGVuRmlsZVBhdGhzLAogICAgICAgIHN1cGVyKF90ZXN0RmlsZSk7CgogIHN0YXRpYyBVcmkgZ2V0IF90ZXN0RmlsZSB7CiAgICBmaW5hbCBiYXNlZGlyID0KICAgICAgICAoZ29sZGVuRmlsZUNvbXBhcmF0b3IgYXMgTG9jYWxGaWxlQ29tcGFyYXRvcikuYmFzZWRpci50b1N0cmluZygpOwogICAgcmV0dXJuIFVyaS5wYXJzZSgiJGJhc2VkaXIvLnRlc3Rfb3B0aW1pemVyLmRhcnQiKTsKICB9CgogIHN0YXRpYyBMaXN0PFN0cmluZz4gZ2V0IF9nb2xkZW5GaWxlUGF0aHMgPT4KICAgICAgRGlyZWN0b3J5LmZyb21VcmkoKGdvbGRlbkZpbGVDb21wYXJhdG9yIGFzIExvY2FsRmlsZUNvbXBhcmF0b3IpLmJhc2VkaXIpCiAgICAgICAgICAubGlzdFN5bmMocmVjdXJzaXZlOiB0cnVlLCBmb2xsb3dMaW5rczogdHJ1ZSkKICAgICAgICAgIC53aGVyZVR5cGU8RmlsZT4oKQogICAgICAgICAgLm1hcCgoZmlsZSkgPT4gZmlsZS5wYXRoKQogICAgICAgICAgLndoZXJlKChwYXRoKSA9PiBwYXRoLmVuZHNXaXRoKCcucG5nJykpCiAgICAgICAgICAudG9MaXN0KCk7CgogIEBvdmVycmlkZQogIFVyaSBnZXRUZXN0VXJpKFVyaSBrZXksIGludD8gdmVyc2lvbikgewogICAgZmluYWwga2V5U3RyaW5nID0ga2V5LnBhdGg7CiAgICByZXR1cm4gVXJpLnBhcnNlKGdvbGRlbkZpbGVQYXRocwogICAgICAgIC5zaW5nbGVXaGVyZSgoZ29sZGVuRmlsZVBhdGgpID0+IGdvbGRlbkZpbGVQYXRoLmVuZHNXaXRoKGtleVN0cmluZykpKTsKICB9Cn0Ke3svaXNGbHV0dGVyfX0=",
"Ly8gR0VORVJBVEVEIENPREUgLSBETyBOT1QgTU9ESUZZIEJZIEhBTkQKLy8gQ29uc2lkZXIgYWRkaW5nIHRoaXMgZmlsZSB0byB5b3VyIC5naXRpZ25vcmUuCgp7eyNpc0ZsdXR0ZXJ9fWltcG9ydCAnZGFydDppbyc7CgppbXBvcnQgJ3BhY2thZ2U6Zmx1dHRlcl90ZXN0L2ZsdXR0ZXJfdGVzdC5kYXJ0JzsKe3svaXNGbHV0dGVyfX17e15pc0ZsdXR0ZXJ9fWltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7e3svaXNGbHV0dGVyfX0KCnt7I3Rlc3RzfX1pbXBvcnQgJ3t7ey59fX0nIGFzIHt7I3NuYWtlQ2FzZX19e3t7Ln19fXt7L3NuYWtlQ2FzZX19Owp7ey90ZXN0c319CnZvaWQgbWFpbigpIHsKe3sjaXNGbHV0dGVyfX0gIGdvbGRlbkZpbGVDb21wYXJhdG9yID0gX1Rlc3RPcHRpbWl6YXRpb25Bd2FyZUdvbGRlbkZpbGVDb21wYXJhdG9yKCk7e3svaXNGbHV0dGVyfX0Ke3sjdGVzdHN9fSAgZ3JvdXAoJ3t7ey59fX0nLCAoKSB7IHt7I3NuYWtlQ2FzZX19e3t7Ln19fXt7L3NuYWtlQ2FzZX19Lm1haW4oKTsgfSk7Cnt7L3Rlc3RzfX19Cgp7eyNpc0ZsdXR0ZXJ9fQpjbGFzcyBfVGVzdE9wdGltaXphdGlvbkF3YXJlR29sZGVuRmlsZUNvbXBhcmF0b3IgZXh0ZW5kcyBMb2NhbEZpbGVDb21wYXJhdG9yIHsKICBmaW5hbCBMaXN0PFN0cmluZz4gZ29sZGVuRmlsZVBhdGhzOwoKICBfVGVzdE9wdGltaXphdGlvbkF3YXJlR29sZGVuRmlsZUNvbXBhcmF0b3IoKQogICAgICA6IGdvbGRlbkZpbGVQYXRocyA9IF9nb2xkZW5GaWxlUGF0aHMsCiAgICAgICAgc3VwZXIoX3Rlc3RGaWxlKTsKCiAgc3RhdGljIFVyaSBnZXQgX3Rlc3RGaWxlIHsKICAgIGZpbmFsIGJhc2VkaXIgPQogICAgICAgIChnb2xkZW5GaWxlQ29tcGFyYXRvciBhcyBMb2NhbEZpbGVDb21wYXJhdG9yKS5iYXNlZGlyLnRvU3RyaW5nKCk7CiAgICByZXR1cm4gVXJpLnBhcnNlKCIkYmFzZWRpci8udGVzdF9vcHRpbWl6ZXIuZGFydCIpOwogIH0KCiAgc3RhdGljIExpc3Q8U3RyaW5nPiBnZXQgX2dvbGRlbkZpbGVQYXRocyA9PgogICAgICBEaXJlY3RvcnkuZnJvbVVyaSgoZ29sZGVuRmlsZUNvbXBhcmF0b3IgYXMgTG9jYWxGaWxlQ29tcGFyYXRvcikuYmFzZWRpcikKICAgICAgICAgIC5saXN0U3luYyhyZWN1cnNpdmU6IHRydWUsIGZvbGxvd0xpbmtzOiB0cnVlKQogICAgICAgICAgLndoZXJlVHlwZTxGaWxlPigpCiAgICAgICAgICAubWFwKChmaWxlKSA9PiBmaWxlLnBhdGgpCiAgICAgICAgICAud2hlcmUoKHBhdGgpID0+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
collection: ^1.17.1
glob: ^2.0.2
lcov_parser: ^0.1.2
mason: ">=0.1.0-dev.41 <0.1.0-dev.42"
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

0 comments on commit 1551929

Please sign in to comment.