Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
7 changed files with 344 additions and 48 deletions
Showing only changes of commit 83e4daefdb - Show all commits
-38
View File
@@ -4,13 +4,11 @@ import (
"encoding/base64"
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"testing"
"time"
"github.com/larvit/fejkdata/internal/grammar"
)
func TestBuiltinIDGenerators(t *testing.T) {
@@ -191,31 +189,6 @@ func ibanValid(s string) bool {
return rem == 1
}
// TestRegistryShapes pins the builtin contract compileFormat relies on: every entry
// supplies prep, and args parsed at compile only behind a check.
func TestRegistryShapes(t *testing.T) {
for name, b := range builtins {
if b.prep == nil {
t.Errorf("builtin %q has no prep: compileFormat would call a nil func", name)
}
if b.arity != 0 && b.checkArgs == nil {
t.Errorf("builtin %q parses args in prep with no check", name)
}
}
}
// TestArgGuardsPanic pins the guards that report a builtin arg its check should
// have rejected. No data reaches them — checkFunc runs a builtin's check before
// compileFormat ever calls prep — so they are exercised directly.
func TestArgGuardsPanic(t *testing.T) {
for name, call := range map[string]func(){
"atoi on an unvalidated arg": func() { atoi("nope") },
"atof on an unvalidated arg": func() { atof("nope") },
} {
mustPanic(t, name, call)
}
}
// mustPanic fails unless call panics with internalError's phrase, which is what
// separates a reported invariant break from a silently wrong value.
func mustPanic(t *testing.T, name string, call func()) {
@@ -450,14 +423,3 @@ func TestArgErrorsNameTheSpelling(t *testing.T) {
}
}
}
func TestNoRefOperandsMatchesTheOperandGrammar(t *testing.T) {
for name, b := range builtins {
if b.operands == nil {
continue
}
if takesRef := slices.ContainsFunc(b.operands([]string{"/x"}), grammar.IsRef); b.noRefOperands == takesRef {
t.Errorf("%s: noRefOperands = %v, but its operands func reads a reference: %v", name, b.noRefOperands, takesRef)
}
}
}
-6
View File
@@ -113,12 +113,6 @@ func TestParsedCallReadsOnlyAnOperandBuiltin(t *testing.T) {
}
}
// TestCalcPrepPanicsOnAnUncheckedExpression pins the guard on calc's own invariant:
// checkCalc parsed the expression before prep sees it.
func TestCalcPrepPanicsOnAnUncheckedExpression(t *testing.T) {
mustPanic(t, "prep on an expression that does not parse", func() { calcPrep([]string{"1 +"}) })
}
// --- one draw, one value: a calc operand reads the expansion's draw ---
// TestANamedCalcOperandIsTheOneShown pins how a calc computes from the value its format
+8 -4
View File
@@ -29,10 +29,14 @@ func TestPackageImports(t *testing.T) {
}
checked := 0
allowed := map[string][]string{
".": {"internal/drawstate", "internal/grammar"},
"cmd/fejkdata": {"."},
"internal/drawstate": nil,
"internal/grammar": nil,
".": {"internal/builtinfunc", "internal/datafiles", "internal/datatype", "internal/drawstate", "internal/grammar", "internal/proven"},
"cmd/fejkdata": {"."},
"internal/builtinfunc": {"internal/datatype", "internal/drawstate", "internal/grammar", "internal/proven"},
"internal/datafiles": {"internal/grammar"},
"internal/datatype": nil,
"internal/drawstate": nil,
"internal/grammar": nil,
"internal/proven": {"internal/datatype", "internal/grammar"},
}
seen := map[string]bool{}
err = filepath.WalkDir(".", func(dir string, d fs.DirEntry, err error) error {
+80
View File
@@ -0,0 +1,80 @@
package builtinfunc
import (
"fmt"
"slices"
"strings"
"testing"
"github.com/larvit/fejkdata/internal/grammar"
)
// TestRegistryShapes pins the builtin contract Prep relies on: every entry supplies prep,
// and args parsed at compile only behind a check.
func TestRegistryShapes(t *testing.T) {
for name, b := range builtins {
if b.prep == nil {
t.Errorf("builtin %q has no prep: Prep would call a nil func", name)
}
if b.arity != 0 && b.checkArgs == nil {
t.Errorf("builtin %q parses args in prep with no check", name)
}
}
}
func TestNoRefOperandsMatchesTheOperandGrammar(t *testing.T) {
for name, b := range builtins {
if b.operands == nil {
continue
}
if takesRef := slices.ContainsFunc(b.operands([]string{"/x"}), grammar.IsRef); b.noRefOperands == takesRef {
t.Errorf("%s: noRefOperands = %v, but its operands func reads a reference: %v", name, b.noRefOperands, takesRef)
}
}
}
func TestCheckNamesWhatACallGetsWrong(t *testing.T) {
for _, c := range []struct {
name string
args []string
want string
}{
{"nope", nil, `unknown function "nope"`},
{"luhn", []string{"1"}, "luhn takes 0 arguments, got 1"},
{"hex", nil, "hex takes 1 argument, got 0"},
{"calc", []string{"a +"}, `calc("a +"): `},
{"calc", []string{"a", "-1"}, "calc decimals "},
{"calc", nil, "calc takes an expression and an optional decimals count, got 0 args"},
} {
if err := Check(c.name, c.args); err == nil || !strings.HasPrefix(err.Error(), c.want) {
t.Errorf("Check(%s, %q) = %v, want it to start %q", c.name, c.args, err, c.want)
}
}
if err := Check("calc", []string{"a / b", "2"}); err != nil {
t.Errorf("Check(calc) = %v, want an expression over any operand accepted", err)
}
}
// TestArgGuardsPanic pins the guards that report a builtin arg its check should have
// rejected. No data reaches them, since Check runs before Prep, so they are called directly.
func TestArgGuardsPanic(t *testing.T) {
for name, call := range map[string]func(){
"atoi on an unvalidated arg": func() { atoi("nope") },
"atof on an unvalidated arg": func() { atof("nope") },
"prep on an expression that won't parse": func() { calcPrep([]string{"1 +"}) },
} {
mustPanic(t, name, call)
}
}
// mustPanic fails unless call panics with internalError's phrase, which is what
// separates a reported invariant break from a silently wrong value.
func mustPanic(t *testing.T, name string, call func()) {
t.Helper()
defer func() {
if r := recover(); r == nil || !strings.HasPrefix(fmt.Sprint(r), "fejkdata: internal error: ") {
t.Errorf("%s: recovered %v, want the invariant reported as an internal error", name, r)
}
}()
call()
}
+143
View File
@@ -0,0 +1,143 @@
package datafiles
import (
"errors"
"path/filepath"
"reflect"
"strings"
"testing"
"testing/fstest"
)
type handed struct {
dir []string
name string
json any
}
func walk(t *testing.T, src Source) ([]handed, error) {
t.Helper()
var got []handed
err := src.Walk(func(c Category) error {
got = append(got, handed{c.Dir, c.Name, c.JSON})
return nil
})
return got, err
}
func TestWalkHandsEachCategoryInOrder(t *testing.T) {
got, err := walk(t, Source{FS: fstest.MapFS{
"a.json": {Data: []byte(`{"x":"1"}`)},
"sub/b.json": {Data: []byte(`"y"`)},
"sub/notes.txt": {Data: []byte("not data")},
"empty/x.txt": {Data: []byte("not data")},
".git/c.json": {Data: []byte(`"hidden"`)},
"sub/.d.json": {Data: []byte(`"hidden"`)},
"sub/deep/e.json": {Data: []byte(`"z"`)},
}})
if err != nil {
t.Fatal(err)
}
want := []handed{
{nil, "a", map[string]any{"x": "1"}},
{[]string{"sub"}, "b", "y"},
{[]string{"sub", "deep"}, "e", "z"},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Walk handed %#v, want %#v", got, want)
}
}
func TestWalkStartsAtTheBaseDir(t *testing.T) {
got, err := walk(t, Source{FS: fstest.MapFS{"data/a.json": {Data: []byte(`"x"`)}, "b.json": {Data: []byte(`"y"`)}}, BaseDir: "data"})
if err != nil || len(got) != 1 || got[0].name != "a" || got[0].dir != nil {
t.Errorf("Walk = %#v, %v, want a alone at the base", got, err)
}
}
func TestReadRowsReadsOnlyAFileBesideTheCategory(t *testing.T) {
src := Source{FS: fstest.MapFS{
"t.json": {Data: []byte(`{"rows":"t.tsv"}`)},
"t.tsv": {Data: []byte("key\nA\n")},
"sub/u.tsv": {Data: []byte("key\nB\n")},
"sub/u.json": {Data: []byte(`{"rows":"u.tsv"}`)},
}, Label: "lbl"}
rows := map[string]string{}
err := src.Walk(func(c Category) error {
data, err := c.ReadRows(c.Name + ".tsv")
rows[c.Name] = data
if err != nil {
return err
}
if _, err := c.ReadRows("x.tsv"); err == nil || err.Error() != "rows names x.tsv, which is not beside it in "+map[string]string{"t": "lbl", "u": "lbl/sub"}[c.Name] {
t.Errorf("ReadRows(x.tsv) = %v, want the file named missing beside %s", err, c.Name)
}
return nil
})
if err != nil || rows["t"] != "key\nA\n" || rows["u"] != "key\nB\n" {
t.Errorf("Walk = %v, read %q", err, rows)
}
}
func TestWalkRefusesARowsFileNoCategoryNames(t *testing.T) {
_, err := walk(t, Source{FS: fstest.MapFS{"a.json": {Data: []byte(`"x"`)}, "a.tsv": {Data: []byte("key\n")}}, Label: "lbl"})
if err == nil || !strings.HasPrefix(err.Error(), "lbl/a.tsv: no category names it in its rows") {
t.Errorf("Walk = %v, want a.tsv refused as named by no category", err)
}
}
func TestWalkLabelsWhatACategoryFileGetsWrong(t *testing.T) {
for _, c := range []struct {
name string
fs fstest.MapFS
want string
}{
{"its compile", fstest.MapFS{"a.json": {Data: []byte(`"x"`)}}, "lbl/a.json: bad"},
{"its JSON", fstest.MapFS{"a.json": {Data: []byte(`{`)}}, "lbl/a.json: unexpected end of JSON input"},
{"its name", fstest.MapFS{"a b.json": {Data: []byte(`"x"`)}}, "lbl/a b.json: category "},
{"its folder's name", fstest.MapFS{"a b/c.json": {Data: []byte(`"x"`)}}, "lbl/a b: folder "},
} {
err := Source{FS: c.fs, Label: "lbl"}.Walk(func(Category) error { return errors.New("bad") })
if err == nil || !strings.HasPrefix(err.Error(), c.want) {
t.Errorf("%s: Walk = %v, want it to start %q", c.name, err, c.want)
}
}
}
func TestWalkChecksAFolderNameOnlyWhenItHoldsData(t *testing.T) {
if _, err := walk(t, Source{FS: fstest.MapFS{"a b/notes.txt": {Data: []byte("x")}, "c.json": {Data: []byte(`"x"`)}}}); err != nil {
t.Errorf("Walk = %v, want a folder holding no data skipped whatever its name", err)
}
}
func TestWalkRefusesADiskPathThatIsNoDirectory(t *testing.T) {
dir := t.TempDir()
for _, c := range []struct{ path, want string }{
{"", "a data path is empty"},
{filepath.Join(dir, "none"), "data path " + filepath.Join(dir, "none") + ": "},
} {
_, err := walk(t, Source{FS: fstest.MapFS{}, Label: c.path, OnDisk: true})
if err == nil || !strings.HasPrefix(err.Error(), c.want) {
t.Errorf("Walk(%q) = %v, want it to start %q", c.path, err, c.want)
}
}
}
func TestLoadHandsOneCategoryAndItsRows(t *testing.T) {
src := Source{FS: fstest.MapFS{
"data/sub/t.json": {Data: []byte(`{"rows":"t.tsv"}`)},
"data/sub/t.tsv": {Data: []byte("key\nA\n")},
"data/sub/u.json": {Data: []byte(`{`)},
}, BaseDir: "data"}
var got handed
var rows string
err := src.Load([]string{"sub"}, "t", func(c Category) error {
got = handed{c.Dir, c.Name, c.JSON}
var err error
rows, err = c.ReadRows("t.tsv")
return err
})
if err != nil || got.name != "t" || !reflect.DeepEqual(got.dir, []string{"sub"}) || rows != "key\nA\n" {
t.Errorf("Load = %v, handed %#v with rows %q", err, got, rows)
}
}
+25
View File
@@ -0,0 +1,25 @@
package datatype
import "testing"
func TestEachDatatypeIsSpelledAsDataWritesIt(t *testing.T) {
for d, want := range map[DataType][2]string{
String: {"string", "text"},
Integer: {"integer", "an integer"},
Number: {"number", "a number"},
Boolean: {"boolean", "a boolean"},
} {
if d.String() != want[0] || Noun(d) != want[1] {
t.Errorf("%d: String = %q, Noun = %q, want %q", int(d), d.String(), Noun(d), want)
}
}
if Count != 4 {
t.Errorf("Count = %d, want the four datatypes", Count)
}
}
func TestStringNamesADatatypeNoDataWrites(t *testing.T) {
if got := DataType(9).String(); got != "DataType(9)" {
t.Errorf("String = %q, want DataType(9)", got)
}
}
+88
View File
@@ -0,0 +1,88 @@
package proven
import (
"math"
"testing"
"github.com/larvit/fejkdata/internal/datatype"
"github.com/larvit/fejkdata/internal/grammar"
)
func TestLiteralSaysWhichDatatypesItsTextIs(t *testing.T) {
for _, c := range []struct {
text string
not [datatype.Count]bool
}{
{"42", [datatype.Count]bool{datatype.Boolean: true}},
{"1.5", [datatype.Count]bool{datatype.Integer: true, datatype.Boolean: true}},
{"true", [datatype.Count]bool{datatype.Integer: true, datatype.Number: true}},
{"007", [datatype.Count]bool{datatype.Integer: true, datatype.Number: true, datatype.Boolean: true}},
{"-0", [datatype.Count]bool{datatype.Integer: true, datatype.Number: true, datatype.Boolean: true}},
} {
v := Literal(c.text)
for d := datatype.Integer; d <= datatype.Boolean; d++ {
if (v.Not[d] != "") != c.not[d] {
t.Errorf("Literal(%q).Not[%s] = %q", c.text, d, v.Not[d])
}
}
}
if v := Literal("-0"); v.Not[datatype.Integer] != `"-0" is zero written with a sign; write "0"` {
t.Errorf("Literal(-0) = %q, want the unsigned spelling named", v.Not[datatype.Integer])
}
}
func TestOrKeepsTheBoundsOfBothAndTheFirstReason(t *testing.T) {
v := Bounded(1, 2, true).Or(Bounded(-3, 0.5, false))
if v.Lo != -3 || v.Hi != 2 || v.Integral || v.NonZero != 0 {
t.Errorf("Or = %+v, want [-3, 2], not integral, possibly zero", v)
}
if v := Unproven("first").Or(Unproven("second")); v.NotOperand != "first" || v.Not[datatype.Number] != "first" {
t.Errorf("Or = %+v, want the first reason kept", v)
}
}
func TestCalcBoundsAnExpressionFromItsOperands(t *testing.T) {
operand := func(name string) Value {
return map[string]Value{"a": Bounded(1, 9, true), "b": Bounded(0, 9, true), "big": Bounded(0, 1e299, true), "word": Unproven("it is text")}[name]
}
for expr, want := range map[string]string{
"a * 2 + 1": "",
"a / b": "divides by b, which is not proven nonzero",
"big * 100": "(big * 100) is not proven within 1e300",
"big": "",
"word + 1": `operand "word": it is text`,
"-(a - 10)": "",
} {
c, err := grammar.ParseCalc(expr)
if err != nil {
t.Fatal(err)
}
v, doubt := Calc(c.Expr, operand)
if doubt != want {
t.Errorf("Calc(%s) doubts %q, want %q", expr, doubt, want)
}
if expr == "a * 2 + 1" && (v.Lo != 3 || v.Hi != 19 || !v.Integral || v.NonZero != 3) {
t.Errorf("Calc(%s) = %+v, want [3, 19], integral, nonzero by 3", expr, v)
}
if expr == "-(a - 10)" && (v.Lo != 1 || v.Hi != 9) {
t.Errorf("Calc(%s) = %+v, want [1, 9]", expr, v)
}
}
lone, _ := grammar.ParseCalc("x")
if _, doubt := Calc(lone.Expr, func(string) Value { return Bounded(0, 1e301, true) }); doubt != "x is not proven within 1e300" {
t.Errorf("Calc(x) doubts %q, want a lone operand held to the limit", doubt)
}
}
func TestPrintedNumberWidensItsBoundsByTheRounding(t *testing.T) {
v := PrintedNumber("float(0,1,1)", Bounded(0.2, 0.8, false), 1)
if math.Abs(v.Lo-0.15) > 1e-12 || math.Abs(v.Hi-0.85) > 1e-12 || v.Not[datatype.Integer] == "" || v.Not[datatype.Number] != "" {
t.Errorf("PrintedNumber = %+v, want [0.15, 0.85], a number and no integer", v)
}
if v := PrintedNumber("calc(a)", Bounded(1, 2, true), ShortestDecimals); v.Not[datatype.Integer] != "" {
t.Errorf("PrintedNumber = %+v, want a whole value printed as an integer", v)
}
if v := Printing("digits(3)", datatype.String, Bounded(0, 999, true)); v.Not[datatype.Integer] != "{digits(3)} prints text, not an integer" {
t.Errorf("Printing = %q, want the text named", v.Not[datatype.Integer])
}
}