diff --git a/builtins_test.go b/builtins_test.go index b719911..6628c65 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -298,8 +298,8 @@ func TestBuiltinDateAndTime(t *testing.T) { func TestBuiltinDateArgs(t *testing.T) { for tmpl, want := range map[string]string{ `"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01", - `"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "before", - `"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "before", + `"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "is after", + `"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "write it as text", `"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'", `"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'", `"{date(1990-01-01,1990-12-31,'x')}"`: "text", @@ -318,3 +318,49 @@ func TestBuiltinDateArgs(t *testing.T) { } } } + +// TestBuiltinLayoutErrorsNameARunnableSpelling pins the two layout errors a user +// can follow: a double-quoted layout is named single-quoted, without its own +// quotes carried into the suggestion, and a bare one says a shell ate them. +func TestBuiltinLayoutErrorsNameARunnableSpelling(t *testing.T) { + _, err := compile(parse(t, `"{date(1990-01-01,1990-12-31,\"2006-01-02\")}"`)) + if err == nil || !strings.Contains(err.Error(), "write '2006-01-02'") { + t.Fatalf("a double-quoted layout = %v, want it named single-quoted", err) + } + if strings.Contains(err.Error(), `'"`) { + t.Errorf("%v names a layout that renders its own quotes", err) + } + _, err = compile(parse(t, `"{time(15:04)}"`)) + if err == nil || !strings.Contains(err.Error(), "write '15:04'") || !strings.Contains(err.Error(), "shell") { + t.Fatalf("a bare layout = %v, want it named quoted and the shell explained", err) + } + // The comma hint belongs to a layout that split, not to a call given extra args. + _, err = compile(parse(t, `"{time(0,12,'15:04')}"`)) + if err == nil || !strings.Contains(err.Error(), "takes 1 argument, got 3") { + t.Fatalf("time with three args = %v, want the count named in the singular", err) + } + if strings.Contains(err.Error(), "holding a comma") { + t.Errorf("%v offers the comma hint though the layout is already quoted", err) + } +} + +// TestBuiltinDateSpansOneDay pins that from == to is a day: with a clock layout it +// draws every second of it, and without one it could only emit one value. +func TestBuiltinDateSpansOneDay(t *testing.T) { + f := engine(1) + seen := map[string]bool{} + for i := 0; i < 500; i++ { + got := mustRender(t, f, `"{date(2026-01-01,2026-01-01,'2006-01-02 15:04:05')}"`) + if !strings.HasPrefix(got, "2026-01-01 ") { + t.Fatalf("date over one day = %q, out of range", got) + } + seen[got] = true + } + if len(seen) < 400 { + t.Fatalf("date over one day drew %d distinct seconds in 500, want the whole day", len(seen)) + } + _, err := compile(parse(t, `"{date(2026-01-01,2026-01-01,'2006-01-02')}"`)) + if err == nil || !strings.Contains(err.Error(), "write it as text") { + t.Fatalf("one day in a date-only layout = %v, want it named a constant", err) + } +} diff --git a/data_test.go b/data_test.go index 0368184..4051ca3 100644 --- a/data_test.go +++ b/data_test.go @@ -392,3 +392,25 @@ func TestShippedStreetNumberFormats(t *testing.T) { } } } + +// TestShippedUSTitleAgreesWithSex pins that one person's title and sex agree, and +// that the everyday titles are reachable. +func TestShippedUSTitleAgreesWithSex(t *testing.T) { + f := newGenerator(t, "data", WithSeed(4)) + female := map[string]bool{"Miss": true, "Mrs": true, "Ms": true} + seen := map[string]bool{} + for i := 0; i < 3000; i++ { + got := fakeTemplate(t, f, `{/en_US.person.prefix}|{/en_US.person.sex}`) + title, sex, _ := strings.Cut(got, "|") + title = strings.TrimSpace(title) + seen[title] = true + if title == "Mr" && sex != "male" || female[title] && sex != "female" { + t.Fatalf("%q: the title contradicts the sex", got) + } + } + for _, want := range []string{"", "Mr", "Ms"} { + if !seen[want] { + t.Errorf("en_US.person.prefix never drew %q in 3000 draws", want) + } + } +} diff --git a/record_test.go b/record_test.go index f75d8dd..e5ec7a3 100644 --- a/record_test.go +++ b/record_test.go @@ -547,3 +547,18 @@ func TestRecordTemplateRejectsATopLevelRepeat(t *testing.T) { t.Errorf("NewTemplate on the same input = %v, want the string view to still compile", err) } } + +// TestRecordOfAFieldlessCategoryNamesTheWrapper pins the way out of the first +// command a fixture author types: a category of one value has no columns, and the +// error names the record that gives it one. +func TestRecordOfAFieldlessCategoryNamesTheWrapper(t *testing.T) { + f := newGenerator(t, "data", WithSeed(1)) + want := `'{"format":"","personnummer":"{/sv_SE.personnummer}"}'` + _, err := f.FakeRecord("sv_SE.personnummer") + if err == nil || !strings.Contains(err.Error(), want) { + t.Fatalf("FakeRecord(sv_SE.personnummer) = %v, want it to name %s", err, want) + } + if _, err := f.FakeRecordTemplate(`{"format":"","personnummer":"{/sv_SE.personnummer}"}`); err != nil { + t.Fatalf("the named wrapper does not render: %v", err) + } +} diff --git a/table_test.go b/table_test.go index 1de23a1..e912adb 100644 --- a/table_test.go +++ b/table_test.go @@ -172,7 +172,7 @@ func TestTableSelectsARowByKeyOrName(t *testing.T) { } for path, want := range map[string]string{ "region[99]": `"99"`, - "locality[Sandby]": "L7 L8", + "locality[Sandby]": "L7, L8", "region[12].code[1]": "not a table", "region[]": "empty", "region[12": "]", @@ -334,7 +334,7 @@ func TestTableSelectorInAReference(t *testing.T) { } for name, c := range map[string]struct{ json, want string }{ "unknown row": {`"{/region[99].name}"`, `"99"`}, - "ambiguous name": {`"{/locality[Sandby].name}"`, "L7 L8"}, + "ambiguous name": {`"{/locality[Sandby].name}"`, "L7, L8"}, "not inside": {`"{/region[12].municipality[0180].name}"`, "not inside"}, "selector on template": {`"{/x[1].a}"`, "not a table"}, } { @@ -744,3 +744,32 @@ func TestNamedTableWithoutAKeyResolvesInsideItsParent(t *testing.T) { t.Fatalf("a name selected inside a pinned parent = %q", got) } } + +// TestAmbiguousNameNamesARunnablePath pins what a shell user reads: each row is +// named as the path they can type, from the root, and the rows are listed plainly. +func TestAmbiguousNameNamesARunnablePath(t *testing.T) { + files := map[string]string{} + for name, body := range siblings() { + files["se/"+name] = body + } + files["se/street.json"] = `{"format":"{name}","rows":"street.tsv","name":"name","parent":"locality","weight":"segments"}` + f := newGenerator(t, writeFiles(t, files), WithSeed(1)) + _, err := f.Fake("se.street[Sandbyvägen]") + if err == nil { + t.Fatal("se.street[Sandbyvägen] resolved, though two rows carry that name") + } + for _, want := range []string{"se.locality[L7].street[Sandbyvägen]", "se.locality[L8].street[Sandbyvägen]"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("%v does not name %s", err, want) + } + if got := fake(t, f, want); got != "Sandbyvägen" { + t.Errorf("Fake(%q) = %q, want the named path to run", want, got) + } + } + if strings.Contains(err.Error(), "[se.locality") { + t.Errorf("%v lists the rows as a Go slice; separate them with commas", err) + } + if _, err := f.Fake("se.locality[Sandby]"); err == nil || !strings.Contains(err.Error(), "one of L7, L8") { + t.Fatalf("se.locality[Sandby] = %v, want its keys listed plainly", err) + } +}