874 lines
32 KiB
Odin
874 lines
32 KiB
Odin
package pdf
|
|||
|
|
|
||
|
|
import "core:math"
|
||
|
|
import "core:testing"
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Shared fixtures for the colour and function tests
|
||
|
|
//
|
||
|
|
// Both layers need a Document to allocate from and to resolve references
|
||
|
|
// against, and a few objects that can only be reached by reference: a function
|
||
|
|
// that names itself, a colour space named from a resource dictionary. The file
|
||
|
|
// below is opened by `cf_open`; its cross-reference table is deliberately
|
||
|
|
// absent, which the reader repairs by scanning, because what these tests need
|
||
|
|
// from it is objects, not structure.
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
CF_PDF: string : "%PDF-1.7\n" +
|
||
|
|
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" +
|
||
|
|
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" +
|
||
|
|
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>\nendobj\n" +
|
||
|
|
// A spot colour: full tint is 100% magenta and yellow, i.e. red.
|
||
|
|
"5 0 obj\n[/Separation /Spot /DeviceCMYK 6 0 R]\nendobj\n" +
|
||
|
|
"6 0 obj\n<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 1 1 0] /N 1 >>\nendobj\n" +
|
||
|
|
// A stitching function whose first sub-function is itself.
|
||
|
|
"7 0 obj\n<< /FunctionType 3 /Domain [0 1] /Functions [7 0 R 8 0 R] " +
|
||
|
|
"/Bounds [0.5] /Encode [0 1 0 1] >>\nendobj\n" +
|
||
|
|
"8 0 obj\n<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>\nendobj\n" +
|
||
|
|
// A colour space that names itself through the resource dictionary.
|
||
|
|
"9 0 obj\n/Self\nendobj\n" +
|
||
|
|
// An Indexed space with a four-entry table, reached by reference.
|
||
|
|
"10 0 obj\n[/Indexed /DeviceRGB 3 <FF0000 00FF00 0000FF FFFFFF>]\nendobj\n" +
|
||
|
|
"trailer\n<< /Size 11 /Root 1 0 R >>\n"
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
cf_open :: proc(t: ^testing.T) -> ^Document {
|
||
|
|
doc, err := open_bytes(transmute([]byte)CF_PDF, Open_Options{skip_page_tree = true})
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
testing.expect(t, doc != nil, "fixture document did not open")
|
||
|
|
return doc
|
||
|
|
}
|
||
|
|
|
||
|
|
// cf_parse parses one object out of a source literal into the Document's
|
||
|
|
// arena. The literal is static, which is what lets names and strings borrow
|
||
|
|
// from it for as long as the Document lives.
|
||
|
|
@(private)
|
||
|
|
cf_parse :: proc(doc: ^Document, src: string) -> Object {
|
||
|
|
p := parser_make(doc, transmute([]byte)src, doc.allocator)
|
||
|
|
o, _ := parse_object(&p)
|
||
|
|
return o
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
cf_bytes :: proc(s: string) -> []byte {
|
||
|
|
return transmute([]byte)s
|
||
|
|
}
|
||
|
|
|
||
|
|
// cf_stream builds a stream out of a dictionary literal and a payload. The
|
||
|
|
// payload is stored, not copied, so it must be static or arena memory.
|
||
|
|
@(private)
|
||
|
|
cf_stream :: proc(doc: ^Document, dict_src: string, payload: []byte) -> ^Stream {
|
||
|
|
d, _ := to_dict(cf_parse(doc, dict_src))
|
||
|
|
return new_stream(d, payload, doc.allocator)
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
cf_close :: proc(t: ^testing.T, doc: ^Document) {
|
||
|
|
close(doc)
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
EPS :: 1e-9
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
expect_near :: proc(t: ^testing.T, got, want: f64, tol: f64, what: string,
|
||
|
|
loc := #caller_location) {
|
||
|
|
if !(abs(got - want) <= tol) {
|
||
|
|
testing.expectf(t, false, "%s: got %f, want %f", what, got, want, loc = loc)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// eval1 evaluates a one-output function at one input.
|
||
|
|
@(private = "file")
|
||
|
|
eval1 :: proc(f: ^Function, x: f64) -> f64 {
|
||
|
|
if f == nil {
|
||
|
|
return math.QNAN_F64
|
||
|
|
}
|
||
|
|
in_ := [1]f64{x}
|
||
|
|
out: [MAX_FUNCTION_OUTPUTS]f64
|
||
|
|
function_eval(f, in_[:], out[:max(f.n_out, 1)])
|
||
|
|
return out[0]
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
load_fn :: proc(t: ^testing.T, doc: ^Document, src: string) -> ^Function {
|
||
|
|
f, err := function_load(doc, cf_parse(doc, src))
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
return f
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
load_sampled :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte) -> ^Function {
|
||
|
|
f, err := function_load(doc, cf_stream(doc, dict_src, payload))
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
return f
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
load_ps :: proc(t: ^testing.T, doc: ^Document, program: string) -> ^Function {
|
||
|
|
return load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 4 /Domain [0 1] /Range [-100000 100000] >>", cf_bytes(program))
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Type 2: exponential interpolation
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type2_defaults :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// C0 defaults to [0.0] and C1 to [1.0], so this is the identity.
|
||
|
|
f := load_fn(t, doc, "<< /FunctionType 2 /Domain [0 1] /N 1 >>")
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_in, 1)
|
||
|
|
testing.expect_value(t, f.n_out, 1)
|
||
|
|
expect_near(t, eval1(f, 0), 0, EPS, "f(0)")
|
||
|
|
expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25)")
|
||
|
|
expect_near(t, eval1(f, 1), 1, EPS, "f(1)")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type2_exponent_and_outputs :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// C0 + x^N (C1 - C0) with N = 2: at x = 0.5 the factor is 0.25.
|
||
|
|
f := load_fn(t, doc,
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [1 0.5 0] /N 2 >>")
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_out, 3)
|
||
|
|
|
||
|
|
in_ := [1]f64{0.5}
|
||
|
|
out: [3]f64
|
||
|
|
function_eval(f, in_[:], out[:])
|
||
|
|
expect_near(t, out[0], 0.25, EPS, "out0")
|
||
|
|
expect_near(t, out[1], 0.125, EPS, "out1")
|
||
|
|
expect_near(t, out[2], 0, EPS, "out2")
|
||
|
|
|
||
|
|
// C0 is returned exactly at the bottom of the domain whatever N is.
|
||
|
|
in_[0] = 0
|
||
|
|
function_eval(f, in_[:], out[:])
|
||
|
|
expect_near(t, out[0], 0, EPS, "out0 at 0")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A negative base under a fractional exponent has no real value. The domain is
|
||
|
|
// supposed to exclude it; a file whose domain does not must still produce a
|
||
|
|
// number rather than a NaN.
|
||
|
|
@(test)
|
||
|
|
test_function_type2_negative_base :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f := load_fn(t, doc,
|
||
|
|
"<< /FunctionType 2 /Domain [-1 1] /C0 [0] /C1 [1] /N 0.5 >>")
|
||
|
|
if f == nil { return }
|
||
|
|
v := eval1(f, -1)
|
||
|
|
testing.expect(t, is_finite(v), "a negative base must not produce a NaN")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Domain and range clamping (ISO 32000-2, 7.10.1)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_domain_clamped :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f := load_fn(t, doc, "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>")
|
||
|
|
if f == nil { return }
|
||
|
|
// Inputs outside /Domain are clamped to it, not extrapolated.
|
||
|
|
expect_near(t, eval1(f, 5), 1, EPS, "f(5)")
|
||
|
|
expect_near(t, eval1(f, -5), 0, EPS, "f(-5)")
|
||
|
|
// And a non-finite input cannot leak through into the output.
|
||
|
|
testing.expect(t, is_finite(eval1(f, math.INF_F64)), "infinite input")
|
||
|
|
testing.expect(t, is_finite(eval1(f, math.QNAN_F64)), "NaN input")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_range_clamped :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// The interpolation reaches 1.0 but /Range stops at 0.5.
|
||
|
|
f := load_fn(t, doc,
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /Range [0 0.5] /C0 [0] /C1 [1] /N 1 >>")
|
||
|
|
if f == nil { return }
|
||
|
|
expect_near(t, eval1(f, 1), 0.5, EPS, "f(1) clamped to /Range")
|
||
|
|
expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25) inside /Range")
|
||
|
|
|
||
|
|
// A type 4 program is clamped the same way: without it a shading paints
|
||
|
|
// whatever the calculator happened to leave behind.
|
||
|
|
g := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", cf_bytes("{ pop 42 }"))
|
||
|
|
if g == nil { return }
|
||
|
|
expect_near(t, eval1(g, 0), 1, EPS, "42 clamped to /Range")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Type 0: sampled
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type0_linear :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Two 8-bit samples, 0 and 255, over [0 1].
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF})
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_in, 1)
|
||
|
|
testing.expect_value(t, f.n_out, 1)
|
||
|
|
expect_near(t, eval1(f, 0), 0, EPS, "f(0)")
|
||
|
|
expect_near(t, eval1(f, 1), 1, EPS, "f(1)")
|
||
|
|
// Halfway between the two samples is 127.5/255.
|
||
|
|
expect_near(t, eval1(f, 0.5), 0.5, EPS, "f(0.5)")
|
||
|
|
expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25)")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type0_two_outputs :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Two samples of two outputs each, interleaved: (0,255) then (255,0).
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1 0 1] /Size [2] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF, 0xFF, 0x00})
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_out, 2)
|
||
|
|
|
||
|
|
in_ := [1]f64{0.5}
|
||
|
|
out: [2]f64
|
||
|
|
function_eval(f, in_[:], out[:])
|
||
|
|
expect_near(t, out[0], 0.5, EPS, "out0 midway")
|
||
|
|
expect_near(t, out[1], 0.5, EPS, "out1 midway")
|
||
|
|
|
||
|
|
in_[0] = 1
|
||
|
|
function_eval(f, in_[:], out[:])
|
||
|
|
expect_near(t, out[0], 1, EPS, "out0 at 1")
|
||
|
|
expect_near(t, out[1], 0, EPS, "out1 at 1")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A 2x2 table: the first input varies fastest (7.10.2), so the samples are
|
||
|
|
// (0,0) (1,0) (0,1) (1,1).
|
||
|
|
@(test)
|
||
|
|
test_function_type0_bilinear :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1 0 1] /Range [0 1] /Size [2 2] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF, 0xFF, 0x00})
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_in, 2)
|
||
|
|
|
||
|
|
check :: proc(t: ^testing.T, f: ^Function, x, y, want: f64, what: string) {
|
||
|
|
in_ := [2]f64{x, y}
|
||
|
|
out: [1]f64
|
||
|
|
function_eval(f, in_[:], out[:])
|
||
|
|
if !(abs(out[0] - want) <= 1e-9) {
|
||
|
|
testing.expectf(t, false, "%s: got %f, want %f", what, out[0], want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
check(t, f, 0, 0, 0, "corner (0,0)")
|
||
|
|
check(t, f, 1, 0, 1, "corner (1,0)")
|
||
|
|
check(t, f, 0, 1, 1, "corner (0,1)")
|
||
|
|
check(t, f, 1, 1, 0, "corner (1,1)")
|
||
|
|
// The centre averages all four corners, which is what makes this a test of
|
||
|
|
// interpolation rather than of nearest-neighbour lookup.
|
||
|
|
check(t, f, 0.5, 0.5, 0.5, "centre")
|
||
|
|
check(t, f, 0.5, 0, 0.5, "midway along x")
|
||
|
|
check(t, f, 0.25, 0, 0.25, "quarter along x")
|
||
|
|
check(t, f, 0, 0.75, 0.75, "three quarters along y")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Three inputs, eight samples: sample value 0 or 255 by parity, so every
|
||
|
|
// corner and the centre have hand-checkable values.
|
||
|
|
@(test)
|
||
|
|
test_function_type0_three_inputs :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Index = i0 + 2*i1 + 4*i2. Values: 0, 255, 0, 255, 0, 255, 0, 255 -- so
|
||
|
|
// the table depends only on the first input.
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1 0 1 0 1] /Range [0 1] /Size [2 2 2] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF})
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_in, 3)
|
||
|
|
|
||
|
|
check :: proc(t: ^testing.T, f: ^Function, x, y, z, want: f64, what: string) {
|
||
|
|
in_ := [3]f64{x, y, z}
|
||
|
|
out: [1]f64
|
||
|
|
function_eval(f, in_[:], out[:])
|
||
|
|
if !(abs(out[0] - want) <= 1e-9) {
|
||
|
|
testing.expectf(t, false, "%s: got %f, want %f", what, out[0], want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
check(t, f, 0, 0, 0, 0, "origin")
|
||
|
|
check(t, f, 1, 0, 0, 1, "along x")
|
||
|
|
check(t, f, 0, 1, 1, 0, "y and z do not matter")
|
||
|
|
check(t, f, 0.5, 0.5, 0.5, 0.5, "centre")
|
||
|
|
check(t, f, 0.25, 1, 0.5, 0.25, "quarter along x, anywhere else")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Every bit width the format allows (Table 38). One sample of all zero bits
|
||
|
|
// and one of all one bits, so the decoded values must be exactly 0 and 1
|
||
|
|
// whatever the width is.
|
||
|
|
@(test)
|
||
|
|
test_function_type0_bit_widths :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
Case :: struct {
|
||
|
|
dict: string,
|
||
|
|
payload: []byte,
|
||
|
|
}
|
||
|
|
cases := []Case{
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 1 >>",
|
||
|
|
[]byte{0b0100_0000}},
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 2 >>",
|
||
|
|
[]byte{0b0011_0000}},
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 4 >>",
|
||
|
|
[]byte{0x0F}},
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF}},
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 12 >>",
|
||
|
|
[]byte{0x00, 0x0F, 0xFF}},
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 16 >>",
|
||
|
|
[]byte{0x00, 0x00, 0xFF, 0xFF}},
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 24 >>",
|
||
|
|
[]byte{0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF}},
|
||
|
|
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 32 >>",
|
||
|
|
[]byte{0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}},
|
||
|
|
}
|
||
|
|
for c, i in cases {
|
||
|
|
f := load_sampled(t, doc, c.dict, c.payload)
|
||
|
|
if f == nil {
|
||
|
|
testing.expectf(t, false, "case %d did not load", i)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
expect_near(t, eval1(f, 0), 0, EPS, "low sample")
|
||
|
|
expect_near(t, eval1(f, 1), 1, EPS, "high sample")
|
||
|
|
expect_near(t, eval1(f, 0.5), 0.5, EPS, "midpoint")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A width the format does not define is refused rather than guessed at.
|
||
|
|
_, err := function_load(doc, cf_stream(doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 7 >>",
|
||
|
|
[]byte{0x00, 0xFF}))
|
||
|
|
testing.expect_value(t, err, Error.Bad_Function)
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type0_encode_and_decode :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// /Encode reverses the table: input 0 reads the last sample.
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 " +
|
||
|
|
"/Encode [1 0] >>",
|
||
|
|
[]byte{0x00, 0xFF})
|
||
|
|
if f == nil { return }
|
||
|
|
expect_near(t, eval1(f, 0), 1, EPS, "reversed f(0)")
|
||
|
|
expect_near(t, eval1(f, 1), 0, EPS, "reversed f(1)")
|
||
|
|
|
||
|
|
// /Decode maps the raw samples onto a different interval than /Range does.
|
||
|
|
g := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 100] /Size [2] /BitsPerSample 8 " +
|
||
|
|
"/Decode [10 20] >>",
|
||
|
|
[]byte{0x00, 0xFF})
|
||
|
|
if g == nil { return }
|
||
|
|
expect_near(t, eval1(g, 0), 10, EPS, "decoded low")
|
||
|
|
expect_near(t, eval1(g, 1), 20, EPS, "decoded high")
|
||
|
|
expect_near(t, eval1(g, 0.5), 15, EPS, "decoded midpoint")
|
||
|
|
|
||
|
|
// A domain that is not [0 1] is mapped onto the samples by /Encode's
|
||
|
|
// default, which is [0 Size-1].
|
||
|
|
h := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [100 200] /Range [0 1] /Size [2] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF})
|
||
|
|
if h == nil { return }
|
||
|
|
expect_near(t, eval1(h, 100), 0, EPS, "domain low")
|
||
|
|
expect_near(t, eval1(h, 150), 0.5, EPS, "domain middle")
|
||
|
|
expect_near(t, eval1(h, 200), 1, EPS, "domain high")
|
||
|
|
}
|
||
|
|
|
||
|
|
// /Order 3 asks for a cubic spline; linear interpolation is substituted, which
|
||
|
|
// the format allows. The function must still load and still interpolate.
|
||
|
|
@(test)
|
||
|
|
test_function_type0_cubic_falls_back :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 /Order 3 >>",
|
||
|
|
[]byte{0x00, 0xFF})
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.order, 3)
|
||
|
|
expect_near(t, eval1(f, 0.5), 0.5, EPS, "cubic falls back to linear")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A sample table shorter than /Size claims is common in truncated files. The
|
||
|
|
// missing samples read as zero; nothing reads past the buffer.
|
||
|
|
@(test)
|
||
|
|
test_function_type0_truncated_samples :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [16] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0xFF, 0xFF})
|
||
|
|
if f == nil { return }
|
||
|
|
expect_near(t, eval1(f, 0), 1, EPS, "the samples that are there")
|
||
|
|
expect_near(t, eval1(f, 1), 0, EPS, "the ones that are not read as zero")
|
||
|
|
testing.expect(t, doc_has_repair(doc, .Bad_Resource), "a short table is a repair")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type0_bad_sizes :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// A zero dimension would make every index into the table invalid.
|
||
|
|
f := load_sampled(t, doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [0] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x80})
|
||
|
|
if f != nil {
|
||
|
|
testing.expect(t, is_finite(eval1(f, 0.5)), "a zero /Size must still evaluate")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A table with more samples than the library will hold is refused, not
|
||
|
|
// allocated.
|
||
|
|
_, err := function_load(doc, cf_stream(doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2000000000] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF}))
|
||
|
|
testing.expect_value(t, err, Error.Bad_Function)
|
||
|
|
|
||
|
|
// /Range is what says how many outputs a sample carries; without it the
|
||
|
|
// table cannot be read at all.
|
||
|
|
_, err2 := function_load(doc, cf_stream(doc,
|
||
|
|
"<< /FunctionType 0 /Domain [0 1] /Size [2] /BitsPerSample 8 >>",
|
||
|
|
[]byte{0x00, 0xFF}))
|
||
|
|
testing.expect_value(t, err2, Error.Bad_Function)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Type 3: stitching
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type3_boundary :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Two sub-functions that disagree at the bound: the first ends at 0.5, the
|
||
|
|
// second starts at 1.0. An input exactly on the bound belongs to the later
|
||
|
|
// one (7.10.4), so 0.5 must read 1.0 and not 0.5.
|
||
|
|
f := load_fn(t, doc,
|
||
|
|
"<< /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [" +
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0.5] /N 1 >> " +
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] >>")
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_in, 1)
|
||
|
|
testing.expect_value(t, f.n_out, 1)
|
||
|
|
|
||
|
|
// First half: [0 0.5] encodes onto [0 1], so 0.25 lands at 0.5 of a
|
||
|
|
// function that runs 0 -> 0.5.
|
||
|
|
expect_near(t, eval1(f, 0), 0, EPS, "start of the first sub-function")
|
||
|
|
expect_near(t, eval1(f, 0.25), 0.25, EPS, "middle of the first sub-function")
|
||
|
|
expect_near(t, eval1(f, 0.5), 1, EPS, "exactly on the bound is the later function")
|
||
|
|
expect_near(t, eval1(f, 0.75), 0.5, EPS, "middle of the second sub-function")
|
||
|
|
expect_near(t, eval1(f, 1), 0, EPS, "end of the domain")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type3_three_way :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Three constant sub-functions, so the value alone says which one ran.
|
||
|
|
f := load_fn(t, doc,
|
||
|
|
"<< /FunctionType 3 /Domain [0 1] /Bounds [0.25 0.75] /Encode [0 1 0 1 0 1] /Functions [" +
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [10] /C1 [10] /N 1 >> " +
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [20] /C1 [20] /N 1 >> " +
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [30] /C1 [30] /N 1 >>] >>")
|
||
|
|
if f == nil { return }
|
||
|
|
expect_near(t, eval1(f, 0), 10, EPS, "below the first bound")
|
||
|
|
expect_near(t, eval1(f, 0.24), 10, EPS, "just below the first bound")
|
||
|
|
expect_near(t, eval1(f, 0.25), 20, EPS, "on the first bound")
|
||
|
|
expect_near(t, eval1(f, 0.74), 20, EPS, "just below the second bound")
|
||
|
|
expect_near(t, eval1(f, 0.75), 30, EPS, "on the second bound")
|
||
|
|
expect_near(t, eval1(f, 1), 30, EPS, "top of the domain")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A stitching function that lists itself among its sub-functions. Loading must
|
||
|
|
// terminate, and the branch that is not self-referential must still work.
|
||
|
|
@(test)
|
||
|
|
test_function_type3_self_reference :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f, err := function_load(doc, Ref{7, 0})
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
if f == nil { return }
|
||
|
|
|
||
|
|
// The second sub-function is the ordinary one: [0.5 1] encodes onto [0 1],
|
||
|
|
// so 0.75 lands halfway up a ramp from 0 to 1.
|
||
|
|
expect_near(t, eval1(f, 0.75), 0.5, EPS, "the sub-function that is not a cycle")
|
||
|
|
// And the self-referential branch terminates with a defined value.
|
||
|
|
testing.expect(t, is_finite(eval1(f, 0.25)), "the cyclic branch must terminate")
|
||
|
|
testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), "the cycle is a repair")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type3_broken_sub_function :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// One sub-function is nonsense. The other must still be reachable.
|
||
|
|
f := load_fn(t, doc,
|
||
|
|
"<< /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [" +
|
||
|
|
"<< /FunctionType 9 >> " +
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [7] /C1 [7] /N 1 >>] >>")
|
||
|
|
if f == nil { return }
|
||
|
|
expect_near(t, eval1(f, 0.75), 7, EPS, "the sub-function that loaded")
|
||
|
|
expect_near(t, eval1(f, 0.25), 0, EPS, "the one that did not reads as zero")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Type 4: the PostScript calculator
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type4_operators :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
Case :: struct {
|
||
|
|
program: string,
|
||
|
|
want: f64,
|
||
|
|
}
|
||
|
|
// Every case is evaluated at x = 0, and the input stays on the stack below
|
||
|
|
// the result, so the value asserted is the top of the stack.
|
||
|
|
cases := []Case{
|
||
|
|
// Arithmetic.
|
||
|
|
{"{ 2 3 add }", 5},
|
||
|
|
{"{ 5 3 sub }", 2},
|
||
|
|
{"{ 3 4 mul }", 12},
|
||
|
|
{"{ 7 2 div }", 3.5},
|
||
|
|
{"{ 7 2 idiv }", 3},
|
||
|
|
{"{ -7 2 idiv }", -3},
|
||
|
|
{"{ 7 2 mod }", 1},
|
||
|
|
{"{ 5 neg }", -5},
|
||
|
|
{"{ -5 abs }", 5},
|
||
|
|
{"{ 1.2 ceiling }", 2},
|
||
|
|
{"{ -1.2 ceiling }", -1},
|
||
|
|
{"{ 1.8 floor }", 1},
|
||
|
|
{"{ -1.8 floor }", -2},
|
||
|
|
{"{ 2.5 round }", 3},
|
||
|
|
// PostScript rounds a halfway case towards positive infinity.
|
||
|
|
{"{ -2.5 round }", -2},
|
||
|
|
{"{ 1.9 truncate }", 1},
|
||
|
|
{"{ -1.9 truncate }", -1},
|
||
|
|
{"{ 9 sqrt }", 3},
|
||
|
|
{"{ 90 sin }", 1},
|
||
|
|
{"{ 0 sin }", 0},
|
||
|
|
{"{ 180 cos }", -1},
|
||
|
|
{"{ 0 cos }", 1},
|
||
|
|
// atan takes num then den and answers in degrees, never negative.
|
||
|
|
{"{ 1 0 atan }", 90},
|
||
|
|
{"{ 0 -1 atan }", 180},
|
||
|
|
{"{ -1 0 atan }", 270},
|
||
|
|
{"{ 0 1 atan }", 0},
|
||
|
|
{"{ 2 10 exp }", 1024},
|
||
|
|
{"{ 9 0.5 exp }", 3},
|
||
|
|
{"{ 1 ln }", 0},
|
||
|
|
{"{ 100 log }", 2},
|
||
|
|
{"{ 3.9 cvi }", 3},
|
||
|
|
{"{ -3.9 cvi }", -3},
|
||
|
|
{"{ 3 cvr }", 3},
|
||
|
|
|
||
|
|
// Relational and boolean. Each result is turned back into a number by
|
||
|
|
// an ifelse so that one assertion covers both.
|
||
|
|
{"{ 1 1 eq { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ 1 2 eq { 10 } { 20 } ifelse }", 20},
|
||
|
|
{"{ 1 2 ne { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ 2 1 gt { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ 1 1 gt { 10 } { 20 } ifelse }", 20},
|
||
|
|
{"{ 2 2 ge { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ 1 2 lt { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ 2 2 le { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ true false and { 10 } { 20 } ifelse }", 20},
|
||
|
|
{"{ true true and { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ true false or { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ true true xor { 10 } { 20 } ifelse }", 20},
|
||
|
|
{"{ true false xor { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ false not { 10 } { 20 } ifelse }", 10},
|
||
|
|
{"{ true not { 10 } { 20 } ifelse }", 20},
|
||
|
|
// On integers the same four operators are bitwise, not logical.
|
||
|
|
{"{ 12 10 and }", 8},
|
||
|
|
{"{ 12 10 or }", 14},
|
||
|
|
{"{ 12 10 xor }", 6},
|
||
|
|
{"{ 0 not }", -1},
|
||
|
|
{"{ 1 3 bitshift }", 8},
|
||
|
|
{"{ 16 -3 bitshift }", 2},
|
||
|
|
|
||
|
|
// Stack operators.
|
||
|
|
{"{ 1 2 pop }", 1},
|
||
|
|
{"{ 1 2 exch }", 1},
|
||
|
|
{"{ 7 dup add }", 14},
|
||
|
|
{"{ 5 7 2 copy add }", 12},
|
||
|
|
{"{ 1 2 3 2 index }", 1},
|
||
|
|
{"{ 1 2 3 3 1 roll }", 2},
|
||
|
|
{"{ 1 2 3 3 -1 roll }", 1},
|
||
|
|
{"{ 1 2 3 3 0 roll }", 3},
|
||
|
|
|
||
|
|
// Conditionals.
|
||
|
|
{"{ true { 10 } if }", 10},
|
||
|
|
// Nothing is pushed, so the top of the stack is still the input, 0.
|
||
|
|
{"{ false { 10 } if }", 0},
|
||
|
|
{"{ true { 1 2 add } { 99 } ifelse }", 3},
|
||
|
|
{"{ false { 99 } { 1 2 add } ifelse }", 3},
|
||
|
|
// Nested conditionals, and a branch that is several operators long.
|
||
|
|
{"{ true { false { 1 } { 2 } ifelse } { 3 } ifelse }", 2},
|
||
|
|
{"{ 3 4 lt { 3 4 add 2 mul } { 0 } ifelse }", 14},
|
||
|
|
|
||
|
|
// The input itself is on the stack when the program starts.
|
||
|
|
{"{ 1 add }", 1},
|
||
|
|
// A comment runs to the end of the line.
|
||
|
|
{"{ 2 3 add % this is ignored\n }", 5},
|
||
|
|
// A word this calculator has no operator for is stepped over rather
|
||
|
|
// than abandoning the program.
|
||
|
|
{"{ 2 3 add nosuchop }", 5},
|
||
|
|
}
|
||
|
|
|
||
|
|
for c, i in cases {
|
||
|
|
f := load_ps(t, doc, c.program)
|
||
|
|
if f == nil {
|
||
|
|
testing.expectf(t, false, "case %d (%s) did not load", i, c.program)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
got := eval1(f, 0)
|
||
|
|
if !(abs(got - c.want) <= 1e-9) {
|
||
|
|
testing.expectf(t, false, "%s: got %f, want %f", c.program, got, c.want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Division by zero, the square root of a negative, the logarithm of zero and
|
||
|
|
// an empty stack must all produce a defined value rather than a NaN, an
|
||
|
|
// infinity or a crash.
|
||
|
|
@(test)
|
||
|
|
test_function_type4_defined_on_bad_input :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
Case :: struct {
|
||
|
|
program: string,
|
||
|
|
want: f64,
|
||
|
|
}
|
||
|
|
cases := []Case{
|
||
|
|
{"{ 1 0 div }", 0},
|
||
|
|
{"{ 1 0 idiv }", 0},
|
||
|
|
{"{ 1 0 mod }", 0},
|
||
|
|
{"{ -4 sqrt }", 0},
|
||
|
|
{"{ 0 ln }", 0},
|
||
|
|
{"{ -1 ln }", 0},
|
||
|
|
{"{ 0 log }", 0},
|
||
|
|
// Every operand gone, then an operator that wants two.
|
||
|
|
{"{ pop add }", 0},
|
||
|
|
{"{ pop pop pop sub }", 0},
|
||
|
|
{"{ pop dup }", 0},
|
||
|
|
{"{ pop exch }", 0},
|
||
|
|
// exp overflowing to infinity must not leave an infinity behind.
|
||
|
|
{"{ 1000 1000 exp }", 0},
|
||
|
|
// index past the bottom of the stack.
|
||
|
|
{"{ 99 index }", 0},
|
||
|
|
// roll and copy with counts that make no sense.
|
||
|
|
{"{ 1 2 -3 1 roll }", 2},
|
||
|
|
{"{ 1 2 99 1 roll }", 2},
|
||
|
|
{"{ 5 -1 copy }", 5},
|
||
|
|
// A block with no `if` after it never runs.
|
||
|
|
{"{ 5 { 99 } }", 5},
|
||
|
|
// An `if` with no block before it is skipped.
|
||
|
|
{"{ 5 if }", 5},
|
||
|
|
}
|
||
|
|
for c in cases {
|
||
|
|
f := load_ps(t, doc, c.program)
|
||
|
|
if f == nil {
|
||
|
|
testing.expectf(t, false, "%s did not load", c.program)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
got := eval1(f, 0)
|
||
|
|
testing.expectf(t, is_finite(got), "%s produced a non-finite value", c.program)
|
||
|
|
if !(abs(got - c.want) <= 1e-9) {
|
||
|
|
testing.expectf(t, false, "%s: got %f, want %f", c.program, got, c.want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A program that pushes more than the hundred operands the format allows
|
||
|
|
// (7.10.5) stops cleanly and still produces a value.
|
||
|
|
@(test)
|
||
|
|
test_function_type4_stack_overflow :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// 200 pushes, in arena memory so that the stream can keep pointing at it.
|
||
|
|
src := make([]byte, 2 + 2 * 200 + 1, doc.allocator)
|
||
|
|
src[0], src[1] = '{', ' '
|
||
|
|
for i in 0 ..< 200 {
|
||
|
|
src[2 + 2 * i] = '1'
|
||
|
|
src[3 + 2 * i] = ' '
|
||
|
|
}
|
||
|
|
src[len(src) - 1] = '}'
|
||
|
|
|
||
|
|
f := load_sampled(t, doc, "<< /FunctionType 4 /Domain [0 1] /Range [0 10] >>", src)
|
||
|
|
if f == nil { return }
|
||
|
|
got := eval1(f, 0)
|
||
|
|
testing.expect(t, is_finite(got), "an overflowing program still produces a value")
|
||
|
|
expect_near(t, got, 1, EPS, "the last value pushed before the stack filled")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A program that jumps backwards forever is not something a compiler here can
|
||
|
|
// produce, but a Function is a plain struct and the interpreter has to survive
|
||
|
|
// one anyway. The step budget is what ends it.
|
||
|
|
@(test)
|
||
|
|
test_function_type4_infinite_loop_terminates :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f := new(Function, doc.allocator)
|
||
|
|
f.kind = .PostScript
|
||
|
|
f.n_in = 1
|
||
|
|
f.n_out = 1
|
||
|
|
f.domain = fn_make_pair(doc, 0, 1)
|
||
|
|
f.range = fn_make_pair(doc, 0, 1)
|
||
|
|
prog := make([]Ps_Op, 1, doc.allocator)
|
||
|
|
prog[0] = Ps_Op{kind = .Jump, target = 0}
|
||
|
|
f.program = prog
|
||
|
|
|
||
|
|
// Reaching the assertion at all is the test: without a step budget this
|
||
|
|
// never returns.
|
||
|
|
expect_near(t, eval1(f, 0.25), 0.25, EPS, "the input is still on the stack")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type4_deep_nesting :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Braces nested past the compiler's depth limit are refused rather than
|
||
|
|
// recursing the native stack away.
|
||
|
|
n := MAX_PS_DEPTH + 8
|
||
|
|
src := make([]byte, 1 + 2 * n, doc.allocator)
|
||
|
|
src[0] = '{'
|
||
|
|
for i in 0 ..< n {
|
||
|
|
src[1 + i] = '{'
|
||
|
|
}
|
||
|
|
for i in 0 ..< n {
|
||
|
|
src[1 + n + i] = '}'
|
||
|
|
}
|
||
|
|
_, err := function_load(doc, cf_stream(doc,
|
||
|
|
"<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", src))
|
||
|
|
testing.expect_value(t, err, Error.Bad_Function)
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_type4_needs_range :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Without /Range there is no way to know how many of the values left on
|
||
|
|
// the stack are outputs.
|
||
|
|
_, err := function_load(doc, cf_stream(doc,
|
||
|
|
"<< /FunctionType 4 /Domain [0 1] >>", cf_bytes("{ 1 }")))
|
||
|
|
testing.expect_value(t, err, Error.Bad_Function)
|
||
|
|
|
||
|
|
// An empty program is not a function either.
|
||
|
|
_, err2 := function_load(doc, cf_stream(doc,
|
||
|
|
"<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", cf_bytes("")))
|
||
|
|
testing.expect_value(t, err2, Error.Bad_Function)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loading: caching, and what a broken function does
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_cached_by_object :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
a, err_a := function_load(doc, Ref{6, 0})
|
||
|
|
b, err_b := function_load(doc, Ref{6, 0})
|
||
|
|
testing.expect_value(t, err_a, Error.None)
|
||
|
|
testing.expect_value(t, err_b, Error.None)
|
||
|
|
testing.expect(t, a != nil && a == b, "the same object must build one function")
|
||
|
|
|
||
|
|
// A different object is a different function.
|
||
|
|
c, _ := function_load(doc, Ref{8, 0})
|
||
|
|
testing.expect(t, c != nil && c != a, "different objects are different functions")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_function_rejects_nonsense :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
_, e1 := function_load(doc, nil)
|
||
|
|
testing.expect_value(t, e1, Error.Bad_Function)
|
||
|
|
|
||
|
|
_, e2 := function_load(doc, cf_parse(doc, "42"))
|
||
|
|
testing.expect_value(t, e2, Error.Bad_Function)
|
||
|
|
|
||
|
|
// An unknown /FunctionType.
|
||
|
|
_, e3 := function_load(doc, cf_parse(doc, "<< /FunctionType 5 /Domain [0 1] >>"))
|
||
|
|
testing.expect_value(t, e3, Error.Bad_Function)
|
||
|
|
|
||
|
|
// A type 2 that is missing everything still loads, because C0 and C1 have
|
||
|
|
// defaults and /Domain does not need to be trusted.
|
||
|
|
f, e4 := function_load(doc, cf_parse(doc, "<< /FunctionType 2 >>"))
|
||
|
|
testing.expect_value(t, e4, Error.None)
|
||
|
|
if f != nil {
|
||
|
|
expect_near(t, eval1(f, 0.5), 0.5, EPS, "defaulted type 2")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Evaluating nothing writes zeros rather than leaving the caller's buffer
|
||
|
|
// untouched.
|
||
|
|
out := [3]f64{9, 9, 9}
|
||
|
|
in_ := [1]f64{0.5}
|
||
|
|
function_eval(nil, in_[:], out[:])
|
||
|
|
testing.expect_value(t, out[0], 0)
|
||
|
|
testing.expect_value(t, out[1], 0)
|
||
|
|
testing.expect_value(t, out[2], 0)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A caller may ask for fewer outputs than the function produces, and must not
|
||
|
|
// have anything written past the end of its buffer.
|
||
|
|
@(test)
|
||
|
|
test_function_short_output_buffer :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
f := load_fn(t, doc,
|
||
|
|
"<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [1 1 1 1] /N 1 >>")
|
||
|
|
if f == nil { return }
|
||
|
|
testing.expect_value(t, f.n_out, 4)
|
||
|
|
|
||
|
|
guard := [3]f64{0, 0, 77}
|
||
|
|
in_ := [1]f64{1}
|
||
|
|
function_eval(f, in_[:], guard[:2])
|
||
|
|
expect_near(t, guard[0], 1, EPS, "first output")
|
||
|
|
expect_near(t, guard[1], 1, EPS, "second output")
|
||
|
|
testing.expect_value(t, guard[2], 77)
|
||
|
|
}
|