With several --dir values, one that did not exist was skipped silently: the no-files guard only fires when every directory is empty, so `--dir lib --dir tset` scanned lib and reported as though it had covered what was asked for. A --dir that does not exist is now an error naming it. A directory that could not be listed, or a baseline that could not be read or written, escaped as an uncaught FileSystemException: exit 255 and a stack trace. Those paths now raise a PipeException with the OS reason (exit 2), and bin/pipe.dart catches any remaining FileSystemException the same way. The exit codes are documented in bin/pipe.dart: 0 success, 1 a gate failed, 2 pipe could not do what was asked, 64 usage. rule_scope_claim_test widened the fixture with a test/ directory it does not have, relying on exactly the silent skip; it now builds a project that has one.
297 lines
10 KiB
Dart
297 lines
10 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:pipe/pipe.dart';
|
|
import 'package:pipe/src/cli.dart';
|
|
import 'package:pipe/src/index.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/// A directory that is a valid project (it has a package config) but has no Dart
|
|
/// files to analyse - an empty `lib/`, or a `--dir` that does not exist.
|
|
String emptyProject(String label) {
|
|
final temp = Directory.systemTemp.createTempSync('pipe-$label');
|
|
final dartTool = Directory('${temp.path}/.dart_tool')
|
|
..createSync(recursive: true);
|
|
File('${dartTool.path}/package_config.json').writeAsStringSync(
|
|
File(
|
|
'test/fixtures/stuck_loading/.dart_tool/package_config.json',
|
|
).readAsStringSync(),
|
|
);
|
|
Directory('${temp.path}/lib').createSync();
|
|
return temp.path;
|
|
}
|
|
|
|
void main() {
|
|
test('a scan that finds no Dart files fails instead of passing quietly', () {
|
|
// Before this, `pipe check . --dir libs` (a typo) or a project with an empty
|
|
// lib/ analysed zero files and exited 0, so a CI gate "passed" having
|
|
// checked nothing at all. Reporting success for an empty scan is the same
|
|
// class of defect as a test that cannot fail.
|
|
expect(
|
|
() => loadProject(emptyProject('empty-lib')),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(error) => error.toString(),
|
|
'message',
|
|
allOf(contains('no Dart files found'), contains('--dir')),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'a nonexistent --dir fails rather than being skipped silently',
|
|
() async {
|
|
final projectPath = emptyProject('missing-dir');
|
|
await expectLater(
|
|
loadProject(projectPath, directories: const ['missing']),
|
|
throwsA(isA<PipeException>()),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('one mistyped --dir among several is an error, not skipped', () async {
|
|
// With a single --dir the no-files guard caught a typo, but with several a
|
|
// missing one was skipped silently and the scan reported on the others as
|
|
// though it covered what was asked for.
|
|
await expectLater(
|
|
loadProject(
|
|
'test/fixtures/stuck_loading',
|
|
directories: const ['lib', 'tset'],
|
|
),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(error) => error.toString(),
|
|
'message',
|
|
contains('--dir tset does not exist'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('a real project still loads', () async {
|
|
final project = await loadProject('test/fixtures/stuck_loading');
|
|
expect(project.dartFiles, isNotEmpty);
|
|
});
|
|
|
|
group('index text accounting', () {
|
|
IndexReport report({required int discovered, required int selected}) =>
|
|
IndexReport(
|
|
root: '/project',
|
|
sdkPath: null,
|
|
sdkSource: 'test',
|
|
isFlutter: false,
|
|
contextCount: 1,
|
|
filesDiscovered: discovered,
|
|
filesSelected: selected,
|
|
filesResolved: selected,
|
|
filesSkipped: 0,
|
|
filesFailed: 0,
|
|
generatedFiles: 0,
|
|
diagnosticsBySeverity: const {},
|
|
diagnosticsByCode: const {},
|
|
unresolvedUris: 0,
|
|
samples: const [],
|
|
failures: const [],
|
|
wall: Duration.zero,
|
|
perFileMicros: const [],
|
|
concurrency: 1,
|
|
);
|
|
|
|
test('explains the gap when files were filtered out', () {
|
|
// `--no-include-generated` used to print "13 discovered, 1 generated,
|
|
// 0 skipped, 0 failed" beside "12/12 resolved", leaving the 13-vs-12 gap
|
|
// looking like a file had gone missing.
|
|
final text = report(discovered: 13, selected: 12).describe();
|
|
expect(text, contains('13 discovered'));
|
|
expect(text, contains('12 selected'));
|
|
expect(text, contains('12/12 resolved'));
|
|
});
|
|
|
|
test('stays quiet when nothing was filtered', () {
|
|
final text = report(discovered: 12, selected: 12).describe();
|
|
expect(text, contains('12 discovered'));
|
|
expect(text, isNot(contains('12 selected')));
|
|
});
|
|
});
|
|
|
|
group('limited runs are never silent', () {
|
|
test(
|
|
'produce a warning naming the partial coverage, but only when limited',
|
|
() {
|
|
// A truncated analysis used to look exactly like a complete one, and
|
|
// `verify --limit 2` read the unscanned findings as RESOLVED.
|
|
expect(
|
|
limitedRunWarning(limit: null, analysed: 12, discovered: 12),
|
|
isNull,
|
|
);
|
|
|
|
final warning = limitedRunWarning(
|
|
limit: 2,
|
|
analysed: 2,
|
|
discovered: 12,
|
|
);
|
|
expect(warning, isNotNull);
|
|
expect(warning, contains('--limit 2'));
|
|
expect(warning, contains('2 of 12'));
|
|
expect(warning, contains('PARTIAL'));
|
|
},
|
|
);
|
|
|
|
test('files excluded by analysis_options make a scan partial too', () {
|
|
// A skipped file was never examined, exactly like a failed one; naming
|
|
// only failures let a scan with excluded files read as complete.
|
|
final warning = partialScanWarning(failed: 0, resolved: 11, skipped: 2);
|
|
expect(warning, isNotNull);
|
|
expect(warning, contains('2 file(s) were not analysed'));
|
|
expect(warning, contains('PARTIAL'));
|
|
});
|
|
|
|
test('verify refuses a partial scan instead of claiming fixes', () async {
|
|
final runner = PipeRunner();
|
|
await expectLater(
|
|
runner.run([
|
|
'verify',
|
|
'test/fixtures/stuck_loading',
|
|
'--baseline',
|
|
'/tmp/sw2/bl1.json',
|
|
'--limit',
|
|
'2',
|
|
'-c',
|
|
'1',
|
|
]),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(error) => error.toString(),
|
|
'message',
|
|
contains('complete scan'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('nothing to analyse is never a pass', () {
|
|
test('every file filtered out by --no-include-generated is an error', () {
|
|
// loadProject rejects a project with no Dart files at all; this is the
|
|
// other half - files WERE discovered but the generated filter removed them
|
|
// all, so the old code analysed nothing and exited 0.
|
|
final temp = Directory.systemTemp.createTempSync('pipe-onlygen');
|
|
final dartTool = Directory('${temp.path}/.dart_tool')
|
|
..createSync(recursive: true);
|
|
File('${dartTool.path}/package_config.json').writeAsStringSync(
|
|
File(
|
|
'test/fixtures/stuck_loading/.dart_tool/package_config.json',
|
|
).readAsStringSync(),
|
|
);
|
|
final lib = Directory('${temp.path}/lib')..createSync();
|
|
File(
|
|
'${lib.path}/thing.g.dart',
|
|
).writeAsStringSync('class Generated {}\n');
|
|
|
|
expect(
|
|
() => resolveProject(
|
|
Project(
|
|
root: temp.path,
|
|
packageConfigPath: '',
|
|
packageConfig: const {},
|
|
flutterRoot: null,
|
|
dartFiles: ['${lib.path}/thing.g.dart'],
|
|
),
|
|
includeGenerated: false,
|
|
),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(error) => error.toString(),
|
|
'message',
|
|
contains('nothing to analyse'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('unreadable files are not a clean result', () {
|
|
test(
|
|
'partialScanWarning reports partial coverage, or nothing when clean',
|
|
() {
|
|
expect(partialScanWarning(failed: 0, resolved: 12), isNull);
|
|
final warning = partialScanWarning(failed: 1, resolved: 11);
|
|
expect(warning, isNotNull);
|
|
expect(warning, contains('1 file(s) could not be analysed'));
|
|
expect(warning, contains('11 resolved'));
|
|
expect(warning, contains('PARTIAL'));
|
|
},
|
|
);
|
|
|
|
test('files excluded by analysis_options make a scan partial too', () {
|
|
// A skipped file was never examined, exactly like a failed one; naming
|
|
// only failures let a scan with excluded files read as complete.
|
|
final warning = partialScanWarning(failed: 0, resolved: 11, skipped: 2);
|
|
expect(warning, isNotNull);
|
|
expect(warning, contains('2 file(s) were not analysed'));
|
|
expect(warning, contains('PARTIAL'));
|
|
});
|
|
|
|
test('a run where every file fails to resolve is an error', () async {
|
|
// An unreadable file comes back from the analyzer as a unit with EMPTY
|
|
// content rather than an error, so it used to look like a successful
|
|
// resolution: a project of 12 unreadable files reported "12/12 resolved"
|
|
// and `check` said 0 findings, indistinguishable from a clean project.
|
|
final temp = Directory.systemTemp.createTempSync('pipe-unreadable');
|
|
Directory('${temp.path}/lib').createSync();
|
|
|
|
await expectLater(
|
|
resolveProject(
|
|
Project(
|
|
root: temp.path,
|
|
packageConfigPath: '',
|
|
packageConfig: const {},
|
|
flutterRoot: null,
|
|
// A path that cannot be read: the same shape as an unreadable file
|
|
// or one deleted between discovery and resolution.
|
|
dartFiles: ['${temp.path}/lib/gone.dart'],
|
|
),
|
|
concurrency: 1,
|
|
),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(error) => error.toString(),
|
|
'message',
|
|
contains('no file could be analysed'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
test(
|
|
'a run whose every file sits outside any context is an error too',
|
|
() async {
|
|
// The guard used to fire only when files FAILED, so a run where every file
|
|
// was SKIPPED (outside every analysis context) could still report success
|
|
// having analysed nothing. The test is now simply "nothing was resolved".
|
|
final temp = Directory.systemTemp.createTempSync('pipe-outside-context');
|
|
await expectLater(
|
|
resolveProject(
|
|
Project(
|
|
root: temp.path,
|
|
packageConfigPath: '',
|
|
packageConfig: const {},
|
|
flutterRoot: null,
|
|
// Outside the project root, so no analysis context covers it: the
|
|
// analyzer reports this as skipped, not failed.
|
|
dartFiles: const ['/etc/hosts'],
|
|
),
|
|
concurrency: 1,
|
|
),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(error) => error.toString(),
|
|
'message',
|
|
contains('no file could be analysed'),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|