Answer architecture round 1: a custom CLI names itself by its file, --version names its module beside fejkdata's and follows a replace, the module path comes from the library's type, the end-to-end test runs main in a subprocess, the CLI tests find the shipped modules by their manifests, and Your own CLI sits under Library
This commit is contained in:
+4
-3
@@ -138,9 +138,10 @@ replacement, and each removed path, column or flag.
|
|||||||
the folders `WithDataPath` names, and fails, naming `WithDataFS`, where no such option
|
the folders `WithDataPath` names, and fails, naming `WithDataFS`, where no such option
|
||||||
is given. A manifest's `namespace` mounts its source under a dot path.
|
is given. A manifest's `namespace` mounts its source under a dot path.
|
||||||
- Package `github.com/larvit/fejkdata/cli` is the `fejkdata` command: `cli.Main(modules...)`
|
- Package `github.com/larvit/fejkdata/cli` is the `fejkdata` command: `cli.Main(modules...)`
|
||||||
runs it over the modules given, and returns its exit code, so a `main` package passing
|
runs it over the modules given, and returns its exit code, so a `main` package calling
|
||||||
`append(data.Modules(), mydata)` builds a CLI carrying your own data. `--version` prints
|
`cli.Main(append(data.Modules(), mydata)...)` builds a CLI carrying your own data, which
|
||||||
the fejkdata module's version, `devel` in a build from a checkout.
|
names itself by its file in its help and errors. `--version` prints the fejkdata
|
||||||
|
module's version, after the custom CLI's own where the CLI is one.
|
||||||
- A data source whose root holds a `.fejkdata.json` manifest with an `index` loads each
|
- A data source whose root holds a `.fejkdata.json` manifest with an `index` loads each
|
||||||
category on the first call reaching it, as each shipped module does. Data failing to load,
|
category on the first call reaching it, as each shipped module does. Data failing to load,
|
||||||
in `New` or on that first call, gives an error matching `ErrLoad`, and the CLI exits 1.
|
in `New` or on that first call, gives an error matching `ErrLoad`, and the CLI exits 1.
|
||||||
|
|||||||
@@ -364,7 +364,9 @@ ok, err := fejkdata.IsTemplate(arg) // an inline template by its shape, el
|
|||||||
|
|
||||||
A shipped package's `Modules()` is its `FS` and every module it reads, a locale's country's
|
A shipped package's `Modules()` is its `FS` and every module it reads, a locale's country's
|
||||||
places among them, which its package imports, so your program links only what you read.
|
places among them, which its package imports, so your program links only what you read.
|
||||||
`data.Modules()` is every shipped module, and links all of them. Two locales are `WithDataFS(append(svse.Modules(), enus.Modules()...)...)`. To stand in your
|
`data.Modules()`, from `github.com/larvit/fejkdata/data`, is every shipped module, and
|
||||||
|
links all of them. Two locales, with `enus` from `github.com/larvit/fejkdata/data/en_US`,
|
||||||
|
are `WithDataFS(append(svse.Modules(), enus.Modules()...)...)`. To stand in your
|
||||||
own places, pass the locale's `FS` alone beside a tree providing `geo.SE.address`, a record
|
own places, pass the locale's `FS` alone beside a tree providing `geo.SE.address`, a record
|
||||||
with a `format` and the columns `sv_SE.address` reads, `street`, `street-number`,
|
with a `format` and the columns `sv_SE.address` reads, `street`, `street-number`,
|
||||||
`postal-code` and `locality`: `WithDataFS(svse.FS, myPlaces)`. `New` fails where no `WithDataFS` or `WithDataPath` names the
|
`postal-code` and `locality`: `WithDataFS(svse.FS, myPlaces)`. `New` fails where no `WithDataFS` or `WithDataPath` names the
|
||||||
@@ -423,6 +425,33 @@ A `*Generator` is safe for concurrent use; a seeded sequence is reproducible onl
|
|||||||
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
||||||
stream for that value and everything drawn after it.
|
stream for that value and everything drawn after it.
|
||||||
|
|
||||||
|
### Your own CLI
|
||||||
|
|
||||||
|
Package `cli` is the `fejkdata` command, and its `Main` takes the modules the command
|
||||||
|
loads, so a CLI carrying your own data is a `main` package of your own:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/cli"
|
||||||
|
"github.com/larvit/fejkdata/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.json *.tsv
|
||||||
|
var mydata embed.FS
|
||||||
|
|
||||||
|
func main() { os.Exit(cli.Main(append(data.Modules(), mydata)...)) }
|
||||||
|
```
|
||||||
|
|
||||||
|
It takes `fejkdata`'s flags and exit codes, and names itself by the file it runs from in
|
||||||
|
its help and errors. `--no-shipped-data` leaves out the modules passed to `Main`, and
|
||||||
|
`--version` prints your module's version beside the fejkdata version it is built with.
|
||||||
|
`fejkdata`'s own `main` is `os.Exit(cli.Main(data.Modules()...))`.
|
||||||
|
|
||||||
## Data format
|
## Data format
|
||||||
|
|
||||||
Every value is a **node**, nestable without limit:
|
Every value is a **node**, nestable without limit:
|
||||||
@@ -1023,32 +1052,6 @@ Where the project is heading; the sections before Audience document what ships t
|
|||||||
16. **Values look real by default. You can also ask for values that can never reach
|
16. **Values look real by default. You can also ask for values that can never reach
|
||||||
anything real,** such as a domain nobody can register.
|
anything real,** such as a domain nobody can register.
|
||||||
|
|
||||||
### Your own CLI
|
|
||||||
|
|
||||||
Package `cli` is the `fejkdata` command, and its `Main` takes the modules the command
|
|
||||||
loads, so a CLI carrying your own data is a `main` package of your own:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/cli"
|
|
||||||
"github.com/larvit/fejkdata/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed *.json
|
|
||||||
var mydata embed.FS
|
|
||||||
|
|
||||||
func main() { os.Exit(cli.Main(append(data.Modules(), mydata)...)) }
|
|
||||||
```
|
|
||||||
|
|
||||||
It takes `fejkdata`'s flags and exit codes, `--no-shipped-data` leaves out the modules
|
|
||||||
passed to `Main`, and `--version` prints the fejkdata version it is built with.
|
|
||||||
`fejkdata`'s own `main` is `os.Exit(cli.Main(data.Modules()...))`.
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+64
-45
@@ -1,6 +1,7 @@
|
|||||||
// Package cli is the fejkdata command: it renders the template on its stdin with fake
|
// Package cli is the fejkdata command: it renders the template on its stdin with fake
|
||||||
// values from the modules it is given and any directories layered over them. A program
|
// values from the modules it is given and any directories layered over them. A program
|
||||||
// passing its own modules to Main is a fejkdata CLI carrying them.
|
// passing its own modules to Main is a fejkdata CLI carrying them, as
|
||||||
|
// os.Exit(cli.Main(append(data.Modules(), mydata)...)).
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,6 +12,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -276,41 +279,41 @@ func (in invocation) checkFlags() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const pipeHint = "echo '{/sv_SE.person}' | fejkdata"
|
func pipeHint(name string) string { return "echo '{/sv_SE.person}' | " + name }
|
||||||
|
|
||||||
// operandError refuses an argument that is no flag, naming the spelling that reads what
|
// operandError refuses an argument that is no flag, naming the spelling that reads what
|
||||||
// it most likely means: dropping it for -, a file for a file's name, else a template
|
// it most likely means: dropping it for -, a file for a file's name, else a template
|
||||||
// piped in.
|
// piped in.
|
||||||
func operandError(arg string) error {
|
func operandError(arg, name string) error {
|
||||||
unexpected := "unexpected argument '" + arg + "'"
|
unexpected := "unexpected argument '" + arg + "'"
|
||||||
if arg == "-" {
|
if arg == "-" {
|
||||||
return fmt.Errorf("%s; stdin is read already, so drop it", unexpected)
|
return fmt.Errorf("%s; stdin is read already, so drop it", unexpected)
|
||||||
}
|
}
|
||||||
if fi, err := os.Stat(arg); err == nil && fi.Mode().IsRegular() {
|
if fi, err := os.Stat(arg); err == nil && fi.Mode().IsRegular() {
|
||||||
return fmt.Errorf("%s; read a file from stdin: fejkdata < %s", unexpected, shellQuoted(arg))
|
return fmt.Errorf("%s; read a file from stdin: %s < %s", unexpected, name, shellQuoted(arg))
|
||||||
}
|
}
|
||||||
ref := "{/" + strings.TrimPrefix(arg, "/") + "}"
|
ref := "{/" + strings.TrimPrefix(arg, "/") + "}"
|
||||||
_, argLone := loneReference(arg)
|
_, argLone := loneReference(arg)
|
||||||
inline, err := fejkdata.IsTemplate(arg)
|
inline, err := fejkdata.IsTemplate(arg)
|
||||||
_, refLone := loneReference(ref)
|
_, refLone := loneReference(ref)
|
||||||
example := pipeHint
|
example := pipeHint(name)
|
||||||
switch {
|
switch {
|
||||||
case argLone || inline:
|
case argLone || inline:
|
||||||
example = piped(arg)
|
example = piped(arg, name)
|
||||||
case err == nil && refLone:
|
case err == nil && refLone:
|
||||||
example = piped(ref)
|
example = piped(ref, name)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s; the template is read from stdin: %s", unexpected, example)
|
return fmt.Errorf("%s; the template is read from stdin: %s", unexpected, example)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check rejects a flag combination or an operand that cannot run, before any data is
|
// check rejects a flag combination or an operand that cannot run, before any data is
|
||||||
// read.
|
// read.
|
||||||
func (in invocation) check() error {
|
func (in invocation) check(name string) error {
|
||||||
if err := in.checkFlags(); err != nil {
|
if err := in.checkFlags(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(in.args) > 0 {
|
if len(in.args) > 0 {
|
||||||
return operandError(in.args[0])
|
return operandError(in.args[0], name)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -541,13 +544,13 @@ func spacedReference(template string, err error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// piped is the command piping template to fejkdata; some shells' echo reads a backslash
|
// piped is the command piping template to the command name; some shells' echo reads a
|
||||||
// as an escape, so a template holding one goes through printf.
|
// backslash as an escape, so a template holding one goes through printf.
|
||||||
func piped(template string) string {
|
func piped(template, name string) string {
|
||||||
if strings.ContainsRune(template, '\\') {
|
if strings.ContainsRune(template, '\\') {
|
||||||
return "printf '%s' " + shellQuoted(template) + " | fejkdata"
|
return "printf '%s' " + shellQuoted(template) + " | " + name
|
||||||
}
|
}
|
||||||
return "echo " + shellQuoted(template) + " | fejkdata"
|
return "echo " + shellQuoted(template) + " | " + name
|
||||||
}
|
}
|
||||||
|
|
||||||
// shellQuoted is s as a shell reads it back: bare where it holds nothing a shell expands.
|
// shellQuoted is s as a shell reads it back: bare where it holds nothing a shell expands.
|
||||||
@@ -566,30 +569,33 @@ func Main(modules ...fs.FS) int {
|
|||||||
if fi, err := os.Stdin.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
|
if fi, err := os.Stdin.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
|
||||||
stdin = nil
|
stdin = nil
|
||||||
}
|
}
|
||||||
return run(modules, os.Args[1:], stdin, os.Stdout, os.Stderr)
|
name := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
|
||||||
|
return run(name, modules, os.Args[1:], stdin, os.Stdout, os.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// run is Main over the given streams. A nil stdin is a terminal, which holds no template.
|
// run is Main over the given streams, run as the command name. A nil stdin is a terminal,
|
||||||
func run(modules []fs.FS, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
// which holds no template.
|
||||||
|
func run(name string, modules []fs.FS, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||||
in, err := parseArgs(args)
|
in, err := parseArgs(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return misuse(stderr, err)
|
return misuse(stderr, name, err)
|
||||||
}
|
}
|
||||||
if in.help {
|
if in.help {
|
||||||
io.WriteString(stdout, usage)
|
io.WriteString(stdout, strings.ReplaceAll(usage, "fejkdata", name))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if in.version {
|
if in.version {
|
||||||
fmt.Fprintln(stdout, "fejkdata "+buildVersion())
|
info, _ := debug.ReadBuildInfo()
|
||||||
|
fmt.Fprintln(stdout, versionLine(name, info))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if err := in.check(); err != nil {
|
if err := in.check(name); err != nil {
|
||||||
return misuse(stderr, err)
|
return misuse(stderr, name, err)
|
||||||
}
|
}
|
||||||
var src input
|
var src input
|
||||||
if !in.list {
|
if !in.list {
|
||||||
if src, err = readStdin(stdin); err != nil {
|
if src, err = readStdin(stdin, name); err != nil {
|
||||||
return fail(stderr, err)
|
return fail(stderr, name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f, err := fejkdata.New(in.options(modules)...)
|
f, err := fejkdata.New(in.options(modules)...)
|
||||||
@@ -604,14 +610,14 @@ func run(modules []fs.FS, args []string, stdin io.Reader, stdout, stderr io.Writ
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if err := in.write(f, src, stdout); err != nil {
|
if err := in.write(f, src, stdout); err != nil {
|
||||||
return fail(stderr, err)
|
return fail(stderr, name, err)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func readStdin(stdin io.Reader) (input, error) {
|
func readStdin(stdin io.Reader, name string) (input, error) {
|
||||||
if stdin == nil {
|
if stdin == nil {
|
||||||
return input{}, inputError{fmt.Errorf("the template comes from stdin, and nothing is piped in: %s", pipeHint)}
|
return input{}, inputError{fmt.Errorf("the template comes from stdin, and nothing is piped in: %s", pipeHint(name))}
|
||||||
}
|
}
|
||||||
raw, err := io.ReadAll(stdin)
|
raw, err := io.ReadAll(stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -628,10 +634,10 @@ func templateError(err error) error {
|
|||||||
return inputError{err}
|
return inputError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fail(stderr io.Writer, err error) int {
|
func fail(stderr io.Writer, name string, err error) int {
|
||||||
var te inputError
|
var te inputError
|
||||||
if errors.As(err, &te) {
|
if errors.As(err, &te) {
|
||||||
return misuse(stderr, te.error)
|
return misuse(stderr, name, te.error)
|
||||||
}
|
}
|
||||||
fmt.Fprintln(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
if errors.Is(err, fejkdata.ErrNoColumns) {
|
if errors.Is(err, fejkdata.ErrNoColumns) {
|
||||||
@@ -640,26 +646,39 @@ func fail(stderr io.Writer, err error) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func misuse(stderr io.Writer, err error) int {
|
func misuse(stderr io.Writer, name string, err error) int {
|
||||||
// A library error already names the program, so the prefix is not doubled.
|
// A library error opens with the library's name, so the prefix is not doubled.
|
||||||
fmt.Fprintf(stderr, "fejkdata: %s\ntry 'fejkdata --help'\n", strings.TrimPrefix(err.Error(), "fejkdata: "))
|
fmt.Fprintf(stderr, "%s: %s\ntry '%s --help'\n", name, strings.TrimPrefix(err.Error(), "fejkdata: "), name)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildVersion is the version of the fejkdata module go install stamps into the binary,
|
// fejkdataModule is the path of the module holding the library; the root package's path is
|
||||||
// whichever module's main package carries the CLI, or "devel".
|
// the module's.
|
||||||
func buildVersion() string {
|
var fejkdataModule = reflect.TypeOf((*fejkdata.Generator)(nil)).Elem().PkgPath()
|
||||||
info, _ := debug.ReadBuildInfo()
|
|
||||||
return moduleVersion(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func moduleVersion(info *debug.BuildInfo) string {
|
// versionLine names the fejkdata version the binary is built with, after the main module's
|
||||||
if info != nil {
|
// version under the command name where another module's main package carries the CLI.
|
||||||
for _, m := range append([]*debug.Module{&info.Main}, info.Deps...) {
|
func versionLine(name string, info *debug.BuildInfo) string {
|
||||||
if m.Path == "github.com/larvit/fejkdata" && m.Version != "" && m.Version != "(devel)" {
|
if info == nil {
|
||||||
return m.Version
|
return "fejkdata devel"
|
||||||
}
|
}
|
||||||
|
line := "fejkdata " + moduleVersion(info.Main)
|
||||||
|
for _, m := range info.Deps {
|
||||||
|
if m.Path == fejkdataModule {
|
||||||
|
line = name + " " + moduleVersion(info.Main) + ", fejkdata " + moduleVersion(*m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "devel"
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// moduleVersion is the version m is built at: the replacement's where a replace directive
|
||||||
|
// names one, and "devel" where it carries none, as a local directory does.
|
||||||
|
func moduleVersion(m debug.Module) string {
|
||||||
|
if m.Replace != nil {
|
||||||
|
m = *m.Replace
|
||||||
|
}
|
||||||
|
if m.Version == "" || m.Version == "(devel)" {
|
||||||
|
return "devel"
|
||||||
|
}
|
||||||
|
return m.Version
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-23
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -15,15 +16,22 @@ import (
|
|||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// shipped holds the shipped modules as read from the checkout, so these tests link no data
|
// shipped holds the shipped modules as read from the checkout, each folder under ../data
|
||||||
// package; cmd/fejkdata runs over the packages themselves.
|
// holding a manifest, so these tests link no data package; cmd/fejkdata runs over the
|
||||||
var shipped = []fs.FS{
|
// packages themselves.
|
||||||
os.DirFS("../data/en_US"),
|
var shipped = func() []fs.FS {
|
||||||
os.DirFS("../data/geo/SE"),
|
var modules []fs.FS
|
||||||
os.DirFS("../data/geo/US"),
|
err := filepath.WalkDir(filepath.Join("..", "data"), func(path string, d fs.DirEntry, err error) error {
|
||||||
os.DirFS("../data/misc"),
|
if err == nil && d.Name() == ".fejkdata.json" {
|
||||||
os.DirFS("../data/sv_SE"),
|
modules = append(modules, os.DirFS(filepath.Dir(path)))
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil || len(modules) == 0 {
|
||||||
|
panic(fmt.Sprintf("no shipped module found under ../data: %v", err))
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
}()
|
||||||
|
|
||||||
// svSE, enUS and misc are copies of a shipped module's files without its manifest, so a test
|
// svSE, enUS and misc are copies of a shipped module's files without its manifest, so a test
|
||||||
// layers its categories unprefixed with -d.
|
// layers its categories unprefixed with -d.
|
||||||
@@ -70,25 +78,25 @@ func bareCopy(tmp, module string) string {
|
|||||||
|
|
||||||
func runOut(stdin string, args ...string) (int, string, string) {
|
func runOut(stdin string, args ...string) (int, string, string) {
|
||||||
var out, errb bytes.Buffer
|
var out, errb bytes.Buffer
|
||||||
code := run(shipped, args, strings.NewReader(stdin), &out, &errb)
|
code := run("fejkdata", shipped, args, strings.NewReader(stdin), &out, &errb)
|
||||||
return code, out.String(), errb.String()
|
return code, out.String(), errb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// runTerminal runs as from a shell with nothing piped in.
|
// runTerminal runs as from a shell with nothing piped in.
|
||||||
func runTerminal(args ...string) (int, string, string) {
|
func runTerminal(args ...string) (int, string, string) {
|
||||||
var out, errb bytes.Buffer
|
var out, errb bytes.Buffer
|
||||||
code := run(shipped, args, nil, &out, &errb)
|
code := run("fejkdata", shipped, args, nil, &out, &errb)
|
||||||
return code, out.String(), errb.String()
|
return code, out.String(), errb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunLoadsTheModulesItIsGiven(t *testing.T) {
|
func TestRunLoadsTheModulesItIsGiven(t *testing.T) {
|
||||||
module := fstest.MapFS{"pet.json": {Data: []byte(`{"format":"{name}","name":["Fido"]}`)}}
|
module := fstest.MapFS{"pet.json": {Data: []byte(`{"format":"{name}","name":["Fido"]}`)}}
|
||||||
var out, errb bytes.Buffer
|
var out, errb bytes.Buffer
|
||||||
if code := run([]fs.FS{module}, nil, strings.NewReader("{/pet}"), &out, &errb); code != 0 || out.String() != "Fido" {
|
if code := run("fejkdata", []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())
|
t.Errorf("run over a module holding pet = %d, %q, %q, want Fido", code, out.String(), errb.String())
|
||||||
}
|
}
|
||||||
out.Reset()
|
out.Reset()
|
||||||
if code := run([]fs.FS{module}, []string{"--list"}, nil, &out, &errb); code != 0 || out.String() != "pet\npet.name\n" {
|
if code := run("fejkdata", []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())
|
t.Errorf("--list over a module holding pet = %d, %q, want pet and pet.name only", code, out.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,7 +339,7 @@ func (failingReader) Read([]byte) (int, error) { return 0, errors.New("bad file
|
|||||||
|
|
||||||
func TestRunUnreadableStdinIsMisuse(t *testing.T) {
|
func TestRunUnreadableStdinIsMisuse(t *testing.T) {
|
||||||
var out, errb bytes.Buffer
|
var out, errb bytes.Buffer
|
||||||
if code := run(shipped, nil, failingReader{}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "cannot be read") {
|
if code := run("fejkdata", 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())
|
t.Errorf("run with an unreadable stdin = %d, %q; want misuse naming it", code, errb.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,20 +442,42 @@ func TestRunHelpOnStdout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestModuleVersionNamesFejkdatasVersion(t *testing.T) {
|
func TestVersionLineNamesFejkdatasVersion(t *testing.T) {
|
||||||
custom := &debug.BuildInfo{
|
fejkdata := func(version string, replace *debug.Module) *debug.Module {
|
||||||
Main: debug.Module{Path: "example.com/mycli", Version: "v9.0.0"},
|
return &debug.Module{Path: "github.com/larvit/fejkdata", Version: version, Replace: replace}
|
||||||
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"}}
|
mycli := debug.Module{Path: "example.com/mycli", Version: "v9.0.0"}
|
||||||
checkout := &debug.BuildInfo{Main: debug.Module{Path: "github.com/larvit/fejkdata", Version: "(devel)"}}
|
for _, c := range []struct {
|
||||||
for info, want := range map[*debug.BuildInfo]string{custom: "v0.1.0", own: "v0.1.0", checkout: "devel", nil: "devel"} {
|
info *debug.BuildInfo
|
||||||
if got := moduleVersion(info); got != want {
|
want string
|
||||||
t.Errorf("moduleVersion(%+v) = %q, want %q", info, got, want)
|
}{
|
||||||
|
{&debug.BuildInfo{Main: *fejkdata("v0.1.0", nil)}, "fejkdata v0.1.0"},
|
||||||
|
{&debug.BuildInfo{Main: *fejkdata("(devel)", nil)}, "fejkdata devel"},
|
||||||
|
{&debug.BuildInfo{Main: mycli, Deps: []*debug.Module{fejkdata("v0.1.0", nil)}}, "mycli v9.0.0, fejkdata v0.1.0"},
|
||||||
|
{&debug.BuildInfo{Main: mycli, Deps: []*debug.Module{fejkdata("v0.1.0", &debug.Module{Path: "../fejkdata"})}}, "mycli v9.0.0, fejkdata devel"},
|
||||||
|
{&debug.BuildInfo{Main: mycli, Deps: []*debug.Module{fejkdata("v0.1.0", &debug.Module{Path: "example.com/fork", Version: "v0.1.1"})}}, "mycli v9.0.0, fejkdata v0.1.1"},
|
||||||
|
{nil, "fejkdata devel"},
|
||||||
|
} {
|
||||||
|
if got := versionLine("mycli", c.info); got != c.want {
|
||||||
|
t.Errorf("versionLine(mycli, %+v) = %q, want %q", c.info, got, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunNamesItselfByTheCommandName(t *testing.T) {
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
if code := run("mycli", shipped, []string{"--nope"}, nil, &out, &errb); code != 2 || errb.String() != "mycli: unknown flag --nope\ntry 'mycli --help'\n" {
|
||||||
|
t.Errorf("mycli --nope = %d, %q", code, errb.String())
|
||||||
|
}
|
||||||
|
errb.Reset()
|
||||||
|
if code := run("mycli", shipped, nil, nil, &out, &errb); code != 2 || !strings.Contains(errb.String(), "| mycli\n") {
|
||||||
|
t.Errorf("mycli with nothing piped = %d, %q, want a pipe into mycli", code, errb.String())
|
||||||
|
}
|
||||||
|
if code := run("mycli", shipped, []string{"--help"}, nil, &out, &errb); code != 0 || !strings.HasPrefix(out.String(), "Usage: <template> | mycli [flags]\n") || strings.Contains(out.String(), "fejkdata") {
|
||||||
|
t.Errorf("mycli --help = %d, %q, want mycli named throughout", code, out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunVersion(t *testing.T) {
|
func TestRunVersion(t *testing.T) {
|
||||||
code, out, errb := runOut("", "--version")
|
code, out, errb := runOut("", "--version")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
|
|||||||
+18
-22
@@ -1,32 +1,28 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/cli"
|
|
||||||
"github.com/larvit/fejkdata/data"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
if os.Getenv("FEJKDATA_RUN_MAIN") == "1" {
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
func TestMainRendersTheShippedData(t *testing.T) {
|
func TestMainRendersTheShippedData(t *testing.T) {
|
||||||
stdin, feed, err := os.Pipe()
|
cmd := exec.Command(os.Args[0], "--seed", "1")
|
||||||
if err != nil {
|
cmd.Env = append(os.Environ(), "FEJKDATA_RUN_MAIN=1")
|
||||||
t.Fatal(err)
|
cmd.Stdin = strings.NewReader("{/misc.territory[SE].alpha2} {/en_US.person.last} {/sv_SE.address.locality}")
|
||||||
}
|
var stderr bytes.Buffer
|
||||||
drain, stdout, err := os.Pipe()
|
cmd.Stderr = &stderr
|
||||||
if err != nil {
|
out, err := cmd.Output()
|
||||||
t.Fatal(err)
|
if fields := strings.Fields(string(out)); err != nil || len(fields) < 3 || fields[0] != "SE" {
|
||||||
}
|
t.Errorf("fejkdata = %v, %q, %q, want SE followed by a last name and a locality", err, out, stderr.String())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake` and `FakeRecord` take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
|
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake` and `FakeRecord` take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
|
||||||
| 166 | 0.1.0 | | **Let a key or name cell hold a bracket, a brace, a double quote or a pipe, so the table loads, its row draws at random, and only a selector cannot reach it.** | 2 | 2 | 3 | 6 | 5.7 | 13.1 |
|
| 166 | 0.1.0 | | **Let a key or name cell hold a bracket, a brace, a double quote or a pipe, so the table loads, its row draws at random, and only a selector cannot reach it.** | 2 | 2 | 3 | 6 | 5.7 | 13.1 |
|
||||||
| 216 | 0.1.0 | | **Share between countries what `geo-se.py` and `data_test.go` hard-code for SE, in the first locale chunk that needs each.** | 2 | 2 | 2 | 6 | 3.2 | 12.3 |
|
| 216 | 0.1.0 | | **Share between countries what `geo-se.py` and `data_test.go` hard-code for SE, in the first locale chunk that needs each.** | 2 | 2 | 2 | 6 | 3.2 | 12.3 |
|
||||||
| 191 | 0.1.0 | defect | **Read an existing tag through `git/ref/tags/{tag}` in `publish_release.py`, so its guard against a tag at another commit fires.** | 1 | 1 | 3 | 6 | 10 | 11.4 |
|
| 191 | 0.1.0 | defect | **Read an existing tag through `git/ref/tags/{tag}` in `publish_release.py`, so its guard against a tag at another commit fires.** | 1 | 2 | 3 | 6 | 10 | 11.1 |
|
||||||
| 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 |
|
| 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 |
|
||||||
| 206 | 0.2.0 | | **Add `zh_CN` with its `geo/CN` tree.** | 4 | 8 | 8 | 8 | 2 | 25.2 |
|
| 206 | 0.2.0 | | **Add `zh_CN` with its `geo/CN` tree.** | 4 | 8 | 8 | 8 | 2 | 25.2 |
|
||||||
| 208 | 0.2.0 | | **Add `es_MX` with its `geo/MX` tree.** | 4 | 8 | 8 | 8 | 2 | 25.2 |
|
| 208 | 0.2.0 | | **Add `es_MX` with its `geo/MX` tree.** | 4 | 8 | 8 | 8 | 2 | 25.2 |
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
|
|
||||||
### 185. Seat a panel of the README's audience personas to pull data in through the module API and build a custom CLI, and fix what it names.
|
### 185. Seat a panel of the README's audience personas to pull data in through the module API and build a custom CLI, and fix what it names.
|
||||||
|
|
||||||
0.1.0 is not cut before it. Carries out the decision "The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0". Seats: the Go test author, the bulk fixture author, a custom CLI's builder, and a data author with a `-d` folder and the replace flag, each in a scratch Go module against the checkout or a pre-release tag. Bench `data.Modules()` and the `.fejkdata.json` manifest a data author writes by hand in use; the package names, the manifest's `reads` and `namespace` and the errors naming the import set were benched on 2026-10-11. Decoding the manifest is most of `New`'s cost (about 0.55 ms and 2449 allocations on 2026-10-10, 2864 allocations once each module carried its own manifest). An entry lists every path below its category, its linked tables' paths too, so `geo.*.region` repeats its whole family. An entry holding only its own paths, with `List` composing a family from the entries naming it as `parent`, would shrink it. Record the ruling in `docs/`.
|
0.1.0 is not cut before it. Carries out the decision "The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0". Seats: the Go test author, the bulk fixture author, a custom CLI's builder, and a data author with a `-d` folder and the replace flag, each in a scratch Go module against the checkout or a pre-release tag. Bench `data.Modules()` and the `.fejkdata.json` manifest a data author writes by hand in use; the package names, the manifest's `reads` and `namespace` and the errors naming the import set were benched on 2026-10-11. Decoding the manifest is most of `New`'s cost (about 0.55 ms and 2449 allocations on 2026-10-10, 2864 allocations once each module carried its own manifest). An entry lists every path below its category, its linked tables' paths too, so `geo.*.region` repeats its whole family. An entry holding only its own paths, with `List` composing a family from the entries naming it as `parent`, would shrink it. The custom CLI's builder also rules whether `--no-shipped-data`, `-d`'s "layer over the shipped data" and the help's `sv_SE` examples keep their wording where `cli.Main` carries their own modules, which the flag drops too. Record the ruling in `docs/`.
|
||||||
|
|
||||||
### 16. Add locale categories: company, phone, finance, vehicle, words.
|
### 16. Add locale categories: company, phone, finance, vehicle, words.
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ A second country reading GeoNames' postal codes moves `geonames()`, and the `str
|
|||||||
|
|
||||||
### 191. Read an existing tag through `git/ref/tags/{tag}` in `publish_release.py`, so its guard against a tag at another commit fires.
|
### 191. Read an existing tag through `git/ref/tags/{tag}` in `publish_release.py`, so its guard against a tag at another commit fires.
|
||||||
|
|
||||||
GitHub has no `tags/{tag}` endpoint and answers 404 whether or not the tag exists. So when the tag already sits at another commit, the release job publishes on that old tag, and the guard never stops it. The guard follows an annotated tag to its commit before comparing.
|
GitHub has no `tags/{tag}` endpoint and answers 404 whether or not the tag exists. So when the tag already sits at another commit, the release job publishes on that old tag, and the guard never stops it. The guard follows an annotated tag to its commit before comparing. A test over a stub of `request` runs in the merge gate and pins the guard, which no test covers today.
|
||||||
|
|
||||||
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
|
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user