Rename the project to fejkdata and record the fork source
Tests / vet + fmt + tests (pull_request) Failing after 6s
Tests / vet + fmt + tests (pull_request) Failing after 6s
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
// Command fejkdata prints one fake value from one or more data directories.
|
||||
//
|
||||
// fejkdata -data-path ./data/sv_SE person # a full person
|
||||
// fejkdata -data-path ./data/sv_SE person.last # just the surname (dotted path)
|
||||
// fejkdata -data-path ./data sv_SE.person # point at the tree, address by folder
|
||||
// fejkdata -data-path ./data/sv_SE -data-path ./mydata person # layer custom data; last dir wins
|
||||
// fejkdata -seed 42 -data-path ./data/sv_SE address
|
||||
//
|
||||
// It is a thin CLI over the fejkdata library: New(dirs) then Fake(path).
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"gitea.larvit.se/larvit/fejkdata"
|
||||
)
|
||||
|
||||
const usage = `Usage: fejkdata -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] <path>
|
||||
|
||||
-data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash)
|
||||
<path> 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)
|
||||
-list list the paths the data offers, then exit
|
||||
-version print the version, then exit
|
||||
|
||||
Flags must come before <path>.`
|
||||
|
||||
// stringList collects a repeatable string flag, preserving order.
|
||||
type stringList []string
|
||||
|
||||
func (s *stringList) String() string { return strings.Join(*s, ",") }
|
||||
|
||||
func (s *stringList) Set(v string) error { *s = append(*s, v); return nil }
|
||||
|
||||
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
||||
|
||||
// run is main's testable core: it returns the process exit code (0 ok, 1
|
||||
// runtime error, 2 misuse) and writes only to the given streams.
|
||||
func run(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("fejkdata", flag.ContinueOnError)
|
||||
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")
|
||||
list := fs.Bool("list", false, "list the paths the data offers, then exit")
|
||||
showVersion := fs.Bool("version", false, "print the version, then exit")
|
||||
var dirs stringList
|
||||
fs.Var(&dirs, "data-path", "a data directory to load (repeatable)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) { // -h/-help already printed usage
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if *showVersion {
|
||||
fmt.Fprintln(stdout, "fejkdata "+buildVersion())
|
||||
return 0
|
||||
}
|
||||
if len(dirs) == 0 {
|
||||
fs.Usage()
|
||||
return 2
|
||||
}
|
||||
|
||||
var opts []fejkdata.Option
|
||||
fs.Visit(func(fl *flag.Flag) {
|
||||
if fl.Name == "seed" {
|
||||
opts = append(opts, fejkdata.WithSeed(*seed))
|
||||
}
|
||||
})
|
||||
|
||||
if *list {
|
||||
f, err := fejkdata.New(dirs, opts...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 1
|
||||
}
|
||||
for _, p := range f.List() {
|
||||
fmt.Fprintln(stdout, p)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if fs.NArg() != 1 {
|
||||
fs.Usage()
|
||||
return 2
|
||||
}
|
||||
if *repeat < 1 {
|
||||
fmt.Fprintln(stderr, "repeat must be a positive integer")
|
||||
return 2
|
||||
}
|
||||
|
||||
path := fs.Arg(0)
|
||||
f, err := fejkdata.New(dirs, opts...)
|
||||
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, strings.Join(vals, *sep))
|
||||
return 0
|
||||
}
|
||||
|
||||
// buildVersion reports the module version stamped into the binary by `go install`
|
||||
// (or "devel" for a local build), read from the build info — no version constant
|
||||
// to bump, no extra dependency.
|
||||
func buildVersion() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
|
||||
return info.Main.Version
|
||||
}
|
||||
return "devel"
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
svSE = "../../data/sv_SE"
|
||||
enUS = "../../data/en_US"
|
||||
)
|
||||
|
||||
// runOut runs the CLI and returns exit code, stdout, stderr.
|
||||
func runOut(args ...string) (int, string, string) {
|
||||
var out, errb bytes.Buffer
|
||||
code := run(args, &out, &errb)
|
||||
return code, out.String(), errb.String()
|
||||
}
|
||||
|
||||
func TestRunOutputsValue(t *testing.T) {
|
||||
code, out, errb := runOut("-data-path", svSE, "person")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
if strings.TrimSpace(out) == "" {
|
||||
t.Fatalf("empty output, stderr=%q", errb)
|
||||
}
|
||||
if !strings.HasSuffix(out, "\n") {
|
||||
t.Errorf("output should end with newline, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDotPath(t *testing.T) {
|
||||
// person.last descends to just a surname — never the full "First Last".
|
||||
code, full, _ := runOut("-seed", "7", "-data-path", svSE, "person")
|
||||
if code != 0 {
|
||||
t.Fatalf("person run = %d", code)
|
||||
}
|
||||
code, last, errb := runOut("-seed", "7", "-data-path", svSE, "person.last")
|
||||
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 TestRunSeedDeterministic(t *testing.T) {
|
||||
_, a, _ := runOut("-seed", "42", "-data-path", svSE, "address")
|
||||
_, b, _ := runOut("-seed", "42", "-data-path", svSE, "address")
|
||||
if a != b {
|
||||
t.Errorf("same seed diverged: %q != %q", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRepeat(t *testing.T) {
|
||||
// -repeat N prints N values, one per line by default.
|
||||
code, out, errb := runOut("-seed", "1", "-repeat", "3", "-data-path", 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", ",", "-data-path", 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", "-data-path", 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, "-data-path", 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 TestRunUsageOnMissingArgs(t *testing.T) {
|
||||
// Need at least one -data-path and exactly one positional path; else misuse.
|
||||
for _, args := range [][]string{{}, {"-data-path", svSE}, {"person"}} {
|
||||
code, _, errb := runOut(args...)
|
||||
if code != 2 {
|
||||
t.Errorf("run(%v) = %d, want 2", args, code)
|
||||
}
|
||||
if !strings.Contains(errb, "Usage") {
|
||||
t.Errorf("run(%v) stderr = %q, want usage", args, errb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMultipleDataPaths(t *testing.T) {
|
||||
// -data-path repeats: dirs merge, last wins; the path stays positional.
|
||||
code, out, errb := runOut("-data-path", enUS, "-data-path", svSE, "person")
|
||||
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("-data-path", svSE, "nope")
|
||||
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) {
|
||||
// -list prints the discoverable paths and exits 0, no positional path needed.
|
||||
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 TestRunHelpExitsZero(t *testing.T) {
|
||||
// -h/-help is success (0), not misuse (2), so scripts under `set -e` survive.
|
||||
for _, h := range []string{"-h", "-help"} {
|
||||
code, _, errb := runOut(h)
|
||||
if code != 0 {
|
||||
t.Errorf("%s = %d, want 0", h, code)
|
||||
}
|
||||
if !strings.Contains(errb, "Usage") {
|
||||
t.Errorf("%s: stderr = %q, want usage", h, errb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVersion(t *testing.T) {
|
||||
code, out, errb := runOut("-version")
|
||||
if code != 0 {
|
||||
t.Fatalf("-version = %d, stderr=%q", code, errb)
|
||||
}
|
||||
if strings.TrimSpace(out) == "" {
|
||||
t.Error("-version: want a version on stdout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMissingDirFails(t *testing.T) {
|
||||
code, _, errb := runOut("-data-path", "../../data/nope", "person")
|
||||
if code != 1 {
|
||||
t.Fatalf("run = %d, want 1", code)
|
||||
}
|
||||
if errb == "" {
|
||||
t.Error("want an error message on stderr")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user