From 35487cd36d2379c39beb5ca6003b16592c496dae Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 11 Oct 2026 20:45:52 +0200 Subject: [PATCH] Test the CLI as package cli's Main over the modules it is given, and fejkdata's main over the shipped packages --- cli/cli_test.go | 885 +++++++++++++++++++++++++++++ {cmd/fejkdata => cli}/lone_test.go | 2 +- cmd/fejkdata/main_test.go | 848 +-------------------------- imports_test.go | 17 +- 4 files changed, 912 insertions(+), 840 deletions(-) create mode 100644 cli/cli_test.go rename {cmd/fejkdata => cli}/lone_test.go (98%) diff --git a/cli/cli_test.go b/cli/cli_test.go new file mode 100644 index 0000000..bf0d88f --- /dev/null +++ b/cli/cli_test.go @@ -0,0 +1,885 @@ +package cli + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "errors" + "io/fs" + "os" + "path/filepath" + "regexp" + "runtime/debug" + "strings" + "testing" + "testing/fstest" +) + +// shipped holds the shipped modules as read from the checkout, so these tests link no data +// package; cmd/fejkdata runs over the packages themselves. +var shipped = []fs.FS{ + os.DirFS("../data/en_US"), + os.DirFS("../data/geo/SE"), + os.DirFS("../data/geo/US"), + os.DirFS("../data/misc"), + os.DirFS("../data/sv_SE"), +} + +// svSE, enUS and misc are copies of a shipped module's files without its manifest, so a test +// layers its categories unprefixed with -d. +var svSE, enUS, misc string + +func TestMain(m *testing.M) { + tmp, err := os.MkdirTemp("", "fejkdata-cli-test") + if err != nil { + panic(err) + } + svSE, enUS, misc = bareCopy(tmp, "sv_SE"), bareCopy(tmp, "en_US"), bareCopy(tmp, "misc") + code := m.Run() + os.RemoveAll(tmp) + os.Exit(code) +} + +// bareCopy copies the category and rows files of the shipped module in data/ into a +// folder under tmp, and returns that folder. +func bareCopy(tmp, module string) string { + src, dst := filepath.Join("..", "data", module), filepath.Join(tmp, module) + if err := os.Mkdir(dst, 0o755); err != nil { + panic(err) + } + for _, pattern := range []string{"*.json", "*.tsv"} { + files, err := filepath.Glob(filepath.Join(src, pattern)) + if err != nil { + panic(err) + } + for _, f := range files { + if strings.HasPrefix(filepath.Base(f), ".") { + continue + } + b, err := os.ReadFile(f) + if err == nil { + err = os.WriteFile(filepath.Join(dst, filepath.Base(f)), b, 0o644) + } + if err != nil { + panic(err) + } + } + } + return dst +} + +func runOut(stdin string, args ...string) (int, string, string) { + var out, errb bytes.Buffer + code := run(shipped, args, strings.NewReader(stdin), &out, &errb) + return code, out.String(), errb.String() +} + +// runTerminal runs as from a shell with nothing piped in. +func runTerminal(args ...string) (int, string, string) { + var out, errb bytes.Buffer + code := run(shipped, args, nil, &out, &errb) + return code, out.String(), errb.String() +} + +func TestRunLoadsTheModulesItIsGiven(t *testing.T) { + module := fstest.MapFS{"pet.json": {Data: []byte(`{"format":"{name}","name":["Fido"]}`)}} + var out, errb bytes.Buffer + if code := run([]fs.FS{module}, nil, strings.NewReader("{/pet}"), &out, &errb); code != 0 || out.String() != "Fido" { + t.Errorf("run over a module holding pet = %d, %q, %q, want Fido", code, out.String(), errb.String()) + } + out.Reset() + if code := run([]fs.FS{module}, []string{"--list"}, nil, &out, &errb); code != 0 || out.String() != "pet\npet.name\n" { + t.Errorf("--list over a module holding pet = %d, %q, want pet and pet.name only", code, out.String()) + } +} + +func TestRunOutputsValue(t *testing.T) { + code, out, errb := runOut("{/person}", "--data-path", svSE) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + if out == "" || strings.Contains(out, "\n") { + t.Fatalf("output = %q, stderr=%q; want one value and nothing added", out, errb) + } +} + +func TestRunDotPath(t *testing.T) { + code, full, _ := runOut("{/person}", "--seed", "7", "--data-path", svSE) + if code != 0 { + t.Fatalf("person run = %d", code) + } + code, last, errb := runOut("{/person.last}", "--seed", "7", "--data-path", svSE) + if code != 0 { + t.Fatalf("person.last run = %d, stderr=%q", code, errb) + } + if strings.TrimSpace(last) == "" { + t.Fatal("empty person.last output") + } + if last == full { + t.Errorf("person.last %q should differ from person %q", last, full) + } +} + +func TestRunSeedSpellings(t *testing.T) { + _, want, _ := runOut("{/address}", "--seed", "42", "--data-path", svSE) + for _, args := range [][]string{ + {"--seed=42", "--data-path", svSE}, + {"-s", "42", "-d", svSE}, + {"--data-path", svSE, "--seed", "42"}, + } { + code, got, errb := runOut("{/address}", args...) + if code != 0 { + t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb) + } + if got != want { + t.Errorf("run(%v) = %q, want %q", args, got, want) + } + } +} + +func TestRunShortFlagValues(t *testing.T) { + _, want, _ := runOut("{/address}", "--seed", "42", "--data-path", svSE) + for _, args := range [][]string{ + {"-s42", "-d", svSE}, + {"-s", "42", "-d" + svSE}, + {"-d", svSE, "-s42"}, + } { + code, got, errb := runOut("{/address}", args...) + if code != 0 { + t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb) + } + if got != want { + t.Errorf("run(%v) = %q, want %q", args, got, want) + } + } + _, three, _ := runOut("{/word}\n", "-s", "1", "-n3", "-d", svSE) + if lines := strings.Split(strings.TrimRight(three, "\n"), "\n"); len(lines) != 3 { + t.Errorf("-n3 gave %d lines: %q", len(lines), three) + } + for _, c := range []struct { + args []string + want string + }{ + {[]string{"-s=42", "-d", svSE}, "-s42"}, + {[]string{"-s=42", "-d", svSE}, "--seed=42"}, + {[]string{"-d=" + svSE}, "--data-path="}, + {[]string{"-nd", "3", "-d", svSE}, `--repeat needs an integer in 0..1048576, got "d"`}, + {[]string{"--seed=", "-d", svSE}, `--seed needs an unsigned integer, got ""`}, + } { + code, out, errb := runOut("{/word}", c.args...) + if code != 2 || out != "" { + t.Errorf("run(%v) = %d, stdout %q, want misuse", c.args, code, out) + } + if !strings.Contains(errb, c.want) { + t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want) + } + } + code, out, _ := runOut("", "-hd", svSE) + if code != 0 || !strings.Contains(out, "Usage") { + t.Errorf("-hd (bundled help) = %d, %q, want usage", code, out) + } +} + +func TestRunDoubleDashEndsFlags(t *testing.T) { + code, out, errb := runOut("{/person}", "--data-path", svSE, "--") + if code != 0 || strings.TrimSpace(out) == "" { + t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb) + } + code, _, errb = runOut("{/person}", "--data-path", svSE, "--", "--list") + if code != 2 || !strings.Contains(errb, "stdin") { + t.Errorf("after --, --list should be an argument: code %d, stderr %q", code, errb) + } +} + +func TestRunSingleDashLongIsRejected(t *testing.T) { + for _, c := range []struct { + args []string + want string + }{ + {[]string{"-seed", "42", "-d", svSE}, "use --seed"}, + {[]string{"-seed=42", "-d", svSE}, "use --seed"}, + {[]string{"-data-path", svSE}, "use --data-path"}, + {[]string{"-list", "-d", svSE}, "use --list"}, + {[]string{"--nope", "-d", svSE}, "unknown flag --nope"}, + {[]string{"-x", "-d", svSE}, "unknown flag -x"}, + } { + code, _, errb := runOut("{/person}", c.args...) + if code != 2 { + t.Errorf("run(%v) = %d, want 2", c.args, code) + } + if !strings.Contains(errb, c.want) { + t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want) + } + } +} + +func TestRunFlagValues(t *testing.T) { + for _, c := range []struct { + args []string + want string + }{ + {[]string{"--seed", "-d", svSE}, `--seed needs an unsigned integer, got "-d"`}, + {[]string{"-d", svSE, "--seed"}, "--seed needs a value"}, + {[]string{"--seed", "x", "-d", svSE}, "--seed"}, + {[]string{"--list=1", "-d", svSE}, "--list takes no value"}, + {[]string{"--repeat", "-1", "-d", svSE}, "--repeat"}, + {[]string{"-n", "-1", "-d", svSE}, "--repeat"}, + } { + code, _, errb := runOut("{/person}", c.args...) + if code != 2 { + t.Errorf("run(%v) = %d, want 2", c.args, code) + } + if !strings.Contains(errb, c.want) { + t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want) + } + } +} + +func TestRunRepeat(t *testing.T) { + code, out, errb := runOut("{/word}\n", "--seed", "1", "--repeat", "3", "--data-path", svSE) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(lines) != 3 { + t.Errorf("repeat=3 gave %d lines: %q", len(lines), out) + } + _, short, _ := runOut("{/word}\n", "--seed", "1", "-n", "3", "-d", svSE) + if short != out { + t.Errorf("-n 3 = %q, want the same as --repeat 3 %q", short, out) + } +} + +func TestRunSeparator(t *testing.T) { + code, out, errb := runOut("{/word}", "--repeat", "3", "--separator", ",", "--data-path", svSE) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + if n := strings.Count(out, "\n"); n != 0 { + t.Errorf("want no newline, got %d: %q", n, out) + } + if !strings.Contains(out, ",") { + t.Errorf("values should be comma-joined: %q", out) + } +} + +func TestRunRepeatAdvancesRNG(t *testing.T) { + code, out, errb := runOut("{/person}\n", "--seed", "1", "--repeat", "5", "--data-path", svSE) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + uniq := map[string]bool{} + for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") { + uniq[l] = true + } + if len(uniq) < 2 { + t.Errorf("repeat should vary output, all identical: %q", out) + } +} + +func TestRunMisuse(t *testing.T) { + for _, c := range []struct { + stdin string + args []string + }{ + {"{/person}", []string{"-d", svSE, "word"}}, + {"", []string{"-d", svSE, "--list", "person"}}, + {"{/sv_SE.person}", []string{"--no-shipped-data"}}, + } { + code, out, errb := runOut(c.stdin, c.args...) + if code != 2 { + t.Errorf("run(%q, %v) = %d, want 2", c.stdin, c.args, code) + } + if !strings.Contains(errb, "try 'fejkdata --help'") { + t.Errorf("run(%q, %v) stderr = %q, want a pointer to --help", c.stdin, c.args, errb) + } + if out != "" { + t.Errorf("run(%q, %v) stdout = %q, want nothing", c.stdin, c.args, out) + } + } +} + +func TestRunTakesNoOperand(t *testing.T) { + file := filepath.Join(t.TempDir(), "users.tmpl") + if err := os.WriteFile(file, []byte("{/sv_SE.person}"), 0o644); err != nil { + t.Fatal(err) + } + for _, c := range []struct { + args []string + want string + }{ + {[]string{"sv_SE.person"}, "echo '{/sv_SE.person}' | fejkdata"}, + {[]string{"/en_US.address"}, "echo '{/en_US.address}' | fejkdata"}, + {[]string{"{/en_US.person}"}, "echo '{/en_US.person}' | fejkdata"}, + {[]string{"name: {/sv_SE.person.last}"}, "echo 'name: {/sv_SE.person.last}' | fejkdata"}, + {[]string{"geo.US.locality[O'Fallon].name"}, `echo '{/geo.US.locality[O'\''Fallon].name}' | fejkdata`}, + {[]string{"en_US.address"}, "echo '{/en_US.address}' | fejkdata"}, + {[]string{`C:\temp {/sv_SE.person}`}, `printf '%s' 'C:\temp {/sv_SE.person}' | fejkdata`}, + {[]string{"-"}, "drop it"}, + {[]string{file}, "fejkdata < " + file}, + } { + code, out, errb := runOut("", c.args...) + if code != 2 || out != "" || !strings.Contains(errb, "unexpected argument") || !strings.Contains(errb, c.want) { + t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want) + } + } +} + +type failingReader struct{} + +func (failingReader) Read([]byte) (int, error) { return 0, errors.New("bad file descriptor") } + +func TestRunUnreadableStdinIsMisuse(t *testing.T) { + var out, errb bytes.Buffer + if code := run(shipped, nil, failingReader{}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "cannot be read") { + t.Errorf("run with an unreadable stdin = %d, %q; want misuse naming it", code, errb.String()) + } +} + +func TestRunTerminalStdinIsMisuse(t *testing.T) { + code, out, errb := runTerminal("--seed", "1") + if code != 2 || out != "" || !strings.Contains(errb, "| fejkdata") { + t.Errorf("run with nothing piped = %d, %q, %q; want misuse showing a template piped to fejkdata", code, out, errb) + } + for _, args := range [][]string{{"--list"}, {"--help"}, {"--version"}} { + if code, out, errb := runTerminal(args...); code != 0 || out == "" { + t.Errorf("run(%v) with nothing piped = %d, %q, %q; want it to run, reading no stdin", args, code, out, errb) + } + } +} + +func TestRunRendersExactlyWhatIsSent(t *testing.T) { + for stdin, want := range map[string]string{ + "hihi": "hihi", + "hihi\n": "hihi\n", + "hihi\r\n": "hihi\r\n", + "hihi\n\n": "hihi\n\n", + "{{x}}\n": "{x}\n", + "\"a\"\n": "a", + " \t\"a\"\r\n\n": "a", + "{/misc.territory[SE].alpha2}": "SE", + "{/misc.territory[SE].alpha2}\n": "SE\n", + } { + code, out, errb := runOut(stdin, "--seed", "1") + if code != 0 || out != want { + t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want) + } + } + if code, out, errb := runOut("{\"format\":\"{x}\\n\",\"x\":[\"a\",\"b\"]}\n"); code != 0 || (out != "a\n" && out != "b\n") { + t.Errorf("a JSON template's format = %d, %q, %q; want its own newline only", code, out, errb) + } + if code, out, errb := runOut("{/sv_SE.word}\n", "--seed", "1", "-n", "3", "--separator", ","); code != 0 || strings.Count(out, "\n,") != 2 || !strings.HasSuffix(out, "\n") { + t.Errorf("echo's newline and a separator = %d, %q, %q; want each render's newline, then the separator", code, out, errb) + } +} + +func TestRunRendersBlankStdin(t *testing.T) { + for _, stdin := range []string{"", "\n", " \r\n\t"} { + if code, out, errb := runOut(stdin); code != 0 || out != stdin { + t.Errorf("run(%q) = %d, %q, %q; want the blank template rendered as it is", stdin, code, out, errb) + } + } +} + +func TestRunTextPrintsAsWritten(t *testing.T) { + for _, stdin := range []string{"sv_SE.person", "[Skåne län]", "x[1]y", `say "hi"`} { + code, out, errb := runOut(stdin) + if code != 0 || out != stdin { + t.Errorf("run(%q) = %d, %q, %q; want the text as written", stdin, code, out, errb) + } + } +} + +func TestRunMultipleDataPaths(t *testing.T) { + code, out, errb := runOut("{/person}", "-d", enUS, "--data-path", svSE) + if code != 0 { + t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb) + } + if strings.TrimSpace(out) == "" { + t.Fatal("empty output for multi-dir run") + } +} + +func TestRunUnknownCategoryFails(t *testing.T) { + code, _, errb := runOut("{/nope}", "--data-path", svSE, "--format", "csv") + if code != 1 { + t.Fatalf("run = %d, want 1", code) + } + if !strings.Contains(errb, "nope") { + t.Errorf("stderr %q should name the unknown category", errb) + } +} + +func TestRunList(t *testing.T) { + code, out, errb := runOut("", "--data-path", svSE, "--list") + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + for _, want := range []string{"person", "person.last", "address"} { + if !strings.Contains(out, want) { + t.Errorf("list output missing %q:\n%s", want, out) + } + } +} + +func TestRunHelpOnStdout(t *testing.T) { + for _, h := range []string{"-h", "--help"} { + code, out, errb := runOut("", h) + if code != 0 { + t.Errorf("%s = %d, want 0", h, code) + } + if !strings.Contains(out, "Usage") || errb != "" { + t.Errorf("%s: stdout = %q, stderr = %q, want usage on stdout only", h, out, errb) + } + } +} + +func TestModuleVersionNamesFejkdatasVersion(t *testing.T) { + custom := &debug.BuildInfo{ + Main: debug.Module{Path: "example.com/mycli", Version: "v9.0.0"}, + Deps: []*debug.Module{{Path: "github.com/larvit/fejkdata", Version: "v0.1.0"}}, + } + own := &debug.BuildInfo{Main: debug.Module{Path: "github.com/larvit/fejkdata", Version: "v0.1.0"}} + checkout := &debug.BuildInfo{Main: debug.Module{Path: "github.com/larvit/fejkdata", Version: "(devel)"}} + for info, want := range map[*debug.BuildInfo]string{custom: "v0.1.0", own: "v0.1.0", checkout: "devel", nil: "devel"} { + if got := moduleVersion(info); got != want { + t.Errorf("moduleVersion(%+v) = %q, want %q", info, got, want) + } + } +} + +func TestRunVersion(t *testing.T) { + code, out, errb := runOut("", "--version") + if code != 0 { + t.Fatalf("--version = %d, stderr=%q", code, errb) + } + version, ok := strings.CutPrefix(out, "fejkdata ") + if !ok || strings.TrimSpace(version) == "" { + t.Errorf("--version = %q, want the command name and a version on stdout", out) + } +} + +func TestRunMissingDirFails(t *testing.T) { + code, _, errb := runOut("{/person}", "--data-path", "../data/nope") + if code != 1 { + t.Fatalf("run = %d, want 1", code) + } + if errb == "" { + t.Error("want an error message on stderr") + } +} + +func TestRunShippedDataByDefault(t *testing.T) { + code, out, errb := runOut("{/sv_SE.person}", "--seed", "1") + if code != 0 || strings.TrimSpace(out) == "" { + t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb) + } + code, list, _ := runOut("", "--list") + if code != 0 || !strings.Contains(list, "en_US.person\n") || !strings.Contains(list, "misc.uuid\n") { + t.Errorf("--list without --data-path = %d, %q", code, list) + } + code, _, errb = runOut("{/person}") + if code != 2 || !strings.Contains(errb, "person") { + t.Errorf("a category outside the shipped tree: code %d, stderr %q", code, errb) + } +} + +func TestRunSelectsATableRow(t *testing.T) { + dir := t.TempDir() + for name, content := range map[string]string{ + "region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`, + "region.tsv": "code\tname\n01\tStockholms län\n12\tSkåne län\n", + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + if code, out, errb := runOut("{/region[12]}", "--no-shipped-data", "-d", dir); code != 0 || out != "Skåne län" { + t.Fatalf("{/region[12]} = %d, %q, stderr=%q", code, out, errb) + } + if code, out, _ := runOut("{/region[Skåne län].code}", "--no-shipped-data", "-d", dir); code != 0 || out != "12" { + t.Fatalf("{/region[Skåne län].code} = %d, %q", code, out) + } + if code, out, _ := runOut("{/region[12]}", "--no-shipped-data", "-d", dir, "--format", "sql"); code != 0 || out != `INSERT INTO "region" ("code", "name") VALUES ('12', 'Skåne län');`+"\n" { + t.Fatalf("--format sql {/region[12]} = %d, %q, want the table named without its selector", code, out) + } + if code, _, errb := runOut("{/region[99]}", "--no-shipped-data", "-d", dir); code != 2 || !strings.Contains(errb, `"99"`) { + t.Fatalf("{/region[99]} = %d, stderr=%q, want misuse naming the row", code, errb) + } + if code, out, _ := runOut("{/misc.territory[SE]}", "--seed", "1", "--format", "csv"); code != 0 || !strings.HasPrefix(out, "alpha2,") || !strings.Contains(out, "\nSE,SWE,") { + t.Fatalf("--format csv {/misc.territory[SE]} = %d, %q", code, out) + } +} + +func TestUsageReferencesResolve(t *testing.T) { + for _, token := range regexp.MustCompile(`\{/[^}]+\}`).FindAllString(usage, -1) { + code, out, errb := runOut(token, "--seed", "1") + if code != 0 || strings.TrimSpace(out) == "" { + t.Errorf("usage advertises %s: run = %d, %q, stderr %q", token, code, out, errb) + } + } +} + +func TestRunOperandMisuseBeforeLoad(t *testing.T) { + code, _, errb := runOut("", "--no-shipped-data", "x") + if code != 2 || !strings.Contains(errb, "unexpected argument") || strings.Contains(errb, "--data-path") { + t.Fatalf("an operand with no data = %d, %q; want the operand refused before any load", code, errb) + } +} + +func TestRunInlineTemplate(t *testing.T) { + code, out, errb := runOut("name: {/sv_SE.person.last}", "--seed", "1") + if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") { + t.Fatalf("inline format string = %d, %q, stderr %q", code, out, errb) + } + code, out, errb = runOut(`{"format":"name: {x}","x":["bosse","lina"]}`, "--seed", "1") + if code != 0 || (out != "name: bosse" && out != "name: lina") { + t.Fatalf("inline JSON template = %d, %q, want one name, stderr %q", code, out, errb) + } + code, out, errb = runOut(`"name: {/sv_SE.person.last}"`, "--seed", "1") + if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") { + t.Fatalf("inline JSON string = %d, %q, stderr %q", code, out, errb) + } + code, out, errb = runOut("{digits(1)}\n", "--seed", "1", "-n", "2") + if code != 0 || len(strings.Split(strings.TrimRight(out, "\n"), "\n")) != 2 { + t.Fatalf("inline template with --repeat = %d, %q, stderr %q", code, out, errb) + } + code, out, errb = runOut("{/sv_SE.person} hihi", "--seed", "1") + if code != 0 || !strings.HasSuffix(out, " hihi") || strings.Contains(out, "{") { + t.Fatalf("a reference and text = %d, %q, stderr %q", code, out, errb) + } +} + +func TestRunFormatTakesOnlyALoneReferenceAsARecord(t *testing.T) { + for _, stdin := range []string{"{//sv_SE.person}", "{/sv_SE.person|/misc.uuid}", "{/sv_SE.person as p}", "{/}", "{/.sv_SE.person}"} { + if code, out, errb := runOut(stdin, "--format", "json"); code != 2 || out != "" { + t.Errorf("run(%q, --format json) = %d, %q, %q; want misuse", stdin, code, out, errb) + } + } + if code, out, errb := runOut(`{"format":"x"}`); code != 0 || out != "x" { + t.Errorf(`run({"format":"x"}) = %d, %q, %q; want x`, code, out, errb) + } +} + +func TestRunTemplateMisuse(t *testing.T) { + for stdin, want := range map[string]string{ + "{bad": "unterminated", + "name: {/no.such.path}": "no entry", + "a } b": "}}", + "{//sv_SE.person}": "write {/sv_SE.person}", + } { + code, out, errb := runOut(stdin, "--seed", "1") + if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) { + t.Errorf("run(%q) = %d, %q, %q; want misuse naming %q and --help", stdin, code, out, errb, want) + } + if strings.Contains(errb, "fejkdata: fejkdata:") { + t.Errorf("run(%q) doubled the program prefix: %q", stdin, errb) + } + } +} + +func TestRunNoShippedData(t *testing.T) { + code, list, errb := runOut("", "--no-shipped-data", "-d", misc, "--list") + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + if strings.Contains(list, "sv_SE") || !strings.Contains(list, "uuid\n") { + t.Errorf("--no-shipped-data --list = %q, want only the given dir", list) + } + code, out, _ := runOut("{/uuid}", "--no-shipped-data", "-d", misc, "-s", "3") + if code != 0 || strings.TrimSpace(out) == "" { + t.Errorf("run = %d, out=%q", code, out) + } + if code, out, errb := runOut("{digits(3)}", "--no-shipped-data"); code != 0 || len(out) != 3 { + t.Errorf("--no-shipped-data alone = %d, %q, %q, want a template reading no data rendered", code, out, errb) + } + if code, _, errb := runOut("{/sv_SE.person}", "--no-shipped-data"); code != 2 || !strings.HasSuffix(errb, "no data is loaded\ntry 'fejkdata --help'\n") { + t.Errorf("--no-shipped-data alone = %d, %q, want a reference to nothing refused", code, errb) + } +} + +func TestRunRepeatOfZeroWritesNoRecord(t *testing.T) { + dir := recordDir(t) + for _, c := range []struct { + args []string + want string + }{ + {nil, ""}, + {[]string{"--format", "json"}, "[]\n"}, + {[]string{"--format", "ndjson"}, ""}, + {[]string{"--format", "csv"}, "first,last\n"}, + {[]string{"--format", "sql"}, ""}, + } { + if code, out, errb := runOut("{/users}", append(c.args, "-n", "0", "-d", dir)...); code != 0 || out != c.want { + t.Errorf("run(-n 0 %v) = %d, %q, %q, want %q", c.args, code, out, errb, c.want) + } + } + if code, _, _ := runOut("{/nope}", "-n", "0", "--format", "json", "-d", dir); code != 1 { + t.Errorf("run(-n 0) of a reference to nothing = %d, want 1", code) + } +} + +func TestRunRepeatIsBounded(t *testing.T) { + for _, args := range [][]string{ + {"--repeat", "9223372036854775807"}, + {"--repeat", "1048577"}, + {"-n", "99999999999"}, + } { + code, out, errb := runOut("{/sv_SE.word}", args...) + if code != 2 || out != "" || !strings.Contains(errb, "0..1048576") { + t.Errorf("run(%v) = %d, %q, %q, want misuse naming the range", args, code, out, errb) + } + } + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "x.json"), []byte(`"x"`), 0o644); err != nil { + t.Fatal(err) + } + code, out, errb := runOut("{/x}", "--no-shipped-data", "-d", dir, "--repeat", "1048576", "--separator", "") + if code != 0 || len(out) != 1048576 { + t.Errorf("repeat at the cap = %d, %d bytes, stderr %q; want every render streamed", code, len(out), errb) + } +} + +func TestRunIgnoresAFlagWithNoEffect(t *testing.T) { + dir := recordDir(t) + for _, c := range []struct { + stdin string + base, ignored []string + }{ + {"", []string{"--list"}, []string{"-n", "3"}}, + {"", []string{"--list"}, []string{"--separator", ","}}, + {"", []string{"--list"}, []string{"--format", "json"}}, + {"", []string{"--list"}, []string{"--table", "t"}}, + {"{/users}", nil, []string{"--table", "t"}}, + {"{/users}", []string{"--format", "json"}, []string{"--table", "t"}}, + } { + args := append([]string{"--seed", "1", "--data-path", dir}, c.base...) + _, want, _ := runOut(c.stdin, args...) + code, got, errb := runOut(c.stdin, append(args, c.ignored...)...) + if code != 0 || want == "" || got != want { + t.Errorf("run(%v + %v) = %d, %q, %q; want what %v alone prints, %q", c.base, c.ignored, code, got, errb, c.base, want) + } + } +} + +func TestRunReadsABareNumberAsAFormatString(t *testing.T) { + if code, out, errb := runOut("42\n", "-n", "3"); code != 0 || out != "42\n42\n42\n" { + t.Errorf("echo 42 | fejkdata -n 3 = %d, %q, %q, want 42 on three lines", code, out, errb) + } +} + +func TestRunRendersAnyJSONValue(t *testing.T) { + for stdin, want := range map[string]string{"42": "42", "42\n": "42\n", "true\n": "true\n", " 7 ": " 7 ", "null\n": "", " null ": ""} { + if code, out, errb := runOut(stdin); code != 0 || out != want { + t.Errorf("run(%q) = %d, %q, %q, want %q", stdin, code, out, errb, want) + } + } +} + +func TestRunUnknownFlagIsNamedByRune(t *testing.T) { + code, _, errb := runOut("{/sv_SE.word}", "-ä") + if code != 2 || !strings.Contains(errb, "unknown flag -ä") { + t.Errorf("run(-ä) = %d, %q, want the flag named whole", code, errb) + } +} + +func TestRunEmptyDataPathIsNamed(t *testing.T) { + code, _, errb := runOut("{/sv_SE.word}", "--data-path=") + if code != 1 || !strings.Contains(errb, "empty") { + t.Errorf("run(--data-path=) = %d, %q, want the empty path named", code, errb) + } +} + +func recordDir(t *testing.T) string { + t.Helper() + dir := t.TempDir() + content := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"]}` + if err := os.WriteFile(filepath.Join(dir, "users.json"), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + return dir +} + +func TestRunRecordJSON(t *testing.T) { + code, out, errb := runOut("{/users}", "--seed", "1", "--format", "json", "--repeat", "2", "--data-path", recordDir(t)) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + var arr []map[string]string + if err := json.Unmarshal([]byte(out), &arr); err != nil { + t.Fatalf("json output is not one JSON array: %v\n%q", err, out) + } + if len(arr) != 2 || len(arr[0]) != 2 || arr[0]["first"] == "" || arr[0]["last"] == "" { + t.Fatalf("json output = %q, want an array of two records with first and last columns", out) + } +} + +func TestRunRecordNDJSON(t *testing.T) { + code, out, errb := runOut("{/users}", "--seed", "1", "--format", "ndjson", "--repeat", "2", "--data-path", recordDir(t)) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(lines) != 2 { + t.Fatalf("ndjson output = %d lines %q, want one object per line", len(lines), out) + } + for _, line := range lines { + var m map[string]string + if err := json.Unmarshal([]byte(line), &m); err != nil || len(m) != 2 { + t.Fatalf("ndjson line %q is not one object: %v", line, err) + } + } +} + +func TestRunRecordCSVRoundTrips(t *testing.T) { + code, out, errb := runOut("{/users}\r\n", "--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t)) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(lines) != 4 { + t.Fatalf("csv output = %d lines %q, want a header and 3 rows", len(lines), out) + } + if lines[0] != "first,last" { + t.Fatalf("csv header = %q, want first,last", lines[0]) + } + r, err := csv.NewReader(strings.NewReader(out)).ReadAll() + if err != nil { + t.Fatalf("csv output is not valid CSV: %v", err) + } + if len(r) != 4 || len(r[0]) != 2 { + t.Fatalf("csv parsed to %v, want 4 records of 2 fields", r) + } +} + +func TestRunRecordSQL(t *testing.T) { + code, out, errb := runOut("{/users}", "--seed", "1", "--format", "sql", "--repeat", "2", "--table", "people", "--data-path", recordDir(t)) + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(lines) != 2 || !strings.HasPrefix(lines[0], `INSERT INTO "people" ("first", "last") VALUES (`) { + t.Fatalf("sql output = %q, want two INSERT INTO people statements", out) + } +} + +func TestRunRecordDefaultTable(t *testing.T) { + code, out, errb := runOut("{/users}", "--seed", "1", "--format", "sql", "--data-path", recordDir(t)) + if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) { + t.Fatalf("default table = %d, %q, want the reference's last segment; stderr %q", code, out, errb) + } +} + +func TestRunRecordOfAQuotedLoneReference(t *testing.T) { + code, out, errb := runOut(`"{/users}"`, "--seed", "1", "--format", "sql", "--data-path", recordDir(t)) + if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) { + t.Fatalf("a JSON string holding {/users} = %d, %q, %q; want the record users", code, out, errb) + } + code, out, errb = runOut("{/users} ", "--format", "csv", "--data-path", recordDir(t)) + if code != 2 || out != "" || !strings.Contains(errb, "whitespace around {/users}") { + t.Errorf("{/users} and a space = %d, %q, %q; want misuse naming the whitespace", code, out, errb) + } +} + +func TestRunRecordInlineTemplate(t *testing.T) { + code, out, errb := runOut(`{"format":"{x}","x":["a","b"]}`, "--seed", "1", "--format", "json") + if code != 0 { + t.Fatalf("inline record = %d, stderr=%q", code, errb) + } + var arr []map[string]string + if err := json.Unmarshal([]byte(out), &arr); err != nil || len(arr) != 1 || (arr[0]["x"] != "a" && arr[0]["x"] != "b") { + t.Fatalf("inline record json = %q, want one record with a column x (err %v)", out, err) + } +} + +func TestRunRecordMisuse(t *testing.T) { + for _, c := range []struct { + args []string + want string + }{ + {[]string{"--format", "yaml"}, "--format takes csv, json, ndjson, sql or text"}, + {[]string{"--format", "sql", "--table", ""}, "--table names the INSERT target"}, + {[]string{"--format", "csv", "--separator", ";"}, `--separator joins text renders; --format csv always separates fields with ","`}, + {[]string{"--format", "json", "--separator", ","}, "--separator joins text renders"}, + {[]string{"--format", "ndjson", "--separator", ","}, "--separator joins text renders"}, + {[]string{"--format", "sql", "--separator", ","}, "--separator joins text renders"}, + } { + code, out, errb := runOut("{/users}", c.args...) + if code != 2 || out != "" || !strings.Contains(errb, c.want) { + t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want) + } + } +} + +func TestRunRecordOnABareValueIsRuntimeError(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "word.json"), []byte(`["a","b"]`), 0o644); err != nil { + t.Fatal(err) + } + code, _, errb := runOut("{/word}", "--format", "json", "--data-path", dir) + if code != 1 { + t.Fatalf("record on a choice = %d, want exit 1 (runtime error), stderr %q", code, errb) + } +} + +func TestRunRecordRejectsATopLevelRepeat(t *testing.T) { + dir := t.TempDir() + content := `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}` + if err := os.WriteFile(filepath.Join(dir, "rep.json"), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + code, _, errb := runOut("{/rep}", "--format", "json", "--data-path", dir) + if code != 1 || !strings.Contains(errb, "carries repeat 3") { + t.Errorf("record on a repeating path = %d, %q; want exit 1 naming the repeat", code, errb) + } + code, _, errb = runOut(content, "--format", "json") + if code != 2 || !strings.Contains(errb, "carries repeat 3") { + t.Errorf("record on a repeating inline template = %d, %q; want exit 2 naming the repeat", code, errb) + } + if code, out, _ := runOut("{/rep}", "--seed", "1", "--data-path", dir); code != 0 || strings.TrimSpace(out) != "x-|x-|x-" { + t.Errorf("text view = %d, %q; want the repeat still composed", code, out) + } +} + +func TestRunNumbersFollowTheShell(t *testing.T) { + _, want, _ := runOut("{/sv_SE.word}", "--seed", "7", "--repeat", "3") + code, got, errb := runOut("{/sv_SE.word}", "--seed", "007", "--repeat", "+3") + if code != 0 || got != want { + t.Errorf("run(--seed 007 --repeat +3) = %d, %q, stderr %q; want the same as --seed 7 --repeat 3 %q", code, got, errb, want) + } +} + +// TestRunRecordOfAFieldlessPath pins the way out of asking a category of one value +// for columns: the record to write, and the quoting a shell needs to take it. The +// template is well-formed and only the data cannot serve it, so the code is 1. +func TestRunRecordOfAFieldlessPath(t *testing.T) { + code, out, errb := runOut("{/sv_SE.personnummer}", "--format", "csv") + if code != 1 { + t.Fatalf("run(--format csv) of {/sv_SE.personnummer} = %d, want 1", code) + } + for _, want := range []string{`{"format":"","personnummer":"{/sv_SE.personnummer}"}`, "single quotes"} { + if !strings.Contains(errb, want) { + t.Errorf("stderr = %q, want it to name %s", errb, want) + } + } + if out != "" { + t.Errorf("stdout = %q, want nothing", out) + } +} + +func TestADataPathCategoryFailingAtFirstReachExits1(t *testing.T) { + dir := t.TempDir() + for name, data := range map[string]string{ + ".fejkdata.json": `{"index": {"bad": {"paths": [""]}}}`, + "bad.json": `"{/nope}"`, + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte(data), 0o644); err != nil { + t.Fatal(err) + } + } + for _, args := range [][]string{{"-d", dir}, {"-d", dir, "--format", "json"}} { + if code, _, errb := runOut("{/bad}", args...); code != 1 || !strings.Contains(errb, "{/nope}") { + t.Errorf("%v on a category failing to load = %d, %q; want exit 1 naming the reference", args, code, errb) + } + } +} diff --git a/cmd/fejkdata/lone_test.go b/cli/lone_test.go similarity index 98% rename from cmd/fejkdata/lone_test.go rename to cli/lone_test.go index b08a0aa..d5282e2 100644 --- a/cmd/fejkdata/lone_test.go +++ b/cli/lone_test.go @@ -1,4 +1,4 @@ -package main +package cli import ( "testing" diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index 7f6162e..1d4dac2 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -1,846 +1,32 @@ package main import ( - "bytes" - "encoding/csv" - "encoding/json" - "errors" + "io" "os" - "path/filepath" - "regexp" - "strings" "testing" + + "github.com/larvit/fejkdata/cli" + "github.com/larvit/fejkdata/data" ) -// svSE, enUS and misc are copies of a shipped module's files without its manifest, so a test -// layers its categories unprefixed with -d. -var svSE, enUS, misc string - -func TestMain(m *testing.M) { - tmp, err := os.MkdirTemp("", "fejkdata-cli-test") +func TestMainRendersTheShippedData(t *testing.T) { + stdin, feed, err := os.Pipe() if err != nil { - panic(err) - } - svSE, enUS, misc = bareCopy(tmp, "sv_SE"), bareCopy(tmp, "en_US"), bareCopy(tmp, "misc") - code := m.Run() - os.RemoveAll(tmp) - os.Exit(code) -} - -// bareCopy copies the category and rows files of the shipped module in data/ into a -// folder under tmp, and returns that folder. -func bareCopy(tmp, module string) string { - src, dst := filepath.Join("..", "..", "data", module), filepath.Join(tmp, module) - if err := os.Mkdir(dst, 0o755); err != nil { - panic(err) - } - for _, pattern := range []string{"*.json", "*.tsv"} { - files, err := filepath.Glob(filepath.Join(src, pattern)) - if err != nil { - panic(err) - } - for _, f := range files { - if strings.HasPrefix(filepath.Base(f), ".") { - continue - } - b, err := os.ReadFile(f) - if err == nil { - err = os.WriteFile(filepath.Join(dst, filepath.Base(f)), b, 0o644) - } - if err != nil { - panic(err) - } - } - } - return dst -} - -func runOut(stdin string, args ...string) (int, string, string) { - var out, errb bytes.Buffer - code := run(args, strings.NewReader(stdin), &out, &errb) - return code, out.String(), errb.String() -} - -// runTerminal runs as from a shell with nothing piped in. -func runTerminal(args ...string) (int, string, string) { - var out, errb bytes.Buffer - code := run(args, nil, &out, &errb) - return code, out.String(), errb.String() -} - -func TestRunOutputsValue(t *testing.T) { - code, out, errb := runOut("{/person}", "--data-path", svSE) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - if out == "" || strings.Contains(out, "\n") { - t.Fatalf("output = %q, stderr=%q; want one value and nothing added", out, errb) - } -} - -func TestRunDotPath(t *testing.T) { - code, full, _ := runOut("{/person}", "--seed", "7", "--data-path", svSE) - if code != 0 { - t.Fatalf("person run = %d", code) - } - code, last, errb := runOut("{/person.last}", "--seed", "7", "--data-path", svSE) - if code != 0 { - t.Fatalf("person.last run = %d, stderr=%q", code, errb) - } - if strings.TrimSpace(last) == "" { - t.Fatal("empty person.last output") - } - if last == full { - t.Errorf("person.last %q should differ from person %q", last, full) - } -} - -func TestRunSeedSpellings(t *testing.T) { - _, want, _ := runOut("{/address}", "--seed", "42", "--data-path", svSE) - for _, args := range [][]string{ - {"--seed=42", "--data-path", svSE}, - {"-s", "42", "-d", svSE}, - {"--data-path", svSE, "--seed", "42"}, - } { - code, got, errb := runOut("{/address}", args...) - if code != 0 { - t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb) - } - if got != want { - t.Errorf("run(%v) = %q, want %q", args, got, want) - } - } -} - -func TestRunShortFlagValues(t *testing.T) { - _, want, _ := runOut("{/address}", "--seed", "42", "--data-path", svSE) - for _, args := range [][]string{ - {"-s42", "-d", svSE}, - {"-s", "42", "-d" + svSE}, - {"-d", svSE, "-s42"}, - } { - code, got, errb := runOut("{/address}", args...) - if code != 0 { - t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb) - } - if got != want { - t.Errorf("run(%v) = %q, want %q", args, got, want) - } - } - _, three, _ := runOut("{/word}\n", "-s", "1", "-n3", "-d", svSE) - if lines := strings.Split(strings.TrimRight(three, "\n"), "\n"); len(lines) != 3 { - t.Errorf("-n3 gave %d lines: %q", len(lines), three) - } - for _, c := range []struct { - args []string - want string - }{ - {[]string{"-s=42", "-d", svSE}, "-s42"}, - {[]string{"-s=42", "-d", svSE}, "--seed=42"}, - {[]string{"-d=" + svSE}, "--data-path="}, - {[]string{"-nd", "3", "-d", svSE}, `--repeat needs an integer in 0..1048576, got "d"`}, - {[]string{"--seed=", "-d", svSE}, `--seed needs an unsigned integer, got ""`}, - } { - code, out, errb := runOut("{/word}", c.args...) - if code != 2 || out != "" { - t.Errorf("run(%v) = %d, stdout %q, want misuse", c.args, code, out) - } - if !strings.Contains(errb, c.want) { - t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want) - } - } - code, out, _ := runOut("", "-hd", svSE) - if code != 0 || !strings.Contains(out, "Usage") { - t.Errorf("-hd (bundled help) = %d, %q, want usage", code, out) - } -} - -func TestRunDoubleDashEndsFlags(t *testing.T) { - code, out, errb := runOut("{/person}", "--data-path", svSE, "--") - if code != 0 || strings.TrimSpace(out) == "" { - t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb) - } - code, _, errb = runOut("{/person}", "--data-path", svSE, "--", "--list") - if code != 2 || !strings.Contains(errb, "stdin") { - t.Errorf("after --, --list should be an argument: code %d, stderr %q", code, errb) - } -} - -func TestRunSingleDashLongIsRejected(t *testing.T) { - for _, c := range []struct { - args []string - want string - }{ - {[]string{"-seed", "42", "-d", svSE}, "use --seed"}, - {[]string{"-seed=42", "-d", svSE}, "use --seed"}, - {[]string{"-data-path", svSE}, "use --data-path"}, - {[]string{"-list", "-d", svSE}, "use --list"}, - {[]string{"--nope", "-d", svSE}, "unknown flag --nope"}, - {[]string{"-x", "-d", svSE}, "unknown flag -x"}, - } { - code, _, errb := runOut("{/person}", c.args...) - if code != 2 { - t.Errorf("run(%v) = %d, want 2", c.args, code) - } - if !strings.Contains(errb, c.want) { - t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want) - } - } -} - -func TestRunFlagValues(t *testing.T) { - for _, c := range []struct { - args []string - want string - }{ - {[]string{"--seed", "-d", svSE}, `--seed needs an unsigned integer, got "-d"`}, - {[]string{"-d", svSE, "--seed"}, "--seed needs a value"}, - {[]string{"--seed", "x", "-d", svSE}, "--seed"}, - {[]string{"--list=1", "-d", svSE}, "--list takes no value"}, - {[]string{"--repeat", "-1", "-d", svSE}, "--repeat"}, - {[]string{"-n", "-1", "-d", svSE}, "--repeat"}, - } { - code, _, errb := runOut("{/person}", c.args...) - if code != 2 { - t.Errorf("run(%v) = %d, want 2", c.args, code) - } - if !strings.Contains(errb, c.want) { - t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want) - } - } -} - -func TestRunRepeat(t *testing.T) { - code, out, errb := runOut("{/word}\n", "--seed", "1", "--repeat", "3", "--data-path", svSE) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - lines := strings.Split(strings.TrimRight(out, "\n"), "\n") - if len(lines) != 3 { - t.Errorf("repeat=3 gave %d lines: %q", len(lines), out) - } - _, short, _ := runOut("{/word}\n", "--seed", "1", "-n", "3", "-d", svSE) - if short != out { - t.Errorf("-n 3 = %q, want the same as --repeat 3 %q", short, out) - } -} - -func TestRunSeparator(t *testing.T) { - code, out, errb := runOut("{/word}", "--repeat", "3", "--separator", ",", "--data-path", svSE) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - if n := strings.Count(out, "\n"); n != 0 { - t.Errorf("want no newline, got %d: %q", n, out) - } - if !strings.Contains(out, ",") { - t.Errorf("values should be comma-joined: %q", out) - } -} - -func TestRunRepeatAdvancesRNG(t *testing.T) { - code, out, errb := runOut("{/person}\n", "--seed", "1", "--repeat", "5", "--data-path", svSE) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - uniq := map[string]bool{} - for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") { - uniq[l] = true - } - if len(uniq) < 2 { - t.Errorf("repeat should vary output, all identical: %q", out) - } -} - -func TestRunMisuse(t *testing.T) { - for _, c := range []struct { - stdin string - args []string - }{ - {"{/person}", []string{"-d", svSE, "word"}}, - {"", []string{"-d", svSE, "--list", "person"}}, - {"{/sv_SE.person}", []string{"--no-shipped-data"}}, - } { - code, out, errb := runOut(c.stdin, c.args...) - if code != 2 { - t.Errorf("run(%q, %v) = %d, want 2", c.stdin, c.args, code) - } - if !strings.Contains(errb, "try 'fejkdata --help'") { - t.Errorf("run(%q, %v) stderr = %q, want a pointer to --help", c.stdin, c.args, errb) - } - if out != "" { - t.Errorf("run(%q, %v) stdout = %q, want nothing", c.stdin, c.args, out) - } - } -} - -func TestRunTakesNoOperand(t *testing.T) { - file := filepath.Join(t.TempDir(), "users.tmpl") - if err := os.WriteFile(file, []byte("{/sv_SE.person}"), 0o644); err != nil { t.Fatal(err) } - for _, c := range []struct { - args []string - want string - }{ - {[]string{"sv_SE.person"}, "echo '{/sv_SE.person}' | fejkdata"}, - {[]string{"/en_US.address"}, "echo '{/en_US.address}' | fejkdata"}, - {[]string{"{/en_US.person}"}, "echo '{/en_US.person}' | fejkdata"}, - {[]string{"name: {/sv_SE.person.last}"}, "echo 'name: {/sv_SE.person.last}' | fejkdata"}, - {[]string{"geo.US.locality[O'Fallon].name"}, `echo '{/geo.US.locality[O'\''Fallon].name}' | fejkdata`}, - {[]string{"en_US.address"}, "echo '{/en_US.address}' | fejkdata"}, - {[]string{`C:\temp {/sv_SE.person}`}, `printf '%s' 'C:\temp {/sv_SE.person}' | fejkdata`}, - {[]string{"-"}, "drop it"}, - {[]string{file}, "fejkdata < " + file}, - } { - code, out, errb := runOut("", c.args...) - if code != 2 || out != "" || !strings.Contains(errb, "unexpected argument") || !strings.Contains(errb, c.want) { - t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want) - } - } -} - -type failingReader struct{} - -func (failingReader) Read([]byte) (int, error) { return 0, errors.New("bad file descriptor") } - -func TestRunUnreadableStdinIsMisuse(t *testing.T) { - var out, errb bytes.Buffer - if code := run(nil, failingReader{}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "cannot be read") { - t.Errorf("run with an unreadable stdin = %d, %q; want misuse naming it", code, errb.String()) - } -} - -func TestRunTerminalStdinIsMisuse(t *testing.T) { - code, out, errb := runTerminal("--seed", "1") - if code != 2 || out != "" || !strings.Contains(errb, "| fejkdata") { - t.Errorf("run with nothing piped = %d, %q, %q; want misuse showing a template piped to fejkdata", code, out, errb) - } - for _, args := range [][]string{{"--list"}, {"--help"}, {"--version"}} { - if code, out, errb := runTerminal(args...); code != 0 || out == "" { - t.Errorf("run(%v) with nothing piped = %d, %q, %q; want it to run, reading no stdin", args, code, out, errb) - } - } -} - -func TestRunRendersExactlyWhatIsSent(t *testing.T) { - for stdin, want := range map[string]string{ - "hihi": "hihi", - "hihi\n": "hihi\n", - "hihi\r\n": "hihi\r\n", - "hihi\n\n": "hihi\n\n", - "{{x}}\n": "{x}\n", - "\"a\"\n": "a", - " \t\"a\"\r\n\n": "a", - "{/misc.territory[SE].alpha2}": "SE", - "{/misc.territory[SE].alpha2}\n": "SE\n", - } { - code, out, errb := runOut(stdin, "--seed", "1") - if code != 0 || out != want { - t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want) - } - } - if code, out, errb := runOut("{\"format\":\"{x}\\n\",\"x\":[\"a\",\"b\"]}\n"); code != 0 || (out != "a\n" && out != "b\n") { - t.Errorf("a JSON template's format = %d, %q, %q; want its own newline only", code, out, errb) - } - if code, out, errb := runOut("{/sv_SE.word}\n", "--seed", "1", "-n", "3", "--separator", ","); code != 0 || strings.Count(out, "\n,") != 2 || !strings.HasSuffix(out, "\n") { - t.Errorf("echo's newline and a separator = %d, %q, %q; want each render's newline, then the separator", code, out, errb) - } -} - -func TestRunRendersBlankStdin(t *testing.T) { - for _, stdin := range []string{"", "\n", " \r\n\t"} { - if code, out, errb := runOut(stdin); code != 0 || out != stdin { - t.Errorf("run(%q) = %d, %q, %q; want the blank template rendered as it is", stdin, code, out, errb) - } - } -} - -func TestRunTextPrintsAsWritten(t *testing.T) { - for _, stdin := range []string{"sv_SE.person", "[Skåne län]", "x[1]y", `say "hi"`} { - code, out, errb := runOut(stdin) - if code != 0 || out != stdin { - t.Errorf("run(%q) = %d, %q, %q; want the text as written", stdin, code, out, errb) - } - } -} - -func TestRunMultipleDataPaths(t *testing.T) { - code, out, errb := runOut("{/person}", "-d", enUS, "--data-path", svSE) - if code != 0 { - t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb) - } - if strings.TrimSpace(out) == "" { - t.Fatal("empty output for multi-dir run") - } -} - -func TestRunUnknownCategoryFails(t *testing.T) { - code, _, errb := runOut("{/nope}", "--data-path", svSE, "--format", "csv") - if code != 1 { - t.Fatalf("run = %d, want 1", code) - } - if !strings.Contains(errb, "nope") { - t.Errorf("stderr %q should name the unknown category", errb) - } -} - -func TestRunList(t *testing.T) { - code, out, errb := runOut("", "--data-path", svSE, "--list") - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - for _, want := range []string{"person", "person.last", "address"} { - if !strings.Contains(out, want) { - t.Errorf("list output missing %q:\n%s", want, out) - } - } -} - -func TestRunHelpOnStdout(t *testing.T) { - for _, h := range []string{"-h", "--help"} { - code, out, errb := runOut("", h) - if code != 0 { - t.Errorf("%s = %d, want 0", h, code) - } - if !strings.Contains(out, "Usage") || errb != "" { - t.Errorf("%s: stdout = %q, stderr = %q, want usage on stdout only", h, out, errb) - } - } -} - -func TestRunVersion(t *testing.T) { - code, out, errb := runOut("", "--version") - if code != 0 { - t.Fatalf("--version = %d, stderr=%q", code, errb) - } - version, ok := strings.CutPrefix(out, "fejkdata ") - if !ok || strings.TrimSpace(version) == "" { - t.Errorf("--version = %q, want the command name and a version on stdout", out) - } -} - -func TestRunMissingDirFails(t *testing.T) { - code, _, errb := runOut("{/person}", "--data-path", "../../data/nope") - if code != 1 { - t.Fatalf("run = %d, want 1", code) - } - if errb == "" { - t.Error("want an error message on stderr") - } -} - -func TestRunShippedDataByDefault(t *testing.T) { - code, out, errb := runOut("{/sv_SE.person}", "--seed", "1") - if code != 0 || strings.TrimSpace(out) == "" { - t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb) - } - code, list, _ := runOut("", "--list") - if code != 0 || !strings.Contains(list, "en_US.person\n") || !strings.Contains(list, "misc.uuid\n") { - t.Errorf("--list without --data-path = %d, %q", code, list) - } - code, _, errb = runOut("{/person}") - if code != 2 || !strings.Contains(errb, "person") { - t.Errorf("a category outside the shipped tree: code %d, stderr %q", code, errb) - } -} - -func TestRunSelectsATableRow(t *testing.T) { - dir := t.TempDir() - for name, content := range map[string]string{ - "region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`, - "region.tsv": "code\tname\n01\tStockholms län\n12\tSkåne län\n", - } { - if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { - t.Fatal(err) - } - } - if code, out, errb := runOut("{/region[12]}", "--no-shipped-data", "-d", dir); code != 0 || out != "Skåne län" { - t.Fatalf("{/region[12]} = %d, %q, stderr=%q", code, out, errb) - } - if code, out, _ := runOut("{/region[Skåne län].code}", "--no-shipped-data", "-d", dir); code != 0 || out != "12" { - t.Fatalf("{/region[Skåne län].code} = %d, %q", code, out) - } - if code, out, _ := runOut("{/region[12]}", "--no-shipped-data", "-d", dir, "--format", "sql"); code != 0 || out != `INSERT INTO "region" ("code", "name") VALUES ('12', 'Skåne län');`+"\n" { - t.Fatalf("--format sql {/region[12]} = %d, %q, want the table named without its selector", code, out) - } - if code, _, errb := runOut("{/region[99]}", "--no-shipped-data", "-d", dir); code != 2 || !strings.Contains(errb, `"99"`) { - t.Fatalf("{/region[99]} = %d, stderr=%q, want misuse naming the row", code, errb) - } - if code, out, _ := runOut("{/misc.territory[SE]}", "--seed", "1", "--format", "csv"); code != 0 || !strings.HasPrefix(out, "alpha2,") || !strings.Contains(out, "\nSE,SWE,") { - t.Fatalf("--format csv {/misc.territory[SE]} = %d, %q", code, out) - } -} - -func TestUsageReferencesResolve(t *testing.T) { - for _, token := range regexp.MustCompile(`\{/[^}]+\}`).FindAllString(usage, -1) { - code, out, errb := runOut(token, "--seed", "1") - if code != 0 || strings.TrimSpace(out) == "" { - t.Errorf("usage advertises %s: run = %d, %q, stderr %q", token, code, out, errb) - } - } -} - -func TestRunOperandMisuseBeforeLoad(t *testing.T) { - code, _, errb := runOut("", "--no-shipped-data", "x") - if code != 2 || !strings.Contains(errb, "unexpected argument") || strings.Contains(errb, "--data-path") { - t.Fatalf("an operand with no data = %d, %q; want the operand refused before any load", code, errb) - } -} - -func TestRunInlineTemplate(t *testing.T) { - code, out, errb := runOut("name: {/sv_SE.person.last}", "--seed", "1") - if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") { - t.Fatalf("inline format string = %d, %q, stderr %q", code, out, errb) - } - code, out, errb = runOut(`{"format":"name: {x}","x":["bosse","lina"]}`, "--seed", "1") - if code != 0 || (out != "name: bosse" && out != "name: lina") { - t.Fatalf("inline JSON template = %d, %q, want one name, stderr %q", code, out, errb) - } - code, out, errb = runOut(`"name: {/sv_SE.person.last}"`, "--seed", "1") - if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") { - t.Fatalf("inline JSON string = %d, %q, stderr %q", code, out, errb) - } - code, out, errb = runOut("{digits(1)}\n", "--seed", "1", "-n", "2") - if code != 0 || len(strings.Split(strings.TrimRight(out, "\n"), "\n")) != 2 { - t.Fatalf("inline template with --repeat = %d, %q, stderr %q", code, out, errb) - } - code, out, errb = runOut("{/sv_SE.person} hihi", "--seed", "1") - if code != 0 || !strings.HasSuffix(out, " hihi") || strings.Contains(out, "{") { - t.Fatalf("a reference and text = %d, %q, stderr %q", code, out, errb) - } -} - -func TestRunFormatTakesOnlyALoneReferenceAsARecord(t *testing.T) { - for _, stdin := range []string{"{//sv_SE.person}", "{/sv_SE.person|/misc.uuid}", "{/sv_SE.person as p}", "{/}", "{/.sv_SE.person}"} { - if code, out, errb := runOut(stdin, "--format", "json"); code != 2 || out != "" { - t.Errorf("run(%q, --format json) = %d, %q, %q; want misuse", stdin, code, out, errb) - } - } - if code, out, errb := runOut(`{"format":"x"}`); code != 0 || out != "x" { - t.Errorf(`run({"format":"x"}) = %d, %q, %q; want x`, code, out, errb) - } -} - -func TestRunTemplateMisuse(t *testing.T) { - for stdin, want := range map[string]string{ - "{bad": "unterminated", - "name: {/no.such.path}": "no entry", - "a } b": "}}", - "{//sv_SE.person}": "write {/sv_SE.person}", - } { - code, out, errb := runOut(stdin, "--seed", "1") - if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) { - t.Errorf("run(%q) = %d, %q, %q; want misuse naming %q and --help", stdin, code, out, errb, want) - } - if strings.Contains(errb, "fejkdata: fejkdata:") { - t.Errorf("run(%q) doubled the program prefix: %q", stdin, errb) - } - } -} - -func TestRunNoShippedData(t *testing.T) { - code, list, errb := runOut("", "--no-shipped-data", "-d", misc, "--list") - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - if strings.Contains(list, "sv_SE") || !strings.Contains(list, "uuid\n") { - t.Errorf("--no-shipped-data --list = %q, want only the given dir", list) - } - code, out, _ := runOut("{/uuid}", "--no-shipped-data", "-d", misc, "-s", "3") - if code != 0 || strings.TrimSpace(out) == "" { - t.Errorf("run = %d, out=%q", code, out) - } - if code, out, errb := runOut("{digits(3)}", "--no-shipped-data"); code != 0 || len(out) != 3 { - t.Errorf("--no-shipped-data alone = %d, %q, %q, want a template reading no data rendered", code, out, errb) - } - if code, _, errb := runOut("{/sv_SE.person}", "--no-shipped-data"); code != 2 || !strings.HasSuffix(errb, "no data is loaded\ntry 'fejkdata --help'\n") { - t.Errorf("--no-shipped-data alone = %d, %q, want a reference to nothing refused", code, errb) - } -} - -func TestRunRepeatOfZeroWritesNoRecord(t *testing.T) { - dir := recordDir(t) - for _, c := range []struct { - args []string - want string - }{ - {nil, ""}, - {[]string{"--format", "json"}, "[]\n"}, - {[]string{"--format", "ndjson"}, ""}, - {[]string{"--format", "csv"}, "first,last\n"}, - {[]string{"--format", "sql"}, ""}, - } { - if code, out, errb := runOut("{/users}", append(c.args, "-n", "0", "-d", dir)...); code != 0 || out != c.want { - t.Errorf("run(-n 0 %v) = %d, %q, %q, want %q", c.args, code, out, errb, c.want) - } - } - if code, _, _ := runOut("{/nope}", "-n", "0", "--format", "json", "-d", dir); code != 1 { - t.Errorf("run(-n 0) of a reference to nothing = %d, want 1", code) - } -} - -func TestRunRepeatIsBounded(t *testing.T) { - for _, args := range [][]string{ - {"--repeat", "9223372036854775807"}, - {"--repeat", "1048577"}, - {"-n", "99999999999"}, - } { - code, out, errb := runOut("{/sv_SE.word}", args...) - if code != 2 || out != "" || !strings.Contains(errb, "0..1048576") { - t.Errorf("run(%v) = %d, %q, %q, want misuse naming the range", args, code, out, errb) - } - } - dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "x.json"), []byte(`"x"`), 0o644); err != nil { - t.Fatal(err) - } - code, out, errb := runOut("{/x}", "--no-shipped-data", "-d", dir, "--repeat", "1048576", "--separator", "") - if code != 0 || len(out) != 1048576 { - t.Errorf("repeat at the cap = %d, %d bytes, stderr %q; want every render streamed", code, len(out), errb) - } -} - -func TestRunIgnoresAFlagWithNoEffect(t *testing.T) { - dir := recordDir(t) - for _, c := range []struct { - stdin string - base, ignored []string - }{ - {"", []string{"--list"}, []string{"-n", "3"}}, - {"", []string{"--list"}, []string{"--separator", ","}}, - {"", []string{"--list"}, []string{"--format", "json"}}, - {"", []string{"--list"}, []string{"--table", "t"}}, - {"{/users}", nil, []string{"--table", "t"}}, - {"{/users}", []string{"--format", "json"}, []string{"--table", "t"}}, - } { - args := append([]string{"--seed", "1", "--data-path", dir}, c.base...) - _, want, _ := runOut(c.stdin, args...) - code, got, errb := runOut(c.stdin, append(args, c.ignored...)...) - if code != 0 || want == "" || got != want { - t.Errorf("run(%v + %v) = %d, %q, %q; want what %v alone prints, %q", c.base, c.ignored, code, got, errb, c.base, want) - } - } -} - -func TestRunReadsABareNumberAsAFormatString(t *testing.T) { - if code, out, errb := runOut("42\n", "-n", "3"); code != 0 || out != "42\n42\n42\n" { - t.Errorf("echo 42 | fejkdata -n 3 = %d, %q, %q, want 42 on three lines", code, out, errb) - } -} - -func TestRunRendersAnyJSONValue(t *testing.T) { - for stdin, want := range map[string]string{"42": "42", "42\n": "42\n", "true\n": "true\n", " 7 ": " 7 ", "null\n": "", " null ": ""} { - if code, out, errb := runOut(stdin); code != 0 || out != want { - t.Errorf("run(%q) = %d, %q, %q, want %q", stdin, code, out, errb, want) - } - } -} - -func TestRunUnknownFlagIsNamedByRune(t *testing.T) { - code, _, errb := runOut("{/sv_SE.word}", "-ä") - if code != 2 || !strings.Contains(errb, "unknown flag -ä") { - t.Errorf("run(-ä) = %d, %q, want the flag named whole", code, errb) - } -} - -func TestRunEmptyDataPathIsNamed(t *testing.T) { - code, _, errb := runOut("{/sv_SE.word}", "--data-path=") - if code != 1 || !strings.Contains(errb, "empty") { - t.Errorf("run(--data-path=) = %d, %q, want the empty path named", code, errb) - } -} - -func recordDir(t *testing.T) string { - t.Helper() - dir := t.TempDir() - content := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"]}` - if err := os.WriteFile(filepath.Join(dir, "users.json"), []byte(content), 0o644); err != nil { - t.Fatal(err) - } - return dir -} - -func TestRunRecordJSON(t *testing.T) { - code, out, errb := runOut("{/users}", "--seed", "1", "--format", "json", "--repeat", "2", "--data-path", recordDir(t)) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - var arr []map[string]string - if err := json.Unmarshal([]byte(out), &arr); err != nil { - t.Fatalf("json output is not one JSON array: %v\n%q", err, out) - } - if len(arr) != 2 || len(arr[0]) != 2 || arr[0]["first"] == "" || arr[0]["last"] == "" { - t.Fatalf("json output = %q, want an array of two records with first and last columns", out) - } -} - -func TestRunRecordNDJSON(t *testing.T) { - code, out, errb := runOut("{/users}", "--seed", "1", "--format", "ndjson", "--repeat", "2", "--data-path", recordDir(t)) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - lines := strings.Split(strings.TrimRight(out, "\n"), "\n") - if len(lines) != 2 { - t.Fatalf("ndjson output = %d lines %q, want one object per line", len(lines), out) - } - for _, line := range lines { - var m map[string]string - if err := json.Unmarshal([]byte(line), &m); err != nil || len(m) != 2 { - t.Fatalf("ndjson line %q is not one object: %v", line, err) - } - } -} - -func TestRunRecordCSVRoundTrips(t *testing.T) { - code, out, errb := runOut("{/users}\r\n", "--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t)) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - lines := strings.Split(strings.TrimRight(out, "\n"), "\n") - if len(lines) != 4 { - t.Fatalf("csv output = %d lines %q, want a header and 3 rows", len(lines), out) - } - if lines[0] != "first,last" { - t.Fatalf("csv header = %q, want first,last", lines[0]) - } - r, err := csv.NewReader(strings.NewReader(out)).ReadAll() + drain, stdout, err := os.Pipe() if err != nil { - t.Fatalf("csv output is not valid CSV: %v", err) - } - if len(r) != 4 || len(r[0]) != 2 { - t.Fatalf("csv parsed to %v, want 4 records of 2 fields", r) - } -} - -func TestRunRecordSQL(t *testing.T) { - code, out, errb := runOut("{/users}", "--seed", "1", "--format", "sql", "--repeat", "2", "--table", "people", "--data-path", recordDir(t)) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - lines := strings.Split(strings.TrimRight(out, "\n"), "\n") - if len(lines) != 2 || !strings.HasPrefix(lines[0], `INSERT INTO "people" ("first", "last") VALUES (`) { - t.Fatalf("sql output = %q, want two INSERT INTO people statements", out) - } -} - -func TestRunRecordDefaultTable(t *testing.T) { - code, out, errb := runOut("{/users}", "--seed", "1", "--format", "sql", "--data-path", recordDir(t)) - if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) { - t.Fatalf("default table = %d, %q, want the reference's last segment; stderr %q", code, out, errb) - } -} - -func TestRunRecordOfAQuotedLoneReference(t *testing.T) { - code, out, errb := runOut(`"{/users}"`, "--seed", "1", "--format", "sql", "--data-path", recordDir(t)) - if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) { - t.Fatalf("a JSON string holding {/users} = %d, %q, %q; want the record users", code, out, errb) - } - code, out, errb = runOut("{/users} ", "--format", "csv", "--data-path", recordDir(t)) - if code != 2 || out != "" || !strings.Contains(errb, "whitespace around {/users}") { - t.Errorf("{/users} and a space = %d, %q, %q; want misuse naming the whitespace", code, out, errb) - } -} - -func TestRunRecordInlineTemplate(t *testing.T) { - code, out, errb := runOut(`{"format":"{x}","x":["a","b"]}`, "--seed", "1", "--format", "json") - if code != 0 { - t.Fatalf("inline record = %d, stderr=%q", code, errb) - } - var arr []map[string]string - if err := json.Unmarshal([]byte(out), &arr); err != nil || len(arr) != 1 || (arr[0]["x"] != "a" && arr[0]["x"] != "b") { - t.Fatalf("inline record json = %q, want one record with a column x (err %v)", out, err) - } -} - -func TestRunRecordMisuse(t *testing.T) { - for _, c := range []struct { - args []string - want string - }{ - {[]string{"--format", "yaml"}, "--format takes csv, json, ndjson, sql or text"}, - {[]string{"--format", "sql", "--table", ""}, "--table names the INSERT target"}, - {[]string{"--format", "csv", "--separator", ";"}, `--separator joins text renders; --format csv always separates fields with ","`}, - {[]string{"--format", "json", "--separator", ","}, "--separator joins text renders"}, - {[]string{"--format", "ndjson", "--separator", ","}, "--separator joins text renders"}, - {[]string{"--format", "sql", "--separator", ","}, "--separator joins text renders"}, - } { - code, out, errb := runOut("{/users}", c.args...) - if code != 2 || out != "" || !strings.Contains(errb, c.want) { - t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want) - } - } -} - -func TestRunRecordOnABareValueIsRuntimeError(t *testing.T) { - dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "word.json"), []byte(`["a","b"]`), 0o644); err != nil { t.Fatal(err) } - code, _, errb := runOut("{/word}", "--format", "json", "--data-path", dir) - if code != 1 { - t.Fatalf("record on a choice = %d, want exit 1 (runtime error), stderr %q", code, errb) - } -} - -func TestRunRecordRejectsATopLevelRepeat(t *testing.T) { - dir := t.TempDir() - content := `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}` - if err := os.WriteFile(filepath.Join(dir, "rep.json"), []byte(content), 0o644); err != nil { - t.Fatal(err) - } - code, _, errb := runOut("{/rep}", "--format", "json", "--data-path", dir) - if code != 1 || !strings.Contains(errb, "carries repeat 3") { - t.Errorf("record on a repeating path = %d, %q; want exit 1 naming the repeat", code, errb) - } - code, _, errb = runOut(content, "--format", "json") - if code != 2 || !strings.Contains(errb, "carries repeat 3") { - t.Errorf("record on a repeating inline template = %d, %q; want exit 2 naming the repeat", code, errb) - } - if code, out, _ := runOut("{/rep}", "--seed", "1", "--data-path", dir); code != 0 || strings.TrimSpace(out) != "x-|x-|x-" { - t.Errorf("text view = %d, %q; want the repeat still composed", code, out) - } -} - -func TestRunNumbersFollowTheShell(t *testing.T) { - _, want, _ := runOut("{/sv_SE.word}", "--seed", "7", "--repeat", "3") - code, got, errb := runOut("{/sv_SE.word}", "--seed", "007", "--repeat", "+3") - if code != 0 || got != want { - t.Errorf("run(--seed 007 --repeat +3) = %d, %q, stderr %q; want the same as --seed 7 --repeat 3 %q", code, got, errb, want) - } -} - -// TestRunRecordOfAFieldlessPath pins the way out of asking a category of one value -// for columns: the record to write, and the quoting a shell needs to take it. The -// template is well-formed and only the data cannot serve it, so the code is 1. -func TestRunRecordOfAFieldlessPath(t *testing.T) { - code, out, errb := runOut("{/sv_SE.personnummer}", "--format", "csv") - if code != 1 { - t.Fatalf("run(--format csv) of {/sv_SE.personnummer} = %d, want 1", code) - } - for _, want := range []string{`{"format":"","personnummer":"{/sv_SE.personnummer}"}`, "single quotes"} { - if !strings.Contains(errb, want) { - t.Errorf("stderr = %q, want it to name %s", errb, want) - } - } - if out != "" { - t.Errorf("stdout = %q, want nothing", out) - } -} - -func TestADataPathCategoryFailingAtFirstReachExits1(t *testing.T) { - dir := t.TempDir() - for name, data := range map[string]string{ - ".fejkdata.json": `{"index": {"bad": {"paths": [""]}}}`, - "bad.json": `"{/nope}"`, - } { - if err := os.WriteFile(filepath.Join(dir, name), []byte(data), 0o644); err != nil { - t.Fatal(err) - } - } - for _, args := range [][]string{{"-d", dir}, {"-d", dir, "--format", "json"}} { - if code, _, errb := runOut("{/bad}", args...); code != 1 || !strings.Contains(errb, "{/nope}") { - t.Errorf("%v on a category failing to load = %d, %q; want exit 1 naming the reference", args, code, errb) - } + args, oldIn, oldOut := os.Args, os.Stdin, os.Stdout + t.Cleanup(func() { os.Args, os.Stdin, os.Stdout = args, oldIn, oldOut }) + os.Args, os.Stdin, os.Stdout = []string{"fejkdata", "--seed", "1"}, stdin, stdout + io.WriteString(feed, "{/misc.territory[SE].alpha2} {/en_US.person.last} {/sv_SE.address.locality}") + feed.Close() + code := cli.Main(data.Modules()...) + stdout.Close() + out, _ := io.ReadAll(drain) + if code != 0 || len(out) < len("SE x y") || string(out[:3]) != "SE " { + t.Errorf("fejkdata = %d, %q, want SE followed by a last name and a locality", code, out) } } diff --git a/imports_test.go b/imports_test.go index ccca1f2..948d343 100644 --- a/imports_test.go +++ b/imports_test.go @@ -30,7 +30,8 @@ func TestPackageImports(t *testing.T) { checked := 0 allowed := map[string][]string{ ".": {"internal/builtinfunc", "internal/datafiles", "internal/datatype", "internal/drawstate", "internal/grammar", "internal/invariant", "internal/jsonvalue", "internal/proven", "internal/rows"}, - "cmd/fejkdata": {".", "data"}, + "cli": {"."}, + "cmd/fejkdata": {"cli", "data"}, "data": {"data/en_US", "data/geo/SE", "data/geo/US", "data/misc", "data/sv_SE"}, "data/en_US": {"data/geo/US"}, "data/geo/SE": nil, @@ -48,13 +49,13 @@ func TestPackageImports(t *testing.T) { "internal/rows": {"internal/drawstate", "internal/grammar", "internal/invariant"}, } testsAlso := map[string][]string{ - ".": {"data", "data/en_US", "data/geo/SE", "data/geo/US", "data/misc", "data/sv_SE"}, - "cmd/fejkdata": {"internal/grammar"}, - "data/en_US": {"."}, - "data/geo/SE": {"."}, - "data/geo/US": {"."}, - "data/misc": {"."}, - "data/sv_SE": {"."}, + ".": {"data", "data/en_US", "data/geo/SE", "data/geo/US", "data/misc", "data/sv_SE"}, + "cli": {"internal/grammar"}, + "data/en_US": {"."}, + "data/geo/SE": {"."}, + "data/geo/US": {"."}, + "data/misc": {"."}, + "data/sv_SE": {"."}, } seen := map[string]bool{} eachPackage(t, func(dir string, pkg *build.Package) {