mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-11 18:26:27 +10:00
rebuild translations
This commit is contained in:
@@ -116,7 +116,8 @@ void main() {
|
||||
});
|
||||
|
||||
test('rejects trailing garbage', () {
|
||||
final Uint8List extra = Uint8List(raw.length + 1)..setRange(0, raw.length, raw);
|
||||
final Uint8List extra = Uint8List(raw.length + 1)
|
||||
..setRange(0, raw.length, raw);
|
||||
expect(
|
||||
() => EntropyTables.parse(extra),
|
||||
throwsA(isA<EntropyTableFormatException>()),
|
||||
|
||||
@@ -16,21 +16,19 @@ import 'package:meshcore_open/services/image_codec_session_io.dart';
|
||||
/// needs to rebuild a bundle that can decode.
|
||||
void main() {
|
||||
ImageCodecBundle fiveAssetBundle() => const ImageCodecBundle(
|
||||
decoderGraphPath: '/models/aeic_decoder_qdq_conv_pct.onnx',
|
||||
entropyGraphPath: '/models/aeic_entropy_side_fp32_op17.onnx',
|
||||
entropyDecodeGraphPath: '/models/aeic_entropy_decode_fp32_op17.onnx',
|
||||
tablesPath: '/models/aeic_cdf_ft32.bin',
|
||||
ratePoint: AeicRatePoint.ft32,
|
||||
);
|
||||
decoderGraphPath: '/models/aeic_decoder_qdq_conv_pct.onnx',
|
||||
entropyGraphPath: '/models/aeic_entropy_side_fp32_op17.onnx',
|
||||
entropyDecodeGraphPath: '/models/aeic_entropy_decode_fp32_op17.onnx',
|
||||
tablesPath: '/models/aeic_cdf_ft32.bin',
|
||||
ratePoint: AeicRatePoint.ft32,
|
||||
);
|
||||
|
||||
group('codec worker boot payload', () {
|
||||
test('a five-asset bundle survives the round trip and can still decode', () {
|
||||
final sent = fiveAssetBundle();
|
||||
expect(sent.supportsDecode, isTrue, reason: 'precondition');
|
||||
|
||||
final rebuilt = debugBundleFromBootPayload(
|
||||
debugBootPayloadFor(sent),
|
||||
);
|
||||
final rebuilt = debugBundleFromBootPayload(debugBootPayloadFor(sent));
|
||||
|
||||
expect(rebuilt.decoderGraphPath, sent.decoderGraphPath);
|
||||
expect(rebuilt.entropyGraphPath, sent.entropyGraphPath);
|
||||
@@ -56,20 +54,22 @@ void main() {
|
||||
expect(payload[6], '/models/aeic_entropy_decode_fp32_op17.onnx');
|
||||
});
|
||||
|
||||
test('a send-only bundle rebuilds as send-only rather than half-decoding',
|
||||
() {
|
||||
const sendOnly = ImageCodecBundle(
|
||||
decoderGraphPath: '/models/decoder.onnx',
|
||||
entropyGraphPath: '/models/entropy.onnx',
|
||||
tablesPath: '/models/tables.bin',
|
||||
ratePoint: AeicRatePoint.ft32,
|
||||
);
|
||||
final rebuilt = debugBundleFromBootPayload(
|
||||
debugBootPayloadFor(sendOnly),
|
||||
);
|
||||
expect(rebuilt.entropyDecodeGraphPath, isNull);
|
||||
expect(rebuilt.supportsDecode, isFalse);
|
||||
});
|
||||
test(
|
||||
'a send-only bundle rebuilds as send-only rather than half-decoding',
|
||||
() {
|
||||
const sendOnly = ImageCodecBundle(
|
||||
decoderGraphPath: '/models/decoder.onnx',
|
||||
entropyGraphPath: '/models/entropy.onnx',
|
||||
tablesPath: '/models/tables.bin',
|
||||
ratePoint: AeicRatePoint.ft32,
|
||||
);
|
||||
final rebuilt = debugBundleFromBootPayload(
|
||||
debugBootPayloadFor(sendOnly),
|
||||
);
|
||||
expect(rebuilt.entropyDecodeGraphPath, isNull);
|
||||
expect(rebuilt.supportsDecode, isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test('a short payload does not crash the worker', () {
|
||||
// An older sender, or a truncated message, must degrade to "cannot
|
||||
|
||||
@@ -250,10 +250,7 @@ void main() {
|
||||
expect(await graph.readAsBytes(), small);
|
||||
expect(await weights.length(), large.length);
|
||||
expect(_sha256(await weights.readAsBytes()), _sha256(large));
|
||||
expect(
|
||||
await File('${tempDir.path}/entropy.onnx').readAsBytes(),
|
||||
entropy,
|
||||
);
|
||||
expect(await File('${tempDir.path}/entropy.onnx').readAsBytes(), entropy);
|
||||
// Two different entropy exports land side by side; neither may overwrite
|
||||
// or be mistaken for the other.
|
||||
expect(
|
||||
|
||||
@@ -121,45 +121,49 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('ENCODE: Dart bitstream is byte-identical to the C++ bitstream',
|
||||
() async {
|
||||
final _ReplayNetwork network = _ReplayNetwork(r);
|
||||
final AeicEntropyCodec codec = AeicEntropyCodec(
|
||||
geometry: geometry,
|
||||
network: network,
|
||||
coders: coders,
|
||||
);
|
||||
final Uint8List got = await codec.encode(
|
||||
Uint8List(geometry.resolution * geometry.resolution * 3),
|
||||
);
|
||||
expect(network.encodeCalls, 1);
|
||||
_expectSameBytes(got, r.u8('enc/bitstream'), name);
|
||||
expect(
|
||||
sha256.convert(got).toString(),
|
||||
r.meta['bitstream_sha256'],
|
||||
reason: '$name: bitstream sha256',
|
||||
);
|
||||
});
|
||||
test(
|
||||
'ENCODE: Dart bitstream is byte-identical to the C++ bitstream',
|
||||
() async {
|
||||
final _ReplayNetwork network = _ReplayNetwork(r);
|
||||
final AeicEntropyCodec codec = AeicEntropyCodec(
|
||||
geometry: geometry,
|
||||
network: network,
|
||||
coders: coders,
|
||||
);
|
||||
final Uint8List got = await codec.encode(
|
||||
Uint8List(geometry.resolution * geometry.resolution * 3),
|
||||
);
|
||||
expect(network.encodeCalls, 1);
|
||||
_expectSameBytes(got, r.u8('enc/bitstream'), name);
|
||||
expect(
|
||||
sha256.convert(got).toString(),
|
||||
r.meta['bitstream_sha256'],
|
||||
reason: '$name: bitstream sha256',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('DECODE: y_hat from the C++ bitstream is exactly the recorded y_hat',
|
||||
() async {
|
||||
final _ReplayNetwork network = _ReplayNetwork(r);
|
||||
final AeicEntropyCodec codec = AeicEntropyCodec(
|
||||
geometry: geometry,
|
||||
network: network,
|
||||
coders: coders,
|
||||
);
|
||||
final Float32List got = await codec.decodeToLatent(
|
||||
r.u8('enc/bitstream'),
|
||||
);
|
||||
expect(network.hyperCalls, 1);
|
||||
expect(network.stageCalls, <int>[0, 1, 2, 3]);
|
||||
_expectSameFloats(got, r.f32('dec/y_hat'), '$name: y_hat');
|
||||
// The recording asserts the decoder's latent equals the encoder's; if
|
||||
// that holds in Python it must hold here too.
|
||||
expect(r.meta['decoded_y_hat_equals_encoder_y_hat'], isTrue);
|
||||
_expectSameFloats(got, r.f32('enc/y_hat'), '$name: y_hat vs encoder');
|
||||
});
|
||||
test(
|
||||
'DECODE: y_hat from the C++ bitstream is exactly the recorded y_hat',
|
||||
() async {
|
||||
final _ReplayNetwork network = _ReplayNetwork(r);
|
||||
final AeicEntropyCodec codec = AeicEntropyCodec(
|
||||
geometry: geometry,
|
||||
network: network,
|
||||
coders: coders,
|
||||
);
|
||||
final Float32List got = await codec.decodeToLatent(
|
||||
r.u8('enc/bitstream'),
|
||||
);
|
||||
expect(network.hyperCalls, 1);
|
||||
expect(network.stageCalls, <int>[0, 1, 2, 3]);
|
||||
_expectSameFloats(got, r.f32('dec/y_hat'), '$name: y_hat');
|
||||
// The recording asserts the decoder's latent equals the encoder's; if
|
||||
// that holds in Python it must hold here too.
|
||||
expect(r.meta['decoded_y_hat_equals_encoder_y_hat'], isTrue);
|
||||
_expectSameFloats(got, r.f32('enc/y_hat'), '$name: y_hat vs encoder');
|
||||
},
|
||||
);
|
||||
|
||||
test('a single flipped bitstream byte does not still pass', () async {
|
||||
// Byte 3 is the first payload byte of sub-stream 0 (1-byte flag +
|
||||
@@ -185,7 +189,8 @@ void main() {
|
||||
expect(
|
||||
_sameFloats(got, r.f32('dec/y_hat')),
|
||||
isFalse,
|
||||
reason: '$name: corrupting the stream changed nothing — the '
|
||||
reason:
|
||||
'$name: corrupting the stream changed nothing — the '
|
||||
'comparison is vacuous',
|
||||
);
|
||||
});
|
||||
@@ -253,7 +258,8 @@ class _ReplayNetwork implements AeicEntropyNetwork {
|
||||
'stage $stage input base',
|
||||
);
|
||||
}
|
||||
final Map<String, dynamic> outputs = call['outputs'] as Map<String, dynamic>;
|
||||
final Map<String, dynamic> outputs =
|
||||
call['outputs'] as Map<String, dynamic>;
|
||||
return AeicStageParams(
|
||||
meansSupp: r.f32(outputs['means'] as String),
|
||||
scalesSupp: r.f32(outputs['scales'] as String),
|
||||
@@ -265,12 +271,11 @@ class _ReplayNetwork implements AeicEntropyNetwork {
|
||||
/// 32-byte header, an 8-byte-aligned tensor blob, then a UTF-8 JSON index.
|
||||
class _Recording {
|
||||
_Recording(this.index, this.bytes)
|
||||
: _entries = <String, Map<String, dynamic>>{
|
||||
for (final Map<String, dynamic> e
|
||||
in (index['entries'] as List<dynamic>)
|
||||
.cast<Map<String, dynamic>>())
|
||||
e['name'] as String: e,
|
||||
};
|
||||
: _entries = <String, Map<String, dynamic>>{
|
||||
for (final Map<String, dynamic> e
|
||||
in (index['entries'] as List<dynamic>).cast<Map<String, dynamic>>())
|
||||
e['name'] as String: e,
|
||||
};
|
||||
|
||||
final Map<String, dynamic> index;
|
||||
final Uint8List bytes;
|
||||
@@ -291,7 +296,9 @@ class _Recording {
|
||||
final int indexLength = bd.getUint32(24, Endian.little);
|
||||
final Map<String, dynamic> index =
|
||||
jsonDecode(
|
||||
utf8.decode(bytes.sublist(indexOffset, indexOffset + indexLength)),
|
||||
utf8.decode(
|
||||
bytes.sublist(indexOffset, indexOffset + indexLength),
|
||||
),
|
||||
)
|
||||
as Map<String, dynamic>;
|
||||
return _Recording(index, bytes);
|
||||
|
||||
@@ -40,25 +40,281 @@ class _Golden {
|
||||
];
|
||||
|
||||
static const List<List<int>> symbols = <List<int>>[
|
||||
<int>[-1, -1, -2, 4, 0, 0, 6, -3, 2, 1, 1, -1, -5, 1, 0, 0, -1, -1, -2, 4,
|
||||
0, 0, 5, -3, 2, 1, 1, -1, -5, 1, 0, 0],
|
||||
<int>[-1, -1, 4, 4, 0, 0, 0, 5, 2, 1, -1, -1, 3, -5, 1, 0, -1, -2, 4, 4,
|
||||
0, 0, 0, 5, 1, 1, -1, -1, 3, -5, 1, 0],
|
||||
<int>[-1, -2, -2, 4, 1, 0, 0, 5, 2, 0, -1, -1, 3, -5, 1, 0, -1, -2, -2, 4,
|
||||
0, 0, 0, 5, 2, -1, -1, -1, 3, -5, 1, 0],
|
||||
<int>[-1, -1, -2, -2, 0, 0, 5, -3, 2, 1, -1, -1, -5, -5, 0, 0, -1, -1, -2,
|
||||
-2, 0, 0, 5, -3, 2, 1, -1, -1, -5, 1, 0, 0],
|
||||
<int>[
|
||||
-1,
|
||||
-1,
|
||||
-2,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
6,
|
||||
-3,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
-1,
|
||||
-5,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
-1,
|
||||
-1,
|
||||
-2,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
-3,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
-1,
|
||||
-5,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
<int>[
|
||||
-1,
|
||||
-1,
|
||||
4,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
2,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
-5,
|
||||
1,
|
||||
0,
|
||||
-1,
|
||||
-2,
|
||||
4,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
1,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
-5,
|
||||
1,
|
||||
0,
|
||||
],
|
||||
<int>[
|
||||
-1,
|
||||
-2,
|
||||
-2,
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
-5,
|
||||
1,
|
||||
0,
|
||||
-1,
|
||||
-2,
|
||||
-2,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
2,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
-5,
|
||||
1,
|
||||
0,
|
||||
],
|
||||
<int>[
|
||||
-1,
|
||||
-1,
|
||||
-2,
|
||||
-2,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
-3,
|
||||
2,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
-5,
|
||||
-5,
|
||||
0,
|
||||
0,
|
||||
-1,
|
||||
-1,
|
||||
-2,
|
||||
-2,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
-3,
|
||||
2,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
-5,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
];
|
||||
|
||||
static const List<List<int>> indexes = <List<int>>[
|
||||
<int>[-1, 6, -1, 8, 12, 15, 13, 16, 0, 9, 3, 10, 13, 16, 14, 17, 0, 9, 3,
|
||||
10, 13, 16, 14, 17, 5, 11, 7, 12, 15, 17, 15, 18],
|
||||
<int>[14, 11, 15, 12, 7, 0, 8, 2, 15, 13, 16, 14, 9, 4, 11, 6, 15, 13, 16,
|
||||
14, 10, 4, 11, 6, 16, 14, 17, 15, 11, 7, 12, 9],
|
||||
<int>[11, 14, 12, 15, -1, 8, 0, 9, 12, 16, 13, 16, 2, 10, 5, 11, 13, 16,
|
||||
13, 16, 3, 10, 5, 11, 14, 17, 15, 17, 7, 12, 8, 13],
|
||||
<int>[5, -1, 7, 0, 15, 12, 15, 13, 8, 1, 10, 4, 16, 14, 16, 14, 8, 2, 10,
|
||||
4, 16, 14, 16, 14, 11, 6, 12, 8, 17, 15, 17, 16],
|
||||
<int>[
|
||||
-1,
|
||||
6,
|
||||
-1,
|
||||
8,
|
||||
12,
|
||||
15,
|
||||
13,
|
||||
16,
|
||||
0,
|
||||
9,
|
||||
3,
|
||||
10,
|
||||
13,
|
||||
16,
|
||||
14,
|
||||
17,
|
||||
0,
|
||||
9,
|
||||
3,
|
||||
10,
|
||||
13,
|
||||
16,
|
||||
14,
|
||||
17,
|
||||
5,
|
||||
11,
|
||||
7,
|
||||
12,
|
||||
15,
|
||||
17,
|
||||
15,
|
||||
18,
|
||||
],
|
||||
<int>[
|
||||
14,
|
||||
11,
|
||||
15,
|
||||
12,
|
||||
7,
|
||||
0,
|
||||
8,
|
||||
2,
|
||||
15,
|
||||
13,
|
||||
16,
|
||||
14,
|
||||
9,
|
||||
4,
|
||||
11,
|
||||
6,
|
||||
15,
|
||||
13,
|
||||
16,
|
||||
14,
|
||||
10,
|
||||
4,
|
||||
11,
|
||||
6,
|
||||
16,
|
||||
14,
|
||||
17,
|
||||
15,
|
||||
11,
|
||||
7,
|
||||
12,
|
||||
9,
|
||||
],
|
||||
<int>[
|
||||
11,
|
||||
14,
|
||||
12,
|
||||
15,
|
||||
-1,
|
||||
8,
|
||||
0,
|
||||
9,
|
||||
12,
|
||||
16,
|
||||
13,
|
||||
16,
|
||||
2,
|
||||
10,
|
||||
5,
|
||||
11,
|
||||
13,
|
||||
16,
|
||||
13,
|
||||
16,
|
||||
3,
|
||||
10,
|
||||
5,
|
||||
11,
|
||||
14,
|
||||
17,
|
||||
15,
|
||||
17,
|
||||
7,
|
||||
12,
|
||||
8,
|
||||
13,
|
||||
],
|
||||
<int>[
|
||||
5,
|
||||
-1,
|
||||
7,
|
||||
0,
|
||||
15,
|
||||
12,
|
||||
15,
|
||||
13,
|
||||
8,
|
||||
1,
|
||||
10,
|
||||
4,
|
||||
16,
|
||||
14,
|
||||
16,
|
||||
14,
|
||||
8,
|
||||
2,
|
||||
10,
|
||||
4,
|
||||
16,
|
||||
14,
|
||||
16,
|
||||
14,
|
||||
11,
|
||||
6,
|
||||
12,
|
||||
8,
|
||||
17,
|
||||
15,
|
||||
17,
|
||||
16,
|
||||
],
|
||||
];
|
||||
|
||||
// --- full case: C = 256, H = W = 16 (the shipping geometry) ---
|
||||
@@ -113,7 +369,21 @@ class _Golden {
|
||||
10000.0,
|
||||
];
|
||||
static const List<int> probeIndexes = <int>[
|
||||
-1, -1, -1, -1, 0, 0, 0, 12, 17, 23, 34, 51, 62, 63, 63,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
12,
|
||||
17,
|
||||
23,
|
||||
34,
|
||||
51,
|
||||
62,
|
||||
63,
|
||||
63,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -220,10 +490,7 @@ void main() {
|
||||
final geometry = AeicEntropyGeometry.forResolution(512);
|
||||
final masks = AeicMaskSet(geometry);
|
||||
for (var stage = 0; stage < 4; stage++) {
|
||||
final live = masks
|
||||
.maskTensor(stage)
|
||||
.where((v) => v == 1.0)
|
||||
.length;
|
||||
final live = masks.maskTensor(stage).where((v) => v == 1.0).length;
|
||||
expect(live, geometry.symbolsPerStage);
|
||||
}
|
||||
});
|
||||
@@ -401,34 +668,37 @@ void main() {
|
||||
});
|
||||
|
||||
group('AeicEntropyCodec', () {
|
||||
test('encode pushes z, y0, y1, y2, y3 in that order and no other', () async {
|
||||
final geometry = AeicEntropyGeometry.forResolution(512);
|
||||
final network = _FakeNetwork(geometry);
|
||||
final coders = _RecordingCoders();
|
||||
final codec = AeicEntropyCodec(
|
||||
geometry: geometry,
|
||||
network: network,
|
||||
coders: coders,
|
||||
);
|
||||
final progress = <double>[];
|
||||
final stream = await codec.encode(
|
||||
Uint8List(512 * 512 * 3),
|
||||
onProgress: progress.add,
|
||||
);
|
||||
test(
|
||||
'encode pushes z, y0, y1, y2, y3 in that order and no other',
|
||||
() async {
|
||||
final geometry = AeicEntropyGeometry.forResolution(512);
|
||||
final network = _FakeNetwork(geometry);
|
||||
final coders = _RecordingCoders();
|
||||
final codec = AeicEntropyCodec(
|
||||
geometry: geometry,
|
||||
network: network,
|
||||
coders: coders,
|
||||
);
|
||||
final progress = <double>[];
|
||||
final stream = await codec.encode(
|
||||
Uint8List(512 * 512 * 3),
|
||||
onProgress: progress.add,
|
||||
);
|
||||
|
||||
expect(coders.encoder.groups, <int>[
|
||||
kAeicZCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
]);
|
||||
expect(coders.encoder.lengths, <int>[2048, 16384, 16384, 16384, 16384]);
|
||||
expect(stream, isNotEmpty);
|
||||
expect(progress.last, 1.0);
|
||||
// The image really was normalized before the graph saw it: 0 -> -1.
|
||||
expect(network.lastInput!.first, -1.0);
|
||||
});
|
||||
expect(coders.encoder.groups, <int>[
|
||||
kAeicZCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
kAeicYCdfGroup,
|
||||
]);
|
||||
expect(coders.encoder.lengths, <int>[2048, 16384, 16384, 16384, 16384]);
|
||||
expect(stream, isNotEmpty);
|
||||
expect(progress.last, 1.0);
|
||||
// The image really was normalized before the graph saw it: 0 -> -1.
|
||||
expect(network.lastInput!.first, -1.0);
|
||||
},
|
||||
);
|
||||
|
||||
test('encode refuses a graph that returns the wrong z size', () async {
|
||||
final geometry = AeicEntropyGeometry.forResolution(512);
|
||||
@@ -697,7 +967,8 @@ class _FakeNetwork implements AeicEntropyNetwork {
|
||||
],
|
||||
scales: <Float32List>[
|
||||
for (var i = 0; i < 4; i++)
|
||||
Float32List(geometry.yElements)..fillRange(0, geometry.yElements, 1.0),
|
||||
Float32List(geometry.yElements)
|
||||
..fillRange(0, geometry.yElements, 1.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ void main() {
|
||||
final Map<String, dynamic> manifest =
|
||||
jsonDecode(File('${goldenDir.path}/manifest.json').readAsStringSync())
|
||||
as Map<String, dynamic>;
|
||||
final List<Map<String, dynamic>> images = (manifest['images'] as List<dynamic>)
|
||||
.cast<Map<String, dynamic>>();
|
||||
final List<Map<String, dynamic>> images =
|
||||
(manifest['images'] as List<dynamic>).cast<Map<String, dynamic>>();
|
||||
final List<Map<String, dynamic>> synthetic =
|
||||
(manifest['synthetic'] as List<dynamic>).cast<Map<String, dynamic>>();
|
||||
|
||||
|
||||
@@ -59,7 +59,10 @@ class _FakeDecoder implements ReceivedImageDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ImageCodecResult?> _run(AeicRatePoint ratePoint, int resolution) async {
|
||||
Future<ImageCodecResult?> _run(
|
||||
AeicRatePoint ratePoint,
|
||||
int resolution,
|
||||
) async {
|
||||
if (delay > Duration.zero) await Future<void>.delayed(delay);
|
||||
switch (mode) {
|
||||
case 'null':
|
||||
@@ -149,7 +152,11 @@ void main() {
|
||||
imgId: 7,
|
||||
firstSeen: DateTime.fromMillisecondsSinceEpoch(0x693F21 * 1000),
|
||||
);
|
||||
expect(id, '1a2b07' '00693f21');
|
||||
expect(
|
||||
id,
|
||||
'1a2b07'
|
||||
'00693f21',
|
||||
);
|
||||
expect(id.length, 14);
|
||||
expect(ReceivedImageRef.parse(ReceivedImageRef.encode(id)), id);
|
||||
});
|
||||
@@ -161,8 +168,10 @@ void main() {
|
||||
expect(ReceivedImageRef.parse('aeic:1:1A2B0700693F21'), isNull);
|
||||
expect(ReceivedImageRef.parse('aeic:2:1a2b0700693f21'), isNull);
|
||||
expect(ReceivedImageRef.parse('@[Bob] hi'), isNull);
|
||||
expect(ReceivedImageRef.parse(' aeic:1:1a2b0700693f21 '),
|
||||
'1a2b0700693f21');
|
||||
expect(
|
||||
ReceivedImageRef.parse(' aeic:1:1a2b0700693f21 '),
|
||||
'1a2b0700693f21',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,8 +193,11 @@ void main() {
|
||||
expect(entry!.state, ReceivedImageState.reassembled);
|
||||
expect(entry.rate, AeicRatePoint.ft32);
|
||||
expect(entry.metadataAssumed, isFalse);
|
||||
expect(h.blobs.hasBitstream(entry.streamId), isTrue,
|
||||
reason: 'bitstream must be written before the state changes');
|
||||
expect(
|
||||
h.blobs.hasBitstream(entry.streamId),
|
||||
isTrue,
|
||||
reason: 'bitstream must be written before the state changes',
|
||||
);
|
||||
|
||||
await h.store.settle();
|
||||
final decoded = h.store.entryFor(entry.streamId)!;
|
||||
@@ -218,8 +230,11 @@ void main() {
|
||||
h.reassembler.addChunk(set.blobs[1], channelIndex: 3),
|
||||
channelIndex: 3,
|
||||
);
|
||||
expect(second!.streamId, first.streamId,
|
||||
reason: 'the sentinel must not change as chunks arrive');
|
||||
expect(
|
||||
second!.streamId,
|
||||
first.streamId,
|
||||
reason: 'the sentinel must not change as chunks arrive',
|
||||
);
|
||||
expect(second.state, ReceivedImageState.reassembled);
|
||||
await h.store.settle();
|
||||
expect(
|
||||
@@ -300,8 +315,10 @@ void main() {
|
||||
expect(entry!.state, ReceivedImageState.reassembled);
|
||||
expect(entry.recoveredWithParity, isTrue);
|
||||
await h.store.settle();
|
||||
expect(h.store.entryFor(entry.streamId)!.state,
|
||||
ReceivedImageState.decoded);
|
||||
expect(
|
||||
h.store.entryFor(entry.streamId)!.state,
|
||||
ReceivedImageState.decoded,
|
||||
);
|
||||
});
|
||||
|
||||
test('loopback and malformed blobs create nothing', () async {
|
||||
@@ -574,37 +591,42 @@ void main() {
|
||||
});
|
||||
|
||||
group('eviction', () {
|
||||
test('byte budget drops the oldest PNG first and keeps the bitstream',
|
||||
() async {
|
||||
final decoder = _FakeDecoder(
|
||||
png: Uint8List.fromList(List<int>.filled(1000, 3)),
|
||||
);
|
||||
final h = _build(decoder: decoder, maxBytes: 2500);
|
||||
final ids = <String>[];
|
||||
for (var i = 0; i < 3; i++) {
|
||||
final entry = await _completeOne(h, imgId: 40 + i, seed: i);
|
||||
await h.store.settle();
|
||||
ids.add(entry.streamId);
|
||||
h.advance(const Duration(seconds: 5));
|
||||
}
|
||||
// 3 x 1000 B PNG > 2500 B, so the oldest PNG must have gone.
|
||||
expect(h.store.entryFor(ids[0])!.state, ReceivedImageState.evicted);
|
||||
expect(h.store.entryFor(ids[0])!.pngStored, isFalse);
|
||||
expect(h.blobs.hasPng(ids[0]), isFalse);
|
||||
expect(h.blobs.hasBitstream(ids[0]), isTrue,
|
||||
reason: 'a ~156 B bitstream is cheap; keep it so we can re-decode');
|
||||
expect(h.store.entryFor(ids[0])!.canRetryDecode, isTrue);
|
||||
expect(h.store.entryFor(ids[2])!.state, ReceivedImageState.decoded);
|
||||
expect(h.store.totalBytes, lessThanOrEqualTo(2500));
|
||||
test(
|
||||
'byte budget drops the oldest PNG first and keeps the bitstream',
|
||||
() async {
|
||||
final decoder = _FakeDecoder(
|
||||
png: Uint8List.fromList(List<int>.filled(1000, 3)),
|
||||
);
|
||||
final h = _build(decoder: decoder, maxBytes: 2500);
|
||||
final ids = <String>[];
|
||||
for (var i = 0; i < 3; i++) {
|
||||
final entry = await _completeOne(h, imgId: 40 + i, seed: i);
|
||||
await h.store.settle();
|
||||
ids.add(entry.streamId);
|
||||
h.advance(const Duration(seconds: 5));
|
||||
}
|
||||
// 3 x 1000 B PNG > 2500 B, so the oldest PNG must have gone.
|
||||
expect(h.store.entryFor(ids[0])!.state, ReceivedImageState.evicted);
|
||||
expect(h.store.entryFor(ids[0])!.pngStored, isFalse);
|
||||
expect(h.blobs.hasPng(ids[0]), isFalse);
|
||||
expect(
|
||||
h.blobs.hasBitstream(ids[0]),
|
||||
isTrue,
|
||||
reason: 'a ~156 B bitstream is cheap; keep it so we can re-decode',
|
||||
);
|
||||
expect(h.store.entryFor(ids[0])!.canRetryDecode, isTrue);
|
||||
expect(h.store.entryFor(ids[2])!.state, ReceivedImageState.decoded);
|
||||
expect(h.store.totalBytes, lessThanOrEqualTo(2500));
|
||||
|
||||
// "Decode again" works off the surviving bitstream, and the image we just
|
||||
// decoded is never the budget's own victim (that would thrash forever).
|
||||
await h.store.requestDecode(ids[0]);
|
||||
await h.store.settle();
|
||||
expect(h.store.entryFor(ids[0])!.state, ReceivedImageState.decoded);
|
||||
expect(h.store.entryFor(ids[1])!.state, ReceivedImageState.evicted);
|
||||
expect(h.store.totalBytes, lessThanOrEqualTo(2500));
|
||||
});
|
||||
// "Decode again" works off the surviving bitstream, and the image we just
|
||||
// decoded is never the budget's own victim (that would thrash forever).
|
||||
await h.store.requestDecode(ids[0]);
|
||||
await h.store.settle();
|
||||
expect(h.store.entryFor(ids[0])!.state, ReceivedImageState.decoded);
|
||||
expect(h.store.entryFor(ids[1])!.state, ReceivedImageState.evicted);
|
||||
expect(h.store.totalBytes, lessThanOrEqualTo(2500));
|
||||
},
|
||||
);
|
||||
|
||||
test('image-count budget evicts oldest first', () async {
|
||||
final h = _build(maxImages: 2);
|
||||
@@ -625,8 +647,10 @@ void main() {
|
||||
final h = _build(maxAge: const Duration(minutes: 10));
|
||||
final entry = await _completeOne(h);
|
||||
await h.store.settle();
|
||||
expect(h.store.entryFor(entry.streamId)!.state,
|
||||
ReceivedImageState.decoded);
|
||||
expect(
|
||||
h.store.entryFor(entry.streamId)!.state,
|
||||
ReceivedImageState.decoded,
|
||||
);
|
||||
h.advance(const Duration(minutes: 11));
|
||||
final evicted = await h.store.evictToBudget();
|
||||
expect(evicted, contains(entry.streamId));
|
||||
@@ -762,8 +786,11 @@ void main() {
|
||||
expect(entry.state, ReceivedImageState.reassembled);
|
||||
expect(entry.needsManualDecode, isTrue);
|
||||
expect(h.store.decodeQueue, isEmpty);
|
||||
expect(h.decoder.calls, 0,
|
||||
reason: 'a radio packet must never start a 2.16 GiB decode by itself');
|
||||
expect(
|
||||
h.decoder.calls,
|
||||
0,
|
||||
reason: 'a radio packet must never start a 2.16 GiB decode by itself',
|
||||
);
|
||||
// Everything the placeholder card needs is already on the entry.
|
||||
expect(entry.senderPrefix, kSender);
|
||||
expect(entry.bitstreamByteCount, greaterThan(0));
|
||||
@@ -779,28 +806,30 @@ void main() {
|
||||
expect(h.decoder.calls, 1);
|
||||
});
|
||||
|
||||
test('a finished model download does not decode the whole backlog',
|
||||
() async {
|
||||
final decoder = _FakeDecoder(
|
||||
availability: ImageCodecAvailability.disabled,
|
||||
);
|
||||
final h = _build(decoder: decoder, processAutomatically: false);
|
||||
final ids = <String>[];
|
||||
for (var i = 0; i < 4; i++) {
|
||||
ids.add((await _completeOne(h, imgId: 80 + i, seed: i)).streamId);
|
||||
h.advance(const Duration(seconds: 2));
|
||||
}
|
||||
// The model lands.
|
||||
decoder.availability = ImageCodecAvailability.ready;
|
||||
h.store.notifyDecoderChanged();
|
||||
await h.store.settle();
|
||||
expect(decoder.calls, 0);
|
||||
expect(h.store.decodeQueue, isEmpty);
|
||||
for (final id in ids) {
|
||||
expect(h.store.entryFor(id)!.state, ReceivedImageState.reassembled);
|
||||
expect(h.store.entryFor(id)!.needsManualDecode, isTrue);
|
||||
}
|
||||
});
|
||||
test(
|
||||
'a finished model download does not decode the whole backlog',
|
||||
() async {
|
||||
final decoder = _FakeDecoder(
|
||||
availability: ImageCodecAvailability.disabled,
|
||||
);
|
||||
final h = _build(decoder: decoder, processAutomatically: false);
|
||||
final ids = <String>[];
|
||||
for (var i = 0; i < 4; i++) {
|
||||
ids.add((await _completeOne(h, imgId: 80 + i, seed: i)).streamId);
|
||||
h.advance(const Duration(seconds: 2));
|
||||
}
|
||||
// The model lands.
|
||||
decoder.availability = ImageCodecAvailability.ready;
|
||||
h.store.notifyDecoderChanged();
|
||||
await h.store.settle();
|
||||
expect(decoder.calls, 0);
|
||||
expect(h.store.decodeQueue, isEmpty);
|
||||
for (final id in ids) {
|
||||
expect(h.store.entryFor(id)!.state, ReceivedImageState.reassembled);
|
||||
expect(h.store.entryFor(id)!.needsManualDecode, isTrue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('turning the setting on affects future arrivals only', () async {
|
||||
final h = _build(processAutomatically: false);
|
||||
@@ -810,15 +839,21 @@ void main() {
|
||||
h.store.processAutomatically = true;
|
||||
await h.store.settle();
|
||||
expect(h.decoder.calls, 0, reason: 'no retro-decode of the backlog');
|
||||
expect(h.store.entryFor(parked.streamId)!.state,
|
||||
ReceivedImageState.reassembled);
|
||||
expect(
|
||||
h.store.entryFor(parked.streamId)!.state,
|
||||
ReceivedImageState.reassembled,
|
||||
);
|
||||
|
||||
final fresh = await _completeOne(h, imgId: 91, seed: 5);
|
||||
await h.store.settle();
|
||||
expect(h.store.entryFor(fresh.streamId)!.state,
|
||||
ReceivedImageState.decoded);
|
||||
expect(h.store.entryFor(parked.streamId)!.state,
|
||||
ReceivedImageState.reassembled);
|
||||
expect(
|
||||
h.store.entryFor(fresh.streamId)!.state,
|
||||
ReceivedImageState.decoded,
|
||||
);
|
||||
expect(
|
||||
h.store.entryFor(parked.streamId)!.state,
|
||||
ReceivedImageState.reassembled,
|
||||
);
|
||||
});
|
||||
|
||||
test('a reassembled entry restored from disk is always tappable', () async {
|
||||
@@ -849,10 +884,7 @@ void main() {
|
||||
h.decoder.availability = ImageCodecAvailability.disabled;
|
||||
expect(h.store.decoderAvailability, ImageCodecAvailability.disabled);
|
||||
final noDecoder = ReceivedImageStore(decoder: null);
|
||||
expect(
|
||||
noDecoder.decoderAvailability,
|
||||
ImageCodecAvailability.unavailable,
|
||||
);
|
||||
expect(noDecoder.decoderAvailability, ImageCodecAvailability.unavailable);
|
||||
noDecoder.dispose();
|
||||
});
|
||||
});
|
||||
@@ -874,8 +906,11 @@ void main() {
|
||||
await h.store.settle();
|
||||
|
||||
expect(decoder.calls, 10);
|
||||
expect(decoder.maxConcurrent, 1,
|
||||
reason: 'two concurrent decodes is ~4.3 GiB and an OOM kill');
|
||||
expect(
|
||||
decoder.maxConcurrent,
|
||||
1,
|
||||
reason: 'two concurrent decodes is ~4.3 GiB and an OOM kill',
|
||||
);
|
||||
for (final id in ids) {
|
||||
expect(h.store.entryFor(id)!.state, ReceivedImageState.decoded);
|
||||
}
|
||||
@@ -895,33 +930,35 @@ void main() {
|
||||
});
|
||||
|
||||
group('deletion hooks', () {
|
||||
test('deleteImagesForChannel reclaims every image of one conversation',
|
||||
() async {
|
||||
final h = _build();
|
||||
final a = await _completeOne(h, imgId: 130);
|
||||
h.advance(const Duration(seconds: 2));
|
||||
final b = await _completeOne(h, imgId: 131, seed: 1);
|
||||
await h.store.settle();
|
||||
// A third image on another channel must survive.
|
||||
final other = await h.store.registerOutgoing(
|
||||
channelIndex: 9,
|
||||
senderPrefix: kSelf,
|
||||
imgId: 5,
|
||||
previewPng: Uint8List.fromList(List<int>.filled(32, 2)),
|
||||
rate: AeicRatePoint.ft32,
|
||||
chunkCount: 1,
|
||||
);
|
||||
test(
|
||||
'deleteImagesForChannel reclaims every image of one conversation',
|
||||
() async {
|
||||
final h = _build();
|
||||
final a = await _completeOne(h, imgId: 130);
|
||||
h.advance(const Duration(seconds: 2));
|
||||
final b = await _completeOne(h, imgId: 131, seed: 1);
|
||||
await h.store.settle();
|
||||
// A third image on another channel must survive.
|
||||
final other = await h.store.registerOutgoing(
|
||||
channelIndex: 9,
|
||||
senderPrefix: kSelf,
|
||||
imgId: 5,
|
||||
previewPng: Uint8List.fromList(List<int>.filled(32, 2)),
|
||||
rate: AeicRatePoint.ft32,
|
||||
chunkCount: 1,
|
||||
);
|
||||
|
||||
final removed = await h.store.deleteImagesForChannel(3);
|
||||
expect(removed, containsAll(<String>[a.streamId, b.streamId]));
|
||||
expect(h.store.entryFor(a.streamId), isNull);
|
||||
expect(h.store.entryFor(b.streamId), isNull);
|
||||
expect(h.blobs.hasPng(a.streamId), isFalse);
|
||||
expect(h.blobs.hasBitstream(a.streamId), isFalse);
|
||||
expect(h.blobs.hasSidecar(a.streamId), isFalse);
|
||||
expect(h.store.entryFor(other.streamId), isNotNull);
|
||||
expect(h.store.totalBytes, other.storedBytes);
|
||||
});
|
||||
final removed = await h.store.deleteImagesForChannel(3);
|
||||
expect(removed, containsAll(<String>[a.streamId, b.streamId]));
|
||||
expect(h.store.entryFor(a.streamId), isNull);
|
||||
expect(h.store.entryFor(b.streamId), isNull);
|
||||
expect(h.blobs.hasPng(a.streamId), isFalse);
|
||||
expect(h.blobs.hasBitstream(a.streamId), isFalse);
|
||||
expect(h.blobs.hasSidecar(a.streamId), isFalse);
|
||||
expect(h.store.entryFor(other.streamId), isNotNull);
|
||||
expect(h.store.totalBytes, other.storedBytes);
|
||||
},
|
||||
);
|
||||
|
||||
test('deleteImageForSentinel takes the message text', () async {
|
||||
final h = _build();
|
||||
@@ -950,9 +987,8 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
FileReceivedImageBlobStore newStore() => FileReceivedImageBlobStore(
|
||||
baseDirectory: () async => tempDir,
|
||||
);
|
||||
FileReceivedImageBlobStore newStore() =>
|
||||
FileReceivedImageBlobStore(baseDirectory: () async => tempDir);
|
||||
|
||||
test('round-trips bytes and sidecars through real files', () async {
|
||||
final blobs = newStore();
|
||||
@@ -964,7 +1000,9 @@ void main() {
|
||||
expect(await blobs.readPng('1a2b0700693f21'), _payload(4096, 3));
|
||||
expect(await blobs.bitstreamSize('1a2b0700693f21'), 155);
|
||||
expect(await blobs.pngSize('1a2b0700693f21'), 4096);
|
||||
expect(await blobs.readSidecars(), {'1a2b0700693f21': '{"streamId":"x"}'});
|
||||
expect(await blobs.readSidecars(), {
|
||||
'1a2b0700693f21': '{"streamId":"x"}',
|
||||
});
|
||||
expect(blobs.pngPath('1a2b0700693f21'), endsWith('1a2b0700693f21.png'));
|
||||
expect(File(blobs.pngPath('1a2b0700693f21')!).existsSync(), isTrue);
|
||||
|
||||
@@ -1044,8 +1082,10 @@ void main() {
|
||||
at: now,
|
||||
);
|
||||
await store.settle();
|
||||
expect(store.entryFor(entry!.streamId)!.state,
|
||||
ReceivedImageState.decoded);
|
||||
expect(
|
||||
store.entryFor(entry!.streamId)!.state,
|
||||
ReceivedImageState.decoded,
|
||||
);
|
||||
store.dispose();
|
||||
|
||||
final reborn = ReceivedImageStore(
|
||||
@@ -1055,8 +1095,11 @@ void main() {
|
||||
);
|
||||
await reborn.load();
|
||||
final loaded = reborn.entryFor(entry.streamId);
|
||||
expect(loaded, isNotNull,
|
||||
reason: 'the whole point: images outlive the process');
|
||||
expect(
|
||||
loaded,
|
||||
isNotNull,
|
||||
reason: 'the whole point: images outlive the process',
|
||||
);
|
||||
expect(loaded!.state, ReceivedImageState.decoded);
|
||||
expect(loaded.pngStored, isTrue);
|
||||
expect(loaded.pngBytes, isNull, reason: 'pixels are read lazily');
|
||||
@@ -1098,33 +1141,35 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('listeners fire for the message list and for the single bubble',
|
||||
() async {
|
||||
final h = _build();
|
||||
var storeNotifications = 0;
|
||||
h.store.addListener(() => storeNotifications++);
|
||||
final set = buildImageChunks(
|
||||
payload: _payload(kImageChunkFirstCapacity + 1),
|
||||
metadata: const ImageStreamMetadata(rate: ImageCodecRatePoint.standard),
|
||||
senderPrefix: kSender,
|
||||
imgId: 77,
|
||||
);
|
||||
final first = await h.store.handleOutcome(
|
||||
h.reassembler.addChunk(set.blobs[0], channelIndex: 3),
|
||||
channelIndex: 3,
|
||||
);
|
||||
final listenable = h.store.listenableFor(first!.streamId);
|
||||
var bubbleNotifications = 0;
|
||||
listenable.addListener(() => bubbleNotifications++);
|
||||
await h.store.handleOutcome(
|
||||
h.reassembler.addChunk(set.blobs[1], channelIndex: 3),
|
||||
channelIndex: 3,
|
||||
);
|
||||
await h.store.settle();
|
||||
expect(storeNotifications, greaterThan(1));
|
||||
expect(bubbleNotifications, greaterThan(1));
|
||||
expect(listenable.value!.state, ReceivedImageState.decoded);
|
||||
});
|
||||
test(
|
||||
'listeners fire for the message list and for the single bubble',
|
||||
() async {
|
||||
final h = _build();
|
||||
var storeNotifications = 0;
|
||||
h.store.addListener(() => storeNotifications++);
|
||||
final set = buildImageChunks(
|
||||
payload: _payload(kImageChunkFirstCapacity + 1),
|
||||
metadata: const ImageStreamMetadata(rate: ImageCodecRatePoint.standard),
|
||||
senderPrefix: kSender,
|
||||
imgId: 77,
|
||||
);
|
||||
final first = await h.store.handleOutcome(
|
||||
h.reassembler.addChunk(set.blobs[0], channelIndex: 3),
|
||||
channelIndex: 3,
|
||||
);
|
||||
final listenable = h.store.listenableFor(first!.streamId);
|
||||
var bubbleNotifications = 0;
|
||||
listenable.addListener(() => bubbleNotifications++);
|
||||
await h.store.handleOutcome(
|
||||
h.reassembler.addChunk(set.blobs[1], channelIndex: 3),
|
||||
channelIndex: 3,
|
||||
);
|
||||
await h.store.settle();
|
||||
expect(storeNotifications, greaterThan(1));
|
||||
expect(bubbleNotifications, greaterThan(1));
|
||||
expect(listenable.value!.state, ReceivedImageState.decoded);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Delivers a whole single-chunk image and returns its entry (state
|
||||
|
||||
Reference in New Issue
Block a user