v0.1.0 28f9d6e

runner_semantics_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package expr
4
5import (
6	"context"
7	"strings"
8	"testing"
9	"unicode/utf16"
10)
11
12// TestRunnerSemantics pins behaviours of the PORTING.md list that the C# runner
13// and the JavaScript reference share or differ on. Every expectation was
14// obtained by running the expression through the runner's own code
15// (gen/runner); the bulk is also covered by TestRunnerOracle.
16func TestRunnerSemantics(t *testing.T) {
17	ctx := map[string]Value{"github": mustJSON(t, `{
18		"commits": [{"id": "1"}, {"id": "2", "x": 1}, {"y": 3}],
19		"obj": {"a": {"a": 1}, "K": "v", "b": {"c": 2}},
20		"nested": [[1, 2], [3], []]}`)}
21	tests := []struct{ src, want string }{
22		// Index access: strings and numbers index arrays by their numeric value,
23		// booleans and null become 1 and 0, negative, NaN and infinite give null.
24		{"github.commits['1'].id", "String:2"},
25		{"github.commits[' 1 '].id", "String:2"},
26		{"github.commits['1.9'].id", "String:2"},
27		{"github.commits[1.9].id", "String:2"},
28		{"github.commits[true].id", "String:2"},
29		{"github.commits[null].id", "String:1"},
30		{"github.commits[-1]", "Null:"},
31		{"github.commits[NaN]", "Null:"},
32		{"github.commits[Infinity]", "Null:"},
33		{"github.commits['x']", "Null:"},
34		{"github.commits[5].id", "Null:"},
35		// On a dictionary only the string form counts, case-insensitively.
36		{"github.obj[true]", "Null:"},
37		{"github.obj[null]", "Null:"},
38		{"github.obj[1]", "Null:"},
39		{"github.obj['K']", "String:v"},
40		{"GITHUB.OBJ.K", "String:v"},
41		{"github.OBJ.k", "String:v"},
42		{"github.nope.deeper", "Null:"},
43		// Filters.
44		{"toJSON(github.obj.*.a)", "String:[\n  1\n]"},
45		{"toJSON(github.commits.*.id)", "String:[\n  \"1\",\n  \"2\"\n]"},
46		{"toJSON(github.commits.*.*)", "String:[\n  \"1\",\n  \"2\",\n  1,\n  3\n]"},
47		{"toJSON(github.nested.*.*)", "String:[\n  1,\n  2,\n  3\n]"},
48		{"toJSON(github.nested.*[0])", "String:[\n  1,\n  3\n]"},
49		{"github.commits.* == github.commits.*", "Boolean:false"},
50		{"github.obj.* == github.obj.*", "Boolean:false"},
51		{"github.commits == github.commits", "Boolean:true"},
52		{"github.obj == github.obj", "Boolean:true"},
53		// Comparison.
54		{"null < 1", "Boolean:true"},
55		{"null > -1", "Boolean:true"},
56		{"null == 0", "Boolean:true"},
57		{"null < true", "Boolean:true"},
58		{"null < ''", "Boolean:false"},
59		{"null == ''", "Boolean:true"},
60		{"NaN == NaN", "Boolean:false"},
61		{"NaN != NaN", "Boolean:true"},
62		{"NaN < 1", "Boolean:false"},
63		{"true > false", "Boolean:true"},
64		{"true > 0", "Boolean:true"},
65		// && and || return an operand.
66		{"'' || 'x'", "String:x"},
67		{"'a' && 'b'", "String:b"},
68		{"0 && 'x'", "Number:0"},
69		{"null || false", "Boolean:false"},
70		// White space may surround "." and "[" and sit between a function name
71		// and its "(" (the lexer looks ahead past it).
72		{"github . obj . K", "String:v"},
73		{"github.obj.\nK", "String:v"},
74		{"github.obj .K", "String:v"},
75		{"github [ 'obj' ].K", "String:v"},
76		{"toJSON (1)", "String:1"},
77		// White space is Char.IsWhiteSpace: U+00A0, U+000B and U+0085 are skipped.
78		{"1\u00a0", "Number:1"},
79		{"\u00a01", "Number:1"},
80		{"\v1", "Number:1"},
81		{"'a'\u0085", "String:a"},
82	}
83	for _, tc := range tests {
84		node, err := Parse(tc.src, []string{"github"}, nil)
85		if err != nil {
86			t.Errorf("Parse(%q): %v", tc.src, err)
87			continue
88		}
89		v, err := Evaluate(context.Background(), node, EvalContext{Contexts: ctx})
90		if err != nil {
91			t.Errorf("Evaluate(%q): %v", tc.src, err)
92			continue
93		}
94		if got := v.Kind().String() + ":" + CoerceString(v); got != tc.want {
95			t.Errorf("%s = %q, want %q", tc.src, got, tc.want)
96		}
97	}
98
99	// Parse errors.
100	for _, tc := range []struct{ src, want string }{
101		{"contains('a','b',)", "Unexpected symbol: ')'. Located at position 18 within expression: contains('a','b',)"},
102		{"contains(,'a')", "Unexpected symbol: ','. Located at position 10 within expression: contains(,'a')"},
103		{"[1] == [1]", "Unexpected symbol: '['. Located at position 1 within expression: [1] == [1]"},
104		{"1 \u00a0 2", "Unexpected symbol: '2'. Located at position 5 within expression: 1 \u00a0 2"},
105	} {
106		if _, err := Parse(tc.src, []string{"github"}, nil); err == nil || err.Error() != tc.want {
107			t.Errorf("Parse(%q) = %v, want %q", tc.src, err, tc.want)
108		}
109	}
110
111	// An empty expression is not an error and has no tree.
112	for _, src := range []string{"", "   ", "\n", "\u00a0"} {
113		if n, err := Parse(src, nil, nil); n != nil || err != nil {
114			t.Errorf("Parse(%q) = %v, %v; want nil, nil", src, n, err)
115		}
116	}
117}
118
119// BenchmarkParseLong parses an expression of the maximum length made of many
120// short tokens spread over many lines: building the position of every token
121// must not rescan the expression.
122func BenchmarkParseLong(b *testing.B) {
123	src := strings.Repeat("1 ||\n", MaxExpressionLength/5-1) + "1"
124	if len(src) > MaxExpressionLength {
125		b.Fatal(len(src))
126	}
127	b.SetBytes(int64(len(src)))
128	for i := 0; i < b.N; i++ {
129		if _, err := Parse(src, nil, nil); err != nil {
130			b.Fatal(err)
131		}
132	}
133}
134
135func TestPositionLines(t *testing.T) {
136	src := "a\n\nbc d\n  e"
137	p := &csParser{units: utf16.Encode([]rune(src))}
138	for _, tc := range []struct{ idx, line, col int }{{0, 0, 0}, {1, 0, 1}, {2, 1, 0}, {3, 2, 0}, {5, 2, 2}, {7, 2, 4}, {8, 3, 0}, {10, 3, 2}, {len(src), 3, 3}} {
139		if got := p.position(tc.idx); got.Line != tc.line || got.Column != tc.col {
140			t.Errorf("position(%d) = %v, want %d:%d", tc.idx, got, tc.line, tc.col)
141		}
142	}
143}

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