Files
fejkdata/data_test.go

651 lines
24 KiB
Go

package fejkdata
import (
"io/fs"
"os"
"regexp"
"slices"
"strconv"
"strings"
"testing"
"time"
)
// 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, 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+)?$`)
ip := regexp.MustCompile(`^((\d{1,3}\.){3}\d{1,3}|([0-9a-f]{4}:){7}[0-9a-f]{4})$`)
color := regexp.MustCompile(`^(#[0-9a-f]{6}|[\pL ]+)$`)
url := regexp.MustCompile(`^https?://([a-z0-9-]+\.)+[a-z]{2,}(/[a-z0-9./-]*)?$`)
email := regexp.MustCompile(`^[a-z0-9._-]+@([a-z0-9-]+\.)+[a-z]{2,}$`)
uname := regexp.MustCompile(`^[a-z][a-z0-9._]*$`)
cases := []struct {
path string
en, sv *regexp.Regexp
}{
{"word", letters, letters},
{"sentence",
regexp.MustCompile(`^[A-Z].* .*[.!?]$`),
regexp.MustCompile(`^[A-ZÅÄÖ].* .*[.!?]$`)},
{"date",
regexp.MustCompile(`^(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])/\d{4}$`),
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$`)},
{"version", semver, semver},
{"email", email, email},
{"ip", ip, ip},
{"company",
regexp.MustCompile(`^.+ (Inc\.|LLC|Corp\.|Co\.|Group|Ltd\.)$`),
regexp.MustCompile(`^.+ (AB|HB|KB)$`)},
{"color", color, color},
{"url", url, url},
{"username", uname, uname},
{"price",
regexp.MustCompile(`^\$\d{1,3}(,\d{3})?\.\d{2}$`),
regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)},
}
f := shipped(t, WithSeed(1))
for _, c := range cases {
for i := 0; i < 200; i++ {
if v := fake(t, f, "en_US."+c.path); !c.en.MatchString(v) {
t.Fatalf("en_US %s = %q, want %s", c.path, v, c.en)
}
if v := fake(t, f, "sv_SE."+c.path); !c.sv.MatchString(v) {
t.Fatalf("sv_SE %s = %q, want %s", c.path, v, c.sv)
}
}
}
}
// TestShippedMiscCategories covers the locale-neutral misc module: a proper
// v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers
// whose trailing {luhn()} check passes.
func TestShippedMiscCategories(t *testing.T) {
f := newBareModule(t, "misc", WithSeed(1))
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)
}
if v := fake(t, f, "uuid"); !v4.MatchString(v) {
t.Fatalf("misc uuid %q is not a valid v4", v)
}
if v := fake(t, f, "mac"); !mac.MatchString(v) {
t.Fatalf("misc mac %q is not a MAC address", v)
}
if v := fake(t, f, "creditcard"); len(v) < 15 || len(v) > 16 || !luhnValid(v) {
t.Fatalf("creditcard %q is not a 15-16 digit Luhn-valid number", v)
}
}
}
// TestShippedMiscReferenceData covers the locale-neutral reference categories:
// codes follow their standard shape, dotted sub-paths resolve, emoji is a real
// glyph, and a coordinate parses to a point within geographic bounds.
func TestShippedMiscReferenceData(t *testing.T) {
f := newBareModule(t, "misc", WithSeed(1))
re := map[string]*regexp.Regexp{
"currency": regexp.MustCompile(`^[A-Z]{3}$`),
"currency.name": regexp.MustCompile(`\p{L}`),
"currency.symbol": regexp.MustCompile(`^\S+$`),
"territory": regexp.MustCompile(`\p{L}`),
"territory.alpha2": regexp.MustCompile(`^[A-Z]{2}$`),
"territory.alpha3": regexp.MustCompile(`^[A-Z]{3}$`),
"territory.country": regexp.MustCompile(`^[A-Z]{2}$`),
"language": regexp.MustCompile(`\p{L}`),
"language.code": regexp.MustCompile(`^[a-z]{2}$`),
"language.code3": regexp.MustCompile(`^[a-z]{3}$`),
"timezone": regexp.MustCompile(`^[A-Za-z]+(/[A-Za-z0-9_+-]+)+$`),
"timezone.offset": regexp.MustCompile(`^[+-](0\d|1[0-4]):[0-5]\d$`),
"timezone.territory": regexp.MustCompile(`^[A-Z]{2}$`),
"timezone.weight": regexp.MustCompile(`^[1-9]\d*$`),
"mimetype": regexp.MustCompile(`^[a-z]+/[a-z0-9.+-]+$`),
"mimetype.ext": regexp.MustCompile(`^\.[a-z0-9_-]+$`),
"httpmethod": regexp.MustCompile(`^(CONNECT|DELETE|GET|HEAD|OPTIONS|PATCH|POST|PUT|TRACE)$`),
"httpmethod.safe": regexp.MustCompile(`^(true|false)$`),
"httpmethod.idempotent": regexp.MustCompile(`^(true|false)$`),
"port": regexp.MustCompile(`^[1-9]\d{0,4}$`),
"port.service": regexp.MustCompile(`^\S+$`),
"protocol": regexp.MustCompile(`^\S+( \S+)*$`),
"protocol.name": regexp.MustCompile(`\S`),
"protocol.number": regexp.MustCompile(`^\d{1,3}$`),
"loglevel": regexp.MustCompile(`^(alert|crit|debug|emerg|err|info|notice|warning)$`),
"loglevel.code": regexp.MustCompile(`^[0-7]$`),
"loglevel.severity": regexp.MustCompile(`^(Alert|Critical|Debug|Emergency|Error|Informational|Notice|Warning)$`),
"tld": regexp.MustCompile(`^\.[a-z0-9]([a-z0-9-]*[a-z0-9])?$`),
"tld.type": regexp.MustCompile(`^(country-code|generic|generic-restricted|infrastructure|sponsored)$`),
"tld.unicode": regexp.MustCompile(`^\.\S+$`),
"httpstatus": regexp.MustCompile(`^[1-5]\d{2} \S.*$`),
"httpstatus.code": regexp.MustCompile(`^[1-5]\d{2}$`),
"useragent": regexp.MustCompile(`^Mozilla/5\.0 .+`),
"useragent.browser": regexp.MustCompile(`^(Chrome|Edge|Firefox|Opera|Safari|Samsung Internet)$`),
"useragent.device": regexp.MustCompile(`^(desktop|mobile)$`),
"useragent.os": regexp.MustCompile(`^(Android|ChromeOS|Linux|Windows|iOS|macOS)$`),
"car": regexp.MustCompile(`^\S.* \S`),
"car.make": regexp.MustCompile(`^\S`),
}
for i := 0; i < 100; i++ {
for p, rx := range re {
if v := fake(t, f, p); !rx.MatchString(v) {
t.Fatalf("misc %s = %q, want %s", p, v, rx)
}
}
if e := fake(t, f, "emoji"); e == "" || []rune(e)[0] < 128 {
t.Fatalf("emoji %q is not a non-ASCII glyph", e)
}
c := fake(t, f, "coordinate")
parts := strings.SplitN(c, ", ", 2)
lat, err1 := strconv.ParseFloat(parts[0], 64)
lon, err2 := strconv.ParseFloat(parts[len(parts)-1], 64)
if len(parts) != 2 || err1 != nil || err2 != nil || lat < -90 || lat > 90 || lon < -180 || lon > 180 {
t.Fatalf("coordinate %q is not a 'lat, lon' point within bounds", c)
}
}
}
// TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape
// regexes miss: no generated name repeats a character three times over.
func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
f := shipped(t, WithSeed(11))
for _, path := range []string{"sv_SE.person", "sv_SE.person.last"} {
for i := 0; i < 20000; i++ {
name := fake(t, f, path)
r := []rune(name)
for j := 0; j+2 < len(r); j++ {
if r[j] == r[j+1] && r[j] == r[j+2] {
t.Fatalf("%s = %q, which triples %q", path, name, string(r[j]))
}
}
}
}
}
// 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 := shipped(t, WithSeed(1))
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.personnummer")
d := digitsOnly(v)
if !re.MatchString(v) {
t.Fatalf("personnummer %q, want YYMMDD-238C or YYMMDD-239C", v)
}
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("personnummer %q fails the Luhn check", v)
}
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")
}
if !birth["238"] || !birth["239"] {
t.Fatalf("birth numbers drawn %v, want both 238 and 239", birth)
}
}
// 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 := shipped(t, 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 := shipped(t, 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
got := fakeTemplate(t, f, `{/`+locale+`.person as p}{p.first}|{p.sex}`)
first, sex, _ := strings.Cut(got, "|")
fake(t, f, locale+".sex["+sex+"].first-name["+first+"]")
}
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))
}
}
}
// TestShippedPersonIsOneNamedPick pins that a person's first name, sex and title come
// from one pick, and that two names bound to one person category are two people.
func TestShippedPersonIsOneNamedPick(t *testing.T) {
f := shipped(t, WithSeed(9))
for _, locale := range []string{"sv_SE", "en_US"} {
apart := false
for i := 0; i < 300; i++ {
got := fakeTemplate(t, f, `{/`+locale+`.person as a}{/`+locale+`.person as b}{a.first}|{a.sex}|{a.prefix}|{b.first}|{b.sex}`)
parts := strings.Split(got, "|")
for _, p := range [][2]string{{parts[0], parts[1]}, {parts[3], parts[4]}} {
fake(t, f, locale+".sex["+p[1]+"].first-name["+p[0]+"]")
}
if title := strings.TrimSpace(parts[2]); title != "" && locale == "en_US" {
fake(t, f, "en_US.sex["+parts[1]+"].title["+title+"]")
}
apart = apart || parts[0] != parts[3]
}
if !apart {
t.Errorf("%s: two names bound to person drew one first name in 300 renders, want two people", locale)
}
}
}
func TestShippedSwedishPhone(t *testing.T) {
f := shipped(t, WithSeed(11))
re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`)
for i := 0; i < 50; i++ {
if n := fake(t, f, "sv_SE.phone"); !re.MatchString(n) {
t.Fatalf("phone %q does not match %s", n, re)
}
}
}
func TestShippedSwedishAddress(t *testing.T) {
f := shipped(t, WithSeed(3))
digit := regexp.MustCompile(`\d`)
for i := 0; i < 30; i++ {
a := fake(t, f, "sv_SE.address")
if !regexp.MustCompile(`\n`).MatchString(a) || !digit.MatchString(a) {
t.Fatalf("address %q is not a multi-line address with a number", a)
}
}
locality := regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`)
for i := 0; i < 30; i++ {
if c := fake(t, f, "sv_SE.address.locality"); !locality.MatchString(c) {
t.Fatalf("locality %q is not a Swedish place name", c)
}
}
}
// TestShippedAddressIsOneNamedPick pins that an address's street, postal code,
// locality and region are one place in the geo tables, read through a name in a template or
// across a struct's tags, and that a reference beside the name is another address.
func TestShippedAddressIsOneNamedPick(t *testing.T) {
f := shipped(t, WithSeed(3))
for _, c := range []struct{ locale, country, template, largest string }{
{"sv_SE", "SE", "{a.street}|{a.postal-code}|{a.locality}|", "Stockholm"},
{"en_US", "US", "{a.street}|{a.postal-code}|{a.locality}|{a.region}", "New York"},
} {
places := geoPlaces(t, c.country)
apart, localities := false, map[string]int{}
for i := 0; i < 2000; i++ {
got := fakeTemplate(t, f, `{/`+c.locale+`.address as a}`+c.template+`|{/`+c.locale+`.address.street}`)
parts := strings.Split(got, "|")
if !places[strings.Join(parts[:4], "|")] {
t.Fatalf("%s.address: %q is no street, postal code, locality and region of one place", c.locale, got)
}
apart = apart || parts[0] != parts[4]
localities[parts[2]]++
}
if !apart {
t.Errorf("%s: a reference beside a name drew the name's street in 2000 renders, want two addresses", c.locale)
}
if c.country == "SE" {
var row struct {
Street string `fake:"{/sv_SE.address as a}{a.street}"`
PostalCode string `fake:"{a.postal-code}"`
Locality string `fake:"{a.locality}"`
}
for i := 0; i < 200; i++ {
if err := f.FakeStruct(&row); err != nil {
t.Fatal(err)
}
if got := row.Street + "|" + row.PostalCode + "|" + row.Locality + "|"; !places[got] {
t.Fatalf("sv_SE.address: struct tags reading one name, %q, are no street, postal code and locality of one place", got)
}
}
}
for l, n := range localities {
if n > localities[c.largest] {
t.Errorf("%s.address drew %s %d times and %s %d, want the locality weighted by population", c.locale, l, n, c.largest, localities[c.largest])
}
}
}
}
// 0644000 is Los Angeles.
func TestShippedPlacesSpreadAddresses(t *testing.T) {
for country, city := range map[string]struct {
key string
least int
}{"SE": {"Göteborg", 1500}, "US": {"0644000", 2500}} {
streets := 0
for _, r := range geoRows(t, country, "street") {
if r[1] == city.key {
streets++
}
}
if streets < city.least {
t.Errorf("geo.%s: %s ships %d streets, want at least %d", country, city.key, streets, city.least)
}
}
header, _, _ := strings.Cut(geoTSV(t, "US", "locality"), "\n")
population := slices.Index(strings.Split(header, "\t"), "population")
if population < 0 {
t.Fatal("geo.US locality.tsv has no population column")
}
for _, r := range geoRows(t, "US", "locality") {
n, err := strconv.Atoi(r[population])
if err != nil {
t.Fatal(err)
}
if n < 2500 {
return
}
}
t.Error("geo.US: every locality holds 2500 people or more, want places drawn down to villages")
}
// TestShippedCapitalsAndBigPlaces pins state capitals and places of 100,000 or more that a
// draw by population leaves out.
func TestShippedCapitalsAndBigPlaces(t *testing.T) {
state := map[string]string{}
for _, r := range geoRows(t, "US", "municipality") {
state[r[0]] = r[2]
}
shipped := map[string]bool{}
for _, r := range geoRows(t, "US", "locality") {
shipped[r[1]+", "+state[r[2]]] = true
}
for _, place := range []string{
"Albany, NY", "Augusta, ME", "Baton Rouge, LA", "Bismarck, ND", "Boise, ID", "Charleston, WV",
"Frankfort, KY", "Harrisburg, PA", "Helena, MT", "Juneau, AK", "Kansas City, KS", "Lafayette, LA",
"Lexington, KY", "Montpelier, VT", "New Orleans, LA", "Pierre, SD", "St. Paul, MN", "Stockton, CA",
"Ventura, CA",
} {
if !shipped[place] {
t.Errorf("geo.US: no locality %s, want every state capital and place of 100,000 or more", place)
}
}
}
func TestShippedSwedishStreetsAreStreetNames(t *testing.T) {
refused := map[string]bool{
"Färjeled Henriksdalsbryggan - Barnängsbryggan": true, "HÄLLSÄTTERSLEDEN": true, "Kärrs VSF": true,
"Länsväg 694": true, "NN": true, "Risövägen,Dejstolvägen,Runvägen": true, "tornslingan": true,
"Öresundsvägen, Bondevägen": true,
}
for _, r := range geoRows(t, "SE", "street") {
if refused[r[0]] {
t.Errorf("geo.SE: street %q in %s, want no road number, ferry route, label, placeholder, two names or miscased name", r[0], r[1])
}
}
}
func geoTSV(t *testing.T, country, table string) string {
b, err := os.ReadFile("data/geo/" + country + "/" + table + ".tsv")
if err != nil {
t.Fatal(err)
}
return strings.TrimSpace(string(b))
}
func geoRows(t *testing.T, country, table string) [][]string {
var out [][]string
for _, line := range strings.Split(geoTSV(t, country, table), "\n")[1:] {
out = append(out, strings.Split(line, "\t"))
}
return out
}
// geoPlaces reads a country's geo tables into every "street|postal-code|locality|region"
// one locality holds, the region empty for SE, whose address carries none.
func geoPlaces(t *testing.T, country string) map[string]bool {
rows := func(table string) [][]string { return geoRows(t, country, table) }
name, region := map[string]string{}, map[string]string{}
if country == "US" {
county := map[string]string{}
for _, r := range rows("municipality") {
county[r[0]] = r[2]
}
for _, r := range rows("locality") {
name[r[0]], region[r[0]] = r[1], county[r[2]]
}
} else {
for _, r := range rows("locality") {
name[r[0]] = r[0]
}
}
codes := map[string][]string{}
for _, r := range rows("postal-code") {
codes[r[1]] = append(codes[r[1]], r[0])
}
places := map[string]bool{}
for _, r := range rows("street") {
for _, code := range codes[r[1]] {
places[strings.Join([]string{r[0], code, name[r[1]], region[r[1]]}, "|")] = true
}
}
return places
}
func TestShippedPersonHasParts(t *testing.T) {
f := shipped(t, WithSeed(7))
for _, path := range []string{"sv_SE.person", "en_US.person"} {
for i := 0; i < 30; i++ {
if name := fake(t, f, path); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) {
t.Fatalf("%s %q lacks first and last name", path, name)
}
}
}
}
// NANPA's central office code records mark N11, 555, 950, 958, 959 and 976 unassignable.
func TestShippedUSPhone(t *testing.T) {
f := shipped(t, WithSeed(11))
re := regexp.MustCompile(`^(?:\(\d{3}\) |\d{3}-)([2-9]\d{2})-\d{4}$`)
unassignable := map[string]bool{"555": true, "950": true, "958": true, "959": true, "976": true}
for i := 0; i < 2000; i++ {
n := fake(t, f, "en_US.phone")
m := re.FindStringSubmatch(n)
if m == nil {
t.Fatalf("phone %q does not match %s", n, re)
}
if exchange := m[1]; exchange[1:] == "11" || unassignable[exchange] {
t.Fatalf("phone %q has the unassignable exchange %s", n, exchange)
}
}
}
func TestShippedTableCellsAreTrimmed(t *testing.T) {
err := fs.WalkDir(os.DirFS("data"), ".", func(path string, d fs.DirEntry, err error) error {
if err != nil || !strings.HasSuffix(path, ".tsv") {
return err
}
b, err := fs.ReadFile(os.DirFS("data"), path)
if err != nil {
return err
}
for i, line := range strings.Split(string(b), "\n") {
for _, cell := range strings.Split(line, "\t") {
if cell != strings.TrimSpace(cell) {
t.Errorf("data/%s:%d: cell %q is padded", path, i+1, cell)
}
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
// TestShippedNamespacedTree loads every shipped module and reaches each locale through its
// namespace: data/sv_SE/person.json, mounted under sv_SE, is sv_SE.person.
func TestShippedNamespacedTree(t *testing.T) {
f := shipped(t, WithSeed(1))
for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} {
if got := fake(t, f, path); got == "" {
t.Fatalf("Fake(%q) returned empty", path)
}
}
if _, err := f.Fake("person"); err == nil {
t.Fatal("Fake(person) = nil error; categories should be namespaced under the locale folder")
}
}
func digitsOnly(s string) string {
var b strings.Builder
for _, r := range s {
if r >= '0' && r <= '9' {
b.WriteRune(r)
}
}
return b.String()
}
// luhnValid verifies a full number (payload + trailing check digit). It doubles
// from the second-from-right, independent of the builtin's own Luhn code.
func luhnValid(s string) bool {
sum, double := 0, false
for i := len(s) - 1; i >= 0; i-- {
n := int(s[i] - '0')
if double {
if n *= 2; n > 9 {
n -= 9
}
}
double = !double
sum += n
}
return sum%10 == 0
}
// swedishName matches one or more letter-words, optionally space/hyphen joined
// ("Storgatan", "Norra Promenaden", "von Flemming"). Used by the composition
// tests so shipped name lists can grow without re-enumerating them here.
var swedishName = regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`)
func TestShippedStreetIsARegisteredName(t *testing.T) {
f := shipped(t, WithSeed(5))
street := regexp.MustCompile(`^\p{L}[\p{L}\d:.-]*([ -][\p{L}\d:.-]+)*$`)
for i := 0; i < 300; i++ {
if s := fake(t, f, "sv_SE.address.street"); !street.MatchString(s) {
t.Fatalf("street %q is not a Swedish street name", s)
}
}
}
func TestShippedLastNameComposition(t *testing.T) {
// last is a choice of patronymic {first}sson templates, compound
// {first}{last} templates and literal surnames.
f := shipped(t, WithSeed(6))
for i := 0; i < 300; i++ {
if s := fake(t, f, "sv_SE.person.last"); !swedishName.MatchString(s) {
t.Fatalf("last name %q is not a Swedish surname", s)
}
}
}
func TestShippedStreetNumberFormats(t *testing.T) {
// Reachable via a hyphenated path; covers all five weighted number variants.
f := shipped(t, WithSeed(8))
re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`)
for i := 0; i < 300; i++ {
if n := fake(t, f, "sv_SE.address.street-number"); !re.MatchString(n) {
t.Fatalf("street-number %q does not match %s", n, re)
}
}
}
// TestShippedUSTitleAgreesWithSex pins that one person's title and sex agree, and
// that the everyday titles are reachable.
func TestShippedUSTitleAgreesWithSex(t *testing.T) {
f := shipped(t, 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 as p}{p.prefix}|{p.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)
}
}
swedish := map[string]bool{"dr": true, "prof": true}
for i := 0; i < 50; i++ {
if got := fake(t, f, "sv_SE.title"); !swedish[got] {
t.Fatalf("sv_SE.title = %q, want an unsexed Swedish honorific", got)
}
}
}