Skip to content
21 changes: 20 additions & 1 deletion lib/src/cli/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,26 @@ class _Cmd {
required bool Function(FileSystemEntity) where,
String cwd = '.',
}) {
return Directory(cwd).listSync(recursive: true).where(where).map(run);
final directories =
Directory(cwd).listSync(recursive: true).where(where).toList()
..sort((a, b) {
/// Linux and macOS have different sorting behaviors
/// regarding the order that the list of folders/files are returned.
/// To ensure consistency across platforms, we apply a
/// uniform sorting logic.
final aSplit = p.split(a.path);
final bSplit = p.split(b.path);
final aLevel = aSplit.length;
final bLevel = bSplit.length;

if (aLevel == bLevel) {
return aSplit.last.compareTo(bSplit.last);
} else {
return aLevel.compareTo(bLevel);
}
});

return directories.map(run);
}

static void _throwIfProcessFailed(
Expand Down
57 changes: 30 additions & 27 deletions lib/src/cli/flutter_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@ class Flutter {
FlutterTestRunner testRunner = flutterTest,
GeneratorBuilder buildGenerator = MasonGenerator.fromBundle,
}) async {
final lcovPath = p.join(cwd, 'coverage', 'lcov.info');
final lcovFile = File(lcovPath);
return _runCommand<int>(
cmd: (cwd) async {
final lcovPath = p.join(cwd, 'coverage', 'lcov.info');
final lcovFile = File(lcovPath);

if (collectCoverage && lcovFile.existsSync()) {
await lcovFile.delete();
}
if (collectCoverage && lcovFile.existsSync()) {
await lcovFile.delete();
}

final results = await _runCommand<int>(
cmd: (cwd) async {
void noop(String? _) {}
final target = DirectoryGeneratorTarget(Directory(p.normalize(cwd)));
final workingDirectory = target.dir.absolute.path;
Expand Down Expand Up @@ -211,30 +211,33 @@ class Flutter {
],
stdout: stdout ?? noop,
stderr: stderr ?? noop,
),
).whenComplete(() {
if (optimizePerformance) {
File(p.join(cwd, 'test', '.test_optimizer.dart')).delete().ignore();
}
});
).whenComplete(() async {
if (optimizePerformance) {
File(p.join(cwd, 'test', '.test_optimizer.dart'))
.delete()
.ignore();
}

if (collectCoverage) {
assert(lcovFile.existsSync(), 'coverage/lcov.info must exist');
}

if (minCoverage != null) {
final records = await Parser.parse(lcovPath);
final coverageMetrics = _CoverageMetrics.fromLcovRecords(
records,
excludeFromCoverage,
);
final coverage = coverageMetrics.percentage;

if (coverage < minCoverage) throw MinCoverageNotMet(coverage);
}
}),
);
},
cwd: cwd,
recursive: recursive,
);

if (collectCoverage) {
assert(lcovFile.existsSync(), 'coverage/lcov.info must exist');
}
if (minCoverage != null) {
final records = await Parser.parse(lcovPath);
final coverageMetrics = _CoverageMetrics.fromLcovRecords(
records,
excludeFromCoverage,
);
final coverage = coverageMetrics.percentage;
if (coverage < minCoverage) throw MinCoverageNotMet(coverage);
}
return results;
}

static T _overrideAnsiOutput<T>(bool? enableAnsiOutput, T Function() body) =>
Expand Down
124 changes: 124 additions & 0 deletions test/src/cli/flutter_cli_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,130 @@ void main() {
directory.delete(recursive: true).ignore();
});

test(
'runs tests w/coverage + min-coverage 100 + recursive (pass)',
() async {
final directory = Directory.systemTemp.createTempSync();
File(p.join(directory.path, 'pubspec.yaml')).createSync();
Directory(p.join(directory.path, 'test')).createSync();

final nestedDirectory = Directory(p.join(directory.path, 'test'))
..createSync();
File(p.join(nestedDirectory.path, 'pubspec.yaml')).createSync();
Directory(p.join(nestedDirectory.path, 'test')).createSync();

await expectLater(
Flutter.test(
cwd: directory.path,
collectCoverage: true,
minCoverage: 100,
recursive: true,
stdout: stdoutLogs.add,
stderr: stderrLogs.add,
testRunner: testRunner(
Stream.fromIterable(
[
const DoneTestEvent(success: true, time: 0),
const ExitTestEvent(exitCode: 0, time: 0),
],
),
onStart: () {
File(p.join(directory.path, 'coverage', 'lcov.info'))
..createSync(recursive: true)
..writeAsStringSync(lcov100);
File(p.join(nestedDirectory.path, 'coverage', 'lcov.info'))
..createSync(recursive: true)
..writeAsStringSync(lcov100);
},
),
logger: logger,
),
completion(equals([ExitCode.success.code, ExitCode.success.code])),
);

expect(
stdoutLogs,
unorderedEquals([
'Running "flutter test" in '
'${p.dirname(nestedDirectory.path)}...\n',
contains('All tests passed!'),
'Running "flutter test" in ${p.dirname(directory.path)}...\n',
contains('All tests passed!'),
]),
);
expect(testRunnerArgs, equals(['--coverage', '--coverage']));

directory.delete(recursive: true).ignore();
nestedDirectory.delete(recursive: true).ignore();
},
);

test(
'runs tests w/coverage + min-coverage 100 + recursive (fail)',
() async {
final directory = Directory.systemTemp.createTempSync();
File(p.join(directory.path, 'pubspec.yaml')).createSync();
Directory(p.join(directory.path, 'test')).createSync();

final nestedDirectory = Directory(p.join(directory.path, 'test'))
..createSync();
File(p.join(nestedDirectory.path, 'pubspec.yaml')).createSync();
Directory(p.join(nestedDirectory.path, 'test')).createSync();

await expectLater(
Flutter.test(
cwd: directory.path,
collectCoverage: true,
minCoverage: 100,
recursive: true,
stdout: stdoutLogs.add,
stderr: stderrLogs.add,
testRunner: testRunner(
Stream.fromIterable(
[
const DoneTestEvent(success: true, time: 0),
const ExitTestEvent(exitCode: 0, time: 0),
],
),
onStart: () {
File(p.join(directory.path, 'coverage', 'lcov.info'))
..createSync(recursive: true)
..writeAsStringSync(lcov100);
File(p.join(nestedDirectory.path, 'coverage', 'lcov.info'))
..createSync(recursive: true)
..writeAsStringSync(lcov95);
},
),
logger: logger,
),
throwsA(
isA<MinCoverageNotMet>().having(
(e) => e.coverage,
'coverage',
95.0,
),
),
);

expect(
stdoutLogs,
unorderedEquals([
'Running "flutter test" in '
'${p.dirname(directory.path)}...\n',
contains('All tests passed!'),
'Running "flutter test" in '
'${p.dirname(nestedDirectory.path)}...\n',
contains('All tests passed!'),
]),
);
expect(stderrLogs, isEmpty);
expect(testRunnerArgs, equals(['--coverage', '--coverage']));

directory.delete(recursive: true).ignore();
nestedDirectory.delete(recursive: true).ignore();
},
);

test('runs tests w/optimizations (passing)', () async {
final directory = Directory.systemTemp.createTempSync();
final originalVars = <String, dynamic>{'package-root': directory.path};
Expand Down