diff --git a/.github/workflows/very_good_cli.yaml b/.github/workflows/very_good_cli.yaml index 77daf330f..cecf2c321 100644 --- a/.github/workflows/very_good_cli.yaml +++ b/.github/workflows/very_good_cli.yaml @@ -38,7 +38,9 @@ jobs: run: flutter pub run test --run-skipped -t pull-request-only - name: Run Tests - run: flutter test -x pull-request-only -x e2e --no-pub --coverage --test-randomize-ordering-seed random + run: | + flutter pub global activate coverage + flutter pub run test -j 1 -x pull-request-only -x e2e --coverage=coverage --test-randomize-ordering-seed random && dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.packages --report-on=lib - name: Check Code Coverage uses: VeryGoodOpenSource/very_good_coverage@v1.2.0 diff --git a/lib/src/cli/cli.dart b/lib/src/cli/cli.dart index ea98f8574..f9bd111d3 100644 --- a/lib/src/cli/cli.dart +++ b/lib/src/cli/cli.dart @@ -5,6 +5,7 @@ import 'package:lcov_parser/lcov_parser.dart'; import 'package:mason/mason.dart'; import 'package:path/path.dart' as p; import 'package:universal_io/io.dart'; +import 'package:very_good_cli/src/commands/test/templates/test_runner_bundle.dart'; import 'package:very_good_test_runner/very_good_test_runner.dart'; part 'dart_cli.dart'; diff --git a/lib/src/cli/flutter_cli.dart b/lib/src/cli/flutter_cli.dart index e247a9141..deaf98a47 100644 --- a/lib/src/cli/flutter_cli.dart +++ b/lib/src/cli/flutter_cli.dart @@ -115,9 +115,11 @@ class Flutter { String cwd = '.', bool recursive = false, bool collectCoverage = false, + bool optimizePerformance = false, double? minCoverage, String? excludeFromCoverage, List? arguments, + void Function([String?]) Function(String message)? progress, void Function(String)? stdout, void Function(String)? stderr, }) async { @@ -129,13 +131,41 @@ class Flutter { } await _runCommand( - cmd: (cwd) { + cmd: (cwd) async { void noop(String? _) {} - stdout?.call('Running "flutter test" in ${p.canonicalize(cwd)}...\n'); + final target = DirectoryGeneratorTarget(Directory(p.normalize(cwd))); + final workingDirectory = target.dir.absolute.path; + stdout?.call( + 'Running "flutter test" in ${p.dirname(workingDirectory)}...\n', + ); + + if (optimizePerformance) { + final optimizationDone = progress?.call('Optimizing tests'); + try { + final generator = await MasonGenerator.fromBundle(testRunnerBundle); + var vars = {'package-root': workingDirectory}; + await generator.hooks.preGen( + vars: vars, + onVarsChanged: (v) => vars = v, + workingDirectory: workingDirectory, + ); + await generator.generate( + target, + vars: vars, + fileConflictResolution: FileConflictResolution.overwrite, + ); + } finally { + optimizationDone?.call(); + } + } + return _flutterTest( cwd: cwd, collectCoverage: collectCoverage, - arguments: arguments, + arguments: [ + ...?arguments, + if (optimizePerformance) p.join('test', '.test_runner.dart') + ], stdout: stdout ?? noop, stderr: stderr ?? noop, ); diff --git a/lib/src/commands/commands.dart b/lib/src/commands/commands.dart index 1143f32a8..16de9fe70 100644 --- a/lib/src/commands/commands.dart +++ b/lib/src/commands/commands.dart @@ -1,3 +1,3 @@ export 'create/create.dart'; export 'packages.dart'; -export 'test.dart'; +export 'test/test.dart'; diff --git a/lib/src/commands/create/create.dart b/lib/src/commands/create/create.dart index 430df720a..9dc60ae31 100644 --- a/lib/src/commands/create/create.dart +++ b/lib/src/commands/create/create.dart @@ -107,7 +107,7 @@ class CreateCommand extends Command { final Analytics _analytics; final Logger _logger; - final Future Function(MasonBundle) _generator; + final GeneratorBuilder _generator; @override String get description => diff --git a/lib/src/commands/test/templates/test_runner_bundle.dart b/lib/src/commands/test/templates/test_runner_bundle.dart new file mode 100644 index 000000000..508fe766b --- /dev/null +++ b/lib/src/commands/test/templates/test_runner_bundle.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: prefer_single_quotes, public_member_api_docs, lines_longer_than_80_chars, implicit_dynamic_list_literal, implicit_dynamic_map_literal + +import 'package:mason/mason.dart'; + +final testRunnerBundle = MasonBundle.fromJson({ + "files": [ + { + "path": "test/.test_runner.dart", + "data": + "Ly8gR0VORVJBVEVEIENPREUgLSBETyBOT1QgTU9ESUZZIEJZIEhBTkQKLy8gQ29uc2lkZXIgYWRkaW5nIHRoaXMgZmlsZSB0byB5b3VyIC5naXRpZ25vcmUuCgp7eyN0ZXN0c319aW1wb3J0ICd7e3sufX19JyBhcyB7eyNzbmFrZUNhc2V9fXt7ey59fX17ey9zbmFrZUNhc2V9fTsKe3svdGVzdHN9fQp2b2lkIG1haW4oKSB7Cnt7I3Rlc3RzfX0gIHt7I3NuYWtlQ2FzZX19e3t7Ln19fXt7L3NuYWtlQ2FzZX19Lm1haW4oKTsKe3svdGVzdHN9fX0K", + "type": "text" + } + ], + "hooks": [ + { + "path": "pre_gen.dart", + "data": + "aW1wb3J0ICdkYXJ0OmlvJzsKCmltcG9ydCAncGFja2FnZTptYXNvbi9tYXNvbi5kYXJ0JzsKaW1wb3J0ICdwYWNrYWdlOnBhdGgvcGF0aC5kYXJ0JyBhcyBwYXRoOwoKRnV0dXJlPHZvaWQ+IHJ1bihIb29rQ29udGV4dCBjb250ZXh0KSBhc3luYyB7CiAgZmluYWwgdGVzdERpciA9IERpcmVjdG9yeShwYXRoLmpvaW4oY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSwgJ3Rlc3QnKSk7CiAgaWYgKCF0ZXN0RGlyLmV4aXN0c1N5bmMoKSkgewogICAgY29udGV4dC5sb2dnZXIuZXJyKCdDb3VsZCBub3QgZmluZCBkaXJlY3RvcnkgJHt0ZXN0RGlyLnBhdGh9Jyk7CiAgICBleGl0KDEpOwogIH0KCiAgZmluYWwgdGVzdHMgPSB0ZXN0RGlyCiAgICAgIC5saXN0U3luYyhyZWN1cnNpdmU6IHRydWUpCiAgICAgIC53aGVyZSgoZW50aXR5KSA9PiBlbnRpdHkuaXNUZXN0KQogICAgICAubWFwKChlbnRpdHkpID0+IHBhdGgucmVsYXRpdmUoZW50aXR5LnBhdGgsIGZyb206IHRlc3REaXIucGF0aCkpCiAgICAgIC50b0xpc3QoKTsKCiAgY29udGV4dC52YXJzID0geyd0ZXN0cyc6IHRlc3RzfTsKfQoKZXh0ZW5zaW9uIG9uIEZpbGVTeXN0ZW1FbnRpdHkgewogIGJvb2wgZ2V0IGlzVGVzdCB7CiAgICByZXR1cm4gdGhpcyBpcyBGaWxlICYmIHBhdGguYmFzZW5hbWUodGhpcy5wYXRoKS5lbmRzV2l0aCgnX3Rlc3QuZGFydCcpOwogIH0KfQo=", + "type": "text" + }, + { + "path": "pubspec.yaml", + "data": + "bmFtZTogX2hvb2tfCnB1Ymxpc2hfdG86IG5vbmUKCmVudmlyb25tZW50OgogIHNkazogIj49Mi4xMi4wIDwzLjAuMCIKCmRlcGVuZGVuY2llczoKICBtYXNvbjogIj49MC4xLjAtZGV2IDwwLjEuMCIKICBwYXRoOiBeMS44LjEK", + "type": "text" + } + ], + "name": "test_runner", + "description": "A brick that generates a single entrypoint for Dart tests.", + "version": "0.1.0+1", + "environment": {"mason": ">=0.1.0-dev <0.1.0"}, + "readme": { + "path": "README.md", + "data": + "IyB0ZXN0X3J1bm5lcgoKQSBicmljayB0aGF0IGdlbmVyYXRlcyBhIHNpbmdsZSBlbnRyeXBvaW50IGZvciBEYXJ0IHRlc3RzLgoKX0dlbmVyYXRlZCBieSBbbWFzb25dWzFdIPCfp7FfCgojIyBHZXR0aW5nIFN0YXJ0ZWQg8J+agAoKYGBgc2gKbWFzb24gbWFrZSB0ZXN0X3J1bm5lciAtLXBhY2thZ2Utcm9vdCAuL3BhdGgvdG8vcGFja2FnZSAtLW9uLWNvbmZsaWN0IG92ZXJ3cml0ZQpgYGAKClRoZSBhYm92ZSBjb21tYW5kIHdpbGwgZ2VuZXJhdGUgYSBgLnRlc3RfcnVubmVyLmRhcnRgIGluIHRoZSBgdGVzdGAgZGlyZWN0b3J5IHRoYXQgaW1wb3J0cyBhbmQgZXhlY3V0ZXMgYWxsIHRlc3RzCgpgYGBkYXJ0Ci8vIEdFTkVSQVRFRCBDT0RFIC0gRE8gTk9UIE1PRElGWSBCWSBIQU5ECi8vIENvbnNpZGVyIGFkZGluZyB0aGlzIGZpbGUgdG8geW91ciAuZ2l0aWdub3JlLgoKaW1wb3J0ICdhcHAvdmlldy9hcHBfdGVzdC5kYXJ0JyBhcyBhcHBfdmlld19hcHBfdGVzdF9kYXJ0OwppbXBvcnQgJ2NvdW50ZXIvY3ViaXQvY291bnRlcl9jdWJpdF90ZXN0LmRhcnQnIGFzIGNvdW50ZXJfY3ViaXRfY291bnRlcl9jdWJpdF90ZXN0X2RhcnQ7CmltcG9ydCAnY291bnRlci92aWV3L2NvdW50ZXJfcGFnZV90ZXN0LmRhcnQnIGFzIGNvdW50ZXJfdmlld19jb3VudGVyX3BhZ2VfdGVzdF9kYXJ0OwoKdm9pZCBtYWluKCkgewogIGFwcF92aWV3X2FwcF90ZXN0X2RhcnQubWFpbigpOwogIGNvdW50ZXJfY3ViaXRfY291bnRlcl9jdWJpdF90ZXN0X2RhcnQubWFpbigpOwogIGNvdW50ZXJfdmlld19jb3VudGVyX3BhZ2VfdGVzdF9kYXJ0Lm1haW4oKTsKfQpgYGAKClsxXTogaHR0cHM6Ly9naXRodWIuY29tL2ZlbGFuZ2VsL21hc29uCg==", + "type": "text" + }, + "changelog": { + "path": "CHANGELOG.md", + "data": "IyAwLjEuMCsxCgotIGZlYXQ6IGluaXRpYWwgcmVsZWFzZQo=", + "type": "text" + }, + "license": { + "path": "LICENSE", + "data": + "TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAyMiBWZXJ5IEdvb2QgVmVudHVyZXMKClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUu", + "type": "text" + }, + "vars": { + "package-root": { + "type": "string", + "description": "The path to the package root.", + "default": ".", + "prompt": "Please enter the path to the package root." + } + } +}); diff --git a/lib/src/commands/test.dart b/lib/src/commands/test/test.dart similarity index 69% rename from lib/src/commands/test.dart rename to lib/src/commands/test/test.dart index 91b68d573..3c6d0752b 100644 --- a/lib/src/commands/test.dart +++ b/lib/src/commands/test/test.dart @@ -6,12 +6,35 @@ import 'package:path/path.dart' as path; import 'package:universal_io/io.dart'; import 'package:very_good_cli/src/cli/cli.dart'; +/// Signature for the [Flutter.installed] method. +typedef FlutterInstalledCommand = Future Function(); + +/// Signature for the [Flutter.test] method. +typedef FlutterTestCommand = Future Function({ + String cwd, + bool recursive, + bool collectCoverage, + bool optimizePerformance, + double? minCoverage, + String? excludeFromCoverage, + List? arguments, + void Function([String?]) Function(String message)? progress, + void Function(String)? stdout, + void Function(String)? stderr, +}); + /// {@template test_command} /// `very_good test` command for running tests. /// {@endtemplate} class TestCommand extends Command { /// {@macro test_command} - TestCommand({Logger? logger}) : _logger = logger ?? Logger() { + TestCommand({ + Logger? logger, + FlutterInstalledCommand? flutterInstalled, + FlutterTestCommand? flutterTest, + }) : _logger = logger ?? Logger(), + _flutterInstalled = flutterInstalled ?? Flutter.installed, + _flutterTest = flutterTest ?? Flutter.test { argParser ..addFlag( 'recursive', @@ -41,6 +64,8 @@ class TestCommand extends Command { } final Logger _logger; + final FlutterInstalledCommand _flutterInstalled; + final FlutterTestCommand _flutterTest; @override String get description => 'Run tests in a Dart or Flutter project.'; @@ -56,21 +81,33 @@ class TestCommand extends Command { @override Future run() async { - final recursive = _argResults['recursive'] as bool; final targetPath = path.normalize(Directory.current.absolute.path); + final pubspec = File(path.join(targetPath, 'pubspec.yaml')); + + if (!pubspec.existsSync()) { + _logger.err( + ''' +Could not find a pubspec.yaml in $targetPath. +This command should be run from the root of your Flutter project.''', + ); + return ExitCode.noInput.code; + } + + final recursive = _argResults['recursive'] as bool; final collectCoverage = _argResults['coverage'] as bool; final minCoverage = double.tryParse( _argResults['min-coverage'] as String? ?? '', ); final excludeTags = _argResults['exclude-tags'] as String?; - final isFlutterInstalled = await Flutter.installed(); - + final isFlutterInstalled = await _flutterInstalled(); final excludeFromCoverage = _argResults['exclude-coverage'] as String?; if (isFlutterInstalled) { try { - await Flutter.test( + await _flutterTest( + optimizePerformance: _argResults.rest.isEmpty, recursive: recursive, + progress: _logger.progress, stdout: _logger.write, stderr: _logger.err, collectCoverage: collectCoverage, @@ -81,13 +118,6 @@ class TestCommand extends Command { ..._argResults.rest, ], ); - } on PubspecNotFound catch (_) { - _logger.err( - ''' -Could not find a pubspec.yaml in $targetPath. -This command should be run from the root of your Flutter project.''', - ); - return ExitCode.noInput.code; } on MinCoverageNotMet catch (e) { _logger.err( '''Expected coverage >= ${minCoverage!.toStringAsFixed(2)}% but actual is ${e.coverage.toStringAsFixed(2)}%.''', diff --git a/pubspec.yaml b/pubspec.yaml index 12c9b9cb6..e94a73db4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: args: ^2.1.0 glob: ^2.0.2 lcov_parser: ^0.1.2 - mason: ">=0.1.0-dev.9 <0.1.0-dev.10" + mason: ">=0.1.0-dev.12 <0.1.0-dev.13" mason_logger: ^0.1.0-dev.6 meta: ^1.3.0 path: ^1.8.0 diff --git a/test/src/cli/flutter_cli_test.dart b/test/src/cli/flutter_cli_test.dart index 6dee072f0..fa4266e20 100644 --- a/test/src/cli/flutter_cli_test.dart +++ b/test/src/cli/flutter_cli_test.dart @@ -5,6 +5,13 @@ import 'package:test/test.dart'; import 'package:universal_io/io.dart'; import 'package:very_good_cli/src/cli/cli.dart'; +const otherContents = ''' +class Other { + void foo() { + print('hello world'); + } +}'''; + const calculatorContents = ''' class Calculator { int add(int x, int y) => x + y; @@ -32,6 +39,18 @@ void main() { }); }'''; +const calculatorTestContentsWithOtherImport = ''' +import 'package:test/test.dart'; +import 'package:example/calculator.dart'; +import 'package:example/other.dart'; + +void main() { + test('...', () { + expect(Calculator().add(1, 2), equals(3)); + expect(Calculator().subtract(43, 1), equals(42)); + }); +}'''; + const testContents = ''' import 'package:test/test.dart'; @@ -216,6 +235,7 @@ void main() { setUp(() { logger = MockLogger(); + when(() => logger.progress(any())).thenReturn(([_]) {}); }); test('GenerateCoverageTimeout toString()', () { @@ -279,7 +299,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -311,7 +335,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -346,7 +374,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -383,7 +415,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -415,7 +451,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify(() => logger.err(any(that: contains('EXCEPTION')))).called(1); @@ -448,7 +488,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -488,12 +532,54 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), + ), + ).called(1); + verify( + () => logger.write(any(that: contains('+1: All tests passed!'))), + ).called(1); + }); + + test('completes when there is a test directory w/optimizations (passing)', + () async { + final directory = Directory.systemTemp.createTempSync(); + final testDirectory = Directory(p.join(directory.path, 'test')) + ..createSync(); + File(p.join(directory.path, 'pubspec.yaml')).writeAsStringSync(pubspec); + File( + p.join(testDirectory.path, 'example_test.dart'), + ).writeAsStringSync(testContents); + await expectLater( + Flutter.test( + cwd: directory.path, + optimizePerformance: true, + stdout: logger.write, + stderr: logger.err, + progress: logger.progress, + ), + completes, + ); + verify(() => logger.progress('Optimizing tests')).called(1); + verify( + () => logger.write( + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( () => logger.write(any(that: contains('+1: All tests passed!'))), ).called(1); + expect( + File(p.join(testDirectory.path, '.test_runner.dart')).existsSync(), + isTrue, + ); }); test('completes when there is a test directory (recursive)', () async { @@ -521,7 +607,46 @@ void main() { () => logger.write( any( that: contains( - 'Running "flutter test" in ${nestedDirectory.path}', + 'Running "flutter test" in ${p.dirname(nestedDirectory.path)}', + ), + ), + ), + ).called(1); + verify( + () => logger.write(any(that: contains('+1: All tests passed!'))), + ).called(1); + }); + + test('completes w specific test target', () async { + final directory = Directory.systemTemp.createTempSync(); + final libDirectory = Directory(p.join(directory.path, 'lib')) + ..createSync(); + final testDirectory = Directory(p.join(directory.path, 'test')) + ..createSync(); + File(p.join(directory.path, 'pubspec.yaml')).writeAsStringSync(pubspec); + File( + p.join(libDirectory.path, 'calculator.dart'), + ).writeAsStringSync(calculatorContents); + File( + p.join(testDirectory.path, 'calculator_test.dart'), + ).writeAsStringSync(calculatorTestContents); + final otherTest = File( + p.join(testDirectory.path, 'other_test.dart'), + )..writeAsStringSync(testContents); + await expectLater( + Flutter.test( + cwd: directory.path, + stdout: logger.write, + stderr: logger.err, + arguments: [otherTest.path], + ), + completes, + ); + verify( + () => logger.write( + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', ), ), ), @@ -555,7 +680,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -595,7 +724,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -634,7 +767,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -673,7 +810,56 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), + ), + ).called(1); + verify( + () => logger.write(any(that: contains('+1: All tests passed!'))), + ).called(1); + expect( + File(p.join(directory.path, 'coverage', 'lcov.info')).existsSync(), + isTrue, + ); + }); + + test('passes when --min-coverage 100 w/exclude coverage', () async { + final directory = Directory.systemTemp.createTempSync(); + final libDirectory = Directory(p.join(directory.path, 'lib')) + ..createSync(); + final testDirectory = Directory(p.join(directory.path, 'test')) + ..createSync(); + File(p.join(directory.path, 'pubspec.yaml')).writeAsStringSync(pubspec); + File( + p.join(libDirectory.path, 'calculator.dart'), + ).writeAsStringSync(calculatorContents); + File( + p.join(libDirectory.path, 'other.dart'), + ).writeAsStringSync(otherContents); + File( + p.join(testDirectory.path, 'calculator_test.dart'), + ).writeAsStringSync(calculatorTestContentsWithOtherImport); + await expectLater( + Flutter.test( + cwd: directory.path, + excludeFromCoverage: 'lib/other.dart', + stdout: logger.write, + stderr: logger.err, + collectCoverage: true, + minCoverage: 100, + ), + completes, + ); + verify( + () => logger.write( + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -710,7 +896,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( @@ -747,7 +937,11 @@ void main() { ); verify( () => logger.write( - any(that: contains('Running "flutter test" in ${directory.path}')), + any( + that: contains( + 'Running "flutter test" in ${p.dirname(directory.path)}', + ), + ), ), ).called(1); verify( diff --git a/test/src/commands/create_test.dart b/test/src/commands/create/create_test.dart similarity index 99% rename from test/src/commands/create_test.dart rename to test/src/commands/create/create_test.dart index 9bf702124..5afdc7f1a 100644 --- a/test/src/commands/create_test.dart +++ b/test/src/commands/create/create_test.dart @@ -12,7 +12,7 @@ import 'package:very_good_cli/src/command_runner.dart'; import 'package:very_good_cli/src/commands/create/create.dart'; import 'package:very_good_cli/src/commands/create/templates/templates.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; const expectedUsage = [ // ignore: no_adjacent_strings_in_list diff --git a/test/src/commands/test/test_test.dart b/test/src/commands/test/test_test.dart new file mode 100644 index 000000000..555dda103 --- /dev/null +++ b/test/src/commands/test/test_test.dart @@ -0,0 +1,301 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:mason/mason.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:very_good_cli/src/cli/cli.dart'; +import 'package:very_good_cli/src/commands/test/test.dart'; + +import '../../../helpers/helpers.dart'; + +const expectedTestUsage = [ + // ignore: no_adjacent_strings_in_list + 'Run tests in a Dart or Flutter project.\n' + '\n' + 'Usage: very_good test [arguments]\n' + '-h, --help Print this usage information.\n' + '-r, --recursive Run tests recursively for all nested ' + 'packages.\n' + ' --coverage Whether to collect coverage information.\n' + ' --min-coverage Whether to enforce a minimum coverage ' + 'percentage.\n' + ' --exclude-coverage A glob which will be used to exclude files ' + 'that match from the coverage.\n' + '''-x, --exclude-tags Run only tests that do not have the specified tags.\n''' + '\n' + 'Run "very_good help" to see global options.', +]; + +// ignore: one_member_abstracts +abstract class FlutterTestCommand { + Future call({ + String cwd = '.', + bool recursive = false, + bool collectCoverage = false, + bool optimizePerformance = false, + double? minCoverage, + String? excludeFromCoverage, + List? arguments, + void Function([String?]) Function(String message)? progress, + void Function(String)? stdout, + void Function(String)? stderr, + }); +} + +class MockLogger extends Mock implements Logger {} + +class MockArgResults extends Mock implements ArgResults {} + +class MockFlutterTestCommand extends Mock implements FlutterTestCommand {} + +void main() { + group('test', () { + final cwd = Directory.current; + + late Logger logger; + late bool isFlutterInstalled; + late ArgResults argResults; + late FlutterTestCommand flutterTest; + late TestCommand testCommand; + + setUp(() { + Directory.current = cwd; + logger = MockLogger(); + isFlutterInstalled = true; + argResults = MockArgResults(); + flutterTest = MockFlutterTestCommand(); + testCommand = TestCommand( + logger: logger, + flutterInstalled: () async => isFlutterInstalled, + flutterTest: flutterTest.call, + )..argResultOverrides = argResults; + when( + () => flutterTest( + cwd: any(named: 'cwd'), + recursive: any(named: 'recursive'), + collectCoverage: any(named: 'collectCoverage'), + optimizePerformance: any(named: 'optimizePerformance'), + minCoverage: any(named: 'minCoverage'), + excludeFromCoverage: any(named: 'excludeFromCoverage'), + arguments: any(named: 'arguments'), + progress: any(named: 'progress'), + stdout: any(named: 'stdout'), + stderr: any(named: 'stderr'), + ), + ).thenAnswer((_) async {}); + when(() => argResults['recursive']).thenReturn(false); + when(() => argResults['coverage']).thenReturn(false); + when(() => argResults.rest).thenReturn([]); + }); + + test( + 'help', + withRunner((commandRunner, logger, printLogs) async { + final result = await commandRunner.run(['test', '--help']); + expect(printLogs, equals(expectedTestUsage)); + expect(result, equals(ExitCode.success.code)); + + printLogs.clear(); + + final resultAbbr = await commandRunner.run(['test', '-h']); + expect(printLogs, equals(expectedTestUsage)); + expect(resultAbbr, equals(ExitCode.success.code)); + }), + ); + + test( + 'throws pubspec not found exception ' + 'when no pubspec.yaml exists', + withRunner((commandRunner, logger, printLogs) async { + final directory = Directory.systemTemp.createTempSync(); + Directory.current = directory.path; + final result = await commandRunner.run(['test']); + expect(result, equals(ExitCode.noInput.code)); + verify(() { + logger.err(any(that: contains('Could not find a pubspec.yaml in'))); + }).called(1); + }), + ); + + test( + 'throws pubspec not found exception ' + 'when no pubspec.yaml exists (recursive)', + withRunner((commandRunner, logger, printLogs) async { + final directory = Directory.systemTemp.createTempSync(); + Directory.current = directory.path; + final result = await commandRunner.run(['test', '-r']); + expect(result, equals(ExitCode.noInput.code)); + verify(() { + logger.err(any(that: contains('Could not find a pubspec.yaml in'))); + }).called(1); + }), + ); + + test('completes normally', () async { + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + optimizePerformance: true, + arguments: [], + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('completes normally --recursive', () async { + when(() => argResults['recursive']).thenReturn(true); + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + recursive: true, + optimizePerformance: true, + arguments: [], + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('completes normally --coverage', () async { + when(() => argResults['coverage']).thenReturn(true); + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + collectCoverage: true, + optimizePerformance: true, + arguments: [], + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('completes normally -x test-tag', () async { + when(() => argResults['exclude-tags']).thenReturn('test-tag'); + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + optimizePerformance: true, + arguments: ['-x', 'test-tag'], + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('completes normally --coverage --min-coverage 0', () async { + when(() => argResults['coverage']).thenReturn(true); + when(() => argResults['min-coverage']).thenReturn('0'); + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + optimizePerformance: true, + collectCoverage: true, + arguments: [], + minCoverage: 0, + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('fails when coverage not met', () async { + when(() => argResults['coverage']).thenReturn(true); + when(() => argResults['min-coverage']).thenReturn('100'); + const exception = MinCoverageNotMet(0); + when( + () => flutterTest( + cwd: any(named: 'cwd'), + recursive: any(named: 'recursive'), + collectCoverage: any(named: 'collectCoverage'), + optimizePerformance: any(named: 'optimizePerformance'), + minCoverage: any(named: 'minCoverage'), + excludeFromCoverage: any(named: 'excludeFromCoverage'), + arguments: any(named: 'arguments'), + progress: any(named: 'progress'), + stdout: any(named: 'stdout'), + stderr: any(named: 'stderr'), + ), + ).thenThrow(exception); + final result = await testCommand.run(); + expect(result, equals(ExitCode.unavailable.code)); + verify( + () => flutterTest( + optimizePerformance: true, + collectCoverage: true, + arguments: [], + minCoverage: 100, + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + verify( + () => logger.err('Expected coverage >= 100.00% but actual is 0.00%.'), + ).called(1); + }); + + test('exclude files from coverage when --exclude-coverage is used', + () async { + when(() => argResults['coverage']).thenReturn(true); + when( + () => argResults['exclude-coverage'], + ).thenReturn('*.g.dart'); + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + optimizePerformance: true, + collectCoverage: true, + excludeFromCoverage: '*.g.dart', + arguments: [], + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('throws when exception occurs', () async { + final exception = Exception('oops'); + when( + () => flutterTest( + cwd: any(named: 'cwd'), + recursive: any(named: 'recursive'), + collectCoverage: any(named: 'collectCoverage'), + optimizePerformance: any(named: 'optimizePerformance'), + minCoverage: any(named: 'minCoverage'), + excludeFromCoverage: any(named: 'excludeFromCoverage'), + arguments: any(named: 'arguments'), + progress: any(named: 'progress'), + stdout: any(named: 'stdout'), + stderr: any(named: 'stderr'), + ), + ).thenThrow(exception); + final result = await testCommand.run(); + expect(result, equals(ExitCode.unavailable.code)); + verify( + () => flutterTest( + optimizePerformance: true, + arguments: [], + progress: logger.progress, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + verify(() => logger.err('$exception')).called(1); + }); + }); +} diff --git a/test/src/commands/test_test.dart b/test/src/commands/test_test.dart deleted file mode 100644 index 9887e5d68..000000000 --- a/test/src/commands/test_test.dart +++ /dev/null @@ -1,375 +0,0 @@ -import 'dart:io'; - -import 'package:mason/mason.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:path/path.dart' as path; -import 'package:test/test.dart'; - -import '../../helpers/helpers.dart'; - -const fooContent = ''' -class Foo { - bool value() => true; -} -'''; - -const barContent = ''' -class Bar { - bool value() => true; -} -'''; -const barrelContent = ''' -export 'bar.dart'; -export 'foo.dart'; -'''; - -const testFooContent = ''' -import 'package:test/test.dart'; -import 'package:example/example.dart'; -void main() { - test('Foo', () { - expect(Foo().value(), isTrue); - }); -}'''; - -const testContent = ''' -import 'package:test/test.dart'; -void main() { - test('example', () { - expect(true, isTrue); - }); -}'''; - -const testTagsContent = ''' -import 'package:test/test.dart'; -void main() { - test('example', () { - expect(true, isTrue); - }); - - test('...', () { - expect(true, isTrue); - }, tags: 'test-tag'); -}'''; - -const expectedTestUsage = [ - // ignore: no_adjacent_strings_in_list - 'Run tests in a Dart or Flutter project.\n' - '\n' - 'Usage: very_good test [arguments]\n' - '-h, --help Print this usage information.\n' - '-r, --recursive Run tests recursively for all nested ' - 'packages.\n' - ' --coverage Whether to collect coverage information.\n' - ' --min-coverage Whether to enforce a minimum coverage ' - 'percentage.\n' - ' --exclude-coverage A glob which will be used to exclude files ' - 'that match from the coverage.\n' - '''-x, --exclude-tags Run only tests that do not have the specified tags.\n''' - '\n' - 'Run "very_good help" to see global options.', -]; - -String pubspecContent([String name = 'example']) { - return ''' -name: $name -version: 0.1.0 - -environment: - sdk: ">=2.12.0 <3.0.0" - -dev_dependencies: - test: any'''; -} - -void main() { - group('test', () { - final cwd = Directory.current; - - setUp(() { - Directory.current = cwd; - }); - - test( - 'help', - withRunner((commandRunner, logger, printLogs) async { - final result = await commandRunner.run(['test', '--help']); - expect(printLogs, equals(expectedTestUsage)); - expect(result, equals(ExitCode.success.code)); - - printLogs.clear(); - - final resultAbbr = await commandRunner.run(['test', '-h']); - expect(printLogs, equals(expectedTestUsage)); - expect(resultAbbr, equals(ExitCode.success.code)); - }), - ); - - test( - 'throws pubspec not found exception ' - 'when no pubspec.yaml exists', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final result = await commandRunner.run(['test']); - expect(result, equals(ExitCode.noInput.code)); - verify(() { - logger.err(any(that: contains('Could not find a pubspec.yaml in'))); - }).called(1); - }), - ); - - test( - 'throws pubspec not found exception ' - 'when no pubspec.yaml exists (recursive)', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final result = await commandRunner.run(['test', '-r']); - expect(result, equals(ExitCode.noInput.code)); - verify(() { - logger.err(any(that: contains('Could not find a pubspec.yaml in'))); - }).called(1); - }), - ); - - test( - 'throws when installation fails', - withRunner( - (commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - File(path.join(directory.path, 'pubspec.yaml')).writeAsStringSync(''); - final result = await commandRunner.run(['test']); - expect(result, equals(ExitCode.unavailable.code)); - }, - ), - ); - - test( - 'completes normally ' - 'when pubspec.yaml and tests exist', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final testDirectory = Directory(path.join(directory.path, 'test')) - ..createSync(); - File( - path.join(directory.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent()); - File( - path.join(testDirectory.path, 'example_test.dart'), - ).writeAsStringSync(testContent); - final result = await commandRunner.run(['test']); - expect(result, equals(ExitCode.success.code)); - verify(() { - logger.write( - any(that: contains('Running "flutter test" in')), - ); - }).called(1); - verify(() { - logger.write(any(that: contains('All tests passed'))); - }).called(1); - }), - ); - - test( - 'completes normally --coverage', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final testDirectory = Directory(path.join(directory.path, 'test')) - ..createSync(); - File( - path.join(directory.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent()); - File( - path.join(testDirectory.path, 'example_test.dart'), - ).writeAsStringSync(testContent); - final result = await commandRunner.run(['test', '--coverage']); - expect(result, equals(ExitCode.success.code)); - verify(() { - logger.write( - any(that: contains('Running "flutter test" in')), - ); - }).called(1); - verify(() { - logger.write(any(that: contains('All tests passed'))); - }).called(1); - }), - ); - - test( - 'completes normally -x test-tag', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final testDirectory = Directory(path.join(directory.path, 'test')) - ..createSync(); - File( - path.join(directory.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent()); - File( - path.join(testDirectory.path, 'example_test.dart'), - ).writeAsStringSync(testTagsContent); - final result = await commandRunner.run(['test', '-x', 'test-tag']); - expect(result, equals(ExitCode.success.code)); - verify(() { - logger.write( - any(that: contains('Running "flutter test" in')), - ); - }).called(1); - verify(() { - logger.write(any(that: contains('+1: All tests passed!'))); - }).called(1); - }), - ); - - test( - 'completes normally --coverage --min-coverage 0', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final testDirectory = Directory(path.join(directory.path, 'test')) - ..createSync(); - File( - path.join(directory.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent()); - File( - path.join(testDirectory.path, 'example_test.dart'), - ).writeAsStringSync(testContent); - final result = await commandRunner.run( - ['test', '--coverage', '--min-coverage', '0'], - ); - expect(result, equals(ExitCode.success.code)); - verify(() { - logger.write( - any(that: contains('Running "flutter test" in')), - ); - }).called(1); - verify(() { - logger.write(any(that: contains('All tests passed'))); - }).called(1); - }), - ); - - test( - 'fails when coverage not met --coverage --min-coverage 100', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final testDirectory = Directory(path.join(directory.path, 'test')) - ..createSync(); - File( - path.join(directory.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent()); - File( - path.join(testDirectory.path, 'example_test.dart'), - ).writeAsStringSync(testContent); - final result = await commandRunner.run( - ['test', '--coverage', '--min-coverage', '100'], - ); - expect(result, equals(ExitCode.unavailable.code)); - verify(() { - logger.write( - any(that: contains('Running "flutter test" in')), - ); - }).called(1); - verify(() { - logger.write(any(that: contains('All tests passed'))); - }).called(1); - verify( - () => logger.err('Expected coverage >= 100.00% but actual is 0.00%.'), - ).called(1); - }), - ); - - test( - 'exclude files from coverage when --exclude-coverage is used', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final testDirectory = Directory(path.join(directory.path, 'test')) - ..createSync(); - final sourceDirectory = Directory(path.join(directory.path, 'lib')) - ..createSync(); - File( - path.join(directory.path, 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent()); - File( - path.join(sourceDirectory.path, 'foo.dart'), - ).writeAsStringSync(fooContent); - File( - path.join(sourceDirectory.path, 'bar.dart'), - ).writeAsStringSync(barContent); - File( - path.join(sourceDirectory.path, 'example.dart'), - ).writeAsStringSync(barrelContent); - File( - path.join(testDirectory.path, 'foo_test.dart'), - ).writeAsStringSync(testFooContent); - - final result = await commandRunner.run( - [ - 'test', - '--coverage', - '--min-coverage', - '100', - '--exclude-coverage', - '**/bar.dart', - ], - ); - expect(result, equals(ExitCode.success.code)); - verify(() { - logger.write( - any(that: contains('Running "flutter test" in')), - ); - }).called(1); - verify(() { - logger.write(any(that: contains('All tests passed'))); - }).called(1); - verifyNever( - () => logger.err('Expected coverage >= 100.00% but actual is 0.00%.'), - ); - }), - ); - - test( - 'completes normally ' - 'when pubspec.yaml and tests exist (recursive)', - withRunner((commandRunner, logger, printLogs) async { - final directory = Directory.systemTemp.createTempSync(); - Directory.current = directory.path; - final testDirectoryA = Directory( - path.join(directory.path, 'example_a', 'test'), - )..createSync(recursive: true); - final testDirectoryB = Directory( - path.join(directory.path, 'example_b', 'test'), - )..createSync(recursive: true); - File( - path.join(testDirectoryA.path, 'example_a_test.dart'), - ).writeAsStringSync(testContent); - File( - path.join(testDirectoryB.path, 'example_b_test.dart'), - ).writeAsStringSync(testContent); - File( - path.join(directory.path, 'example_a', 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent('example_a')); - File( - path.join(directory.path, 'example_b', 'pubspec.yaml'), - ).writeAsStringSync(pubspecContent('example_b')); - - final result = await commandRunner.run(['test', '--recursive']); - expect(result, equals(ExitCode.success.code)); - verify(() { - logger.write( - any(that: contains('Running "flutter test" in')), - ); - }).called(2); - verify(() { - logger.write(any(that: contains('All tests passed'))); - }).called(2); - }), - ); - }); -}