turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

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

dockerfile_test.go · 219 lines · 6.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 20h ago1package syntax
2
3import (
4 "strings"
5 "testing"
6)
7
8// dockerClassOf returns the class covering the first occurrence of a piece of
9// text in a Dockerfile.
10func dockerClassOf(t *testing.T, src, want string) Class {
11 t.Helper()
12
13 index := strings.Index(src, want)
14 if index < 0 {
15 t.Fatalf("%q does not appear in the source", want)
16 }
17 line := strings.Count(src[:index], "\n")
18 col := index - (strings.LastIndex(src[:index], "\n") + 1)
19
20 class, ok := classAt(Highlight(LanguageDockerfile, src), line, col)
21 if !ok {
22 t.Fatalf("no span covers %q at line %d column %d", want, line, col)
23 }
24 return class
25}
26
27func TestADockerfileIsRecognisedByItsName(t *testing.T) {
28 // It has no extension and no shebang, which is the whole reason Filenames
29 // exists.
30 for _, name := range []string{
31 "Dockerfile", "dockerfile", "Containerfile",
32 "Dockerfile.dev", "Dockerfile.prod",
33 "docker/Dockerfile", "web.dockerfile",
34 } {
35 t.Run(name, func(t *testing.T) {
36 if got := LanguageOf(name, ""); got != LanguageDockerfile {
37 t.Errorf("LanguageOf(%q) = %q, want dockerfile", name, got)
38 }
39 })
40 }
41}
42
43func TestAnUnregisteredNameIsStillPlainText(t *testing.T) {
44 // The stem rule must not become a wildcard: a Makefile is not a Dockerfile
45 // merely because nothing else claims it.
46 for _, name := range []string{"Makefile", "Justfile", "LICENSE", "dockerfiles.txt"} {
47 t.Run(name, func(t *testing.T) {
48 if got := LanguageOf(name, ""); got == LanguageDockerfile {
49 t.Errorf("LanguageOf(%q) = dockerfile", name)
50 }
51 })
52 }
53}
54
55func TestADotfileHasNoStemToMatchOn(t *testing.T) {
56 // ".gitignore" is all extension: its stem is empty, and an empty stem must
57 // match nothing rather than everything.
58 if got := LanguageOf(".gitignore", ""); got != LanguageNone {
59 t.Errorf("LanguageOf(\".gitignore\") = %q, want none", got)
60 }
61}
62
63func TestAnExtensionOutranksAName(t *testing.T) {
64 // A file called Dockerfile.md is documentation about a Dockerfile.
65 if got := LanguageOf("Dockerfile.md", ""); got != LanguageMarkdown {
66 t.Errorf("LanguageOf(\"Dockerfile.md\") = %q, want markdown", got)
67 }
68}
69
70func TestDockerfileColoursItsParts(t *testing.T) {
71 const src = `# syntax=docker/dockerfile:1
72FROM golang:1.26 AS builder
73WORKDIR /src
74COPY --from=cache /go/pkg /go/pkg
75ARG VERSION=1.0
76RUN go build -o app .
77EXPOSE 8080
78CMD ["./app"]
79`
80
81 tests := map[string]Class{
82 "# syntax=docker/dockerfile:1": ClassComment,
83 "FROM": ClassKeyword,
84 "AS": ClassKeyword,
85 "WORKDIR": ClassKeyword,
86 "--from": ClassAttribute,
87 "8080": ClassNumber,
88 `"./app"`: ClassString,
89 "golang:1.26": ClassIdentifier,
90 }
91
92 for text, want := range tests {
93 t.Run(text, func(t *testing.T) {
94 if got := dockerClassOf(t, src, text); got != want {
95 t.Errorf("%q is %v, want %v", text, got, want)
96 }
97 })
98 }
99}
100
101func TestAnInstructionIsMatchedWhateverItsCase(t *testing.T) {
102 // Docker accepts any case and real files are inconsistent about it.
103 for _, spelling := range []string{"FROM", "from", "From"} {
104 t.Run(spelling, func(t *testing.T) {
105 if got := dockerClassOf(t, spelling+" alpine\n", spelling); got != ClassKeyword {
106 t.Errorf("%q is %v, want keyword", spelling, got)
107 }
108 })
109 }
110}
111
112func TestAWordThatIsNotAnInstructionIsAnArgument(t *testing.T) {
113 // A continuation line opens with an ordinary word, and colouring it as an
114 // instruction would put a keyword in the middle of a shell command.
115 const src = "RUN apt-get update \\\n && apt-get install -y curl\n"
116
117 if got := dockerClassOf(t, src, "apt-get"); got != ClassIdentifier {
118 t.Errorf("apt-get is %v, want identifier", got)
119 }
120}
121
122func TestTheLineContinuationIsColouredAsStructure(t *testing.T) {
123 const src = "RUN a \\\n b\n"
124 spans := Highlight(LanguageDockerfile, src)
125
126 class, ok := classAt(spans, 0, len("RUN a "))
127 if !ok || class != ClassOperator {
128 t.Errorf("the trailing backslash is %v (covered: %v), want operator", class, ok)
129 }
130}
131
132func TestDockerfileColoursVariables(t *testing.T) {
133 for _, variable := range []string{"$HOME", "${VERSION}", "${TAG:-latest}"} {
134 t.Run(variable, func(t *testing.T) {
135 src := "RUN echo " + variable + "\n"
136 if got := dockerClassOf(t, src, variable); got != ClassBuiltin {
137 t.Errorf("%q is %v, want builtin", variable, got)
138 }
139 })
140 }
141}
142
143func TestAVariableIsOneSpanToItsClosingBrace(t *testing.T) {
144 const variable = "${TAG:-latest}"
145 src := "RUN echo " + variable + " done\n"
146 spans := Highlight(LanguageDockerfile, src)
147 start := strings.Index(src, variable)
148
149 for col := start; col < start+len(variable); col++ {
150 class, ok := classAt(spans, 0, col)
151 if !ok || class != ClassBuiltin {
152 t.Fatalf("column %d of %q is %v (covered: %v); the variable was split", col-start, variable, class, ok)
153 }
154 }
155}
156
157func TestAPathOrAnImageReferenceIsOneWord(t *testing.T) {
158 // Splitting /usr/local/bin or golang:1.26-alpine would be three colours for
159 // one argument, which is noise rather than information. The image reference
160 // is the case that matters: it is on the first line of nearly every file.
161 tests := map[string]string{
162 "WORKDIR /usr/local/bin": "/usr/local/bin",
163 "FROM golang:1.26-alpine": "golang:1.26-alpine",
164 "COPY app /opt/app": "/opt/app",
165 }
166
167 for src, word := range tests {
168 t.Run(word, func(t *testing.T) {
169 spans := Highlight(LanguageDockerfile, src)
170 start := strings.Index(src, word)
171
172 for col := start; col < start+len(word); col++ {
173 class, ok := classAt(spans, 0, col)
174 if !ok || class != ClassIdentifier {
175 t.Fatalf("column %d of %q is %v (covered: %v); the word was split", col-start, word, class, ok)
176 }
177 }
178 })
179 }
180}
181
182func TestDockerfileReturnsOneEntryPerLine(t *testing.T) {
183 tests := map[string]int{
184 "": 1,
185 "FROM alpine": 1,
186 "FROM alpine\n": 2,
187 "FROM a\nRUN b\n": 3,
188 }
189
190 for src, want := range tests {
191 if got := len(Highlight(LanguageDockerfile, src)); got != want {
192 t.Errorf("Highlight(%q) returned %d lines, want %d", src, got, want)
193 }
194 }
195}
196
197func TestBrokenDockerfileIsStillColoured(t *testing.T) {
198 for _, src := range []string{"RUN echo \"unterminated", "FROM", "${", "--", "$"} {
199 t.Run(src, func(t *testing.T) {
200 if got := len(Highlight(LanguageDockerfile, src)); got != 1 {
201 t.Errorf("Highlight(%q) returned %d lines, want 1", src, got)
202 }
203 })
204 }
205}
206
207func TestDockerfileSpansAreOrderedAndDoNotOverlap(t *testing.T) {
208 const src = "FROM golang:1.26 AS build\nCOPY --chown=me:me . /src\nRUN echo \"${TAG:-x}\" # note\n"
209
210 for line, onLine := range Highlight(LanguageDockerfile, src) {
211 previousEnd := 0
212 for _, span := range onLine {
213 if span.Start < previousEnd {
214 t.Errorf("line %d: span %+v starts before the previous one ended at %d", line, span, previousEnd)
215 }
216 previousEnd = span.End
217 }
218 }
219}