diff --git a/calc_test.go b/calc_test.go index f37f783..b24696c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -72,12 +72,16 @@ func TestCalcFields(t *testing.T) { } } -// TestCalcNonNumericIsNaN pins the never-fail rule: a field that doesn't render -// to a number becomes NaN, which propagates and prints visibly. +// TestCalcNonNumericIsNaN pins the never-fail rule: a field that sometimes does +// not render to a number becomes NaN then, which propagates and prints visibly. func TestCalcNonNumericIsNaN(t *testing.T) { - if got := mustRender(t, engine(1), `{"format":"{calc(x * 2)}","x":"abc"}`); got != "NaN" { - t.Fatalf("calc over non-numeric field = %q, want NaN", got) + f := engine(1) + for i := 0; i < 50; i++ { + if got := mustRender(t, f, `{"format":"{calc(x * 2)}","x":["abc","1"]}`); got == "NaN" { + return + } } + t.Fatal("calc over a sometimes non-numeric field never printed NaN in 50 draws") } // TestCalcReproducible pins that a calc over a random operand stays seed-stable. @@ -218,3 +222,26 @@ func TestCalcHoldIsPerTemplate(t *testing.T) { t.Fatal("the nested template never differed from its parent, want its own draw") } } + +func TestCalcOverANeverNumericOperandIsRejected(t *testing.T) { + for src, want := range map[string]string{ + `{"format":"{calc(x * 2)}","x":"abc"}`: `"abc"`, + `{"format":"{calc(x * 2)}","x":["a","b"]}`: `"x"`, + `{"format":"{calc(x + y)}","x":"1","y":"{{2}}"}`: `"{2}"`, + } { + _, err := compile(parse(t, src)) + if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "never a number") { + t.Errorf("compile(%s) = %v, want the operand rejected naming %s", src, err, want) + } + } + for _, ok := range []string{ + `{"format":"{calc(x * 2)}","x":"3"}`, + `{"format":"{calc(x * 2)}","x":" 2.5 "}`, + `{"format":"{calc(x * 2)}","x":["1","abc"]}`, + `{"format":"{calc(x * 2)}","x":"{digits(2)}"}`, + } { + if _, err := compile(parse(t, ok)); err != nil { + t.Errorf("compile(%s) = %v, want it accepted", ok, err) + } + } +} diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index 86fd3e3..d8bc864 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -311,4 +311,8 @@ func TestRunNoShippedData(t *testing.T) { if code != 0 || strings.TrimSpace(out) == "" { t.Errorf("run = %d, out=%q", code, out) } + code, _, errb = runOut("--no-shipped-data", "sv_SE.person") + if code != 2 || !strings.Contains(errb, "--no-shipped-data needs at least one --data-path") { + t.Errorf("--no-shipped-data alone = %d, %q, want misuse naming --data-path", code, errb) + } } diff --git a/loading_test.go b/loading_test.go index f7aadfc..9c711f7 100644 --- a/loading_test.go +++ b/loading_test.go @@ -181,3 +181,23 @@ func TestHiddenEntriesAreSkipped(t *testing.T) { } } } + +func TestRepeatProductAlongAPathIsCapped(t *testing.T) { + for name, files := range map[string]map[string]string{ + "nested": {"cat": `{"format":"{a}","repeat":2048,"a":{"format":"{b}","repeat":2048,"b":"x"}}`}, + "through a reference": { + "a": `{"format":"{..b}","repeat":2048}`, + "b": `{"format":"x","repeat":2048}`, + }, + } { + _, err := New(WithoutShippedData(), WithDataPath(writeData(t, files))) + if err == nil || !strings.Contains(err.Error(), "repeat") || !strings.Contains(err.Error(), "1048576") { + t.Errorf("%s: New = %v, want the repeat product rejected naming the maximum", name, err) + } + } + if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ + "cat": `{"format":"{a}{c}","repeat":1024,"a":{"format":"{b}","repeat":1024,"b":"x"},"c":{"format":"y","repeat":1024}}`, + }))); err != nil { + t.Errorf("New = %v, want 1024 x 1024 along one path accepted", err) + } +} diff --git a/shipped_data_test.go b/shipped_data_test.go index c1d97e3..24aad04 100644 --- a/shipped_data_test.go +++ b/shipped_data_test.go @@ -1,6 +1,7 @@ package fejkdata import ( + "errors" "reflect" "slices" "strings" @@ -55,8 +56,8 @@ func TestUserDataMayReferenceShipped(t *testing.T) { func TestWithoutShippedDataNeedsASource(t *testing.T) { _, err := New(WithoutShippedData()) - if err == nil || !strings.Contains(err.Error(), "WithDataPath") { - t.Fatalf("New(WithoutShippedData()) = %v, want an error naming WithDataPath", err) + if err == nil || !strings.Contains(err.Error(), "WithDataPath") || !errors.Is(err, ErrNoData) { + t.Fatalf("New(WithoutShippedData()) = %v, want ErrNoData naming WithDataPath", err) } } diff --git a/template_test.go b/template_test.go index 1f8a59a..1f7f8f4 100644 --- a/template_test.go +++ b/template_test.go @@ -121,12 +121,10 @@ func TestAlternationPicksOneField(t *testing.T) { } } -func TestWeightZeroNeverChosen(t *testing.T) { - f := engine(5) - for i := 0; i < 200; i++ { - if got := mustRender(t, f, `[{"format":"X","weight":0},"Y"]`); got != "Y" { - t.Fatalf("weight-0 variant chosen: %q", got) - } +func TestWeightZeroIsRejected(t *testing.T) { + _, err := compile(parse(t, `[{"format":"X","weight":0},"Y"]`)) + if err == nil || !strings.Contains(err.Error(), "weight 0") || !strings.Contains(err.Error(), "remove") { + t.Fatalf("compile(weight 0) = %v, want it rejected naming the fix", err) } }