From 770dca507aa9481fce1fb01f231d4beadaeeef18 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 15 Sep 2026 14:31:27 +0200 Subject: [PATCH] Tests: a typed column holds one value bounded by its arguments, and a lone null CSV column is a blank line --- datatype_test.go | 79 +++++++++++++++++++++++++++--------------------- record_test.go | 28 +++++++++-------- 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/datatype_test.go b/datatype_test.go index aa8c4a1..e65f711 100644 --- a/datatype_test.go +++ b/datatype_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "regexp" "slices" + "strconv" "strings" "testing" ) @@ -29,8 +30,9 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) { `{"format":"{p}","p":{"format":"{n}","n":{"format":"1","datatype":"integer"}}}`: "datatype only types a record column", `{"format":"{n}","repeat":2,"n":{"format":"1","datatype":"integer"}}`: "datatype only types a record column", `null`: `so write ""`, - `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`, - `{"format":"","c":[{"format":"1","datatype":"integer"},"x"]}`: "a column holds one datatype", + `{"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":"true","datatype":"boolean"}]}`: "a column holds one datatype", } { if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("compile(%s) = %v, want an error containing %q", src, err, want) @@ -38,52 +40,66 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) { } } -func TestDatatypeRejectsARenderItsTypeRejects(t *testing.T) { - cat := `[{"format":"{code}","code":"200"},{"format":"{code}","code":"2x"}]` +func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { + tree := map[string]string{ + "cat": `[{"format":"{code}","code":"200"},{"format":"{code}","code":"2x"}]`, + "src": `{"format":"","score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`, + } for _, c := range []struct{ name, column, want string }{ - {"a leading zero", `{"format":"{digits(3)}","datatype":"integer"}`, "which is not an integer"}, - {"a fraction", `{"format":"{v}","v":["1","1.5"],"datatype":"integer"}`, `can render "1.5", which is not an integer`}, - {"a signed sample before digits", `{"format":"{int(-5,5)}{digits(2)}","datatype":"integer"}`, "which is not an integer"}, - {"a separator", `{"format":"{int(1,9)}","repeat":2,"separator":",","datatype":"integer"}`, "which is not an integer"}, - {"through a reference", `{"format":"{/cat.code}","datatype":"integer"}`, `can render "2x"`}, - {"a bare dot", `{"format":".5","datatype":"number"}`, `can render ".5", which is not a number`}, - {"a trailing dot", `{"format":"{int(1,9)}.","datatype":"number"}`, "which is not a number"}, - {"a plus sign", `{"format":"+1","datatype":"number"}`, `can render "+1"`}, - {"a capital", `{"format":"{b}","b":["true","True"],"datatype":"boolean"}`, `can render "True", which is not a boolean`}, - {"an upper-casing transform", `{"format":"{uppercase(b)}","b":["true","false"],"datatype":"boolean"}`, "which is not a boolean"}, - {"an operand that is not always a number", `{"format":"{calc(a * 2)}","a":["1","x"],"datatype":"number"}`, `operand "a" can render "x"`}, - {"a divisor that can be zero", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{int(0,9)}","datatype":"number"}`, "divides by b, which can be zero"}, - {"an overflow", `{"format":"{calc(a * a)}","a":"{digits(200)}","datatype":"number"}`, "can overflow"}, - {"a division in an integer column", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`, "which is not an integer"}, + {"a sample with leading zeros", `{"format":"{digits(3)}","datatype":"integer"}`, "{digits(3)} prints text, not an integer"}, + {"a fraction", `{"format":"{v}","v":["1","1.5"],"datatype":"integer"}`, `"1.5" is not an integer`}, + {"past int64", `{"format":"9223372036854775808","datatype":"integer"}`, "past the int64 range"}, + {"a float sample", `{"format":"{float(0,1,2)}","datatype":"integer"}`, "{float(0,1,2)} prints a number, not an integer"}, + {"composed digits", `{"format":"1{digits(2)}","datatype":"integer"}`, "is not one value"}, + {"a sign before a sample", `{"format":"-{int(1,9)}","datatype":"integer"}`, "is not one value"}, + {"a repeat", `{"format":"{int(1,9)}","repeat":2,"separator":",","datatype":"integer"}`, "carries a repeat"}, + {"through a reference", `{"format":"{/cat.code}","datatype":"integer"}`, `"2x" is not an integer`}, + {"a null through a reference", `{"format":"{/src.score}","datatype":"integer"}`, "reads a null"}, + {"a bare dot", `{"format":".5","datatype":"number"}`, `".5" is not a number`}, + {"a plus sign", `{"format":"+1","datatype":"number"}`, `"+1" is not a number`}, + {"a text sample", `{"format":"{hex(4)}","datatype":"number"}`, "{hex(4)} prints text, not a number"}, + {"a capital", `{"format":"{b}","b":["true","True"],"datatype":"boolean"}`, `"True" is not a boolean`}, + {"a transform", `{"format":"{lowercase(b)}","b":["TRUE","FALSE"],"datatype":"boolean"}`, "{lowercase(b)} rewrites text"}, + {"a number as a boolean", `{"format":"{int(0,1)}","datatype":"boolean"}`, "{int(0,1)} prints an integer, not a boolean"}, + {"an operand that is not always a number", `{"format":"{calc(a * 2)}","a":["1","x"],"datatype":"number"}`, `operand "a": "x" is not a number`}, + {"a divisor that can be zero", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{int(0,9)}","datatype":"number"}`, "divides by b, which is not proven nonzero"}, + {"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"}, } { row := `{"format":"","col":` + c.column + `}` - _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"cat": cat, "row": row}))) + files := map[string]string{"row": row} + for name, body := range tree { + files[name] = body + } + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: New = %v, want an error containing %q", c.name, err, c.want) } - f := newGenerator(t, writeData(t, map[string]string{"cat": cat})) + f := newGenerator(t, writeData(t, tree)) if _, err := f.NewTemplate(row); err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: NewTemplate = %v, want the inline template refused the same way", c.name, err) } } } -var integerText = 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"}]` for _, column := range []string{ `{"format":"{int(1,99)}","datatype":"integer"}`, - `{"format":"-{int(1,9)}","datatype":"integer"}`, - `{"format":"1{digits(2)}","datatype":"integer"}`, + `{"format":"{int(-9,-1)}","datatype":"integer"}`, `{"format":"{seq()}","datatype":"integer"}`, `{"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,2)}","datatype":"number"}`, - `{"format":"{int(1,9)}e{int(1,9)}","datatype":"number"}`, - `{"format":"6.022e23","datatype":"number"}`, - `{"format":"{lowercase(b)}","b":["TRUE","False"],"datatype":"boolean"}`, + `{"format":"{v}","v":["1","2.5","6.022e23"],"datatype":"number"}`, + `{"format":"{b}","b":["true","false"],"datatype":"boolean"}`, `{"format":"{calc(net * qty, 2)}","net":["19.99","5.00"],"qty":["3","7"],"datatype":"number"}`, `{"format":"{calc(a + b)}","a":"{int(1,9)}","b":"{int(-9,9)}","datatype":"integer"}`, + `{"format":"{calc(x / (b - c), 2)}","x":"{int(1,9)}","b":"{int(10,20)}","c":"{int(1,5)}","datatype":"number"}`, + `{"format":"{calc(x * x * x * x, 2)}","x":"{float(0,1,80)}","datatype":"number"}`, `{"format":"{calc(a / (b + 1), 2)}","a":"{int(1,9)}","b":"{digits(2)}","datatype":"number"}`, `{"format":"{calc(a / b, 0)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`, `{"format":"{calc(sub * 1.25, 2)}","sub":{"format":"{calc(a * b)}","a":"{int(1,9)}","b":"{float(0,5,2)}"},"datatype":"number"}`, @@ -107,7 +123,8 @@ func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) { c := r.Columns()[0] _, isBool := m["col"].(bool) _, isNumber := m["col"].(float64) - if c.DataType == DataTypeBoolean && !isBool || c.DataType != DataTypeBoolean && !isNumber || c.DataType == DataTypeInteger && !integerText.MatchString(c.Value) { + _, int64Err := strconv.ParseInt(c.Value, 10, 64) + if c.DataType == DataTypeBoolean && !isBool || c.DataType != DataTypeBoolean && !isNumber || c.DataType == DataTypeInteger && (!jsonInteger.MatchString(c.Value) || int64Err != nil) { t.Errorf("%s: column %+v written as %s, want its datatype", column, c, r.JSON()) break } @@ -151,11 +168,3 @@ func TestNullColumn(t *testing.T) { t.Errorf("List() = %v, want the null column row.gone, which Fake accepts", f.List()) } } - -func TestEveryBuiltinSaysWhatItEmits(t *testing.T) { - for name, b := range builtins { - if _, isTransform := transforms[name]; b.emits == nil && name != "calc" && !isTransform { - t.Errorf("builtin %s declares no emits, so a typed column calling it cannot be checked", name) - } - } -} diff --git a/record_test.go b/record_test.go index 520ceef..c5fb717 100644 --- a/record_test.go +++ b/record_test.go @@ -134,19 +134,21 @@ func TestRecordCSV(t *testing.T) { } func TestRecordCSVEmptyValueStaysARow(t *testing.T) { - for _, body := range []string{`{"format": "", "note": ""}`, `{"format": "", "note": null}`} { - f := newGenerator(t, writeData(t, map[string]string{"blank": body}), WithSeed(1)) - r, err := f.FakeRecord("blank") - if err != nil { - t.Fatal(err) - } - rows, err := csv.NewReader(strings.NewReader(r.CSVHeader() + "\n" + r.CSVLine() + "\n")).ReadAll() - if err != nil { - t.Fatalf("csv: %v", err) - } - if len(rows) != 2 || len(rows[1]) != 1 || rows[1][0] != "" { - t.Fatalf("%s: one empty column parsed to %v, want a header and one row of one empty field", body, rows) - } + dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`, "gone": `{"format": "", "note": null}`}) + f := newGenerator(t, dir, WithSeed(1)) + r, err := f.FakeRecord("blank") + if err != nil { + t.Fatal(err) + } + rows, err := csv.NewReader(strings.NewReader(r.CSVHeader() + "\n" + r.CSVLine() + "\n")).ReadAll() + if err != nil { + t.Fatalf("csv: %v", err) + } + 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) } }