-
Notifications
You must be signed in to change notification settings - Fork 238
Expand file tree
/
Copy pathflutter_cli.dart
More file actions
356 lines (314 loc) · 10 KB
/
Copy pathflutter_cli.dart
File metadata and controls
356 lines (314 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
part of 'cli.dart';
const _testOptimizerFileName = '.test_optimizer.dart';
/// This class facilitates overriding `ProcessSignal` related behavior.
/// It should be extended by another class in client code with overrides
/// that construct a custom implementation.
@visibleForTesting
abstract class ProcessSignalOverrides {
static final _token = Object();
StreamController<ProcessSignal>? _sigintStreamController;
/// Returns the current [ProcessSignalOverrides] instance.
///
/// This will return `null` if the current [Zone] does not contain
/// any [ProcessSignalOverrides].
///
/// See also:
/// * [ProcessSignalOverrides.runZoned] to provide [ProcessSignalOverrides]
/// in a fresh [Zone].
static ProcessSignalOverrides? get current {
return Zone.current[_token] as ProcessSignalOverrides?;
}
/// Runs [body] in a fresh [Zone] using the provided overrides.
static R runZoned<R>(
R Function() body, {
Stream<ProcessSignal>? sigintStream,
}) {
final overrides = _ProcessSignalOverridesScope(sigintStream);
return _asyncRunZoned(body, zoneValues: {_token: overrides});
}
/// Provides a custom [Stream] of [ProcessSignal.sigint] events.
Stream<ProcessSignal>? get sigintWatch;
/// Emits a [ProcessSignal.sigint] event on the [sigintWatch] stream.
///
/// If no custom [sigintWatch] stream is provided, this method does nothing.
void addSIGINT() {
_sigintStreamController?.add(ProcessSignal.sigint);
}
}
class _ProcessSignalOverridesScope extends ProcessSignalOverrides {
_ProcessSignalOverridesScope(Stream<ProcessSignal>? mockSigintStream) {
if (mockSigintStream != null) {
_sigintStreamController = StreamController<ProcessSignal>();
}
}
@override
Stream<ProcessSignal>? get sigintWatch {
return _sigintStreamController?.stream;
}
}
/// Thrown when `flutter pub get` is executed without a `pubspec.yaml`.
class PubspecNotFound implements Exception {}
/// {@template coverage_metrics}
/// Aggregated coverage metrics computed from a list of LCOV records.
/// {@endtemplate}
class CoverageMetrics {
/// {@macro coverage_metrics}
@visibleForTesting
const CoverageMetrics({
this.totalHits = 0,
this.totalFound = 0,
this.uncoveredLines = const {},
});
/// Generate coverage metrics from a list of lcov records.
factory CoverageMetrics.fromLcovRecords(
List<Record> records, {
String? excludeFromCoverage,
}) {
final globs = <Glob>[];
if (excludeFromCoverage != null && excludeFromCoverage.isNotEmpty) {
for (final glob in excludeFromCoverage.trim().split(' ')) {
if (glob.isNotEmpty) globs.add(Glob(glob));
}
}
return records.fold<CoverageMetrics>(const CoverageMetrics(), (
current,
record,
) {
final found = record.lines?.found ?? 0;
final hit = record.lines?.hit ?? 0;
if (globs.isNotEmpty && record.file != null) {
for (final glob in globs) {
if (glob.matches(record.file!)) return current;
}
}
final file = record.file;
final details = record.lines?.details;
final uncoveredLines = Map<String, List<int>>.from(
current.uncoveredLines,
);
if (file != null && details != null) {
for (final line in details) {
if ((line.hit ?? 1) == 0 && line.line != null) {
uncoveredLines.update(
file,
(lines) => [...lines, line.line!],
ifAbsent: () => [line.line!],
);
}
}
}
return CoverageMetrics(
totalFound: current.totalFound + found,
totalHits: current.totalHits + hit,
uncoveredLines: uncoveredLines,
);
});
}
/// Total number of lines hit (covered) across all included files.
final int totalHits;
/// Total number of instrumented lines found across all included files.
final int totalFound;
/// Lines not covered.
/// Keyed by file path, values are sorted line numbers.
final Map<String, List<int>> uncoveredLines;
/// Coverage percentage: [totalHits] / [totalFound] * 100.
///
/// Returns `0` when [totalFound] is less than 1.
double get percentage {
return totalFound < 1 ? 0 : (totalHits / totalFound * 100);
}
}
/// Flutter CLI
class Flutter {
/// Determine whether flutter is installed.
static Future<bool> installed({required Logger logger}) async {
try {
await _Cmd.run('flutter', ['--version'], logger: logger);
return true;
} on Exception catch (_) {
return false;
}
}
/// Install dart dependencies (`flutter pub get`).
static Future<bool> pubGet({
required Logger logger,
String cwd = '.',
bool recursive = false,
Set<String> ignore = const {},
Duration timeout = _pubGetTimeout,
}) async {
final initialCwd = cwd;
final result = await _runCommand(
cmd: (cwd) async {
final relativePath = p.relative(cwd, from: initialCwd);
final path = relativePath == '.'
? '.'
: '.${p.context.separator}$relativePath';
final installProgress = logger.progress(
'Running "flutter pub get" in $path ',
);
try {
await _verifyGitDependencies(cwd, logger: logger);
} catch (_) {
installProgress.fail();
rethrow;
}
try {
return await _Cmd.run(
'flutter',
['pub', 'get', '--no-example'],
workingDirectory: cwd,
logger: logger,
timeout: timeout,
);
} finally {
installProgress.complete();
}
},
cwd: cwd,
recursive: recursive,
ignore: ignore,
);
return result.every((e) => e.exitCode == ExitCode.success.code);
}
/// Run tests (`flutter test`).
/// Returns a list of exit codes for each test process.
static Future<List<int>> test({
required Logger logger,
String cwd = '.',
bool recursive = false,
bool collectCoverage = false,
bool optimizePerformance = false,
Set<String> ignore = const {},
double? minCoverage,
bool showUncovered = false,
String? excludeFromCoverage,
CoverageCollectionMode collectCoverageFrom = CoverageCollectionMode.imports,
String? randomSeed,
bool? forceAnsi,
List<String>? arguments,
void Function(String)? stdout,
void Function(String)? stderr,
GeneratorBuilder buildGenerator = MasonGenerator.fromBundle,
List<String>? reportOn,
}) async {
return TestCLIRunner.test(
logger: logger,
testType: TestRunType.flutter,
cwd: cwd,
recursive: recursive,
collectCoverage: collectCoverage,
optimizePerformance: optimizePerformance,
ignore: ignore,
minCoverage: minCoverage,
showUncovered: showUncovered,
excludeFromCoverage: excludeFromCoverage,
collectCoverageFrom: collectCoverageFrom,
randomSeed: randomSeed,
forceAnsi: forceAnsi,
arguments: arguments,
stdout: stdout,
stderr: stderr,
buildGenerator: buildGenerator,
reportOn: reportOn,
);
}
}
/// Ensures all git dependencies are reachable for the pubspec
/// located in the [cwd].
///
/// If any git dependencies are unreachable,
/// an [UnreachableGitDependency] is thrown.
Future<void> _verifyGitDependencies(
String cwd, {
required Logger logger,
}) async {
final pubspec = Pubspec.parse(
await File(p.join(cwd, 'pubspec.yaml')).readAsString(),
);
final dependencies = pubspec.dependencies;
final devDependencies = pubspec.devDependencies;
final dependencyOverrides = pubspec.dependencyOverrides;
final gitDependencies =
[
...dependencies.entries,
...devDependencies.entries,
...dependencyOverrides.entries,
]
.where((entry) => entry.value is GitDependency)
.map((entry) => entry.value)
.cast<GitDependency>()
.toList();
await Future.wait(
gitDependencies.map(
(dependency) => Git.reachable(dependency.url, logger: logger),
),
);
}
/// Run a command on directories with a `pubspec.yaml`.
Future<List<T>> _runCommand<T>({
required Future<T> Function(String cwd) cmd,
required String cwd,
required bool recursive,
required Set<String> ignore,
}) async {
if (!recursive) {
final pubspec = File(p.join(cwd, 'pubspec.yaml'));
if (!pubspec.existsSync()) throw PubspecNotFound();
return [await cmd(cwd)];
}
final processes = _Cmd.runWhere<T>(
run: (entity) => cmd(entity.parent.path),
where: (entity) => !ignore.excludes(entity) && _isPubspec(entity),
cwd: cwd,
);
if (processes.isEmpty) throw PubspecNotFound();
final results = <T>[];
for (final process in processes) {
results.add(await process);
}
return results;
}
// The extension is intended to be unnamed, but it's not possible due to
// an issue with Dart SDK 2.18.0.
//
// Once the min Dart SDK is bumped, this extension can be unnamed again.
extension _TestEvent on TestEvent {
bool shouldCancelTimer() {
final event = this;
if (event is MessageTestEvent) return true;
if (event is ErrorTestEvent) return true;
if (event is DoneTestEvent) return true;
if (event is TestDoneEvent) return !event.hidden;
return false;
}
}
extension on Duration {
String formatted() {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final twoDigitMinutes = twoDigits(inMinutes.remainder(60));
final twoDigitSeconds = twoDigits(inSeconds.remainder(60));
return darkGray.wrap('$twoDigitMinutes:$twoDigitSeconds')!;
}
}
extension on int {
String formatSuccess() {
return this > 0 ? lightGreen.wrap('+$this')! : '';
}
String formatFailure() {
return this > 0 ? lightRed.wrap('-$this')! : '';
}
String formatSkipped() {
return this > 0 ? lightYellow.wrap('~$this')! : '';
}
}
extension on String {
String truncated(int maxLength) {
if (length <= maxLength) return this;
final truncated = substring(length - maxLength, length).trim();
return '...$truncated';
}
String toSingleLine() {
return replaceAll('\n', '').replaceAll(RegExp(r'\s\s+'), ' ');
}
}