diff --git a/bricks/test_optimizer/hooks/test/dart_identifier_generator_test.dart b/bricks/test_optimizer/hooks/test/dart_identifier_generator_test.dart index e85749e9c..888fd54bf 100644 --- a/bricks/test_optimizer/hooks/test/dart_identifier_generator_test.dart +++ b/bricks/test_optimizer/hooks/test/dart_identifier_generator_test.dart @@ -2,7 +2,7 @@ import 'package:hooks/dart_identifier_generator.dart'; import 'package:test/test.dart'; void main() { - group('$DartIdentifierGenerator', () { + group(DartIdentifierGenerator, () { test('can be instantiated', () { expect(DartIdentifierGenerator.new, returnsNormally); }); diff --git a/e2e/test/commands/test/compilation_error/compilation_error_test.dart b/e2e/test/commands/test/compilation_error/compilation_error_test.dart index ebb7402d8..bdd60c730 100644 --- a/e2e/test/commands/test/compilation_error/compilation_error_test.dart +++ b/e2e/test/commands/test/compilation_error/compilation_error_test.dart @@ -31,7 +31,7 @@ void main() { final result = await commandRunner.run(['test']); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); verify( () => logger.err(any(that: contains('- test/.test_optimizer.dart'))), ).called(1); diff --git a/e2e/test/commands/test/very_good_config/very_good_config_test.dart b/e2e/test/commands/test/very_good_config/very_good_config_test.dart index ca54a49b0..69c1832a3 100644 --- a/e2e/test/commands/test/very_good_config/very_good_config_test.dart +++ b/e2e/test/commands/test/very_good_config/very_good_config_test.dart @@ -37,7 +37,7 @@ void main() { await expectLater( commandRunner.run(['test', '--coverage']), - completion(equals(ExitCode.unavailable.code)), + completion(equals(ExitCode.software.code)), ); verify( () => logger.err(any(that: contains('Expected coverage >= 100.00%'))), diff --git a/lib/src/cli/templates/test_optimizer_bundle.dart b/lib/src/cli/templates/test_optimizer_bundle.dart index 22cc57024..63eb98a44 100644 --- a/lib/src/cli/templates/test_optimizer_bundle.dart +++ b/lib/src/cli/templates/test_optimizer_bundle.dart @@ -35,7 +35,7 @@ final testOptimizerBundle = MasonBundle.fromJson({ }, { "path": "test/dart_identifier_generator_test.dart", - "data": "aW1wb3J0ICdwYWNrYWdlOmhvb2tzL2RhcnRfaWRlbnRpZmllcl9nZW5lcmF0b3IuZGFydCc7CmltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7Cgp2b2lkIG1haW4oKSB7CiAgZ3JvdXAoJyREYXJ0SWRlbnRpZmllckdlbmVyYXRvcicsICgpIHsKICAgIHRlc3QoJ2NhbiBiZSBpbnN0YW50aWF0ZWQnLCAoKSB7CiAgICAgIGV4cGVjdChEYXJ0SWRlbnRpZmllckdlbmVyYXRvci5uZXcsIHJldHVybnNOb3JtYWxseSk7CiAgICB9KTsKCiAgICBncm91cCgnbmV4dCcsICgpIHsKICAgICAgdGVzdCgncmV0dXJucyBub3JtYWxseScsICgpIHsKICAgICAgICBmaW5hbCBnZW5lcmF0b3IgPSBEYXJ0SWRlbnRpZmllckdlbmVyYXRvcigpOwogICAgICAgIGV4cGVjdChnZW5lcmF0b3IubmV4dCwgcmV0dXJuc05vcm1hbGx5KTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdnZW5lcmF0ZXMgdW5pcXVlIHN0cmluZ3MnLCAoKSB7CiAgICAgICAgZmluYWwgZ2VuZXJhdG9yID0gRGFydElkZW50aWZpZXJHZW5lcmF0b3IoKTsKICAgICAgICBmaW5hbCBpZHMgPSA8U3RyaW5nPnt9OwogICAgICAgIGNvbnN0IGNvdW50ID0gMTAwMDsKICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGNvdW50OyBpKyspIHsKICAgICAgICAgIGZpbmFsIGlkID0gZ2VuZXJhdG9yLm5leHQoKTsKICAgICAgICAgIGlkcy5hZGQoaWQpOwogICAgICAgIH0KICAgICAgICBleHBlY3QoaWRzLmxlbmd0aCwgY291bnQpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ2dlbmVyYXRlcyB2YWxpZCBkYXJ0IGlkZW50aWZpZXJzJywgKCkgewogICAgICAgIC8vIEZvciBhIGZ1bGwgc3BlY2lmaWNhdGlvbiBvZiB2YWxpZCBkYXJ0IGlkZW50aWZpZXJzLCByZWFkCiAgICAgICAgLy8gU2VjdGlvbiAxNy4zNyBmcm9tIHRoZSBbRGFydCBMYW5ndWFnZSBTcGVjaWZpY2F0aW9uXShodHRwczovL2RhcnQuZGV2L2d1aWRlcy9sYW5ndWFnZS9zcGVjaWZpY2F0aW9ucy9EYXJ0TGFuZ1NwZWMtdjIuMTAucGRmKS4KICAgICAgICBmaW5hbCBnZW5lcmF0b3IgPSBEYXJ0SWRlbnRpZmllckdlbmVyYXRvcigpOwogICAgICAgIGZpbmFsIGlkcyA9IDxTdHJpbmc+W107CiAgICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCAxMDAwOyBpKyspIHsKICAgICAgICAgIGZpbmFsIGlkID0gZ2VuZXJhdG9yLm5leHQoKTsKICAgICAgICAgIGlkcy5hZGQoaWQpOwogICAgICAgIH0KCiAgICAgICAgZXhwZWN0KGlkcy53aGVyZSgoaWQpID0+IF9kYXJ0UmVzZXJ2ZWRLZXl3b3Jkcy5jb250YWlucyhpZCkpLCBpc0VtcHR5KTsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBpZHMuZXZlcnkoKGlkKSB7CiAgICAgICAgICAgIGZpbmFsIGlkU3RhcnQgPSBpZC5jb2RlVW5pdEF0KDApOwogICAgICAgICAgICBmaW5hbCBpc0FscGhhYmV0aWMgPQogICAgICAgICAgICAgICAgKGlkU3RhcnQgPj0gNjUgJiYgaWRTdGFydCA8PSA5MCkgfHwKICAgICAgICAgICAgICAgIChpZFN0YXJ0ID49IDk3ICYmIGlkU3RhcnQgPD0gMTIyKTsKICAgICAgICAgICAgZmluYWwgaXNVbmRlcnNjb3JlID0gaWRTdGFydCA9PSA5NTsKICAgICAgICAgICAgZmluYWwgaXNEb2xsYXJTaWduID0gaWRTdGFydCA9PSAzNjsKICAgICAgICAgICAgcmV0dXJuIGlzQWxwaGFiZXRpYyB8fCBpc1VuZGVyc2NvcmUgfHwgaXNEb2xsYXJTaWduOwogICAgICAgICAgfSksCiAgICAgICAgICB0cnVlLAogICAgICAgICk7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgaWRzLmV2ZXJ5KChpZCkgewogICAgICAgICAgICBmaW5hbCBpZFBhcnQgPSBpZC5jb2RlVW5pdHMuc2tpcCgxKTsKICAgICAgICAgICAgcmV0dXJuIGlkUGFydC5ldmVyeSgoYXNjaWkpIHsKICAgICAgICAgICAgICBmaW5hbCBpc0FscGhhYmV0aWMgPQogICAgICAgICAgICAgICAgICAoYXNjaWkgPj0gNjUgJiYgYXNjaWkgPD0gOTApIHx8IChhc2NpaSA+PSA5NyAmJiBhc2NpaSA8PSAxMjIpOwogICAgICAgICAgICAgIGZpbmFsIGlzVW5kZXJzY29yZSA9IGFzY2lpID09IDk1OwogICAgICAgICAgICAgIGZpbmFsIGlzRG9sbGFyU2lnbiA9IGFzY2lpID09IDM2OwogICAgICAgICAgICAgIGZpbmFsIGlzRGlnaXQgPSBhc2NpaSA+PSA0OCAmJiBhc2NpaSA8PSA1NzsKICAgICAgICAgICAgICByZXR1cm4gaXNBbHBoYWJldGljIHx8IGlzVW5kZXJzY29yZSB8fCBpc0RvbGxhclNpZ24gfHwgaXNEaWdpdDsKICAgICAgICAgICAgfSk7CiAgICAgICAgICB9KSwKICAgICAgICAgIHRydWUsCiAgICAgICAgKTsKICAgICAgfSk7CiAgICB9KTsKICB9KTsKfQoKLy8gQWxsIHJlc2VydmVkIGtleXdvcmRzIGluIFtEYXJ0IDIuMTkuMl0oaHR0cHM6Ly9kYXJ0LmRldi9ndWlkZXMvbGFuZ3VhZ2UvbGFuZ3VhZ2UtdG91ciNrZXl3b3JkcykuCmNvbnN0IF9kYXJ0UmVzZXJ2ZWRLZXl3b3JkcyA9IFsKICAnYWJzdHJhY3QnLAogICdhcycsCiAgJ2Fzc2VydCcsCiAgJ2FzeW5jJywKICAnYXdhaXQnLAogICdicmVhaycsCiAgJ2Nhc2UnLAogICdjYXRjaCcsCiAgJ2NsYXNzJywKICAnY29uc3QnLAogICdjb250aW51ZScsCiAgJ2NvdmFyaWFudCcsCiAgJ2RlZmF1bHQnLAogICdkZWZlcnJlZCcsCiAgJ2RvJywKICAnZHluYW1pYycsCiAgJ2Vsc2UnLAogICdlbnVtJywKICAnZXhwb3J0JywKICAnZXh0ZW5kcycsCiAgJ2V4dGVuc2lvbicsCiAgJ2V4dGVybmFsJywKICAnZmFjdG9yeScsCiAgJ2ZhbHNlJywKICAnZmluYWwnLAogICdmaW5hbGx5JywKICAnZm9yJywKICAnRnVuY3Rpb24nLAogICdnZXQnLAogICdoaWRlJywKICAnaWYnLAogICdpbXBsZW1lbnRzJywKICAnaW1wb3J0JywKICAnaW4nLAogICdpbnRlcmZhY2UnLAogICdpcycsCiAgJ2xhdGUnLAogICdsaWJyYXJ5JywKICAnbWl4aW4nLAogICduZXcnLAogICdudWxsJywKICAnb24nLAogICdvcGVyYXRvcicsCiAgJ3BhcnQnLAogICdyZXF1aXJlZCcsCiAgJ3JldGhyb3cnLAogICdyZXR1cm4nLAogICdzZXQnLAogICdzaG93JywKICAnc3RhdGljJywKICAnc3VwZXInLAogICdzd2l0Y2gnLAogICdzeW5jJywKICAndGhpcycsCiAgJ3Rocm93JywKICAndHJ1ZScsCiAgJ3RyeScsCiAgJ3R5cGVkZWYnLAogICd2YXInLAogICd2b2lkJywKICAnd2hpbGUnLAogICd3aXRoJywKICAneWllbGQnLApdOwo=", + "data": "aW1wb3J0ICdwYWNrYWdlOmhvb2tzL2RhcnRfaWRlbnRpZmllcl9nZW5lcmF0b3IuZGFydCc7CmltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7Cgp2b2lkIG1haW4oKSB7CiAgZ3JvdXAoRGFydElkZW50aWZpZXJHZW5lcmF0b3IsICgpIHsKICAgIHRlc3QoJ2NhbiBiZSBpbnN0YW50aWF0ZWQnLCAoKSB7CiAgICAgIGV4cGVjdChEYXJ0SWRlbnRpZmllckdlbmVyYXRvci5uZXcsIHJldHVybnNOb3JtYWxseSk7CiAgICB9KTsKCiAgICBncm91cCgnbmV4dCcsICgpIHsKICAgICAgdGVzdCgncmV0dXJucyBub3JtYWxseScsICgpIHsKICAgICAgICBmaW5hbCBnZW5lcmF0b3IgPSBEYXJ0SWRlbnRpZmllckdlbmVyYXRvcigpOwogICAgICAgIGV4cGVjdChnZW5lcmF0b3IubmV4dCwgcmV0dXJuc05vcm1hbGx5KTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdnZW5lcmF0ZXMgdW5pcXVlIHN0cmluZ3MnLCAoKSB7CiAgICAgICAgZmluYWwgZ2VuZXJhdG9yID0gRGFydElkZW50aWZpZXJHZW5lcmF0b3IoKTsKICAgICAgICBmaW5hbCBpZHMgPSA8U3RyaW5nPnt9OwogICAgICAgIGNvbnN0IGNvdW50ID0gMTAwMDsKICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGNvdW50OyBpKyspIHsKICAgICAgICAgIGZpbmFsIGlkID0gZ2VuZXJhdG9yLm5leHQoKTsKICAgICAgICAgIGlkcy5hZGQoaWQpOwogICAgICAgIH0KICAgICAgICBleHBlY3QoaWRzLmxlbmd0aCwgY291bnQpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ2dlbmVyYXRlcyB2YWxpZCBkYXJ0IGlkZW50aWZpZXJzJywgKCkgewogICAgICAgIC8vIEZvciBhIGZ1bGwgc3BlY2lmaWNhdGlvbiBvZiB2YWxpZCBkYXJ0IGlkZW50aWZpZXJzLCByZWFkCiAgICAgICAgLy8gU2VjdGlvbiAxNy4zNyBmcm9tIHRoZSBbRGFydCBMYW5ndWFnZSBTcGVjaWZpY2F0aW9uXShodHRwczovL2RhcnQuZGV2L2d1aWRlcy9sYW5ndWFnZS9zcGVjaWZpY2F0aW9ucy9EYXJ0TGFuZ1NwZWMtdjIuMTAucGRmKS4KICAgICAgICBmaW5hbCBnZW5lcmF0b3IgPSBEYXJ0SWRlbnRpZmllckdlbmVyYXRvcigpOwogICAgICAgIGZpbmFsIGlkcyA9IDxTdHJpbmc+W107CiAgICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCAxMDAwOyBpKyspIHsKICAgICAgICAgIGZpbmFsIGlkID0gZ2VuZXJhdG9yLm5leHQoKTsKICAgICAgICAgIGlkcy5hZGQoaWQpOwogICAgICAgIH0KCiAgICAgICAgZXhwZWN0KGlkcy53aGVyZSgoaWQpID0+IF9kYXJ0UmVzZXJ2ZWRLZXl3b3Jkcy5jb250YWlucyhpZCkpLCBpc0VtcHR5KTsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBpZHMuZXZlcnkoKGlkKSB7CiAgICAgICAgICAgIGZpbmFsIGlkU3RhcnQgPSBpZC5jb2RlVW5pdEF0KDApOwogICAgICAgICAgICBmaW5hbCBpc0FscGhhYmV0aWMgPQogICAgICAgICAgICAgICAgKGlkU3RhcnQgPj0gNjUgJiYgaWRTdGFydCA8PSA5MCkgfHwKICAgICAgICAgICAgICAgIChpZFN0YXJ0ID49IDk3ICYmIGlkU3RhcnQgPD0gMTIyKTsKICAgICAgICAgICAgZmluYWwgaXNVbmRlcnNjb3JlID0gaWRTdGFydCA9PSA5NTsKICAgICAgICAgICAgZmluYWwgaXNEb2xsYXJTaWduID0gaWRTdGFydCA9PSAzNjsKICAgICAgICAgICAgcmV0dXJuIGlzQWxwaGFiZXRpYyB8fCBpc1VuZGVyc2NvcmUgfHwgaXNEb2xsYXJTaWduOwogICAgICAgICAgfSksCiAgICAgICAgICB0cnVlLAogICAgICAgICk7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgaWRzLmV2ZXJ5KChpZCkgewogICAgICAgICAgICBmaW5hbCBpZFBhcnQgPSBpZC5jb2RlVW5pdHMuc2tpcCgxKTsKICAgICAgICAgICAgcmV0dXJuIGlkUGFydC5ldmVyeSgoYXNjaWkpIHsKICAgICAgICAgICAgICBmaW5hbCBpc0FscGhhYmV0aWMgPQogICAgICAgICAgICAgICAgICAoYXNjaWkgPj0gNjUgJiYgYXNjaWkgPD0gOTApIHx8IChhc2NpaSA+PSA5NyAmJiBhc2NpaSA8PSAxMjIpOwogICAgICAgICAgICAgIGZpbmFsIGlzVW5kZXJzY29yZSA9IGFzY2lpID09IDk1OwogICAgICAgICAgICAgIGZpbmFsIGlzRG9sbGFyU2lnbiA9IGFzY2lpID09IDM2OwogICAgICAgICAgICAgIGZpbmFsIGlzRGlnaXQgPSBhc2NpaSA+PSA0OCAmJiBhc2NpaSA8PSA1NzsKICAgICAgICAgICAgICByZXR1cm4gaXNBbHBoYWJldGljIHx8IGlzVW5kZXJzY29yZSB8fCBpc0RvbGxhclNpZ24gfHwgaXNEaWdpdDsKICAgICAgICAgICAgfSk7CiAgICAgICAgICB9KSwKICAgICAgICAgIHRydWUsCiAgICAgICAgKTsKICAgICAgfSk7CiAgICB9KTsKICB9KTsKfQoKLy8gQWxsIHJlc2VydmVkIGtleXdvcmRzIGluIFtEYXJ0IDIuMTkuMl0oaHR0cHM6Ly9kYXJ0LmRldi9ndWlkZXMvbGFuZ3VhZ2UvbGFuZ3VhZ2UtdG91ciNrZXl3b3JkcykuCmNvbnN0IF9kYXJ0UmVzZXJ2ZWRLZXl3b3JkcyA9IFsKICAnYWJzdHJhY3QnLAogICdhcycsCiAgJ2Fzc2VydCcsCiAgJ2FzeW5jJywKICAnYXdhaXQnLAogICdicmVhaycsCiAgJ2Nhc2UnLAogICdjYXRjaCcsCiAgJ2NsYXNzJywKICAnY29uc3QnLAogICdjb250aW51ZScsCiAgJ2NvdmFyaWFudCcsCiAgJ2RlZmF1bHQnLAogICdkZWZlcnJlZCcsCiAgJ2RvJywKICAnZHluYW1pYycsCiAgJ2Vsc2UnLAogICdlbnVtJywKICAnZXhwb3J0JywKICAnZXh0ZW5kcycsCiAgJ2V4dGVuc2lvbicsCiAgJ2V4dGVybmFsJywKICAnZmFjdG9yeScsCiAgJ2ZhbHNlJywKICAnZmluYWwnLAogICdmaW5hbGx5JywKICAnZm9yJywKICAnRnVuY3Rpb24nLAogICdnZXQnLAogICdoaWRlJywKICAnaWYnLAogICdpbXBsZW1lbnRzJywKICAnaW1wb3J0JywKICAnaW4nLAogICdpbnRlcmZhY2UnLAogICdpcycsCiAgJ2xhdGUnLAogICdsaWJyYXJ5JywKICAnbWl4aW4nLAogICduZXcnLAogICdudWxsJywKICAnb24nLAogICdvcGVyYXRvcicsCiAgJ3BhcnQnLAogICdyZXF1aXJlZCcsCiAgJ3JldGhyb3cnLAogICdyZXR1cm4nLAogICdzZXQnLAogICdzaG93JywKICAnc3RhdGljJywKICAnc3VwZXInLAogICdzd2l0Y2gnLAogICdzeW5jJywKICAndGhpcycsCiAgJ3Rocm93JywKICAndHJ1ZScsCiAgJ3RyeScsCiAgJ3R5cGVkZWYnLAogICd2YXInLAogICd2b2lkJywKICAnd2hpbGUnLAogICd3aXRoJywKICAneWllbGQnLApdOwo=", "type": "text", }, { diff --git a/lib/src/cli/test_cli_runner.dart b/lib/src/cli/test_cli_runner.dart index e78aeb786..40f967fd8 100644 --- a/lib/src/cli/test_cli_runner.dart +++ b/lib/src/cli/test_cli_runner.dart @@ -301,12 +301,11 @@ class TestCLIRunner { ? body.call() : overrideAnsiOutput(enableAnsiOutput, body); - /// Handles the [MinCoverageNotMet] exception by logging the error message. - /// - /// If [e] contains uncovered lines, they are logged after the error message. - static void handleMinCoverageNotMet({ + /// Logs [error], along with its uncovered lines when it carries any, and + /// returns the exit code an unmet coverage threshold reports. + static int handleMinCoverageNotMet( + MinCoverageNotMet error, { required Logger logger, - required MinCoverageNotMet e, double? minCoverage, }) { var decimalPlaces = 2; @@ -316,22 +315,24 @@ class TestCLIRunner { return (x * b).roundToDouble() / b; } - if (e.coverage < minCoverage!) { - var rounded = round(e.coverage); + if (error.coverage < minCoverage!) { + var rounded = round(error.coverage); while (rounded == minCoverage) { decimalPlaces++; - rounded = round(e.coverage); + rounded = round(error.coverage); } } logger.err( - '''Expected coverage >= ${minCoverage.toStringAsFixed(decimalPlaces)}% but actual is ${e.coverage.toStringAsFixed(decimalPlaces)}%.''', + '''Expected coverage >= ${minCoverage.toStringAsFixed(decimalPlaces)}% but actual is ${error.coverage.toStringAsFixed(decimalPlaces)}%.''', ); - final uncoveredLines = e.uncoveredLines; + final uncoveredLines = error.uncoveredLines; if (uncoveredLines != null && uncoveredLines.isNotEmpty) { logger.err(formatUncoveredLines(uncoveredLines)); } + + return ExitCode.software.code; } /// Formats a map of uncovered lines into a human-readable string. diff --git a/lib/src/commands/dart/commands/dart_test_command.dart b/lib/src/commands/dart/commands/dart_test_command.dart index 08896162c..558ffc05a 100644 --- a/lib/src/commands/dart/commands/dart_test_command.dart +++ b/lib/src/commands/dart/commands/dart_test_command.dart @@ -379,62 +379,63 @@ This command should be run from the root of your Dart project.'''); final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger); if (config == null) return ExitCode.config.code; - final isDartInstalled = await _dartInstalled(logger: _logger); + if (!await _dartInstalled(logger: _logger)) { + return ExitCode.success.code; + } final options = DartTestOptions.parse(_argResults, config: config); - if (isDartInstalled) { - try { - final results = await _dartTest( - optimizePerformance: - options.optimizePerformance && - !TestCLIRunner.isTargettingTestFiles(options.rest) && - // Disabled optimization when platform is specified - // https://github.com/VeryGoodOpenSource/very_good_cli/issues/1363 - options.platform == null, - recursive: recursive, - logger: _logger, - stdout: _logger.write, - stderr: _logger.err, - collectCoverage: - options.collectCoverage || - options.minCoverage != null || - options.showUncovered, - minCoverage: options.minCoverage, - showUncovered: options.showUncovered, - excludeFromCoverage: options.excludeFromCoverage, - collectCoverageFrom: options.collectCoverageFrom, - randomSeed: options.randomSeed, - forceAnsi: options.forceAnsi, - arguments: [ - if (options.excludeTags != null) ...['-x', options.excludeTags!], - if (options.tags != null) ...['-t', options.tags!], - if (options.failFast) '--fail-fast', - if (options.runSkipped) '--run-skipped', - if (options.platform != null) ...['--platform', options.platform!], - if (options.platform == null) ...['-j', options.concurrency], - if (options.fileReporter != null) - '--file-reporter=${options.fileReporter}', - ...options.rest, - ], - reportOn: options.reportOn.isEmpty ? null : options.reportOn, - checkIgnore: options.checkIgnore, - ); - if (results.any((code) => code != ExitCode.success.code)) { - return ExitCode.unavailable.code; - } - } on MinCoverageNotMet catch (e) { - TestCLIRunner.handleMinCoverageNotMet( - logger: _logger, - minCoverage: options.minCoverage, - e: e, - ); - return ExitCode.unavailable.code; - } on Exception catch (error) { - _logger.err('$error'); - return ExitCode.unavailable.code; + try { + final results = await _dartTest( + optimizePerformance: + options.optimizePerformance && + !TestCLIRunner.isTargettingTestFiles(options.rest) && + // Disabled optimization when platform is specified + // https://github.com/VeryGoodOpenSource/very_good_cli/issues/1363 + options.platform == null, + recursive: recursive, + logger: _logger, + stdout: _logger.write, + stderr: _logger.err, + collectCoverage: + options.collectCoverage || + options.minCoverage != null || + options.showUncovered, + minCoverage: options.minCoverage, + showUncovered: options.showUncovered, + excludeFromCoverage: options.excludeFromCoverage, + collectCoverageFrom: options.collectCoverageFrom, + randomSeed: options.randomSeed, + forceAnsi: options.forceAnsi, + arguments: [ + if (options.excludeTags != null) ...['-x', options.excludeTags!], + if (options.tags != null) ...['-t', options.tags!], + if (options.failFast) '--fail-fast', + if (options.runSkipped) '--run-skipped', + if (options.platform != null) ...['--platform', options.platform!], + if (options.platform == null) ...['-j', options.concurrency], + if (options.fileReporter != null) + '--file-reporter=${options.fileReporter}', + ...options.rest, + ], + reportOn: options.reportOn.isEmpty ? null : options.reportOn, + checkIgnore: options.checkIgnore, + ); + + if (results.any((code) => code != ExitCode.success.code)) { + return ExitCode.software.code; } + } on MinCoverageNotMet catch (error) { + return TestCLIRunner.handleMinCoverageNotMet( + error, + logger: _logger, + minCoverage: options.minCoverage, + ); + } on Exception catch (error) { + _logger.err('$error'); + return ExitCode.unavailable.code; } + return ExitCode.success.code; } } diff --git a/lib/src/commands/test/test.dart b/lib/src/commands/test/test.dart index 698dce96b..2114b1aaf 100644 --- a/lib/src/commands/test/test.dart +++ b/lib/src/commands/test/test.dart @@ -457,72 +457,73 @@ This command should be run from the root of your Flutter project.'''); final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger); if (config == null) return ExitCode.config.code; - final isFlutterInstalled = await _flutterInstalled(logger: _logger); + if (!await _flutterInstalled(logger: _logger)) { + return ExitCode.success.code; + } final options = FlutterTestOptions.parse(_argResults, config: config); - if (isFlutterInstalled) { - try { - final results = await _flutterTest( - optimizePerformance: - options.optimizePerformance && - !TestCLIRunner.isTargettingTestFiles(options.rest) && - !options.updateGoldens && - // Disabled optimization when platform is specified - // https://github.com/VeryGoodOpenSource/very_good_cli/issues/1363 - options.platform == null, - recursive: recursive, - logger: _logger, - stdout: _logger.write, - stderr: _logger.err, - collectCoverage: - options.collectCoverage || - options.minCoverage != null || - options.showUncovered, - minCoverage: options.minCoverage, - showUncovered: options.showUncovered, - excludeFromCoverage: options.excludeFromCoverage, - collectCoverageFrom: options.collectCoverageFrom, - randomSeed: options.randomSeed, - forceAnsi: options.forceAnsi, - reportOn: options.reportOn.isEmpty ? null : options.reportOn, - arguments: [ - if (options.excludeTags != null) ...['-x', options.excludeTags!], - if (options.tags != null) ...['-t', options.tags!], - if (options.updateGoldens) '--update-goldens', - if (options.failFast) '--fail-fast', - if (options.runSkipped) '--run-skipped', - if (options.flavor != null) ...['--flavor', options.flavor!], - if (options.platform != null) ...['--platform', options.platform!], - if (options.dartDefine != null) - for (final value in options.dartDefine!) '--dart-define=$value', - if (options.dartDefineFromFile != null) - for (final value in options.dartDefineFromFile!) - '--dart-define-from-file=$value', - if (options.platform == null) ...['-j', options.concurrency], - '--no-pub', - if (options.timeout != null) - '--timeout=${options.timeout!.inSeconds}s', - if (options.fileReporter != null) - '--file-reporter=${options.fileReporter}', - ...options.rest, - ], - ); - if (results.any((code) => code != ExitCode.success.code)) { - return ExitCode.unavailable.code; - } - } on MinCoverageNotMet catch (e) { - TestCLIRunner.handleMinCoverageNotMet( - logger: _logger, - minCoverage: options.minCoverage, - e: e, - ); - return ExitCode.unavailable.code; - } on Exception catch (error) { - _logger.err('$error'); - return ExitCode.unavailable.code; + try { + final results = await _flutterTest( + optimizePerformance: + options.optimizePerformance && + !TestCLIRunner.isTargettingTestFiles(options.rest) && + !options.updateGoldens && + // Disabled optimization when platform is specified + // https://github.com/VeryGoodOpenSource/very_good_cli/issues/1363 + options.platform == null, + recursive: recursive, + logger: _logger, + stdout: _logger.write, + stderr: _logger.err, + collectCoverage: + options.collectCoverage || + options.minCoverage != null || + options.showUncovered, + minCoverage: options.minCoverage, + showUncovered: options.showUncovered, + excludeFromCoverage: options.excludeFromCoverage, + collectCoverageFrom: options.collectCoverageFrom, + randomSeed: options.randomSeed, + forceAnsi: options.forceAnsi, + reportOn: options.reportOn.isEmpty ? null : options.reportOn, + arguments: [ + if (options.excludeTags != null) ...['-x', options.excludeTags!], + if (options.tags != null) ...['-t', options.tags!], + if (options.updateGoldens) '--update-goldens', + if (options.failFast) '--fail-fast', + if (options.runSkipped) '--run-skipped', + if (options.flavor != null) ...['--flavor', options.flavor!], + if (options.platform != null) ...['--platform', options.platform!], + if (options.dartDefine != null) + for (final value in options.dartDefine!) '--dart-define=$value', + if (options.dartDefineFromFile != null) + for (final value in options.dartDefineFromFile!) + '--dart-define-from-file=$value', + if (options.platform == null) ...['-j', options.concurrency], + '--no-pub', + if (options.timeout != null) + '--timeout=${options.timeout!.inSeconds}s', + if (options.fileReporter != null) + '--file-reporter=${options.fileReporter}', + ...options.rest, + ], + ); + + if (results.any((code) => code != ExitCode.success.code)) { + return ExitCode.software.code; } + } on MinCoverageNotMet catch (error) { + return TestCLIRunner.handleMinCoverageNotMet( + error, + logger: _logger, + minCoverage: options.minCoverage, + ); + } on Exception catch (error) { + _logger.err('$error'); + return ExitCode.unavailable.code; } + return ExitCode.success.code; } } diff --git a/lib/src/mcp/structured_tool_error.dart b/lib/src/mcp/structured_tool_error.dart index 7025e5cd9..5414b2817 100644 --- a/lib/src/mcp/structured_tool_error.dart +++ b/lib/src/mcp/structured_tool_error.dart @@ -11,8 +11,10 @@ import 'package:meta/meta.dart'; /// * `permission` — the process lacks filesystem or credential access. /// * `transient` — an environment or infrastructure hiccup; retrying may /// succeed. -/// * `business` — a domain rule was violated; the safest default for an -/// outcome that can't be attributed to the other three. +/// * `business` — a deterministic check reported a failure: a failing test +/// suite, an unmet coverage threshold, a violated project rule. Also the +/// safest default for an outcome that can't be attributed to the other +/// three. enum ToolFailureType { /// A caller-supplied argument was invalid. validation, @@ -23,15 +25,17 @@ enum ToolFailureType { /// An environment or infrastructure hiccup; retrying may resolve it. transient, - /// A domain rule was violated. + /// A deterministic check reported a failure. Re-running the same command + /// reproduces the same outcome. business; /// Classifies an [exitCode] into a [ToolFailureType]. /// /// Codes follow the sysexits.h conventions surfaced by `package:io`'s - /// [ExitCode]; unknown codes fall back to [ToolFailureType.business], the - /// safest default for an outcome we can't attribute to a transient failure - /// or a bad input. + /// [ExitCode]. A deterministic failure reports [ExitCode.software]; a run + /// that could not complete at all reports [ExitCode.unavailable]. Unknown + /// codes fall back to [ToolFailureType.business], the safest default for an + /// outcome we can't attribute to a transient failure or a bad input. factory fromExitCode(int exitCode) { if (exitCode == ExitCode.usage.code || exitCode == ExitCode.data.code || @@ -81,8 +85,8 @@ List alternativeApproachesFor(ToolFailureType failureType) { 'Cannot be retried as-is without an authorization change.', ], ToolFailureType.business: [ - 'Inspect the captured output for the specific rule reported.', - 'Try an alternate subcommand, template, or configuration.', + 'Read the captured output for the check that failed.', + 'Fix the reported problem; an unchanged re-run fails the same way.', 'Escalate to the user if the constraint cannot be satisfied.', ], }; diff --git a/test/src/commands/dart/commands/dart_test_test.dart b/test/src/commands/dart/commands/dart_test_test.dart index ea9f483e6..b9aa9be05 100644 --- a/test/src/commands/dart/commands/dart_test_test.dart +++ b/test/src/commands/dart/commands/dart_test_test.dart @@ -211,6 +211,33 @@ void main() { ).called(1); }); + test('completes normally without running tests ' + 'when Dart is not installed', () async { + isFlutterInstalled = false; + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verifyNever( + () => dartTest( + cwd: any(named: 'cwd'), + recursive: any(named: 'recursive'), + checkIgnore: any(named: 'checkIgnore'), + showUncovered: any(named: 'showUncovered'), + collectCoverage: any(named: 'collectCoverage'), + optimizePerformance: any(named: 'optimizePerformance'), + minCoverage: any(named: 'minCoverage'), + excludeFromCoverage: any(named: 'excludeFromCoverage'), + collectCoverageFrom: any(named: 'collectCoverageFrom'), + randomSeed: any(named: 'randomSeed'), + arguments: any(named: 'arguments'), + logger: any(named: 'logger'), + stdout: any(named: 'stdout'), + stderr: any(named: 'stderr'), + forceAnsi: any(named: 'forceAnsi'), + reportOn: any(named: 'reportOn'), + ), + ); + }); + test('exits with 70 when tests do not pass', () async { when( () => dartTest( @@ -231,7 +258,7 @@ void main() { (_) async => [ExitCode.success.code, ExitCode.unavailable.code], ); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); }); test('completes normally --recursive', () async { @@ -548,7 +575,7 @@ void main() { ), ).thenThrow(exception); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); verify( () => dartTest( optimizePerformance: true, @@ -592,7 +619,7 @@ void main() { ), ).thenThrow(exception); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); verify( () => logger.err('Expected coverage >= 100.00% but actual is 95.00%.'), ).called(1); @@ -623,7 +650,7 @@ void main() { ), ).thenThrow(exception); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); verify( () => dartTest( optimizePerformance: true, diff --git a/test/src/commands/test/test_test.dart b/test/src/commands/test/test_test.dart index df58d189c..bf97aa116 100644 --- a/test/src/commands/test/test_test.dart +++ b/test/src/commands/test/test_test.dart @@ -214,6 +214,32 @@ void main() { ).called(1); }); + test('completes normally without running tests ' + 'when Flutter is not installed', () async { + isFlutterInstalled = false; + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verifyNever( + () => flutterTest( + cwd: any(named: 'cwd'), + recursive: any(named: 'recursive'), + collectCoverage: any(named: 'collectCoverage'), + optimizePerformance: any(named: 'optimizePerformance'), + minCoverage: any(named: 'minCoverage'), + showUncovered: any(named: 'showUncovered'), + excludeFromCoverage: any(named: 'excludeFromCoverage'), + collectCoverageFrom: any(named: 'collectCoverageFrom'), + randomSeed: any(named: 'randomSeed'), + arguments: any(named: 'arguments'), + logger: any(named: 'logger'), + stdout: any(named: 'stdout'), + stderr: any(named: 'stderr'), + forceAnsi: any(named: 'forceAnsi'), + reportOn: any(named: 'reportOn'), + ), + ); + }); + group('errors', () { test('exits with 70 when tests do not pass', () async { when( @@ -234,7 +260,7 @@ void main() { (_) async => [ExitCode.success.code, ExitCode.unavailable.code], ); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); }); test('throws when exception occurs', () async { @@ -728,7 +754,7 @@ void main() { ), ).thenThrow(exception); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); verify( () => flutterTest( optimizePerformance: true, @@ -771,7 +797,7 @@ void main() { ), ).thenThrow(exception); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); verify( () => logger.err('Expected coverage >= 100.00% but actual is 95.00%.'), @@ -805,7 +831,7 @@ void main() { ), ).thenThrow(exception); final result = await testCommand.run(); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); verify( () => flutterTest( optimizePerformance: true, diff --git a/test/src/mcp/mcp_server_test.dart b/test/src/mcp/mcp_server_test.dart index c39ad6161..1a9fa0ed8 100644 --- a/test/src/mcp/mcp_server_test.dart +++ b/test/src/mcp/mcp_server_test.dart @@ -821,7 +821,7 @@ void main() { when(() => mockCommandRunner.run(any())).thenAnswer((_) async { stdout.write('compile error: boom'); stderr.write('stderr detail'); - return ExitCode.unavailable.code; + return ExitCode.software.code; }); final response = await sendRequest( @@ -835,8 +835,8 @@ void main() { expect(result.isError, isTrue); final payload = _errorPayload(result); expect(payload['status'], equals('partial_failure')); - expect(payload['failureType'], equals('transient')); - expect(payload['reason'], contains('failed with exit code 69')); + expect(payload['failureType'], equals('business')); + expect(payload['reason'], contains('failed with exit code 70')); final partial = payload['partialResults']! as String; expect(partial, contains('compile error: boom')); expect(partial, contains('stderr detail')); diff --git a/test/src/mcp/structured_tool_error_test.dart b/test/src/mcp/structured_tool_error_test.dart index 09e838bde..d68261765 100644 --- a/test/src/mcp/structured_tool_error_test.dart +++ b/test/src/mcp/structured_tool_error_test.dart @@ -83,6 +83,13 @@ void main() { ); } }); + + test('does not advise a retry for a deterministic failure', () { + expect( + alternativeApproachesFor(ToolFailureType.business), + everyElement(isNot(contains('Retry the command'))), + ); + }); }); group(StructuredToolError, () {