import 'dart:convert'; import 'dart:io'; import 'package:pipe/pipe.dart'; import 'package:test/test.dart'; void main() { late String manifestPath; setUpAll(() { // Point the manifest at the fixture project so recall machinery is covered // in CI without depending on external repos. final fixture = Directory('test/fixtures/stuck_loading').absolute.path; final temp = Directory.systemTemp.createTempSync('pipe-recall'); manifestPath = '${temp.path}/manifest.json'; File(manifestPath).writeAsStringSync( const JsonEncoder.withIndent(' ').convert({ 'version': 1, 'cases': [ { 'id': 'known-real-bug', 'repo': fixture, 'rule': 'emitted-not-rendered', 'match': {'union': 'ItemState'}, }, { 'id': 'bug-that-does-not-exist', 'repo': fixture, 'rule': 'emitted-not-rendered', 'match': {'union': 'NoSuchState'}, 'note': 'expected on master; the fix lives on an unmerged branch', }, { 'id': 'repo-absent-on-this-machine', 'repo': 'definitely-not-a-real-repo', 'rule': 'emitted-not-rendered', 'match': {'union': 'ItemState'}, }, ], }), ); }); test('reports found, missed and skipped separately', () async { final report = await runRecall( manifestPath: manifestPath, root: Directory.current.path, concurrency: 1, ); expect(report.outcomes, hasLength(3)); expect(report.evaluated, hasLength(2)); expect(report.skippedCases, hasLength(1)); final found = report.evaluated.where((o) => o.found).toList(); expect(found, hasLength(1)); expect(found.single.testCase.id, 'known-real-bug'); expect(found.single.at, contains('item_bloc.dart')); expect(found.single.at, contains(':'), reason: 'should be file:line'); expect(report.missed, hasLength(1)); expect(report.missed.single.testCase.id, 'bug-that-does-not-exist'); expect(report.allEvaluatedFound, isFalse); }); test('per-rule recall is found/total over evaluated cases only', () async { final report = await runRecall( manifestPath: manifestPath, root: Directory.current.path, concurrency: 1, ); // The skipped case must not dilute recall. expect(report.recallByRule, {'emitted-not-rendered': '1/2'}); expect(report.toJson()['skipped'], 1); expect(report.toJson()['evaluated'], 2); }); test('a missing manifest is an actionable error, not a crash', () { expect( () => loadRecallCases('/nope/does-not-exist.json'), throwsA(isA()), ); }); test('a malformed manifest is rejected', () { final temp = Directory.systemTemp.createTempSync('pipe-recall-bad'); final bad = '${temp.path}/bad.json'; File(bad).writeAsStringSync('{"cases": [{"id": "x"}]}'); expect(() => loadRecallCases(bad), throwsA(isA())); }); test( 'a MISSED case prints its note, so an expected miss explains itself', () async { // Without this, the manifest note never reached the reader: a case that // misses because its fix is on an unmerged branch looked exactly like a // regression in pipe. final report = await runRecall( manifestPath: manifestPath, root: Directory.current.path, concurrency: 1, ); final text = report.describe(); expect(text, contains('MISSED')); expect( text, contains( 'note: expected on master; the fix lives on an unmerged branch', ), ); }, ); test('an unknown status is rejected instead of silently read as open', () { // 'fix' is the typo a reader is most likely to write. Defaulting it to // `open` reported a MISSED case and sent them hunting for a regression that // was really a manifest typo. final temp = Directory.systemTemp.createTempSync('pipe-recall-status'); final bad = '${temp.path}/status.json'; File(bad).writeAsStringSync( const JsonEncoder.withIndent(' ').convert({ 'version': 1, 'cases': [ { 'id': 'typo-status', 'repo': 'test/fixtures/stuck_loading', 'rule': 'emitted-not-rendered', 'match': {'union': 'ItemState'}, 'status': 'fix', }, ], }), ); expect( () => loadRecallCases(bad), throwsA( isA().having( (e) => e.toString(), 'message', allOf(contains('status "fix"'), contains('open'), contains('fixed')), ), ), ); }); test('a vacuous recall run fails instead of reporting 0/0 as success', () async { // "0/0 known bugs accounted for" reads as a pass, so both ways of evaluating // nothing are now failures: an empty manifest, and a --root under which none // of the manifest's repos exist. final temp = Directory.systemTemp.createTempSync('pipe-recall-vacuous'); final emptyManifest = '${temp.path}/empty.json'; File(emptyManifest).writeAsStringSync('{"version": 1, "cases": []}'); await expectLater( runRecall( manifestPath: emptyManifest, root: Directory.current.path, concurrency: 1, ), throwsA(isA()), ); // A case whose repo is a RELATIVE name that does not resolve under --root is // skipped; when that is all of them, the run verified nothing. final absentManifest = '${temp.path}/absent.json'; File(absentManifest).writeAsStringSync( const JsonEncoder.withIndent(' ').convert({ 'version': 1, 'cases': [ { 'id': 'absent-repo', 'repo': 'definitely-not-a-real-repo', 'rule': 'dead-handler', 'match': {'event': 'NeverDispatched'}, }, ], }), ); await expectLater( runRecall(manifestPath: absentManifest, root: temp.path, concurrency: 1), throwsA( isA().having( (error) => error.toString(), 'message', contains('evaluated none'), ), ), ); }); }