test(expr): replay the languageservices fixtures like xlang.test.ts
- commit
- 9588af7799711387ab70d692f07f6ca5bfc5a074
- author
- brice · · 2026-10-12 17:10 UTC
- parent
- 922360b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +276 -0
A expr/xlang_test.go +276 -0
| @@ -0,0 +1,276 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "encoding/json" | |
| 8 | + | "errors" | |
| 9 | + | "fmt" | |
| 10 | + | "math" | |
| 11 | + | "os" | |
| 12 | + | "path/filepath" | |
| 13 | + | "sort" | |
| 14 | + | "strconv" | |
| 15 | + | "strings" | |
| 16 | + | "testing" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // The cross-language fixtures of actions/languageservices | |
| 20 | + | // (testdata/languageservices, see SOURCE there) are shared by the TypeScript | |
| 21 | + | // and the C# implementations. This runner reproduces | |
| 22 | + | // expressions/src/xlang.test.ts of that repository: | |
| 23 | + | // | |
| 24 | + | // - contexts are revived into expression data, and their names are the | |
| 25 | + | // context names given to the parser; no extension functions are declared; | |
| 26 | + | // - a lexing error is compared with the error message of the lexer; | |
| 27 | + | // - a parsing error with "<message>. Located at position <column+1> within | |
| 28 | + | // expression: <expr>"; | |
| 29 | + | // - an evaluation error with "<message>. Located within expression: <expr>"; | |
| 30 | + | // - error expectations match by substring, result kinds by name, result | |
| 31 | + | // values by their JSON.stringify form (NaN and Infinity are null). | |
| 32 | + | // | |
| 33 | + | // The only deliberate difference from the TypeScript runner is that cases | |
| 34 | + | // marked options.skip = ["typescript"] are run too: they are skipped there | |
| 35 | + | // because the TypeScript port lacks a feature, not because the fixture is | |
| 36 | + | // wrong. Fixtures that this package cannot satisfy are listed in | |
| 37 | + | // xlangKnownDifferences with the reason. | |
| 38 | + | ||
| 39 | + | type xlangResult struct { | |
| 40 | + | Kind string `json:"kind"` | |
| 41 | + | Value json.RawMessage `json:"value"` | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | type xlangErr struct { | |
| 45 | + | Kind string `json:"kind"` | |
| 46 | + | Value string `json:"value"` | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | type xlangCase struct { | |
| 50 | + | Expr string `json:"expr"` | |
| 51 | + | Result *xlangResult `json:"result"` | |
| 52 | + | Err *xlangErr `json:"err"` | |
| 53 | + | Contexts json.RawMessage `json:"contexts"` | |
| 54 | + | Options struct { | |
| 55 | + | Skip []string `json:"skip"` | |
| 56 | + | } `json:"options"` | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // xlangKnownDifferences maps "file: group (expr prefix)" keys to the reason | |
| 60 | + | // the fixture is not satisfied. Keep it empty if possible. | |
| 61 | + | var xlangKnownDifferences = map[string]string{ | |
| 62 | + | "syntax-errors: memory-errors#0": "memory accounting differs from the runner's EvaluationMemory", | |
| 63 | + | "syntax-errors: parsing-errors#25": "position is the column in the line, the runner reports the index in the expression", | |
| 64 | + | "syntax-errors: parsing-errors#26": "position is the column in the line, the runner reports the index in the expression", | |
| 65 | + | "syntax-errors: parsing-errors#27": "position is the column in the line, the runner reports the index in the expression", | |
| 66 | + | "syntax-errors: parsing-errors#28": "position is the column in the line, the runner reports the index in the expression", | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // xlangKind is the kind name the fixtures use. | |
| 70 | + | func xlangKind(v Value) string { | |
| 71 | + | switch v.Kind() { | |
| 72 | + | case KindArray: | |
| 73 | + | return "Array" | |
| 74 | + | case KindBoolean: | |
| 75 | + | return "Boolean" | |
| 76 | + | case KindNull: | |
| 77 | + | return "Null" | |
| 78 | + | case KindNumber: | |
| 79 | + | return "Number" | |
| 80 | + | case KindString: | |
| 81 | + | return "String" | |
| 82 | + | } | |
| 83 | + | return "Object" | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // jsonStringify mimics JSON.stringify(v) (no indentation) for a value. | |
| 87 | + | func jsonStringify(b *strings.Builder, v Value) { | |
| 88 | + | switch x := v.(type) { | |
| 89 | + | case nil, Null: | |
| 90 | + | b.WriteString("null") | |
| 91 | + | case Boolean: | |
| 92 | + | b.WriteString(strconv.FormatBool(bool(x))) | |
| 93 | + | case Number: | |
| 94 | + | f := float64(x) | |
| 95 | + | if math.IsNaN(f) || math.IsInf(f, 0) { | |
| 96 | + | b.WriteString("null") | |
| 97 | + | } else { | |
| 98 | + | b.WriteString(jsNumberToString(f)) | |
| 99 | + | } | |
| 100 | + | case String: | |
| 101 | + | s, _ := json.Marshal(string(x)) | |
| 102 | + | b.Write(s) | |
| 103 | + | case *Array: | |
| 104 | + | b.WriteByte('[') | |
| 105 | + | for i, it := range x.Items() { | |
| 106 | + | if i > 0 { | |
| 107 | + | b.WriteByte(',') | |
| 108 | + | } | |
| 109 | + | jsonStringify(b, it) | |
| 110 | + | } | |
| 111 | + | b.WriteByte(']') | |
| 112 | + | case *Dictionary: | |
| 113 | + | b.WriteByte('{') | |
| 114 | + | for i, p := range x.Pairs() { | |
| 115 | + | if i > 0 { | |
| 116 | + | b.WriteByte(',') | |
| 117 | + | } | |
| 118 | + | k, _ := json.Marshal(p.Key) | |
| 119 | + | b.Write(k) | |
| 120 | + | b.WriteByte(':') | |
| 121 | + | jsonStringify(b, p.Value) | |
| 122 | + | } | |
| 123 | + | b.WriteByte('}') | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func stringifyValue(v Value) string { | |
| 128 | + | var b strings.Builder | |
| 129 | + | jsonStringify(&b, v) | |
| 130 | + | return b.String() | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // xlangContexts returns the revived contexts and their names, sorted. | |
| 134 | + | func xlangContexts(tb testing.TB, raw json.RawMessage) (map[string]Value, []string) { | |
| 135 | + | tb.Helper() | |
| 136 | + | ctx := map[string]Value{} | |
| 137 | + | if len(raw) == 0 { | |
| 138 | + | return ctx, nil | |
| 139 | + | } | |
| 140 | + | var m map[string]json.RawMessage | |
| 141 | + | if err := json.Unmarshal(raw, &m); err != nil { | |
| 142 | + | tb.Fatalf("contexts: %v", err) | |
| 143 | + | } | |
| 144 | + | var names []string | |
| 145 | + | for k, r := range m { | |
| 146 | + | ctx[k] = mustJSON(tb, string(r)) | |
| 147 | + | names = append(names, k) | |
| 148 | + | } | |
| 149 | + | sort.Strings(names) | |
| 150 | + | return ctx, names | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // runXlangCase returns "" when the case passes, else the reason it fails. | |
| 154 | + | func runXlangCase(tb testing.TB, c xlangCase) string { | |
| 155 | + | ctxValues, names := xlangContexts(tb, c.Contexts) | |
| 156 | + | expectErr := func(kind string) bool { return c.Err != nil && c.Err.Kind == kind } | |
| 157 | + | ||
| 158 | + | // Lex. | |
| 159 | + | _, err := Lex(c.Expr) | |
| 160 | + | if err != nil { | |
| 161 | + | if expectErr("lexing") { | |
| 162 | + | if strings.Contains(err.Error(), c.Err.Value) { | |
| 163 | + | return "" | |
| 164 | + | } | |
| 165 | + | return fmt.Sprintf("lexing error %q does not contain %q", err.Error(), c.Err.Value) | |
| 166 | + | } | |
| 167 | + | return "unexpected error lexing expression: " + err.Error() | |
| 168 | + | } | |
| 169 | + | if expectErr("lexing") { | |
| 170 | + | return "expected error lexing expression, but got none" | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | // Parse. | |
| 174 | + | node, err := Parse(c.Expr, names, nil) | |
| 175 | + | if err != nil { | |
| 176 | + | if expectErr("parsing") { | |
| 177 | + | var pe *Error | |
| 178 | + | if !errors.As(err, &pe) { | |
| 179 | + | return fmt.Sprintf("parse error has type %T", err) | |
| 180 | + | } | |
| 181 | + | full := fmt.Sprintf("%s. Located at position %d within expression: %s", pe.Message, pe.Pos.Column+1, c.Expr) | |
| 182 | + | if strings.Contains(full, c.Err.Value) { | |
| 183 | + | return "" | |
| 184 | + | } | |
| 185 | + | return fmt.Sprintf("parsing error %q does not contain %q", full, c.Err.Value) | |
| 186 | + | } | |
| 187 | + | return "unexpected error parsing expression: " + err.Error() | |
| 188 | + | } | |
| 189 | + | if expectErr("parsing") { | |
| 190 | + | return "expected error parsing expression, but got none" | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | // Evaluate. An empty expression is null. | |
| 194 | + | var result Value = Null{} | |
| 195 | + | if node != nil { | |
| 196 | + | result, err = Evaluate(context.Background(), node, EvalContext{Contexts: ctxValues}) | |
| 197 | + | } | |
| 198 | + | if err != nil { | |
| 199 | + | if expectErr("evaluation") { | |
| 200 | + | full := err.Error() + ". Located within expression: " + c.Expr | |
| 201 | + | if strings.Contains(full, c.Err.Value) { | |
| 202 | + | return "" | |
| 203 | + | } | |
| 204 | + | return fmt.Sprintf("evaluation error %q does not contain %q", full, c.Err.Value) | |
| 205 | + | } | |
| 206 | + | return "unexpected error evaluating expression: " + err.Error() | |
| 207 | + | } | |
| 208 | + | if expectErr("evaluation") { | |
| 209 | + | return "expected error evaluating expression, but got none" | |
| 210 | + | } | |
| 211 | + | if c.Result == nil { | |
| 212 | + | return "fixture has neither result nor err" | |
| 213 | + | } | |
| 214 | + | if k := xlangKind(result); k != c.Result.Kind { | |
| 215 | + | return fmt.Sprintf("kind: got %s, want %s", k, c.Result.Kind) | |
| 216 | + | } | |
| 217 | + | want := stringifyValue(mustJSON(tb, string(c.Result.Value))) | |
| 218 | + | if got := stringifyValue(result); got != want { | |
| 219 | + | return fmt.Sprintf("value: got %s, want %s", got, want) | |
| 220 | + | } | |
| 221 | + | return "" | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | func TestLanguageServicesFixtures(t *testing.T) { | |
| 225 | + | files, err := filepath.Glob("testdata/languageservices/*.json") | |
| 226 | + | if err != nil || len(files) != 28 { | |
| 227 | + | t.Fatalf("fixture files: %d, %v", len(files), err) | |
| 228 | + | } | |
| 229 | + | var total, skippedByTS, known, failed int | |
| 230 | + | for _, file := range files { | |
| 231 | + | name := strings.TrimSuffix(filepath.Base(file), ".json") | |
| 232 | + | data, err := os.ReadFile(file) | |
| 233 | + | if err != nil { | |
| 234 | + | t.Fatal(err) | |
| 235 | + | } | |
| 236 | + | var groups map[string][]xlangCase | |
| 237 | + | if err := json.Unmarshal(data, &groups); err != nil { | |
| 238 | + | t.Fatalf("%s: %v", file, err) | |
| 239 | + | } | |
| 240 | + | groupNames := make([]string, 0, len(groups)) | |
| 241 | + | for g := range groups { | |
| 242 | + | groupNames = append(groupNames, g) | |
| 243 | + | } | |
| 244 | + | sort.Strings(groupNames) | |
| 245 | + | for _, g := range groupNames { | |
| 246 | + | for i, c := range groups[g] { | |
| 247 | + | total++ | |
| 248 | + | for _, s := range c.Options.Skip { | |
| 249 | + | if s == "typescript" { | |
| 250 | + | skippedByTS++ | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | key := name + ": " + g | |
| 254 | + | label := fmt.Sprintf("%s/%s/%d", name, g, i) | |
| 255 | + | reason, isKnown := xlangKnownDifferences[fmt.Sprintf("%s#%d", key, i)] | |
| 256 | + | t.Run(label, func(t *testing.T) { | |
| 257 | + | msg := runXlangCase(t, c) | |
| 258 | + | switch { | |
| 259 | + | case msg == "" && isKnown: | |
| 260 | + | t.Errorf("listed as a known difference (%s) but passes", reason) | |
| 261 | + | case msg != "" && isKnown: | |
| 262 | + | known++ | |
| 263 | + | t.Logf("known difference (%s): %s [%s]", reason, msg, c.Expr) | |
| 264 | + | case msg != "": | |
| 265 | + | failed++ | |
| 266 | + | t.Errorf("%s [%s]", msg, c.Expr) | |
| 267 | + | } | |
| 268 | + | }) | |
| 269 | + | } | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | t.Logf("%d fixtures in %d files (%d skipped by the TypeScript runner and run here), %d known differences, %d failures", total, len(files), skippedByTS, known, failed) | |
| 273 | + | if total != 1020 { | |
| 274 | + | t.Errorf("expected 1020 fixtures, found %d", total) | |
| 275 | + | } | |
| 276 | + | } | |
| @@ -0,0 +1,276 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "encoding/json" | |||
| 8 | + | "errors" | |||
| 9 | + | "fmt" | |||
| 10 | + | "math" | |||
| 11 | + | "os" | |||
| 12 | + | "path/filepath" | |||
| 13 | + | "sort" | |||
| 14 | + | "strconv" | |||
| 15 | + | "strings" | |||
| 16 | + | "testing" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | // The cross-language fixtures of actions/languageservices | |||
| 20 | + | // (testdata/languageservices, see SOURCE there) are shared by the TypeScript | |||
| 21 | + | // and the C# implementations. This runner reproduces | |||
| 22 | + | // expressions/src/xlang.test.ts of that repository: | |||
| 23 | + | // | |||
| 24 | + | // - contexts are revived into expression data, and their names are the | |||
| 25 | + | // context names given to the parser; no extension functions are declared; | |||
| 26 | + | // - a lexing error is compared with the error message of the lexer; | |||
| 27 | + | // - a parsing error with "<message>. Located at position <column+1> within | |||
| 28 | + | // expression: <expr>"; | |||
| 29 | + | // - an evaluation error with "<message>. Located within expression: <expr>"; | |||
| 30 | + | // - error expectations match by substring, result kinds by name, result | |||
| 31 | + | // values by their JSON.stringify form (NaN and Infinity are null). | |||
| 32 | + | // | |||
| 33 | + | // The only deliberate difference from the TypeScript runner is that cases | |||
| 34 | + | // marked options.skip = ["typescript"] are run too: they are skipped there | |||
| 35 | + | // because the TypeScript port lacks a feature, not because the fixture is | |||
| 36 | + | // wrong. Fixtures that this package cannot satisfy are listed in | |||
| 37 | + | // xlangKnownDifferences with the reason. | |||
| 38 | + | ||||
| 39 | + | type xlangResult struct { | |||
| 40 | + | Kind string `json:"kind"` | |||
| 41 | + | Value json.RawMessage `json:"value"` | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | type xlangErr struct { | |||
| 45 | + | Kind string `json:"kind"` | |||
| 46 | + | Value string `json:"value"` | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | type xlangCase struct { | |||
| 50 | + | Expr string `json:"expr"` | |||
| 51 | + | Result *xlangResult `json:"result"` | |||
| 52 | + | Err *xlangErr `json:"err"` | |||
| 53 | + | Contexts json.RawMessage `json:"contexts"` | |||
| 54 | + | Options struct { | |||
| 55 | + | Skip []string `json:"skip"` | |||
| 56 | + | } `json:"options"` | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // xlangKnownDifferences maps "file: group (expr prefix)" keys to the reason | |||
| 60 | + | // the fixture is not satisfied. Keep it empty if possible. | |||
| 61 | + | var xlangKnownDifferences = map[string]string{ | |||
| 62 | + | "syntax-errors: memory-errors#0": "memory accounting differs from the runner's EvaluationMemory", | |||
| 63 | + | "syntax-errors: parsing-errors#25": "position is the column in the line, the runner reports the index in the expression", | |||
| 64 | + | "syntax-errors: parsing-errors#26": "position is the column in the line, the runner reports the index in the expression", | |||
| 65 | + | "syntax-errors: parsing-errors#27": "position is the column in the line, the runner reports the index in the expression", | |||
| 66 | + | "syntax-errors: parsing-errors#28": "position is the column in the line, the runner reports the index in the expression", | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // xlangKind is the kind name the fixtures use. | |||
| 70 | + | func xlangKind(v Value) string { | |||
| 71 | + | switch v.Kind() { | |||
| 72 | + | case KindArray: | |||
| 73 | + | return "Array" | |||
| 74 | + | case KindBoolean: | |||
| 75 | + | return "Boolean" | |||
| 76 | + | case KindNull: | |||
| 77 | + | return "Null" | |||
| 78 | + | case KindNumber: | |||
| 79 | + | return "Number" | |||
| 80 | + | case KindString: | |||
| 81 | + | return "String" | |||
| 82 | + | } | |||
| 83 | + | return "Object" | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // jsonStringify mimics JSON.stringify(v) (no indentation) for a value. | |||
| 87 | + | func jsonStringify(b *strings.Builder, v Value) { | |||
| 88 | + | switch x := v.(type) { | |||
| 89 | + | case nil, Null: | |||
| 90 | + | b.WriteString("null") | |||
| 91 | + | case Boolean: | |||
| 92 | + | b.WriteString(strconv.FormatBool(bool(x))) | |||
| 93 | + | case Number: | |||
| 94 | + | f := float64(x) | |||
| 95 | + | if math.IsNaN(f) || math.IsInf(f, 0) { | |||
| 96 | + | b.WriteString("null") | |||
| 97 | + | } else { | |||
| 98 | + | b.WriteString(jsNumberToString(f)) | |||
| 99 | + | } | |||
| 100 | + | case String: | |||
| 101 | + | s, _ := json.Marshal(string(x)) | |||
| 102 | + | b.Write(s) | |||
| 103 | + | case *Array: | |||
| 104 | + | b.WriteByte('[') | |||
| 105 | + | for i, it := range x.Items() { | |||
| 106 | + | if i > 0 { | |||
| 107 | + | b.WriteByte(',') | |||
| 108 | + | } | |||
| 109 | + | jsonStringify(b, it) | |||
| 110 | + | } | |||
| 111 | + | b.WriteByte(']') | |||
| 112 | + | case *Dictionary: | |||
| 113 | + | b.WriteByte('{') | |||
| 114 | + | for i, p := range x.Pairs() { | |||
| 115 | + | if i > 0 { | |||
| 116 | + | b.WriteByte(',') | |||
| 117 | + | } | |||
| 118 | + | k, _ := json.Marshal(p.Key) | |||
| 119 | + | b.Write(k) | |||
| 120 | + | b.WriteByte(':') | |||
| 121 | + | jsonStringify(b, p.Value) | |||
| 122 | + | } | |||
| 123 | + | b.WriteByte('}') | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func stringifyValue(v Value) string { | |||
| 128 | + | var b strings.Builder | |||
| 129 | + | jsonStringify(&b, v) | |||
| 130 | + | return b.String() | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // xlangContexts returns the revived contexts and their names, sorted. | |||
| 134 | + | func xlangContexts(tb testing.TB, raw json.RawMessage) (map[string]Value, []string) { | |||
| 135 | + | tb.Helper() | |||
| 136 | + | ctx := map[string]Value{} | |||
| 137 | + | if len(raw) == 0 { | |||
| 138 | + | return ctx, nil | |||
| 139 | + | } | |||
| 140 | + | var m map[string]json.RawMessage | |||
| 141 | + | if err := json.Unmarshal(raw, &m); err != nil { | |||
| 142 | + | tb.Fatalf("contexts: %v", err) | |||
| 143 | + | } | |||
| 144 | + | var names []string | |||
| 145 | + | for k, r := range m { | |||
| 146 | + | ctx[k] = mustJSON(tb, string(r)) | |||
| 147 | + | names = append(names, k) | |||
| 148 | + | } | |||
| 149 | + | sort.Strings(names) | |||
| 150 | + | return ctx, names | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // runXlangCase returns "" when the case passes, else the reason it fails. | |||
| 154 | + | func runXlangCase(tb testing.TB, c xlangCase) string { | |||
| 155 | + | ctxValues, names := xlangContexts(tb, c.Contexts) | |||
| 156 | + | expectErr := func(kind string) bool { return c.Err != nil && c.Err.Kind == kind } | |||
| 157 | + | ||||
| 158 | + | // Lex. | |||
| 159 | + | _, err := Lex(c.Expr) | |||
| 160 | + | if err != nil { | |||
| 161 | + | if expectErr("lexing") { | |||
| 162 | + | if strings.Contains(err.Error(), c.Err.Value) { | |||
| 163 | + | return "" | |||
| 164 | + | } | |||
| 165 | + | return fmt.Sprintf("lexing error %q does not contain %q", err.Error(), c.Err.Value) | |||
| 166 | + | } | |||
| 167 | + | return "unexpected error lexing expression: " + err.Error() | |||
| 168 | + | } | |||
| 169 | + | if expectErr("lexing") { | |||
| 170 | + | return "expected error lexing expression, but got none" | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | // Parse. | |||
| 174 | + | node, err := Parse(c.Expr, names, nil) | |||
| 175 | + | if err != nil { | |||
| 176 | + | if expectErr("parsing") { | |||
| 177 | + | var pe *Error | |||
| 178 | + | if !errors.As(err, &pe) { | |||
| 179 | + | return fmt.Sprintf("parse error has type %T", err) | |||
| 180 | + | } | |||
| 181 | + | full := fmt.Sprintf("%s. Located at position %d within expression: %s", pe.Message, pe.Pos.Column+1, c.Expr) | |||
| 182 | + | if strings.Contains(full, c.Err.Value) { | |||
| 183 | + | return "" | |||
| 184 | + | } | |||
| 185 | + | return fmt.Sprintf("parsing error %q does not contain %q", full, c.Err.Value) | |||
| 186 | + | } | |||
| 187 | + | return "unexpected error parsing expression: " + err.Error() | |||
| 188 | + | } | |||
| 189 | + | if expectErr("parsing") { | |||
| 190 | + | return "expected error parsing expression, but got none" | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | // Evaluate. An empty expression is null. | |||
| 194 | + | var result Value = Null{} | |||
| 195 | + | if node != nil { | |||
| 196 | + | result, err = Evaluate(context.Background(), node, EvalContext{Contexts: ctxValues}) | |||
| 197 | + | } | |||
| 198 | + | if err != nil { | |||
| 199 | + | if expectErr("evaluation") { | |||
| 200 | + | full := err.Error() + ". Located within expression: " + c.Expr | |||
| 201 | + | if strings.Contains(full, c.Err.Value) { | |||
| 202 | + | return "" | |||
| 203 | + | } | |||
| 204 | + | return fmt.Sprintf("evaluation error %q does not contain %q", full, c.Err.Value) | |||
| 205 | + | } | |||
| 206 | + | return "unexpected error evaluating expression: " + err.Error() | |||
| 207 | + | } | |||
| 208 | + | if expectErr("evaluation") { | |||
| 209 | + | return "expected error evaluating expression, but got none" | |||
| 210 | + | } | |||
| 211 | + | if c.Result == nil { | |||
| 212 | + | return "fixture has neither result nor err" | |||
| 213 | + | } | |||
| 214 | + | if k := xlangKind(result); k != c.Result.Kind { | |||
| 215 | + | return fmt.Sprintf("kind: got %s, want %s", k, c.Result.Kind) | |||
| 216 | + | } | |||
| 217 | + | want := stringifyValue(mustJSON(tb, string(c.Result.Value))) | |||
| 218 | + | if got := stringifyValue(result); got != want { | |||
| 219 | + | return fmt.Sprintf("value: got %s, want %s", got, want) | |||
| 220 | + | } | |||
| 221 | + | return "" | |||
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | func TestLanguageServicesFixtures(t *testing.T) { | |||
| 225 | + | files, err := filepath.Glob("testdata/languageservices/*.json") | |||
| 226 | + | if err != nil || len(files) != 28 { | |||
| 227 | + | t.Fatalf("fixture files: %d, %v", len(files), err) | |||
| 228 | + | } | |||
| 229 | + | var total, skippedByTS, known, failed int | |||
| 230 | + | for _, file := range files { | |||
| 231 | + | name := strings.TrimSuffix(filepath.Base(file), ".json") | |||
| 232 | + | data, err := os.ReadFile(file) | |||
| 233 | + | if err != nil { | |||
| 234 | + | t.Fatal(err) | |||
| 235 | + | } | |||
| 236 | + | var groups map[string][]xlangCase | |||
| 237 | + | if err := json.Unmarshal(data, &groups); err != nil { | |||
| 238 | + | t.Fatalf("%s: %v", file, err) | |||
| 239 | + | } | |||
| 240 | + | groupNames := make([]string, 0, len(groups)) | |||
| 241 | + | for g := range groups { | |||
| 242 | + | groupNames = append(groupNames, g) | |||
| 243 | + | } | |||
| 244 | + | sort.Strings(groupNames) | |||
| 245 | + | for _, g := range groupNames { | |||
| 246 | + | for i, c := range groups[g] { | |||
| 247 | + | total++ | |||
| 248 | + | for _, s := range c.Options.Skip { | |||
| 249 | + | if s == "typescript" { | |||
| 250 | + | skippedByTS++ | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | key := name + ": " + g | |||
| 254 | + | label := fmt.Sprintf("%s/%s/%d", name, g, i) | |||
| 255 | + | reason, isKnown := xlangKnownDifferences[fmt.Sprintf("%s#%d", key, i)] | |||
| 256 | + | t.Run(label, func(t *testing.T) { | |||
| 257 | + | msg := runXlangCase(t, c) | |||
| 258 | + | switch { | |||
| 259 | + | case msg == "" && isKnown: | |||
| 260 | + | t.Errorf("listed as a known difference (%s) but passes", reason) | |||
| 261 | + | case msg != "" && isKnown: | |||
| 262 | + | known++ | |||
| 263 | + | t.Logf("known difference (%s): %s [%s]", reason, msg, c.Expr) | |||
| 264 | + | case msg != "": | |||
| 265 | + | failed++ | |||
| 266 | + | t.Errorf("%s [%s]", msg, c.Expr) | |||
| 267 | + | } | |||
| 268 | + | }) | |||
| 269 | + | } | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | t.Logf("%d fixtures in %d files (%d skipped by the TypeScript runner and run here), %d known differences, %d failures", total, len(files), skippedByTS, known, failed) | |||
| 273 | + | if total != 1020 { | |||
| 274 | + | t.Errorf("expected 1020 fixtures, found %d", total) | |||
| 275 | + | } | |||
| 276 | + | } | |||