Rename the project to fejkdata and record the fork source
Tests / vet + fmt + tests (pull_request) Failing after 6s

This commit is contained in:
2026-09-01 20:24:03 +02:00
parent 64a0d29496
commit 05dc042e49
23 changed files with 136 additions and 134 deletions
+125
View File
@@ -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"
}
+192
View File
@@ -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")
}
}