Line data Source code
1 : import 'dart:async';
2 : import 'package:collection/collection.dart';
3 : import 'package:glob/glob.dart';
4 : import 'package:lcov_parser/lcov_parser.dart';
5 : import 'package:mason/mason.dart';
6 : import 'package:meta/meta.dart';
7 : import 'package:path/path.dart' as p;
8 : import 'package:pubspec_parse/pubspec_parse.dart';
9 : import 'package:universal_io/io.dart';
10 : import 'package:very_good_cli/src/commands/test/templates/test_optimizer_bundle.dart';
11 : import 'package:very_good_test_runner/very_good_test_runner.dart';
12 :
13 : part 'dart_cli.dart';
14 :
15 : part 'flutter_cli.dart';
16 :
17 : part 'git_cli.dart';
18 :
19 : const _asyncRunZoned = runZoned;
20 :
21 : /// Type definition for [Process.run].
22 : typedef RunProcess = Future<ProcessResult> Function(
23 : String executable,
24 : List<String> arguments, {
25 : String? workingDirectory,
26 : bool runInShell,
27 : });
28 :
29 : /// This class facilitates overriding [Process.run].
30 : /// It should be extended by another class in client code with overrides
31 : /// that construct a custom implementation.
32 : @visibleForTesting
33 : abstract class ProcessOverrides {
34 3 : static final _token = Object();
35 :
36 : /// Returns the current [ProcessOverrides] instance.
37 : ///
38 : /// This will return `null` if the current [Zone] does not contain
39 : /// any [ProcessOverrides].
40 : ///
41 : /// See also:
42 : /// * [ProcessOverrides.runZoned] to provide [ProcessOverrides]
43 : /// in a fresh [Zone].
44 : ///
45 1 : static ProcessOverrides? get current {
46 3 : return Zone.current[_token] as ProcessOverrides?;
47 : }
48 :
49 : /// Runs [body] in a fresh [Zone] using the provided overrides.
50 1 : static R runZoned<R>(
51 : R Function() body, {
52 : RunProcess? runProcess,
53 : }) {
54 1 : final overrides = _ProcessOverridesScope(runProcess);
55 3 : return _asyncRunZoned(body, zoneValues: {_token: overrides});
56 : }
57 :
58 : /// The method used to run a [Process].
59 1 : RunProcess get runProcess => Process.run;
60 : }
61 :
62 : class _ProcessOverridesScope extends ProcessOverrides {
63 1 : _ProcessOverridesScope(this._runProcess);
64 :
65 : final ProcessOverrides? _previous = ProcessOverrides.current;
66 : final RunProcess? _runProcess;
67 :
68 1 : @override
69 : RunProcess get runProcess {
70 4 : return _runProcess ?? _previous?.runProcess ?? super.runProcess;
71 : }
72 : }
73 :
74 : /// Abstraction for running commands via command-line.
75 : class _Cmd {
76 : /// Runs the specified [cmd] with the provided [args].
77 1 : static Future<ProcessResult> run(
78 : String cmd,
79 : List<String> args, {
80 : required Logger logger,
81 : bool throwOnError = true,
82 : String? workingDirectory,
83 : }) async {
84 2 : logger.detail('Running: $cmd with $args');
85 2 : final runProcess = ProcessOverrides.current?.runProcess ?? Process.run;
86 1 : final result = await runProcess(
87 : cmd,
88 : args,
89 : workingDirectory: workingDirectory,
90 : runInShell: true,
91 : );
92 : logger
93 3 : ..detail('stdout:\n${result.stdout}')
94 3 : ..detail('stderr:\n${result.stderr}');
95 :
96 : if (throwOnError) {
97 1 : _throwIfProcessFailed(result, cmd, args);
98 : }
99 : return result;
100 : }
101 :
102 1 : static Iterable<Future<T>> runWhere<T>({
103 : required Future<T> Function(FileSystemEntity) run,
104 : required bool Function(FileSystemEntity) where,
105 : String cwd = '.',
106 : }) {
107 : final directories =
108 4 : Directory(cwd).listSync(recursive: true).where(where).toList()
109 2 : ..sort((a, b) {
110 : /// Linux and macOS have different sorting behaviors
111 : /// regarding the order that the list of folders/files are returned.
112 : /// To ensure consistency across platforms, we apply a
113 : /// uniform sorting logic.
114 2 : final aSplit = p.split(a.path);
115 2 : final bSplit = p.split(b.path);
116 1 : final aLevel = aSplit.length;
117 1 : final bLevel = bSplit.length;
118 :
119 1 : if (aLevel == bLevel) {
120 3 : return aSplit.last.compareTo(bSplit.last);
121 : } else {
122 1 : return aLevel.compareTo(bLevel);
123 : }
124 : });
125 :
126 1 : return directories.map(run);
127 : }
128 :
129 1 : static void _throwIfProcessFailed(
130 : ProcessResult pr,
131 : String process,
132 : List<String> args,
133 : ) {
134 2 : if (pr.exitCode != 0) {
135 1 : final values = {
136 3 : 'Standard out': pr.stdout.toString().trim(),
137 3 : 'Standard error': pr.stderr.toString().trim(),
138 3 : }..removeWhere((k, v) => v.isEmpty);
139 :
140 : var message = 'Unknown error';
141 1 : if (values.isNotEmpty) {
142 7 : message = values.entries.map((e) => '${e.key}\n${e.value}').join('\n');
143 : }
144 :
145 2 : throw ProcessException(process, args, message, pr.exitCode);
146 : }
147 : }
148 : }
149 :
150 : const _ignoredDirectories = {
151 : 'ios',
152 : 'android',
153 : 'windows',
154 : 'linux',
155 : 'macos',
156 : '.symlinks',
157 : '.plugin_symlinks',
158 : '.dart_tool',
159 : 'build',
160 : '.fvm',
161 : };
162 :
163 1 : bool _isPubspec(FileSystemEntity entity) {
164 1 : if (entity is! File) return false;
165 3 : return p.basename(entity.path) == 'pubspec.yaml';
166 : }
167 :
168 : // The extension is intended to be unnamed, but it's not possible due to
169 : // an issue with Dart SDK 2.18.0.
170 : //
171 : // Once the min Dart SDK is bumped, this extension can be unnamed again.
172 : extension _Set on Set<String> {
173 1 : bool excludes(FileSystemEntity entity) {
174 3 : final segments = p.split(entity.path).toSet();
175 2 : if (segments.intersection(_ignoredDirectories).isNotEmpty) return true;
176 2 : if (segments.intersection(this).isNotEmpty) return true;
177 :
178 2 : for (final value in this) {
179 4 : if (value.isNotEmpty && Glob(value).matches(entity.path)) {
180 : return true;
181 : }
182 : }
183 :
184 : return false;
185 : }
186 : }
|