Third step of the state-map seam, and the proof that it holds. stateFlowFromJson rebuilds a StateFlow from its toJson form (computed members - totals, coverage differences, Riverpod gaps - are recomputed), and `pipe check --model <file>` runs every rule on such a file instead of analysing Dart. A front end for another language now only has to write that JSON to get every rule, --baseline ratcheting and --fail-on gating. StateUnion.toJson now carries uiReadsBaseFields (only when true), which the round trip showed it had been dropping. Checked byte for byte, through a file on disk: `pipe state --format json` then `pipe check --model` reports exactly what `pipe check` does on the fixture and on resolved mirrors of nuwayze (102 findings), tick, dex and word_alive. state_map_roundtrip_test.dart asserts the same in process (model JSON is idempotent; findings identical; every rule fires, so the comparison is not vacuous), and model_cli_test.dart covers the CLI path. --model refuses --write-baseline: a model does not record whether its front end read every file, so the baseline could silently be partial - the hazard the source path already refuses. A malformed model is a PipeException, not a crash. 134 tests pass.
116 lines
3 KiB
Dart
116 lines
3 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:pipe/pipe.dart';
|
|
import 'package:pipe/src/cli.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/// Collects what a command writes to stdout.
|
|
class _CapturedStdout implements Stdout {
|
|
final StringBuffer buffer = StringBuffer();
|
|
|
|
@override
|
|
void write(Object? object) => buffer.write(object);
|
|
|
|
@override
|
|
void writeln([Object? object = '']) => buffer.writeln(object);
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => null;
|
|
}
|
|
|
|
Future<(int?, String)> _run(List<String> args) async {
|
|
final out = _CapturedStdout();
|
|
final code = await IOOverrides.runZoned(
|
|
() => PipeRunner().run(args),
|
|
stdout: () => out,
|
|
);
|
|
return (code, out.buffer.toString());
|
|
}
|
|
|
|
const String _fixture = 'test/fixtures/stuck_loading';
|
|
|
|
void main() {
|
|
late String model;
|
|
|
|
setUpAll(() async {
|
|
final dir = Directory.systemTemp.createTempSync('pipe-model');
|
|
model = '${dir.path}/model.json';
|
|
final (code, json) = await _run([
|
|
'state',
|
|
_fixture,
|
|
'--format',
|
|
'json',
|
|
'-c',
|
|
'1',
|
|
]);
|
|
expect(code, 0);
|
|
File(model).writeAsStringSync(json);
|
|
});
|
|
|
|
test('check --model reports exactly what a source check reports', () async {
|
|
// The front-end seam as a separate process would use it: the model goes
|
|
// through a file, and the rules must not notice.
|
|
final (_, fromSource) = await _run([
|
|
'check',
|
|
_fixture,
|
|
'--format',
|
|
'json',
|
|
'-c',
|
|
'1',
|
|
]);
|
|
final (_, fromModel) = await _run([
|
|
'check',
|
|
'--model',
|
|
model,
|
|
'--format',
|
|
'json',
|
|
]);
|
|
expect(fromModel, fromSource);
|
|
});
|
|
|
|
test('check --model honours --rule and --fail-on', () async {
|
|
final (code, output) = await _run([
|
|
'check',
|
|
'--model',
|
|
model,
|
|
'--rule',
|
|
ruleEventWithoutHandler,
|
|
'--fail-on',
|
|
'error',
|
|
'--format',
|
|
'json',
|
|
]);
|
|
expect(code, 1, reason: 'event-without-handler fires at error severity');
|
|
expect(output, contains(ruleEventWithoutHandler));
|
|
expect(output, isNot(contains(ruleDeadHandler)));
|
|
});
|
|
|
|
test('check --model refuses --write-baseline', () async {
|
|
final target = '${File(model).parent.path}/baseline.json';
|
|
await expectLater(
|
|
_run(['check', '--model', model, '--write-baseline', target]),
|
|
throwsA(
|
|
isA<PipeException>().having(
|
|
(error) => error.toString(),
|
|
'message',
|
|
contains('--model'),
|
|
),
|
|
),
|
|
);
|
|
expect(File(target).existsSync(), isFalse);
|
|
});
|
|
|
|
test('check --model rejects a file that is not a model', () async {
|
|
final dir = File(model).parent.path;
|
|
File('$dir/array.json').writeAsStringSync('[1, 2]');
|
|
// An object missing required fields must be a clear error, not a crash.
|
|
File('$dir/partial.json').writeAsStringSync('{"unions": [{"name": 1}]}');
|
|
for (final bad in ['array.json', 'partial.json']) {
|
|
await expectLater(
|
|
_run(['check', '--model', '$dir/$bad']),
|
|
throwsA(isA<PipeException>()),
|
|
reason: bad,
|
|
);
|
|
}
|
|
});
|
|
}
|