diff --git a/README.md b/README.md index dc3821d..5b6dc2c 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,13 @@ fakes ./data/sv_SE person.last # Eriksson (dotted path into a category) fakes ./data sv_SE.person # point at the tree; the folder is a segment fakes ./data/sv_SE ./mydata word # layer dirs; the last wins a name clash fakes -seed 42 ./data/sv_SE address +fakes -repeat 3 ./data/sv_SE person # three values, one per line +fakes -repeat 3 -separator ', ' ./data/sv_SE word # nät, barn, sol ``` +`-repeat N` renders the path N times — each an independent draw — joined by +`-separator` (default a newline, so values land one per line). + Without installing, run it from a checkout with `go run ./cmd/fakes …`. Exit codes: `0` success, `1` runtime error (missing dir, unknown path), `2` misuse. @@ -68,11 +73,11 @@ fakes -seed 1 ./data/sv_SE sql # INSERT INTO users VALUES('zoom'),('wahoo'),('blip'); ``` -Raise `repeat` for more rows per statement, or loop in the shell to build a -whole seed file: +Raise the template's `repeat` for more rows per statement; use the CLI's +`-repeat` for more statements — together they build a whole seed file: ```sh -for _ in $(seq 100); do fakes ./data/sv_SE sql; done > seed.sql +fakes -repeat 100 ./data/sv_SE sql > seed.sql ``` ## Library diff --git a/cmd/fakes/main.go b/cmd/fakes/main.go index 536738c..a224184 100644 --- a/cmd/fakes/main.go +++ b/cmd/fakes/main.go @@ -14,15 +14,18 @@ import ( "fmt" "io" "os" + "strings" "github.com/Timewave-AB/fakes" ) -const usage = `Usage: fakes [-seed N] ... +const usage = `Usage: fakes [-seed N] [-repeat N] [-separator S] ... - one or more data directories, e.g. ./data/sv_SE (last wins on clash) - a category, or a dotted path into one (person, person.last) - -seed N seed for reproducible output` + one or more data directories, e.g. ./data/sv_SE (last wins on clash) + a category, or a dotted path into one (person, person.last) + -seed N seed for reproducible output + -repeat N render the path N times (default 1) + -separator S string between repeated values (default newline)` func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) } @@ -33,6 +36,8 @@ func run(args []string, stdout, stderr io.Writer) int { fs.SetOutput(stderr) fs.Usage = func() { fmt.Fprintln(stderr, usage) } seed := fs.Uint64("seed", 0, "seed for reproducible output") + repeat := fs.Int("repeat", 1, "render the path this many times") + sep := fs.String("separator", "\n", "string between repeated values") if err := fs.Parse(args); err != nil { return 2 } @@ -40,6 +45,10 @@ func run(args []string, stdout, stderr io.Writer) int { fs.Usage() return 2 } + if *repeat < 1 { + fmt.Fprintln(stderr, "repeat must be a positive integer") + return 2 + } var opts []fakes.Option fs.Visit(func(fl *flag.Flag) { @@ -54,11 +63,13 @@ func run(args []string, stdout, stderr io.Writer) int { fmt.Fprintln(stderr, err) return 1 } - v, err := f.Fake(path) - if err != nil { - fmt.Fprintln(stderr, err) - return 1 + vals := make([]string, *repeat) + for i := range vals { + if vals[i], err = f.Fake(path); err != nil { + fmt.Fprintln(stderr, err) + return 1 + } } - fmt.Fprintln(stdout, v) + fmt.Fprintln(stdout, strings.Join(vals, *sep)) return 0 } diff --git a/cmd/fakes/main_test.go b/cmd/fakes/main_test.go index a5e5452..e446d12 100644 --- a/cmd/fakes/main_test.go +++ b/cmd/fakes/main_test.go @@ -57,6 +57,60 @@ func TestRunSeedDeterministic(t *testing.T) { } } +func TestRunRepeat(t *testing.T) { + // -repeat N prints N values, one per line by default. + code, out, errb := runOut("-seed", "1", "-repeat", "3", svSE, "word") + 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) + } +} + +func TestRunSeparator(t *testing.T) { + // -separator joins the repeated values instead of newlines. + code, out, errb := runOut("-repeat", "3", "-separator", ",", svSE, "word") + if code != 0 { + t.Fatalf("run = %d, stderr=%q", code, errb) + } + if n := strings.Count(out, "\n"); n != 1 { + t.Errorf("want one trailing newline, got %d: %q", n, out) + } + if !strings.Contains(out, ",") { + t.Errorf("values should be comma-joined: %q", out) + } +} + +func TestRunRepeatAdvancesRNG(t *testing.T) { + // Each repeat is a fresh draw, not the same value N times. + code, out, errb := runOut("-seed", "1", "-repeat", "5", svSE, "person") + 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 TestRunRepeatInvalid(t *testing.T) { + // A non-positive repeat is misuse. + for _, r := range []string{"0", "-1"} { + code, _, errb := runOut("-repeat", r, svSE, "word") + if code != 2 { + t.Errorf("repeat=%s = %d, want 2", r, code) + } + if errb == "" { + t.Errorf("repeat=%s: want an error message", r) + } + } +} + func TestRunUsageOnTooFewArgs(t *testing.T) { // Need at least one data dir plus a path; fewer is misuse. for _, args := range [][]string{{}, {svSE}} {