From ce5d1de2d57cff09366c9ca3401a3f82e319a257 Mon Sep 17 00:00:00 2001 From: Damien LeBerrigaud Date: Fri, 6 Oct 2023 09:37:14 -0600 Subject: [PATCH] Change command line arguments. --path or -p to specify the path to the module --max-depth or -d to specify the max depth (previously unavailable) --- Makefile | 6 +- README.md | 5 + bin/cyclic_dependency_checks.dart | 28 +----- .../cycle_detector_runner.dart | 86 ++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 2 + test/cycle_detector_runner_test.dart | 98 +++++++++++++++++++ 7 files changed, 198 insertions(+), 29 deletions(-) create mode 100644 lib/cycle_detection/cycle_detector_runner.dart create mode 100644 test/cycle_detector_runner_test.dart diff --git a/Makefile b/Makefile index f7f08d3..543a7a5 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,6 @@ install: dart pub get check: - dart format lib --line-length 100 --set-exit-if-changed; - dart scripts/generate_big_codebase.dart; - dart test; + dart format lib --line-length 100 --set-exit-if-changed + dart scripts/generate_big_codebase.dart + dart test diff --git a/README.md b/README.md index f8929cb..e05760e 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,8 @@ Again, from inside your Dart project ``` dart run cyclic_dependency_checks ``` + +## Command-Line flags + + * `--path` or `-p` for the path to the module that is checked, defaults to current working directory. + * `--max-depth` or `-d` to limit the depth of the check, defaults to no max depth. diff --git a/bin/cyclic_dependency_checks.dart b/bin/cyclic_dependency_checks.dart index 6ddd418..cc5401f 100644 --- a/bin/cyclic_dependency_checks.dart +++ b/bin/cyclic_dependency_checks.dart @@ -1,33 +1,11 @@ import 'dart:io'; -import 'package:cyclic_dependency_checks/cycle_detection/cycle_detector.dart'; -import 'package:cyclic_dependency_checks/cycle_detection/module_dependency.dart'; -import 'package:cyclic_dependency_checks/cycle_detection/module_dependency_graph.dart'; +import 'package:cyclic_dependency_checks/cycle_detection/cycle_detector_runner.dart'; void main(List args) async { - await _run(path: args.firstOrNull ?? '.'); -} - -Future _run({required String path}) async { - final stopwatch = Stopwatch()..start(); - final cycles = await CycleDetector().detect(path); - stopwatch.stop(); + final success = await CycleDetectorRunner().run(args); - final formattedTime = stopwatch.elapsed.toString().substring(0, 11); - - if (cycles.isNotEmpty) { - stderr.writeln('Detected cycles after ${formattedTime}'); - for (final cycle in cycles) { - cycle.printError(); - } + if (!success) { exit(1); } - - stdout.writeln('No import cycles were detected after ${formattedTime}'); -} - -extension ErrorPrinting on List { - void printError() { - stderr.writeln(path().join(' -> ')); - } } diff --git a/lib/cycle_detection/cycle_detector_runner.dart b/lib/cycle_detection/cycle_detector_runner.dart new file mode 100644 index 0000000..ef48d75 --- /dev/null +++ b/lib/cycle_detection/cycle_detector_runner.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:args/args.dart'; + +import 'cycle_detector.dart'; +import 'module_dependency_graph.dart'; + +class Printer { + out(String text) => stdout.writeln(text); + + err(String text) => stderr.writeln(text); +} + +class CycleDetectorRunner { + final CycleDetector detector; + final Printer printer; + + CycleDetectorRunner({ + CycleDetector? detector, + Printer? printer, + }) : detector = detector ?? CycleDetector(), + printer = printer ?? Printer(); + + Future run(List args) async { + final parser = ArgParser() + ..addOption('path', abbr: 'p') + ..addOption('max-depth', abbr: 'd'); + + final parsedArgs = parser.tryParse(args); + if (parsedArgs == null) { + return false; + } + + final path = parsedArgs.getString('path', fallback: '.'); + final maxDepth = parsedArgs.tryGetInt('max-depth'); + + final stopwatch = Stopwatch()..start(); + final cycles = await detector.detect(path, maxDepth: maxDepth); + + stopwatch.stop(); + + final formattedTime = stopwatch.elapsed.toString().substring(0, 11); + + if (cycles.isNotEmpty) { + printer.err('Detected cycles after ${formattedTime}'); + + for (final cycle in cycles) { + printer.err(cycle.path().join(' -> ')); + } + + return false; + } + + printer.out('No import cycles were detected after ${formattedTime}'); + + return true; + } +} + +extension _SafeParse on ArgParser { + ArgResults? tryParse(List args, {Printer? printer}) { + try { + return parse(args); + } catch (e) { + printer?.err('Failed to parse arguments'); + printer?.err(usage); + return null; + } + } +} + +extension _TypeSafeArgs on ArgResults { + String getString(String name, {required String fallback}) { + return this[name] ?? fallback; + } + + int? tryGetInt(String name) { + final value = this[name]; + + if (value == null) { + return null; + } + + return int.tryParse(value); + } +} diff --git a/pubspec.lock b/pubspec.lock index 81fb6f1..a02281a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "6.1.0" args: - dependency: transitive + dependency: "direct main" description: name: args sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 diff --git a/pubspec.yaml b/pubspec.yaml index 002253e..e337d81 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,3 +7,5 @@ environment: dev_dependencies: test: ^1.24.5 +dependencies: + args: ^2.4.2 diff --git a/test/cycle_detector_runner_test.dart b/test/cycle_detector_runner_test.dart new file mode 100644 index 0000000..b7d0652 --- /dev/null +++ b/test/cycle_detector_runner_test.dart @@ -0,0 +1,98 @@ +import 'package:cyclic_dependency_checks/cycle_detection/cycle_detector.dart'; +import 'package:cyclic_dependency_checks/cycle_detection/cycle_detector_runner.dart'; +import 'package:cyclic_dependency_checks/cycle_detection/module_dependency.dart'; +import 'package:test/test.dart'; + +class TestCycleDetector extends CycleDetector { + List> stubbedCycles = []; + String? recordedPackagePath; + int? recordedMaxDepth; + + @override + Future>> detect(String packagePath, {int? maxDepth}) async { + recordedPackagePath = packagePath; + recordedMaxDepth = maxDepth; + return stubbedCycles; + } +} + +class RecordingPrinter extends Printer { + final List outLog = []; + final List errLog = []; + + out(String text) => outLog.add(text); + + err(String text) => errLog.add(text); +} + +void main() { + test('with no arguments, on success', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + final success = await runner.run([]); + + expect(success, isTrue); + expect(printer.errLog, isEmpty); + expect(detector.recordedPackagePath, equals('.')); + expect(detector.recordedMaxDepth, isNull); + }); + + test('with no arguments, on error', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector() + ..stubbedCycles.add([ + ModuleDependency(from: Module('a'), to: Module('b')), + ModuleDependency(from: Module('b'), to: Module('a')), + ]); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + final success = await runner.run([]); + + expect(success, isFalse); + expect(printer.errLog, contains('module:a -> module:b -> module:a')); + }); + + test('with full length args', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + await runner.run([ + '--path', '/some/path', + '--max-depth', '10', + ]); + + expect(detector.recordedPackagePath, equals('/some/path')); + expect(detector.recordedMaxDepth, equals(10)); + }); + + test('with short name args', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + await runner.run([ + '-p', '/some/path', + '-d', '10', + ]); + + expect(detector.recordedPackagePath, equals('/some/path')); + expect(detector.recordedMaxDepth, equals(10)); + }); + + test('with invalid arguments', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + final success = await runner.run([ + '--oops' + ]); + + expect(success, equals(false)); + expect(detector.recordedPackagePath, isNull); + expect(detector.recordedMaxDepth, isNull); + }); +}