Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/limatmpl/embed_test.go
2604 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package limatmpl
5
6
import (
7
"fmt"
8
"os"
9
"reflect"
10
"strings"
11
"testing"
12
13
"github.com/sirupsen/logrus"
14
"gotest.tools/v3/assert"
15
"gotest.tools/v3/assert/cmp"
16
17
"github.com/lima-vm/lima/v2/pkg/limatype"
18
"github.com/lima-vm/lima/v2/pkg/limayaml"
19
)
20
21
type embedTestCase struct {
22
description string
23
template string
24
base string
25
expected string
26
}
27
28
// Notes:
29
// * When the template starts with "#" then the comparison will be textual instead of structural.
30
// This is required to verify comment handling.
31
// * If the description starts with "TODO" then the test is expected to fail (until it is fixed).
32
// * If the description starts with "ERROR" then the test is expected to fail with an error containing the expected string.
33
// * base is split on "---\n" and stored as base0.yaml, base1.yaml, ... in the same dir as the template.
34
// * If any base template starts with "#!" then the extension will be .sh instead of .yaml.
35
// * The template is automatically prefixed with "base: base0.yaml" unless base0 is a script.
36
// * All line comments will be separated by 2 spaces from the value on output.
37
// * Merge order of additionalDisks, mounts, and networks depends on the logic in the
38
// combineListEntries() functions and will not follow the order of the base template(s).
39
40
var embedTestCases = []embedTestCase{
41
{
42
"Empty template",
43
"",
44
"vmType: qemu",
45
"vmType: qemu",
46
},
47
{
48
"Base doesn't override existing values",
49
"vmType: vz",
50
"{arch: aarch64, vmType: qemu}",
51
"{arch: aarch64, vmType: vz}",
52
},
53
{
54
"Comments are copied over as well",
55
`#
56
# VM Type is QEMU
57
vmType: qemu # QEMU
58
`,
59
`
60
# Arch is x86_64
61
arch: x86_64 # X86
62
`,
63
`
64
# VM Type is QEMU
65
vmType: qemu # QEMU
66
# Arch is x86_64
67
arch: x86_64 # X86
68
`,
69
},
70
{
71
"mountTypesUnsupported are concatenated and duplicates removed",
72
"mountTypesUnsupported: [9p,reverse-sshfs]",
73
"mountTypesUnsupported: [9p,virtiofs]",
74
"mountTypesUnsupported: [9p,reverse-sshfs,virtiofs]",
75
},
76
{
77
"minimumLimaVersion (including comments) is updated when the base version is higher",
78
`#
79
# Works with Lima 0.8.0 and later
80
minimumLimaVersion: 0.8.0 # needs 0.8.0
81
`,
82
`
83
# Requires at least 1.0.2
84
minimumLimaVersion: 1.0.2 # or later
85
`,
86
`
87
# Requires at least 1.0.2
88
minimumLimaVersion: 1.0.2 # or later
89
`,
90
},
91
{
92
"vmOpts.qmu.minimumVersion is updated when the base version is higher",
93
"vmOpts: {qemu: {minimumVersion: 8.2.1}}",
94
"vmOpts: {qemu: {minimumVersion: 9.1.0}}",
95
"vmOpts: {qemu: {minimumVersion: 9.1.0}}",
96
},
97
{
98
"dns list is not appended, but the highest priority one is picked",
99
"dns: [1.1.1.1]",
100
"dns: [8.8.8.8, 1.2.3.4]",
101
"dns: [1.1.1.1]",
102
},
103
{
104
"Update comments on existing maps and lists that don't have comments yet",
105
`#
106
additionalDisks:
107
- name: disk1 # One
108
`,
109
`
110
# Mount additional disks
111
additionalDisks: # comment
112
# This is disk2
113
- name: disk2 # Two
114
`,
115
`
116
# Mount additional disks
117
additionalDisks: # comment
118
- name: disk1 # One
119
# This is disk2
120
- name: disk2 # Two
121
`,
122
},
123
{
124
"probes and provision scripts are prepended instead of appended",
125
"probes: [{script: 1}]\nprovision: [{script: One}]",
126
"probes: [{script: 2}]\nprovision: [{script: Two}]",
127
"probes: [{script: 2},{script: 1}]\nprovision: [{script: Two},{script: One}]",
128
},
129
{
130
"additionalDisks append, but merge fields on shared name",
131
"additionalDisks: [{name: disk1}]",
132
"additionalDisks: [{name: disk2},{name: disk1, format: true}]",
133
"additionalDisks: [{name: disk1, format: true},{name: disk2}]",
134
},
135
{
136
// This test fails because there are 2 spurious newlines in the merged output
137
"TODO mounts append, but merge fields on shared mountPoint",
138
`#
139
# My mounts
140
mounts:
141
- location: loc1 # mountPoint loc1
142
- location: loc1
143
mountPoint: loc2
144
`,
145
`
146
mounts:
147
# will update mountPoint loc2
148
- location: loc1
149
mountPoint: loc2
150
writable: true
151
# SSHFS
152
sshfs: # ssh
153
followSymlinks: true
154
# will create new mountPoint loc3
155
- location: loc1
156
mountPoint: loc3
157
writable: true
158
`,
159
`
160
# My mounts
161
mounts:
162
- location: loc1 # mountPoint loc1
163
# will update mountPoint loc2
164
- location: loc1
165
mountPoint: loc2
166
writable: true
167
# SSHFS
168
sshfs: # ssh
169
followSymlinks: true
170
# will create new mountPoint loc3
171
- location: loc1
172
mountPoint: loc3
173
writable: true
174
`,
175
},
176
{
177
// This entry can be deleted when the previous one no longer fails
178
"mounts append, but merge fields on shared mountPoint (no comments version)",
179
`mounts: [{location: loc1}, {location: loc1, mountPoint: loc2}]`,
180
`mounts: [{location: loc1, mountPoint: loc2, writable: true, sshfs: {followSymlinks: true}}, {location: loc1, mountPoint: loc3, writable: true}]`,
181
`mounts: [{location: loc1}, {location: loc1, mountPoint: loc2, writable: true, sshfs: {followSymlinks: true}}, {location: loc1, mountPoint: loc3, writable: true}]`,
182
},
183
{
184
"template: URLs are not embedded when embedAll is false",
185
// also tests file.url format
186
``,
187
`
188
base: template:default
189
provision:
190
- file:
191
url: template:provision.sh
192
probes:
193
- file:
194
url: template:probe.sh
195
`,
196
`
197
base: template:default
198
provision:
199
- file: template:provision.sh
200
probes:
201
- file: template:probe.sh
202
`,
203
},
204
{
205
"ERROR Each template must only be embedded once",
206
`#
207
arch: aarch64
208
`,
209
`
210
base: base0.yaml
211
# failure would mean this test loops forever, not that it fails the test
212
vmType: qemu
213
`,
214
`base template loop detected`,
215
},
216
{
217
"ERROR All bases following template: bases must be template: URLs too when embedAll is false",
218
``,
219
`base: [template:default, base1.yaml]`,
220
"after not embedding",
221
},
222
{
223
"ERROR All bases following template: bases must be template: URLs too when embedAll is false",
224
``,
225
`
226
base: [base1.yaml, base2.yaml]
227
---
228
base: template:default
229
---
230
base: baseX.yaml`,
231
"after not embedding",
232
},
233
{
234
"Bases are embedded depth-first",
235
`#`,
236
`
237
base: [base1.yaml, {url: base2.yaml}] # also test file.url format
238
additionalDisks: [disk0]
239
---
240
base: base3.yaml
241
additionalDisks: [disk1]
242
---
243
additionalDisks: [disk2]
244
---
245
additionalDisks: [disk3]
246
`,
247
`
248
additionalDisks: [disk0, disk1, disk3, disk2]
249
`,
250
},
251
{
252
"additionalDisks with name '*' are merged with all previous entries",
253
`
254
additionalDisks:
255
- name: disk1
256
- name: disk2
257
- name: disk3
258
format: false
259
`,
260
`
261
additionalDisks:
262
- name: disk4
263
- name: "*"
264
format: true # will apply to disk1, disk2, and disk4
265
- name: disk5
266
`,
267
`
268
additionalDisks:
269
- name: disk1
270
format: true
271
- name: disk2
272
format: true
273
- name: disk3
274
format: false
275
- name: disk4
276
format: true
277
- name: disk5
278
`,
279
},
280
{
281
// This test fails because the yq commands don't handle comments properly; may need to be fixed in yq
282
"TODO additionalDisks will be upgraded from string to map",
283
`#
284
additionalDisks:
285
# my head comment
286
- mine # my line comment
287
`,
288
`
289
# head comment
290
additionalDisks: # line comment
291
- name: "*"
292
format: true # formatting is good for you
293
`,
294
`
295
# head comment
296
additionalDisks: # line comment
297
# my head comment
298
- name: mine # my line comment
299
format: true # formatting is good for you
300
`,
301
},
302
{
303
// This entry can be deleted when the previous one no longer fails
304
"additionalDisks will be upgraded from string to map (no comments version)",
305
`additionalDisks: [mine]`,
306
`additionalDisks: [{name: "*", format: true}]`,
307
`additionalDisks: [{name: mine, format: true}]`,
308
},
309
{
310
"networks without interface name are not merged",
311
`
312
networks:
313
- interface: lima1
314
`,
315
`
316
networks:
317
- interface: lima2
318
# The metric will not be merged with anything
319
- metric: 250
320
- interface: lima1
321
metric: 100 # will be set on the first entry
322
- interface: '*' # wildcard
323
metric: 123 # will be set on the first entry
324
`,
325
`
326
networks:
327
- interface: lima1
328
metric: 100 # will be set on the first entry
329
- interface: lima2
330
metric: 123 # will be set on the first entry
331
# The metric will not be merged with anything
332
- metric: 250
333
`,
334
},
335
{
336
"Scripts are embedded with comments moved",
337
`#
338
# Hi There!
339
provision:
340
# This script will be merged from an external file
341
- file: base1.sh # This comment will move to the "script" key
342
# This is just a data file
343
- mode: data
344
file: base1.sh # This comment will move to the "content" key
345
path: /tmp/data
346
- mode: yq
347
file: base1.sh # This comment will move to the "expression" key
348
path: /tmp/yq
349
`,
350
`
351
# base0.yaml is ignored
352
---
353
#!/usr/bin/env bash
354
echo "This is base1.sh"
355
`,
356
// TODO: the empty line after the `path` is unexpected
357
`
358
# Hi There!
359
provision:
360
# This script will be merged from an external file
361
- script: |- # This comment will move to the "script" key
362
#!/usr/bin/env bash
363
echo "This is base1.sh"
364
# This is just a data file
365
- mode: data
366
content: |- # This comment will move to the "content" key
367
#!/usr/bin/env bash
368
echo "This is base1.sh"
369
path: /tmp/data
370
- mode: yq
371
expression: |- # This comment will move to the "expression" key
372
#!/usr/bin/env bash
373
echo "This is base1.sh"
374
path: /tmp/yq
375
376
# base0.yaml is ignored
377
`,
378
},
379
{
380
"Script files are embedded even when no base property exists",
381
"provision: [{file: base0.sh}]",
382
"#! my script",
383
`provision: [{script: "#! my script"}]`,
384
},
385
{
386
"ERROR base digest is not yet implemented",
387
"",
388
"base: [{url: base.yaml, digest: deafbad}]",
389
"not yet implemented",
390
},
391
{
392
"Image URLs will be converted into a template",
393
"",
394
"base: https://example.com/lima-linux-riscv64.img",
395
"{arch: riscv64, images: [{location: https://example.com/lima-linux-riscv64.img, arch: riscv64}]}",
396
},
397
{
398
"Binary files are base64 encoded",
399
`#
400
provision:
401
- mode: data
402
file: base1.sh # This comment will move to the "content" key
403
path: /tmp/data
404
`,
405
// base1.sh is binary because it contains an audible bell character '\a'
406
"# base0.yaml is ignored\n---\n#!\a123456789012345678901234567890123456789012345678901234567890",
407
`
408
provision:
409
- mode: data
410
content: !!binary | # This comment will move to the "content" key
411
IyEHMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0
412
NTY3ODkw
413
path: /tmp/data
414
415
# base0.yaml is ignored
416
`,
417
},
418
}
419
420
func TestEmbed(t *testing.T) {
421
focus := os.Getenv("TEST_FOCUS")
422
for _, tc := range embedTestCases {
423
if focus != "" {
424
if !strings.Contains(tc.description, focus) {
425
continue
426
}
427
logrus.SetLevel(logrus.DebugLevel)
428
}
429
t.Run(tc.description, func(t *testing.T) { RunEmbedTest(t, tc) })
430
}
431
logrus.SetLevel(logrus.InfoLevel)
432
}
433
434
func RunEmbedTest(t *testing.T, tc embedTestCase) {
435
todo := strings.HasPrefix(tc.description, "TODO")
436
expectError := strings.HasPrefix(tc.description, "ERROR")
437
stringCompare := strings.HasPrefix(tc.template, "#")
438
439
// Normalize testcase data
440
tc.template = strings.TrimSpace(strings.TrimPrefix(tc.template, "#"))
441
tc.base = strings.TrimSpace(tc.base)
442
tc.expected = strings.TrimSpace(tc.expected)
443
444
// Change to temp directory so all template and script names don't include a slash.
445
t.Chdir(t.TempDir())
446
447
for i, base := range strings.Split(tc.base, "---\n") {
448
extension := ".yaml"
449
if strings.HasPrefix(base, "#!") {
450
extension = ".sh"
451
}
452
baseFilename := fmt.Sprintf("base%d%s", i, extension)
453
err := os.WriteFile(baseFilename, []byte(base), 0o600)
454
assert.NilError(t, err, tc.description)
455
}
456
tmpl := &Template{
457
Bytes: fmt.Appendf(nil, "base: base0.yaml\n%s", tc.template),
458
Locator: "tmpl.yaml",
459
}
460
// Don't include `base` property if base0 is a script
461
if strings.HasPrefix(tc.base, "#!") {
462
tmpl.Bytes = []byte(tc.template)
463
}
464
err := tmpl.Embed(t.Context(), false, false)
465
if expectError {
466
assert.ErrorContains(t, err, tc.expected, tc.description)
467
return
468
}
469
assert.NilError(t, err, tc.description)
470
471
if stringCompare {
472
actual := strings.TrimSpace(string(tmpl.Bytes))
473
if todo {
474
assert.Assert(t, actual != tc.expected, tc.description)
475
} else {
476
assert.Equal(t, actual, tc.expected, tc.description)
477
}
478
return
479
}
480
481
err = tmpl.Unmarshal()
482
assert.NilError(t, err, tc.description)
483
484
var expected limatype.LimaYAML
485
err = limayaml.Unmarshal([]byte(tc.expected), &expected, "expected")
486
assert.NilError(t, err, tc.description)
487
488
if todo {
489
// using reflect.DeepEqual because cmp.DeepEqual can't easily be negated
490
assert.Assert(t, !reflect.DeepEqual(tmpl.Config, &expected), tc.description)
491
} else {
492
assert.Assert(t, cmp.DeepEqual(tmpl.Config, &expected), tc.description)
493
}
494
}
495
496
func TestEncodeScriptReason(t *testing.T) {
497
maxLineLength = 8
498
t.Run("regular script", func(t *testing.T) {
499
reason := encodeScriptReason("0123456\n")
500
assert.Equal(t, reason, "")
501
})
502
t.Run("binary script", func(t *testing.T) {
503
reason := encodeScriptReason("abc\a123")
504
assert.Equal(t, reason, "unprintable character '\\a' at offset 3")
505
})
506
t.Run("contains a tab character", func(t *testing.T) {
507
// newline character is included in character count
508
reason := encodeScriptReason("foo\tbar")
509
assert.Equal(t, reason, "unprintable character '\\t' at offset 3")
510
})
511
t.Run("long line", func(t *testing.T) {
512
// newline character is included in character count
513
reason := encodeScriptReason("line 1\nline 2\n01234567\n")
514
assert.Equal(t, reason, "line 3 (offset 14) is longer than 8 characters")
515
})
516
}
517
518