diff --git a/builtins_test.go b/builtins_test.go index 7a63509..31b2f72 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -69,6 +69,9 @@ func TestBuiltinFloat(t *testing.T) { if got := mustRender(t, f, `"{float(1,2,3)}"`); !re.MatchString(got) { t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got) } + if got := mustRender(t, f, `"{float(-1,1,0)}"`); got == "-0" { + t.Fatalf("float(-1,1,0) = %q, want a zero printed unsigned", got) + } } } diff --git a/calc_test.go b/calc_test.go index 77adf77..7a8b7bd 100644 --- a/calc_test.go +++ b/calc_test.go @@ -36,6 +36,7 @@ func TestCalcAuto(t *testing.T) { `"{calc(10 / 3)}"`: "3.3333333333333335", `"{calc(6 / 2)}"`: "3", `"{calc(1 / 4)}"`: "0.25", + `"{calc(0 * -1)}"`: "0", } for tmpl, want := range cases { if got := mustRender(t, f, tmpl); got != want { @@ -52,6 +53,7 @@ func TestCalcDecimals(t *testing.T) { `"{calc(10 / 3, 2)}"`: "3.33", `"{calc(10 / 3, 0)}"`: "3", `"{calc(2 * 3, 2)}"`: "6.00", + `"{calc(-0.001, 2)}"`: "0.00", } for tmpl, want := range cases { if got := mustRender(t, f, tmpl); got != want { diff --git a/datatype_test.go b/datatype_test.go index 995baf2..f2955cf 100644 --- a/datatype_test.go +++ b/datatype_test.go @@ -32,6 +32,7 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) { `null`: `so write ""`, `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`, `{"format":"","c":[{"format":"1","datatype":"integer"},"x"]}`: `write it as {"format":"x","datatype":"integer"}`, + `{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"2","weight":3}]}`: `give it "datatype": "integer"`, `{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"true","datatype":"boolean"}]}`: "a column holds one datatype", } { if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) { @@ -66,6 +67,17 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { {"an overflow", `{"format":"{calc(a * a)}","a":"{digits(200)}","datatype":"number"}`, "is not proven within 1e300"}, {"a division in an integer column", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`, "prints a number, not an integer"}, {"a composed operand", `{"format":"{calc(n * 2)}","n":"{int(1,99)}.{digits(2)}","datatype":"number"}`, "{seq()}, {digits()} or {calc()}"}, + {"a divisor that prints as zero", `{"format":"{calc(1 / b, 2)}","b":"{float(4.9999999999999994e-79,5e-79,78)}","datatype":"number"}`, "divides by b, which is not proven nonzero"}, + {"a divisor that rounds to zero", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{float(0.1,1,0)}","datatype":"number"}`, "divides by b, which is not proven nonzero"}, + {"a zero among a divisor's literals", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":["0","5"],"datatype":"number"}`, "divides by b, which is not proven nonzero"}, + {"a negated divisor crossing zero", `{"format":"{calc(a / (-b + 10))}","a":"{int(1,9)}","b":"{int(1,20)}","datatype":"number"}`, "which is not proven nonzero"}, + {"a subtracted divisor crossing zero", `{"format":"{calc(a / (10 - b))}","a":"{int(1,9)}","b":"{int(1,20)}","datatype":"number"}`, "which is not proven nonzero"}, + {"a quotient past the limit", `{"format":"{calc(a / b / b)}","a":"{digits(300)}","b":"{float(0.000001,1,6)}","datatype":"number"}`, "is not proven within 1e300"}, + {"an operand past the limit", `{"format":"{calc(a)}","a":"{digits(400)}","datatype":"number"}`, "is not proven within 1e300"}, + {"a whole calc past int64", `{"format":"{calc(a * 2)}","a":"{seq()}","datatype":"integer"}`, "{calc(a * 2)} is not proven within int64"}, + {"a whole float past int64", `{"format":"{float(0,1e19,0)}","datatype":"integer"}`, "{float(0,1e19,0)} is not proven within int64"}, + {"a signed zero integer", `{"format":"-0","datatype":"integer"}`, `"-0" is zero written with a sign; write "0"`}, + {"a signed zero number", `{"format":"-0.00","datatype":"number"}`, `"-0.00" is zero written with a sign; write "0.00"`}, } { row := `{"format":"","col":` + c.column + `}` files := map[string]string{"row": row} @@ -83,7 +95,7 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { } } -var jsonInteger = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`) +var jsonInteger = regexp.MustCompile(`^(0|-?[1-9][0-9]*)$`) func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) { cat := `[{"format":"{code}","code":"200"},{"format":"{code}","code":"404"}]` @@ -94,6 +106,9 @@ func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) { `{"format":"{/cat.code}","datatype":"integer"}`, `{"format":"{a|b}","a":"1","b":"{int(5,9)}","datatype":"integer"}`, `{"format":"{float(-1.5,9.5,0)}","datatype":"integer"}`, + `{"format":"{float(-1,1,0)}","datatype":"integer"}`, + `{"format":"{calc(a * b)}","a":"{int(-9,-1)}","b":"{int(0,9)}","datatype":"integer"}`, + `{"format":"{calc(x + 1)}","x":"{float(0,9,0)}","datatype":"integer"}`, `{"format":"{float(-1,1,2)}","datatype":"number"}`, `{"format":"{v}","v":["1","2.5","6.022e23"],"datatype":"number"}`, `{"format":"{b}","b":["true","false"],"datatype":"boolean"}`, diff --git a/one_spelling_test.go b/one_spelling_test.go index ca1aeac..8253b1c 100644 --- a/one_spelling_test.go +++ b/one_spelling_test.go @@ -18,6 +18,7 @@ func TestRepeatedChoiceItemIsRejected(t *testing.T) { `["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`, `[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`: "repeats item", `{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`, + `{"format":"","w":[null,null,"a"]}`: "a null takes no weight", } { if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want) diff --git a/perf_test.go b/perf_test.go index a3b750d..0ee6dcb 100644 --- a/perf_test.go +++ b/perf_test.go @@ -68,6 +68,10 @@ func TestNoRecordAllocRegression(t *testing.T) { if allocs := testing.AllocsPerRun(10000, func() { f.FakeRecord("x") }); allocs > base*1.10 { t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a record fence running per draw is the usual cause", s.name, allocs, base*1.10, base) } + r, _ := f.FakeRecord("x") + if allocs := testing.AllocsPerRun(10000, func() { _ = r.CSVLine() }); allocs > 2 { + t.Errorf("%s: CSVLine() makes %.1f allocs/op, want 2: the fields slice and the joined line", s.name, allocs) + } } } diff --git a/record_test.go b/record_test.go index c5fb717..c57bdec 100644 --- a/record_test.go +++ b/record_test.go @@ -134,7 +134,7 @@ func TestRecordCSV(t *testing.T) { } func TestRecordCSVEmptyValueStaysARow(t *testing.T) { - dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`, "gone": `{"format": "", "note": null}`}) + dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`}) f := newGenerator(t, dir, WithSeed(1)) r, err := f.FakeRecord("blank") if err != nil { @@ -147,9 +147,6 @@ func TestRecordCSVEmptyValueStaysARow(t *testing.T) { if len(rows) != 2 || len(rows[1]) != 1 || rows[1][0] != "" { t.Fatalf("one empty column parsed to %v, want a header and one row of one empty field", rows) } - if r, err := f.FakeRecord("gone"); err != nil || r.CSVLine() != "" { - t.Fatalf("a lone null column wrote %q, %v; want the blank line PostgreSQL's COPY reads as null", r.CSVLine(), err) - } } func TestRecordWritesTypedAndNullColumns(t *testing.T) { @@ -183,6 +180,13 @@ func TestRecordWritesTypedAndNullColumns(t *testing.T) { if got := DataTypeNumber.String(); got != "number" { t.Errorf("DataTypeNumber.String() = %q, want the data's spelling", got) } + lone, err := newGenerator(t, writeData(t, map[string]string{"gone": `{"format":"","note":null}`})).FakeRecord("gone") + if err != nil { + t.Fatal(err) + } + if got := lone.CSVLine(); got != "" { + t.Errorf("a lone null column wrote CSVLine() = %q, want the blank line PostgreSQL's COPY reads as null", got) + } } func TestRecordRejectsOverlappingReferenceColumns(t *testing.T) {