turbo-editors/turbo-golopublic Fork 0
dbc7ac5b5839e9bdd9639c48a17d1a63d181e7eb
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-golo.git
git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

reference_test.go · 199 lines · 7.2 KBGo Blame HistoryRaw
📦 Turbo Golo d710c1b k33g yesterday1package gololang_test
2
3import (
4 "os"
5 "strings"
6 "testing"
7
8 "rickub.com/turbo-editors/turbo-core/syntax"
9
10 "rickub.com/turbo-editors/turbo-golo/internal/gololang"
11)
12
13// TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the
14// scanner. Every row of its Golo table that no other test here covers is
15// checked, so a reference claim and the code cannot drift apart quietly.
16//
17// The last rows document *limitations* rather than features — a constructor
18// read as a type, a digit separator that is not one, a keyword after a colon.
19// The reference says each of those in so many words, and these rows are what
20// stops somebody "fixing" one without also fixing the sentence.
21func TestTheLanguagesReferenceIsTrue(t *testing.T) {
22 tests := []struct {
23 src string
24 word string
25 want syntax.Class
26 }{
27 // Keywords the reference lists that no other test exercises.
28 {"augmentation Named = {", "augmentation", syntax.ClassKeyword},
29 {"await task", "await", syntax.ClassKeyword},
30 {"case x {", "case", syntax.ClassKeyword},
31 {"x isnt null", "isnt", syntax.ClassKeyword},
32 {"spawn { work() }", "spawn", syntax.ClassKeyword},
33 {"augment Dog with Runnable", "with", syntax.ClassKeyword},
34 {"while running {", "while", syntax.ClassKeyword},
35 {"otherwise 0", "otherwise", syntax.ClassKeyword},
36
37 // Constants and builtins the reference names.
38 {"let x = false", "false", syntax.ClassConstant},
39 {"print(x)", "print", syntax.ClassBuiltin},
40 {"str(x)", "str", syntax.ClassBuiltin},
41 {"len(xs)", "len", syntax.ClassBuiltin},
42 {"map[[1, 2]]", "map", syntax.ClassBuiltin},
43 {"set[1, 2]", "set", syntax.ClassBuiltin},
44 {"array[1, 2]", "array", syntax.ClassBuiltin},
45 {"vector[1, 2]", "vector", syntax.ClassBuiltin},
46 {"readFile(path)", "readFile", syntax.ClassBuiltin},
47 {"toJSON(x)", "toJSON", syntax.ClassBuiltin},
48 {"fromJSON(x)", "fromJSON", syntax.ClassBuiltin},
49 {"httpGet(url)", "httpGet", syntax.ClassBuiltin},
50 {"DynamicObject()", "DynamicObject", syntax.ClassBuiltin},
51
52 // Types, by convention.
53 {"let s = Some(1)", "Some", syntax.ClassType},
54 {"let r = Result_Failure(\"no\")", "Result_Failure", syntax.ClassType},
55 {"import java.util.List", "java.util.List", syntax.ClassType},
56
57 // Names.
58 {"let été = 1", "été", syntax.ClassIdentifier},
59 {"let 名前 = 1", "名前", syntax.ClassIdentifier},
60 {"function 🚀launch = {", "🚀launch", syntax.ClassFunction},
61
62 // Literals and numbers, one per item of the reference's list.
63 {"let s = \"a\\x41b\"", "\"a\\x41b\"", syntax.ClassString},
64 {"let s = \"\"\"raw\"\"\"", "\"\"\"raw\"\"\"", syntax.ClassString},
65 {"let c = '\\''", "'\\''", syntax.ClassChar},
66 {"let n = 2E10", "2E10", syntax.ClassNumber},
67 {"let n = 3.14F", "3.14F", syntax.ClassNumber},
68 {"let n = 2.0f", "2.0f", syntax.ClassNumber},
69 {"let n = 1e", "1e", syntax.ClassNumber},
70
71 // Comments, operators, punctuation.
72 {"#!/usr/bin/env golo", "#!/usr/bin/env golo", syntax.ClassComment},
73 {"---- one line ----", "---- one line ----", syntax.ClassComment},
74 {"a >= b", ">=", syntax.ClassOperator},
75 {"a == b", "==", syntax.ClassOperator},
76 {"f(a...)", "...", syntax.ClassOperator},
77 {"x; y", ";", syntax.ClassPunctuation},
78
79 // The documented limitations.
80 {"let n = 1_000", "1", syntax.ClassNumber},
81 {"let n = 0b1010", "b1010", syntax.ClassIdentifier},
82 {"let n = 0o17", "o17", syntax.ClassIdentifier},
83 {"let n = .5", ".", syntax.ClassPunctuation},
84 {"x = 42l", "l", syntax.ClassIdentifier},
85 {"a --- b", "---", syntax.ClassOperator},
86 {"obj: match()", "match", syntax.ClassKeyword},
87 {"let c = Circle(1.0)", "Circle", syntax.ClassType},
88 {"let Count = 1", "Count", syntax.ClassType},
89 }
90
91 for _, test := range tests {
92 t.Run(test.word, func(t *testing.T) {
93 index := len([]rune(test.src[:strings.Index(test.src, test.word)]))
94 if strings.Index(test.src, test.word) < 0 {
95 t.Fatalf("%q not in %q", test.word, test.src)
96 }
97 got, ok := classAt(gololang.Highlight(test.src), 0, index)
98 if !ok || got != test.want {
99 t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want)
100 }
101 })
102 }
103}
104
105// classAt returns the class covering one rune column of one line.
106func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) {
107 if line < 0 || line >= len(spans) {
108 return 0, false
109 }
110 for _, s := range spans[line] {
111 if col >= s.Start && col < s.End {
112 return s.Class, true
113 }
114 }
115 return 0, false
116}
117
118// The reference's Classes table says Golo produces no heading, tag, attribute,
119// emphasis or link. That is a claim about every span the scanner can ever
120// emit, so it is checked over a file that uses every construct the Golo table
121// names.
122func TestTheScannerNeverProducesAMarkupClass(t *testing.T) {
123 const src = "#!/usr/bin/env golo\n" +
124 "module demo.Tour\n" +
125 "import gololang.Errors\n" +
126 "---- a block\ncomment ----\n" +
127 "struct Point = { x, y }\n" +
128 "union Shape = { Circle = { radius } }\n" +
129 "augment Shape$Circle { function d = |this| -> this: radius() * 2 }\n" +
130 "function main = |args| {\n" +
131 " let n = 42L + 3.14F + 1.5e-3\n" +
132 " let s = \"a \\\"b\\\" \\x41\" + \"\"\"multi\n\"quoted\"\n\"\"\" + 'c'\n" +
133 " let 😀 = list[1..3, x...]\n" +
134 " let ok = p?: x() orIfNull 0 # trailing\n" +
135 " let broken = \"unterminated\n" +
136 "}\n"
137
138 forbidden := map[syntax.Class]bool{
139 syntax.ClassHeading: true,
140 syntax.ClassTag: true,
141 syntax.ClassAttribute: true,
142 syntax.ClassEmphasis: true,
143 syntax.ClassLink: true,
144 }
145 for line, spans := range gololang.Highlight(src) {
146 for _, span := range spans {
147 if forbidden[span.Class] {
148 t.Errorf("line %d holds a %s span at %d; the reference says Golo produces none", line+1, span.Class, span.Start)
149 }
150 }
151 }
152}
153
154// Every keyword the reference's table lists is one the scanner really treats as
155// a keyword, and every keyword the scanner knows is in the table. The table is
156// read out of the page rather than repeated here, so a word added to one and
157// not the other is what fails.
158func TestEveryKeywordTheReferenceListsIsAKeyword(t *testing.T) {
159 for _, page := range []string{"../../docs/en/reference/languages.md", "../../docs/fr/reference/languages.md"} {
160 raw, err := os.ReadFile(page)
161 if err != nil {
162 t.Fatalf("reading %s: %v", page, err)
163 }
164
165 words := keywordRowOf(t, string(raw))
166 if len(words) != len(gololang.Keywords()) {
167 t.Errorf("%s lists %d keywords; the scanner knows %d", page, len(words), len(gololang.Keywords()))
168 }
169 for _, word := range words {
170 src := word + " x"
171 got, ok := classAt(gololang.Highlight(src), 0, 0)
172 if !ok || got != syntax.ClassKeyword {
173 t.Errorf("%s says %q is a keyword; the scanner colours it %v", page, word, got)
174 }
175 }
176 }
177}
178
179// keywordRowOf pulls the back-quoted words out of the reference's keyword row.
180func keywordRowOf(t *testing.T, page string) []string {
181 t.Helper()
182
183 for _, line := range strings.Split(page, "\n") {
184 if !strings.Contains(line, "`function`") || !strings.Contains(line, "`orIfNull`") {
185 continue
186 }
187 var words []string
188 for i, part := range strings.Split(line, "`") {
189 if i%2 == 1 {
190 words = append(words, part)
191 }
192 }
193 // The row ends "| keyword |" in English and "| mot-clé |" in French;
194 // the back-quoted words are the same in both.
195 return words
196 }
197 t.Fatal("no keyword row found in the reference")
198 return nil
199}