main 98594d0

runner_oracle_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package expr
4
5import (
6	"compress/gzip"
7	"context"
8	"encoding/json"
9	"errors"
10	"os"
11	"strings"
12	"testing"
13	"unicode/utf8"
14)
15
16// testdata/runner.json.gz holds what the C# runner's expression engine
17// (actions/runner, src/Sdk/DTExpressions2) answers on the corpus of
18// testdata/oracle.json.gz. It is produced by gen/runner. Where the runner and
19// the JavaScript reference disagree, the runner is what users see at run time
20// and this package follows it (see PORTING.md). TestOracle excuses a mismatch
21// with the JavaScript only if this package gives the runner's answer there.
22
23// runnerCommit is the commit of actions/runner that every file recorded from the
24// runner is about. gen/runner/runner-commit.txt holds the same value and the
25// generator scripts refuse a clone at another commit; TestRunnerCommit checks
26// that the two agree and that runner.json.gz was made from it.
27const runnerCommit = "5f0b3ddcc6ee9fe9441e00d55e85b94ea4aad798"
28
29type runnerFile struct {
30	Reference struct {
31		Source  string `json:"source"`
32		Commit  string `json:"commit"`
33		Runtime string `json:"runtime"`
34	} `json:"reference"`
35	Cases []runnerCase `json:"cases"`
36}
37
38type runnerCase struct {
39	E     string     `json:"e"`
40	Empty bool       `json:"empty"`
41	K     string     `json:"k"`
42	S     string     `json:"s"`
43	J     string     `json:"j"`
44	Err   *runnerErr `json:"err"`
45}
46
47// runnerErr is an error of the runner: the phase, the message, and the name of
48// the .NET exception class ("JsonReaderException" for a fromJSON failure).
49type runnerErr struct {
50	T string `json:"t"`
51	M string `json:"m"`
52	X string `json:"x"`
53}
54
55func loadRunner(tb testing.TB) *runnerFile {
56	tb.Helper()
57	f, err := os.Open("testdata/runner.json.gz")
58	if err != nil {
59		tb.Fatal(err)
60	}
61	defer func() { _ = f.Close() }()
62	zr, err := gzip.NewReader(f)
63	if err != nil {
64		tb.Fatal(err)
65	}
66	var rf runnerFile
67	if err := json.NewDecoder(zr).Decode(&rf); err != nil {
68		tb.Fatal(err)
69	}
70	return &rf
71}
72
73// lossy replaces every lone surrogate (WTF-8) by U+FFFD, which is what a JSON
74// decoder makes of the \ud800 escapes in the recorded answers.
75func lossy(s string) string {
76	if !hasWTF8(s) {
77		return s
78	}
79	var b strings.Builder
80	for i := 0; i < len(s); {
81		r, w := decodeRune(s[i:])
82		if r >= 0xD800 && r < 0xE000 {
83			b.WriteRune(utf8.RuneError)
84		} else {
85			b.WriteString(s[i : i+w])
86		}
87		i += w
88	}
89	return b.String()
90}
91
92// goAnswer evaluates e like the runner harness does and returns its answer in
93// the runner's format.
94func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase {
95	rc := runnerCase{E: e}
96	node, err := Parse(e, ctxNames, oracleFuncInfos)
97	if err != nil {
98		rc.Err = &runnerErr{T: "parse", M: err.Error()}
99		return rc
100	}
101	if node == nil {
102		rc.Empty = true
103		return rc
104	}
105	v, err := Evaluate(context.Background(), node, ec)
106	if err != nil {
107		rc.Err = &runnerErr{T: "eval", M: err.Error(), X: goErrorClass(err)}
108		return rc
109	}
110	rc.K = v.Kind().String()
111	rc.S = lossy(CoerceString(v))
112	rc.J = lossy(ToJSON(v))
113	return rc
114}
115
116// goErrorClass names the kind of an evaluation error of this package in the
117// terms of runnerErr.X: "JsonReaderException" for the failures of fromJSON.
118func goErrorClass(err error) string {
119	var je *JSONError
120	if errors.As(err, &je) {
121		return "JsonReaderException"
122	}
123	return ""
124}
125
126// sameAnswer reports whether a Go answer matches the runner's. Errors match
127// when they are of the same phase and have the same message, parse errors
128// included (token, position and expression), except a fromJSON failure, whose runner message is Json.NET's: it
129// matches only a fromJSON failure of this package (both are JSON reader
130// failures, never just "the expression mentions fromJSON").
131func sameAnswer(g, r runnerCase) bool {
132	if (g.Err == nil) != (r.Err == nil) {
133		return false
134	}
135	if g.Err == nil {
136		return g.Empty == r.Empty && g.K == r.K && g.S == r.S && g.J == r.J
137	}
138	if g.Err.T != r.Err.T {
139		return false
140	}
141	if r.Err.X == "JsonReaderException" || g.Err.X == "JsonReaderException" {
142		return r.Err.X == g.Err.X
143	}
144	return g.Err.M == r.Err.M
145}
146
147func clip(s string) string {
148	if len(s) > 160 {
149		return s[:160] + "..."
150	}
151	return s
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.
157func TestSameAnswerFromJSON(t *testing.T) {
158	ev := func(m, x string) runnerCase {
159		return runnerCase{E: "fromJSON('x')", Err: &runnerErr{T: "eval", M: m, X: x}}
160	}
161	reader := ev("Error parsing JSON when evaluating fromJson", "JsonReaderException")
162	runnerReader := ev("Unexpected character encountered while parsing value: x. Path '', line 0, position 0.", "JsonReaderException")
163	format := ev("The following format string is invalid: {", "FormatException")
164	value := runnerCase{E: "fromJSON('1')", K: "Number", S: "1", J: "1"}
165	for _, tc := range []struct {
166		name string
167		g, r runnerCase
168		want bool
169	}{
170		{"both reader failures", reader, runnerReader, true},
171		{"go fails, runner succeeds", reader, value, false},
172		{"runner fails, go succeeds", value, runnerReader, false},
173		{"go reader failure, runner format error", reader, format, false},
174		{"runner reader failure, go format error", format, runnerReader, false},
175		{"same non-reader error", format, format, true},
176	} {
177		if got := sameAnswer(tc.g, tc.r); got != tc.want {
178			t.Errorf("%s: sameAnswer = %v, want %v", tc.name, got, tc.want)
179		}
180	}
181}
182
183func TestRunnerCommit(t *testing.T) {
184	txt, err := os.ReadFile("gen/runner/runner-commit.txt")
185	if err != nil {
186		t.Fatal(err)
187	}
188	if got := strings.TrimSpace(string(txt)); got != runnerCommit {
189		t.Errorf("gen/runner/runner-commit.txt = %q, runnerCommit = %q", got, runnerCommit)
190	}
191	rf := loadRunner(t)
192	if rf.Reference.Commit != runnerCommit {
193		t.Errorf("testdata/runner.json.gz was recorded at %s, want %s", rf.Reference.Commit, runnerCommit)
194	}
195	if !strings.HasPrefix(rf.Reference.Runtime, "Microsoft.NETCore.App 9.0.") {
196		t.Errorf("runtime %q, want Microsoft.NETCore.App 9.0.x", rf.Reference.Runtime)
197	}
198	doc, err := os.ReadFile("PORTING.md")
199	if err != nil {
200		t.Fatal(err)
201	}
202	if !strings.Contains(string(doc), runnerCommit) {
203		t.Errorf("PORTING.md does not mention the runner commit %s", runnerCommit)
204	}
205}

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