pipe/test/recall_test.dart
trvon 74b5aa95e3
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
style: apply dart format to the two tests added in 4c65166
The committed versions were not in canonical format; they were reformatted after
the commit, which would have made the pre-commit hook rewrite them on the next
commit and block it.
2026-09-20 21:59:32 -06:00

191 lines
6.1 KiB
Dart

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<PipeException>()),
);
});
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<PipeException>()));
});
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<PipeException>().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<PipeException>()),
);
// 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<PipeException>().having(
(error) => error.toString(),
'message',
contains('evaluated none'),
),
),
);
});
}