The fingerprint is documented as surviving line shifts, but eight of the fixture's findings carried a line number, so moving code down a file turned accepted findings into new ones on the next ratcheted check: - dormant emitted-not-rendered (keyed by union + owner, neither counted as identifying, so the line was appended) - notifier-inert-consumer and notifier-field-unread (owner only) - loading-not-reset, whose `catchLine` key is a line by another name `union` and `owner` now count as identifying (each of those rules emits at most one finding per union or notifier per file), and loading-not-reset keys on the catch's ordinal within its member. Its `on<E>` handler closures are also named `on<E>` like loading-flag-not-reset's, so the ordinal cannot collide across handlers. The existing move test only bumped `line`, not `catchLine` in the data, so it passed while real moves did not. fingerprint_test.dart now shifts every file of the fixture for real and requires (a) every fingerprint to survive and (b) no two findings to share one. (a) fails on the previous rules.dart with exactly the eight findings above; (b) passed there too, and guards the ordinal and closure naming that replace the line. Baselines written before this change see the affected findings once as new; re-accept them with --write-baseline. No committed baselines exist in the dogfood repos.
84 lines
2.8 KiB
Dart
84 lines
2.8 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:pipe/pipe.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/// The fixture copied with [padding] blank lines inserted at the top of every
|
|
/// Dart source, so every finding moves down without any semantic change.
|
|
String _shiftedFixture(int padding) {
|
|
final from = Directory('test/fixtures/stuck_loading');
|
|
final to = Directory.systemTemp.createTempSync('pipe-shifted');
|
|
for (final entity in from.listSync(recursive: true)) {
|
|
final relative = entity.path.substring(from.path.length + 1);
|
|
final target = '${to.path}/$relative';
|
|
if (entity is Directory) {
|
|
Directory(target).createSync(recursive: true);
|
|
} else if (entity is File) {
|
|
File(target).createSync(recursive: true);
|
|
if (entity.path.endsWith('.dart')) {
|
|
// After any `library;` / import header would change nothing; a comment
|
|
// block at the very top shifts every line in the file.
|
|
File(target).writeAsStringSync(
|
|
'${List.filled(padding, '//').join('\n')}\n'
|
|
'${entity.readAsStringSync()}',
|
|
);
|
|
} else {
|
|
File(target).writeAsBytesSync(entity.readAsBytesSync());
|
|
}
|
|
}
|
|
}
|
|
return to.path;
|
|
}
|
|
|
|
Future<List<Finding>> _findings(String root) async {
|
|
final analysis = await analyzeProject(
|
|
await loadProject(root),
|
|
concurrency: 1,
|
|
);
|
|
try {
|
|
return analysis.report.findings;
|
|
} finally {
|
|
await analysis.dispose();
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
late List<Finding> original;
|
|
late List<Finding> shifted;
|
|
|
|
setUpAll(() async {
|
|
original = await _findings('test/fixtures/stuck_loading');
|
|
shifted = await _findings(_shiftedFixture(7));
|
|
});
|
|
|
|
test('moving code down a file does not change any fingerprint', () {
|
|
// A baseline must keep accepting a finding when unrelated edits move it.
|
|
// Any fingerprint carrying a line number resurrected accepted findings as
|
|
// new on the next check.
|
|
expect(
|
|
shifted.map((f) => f.line),
|
|
isNot(equals(original.map((f) => f.line))),
|
|
reason: 'the control must actually move the findings',
|
|
);
|
|
final before = {for (final f in original) f.fingerprint: f.rule};
|
|
final after = {for (final f in shifted) f.fingerprint: f.rule};
|
|
final moved = {
|
|
for (final entry in before.entries)
|
|
if (!after.containsKey(entry.key)) entry.key: entry.value,
|
|
};
|
|
expect(moved, isEmpty, reason: 'fingerprints that depend on position');
|
|
});
|
|
|
|
test('no two findings share a fingerprint', () {
|
|
// A collision makes the baseline keep one finding and silently drop the
|
|
// other, so a second real defect can never be reported as new.
|
|
final counts = <String, int>{};
|
|
for (final f in original) {
|
|
counts[f.fingerprint] = (counts[f.fingerprint] ?? 0) + 1;
|
|
}
|
|
expect({
|
|
for (final e in counts.entries)
|
|
if (e.value > 1) e.key: e.value,
|
|
}, isEmpty);
|
|
});
|
|
}
|