Skip to content
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
1 change: 1 addition & 0 deletions .github/workflows/very_good_cli.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
# E2E tests for the create command
- test/src/commands/create/e2e/flutter_app/core_test.dart
- test/src/commands/create/e2e/dart_package/dart_pkg_test.dart
- test/src/commands/create/e2e/dart_cli/dart_cli_test.dart

# E2E tests for the legacy create command syntax
- test/src/commands/create/e2e/legacy/core_test.dart
Expand Down
1 change: 1 addition & 0 deletions lib/src/commands/create/commands/commands.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'create_subcommand.dart';
export 'dart_cli.dart';
export 'dart_package.dart';
export 'flutter_app.dart';
export 'legacy.dart';
49 changes: 49 additions & 0 deletions lib/src/commands/create/commands/dart_cli.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:usage/usage.dart';
import 'package:very_good_cli/src/commands/create/commands/commands.dart';
import 'package:very_good_cli/src/commands/create/templates/templates.dart';

/// {@template very_good_create_dart_cli_command}
/// A [CreateSubCommand] for creating Dart command line interfaces.
/// {@endtemplate}
class CreateDartCLI extends CreateSubCommand with Publishable {
/// {@macro very_good_create_dart_cli_command}
CreateDartCLI({
required Analytics analytics,
required Logger logger,
required MasonGeneratorFromBundle? generatorFromBundle,
required MasonGeneratorFromBrick? generatorFromBrick,
}) : super(
analytics: analytics,
logger: logger,
generatorFromBundle: generatorFromBundle,
generatorFromBrick: generatorFromBrick,
) {
argParser.addOption(
'executable-name',
help: 'The CLI executable name (defaults to the project name)',
);
}

@override
String get name => 'dart_cli';

@override
String get description =>
'Creates a new very good Dart CLI in the specified directory.';

@override
Template get template => VeryGoodDartCLITemplate();

@override
Map<String, dynamic> getTemplateVars() {
final vars = super.getTemplateVars();

final executableName =
argResults['executable-name'] as String? ?? projectName;

vars['executable_name'] = executableName;

return vars;
}
}
10 changes: 10 additions & 0 deletions lib/src/commands/create/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ class CreateCommand extends Command<int> {
generatorFromBrick: generatorFromBrick,
),
);

// very_good create dart_cli <args>
addSubcommand(
CreateDartCLI(
analytics: analytics,
logger: logger,
generatorFromBundle: generatorFromBundle,
generatorFromBrick: generatorFromBrick,
),
);
}

@override
Expand Down
219 changes: 219 additions & 0 deletions test/src/commands/create/commands/dart_cli_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import 'dart:io';

import 'package:args/args.dart';
import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:usage/usage.dart';
import 'package:very_good_cli/src/commands/commands.dart';

import '../../../../helpers/helpers.dart';

class MockAnalytics extends Mock implements Analytics {}

class MockLogger extends Mock implements Logger {}

class MockMasonGenerator extends Mock implements MasonGenerator {}

class MockGeneratorHooks extends Mock implements GeneratorHooks {}

class MockArgResults extends Mock implements ArgResults {}

class FakeLogger extends Fake implements Logger {}

class FakeDirectoryGeneratorTarget extends Fake
implements DirectoryGeneratorTarget {}

final expectedUsage = [
'''
Creates a new very good Dart CLI in the specified directory.

Usage: very_good create dart_cli <project-name> [arguments]
-h, --help Print this usage information.
-o, --output-directory The desired output directory when creating a new project.
--description The description for this new project.
(defaults to "A Very Good Project created by Very Good CLI.")
--publishable Whether the generated project is intended to be published.
--executable-name The CLI executable name (defaults to the project name)

Run "very_good help" to see global options.''',
];

const pubspec = '''
name: example
environment:
sdk: ">=2.13.0 <3.0.0"
''';

void main() {
late Analytics analytics;
late Logger logger;

setUpAll(() {
registerFallbackValue(FakeDirectoryGeneratorTarget());
registerFallbackValue(FakeLogger());
});

setUp(() {
analytics = MockAnalytics();
when(
() => analytics.sendEvent(any(), any(), label: any(named: 'label')),
).thenAnswer((_) async {});
when(
() => analytics.waitForLastPing(timeout: any(named: 'timeout')),
).thenAnswer((_) async {});

logger = MockLogger();

final progress = MockProgress();

when(() => logger.progress(any())).thenReturn(progress);
});

group('can be instantiated', () {
test('with default options', () {
final logger = Logger();
final command = CreateDartCLI(
analytics: analytics,
logger: logger,
generatorFromBundle: null,
generatorFromBrick: null,
);
expect(command.name, equals('dart_cli'));
expect(
command.description,
equals('Creates a new very good Dart CLI in the specified directory.'),
);
expect(command.logger, equals(logger));
expect(command, isA<Publishable>());
expect(command.argParser.options, contains('executable-name'));
});
});

group('create dart_cli', () {
test(
'help',
withRunner((commandRunner, logger, pubUpdater, printLogs) async {
final result =
await commandRunner.run(['create', 'dart_cli', '--help']);
expect(printLogs, equals(expectedUsage));
expect(result, equals(ExitCode.success.code));

printLogs.clear();

final resultAbbr =
await commandRunner.run(['create', 'dart_cli', '-h']);
expect(printLogs, equals(expectedUsage));
expect(resultAbbr, equals(ExitCode.success.code));
}),
);

group('running the command', () {
final generatedFiles =
List.filled(10, const GeneratedFile.created(path: ''));

late GeneratorHooks hooks;
late MasonGenerator generator;

setUp(() {
hooks = MockGeneratorHooks();
generator = MockMasonGenerator();

when(() => generator.hooks).thenReturn(hooks);
when(
() => hooks.preGen(
vars: any(named: 'vars'),
onVarsChanged: any(named: 'onVarsChanged'),
),
).thenAnswer((_) async {});

when(
() => generator.generate(
any(),
vars: any(named: 'vars'),
logger: any(named: 'logger'),
),
).thenAnswer((_) async {
return generatedFiles;
});

when(() => generator.id).thenReturn('generator_id');
when(() => generator.description).thenReturn('generator description');
when(() => generator.hooks).thenReturn(hooks);

when(
() => hooks.preGen(
vars: any(named: 'vars'),
onVarsChanged: any(named: 'onVarsChanged'),
),
).thenAnswer((_) async {});
when(
() => generator.generate(
any(),
vars: any(named: 'vars'),
logger: any(named: 'logger'),
),
).thenAnswer((_) async {
final target =
_.positionalArguments.first as DirectoryGeneratorTarget;
File(path.join(target.dir.path, 'my_cli', 'pubspec.yaml'))
..createSync(recursive: true)
..writeAsStringSync(pubspec);
return generatedFiles;
});
});

test('creates dart package', () async {
final tempDir = Directory.systemTemp.createTempSync();
addTearDown(() => tempDir.deleteSync(recursive: true));
final argResults = MockArgResults();
final command = CreateDartCLI(
analytics: analytics,
logger: logger,
generatorFromBundle: (_) async => throw Exception('oops'),
generatorFromBrick: (_) async => generator,
)..argResultOverrides = argResults;
when(() => argResults['output-directory'] as String?)
.thenReturn(tempDir.path);
when(() => argResults.rest).thenReturn(['my_cli']);
when(() => argResults['executable-name'] as String?).thenReturn(
'my_executable',
);

final result = await command.run();

expect(command.template.name, 'dart_cli');
expect(result, equals(ExitCode.success.code));

verify(() => logger.progress('Bootstrapping')).called(1);
verify(
() => hooks.preGen(
vars: <String, dynamic>{
'project_name': 'my_cli',
'description': '',
'publishable': false,
'executable_name': 'my_executable',
},
onVarsChanged: any(named: 'onVarsChanged'),
),
);
verify(
() => generator.generate(
any(),
vars: <String, dynamic>{
'project_name': 'my_cli',
'description': '',
'publishable': false,
'executable_name': 'my_executable',
},
logger: logger,
),
).called(1);
verify(
() => logger.info('Created a Very Good Dart CLI application! 🦄'),
).called(1);
});
});
});
}
1 change: 1 addition & 0 deletions test/src/commands/create/commands/legacy_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Usage: very_good create <subcommand> <project-name> [arguments]
-h, --help Print this usage information.

Available subcommands:
dart_cli Creates a new very good Dart CLI in the specified directory.
dart_package Creates a new very good Dart package in the specified directory.
flutter_app Creates a new very good Flutter app in the specified directory.

Expand Down
1 change: 1 addition & 0 deletions test/src/commands/create/create_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Usage: very_good create <subcommand> <project-name> [arguments]
-h, --help Print this usage information.

Available subcommands:
dart_cli Creates a new very good Dart CLI in the specified directory.
dart_package Creates a new very good Dart package in the specified directory.
flutter_app Creates a new very good Flutter app in the specified directory.

Expand Down
67 changes: 67 additions & 0 deletions test/src/commands/create/e2e/dart_cli/dart_cli_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@Tags(['e2e'])
import 'package:mason/mason.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:universal_io/io.dart';

import '../../../../../helpers/helpers.dart';

void main() {
test(
'create dart_cli',
withRunner((commandRunner, logger, updater, logs) async {
final directory = Directory.systemTemp.createTempSync();

final result = await commandRunner.run(
[
'create',
'dart_cli',
'very_good_dart_cli',
'-o',
directory.path,
],
);
expect(result, equals(ExitCode.success.code));

final formatResult = await Process.run(
'flutter',
['format', '--set-exit-if-changed', '.'],
workingDirectory: path.join(directory.path, 'very_good_dart_cli'),
runInShell: true,
);
expect(formatResult.exitCode, equals(ExitCode.success.code));
expect(formatResult.stderr, isEmpty);

final analyzeResult = await Process.run(
'flutter',
['analyze', '.'],
workingDirectory: path.join(directory.path, 'very_good_dart_cli'),
runInShell: true,
);
expect(analyzeResult.exitCode, equals(ExitCode.success.code));
expect(analyzeResult.stderr, isEmpty);
expect(analyzeResult.stdout, contains('No issues found!'));

final testResult = await Process.run(
'flutter',
['test', '--no-pub', '--coverage'],
workingDirectory: path.join(directory.path, 'very_good_dart_cli'),
runInShell: true,
);
expect(testResult.exitCode, equals(ExitCode.success.code));
expect(testResult.stderr, isEmpty);
expect(testResult.stdout, contains('All tests passed!'));

final testCoverageResult = await Process.run(
'genhtml',
['coverage/lcov.info', '-o', 'coverage'],
workingDirectory: path.join(directory.path, 'very_good_dart_cli'),
runInShell: true,
);
expect(testCoverageResult.exitCode, equals(ExitCode.success.code));
expect(testCoverageResult.stderr, isEmpty);
expect(testCoverageResult.stdout, contains('lines......: 100.0%'));
}),
timeout: const Timeout(Duration(minutes: 2)),
);
}