import 'package:pipe/pipe.dart'; import 'package:test/test.dart'; Finding _finding({ required String rule, String? file, int? line, Map data = const {}, }) => Finding( rule: rule, severity: PipeSeverity.warning, confidence: 'high', message: 'message', file: file, line: line, data: data, ); void main() { final present = _finding( rule: 'emitted-not-rendered', file: 'lib/a.dart', line: 10, data: const {'variant': 'StillBroken'}, ); final fixed = _finding( rule: 'loading-flag-not-reset', file: 'lib/b.dart', line: 20, data: const {'member': 'loadNow'}, ); test('baseline entries round-trip with a readable label', () { final encoded = encodeBaseline('/root', [present, fixed]); final entries = decodeBaselineEntries(encoded); expect(entries, hasLength(2)); final byLabel = {for (final entry in entries) entry.label: entry}; expect(byLabel.keys, containsAll(['StillBroken', 'loadNow'])); expect(byLabel['StillBroken']!.rule, 'emitted-not-rendered'); expect(byLabel['StillBroken']!.location, 'lib/a.dart:10'); // The bare fingerprint list must keep working for `check --baseline`. expect(decodeBaseline(encoded), hasLength(2)); }); test('a legacy fingerprints-only baseline still decodes', () { const legacy = '{"version":1,"fingerprints":["r|a.dart|variant=V"]}'; expect(decodeBaseline(legacy), {'r|a.dart|variant=V'}); final entries = decodeBaselineEntries(legacy); expect(entries.single.label, 'r|a.dart|variant=V'); expect(entries.single.rule, 'r'); }); test('verify separates resolved from still-present', () { final report = FindingReport( root: '/root', findings: [present], suppressed: const [], ); final baseline = [ BaselineEntry( fingerprint: present.fingerprint, rule: present.rule, file: present.file!, line: present.line, label: 'StillBroken', ), BaselineEntry( fingerprint: fixed.fingerprint, rule: fixed.rule, file: fixed.file!, line: fixed.line, label: 'loadNow', ), ]; final verification = verifyAgainstBaseline( report: report, baseline: baseline, ); expect(verification.baselineTotal, 2); expect(verification.resolved.map((e) => e.label), ['loadNow']); expect(verification.stillPresent.map((e) => e.label), ['StillBroken']); expect(verification.newFindings, isEmpty); expect(verification.allResolved, isFalse); expect(verification.toJson(), containsPair('resolved', 1)); expect(verification.describe(), contains('these fixes are confirmed')); expect(verification.describe(), contains('loadNow')); }); test('allResolved is true only when nothing accepted remains', () { final report = FindingReport( root: '/root', findings: const [], suppressed: const [], ); final verification = verifyAgainstBaseline( report: report, baseline: [ BaselineEntry( fingerprint: fixed.fingerprint, rule: fixed.rule, file: fixed.file!, line: fixed.line, label: 'loadNow', ), ], ); expect(verification.allResolved, isTrue); expect(verification.resolved, hasLength(1)); }); test('a finding that is new since the baseline is reported as new', () { final fresh = _finding( rule: 'dead-handler', file: 'lib/c.dart', line: 1, data: const {'event': 'NewlyDead'}, ); final report = FindingReport( root: '/root', findings: [fresh], suppressed: const [], ); final verification = verifyAgainstBaseline( report: report, baseline: const [], ); expect(verification.newFindings.map((f) => f.baselineLabel), ['NewlyDead']); expect(verification.resolved, isEmpty); }); test('verify --rule judges only the baselined findings of that rule', () { // `verify --rule X` compared against the WHOLE baseline, so every accepted // finding of every other rule was absent from the fresh report - because // those rules never ran - and was reported as RESOLVED. final report = FindingReport( root: '/root', findings: [present], suppressed: const [], ); final verification = verifyAgainstBaseline( report: report, baseline: [ BaselineEntry( fingerprint: present.fingerprint, rule: present.rule, file: present.file!, line: present.line, label: 'StillBroken', ), BaselineEntry( fingerprint: fixed.fingerprint, rule: fixed.rule, file: fixed.file!, line: fixed.line, label: 'loadNow', ), // A fingerprints-only baseline carries no rule; it is the fingerprint's // first segment. BaselineEntry( fingerprint: 'dead-handler|lib/c.dart|event=Old', rule: '', file: '', label: 'dead-handler|lib/c.dart|event=Old', ), ], rules: {'emitted-not-rendered'}, ); expect(verification.resolved, isEmpty); expect(verification.stillPresent.map((e) => e.label), ['StillBroken']); expect(verification.baselineTotal, 1); expect(verification.outOfScope, 2); expect(verification.describe(), isNot(contains('confirmed'))); expect(verification.describe(), contains('rules not run')); }); test('a baselined finding in an unscanned file is not resolved', () { // Absent from the report because nothing read its file - excluded by // analysis_options, or outside the scanned --dir - is not the same as // fixed, so it must not be counted as a confirmed fix. final outsideDirs = _finding( rule: 'dead-handler', file: 'test/helpers.dart', line: 3, data: const {'event': 'OnlyInTests'}, ); final report = FindingReport( root: '/root', findings: const [], suppressed: const [], ); BaselineEntry entryOf(Finding finding, String label) => BaselineEntry( fingerprint: finding.fingerprint, rule: finding.rule, file: finding.file!, line: finding.line, label: label, ); final verification = verifyAgainstBaseline( report: report, baseline: [ entryOf(present, 'StillBroken'), entryOf(fixed, 'loadNow'), entryOf(outsideDirs, 'OnlyInTests'), ], unscannedFiles: {'lib/b.dart'}, scannedDirectories: ['lib'], ); expect(verification.resolved.map((e) => e.label), ['StillBroken']); expect( verification.notScanned.map((e) => e.label), unorderedEquals(['loadNow', 'OnlyInTests']), ); expect(verification.allResolved, isFalse); expect(verification.toJson(), containsPair('notScanned', 2)); expect(verification.describe(), contains('NOT SCANNED')); }); test('a finding whose subject was deleted is removed, not resolved', () { // nuwayze deleted a dead handler's event ("delete dead firestore query // code") and verify printed it under "these fixes are confirmed". Under the // default wire-up policy a deletion is not the fix the finding asked for. Finding dead(String event, String file) => _finding( rule: 'dead-handler', file: file, line: 5, data: {'union': 'E', 'owner': 'B', 'event': event}, ); final wired = dead('Wired', 'lib/bloc.dart'); final deletedHandler = dead('HandlerGone', 'lib/bloc.dart'); final deletedFile = _finding( rule: 'loading-flag-not-reset', file: 'lib/gone.dart', line: 3, data: const {'owner': 'C', 'member': 'load'}, ); final memberFixed = _finding( rule: 'loading-flag-not-reset', file: 'lib/bloc.dart', line: 9, data: const {'owner': 'C', 'member': 'refresh'}, ); final baseline = decodeBaselineEntries( encodeBaseline('/root', [ wired, deletedHandler, deletedFile, memberFixed, ]), ); final verification = verifyAgainstBaseline( report: FindingReport( root: '/root', findings: const [], suppressed: const [], ), baseline: baseline, existingFiles: const {'lib/bloc.dart'}, declaredNames: const {'Wired', 'HandlerGone', 'B', 'C'}, handledEvents: const {'Wired'}, ); // Wired still has its handler and is simply no longer reported: a fix. // refresh is a member label, which the declaration check cannot judge, so // it stays resolved rather than being guessed either way. expect(verification.resolved.map((e) => e.label), {'Wired', 'refresh'}); expect(verification.removed.map((e) => e.label), {'HandlerGone', 'load'}); expect(verification.allResolved, isTrue, reason: 'removed is still gone'); expect(verification.describe(), contains('REMOVED')); expect(verification.toJson()['removed'], 2); }); }