Tests for date() and time(), a named table keyed by its parent, and weighted names and valid ids in both locales
This commit is contained in:
+1
-1
@@ -30,7 +30,7 @@ func BenchmarkPerson(b *testing.B) { benchPath(b, "data", "sv_SE.person") }
|
||||
func BenchmarkAddress(b *testing.B) { benchPath(b, "data", "sv_SE.address") }
|
||||
func BenchmarkWord(b *testing.B) { benchPath(b, "data", "sv_SE.word") }
|
||||
func BenchmarkCreditcard(b *testing.B) { benchPath(b, "data", "misc.creditcard") }
|
||||
func BenchmarkSSN(b *testing.B) { benchPath(b, "data", "sv_SE.ssn") }
|
||||
func BenchmarkSSN(b *testing.B) { benchPath(b, "data", "sv_SE.personnummer") }
|
||||
func BenchmarkUUIDv7(b *testing.B) { benchPath(b, "data", "misc.uuid") }
|
||||
|
||||
func tmpData(b *testing.B, name, body string) string {
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"encoding/base64"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBuiltinIDGenerators(t *testing.T) {
|
||||
@@ -41,6 +43,7 @@ func TestBuiltinSamplesReproducible(t *testing.T) {
|
||||
`"{uuid()}"`, `"{ulid()}"`,
|
||||
`"{nanoid(12)}"`, `"{int(1,1000000)}"`,
|
||||
`"{float(0,1,6)}"`, `"{base64(12)}"`, `"{iban(SE)}"`,
|
||||
`"{date(2000-01-01,2020-12-31,'2006-01-02 15:04:05')}"`, `"{time('15:04:05')}"`,
|
||||
} {
|
||||
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||
t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b)
|
||||
@@ -233,3 +236,82 @@ func TestClassBuiltinArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltinDateAndTime pins the two clock-free samples: date draws a second in
|
||||
// [from 00:00:00, to 23:59:59], both days reachable, and renders it in the quoted Go
|
||||
// layout, commas and English names included; time draws a second within one day.
|
||||
func TestBuiltinDateAndTime(t *testing.T) {
|
||||
f := engine(1)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 500; i++ {
|
||||
got := mustRender(t, f, `"{date(1990-01-01,1990-12-31,'2006-01-02')}"`)
|
||||
if d, err := time.Parse("2006-01-02", got); err != nil || d.Year() != 1990 {
|
||||
t.Fatalf("date = %q, want a 1990 calendar date (err %v)", got, err)
|
||||
}
|
||||
seen[got] = true
|
||||
}
|
||||
if len(seen) < 200 {
|
||||
t.Fatalf("date drew %d distinct days of 365 in 500, want a uniform spread", len(seen))
|
||||
}
|
||||
lo, hi := false, false
|
||||
for i := 0; i < 200; i++ {
|
||||
got := mustRender(t, f, `"{date(2020-02-28,2020-02-29,'2006-01-02')}"`)
|
||||
if got != "2020-02-28" && got != "2020-02-29" {
|
||||
t.Fatalf("date(2020-02-28,2020-02-29) = %q, out of range", got)
|
||||
}
|
||||
lo, hi = lo || got == "2020-02-28", hi || got == "2020-02-29"
|
||||
}
|
||||
if !lo || !hi {
|
||||
t.Fatalf("date never hit a bound: lo=%v hi=%v (bounds must be inclusive)", lo, hi)
|
||||
}
|
||||
if got := mustRender(t, f, `"{date(2020-07-04,2020-07-05,'January 2, 2006')}"`); got != "July 4, 2020" && got != "July 5, 2020" {
|
||||
t.Fatalf("date with a comma in its layout = %q", got)
|
||||
}
|
||||
rfc := regexp.MustCompile(`^2021-\d\d-\d\dT\d\d:\d\d:\d\dZ$`)
|
||||
clock := regexp.MustCompile(`^([01]\d|2[0-3]):[0-5]\d$`)
|
||||
ampm := regexp.MustCompile(`^(1[0-2]|[1-9]):[0-5]\d (AM|PM)$`)
|
||||
seconds := map[string]bool{}
|
||||
for i := 0; i < 300; i++ {
|
||||
got := mustRender(t, f, `"{date(2021-01-01,2021-12-31,'2006-01-02T15:04:05Z07:00')}"`)
|
||||
if !rfc.MatchString(got) {
|
||||
t.Fatalf("date in an RFC 3339 layout = %q, want %s", got, rfc)
|
||||
}
|
||||
seconds[got[17:19]] = true
|
||||
if got := mustRender(t, f, `"{time('15:04')}"`); !clock.MatchString(got) {
|
||||
t.Fatalf("time('15:04') = %q, want %s", got, clock)
|
||||
}
|
||||
if got := mustRender(t, f, `"{time('3:04 PM')}"`); !ampm.MatchString(got) {
|
||||
t.Fatalf("time('3:04 PM') = %q, want %s", got, ampm)
|
||||
}
|
||||
got = mustRender(t, f, `"{date(1950-01-01,2000-12-31,'060102')}-238{luhn()}"`)
|
||||
if d := digitsOnly(got); len(d) != 10 || !luhnValid(d) {
|
||||
t.Fatalf("a personnummer over date() = %q, want ten Luhn-valid digits", got)
|
||||
}
|
||||
}
|
||||
if len(seconds) < 30 {
|
||||
t.Fatalf("date drew %d distinct seconds in 300, want the whole day, not midnight", len(seconds))
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltinDateArgs pins the New-time checks: bounds are calendar dates in order,
|
||||
// the layout is quoted, names a field, and for time names no date field.
|
||||
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-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",
|
||||
`"{date(1990-01-01,1990-12-31,'')}"`: "text",
|
||||
`"{date(1990-01-01,1990-12-31)}"`: "3 args",
|
||||
`"{time(15:04)}"`: "'15:04'",
|
||||
`"{time('2006-01-02 15:04')}"`: "date(",
|
||||
`"{time('x')}"`: "text",
|
||||
} {
|
||||
_, err := compile(parse(t, tmpl))
|
||||
if err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("compile(%s) = %v, want an error mentioning %q", tmpl, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+110
-18
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// TestShippedDataCategories asserts every shipped locale emits only well-formed
|
||||
// values for each data category. en and sv patterns differ where the format is
|
||||
// locale-specific (date, time, ssn, company, price, ...); the rest are shared.
|
||||
// locale-specific (date, time, company, price, ...); the rest are shared.
|
||||
func TestShippedDataCategories(t *testing.T) {
|
||||
letters := regexp.MustCompile(`^[\pL'-]+$`)
|
||||
semver := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$`)
|
||||
@@ -33,10 +33,7 @@ func TestShippedDataCategories(t *testing.T) {
|
||||
regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$`)},
|
||||
{"time",
|
||||
regexp.MustCompile(`^([1-9]|1[0-2]):[0-5]\d (AM|PM)$`),
|
||||
regexp.MustCompile(`^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$`)},
|
||||
{"ssn",
|
||||
regexp.MustCompile(`^[1-9]\d{2}-\d{2}-\d{4}$`),
|
||||
regexp.MustCompile(`^\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])-\d{4}$`)},
|
||||
regexp.MustCompile(`^([01]\d|2[0-3]):[0-5]\d$`)},
|
||||
{"version", semver, semver},
|
||||
{"email", email, email},
|
||||
{"ip", ip, ip},
|
||||
@@ -72,7 +69,11 @@ func TestShippedMiscCategories(t *testing.T) {
|
||||
v4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
|
||||
mac := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
|
||||
objectid := regexp.MustCompile(`^[0-9a-f]{24}$`)
|
||||
datetime := regexp.MustCompile(`^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$`)
|
||||
for i := 0; i < 200; i++ {
|
||||
if v := fake(t, f, "datetime"); !datetime.MatchString(v) {
|
||||
t.Fatalf("misc datetime %q is not an RFC 3339 UTC instant", v)
|
||||
}
|
||||
if v := fake(t, f, "objectid"); !objectid.MatchString(v) {
|
||||
t.Fatalf("misc objectid %q is not 24 hex chars", v)
|
||||
}
|
||||
@@ -147,30 +148,121 @@ func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSwedishPersonnummer checks the two rules the shape regex can't: the date
|
||||
// is a real calendar date (so month-length variants never emit e.g. Apr 31 or
|
||||
// Feb 30) and the trailing digit is a valid Luhn checksum over the other nine.
|
||||
// TestSwedishPersonnummer checks what the shape regex can't: the date is a real
|
||||
// calendar date, the birth number is Skatteverket's test series (238 female, 239
|
||||
// male), the trailing digit is a Luhn checksum over the other nine, and a
|
||||
// samordningsnummer is the same with 60 added to the day.
|
||||
func TestSwedishPersonnummer(t *testing.T) {
|
||||
sv := newGenerator(t, "data", WithSeed(1))
|
||||
sawLongMonthEnd := false
|
||||
re := regexp.MustCompile(`^\d{6}-23[89]\d$`)
|
||||
sawLongMonthEnd, birth := false, map[string]bool{}
|
||||
for i := 0; i < 2000; i++ {
|
||||
v := fake(t, sv, "sv_SE.ssn")
|
||||
v := fake(t, sv, "sv_SE.personnummer")
|
||||
d := digitsOnly(v)
|
||||
if len(d) != 10 {
|
||||
t.Fatalf("ssn %q has %d digits, want 10", v, len(d))
|
||||
if !re.MatchString(v) {
|
||||
t.Fatalf("personnummer %q, want YYMMDD-238C or YYMMDD-239C", v)
|
||||
}
|
||||
if _, err := time.Parse("060102", d[:6]); err != nil { // 2-digit year, real-date check
|
||||
t.Fatalf("ssn %q is not a valid calendar date: %v", v, err)
|
||||
if _, err := time.Parse("060102", d[:6]); err != nil {
|
||||
t.Fatalf("personnummer %q is not a valid calendar date: %v", v, err)
|
||||
}
|
||||
if !luhnValid(d) {
|
||||
t.Fatalf("ssn %q fails the Luhn check", v)
|
||||
t.Fatalf("personnummer %q fails the Luhn check", v)
|
||||
}
|
||||
if d[4:6] == "31" {
|
||||
sawLongMonthEnd = true
|
||||
sawLongMonthEnd = sawLongMonthEnd || d[4:6] == "31"
|
||||
birth[d[6:9]] = true
|
||||
s := fake(t, sv, "sv_SE.samordningsnummer")
|
||||
sd := digitsOnly(s)
|
||||
day, err := strconv.Atoi(sd[4:6])
|
||||
if !re.MatchString(s) || err != nil || day < 61 || day > 88 || !luhnValid(sd) {
|
||||
t.Fatalf("samordningsnummer %q, want YYMM(61-88)-23[89]C, Luhn-valid", s)
|
||||
}
|
||||
if _, err := time.Parse("0601", sd[:4]); err != nil {
|
||||
t.Fatalf("samordningsnummer %q is not a valid year and month: %v", s, err)
|
||||
}
|
||||
}
|
||||
if !sawLongMonthEnd {
|
||||
t.Fatal("never generated a 31st — 31-day months are not reaching their last day")
|
||||
t.Fatal("never generated a 31st")
|
||||
}
|
||||
if !birth["238"] || !birth["239"] {
|
||||
t.Fatalf("birth numbers drawn %v, want both 238 and 239", birth)
|
||||
}
|
||||
for i := 0; i < 200; i++ {
|
||||
got := fakeTemplate(t, sv, `{/sv_SE.person.sex} {/sv_SE.personnummer}`)
|
||||
if f, m := strings.HasPrefix(got, "kvinna ") && got[14:17] == "238", strings.HasPrefix(got, "man ") && got[11:14] == "239"; !f && !m {
|
||||
t.Fatalf("%q: a person and a personnummer in one render disagree on sex", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestShippedUSTaxIds pins the SSA and IRS ranges: an SSN's area is 001-899 but
|
||||
// 666, its group 01-99 and its serial 0001-9999; an ITIN is 9XX-GG-XXXX with GG
|
||||
// in 50-65, 70-88, 90-92 or 94-99.
|
||||
func TestShippedUSTaxIds(t *testing.T) {
|
||||
f := newGenerator(t, "data", WithSeed(5))
|
||||
ssn := regexp.MustCompile(`^(\d{3})-(\d{2})-(\d{4})$`)
|
||||
itin := regexp.MustCompile(`^9\d{2}-(5\d|6[0-5]|7\d|8[0-8]|9[0-24-9])-\d{4}$`)
|
||||
for i := 0; i < 2000; i++ {
|
||||
v := fake(t, f, "en_US.ssn")
|
||||
m := ssn.FindStringSubmatch(v)
|
||||
if m == nil {
|
||||
t.Fatalf("ssn %q, want AAA-GG-SSSS", v)
|
||||
}
|
||||
area, _ := strconv.Atoi(m[1])
|
||||
group, _ := strconv.Atoi(m[2])
|
||||
serial, _ := strconv.Atoi(m[3])
|
||||
if area < 1 || area > 899 || area == 666 || group < 1 || serial < 1 {
|
||||
t.Fatalf("ssn %q is in a range the SSA never assigns", v)
|
||||
}
|
||||
if v := fake(t, f, "en_US.itin"); !itin.MatchString(v) {
|
||||
t.Fatalf("itin %q, want %s", v, itin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestShippedPersonNames pins the name tables: a sex pins its first names, a name
|
||||
// both sexes carry appears under both, and the record's sex agrees with its name.
|
||||
func TestShippedPersonNames(t *testing.T) {
|
||||
f := newGenerator(t, "data", WithSeed(9))
|
||||
for path, want := range map[string]string{
|
||||
"sv_SE.sex[f]": "kvinna",
|
||||
"sv_SE.sex[man].code": "m",
|
||||
"en_US.sex[f]": "female",
|
||||
"sv_SE.sex[f].first-name[Anna]": "Anna",
|
||||
"sv_SE.sex[m].first-name[Erik].sex": "m",
|
||||
"en_US.sex[m].first-name[James]": "James",
|
||||
"en_US.sex[f].first-name[Taylor].sex": "f",
|
||||
"en_US.sex[m].first-name[Taylor].sex": "m",
|
||||
"sv_SE.last-name[Andersson]": "Andersson",
|
||||
"sv_SE.last-name[Andersson].count": "",
|
||||
"en_US.last-name[Smith]": "Smith",
|
||||
} {
|
||||
got := fake(t, f, path)
|
||||
if want == "" && !regexp.MustCompile(`^[1-9]\d*$`).MatchString(got) {
|
||||
t.Errorf("Fake(%q) = %q, want a count", path, got)
|
||||
} else if want != "" && got != want {
|
||||
t.Errorf("Fake(%q) = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
if _, err := f.Fake("en_US.first-name[Taylor]"); err == nil || !strings.Contains(err.Error(), "sex[f].first-name[Taylor]") {
|
||||
t.Fatalf("Fake(en_US.first-name[Taylor]) = %v, want both sexes' rows listed", err)
|
||||
}
|
||||
for _, locale := range []string{"sv_SE", "en_US"} {
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 2000; i++ {
|
||||
seen[fake(t, f, locale+".sex[f].first-name")] = true
|
||||
first, sex := fake(t, f, locale+".person.first"), fake(t, f, locale+".person.sex")
|
||||
if first == "" || sex == "" {
|
||||
t.Fatalf("%s.person lacks a first name or a sex", locale)
|
||||
}
|
||||
}
|
||||
if len(seen) < 300 || !seen["Anna"] && locale == "sv_SE" || !seen["Mary"] && locale == "en_US" {
|
||||
t.Fatalf("%s female first names: %d distinct in 2000, want a weighted register", locale, len(seen))
|
||||
}
|
||||
got := fakeTemplate(t, f, `{/`+locale+`.sex.code}|{/`+locale+`.person.first}`)
|
||||
code, first, _ := strings.Cut(got, "|")
|
||||
if v := fake(t, f, locale+".sex["+code+"].first-name["+first+"]"); v != first {
|
||||
t.Fatalf("%s: %q drawn as a %s name is not one", locale, first, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,3 +81,14 @@ func TestDifferentSeedsDiffer(t *testing.T) {
|
||||
}
|
||||
t.Fatal("seeds 1 and 2 produced identical sequences")
|
||||
}
|
||||
|
||||
// fakeTemplate renders an inline template against a loaded generator, so its
|
||||
// references resolve.
|
||||
func fakeTemplate(t *testing.T, f *Generator, s string) string {
|
||||
t.Helper()
|
||||
got, err := f.FakeTemplate(s)
|
||||
if err != nil {
|
||||
t.Fatalf("FakeTemplate(%s) = %v", s, err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
@@ -440,6 +440,7 @@ func TestTableFences(t *testing.T) {
|
||||
"a bracket in a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\nx[1]\ny\n"}, `"["`},
|
||||
"a brace in a name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\tx{1}\ny\ty\n"}, `"{"`},
|
||||
"name without a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"a"}`, "t.tsv": "a\nx\nx\n"}, "key"},
|
||||
"a name repeating inside one parent row": {with(siblings(), map[string]string{"street.json": `{"format":"{name}","rows":"street.tsv","name":"name","parent":"locality"}`, "street.tsv": "name\tlocality\nAvenyn\tL1\nAvenyn\tL1\nStorgatan\tL2\nStorgatan\tL3\nStorgatan\tL4\nStorgatan\tL5\nStorgatan\tL6\nStorgatan\tL7\nStorgatan\tL8\n"}), `"Avenyn"`},
|
||||
"a name that is another row's key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\ty\ny\tz\n"}, `"y"`},
|
||||
"a cell reading its family": {with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\tnote\nL1\tStockholm\t0180\t{/municipality.code}\nL2\tSolna\t0184\t-\nL3\tMalmö\t1280\t-\nL4\tLund\t1281\t-\nL5\tGöteborg\t1480\t-\n"}), "family"},
|
||||
"a format reading its family": {with(geo(), map[string]string{"locality.json": `{"format":"{name} {/region.name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`}), "family"},
|
||||
@@ -709,3 +710,37 @@ func TestShippedTables(t *testing.T) {
|
||||
t.Fatalf("country draws %d distinct rows in 5000, want the full register", len(count))
|
||||
}
|
||||
}
|
||||
|
||||
// TestNamedTableWithoutAKeyResolvesInsideItsParent pins a table whose rows are
|
||||
// told apart only by their parent: a name selects a row inside the parent pinned
|
||||
// before it, an ambiguous one is listed by its parent's spelling, and a name
|
||||
// repeating inside one parent row is a load error (see TestTableFences).
|
||||
func TestNamedTableWithoutAKeyResolvesInsideItsParent(t *testing.T) {
|
||||
files := siblings()
|
||||
files["street.json"] = `{"format":"{name}","rows":"street.tsv","name":"name","parent":"locality","weight":"segments"}`
|
||||
f := newGenerator(t, writeFiles(t, files), WithSeed(1))
|
||||
for path, want := range map[string]string{
|
||||
"street[Avenyn]": "Avenyn",
|
||||
"street[Avenyn].locality": "L5",
|
||||
"locality[L7].street[Sandbyvägen]": "Sandbyvägen",
|
||||
"locality[L7].street[Sandbyvägen].segments": "2",
|
||||
"municipality[0184].locality[Sandby].street[Sandbyvägen]": "Sandbyvägen",
|
||||
"municipality[0184].locality[Sandby].street[Sandbyvägen].locality": "L8",
|
||||
} {
|
||||
if got := fake(t, f, path); got != want {
|
||||
t.Errorf("Fake(%q) = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
for path, want := range map[string]string{
|
||||
"street[Sandbyvägen]": "locality[L7].street[Sandbyvägen]",
|
||||
"locality[L1].street[Avenyn]": "not inside",
|
||||
"street[Kungsgatan]": `"Kungsgatan"`,
|
||||
} {
|
||||
if _, err := f.Fake(path); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("Fake(%q) = %v, want an error mentioning %s", path, err, want)
|
||||
}
|
||||
}
|
||||
if got := fakeTemplate(t, f, `{/locality[L8].name}: {/locality[L8].street[Sandbyvägen].name}`); got != "Sandby: Sandbyvägen" {
|
||||
t.Fatalf("a name selected inside a pinned parent = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user