pipe/test/rule_scope_claim_test.dart
trvon ae412a7964
fix(cli): reject a missing --dir and report filesystem errors cleanly
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.
2026-09-22 12:05:12 -06:00

86 lines
3.1 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:pipe/pipe.dart';
import 'package:test/test.dart';
/// A finding's claim must not be wider than the evidence behind it.
///
/// The dead-handler message used to say "nothing anywhere ever dispatches or
/// constructs X" while the analysis only ever scanned `--dir` (default `lib`).
/// On a real app that overclaim was false for 67 of 77 findings, because their
/// events are constructed by tests - and it nearly justified 77 needless UI
/// wirings, and the deletion of code covered by seeded fuzz tests.
void main() {
test(
'the dead-handler message names the directories actually scanned',
() async {
final project = await loadProject('test/fixtures/stuck_loading');
final analysis = await analyzeProject(project, concurrency: 1);
try {
final findings = analysis.report.findings
.where((finding) => finding.rule == 'dead-handler')
.toList();
expect(findings, isNotEmpty, reason: 'the fixture has dead handlers');
for (final finding in findings) {
expect(
finding.message,
isNot(contains('anywhere')),
reason: 'never claim more than the scanned scope supports',
);
expect(finding.message, contains(project.directories.join(', ')));
}
expect(
findings.first.remediation,
contains('--dir'),
reason:
'the reader should be told how to widen the scan before wiring',
);
} finally {
await analysis.dispose();
}
},
);
test('the scanned directories are recorded on the project', () async {
final defaulted = await loadProject('test/fixtures/stuck_loading');
expect(defaulted.directories, const ['lib']);
// The fixture has no test/ directory, and a --dir that does not exist is
// now an error rather than skipped, so widen a copy that has one.
final temp = Directory.systemTemp.createTempSync('pipe-widened');
Directory('${temp.path}/.dart_tool').createSync();
File('${temp.path}/.dart_tool/package_config.json').writeAsStringSync(
File(
'test/fixtures/stuck_loading/.dart_tool/package_config.json',
).readAsStringSync(),
);
File('${temp.path}/lib/a.dart')
..createSync(recursive: true)
..writeAsStringSync('class A {}\n');
File('${temp.path}/test/a_test.dart')
..createSync(recursive: true)
..writeAsStringSync('void main() {}\n');
final widened = await loadProject(
temp.path,
directories: const ['lib', 'test'],
);
expect(widened.directories, const ['lib', 'test']);
});
test('scanning test/ as well changes the claim set', () async {
// The mechanism behind the overclaim: the same project yields different
// dead-handler findings depending on scope, so the message must name it.
final report = await analyzeProject(
await loadProject('test/fixtures/stuck_loading'),
concurrency: 1,
);
try {
expect(jsonEncode(report.project.directories), jsonEncode(const ['lib']));
} finally {
await report.dispose();
}
});
}