1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
// Guards the nf_mandelbrot contract at the C boundary.
//
// Everything else in the suite goes through lib/nimflutter_ffi.dart, which
// always passes a correctly sized buffer. These tests deliberately bypass it
// and call the raw symbol the way a careless C caller would, because the
// point of `buf_len` is to survive exactly that. Needs the native build on
// the loader path — see widget_test.dart.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nimflutter_ffi/nimflutter_ffi.dart' as v;
typedef _MandelbrotNative = Void Function(Pointer<Uint8>, Size, Int32, Int32,
Double, Double, Double, Int32, Int32, Int32);
typedef _MandelbrotDart = void Function(Pointer<Uint8>, int, int, int, double,
double, double, int, int, int);
void main() {
v.ensureInitialized();
final mandelbrot = DynamicLibrary.open('libnimflutter.so')
.lookupFunction<_MandelbrotNative, _MandelbrotDart>('nf_mandelbrot');
const w = 16;
const h = 16;
const bytes = w * h * 4;
// A canary region past the band. Nothing below may touch it.
const canary = 1024;
const canaryByte = 0xAB;
late Pointer<Uint8> buf;
setUp(() {
buf = calloc<Uint8>(bytes + canary);
for (var i = bytes; i < bytes + canary; i++) {
buf[i] = canaryByte;
}
});
tearDown(() => calloc.free(buf));
bool canaryIntact() {
for (var i = bytes; i < bytes + canary; i++) {
if (buf[i] != canaryByte) return false;
}
return true;
}
/// True if Nim wrote anything into the band at all.
bool bandTouched() {
for (var i = 0; i < bytes; i++) {
if (buf[i] != 0) return true;
}
return false;
}
void expectNoOp(String why) {
expect(bandTouched(), isFalse, reason: '$why: Nim wrote into the band');
expect(canaryIntact(), isTrue, reason: '$why: Nim wrote past the band');
}
test('a correctly sized call fills the whole band', () {
mandelbrot(buf, bytes, w, h, -0.5, 0.0, 3.0, 50, 0, h);
for (var i = 3; i < bytes; i += 4) {
expect(buf[i], 255, reason: 'alpha at pixel ${i ~/ 4}');
}
expect(canaryIntact(), isTrue);
});
test('a buf_len one byte short is refused outright', () {
// The tempting bug is to fill what fits and overrun by one; this must
// write nothing at all.
mandelbrot(buf, bytes - 1, w, h, -0.5, 0.0, 3.0, 50, 0, h);
expectNoOp('buf_len short by one');
});
test('a buf_len sized for a single row is refused', () {
mandelbrot(buf, w * 4, w, h, -0.5, 0.0, 3.0, 50, 0, h);
expectNoOp('buf_len sized for one row');
});
test('a zero buf_len is refused', () {
mandelbrot(buf, 0, w, h, -0.5, 0.0, 3.0, 50, 0, h);
expectNoOp('buf_len zero');
});
test('a null buffer is a no-op, not a crash', () {
mandelbrot(nullptr, bytes, w, h, -0.5, 0.0, 3.0, 50, 0, h);
expectNoOp('null buf');
});
test('an inverted band is refused', () {
mandelbrot(buf, bytes, w, h, -0.5, 0.0, 3.0, 50, 10, 2);
expectNoOp('y1 < y0');
});
test('geometry whose byte count overflows int32 is refused', () {
// (y1 - y0) * w * 4 = 2 * 2^30 * 4 wraps to 0 in 32-bit arithmetic, which
// is the case a length check alone would happily accept.
mandelbrot(buf, bytes, 1 << 30, h, -0.5, 0.0, 3.0, 50, 0, 2);
expectNoOp('w = 1 << 30');
mandelbrot(buf, bytes, w, h, -0.5, 0.0, 3.0, 50, 0, 0x7FFFFFFF);
expectNoOp('y1 = INT32_MAX');
});
}
|