Files
fejkdata/record_test.go

569 lines
21 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fejkdata
import (
"encoding/csv"
"encoding/json"
"reflect"
"strings"
"testing"
)
func recordCat(t *testing.T) *Generator {
t.Helper()
dir := writeData(t, map[string]string{
"users": `{
"format": "{first} {last}",
"first": ["Ada", "Bo"],
"last": ["Lovelace", "Ek"]
}`,
})
return newGenerator(t, dir, WithSeed(1))
}
func TestRecordProjectsFieldsAsColumns(t *testing.T) {
f := recordCat(t)
r, err := f.FakeRecord("users")
if err != nil {
t.Fatal(err)
}
got := r.Columns()
if len(got) != 2 || got[0].Name != "first" || got[1].Name != "last" {
t.Fatalf("Record.Columns() = %v, want columns first, last in name order", got)
}
if (got[0].Value != "Ada" && got[0].Value != "Bo") || (got[1].Value != "Lovelace" && got[1].Value != "Ek") {
t.Fatalf("columns = %v, want the field values", got)
}
}
func TestRecordIsDeterministic(t *testing.T) {
a, b := recordCat(t), recordCat(t)
for i := 0; i < 20; i++ {
x, _ := a.FakeRecord("users")
y, _ := b.FakeRecord("users")
if x.JSON() != y.JSON() {
t.Fatalf("same seed diverged: %s != %s", x.JSON(), y.JSON())
}
}
}
func TestRecordSkipsReferenceBindings(t *testing.T) {
dir := writeData(t, map[string]string{
"name": `{"format": "{first} {last}", "first": "Ada", "last": "Lovelace"}`,
"user": `{"format": "they are {/name}", "id": "1"}`,
})
f := newGenerator(t, dir, WithSeed(1))
r, err := f.FakeRecord("user")
if err != nil {
t.Fatal(err)
}
got := r.Columns()
if len(got) != 1 || got[0].Name != "id" {
t.Fatalf("Record.Columns() = %v, want only the id column (a {/path} is not a column)", got)
}
}
func TestRecordErrors(t *testing.T) {
dir := writeData(t, map[string]string{
"bare": `"hello"`,
"pick": `["a", "b"]`,
"group/cat": `"x"`,
})
f := newGenerator(t, dir, WithSeed(1))
for _, c := range []struct {
path string
want string
}{
{"bare", "has no fields, so no columns"},
{"pick", `names a choice, not a template; a record is a template whose fields are its columns; render it as a column of one: {"format":"","pick":"{/pick}"}`},
{"group", "names a folder"},
{"nope", "no entry"},
} {
if _, err := f.FakeRecord(c.path); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("FakeRecord(%q) = %v, want an error containing %q", c.path, err, c.want)
}
}
}
func TestRecordJSON(t *testing.T) {
f := recordCat(t)
r, err := f.FakeRecord("users")
if err != nil {
t.Fatal(err)
}
var m map[string]string
if err := json.Unmarshal([]byte(r.JSON()), &m); err != nil {
t.Fatalf("Record.JSON() is not valid JSON: %v\n%s", err, r.JSON())
}
if len(m) != 2 || (m["first"] != "Ada" && m["first"] != "Bo") {
t.Fatalf("Record.JSON() = %s, want two addressable columns", r.JSON())
}
for _, f := range r.Columns() {
if m[f.Name] != f.Value {
t.Fatalf("JSON column %q = %q, want %q", f.Name, m[f.Name], f.Value)
}
}
}
func TestRecordCSV(t *testing.T) {
dir := writeData(t, map[string]string{
"note": `{"format": "{word}, {word}", "word": ["a", "b,c", "d\"e", "f\n"]}`,
})
f := newGenerator(t, dir, WithSeed(1))
seen := map[string]bool{}
for i := 0; i < 200 && len(seen) < 4; i++ {
r, err := f.FakeRecord("note")
if err != nil {
t.Fatal(err)
}
if got := r.CSVHeader(); got != "word" {
t.Fatalf("CSVHeader() = %q, want word", got)
}
rec, err := csv.NewReader(strings.NewReader(r.CSVLine() + "\n")).Read()
if err != nil {
t.Fatalf("CSVLine() is not valid CSV: %v\n%q", err, r.CSVLine())
}
want := r.Columns()[0].Value
if len(rec) != 1 || rec[0] != want {
t.Fatalf("CSVLine() = %v, want the field value %q round-tripped", rec, want)
}
seen[want] = true
}
if len(seen) != 4 {
t.Fatalf("round-tripped %d of the 4 values; the comma, quote and newline shapes must each survive", len(seen))
}
for value, want := range map[string]string{`\.`: `"\."`, " x": `" x"`, " x": "\" x\"", "a\rb": "\"a\rb\""} {
body, _ := json.Marshal(map[string]string{"format": "", "v": value})
r, err := newGenerator(t, writeData(t, map[string]string{"q": string(body)})).FakeRecord("q")
if err != nil {
t.Fatal(err)
}
if got := r.CSVLine(); got != want {
t.Errorf("CSVLine() of %q = %q, want %q, quoted where encoding/csv quotes", value, got, want)
}
}
}
func TestRecordCSVEmptyValueStaysARow(t *testing.T) {
dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`})
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)
}
}
func TestRecordWritesTypedAndNullColumns(t *testing.T) {
dir := writeData(t, map[string]string{
"row": `{"format":"","age":{"format":"42","datatype":"integer"},"gone":null,"name":"O'Brien","nick":"","paid":{"format":"true","datatype":"boolean"},"price":{"format":"19.99","datatype":"number"}}`,
})
r, err := newGenerator(t, dir, WithSeed(1)).FakeRecord("row")
if err != nil {
t.Fatal(err)
}
want := []Column{
{Name: "age", DataType: DataTypeInteger, Value: "42"},
{Name: "gone", Null: true},
{Name: "name", Value: "O'Brien"},
{Name: "nick"},
{Name: "paid", DataType: DataTypeBoolean, Value: "true"},
{Name: "price", DataType: DataTypeNumber, Value: "19.99"},
}
if got := r.Columns(); !reflect.DeepEqual(got, want) {
t.Errorf("Columns() = %+v, want %+v", got, want)
}
if got, want := r.JSON(), `{"age":42,"gone":null,"name":"O'Brien","nick":"","paid":true,"price":19.99}`; got != want {
t.Errorf("JSON() = %s, want %s", got, want)
}
if got, want := r.SQLInsert("t"), `INSERT INTO "t" ("age", "gone", "name", "nick", "paid", "price") VALUES (42, NULL, 'O''Brien', '', true, 19.99);`; got != want {
t.Errorf("SQLInsert() = %s, want %s", got, want)
}
if got, want := r.CSVLine(), `42,,O'Brien,"",true,19.99`; got != want {
t.Errorf("CSVLine() = %s, want %s: null an unquoted empty field, an empty string quoted", got, want)
}
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 TestRecordColumnsReadingOneReferenceLoad(t *testing.T) {
cat := `{"format":"{a}","a":[{"format":"A={b}","b":"1"},{"format":"A={b}","b":"2"}]}`
mid := `"{/cat.a}"`
for _, row := range []string{
`{"format":"","whole":"{/cat.a}","inner":{"format":"{/cat.a.b}","repeat":2,"separator":"-"}}`,
`{"format":"","whole":"{/cat.a}","inner":"{/cat.a.b}"}`,
`{"format":"","whole":"{/cat.a}","inner":{"format":"{/cat.a.b} {x}","x":"1"}}`,
`{"format":"","whole":"{/cat.a}","inner":[{"format":"{/cat.a.b} {x}","x":"1"},{"format":"{/cat.a.b}! {x}","x":"2"}]}`,
`{"format":"","whole":"{uppercase(/cat.a)}","inner":"{/cat.a.b}"}`,
`{"format":"","whole":"{/cat}","inner":"{/cat.a.b}"}`,
`{"format":"","whole":"{/mid}","inner":"{/cat.a.b}"}`,
} {
f := newGenerator(t, writeData(t, map[string]string{"cat": cat, "mid": mid, "row": row}), WithSeed(1))
if _, err := f.FakeRecord("row"); err != nil {
t.Errorf("%s: FakeRecord = %v, want it accepted", row, err)
}
}
}
func TestRecordColumnReadingItsOwnRecordIsAFreshDraw(t *testing.T) {
person := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"],"other":"{/person.first}"}`
f := newGenerator(t, writeData(t, map[string]string{"person": person}), WithSeed(1))
apart := false
for i := 0; i < 50 && !apart; i++ {
r, err := f.FakeRecord("person")
if err != nil {
t.Fatal(err)
}
c := r.Columns()
apart = c[0].Value != c[2].Value
}
if !apart {
t.Error("other matched first in 50 records, want a draw of its own")
}
}
func TestRecordBareReferenceStaysIndependentAsAnOperand(t *testing.T) {
dir := writeData(t, map[string]string{
"cur": `[{"format":"{code}","code":"aud"},{"format":"{code}","code":"eur","symbol":"€"}]`,
"row": `{"format":"","up":"{uppercase(/cur)}","low":"{lowercase(/cur)}"}`,
})
f := newGenerator(t, dir, WithSeed(1))
sawMismatch := false
for i := 0; i < 200 && !sawMismatch; i++ {
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
m := map[string]string{}
for _, c := range r.Columns() {
m[c.Name] = c.Value
}
sawMismatch = !strings.EqualFold(m["up"], m["low"])
}
if !sawMismatch {
t.Error("two bare-reference operand columns never disagreed; a bare reference draws on its own, as the plain spelling does")
}
for i := 0; i < 200; i++ {
v, err := f.FakeTemplate("{/cur}|{uppercase(/cur)}")
if err != nil {
t.Fatal(err)
}
if parts := strings.Split(v, "|"); !strings.EqualFold(parts[0], parts[1]) {
return
}
}
t.Error("{/cur}|{uppercase(/cur)} never disagreed in 200 renders; each reference draws on its own")
}
func TestRecordSQLInsert(t *testing.T) {
dir := writeData(t, map[string]string{
"person": `{"format": "{last}", "last": "O'Brien"}`,
})
f := newGenerator(t, dir, WithSeed(1))
r, err := f.FakeRecord("person")
if err != nil {
t.Fatal(err)
}
got := r.SQLInsert("people")
if got != `INSERT INTO "people" ("last") VALUES ('O''Brien');` {
t.Fatalf("SQLInsert() = %q, want quoted identifiers and the single quote doubled", got)
}
}
func TestRecordRejectsFieldDescent(t *testing.T) {
dir := writeData(t, map[string]string{
"cat": `{"format":"{sub}","sub":{"format":"{x}","x":"1"}}`,
"row": `[{"format":"{x}","x":"1"},{"format":"{x}","x":"2","y":"b"}]`,
})
f := newGenerator(t, dir, WithSeed(1))
for _, path := range []string{"cat.sub", "row.x"} {
if _, err := f.FakeRecord(path); err == nil || !strings.Contains(err.Error(), "field") {
t.Errorf("FakeRecord(%q) = %v, want a 'descends into a field' error", path, err)
}
}
}
func TestRecordColumnsReadOneName(t *testing.T) {
dir := writeData(t, map[string]string{
"currency": `[{"format":"{code}","code":"AUD","symbol":"$"},{"format":"{code}","code":"EUR","symbol":"€","name":"Euro"}]`,
"price": `{"format":"{code} {symbol}","code":"{/currency as c}{c.code}","symbol":"{c.symbol}"}`,
})
f := newGenerator(t, dir, WithSeed(1))
symbols := map[string]string{"AUD": "$", "EUR": "€"}
for i := 0; i < 100; i++ {
r, err := f.FakeRecord("price")
if err != nil {
t.Fatal(err)
}
m := map[string]string{}
for _, c := range r.Columns() {
m[c.Name] = c.Value
}
if symbol, known := symbols[m["code"]]; !known || m["symbol"] != symbol {
t.Fatalf("record %s, want one currency draw across columns", r.JSON())
}
v := fake(t, f, "price")
if code, symbol, _ := strings.Cut(v, " "); symbol == "" || symbols[code] != symbol {
t.Fatalf("Fake(price) = %q, want its fields one currency draw, as the record's columns are", v)
}
}
}
func TestRepeatIterationsPickNamesAnew(t *testing.T) {
dir := writeData(t, map[string]string{
"party": `{"format":"{host}: {guests}","guests":{"format":"{/person as p}{p.first} {p.last}","repeat":3,"separator":", "},"host":"{/person as h}{h.first} {h.last}"}`,
"person": drawPeople,
})
f := newGenerator(t, dir, WithSeed(1))
differed := map[[2]int]bool{}
check := func(view string, names []string) {
t.Helper()
if len(names) != 4 {
t.Fatalf("%s rendered %q, want a host and three guests", view, names)
}
for i, name := range names {
if !onePerson(name) {
t.Fatalf("%s rendered %q, want each name one person", view, names)
}
for j := range names[:i] {
differed[[2]int{j, i}] = differed[[2]int{j, i}] || names[j] != name
}
}
}
for i := 0; i < 100; i++ {
r, err := f.FakeRecord("party")
if err != nil {
t.Fatal(err)
}
guests, host := r.Columns()[0].Value, r.Columns()[1].Value
check("FakeRecord", append([]string{host}, strings.Split(guests, ", ")...))
host, guests, _ = strings.Cut(fake(t, f, "party"), ": ")
check("Fake", append([]string{host}, strings.Split(guests, ", ")...))
}
for pair, ok := range differed {
if !ok {
t.Errorf("names %v never differed in 200 renders; the host and each repeat iteration are a draw of their own, so four people", pair)
}
}
}
func TestRecordNamesDrawApart(t *testing.T) {
dir := writeData(t, map[string]string{
"person": drawPeople,
"transfer": `{"format":"{from_first} {from_last} to {to_first} {to_last}","from_first":"{/person as from}{from.first}","from_last":"{from.last}","to_first":"{/person as to}{to.first}","to_last":"{to.last}"}`,
})
f := newGenerator(t, dir, WithSeed(1))
apart := map[string]bool{}
for i := 0; i < 100; i++ {
r, err := f.FakeRecord("transfer")
if err != nil {
t.Fatal(err)
}
m := map[string]string{}
for _, c := range r.Columns() {
m[c.Name] = c.Value
}
from, to, _ := strings.Cut(fake(t, f, "transfer"), " to ")
for view, pair := range map[string][2]string{"FakeRecord": {m["from_first"] + " " + m["from_last"], m["to_first"] + " " + m["to_last"]}, "Fake": {from, to}} {
if !onePerson(pair[0]) || !onePerson(pair[1]) {
t.Fatalf("%s drew %q and %q, want each name one person", view, pair[0], pair[1])
}
apart[view] = apart[view] || pair[0] != pair[1]
}
}
for _, view := range []string{"FakeRecord", "Fake"} {
if !apart[view] {
t.Errorf("%s: names from and to drew one person in 100 renders, want a pick each", view)
}
}
}
func TestRecordColumnOfOneReferenceIsTheColumnItReads(t *testing.T) {
dir := writeData(t, map[string]string{
"mid": `{"format":"","score":"{/src.score}"}`,
"row": `{"format":"","chain":"{/mid.score}","code":{"format":"{/src.code}","datatype":"integer"},"dot":"{.src.score}","label":"n={/src.score}","mixed":[{"format":"{text}","text":"{/src.score}"},"n/a"],"pick":["{/src.score}",{"format":"7","datatype":"integer"}],"same":"{/src.score}","score":"{/src.score}","text":"{/src.code}"}`,
"src": `{"format":"","code":[null,"200","404"],"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
})
f := newGenerator(t, dir, WithSeed(1))
inline, err := f.NewRecordTemplate(`{"format":"","score":"{/src.score}"}`)
if err != nil {
t.Fatal(err)
}
nulls := map[string]int{}
for i := 0; i < 200; i++ {
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
cols := map[string]Column{}
for _, c := range r.Columns() {
cols[c.Name] = c
}
score, code := cols["score"], cols["code"]
digit := func(c Column) bool { return len(c.Value) == 1 && c.Value >= "1" && c.Value <= "9" }
isScore := func(name string) bool { return cols[name].Null != digit(cols[name]) }
switch {
case score.DataType != DataTypeInteger || cols["chain"].DataType != DataTypeInteger || code.DataType != DataTypeInteger || cols["pick"].DataType != DataTypeInteger || cols["label"].DataType != DataTypeString || cols["mixed"].DataType != DataTypeString || cols["text"].DataType != DataTypeString:
t.Fatalf("%s: want score, chain, code and pick integer columns, label, mixed and text string ones", r.JSON())
case !isScore("score") || !isScore("same") || !isScore("chain") || !isScore("dot"):
t.Fatalf("%s: want score, same, chain and dot each null or a digit from src.score", r.JSON())
case code.Null == (code.Value == "200" || code.Value == "404"):
t.Fatalf("code = %+v, want null or a code from src.code", code)
case cols["pick"].Value != "7" && !isScore("pick"), cols["mixed"].Null || cols["mixed"].Value != "n/a" && cols["mixed"].Value != "" && !digit(cols["mixed"]):
t.Fatalf("pick = %+v, mixed = %+v: want pick 7 or a score, mixed n/a or a score's text", cols["pick"], cols["mixed"])
case cols["label"].Null || !strings.HasPrefix(cols["label"].Value, "n="):
t.Fatalf("label = %+v, want the text of the read, a null as \"\"", cols["label"])
case strings.Contains(r.JSON(), `"score":null`) != score.Null:
t.Fatalf("JSON() = %s, want a null score written null", r.JSON())
}
ir := inline.Fake()
ic, sqlValue := ir.Columns()[0], ir.Columns()[0].Value
if ic.Null {
sqlValue = "NULL"
}
if ic.DataType != DataTypeInteger || ic.Null == (len(ic.Value) == 1) || ir.SQLInsert("t") != `INSERT INTO "t" ("score") VALUES (`+sqlValue+`);` || ir.CSVLine() != ic.Value {
t.Fatalf("inline score = %+v written %s and %q, want an integer column, NULL and an empty field or a bare digit", ic, ir.SQLInsert("t"), ir.CSVLine())
}
for name, c := range map[string]Column{"code": code, "inline": ic, "score": score} {
if c.Null {
nulls[name]++
}
}
}
for _, name := range []string{"code", "inline", "score"} {
if n := nulls[name]; n == 0 || n == 200 {
t.Errorf("%s drew null %d times in 200 records, want both outcomes", name, n)
}
}
}
func TestRecordSQLQuotesIdentifiers(t *testing.T) {
dir := writeData(t, map[string]string{
"row": `{"format": "", "postal-code": "1", "street-number": "2"}`,
})
f := newGenerator(t, dir, WithSeed(1))
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
got := r.SQLInsert("my-table")
if got != `INSERT INTO "my-table" ("postal-code", "street-number") VALUES ('1', '2');` {
t.Fatalf("SQLInsert() = %q, want hyphenated identifiers and table quoted", got)
}
}
func TestFakeRecordTemplateAndNewRecordTemplate(t *testing.T) {
f := recordCat(t)
in := `{"format":"{x} {y}","x":["1","2"],"y":["3","4"]}`
want, err := f.FakeRecordTemplate(in)
if err != nil {
t.Fatalf("FakeRecordTemplate: %v", err)
}
if len(want.Columns()) != 2 {
t.Fatalf("FakeRecordTemplate columns = %v, want two columns", want.Columns())
}
reusable, err := f.NewRecordTemplate(in)
if err != nil {
t.Fatalf("NewRecordTemplate: %v", err)
}
for i := 0; i < 20; i++ {
if got := reusable.Fake(); len(got.Columns()) != 2 {
t.Fatalf("RecordTemplate.Fake() = %v, want two columns", got.Columns())
}
}
}
func TestInlineRecordErrors(t *testing.T) {
f := recordCat(t)
for _, c := range []struct {
input string
want string
}{
{`"hello"`, "has no fields, so no columns"},
{`["a","b"]`, "a record is a template whose fields are its columns"},
{`{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`, "carries repeat 3"},
} {
if _, err := f.FakeRecordTemplate(c.input); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("FakeRecordTemplate(%q) = %v, want an error naming %q", c.input, err, c.want)
}
}
}
func TestRecordRejectsATopLevelRepeat(t *testing.T) {
dir := writeData(t, map[string]string{
"rep": `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`,
})
f := newGenerator(t, dir, WithSeed(1))
if _, err := f.FakeRecord("rep"); err == nil || !strings.Contains(err.Error(), "carries repeat 3") {
t.Errorf("FakeRecord on a repeating template = %v, want an error naming the repeat", err)
}
if v, err := f.Fake("rep"); err != nil || v != "x-|x-|x-" {
t.Errorf("Fake(rep) = %q, %v, want the repeat still composed for the string view", v, err)
}
}
func TestRecordTemplateRejectsATopLevelRepeat(t *testing.T) {
f := recordCat(t)
in := `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`
if _, err := f.NewRecordTemplate(in); err == nil || !strings.Contains(err.Error(), "carries repeat 3") {
t.Errorf("NewRecordTemplate on a repeating template = %v, want an error naming the repeat", err)
}
if _, err := f.NewTemplate(in); err != nil {
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 := shipped(t, 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)
}
}
func TestEveryRecordCarriesItsShapeOnceLoaded(t *testing.T) {
f := newShippedWhole(t, WithSeed(1))
records := 0
if err := eachNode(&f.root, "", func(label string, n node) error {
tm, isTemplate := n.(*template)
if !isTemplate || !tm.isRecord || len(tm.fields) == 0 {
return nil
}
records++
if len(tm.columns) != len(tm.fields) {
t.Errorf("%s: %d record columns, want one per field, %d", label, len(tm.columns), len(tm.fields))
}
for _, c := range tm.columns {
if c.field != tm.fields[c.name] || c.datatype != columnDatatype(c.field) {
t.Errorf("%s: column %q holds field %v of datatype %s, want field %q's", label, c.name, c.field, c.datatype, c.name)
}
}
return nil
}); err != nil {
t.Fatal(err)
}
if records == 0 {
t.Fatal("no shipped record walked")
}
}