detached 74a3afb

test(expr): excuse fromJSON error text differences only when both sides are reader failures (GO-004)

commit
74a3afb1d052a6391a4234e5d66e46a0769ef4b4
author
brice · · 2026-10-12 18:30 UTC
committer
brice · · 2026-10-18 14:59 UTC
parent
c13f270
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +61 -23
M expr/gen/runner/answers.mjs +0 -1
@@ -28,7 +28,6 @@ const git = (...a) => execFileSync("git", ["-C", runnerClone, ...a]).toString().
2828 const dotnet = execFileSync("dotnet", ["--list-runtimes"]).toString().split("\n").find(l => l.includes("Microsoft.NETCore.App 9")) ?? "";
2929 const cases = out.map((l, i) => {
3030 const r = JSON.parse(l);
31- if (r.err) delete r.err.x; // .NET exception type name
3231 return { e: oracle.cases[i].e, ...r };
3332 });
3433 process.stdout.write(JSON.stringify({
@@ -28,7 +28,6 @@ const git = (...a) => execFileSync("git", ["-C", runnerClone, ...a]).toString().
28 const dotnet = execFileSync("dotnet", ["--list-runtimes"]).toString().split("\n").find(l => l.includes("Microsoft.NETCore.App 9")) ?? "";28 const dotnet = execFileSync("dotnet", ["--list-runtimes"]).toString().split("\n").find(l => l.includes("Microsoft.NETCore.App 9")) ?? "";
29 const cases = out.map((l, i) => {29 const cases = out.map((l, i) => {
30 const r = JSON.parse(l);30 const r = JSON.parse(l);
31- if (r.err) delete r.err.x; // .NET exception type name
32 return { e: oracle.cases[i].e, ...r };31 return { e: oracle.cases[i].e, ...r };
33 });32 });
34 process.stdout.write(JSON.stringify({33 process.stdout.write(JSON.stringify({
M expr/runner_oracle_test.go +61 -22
@@ -6,6 +6,7 @@ import (
66 "compress/gzip"
77 "context"
88 "encoding/json"
9+ "errors"
910 "os"
1011 "strings"
1112 "testing"
@@ -29,15 +30,20 @@ type runnerFile struct {
2930 }
3031
3132 type runnerCase struct {
32- E string `json:"e"`
33- Empty bool `json:"empty"`
34- K string `json:"k"`
35- S string `json:"s"`
36- J string `json:"j"`
37- Err *struct {
38- T string `json:"t"`
39- M string `json:"m"`
40- } `json:"err"`
33+ E string `json:"e"`
34+ Empty bool `json:"empty"`
35+ K string `json:"k"`
36+ S string `json:"s"`
37+ J string `json:"j"`
38+ Err *runnerErr `json:"err"`
39+}
40+
41+// runnerErr is an error of the runner: the phase, the message, and the name of
42+// the .NET exception class ("JsonReaderException" for a fromJSON failure).
43+type runnerErr struct {
44+ T string `json:"t"`
45+ M string `json:"m"`
46+ X string `json:"x"`
4147 }
4248
4349 func loadRunner(tb testing.TB) *runnerFile {
@@ -83,10 +89,7 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase {
8389 rc := runnerCase{E: e}
8490 node, err := Parse(e, ctxNames, oracleFuncInfos)
8591 if err != nil {
86- rc.Err = &struct {
87- T string `json:"t"`
88- M string `json:"m"`
89- }{"parse", err.Error()}
92+ rc.Err = &runnerErr{T: "parse", M: err.Error()}
9093 return rc
9194 }
9295 if node == nil {
@@ -95,10 +98,7 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase {
9598 }
9699 v, err := Evaluate(context.Background(), node, ec)
97100 if err != nil {
98- rc.Err = &struct {
99- T string `json:"t"`
100- M string `json:"m"`
101- }{"eval", err.Error()}
101+ rc.Err = &runnerErr{T: "eval", M: err.Error(), X: goErrorClass(err)}
102102 return rc
103103 }
104104 rc.K = v.Kind().String()
@@ -107,11 +107,23 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase {
107107 return rc
108108 }
109109
110+// goErrorClass names the kind of an evaluation error of this package in the
111+// terms of runnerErr.X: "JsonReaderException" for the failures of fromJSON.
112+func goErrorClass(err error) string {
113+ var ee *EvalError
114+ if errors.As(err, &ee) && (ee.Message == "empty input" || ee.Message == "Error parsing JSON when evaluating fromJson") {
115+ return "JsonReaderException"
116+ }
117+ return ""
118+}
119+
110120 // sameAnswer reports whether a Go answer matches the runner's. Errors match
111121 // when they are of the same phase and, for parse errors, name the same kind of
112-// problem (the text before the colon); the runner appends the position and
113-// names the parenthesis rather than the function in arity errors, and its
114-// fromJSON errors are Json.NET messages. Other evaluation errors must be equal.
122+// problem (the text before the colon: the runner appends the position, and its
123+// arity errors name the parenthesis). Evaluation errors must have the same
124+// message, except a fromJSON failure, whose runner message is Json.NET's: it
125+// matches only a fromJSON failure of this package (both are JSON reader
126+// failures, never just "the expression mentions fromJSON").
115127 func sameAnswer(g, r runnerCase) bool {
116128 if (g.Err == nil) != (r.Err == nil) {
117129 return false
@@ -126,8 +138,8 @@ func sameAnswer(g, r runnerCase) bool {
126138 desc := func(m string) string { d, _, _ := strings.Cut(m, ":"); return d }
127139 return desc(g.Err.M) == desc(r.Err.M)
128140 }
129- if strings.Contains(strings.ToLower(r.E), "fromjson") {
130- return true
141+ if r.Err.X == "JsonReaderException" || g.Err.X == "JsonReaderException" {
142+ return r.Err.X == g.Err.X
131143 }
132144 return g.Err.M == r.Err.M
133145 }
@@ -138,3 +150,30 @@ func clip(s string) string {
138150 }
139151 return s
140152 }
153+
154+// TestSameAnswerFromJSON: an error is excused as "a fromJSON failure" only
155+// when both sides are one; an expression that merely mentions fromJSON does not
156+// get a free pass.
157+func TestSameAnswerFromJSON(t *testing.T) {
158+ ev := func(m, x string) runnerCase { return runnerCase{E: "fromJSON('x')", Err: &runnerErr{T: "eval", M: m, X: x}} }
159+ reader := ev("Error parsing JSON when evaluating fromJson", "JsonReaderException")
160+ runnerReader := ev("Unexpected character encountered while parsing value: x. Path '', line 0, position 0.", "JsonReaderException")
161+ format := ev("The following format string is invalid: {", "FormatException")
162+ value := runnerCase{E: "fromJSON('1')", K: "Number", S: "1", J: "1"}
163+ for _, tc := range []struct {
164+ name string
165+ g, r runnerCase
166+ want bool
167+ }{
168+ {"both reader failures", reader, runnerReader, true},
169+ {"go fails, runner succeeds", reader, value, false},
170+ {"runner fails, go succeeds", value, runnerReader, false},
171+ {"go reader failure, runner format error", reader, format, false},
172+ {"runner reader failure, go format error", format, runnerReader, false},
173+ {"same non-reader error", format, format, true},
174+ } {
175+ if got := sameAnswer(tc.g, tc.r); got != tc.want {
176+ t.Errorf("%s: sameAnswer = %v, want %v", tc.name, got, tc.want)
177+ }
178+ }
179+}
@@ -6,6 +6,7 @@ import (
6 "compress/gzip"6 "compress/gzip"
7 "context"7 "context"
8 "encoding/json"8 "encoding/json"
9+ "errors"
9 "os"10 "os"
10 "strings"11 "strings"
11 "testing"12 "testing"
@@ -29,15 +30,20 @@ type runnerFile struct {
29 }30 }
30 31
31 type runnerCase struct {32 type runnerCase struct {
32- E string `json:"e"`33+ E string `json:"e"`
33- Empty bool `json:"empty"`34+ Empty bool `json:"empty"`
34- K string `json:"k"`35+ K string `json:"k"`
35- S string `json:"s"`36+ S string `json:"s"`
36- J string `json:"j"`37+ J string `json:"j"`
37- Err *struct {38+ Err *runnerErr `json:"err"`
38- T string `json:"t"`39+}
39- M string `json:"m"`40+
40- } `json:"err"`41+// runnerErr is an error of the runner: the phase, the message, and the name of
42+// the .NET exception class ("JsonReaderException" for a fromJSON failure).
43+type runnerErr struct {
44+ T string `json:"t"`
45+ M string `json:"m"`
46+ X string `json:"x"`
41 }47 }
42 48
43 func loadRunner(tb testing.TB) *runnerFile {49 func loadRunner(tb testing.TB) *runnerFile {
@@ -83,10 +89,7 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase {
83 rc := runnerCase{E: e}89 rc := runnerCase{E: e}
84 node, err := Parse(e, ctxNames, oracleFuncInfos)90 node, err := Parse(e, ctxNames, oracleFuncInfos)
85 if err != nil {91 if err != nil {
86- rc.Err = &struct {92+ rc.Err = &runnerErr{T: "parse", M: err.Error()}
87- T string `json:"t"`
88- M string `json:"m"`
89- }{"parse", err.Error()}
90 return rc93 return rc
91 }94 }
92 if node == nil {95 if node == nil {
@@ -95,10 +98,7 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase {
95 }98 }
96 v, err := Evaluate(context.Background(), node, ec)99 v, err := Evaluate(context.Background(), node, ec)
97 if err != nil {100 if err != nil {
98- rc.Err = &struct {101+ rc.Err = &runnerErr{T: "eval", M: err.Error(), X: goErrorClass(err)}
99- T string `json:"t"`
100- M string `json:"m"`
101- }{"eval", err.Error()}
102 return rc102 return rc
103 }103 }
104 rc.K = v.Kind().String()104 rc.K = v.Kind().String()
@@ -107,11 +107,23 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase {
107 return rc107 return rc
108 }108 }
109 109
110+// goErrorClass names the kind of an evaluation error of this package in the
111+// terms of runnerErr.X: "JsonReaderException" for the failures of fromJSON.
112+func goErrorClass(err error) string {
113+ var ee *EvalError
114+ if errors.As(err, &ee) && (ee.Message == "empty input" || ee.Message == "Error parsing JSON when evaluating fromJson") {
115+ return "JsonReaderException"
116+ }
117+ return ""
118+}
119+
110 // sameAnswer reports whether a Go answer matches the runner's. Errors match120 // sameAnswer reports whether a Go answer matches the runner's. Errors match
111 // when they are of the same phase and, for parse errors, name the same kind of121 // when they are of the same phase and, for parse errors, name the same kind of
112-// problem (the text before the colon); the runner appends the position and122+// problem (the text before the colon: the runner appends the position, and its
113-// names the parenthesis rather than the function in arity errors, and its123+// arity errors name the parenthesis). Evaluation errors must have the same
114-// fromJSON errors are Json.NET messages. Other evaluation errors must be equal.124+// message, except a fromJSON failure, whose runner message is Json.NET's: it
125+// matches only a fromJSON failure of this package (both are JSON reader
126+// failures, never just "the expression mentions fromJSON").
115 func sameAnswer(g, r runnerCase) bool {127 func sameAnswer(g, r runnerCase) bool {
116 if (g.Err == nil) != (r.Err == nil) {128 if (g.Err == nil) != (r.Err == nil) {
117 return false129 return false
@@ -126,8 +138,8 @@ func sameAnswer(g, r runnerCase) bool {
126 desc := func(m string) string { d, _, _ := strings.Cut(m, ":"); return d }138 desc := func(m string) string { d, _, _ := strings.Cut(m, ":"); return d }
127 return desc(g.Err.M) == desc(r.Err.M)139 return desc(g.Err.M) == desc(r.Err.M)
128 }140 }
129- if strings.Contains(strings.ToLower(r.E), "fromjson") {141+ if r.Err.X == "JsonReaderException" || g.Err.X == "JsonReaderException" {
130- return true142+ return r.Err.X == g.Err.X
131 }143 }
132 return g.Err.M == r.Err.M144 return g.Err.M == r.Err.M
133 }145 }
@@ -138,3 +150,30 @@ func clip(s string) string {
138 }150 }
139 return s151 return s
140 }152 }
153+
154+// TestSameAnswerFromJSON: an error is excused as "a fromJSON failure" only
155+// when both sides are one; an expression that merely mentions fromJSON does not
156+// get a free pass.
157+func TestSameAnswerFromJSON(t *testing.T) {
158+ ev := func(m, x string) runnerCase { return runnerCase{E: "fromJSON('x')", Err: &runnerErr{T: "eval", M: m, X: x}} }
159+ reader := ev("Error parsing JSON when evaluating fromJson", "JsonReaderException")
160+ runnerReader := ev("Unexpected character encountered while parsing value: x. Path '', line 0, position 0.", "JsonReaderException")
161+ format := ev("The following format string is invalid: {", "FormatException")
162+ value := runnerCase{E: "fromJSON('1')", K: "Number", S: "1", J: "1"}
163+ for _, tc := range []struct {
164+ name string
165+ g, r runnerCase
166+ want bool
167+ }{
168+ {"both reader failures", reader, runnerReader, true},
169+ {"go fails, runner succeeds", reader, value, false},
170+ {"runner fails, go succeeds", value, runnerReader, false},
171+ {"go reader failure, runner format error", reader, format, false},
172+ {"runner reader failure, go format error", format, runnerReader, false},
173+ {"same non-reader error", format, format, true},
174+ } {
175+ if got := sameAnswer(tc.g, tc.r); got != tc.want {
176+ t.Errorf("%s: sameAnswer = %v, want %v", tc.name, got, tc.want)
177+ }
178+ }
179+}
M expr/testdata/runner.json.gz +0 -0 binary file: no text diff

binary file: no text diff

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close