fork of https://github.com/sourcegraph/zoekt
1// Copyright 2021 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package gitindex
16
17import (
18 "bytes"
19 "context"
20 "fmt"
21 "os"
22 "os/exec"
23 "path/filepath"
24 "sort"
25 "testing"
26
27 "github.com/go-git/go-git/v5"
28 "github.com/go-git/go-git/v5/plumbing"
29 "github.com/google/go-cmp/cmp"
30 "github.com/google/go-cmp/cmp/cmpopts"
31 "github.com/sourcegraph/zoekt"
32 "github.com/sourcegraph/zoekt/build"
33 "github.com/sourcegraph/zoekt/ignore"
34 "github.com/sourcegraph/zoekt/query"
35 "github.com/sourcegraph/zoekt/shards"
36)
37
38func TestIndexEmptyRepo(t *testing.T) {
39 dir := t.TempDir()
40
41 cmd := exec.Command("git", "init", "-b", "master", "repo")
42 cmd.Dir = dir
43
44 if err := cmd.Run(); err != nil {
45 t.Fatalf("cmd.Run: %v", err)
46 }
47
48 desc := zoekt.Repository{
49 Name: "repo",
50 }
51 opts := Options{
52 RepoDir: filepath.Join(dir, "repo", ".git"),
53 BuildOptions: build.Options{
54 RepositoryDescription: desc,
55 IndexDir: dir,
56 },
57 }
58
59 if err := IndexGitRepo(opts); err != nil {
60 t.Fatalf("IndexGitRepo: %v", err)
61 }
62}
63
64func TestIndexDeltaBasic(t *testing.T) {
65 type branchToDocumentMap map[string][]zoekt.Document
66
67 type step struct {
68 name string
69 addedDocuments branchToDocumentMap
70 deletedDocuments branchToDocumentMap
71 optFn func(t *testing.T, options *Options)
72
73 expectedFallbackToNormalBuild bool
74 expectedDocuments []zoekt.Document
75 }
76
77 helloWorld := zoekt.Document{Name: "hello_world.txt", Content: []byte("hello")}
78
79 fruitV1 := zoekt.Document{Name: "best_fruit.txt", Content: []byte("strawberry")}
80 fruitV1InFolder := zoekt.Document{Name: "the_best/best_fruit.txt", Content: fruitV1.Content}
81 fruitV1WithNewName := zoekt.Document{Name: "new_fruit.txt", Content: fruitV1.Content}
82
83 fruitV2 := zoekt.Document{Name: "best_fruit.txt", Content: []byte("grapes")}
84 fruitV2InFolder := zoekt.Document{Name: "the_best/best_fruit.txt", Content: fruitV2.Content}
85
86 fruitV3 := zoekt.Document{Name: "best_fruit.txt", Content: []byte("oranges")}
87 fruitV4 := zoekt.Document{Name: "best_fruit.txt", Content: []byte("apples")}
88
89 foo := zoekt.Document{Name: "foo.txt", Content: []byte("bar")}
90
91 emptySourcegraphIgnore := zoekt.Document{Name: ignore.IgnoreFile}
92 sourcegraphIgnoreWithContent := zoekt.Document{Name: ignore.IgnoreFile, Content: []byte("good_content.txt")}
93
94 for _, test := range []struct {
95 name string
96 branches []string
97 steps []step
98 }{
99 {
100 name: "modification",
101 branches: []string{"main"},
102 steps: []step{
103 {
104 name: "setup",
105 addedDocuments: branchToDocumentMap{
106 "main": []zoekt.Document{helloWorld, fruitV1},
107 },
108
109 expectedDocuments: []zoekt.Document{helloWorld, fruitV1},
110 },
111 {
112 name: "add newer version of fruits",
113 addedDocuments: branchToDocumentMap{
114 "main": []zoekt.Document{fruitV2},
115 },
116 optFn: func(t *testing.T, o *Options) {
117 o.BuildOptions.IsDelta = true
118 },
119
120 expectedDocuments: []zoekt.Document{helloWorld, fruitV2},
121 },
122 },
123 },
124 {
125 name: "modification only inside nested folder",
126 branches: []string{"main"},
127 steps: []step{
128 {
129 name: "setup",
130 addedDocuments: branchToDocumentMap{
131 "main": []zoekt.Document{foo, fruitV1InFolder},
132 },
133
134 expectedDocuments: []zoekt.Document{foo, fruitV1InFolder},
135 },
136 {
137 name: "add newer version of fruits inside folder",
138 addedDocuments: branchToDocumentMap{
139 "main": []zoekt.Document{fruitV2InFolder},
140 },
141 optFn: func(t *testing.T, o *Options) {
142 o.BuildOptions.IsDelta = true
143 },
144
145 expectedDocuments: []zoekt.Document{foo, fruitV2InFolder},
146 },
147 },
148 },
149 {
150 name: "addition",
151 branches: []string{"main"},
152 steps: []step{
153 {
154 name: "setup",
155 addedDocuments: branchToDocumentMap{
156 "main": []zoekt.Document{helloWorld, fruitV1},
157 },
158
159 expectedDocuments: []zoekt.Document{helloWorld, fruitV1},
160 },
161 {
162 name: "add new file - foo",
163 addedDocuments: branchToDocumentMap{
164 "main": []zoekt.Document{foo},
165 },
166 optFn: func(t *testing.T, o *Options) {
167 o.BuildOptions.IsDelta = true
168 },
169
170 expectedDocuments: []zoekt.Document{helloWorld, fruitV1, foo},
171 },
172 },
173 },
174 {
175 name: "deletion",
176 branches: []string{"main"},
177 steps: []step{
178 {
179 name: "setup",
180 addedDocuments: branchToDocumentMap{
181 "main": []zoekt.Document{helloWorld, fruitV1, foo},
182 },
183
184 expectedDocuments: []zoekt.Document{helloWorld, fruitV1, foo},
185 },
186 {
187 name: "delete foo file",
188 addedDocuments: nil,
189 deletedDocuments: branchToDocumentMap{
190 "main": []zoekt.Document{foo},
191 },
192
193 optFn: func(t *testing.T, o *Options) {
194 o.BuildOptions.IsDelta = true
195 },
196
197 expectedDocuments: []zoekt.Document{helloWorld, fruitV1},
198 },
199 },
200 },
201 {
202 name: "addition and deletion on only one branch",
203 branches: []string{"main", "release", "dev"},
204 steps: []step{
205 {
206 name: "setup",
207 addedDocuments: branchToDocumentMap{
208 "main": []zoekt.Document{fruitV1},
209 "release": []zoekt.Document{fruitV2},
210 "dev": []zoekt.Document{fruitV3},
211 },
212
213 expectedDocuments: []zoekt.Document{fruitV1, fruitV2, fruitV3},
214 },
215 {
216 name: "replace fruits v3 with v4 on 'dev', delete fruits on 'main'",
217 addedDocuments: branchToDocumentMap{
218 "dev": []zoekt.Document{fruitV4},
219 },
220 deletedDocuments: branchToDocumentMap{
221 "main": []zoekt.Document{fruitV1},
222 },
223
224 optFn: func(t *testing.T, o *Options) {
225 o.BuildOptions.IsDelta = true
226 },
227
228 expectedDocuments: []zoekt.Document{fruitV2, fruitV4},
229 },
230 },
231 },
232 {
233 name: "rename",
234 branches: []string{"main", "release"},
235 steps: []step{
236 {
237 name: "setup",
238 addedDocuments: branchToDocumentMap{
239 "main": []zoekt.Document{fruitV1},
240 "release": []zoekt.Document{fruitV2},
241 },
242 expectedDocuments: []zoekt.Document{fruitV1, fruitV2},
243 },
244 {
245 name: "rename fruits file on 'main' + ensure that unmodified fruits file on 'release' is still searchable",
246 addedDocuments: branchToDocumentMap{
247 "main": []zoekt.Document{fruitV1WithNewName},
248 },
249 deletedDocuments: branchToDocumentMap{
250 "main": []zoekt.Document{fruitV1},
251 },
252
253 optFn: func(t *testing.T, o *Options) {
254 o.BuildOptions.IsDelta = true
255 },
256
257 expectedDocuments: []zoekt.Document{fruitV1WithNewName, fruitV2},
258 },
259 },
260 },
261 {
262 name: "modification: update one branch with version of document from another branch (a.k.a. Keegan's test)",
263 branches: []string{"main", "dev"},
264 steps: []step{
265 {
266 name: "setup",
267 addedDocuments: branchToDocumentMap{
268 "main": []zoekt.Document{fruitV1},
269 "dev": []zoekt.Document{fruitV2},
270 },
271 expectedDocuments: []zoekt.Document{fruitV1, fruitV2},
272 },
273 {
274 name: "switch main to dev's older version of fruits + bump dev's fruits to new version",
275 addedDocuments: branchToDocumentMap{
276 "main": []zoekt.Document{fruitV2},
277 "dev": []zoekt.Document{fruitV3},
278 },
279
280 optFn: func(t *testing.T, o *Options) {
281 o.BuildOptions.IsDelta = true
282 },
283
284 expectedDocuments: []zoekt.Document{fruitV2, fruitV3},
285 },
286 },
287 },
288 {
289 name: "no-op delta builds (reindexing the same commits)",
290 branches: []string{"main", "dev"},
291 steps: []step{
292 {
293 name: "setup",
294 addedDocuments: branchToDocumentMap{
295 "main": []zoekt.Document{fruitV1, foo},
296 "dev": []zoekt.Document{helloWorld},
297 },
298 expectedDocuments: []zoekt.Document{fruitV1, foo, helloWorld},
299 },
300 {
301 name: "first no-op (normal build -> delta build)",
302 optFn: func(t *testing.T, o *Options) {
303 o.BuildOptions.IsDelta = true
304 },
305
306 expectedDocuments: []zoekt.Document{fruitV1, foo, helloWorld},
307 },
308 {
309 name: "second no-op (delta build -> delta build)",
310 optFn: func(t *testing.T, o *Options) {
311 o.BuildOptions.IsDelta = true
312 },
313
314 expectedDocuments: []zoekt.Document{fruitV1, foo, helloWorld},
315 },
316 },
317 },
318 {
319 name: "should fallback to normal build if no prior shards exist",
320 branches: []string{"main"},
321 steps: []step{
322 {
323 name: "attempt delta build on a repository that hasn't been indexed yet",
324 addedDocuments: branchToDocumentMap{
325 "main": []zoekt.Document{helloWorld},
326 },
327 optFn: func(t *testing.T, o *Options) {
328 o.BuildOptions.IsDelta = true
329 },
330
331 expectedFallbackToNormalBuild: true,
332 expectedDocuments: []zoekt.Document{helloWorld},
333 },
334 },
335 },
336 {
337 name: "should fallback to normal build if the set of requested repository branches changes",
338 branches: []string{"main", "release", "dev"},
339 steps: []step{
340 {
341 name: "setup",
342 addedDocuments: branchToDocumentMap{
343 "main": []zoekt.Document{fruitV1},
344 "release": []zoekt.Document{fruitV2},
345 "dev": []zoekt.Document{fruitV3},
346 },
347
348 expectedDocuments: []zoekt.Document{fruitV1, fruitV2, fruitV3},
349 },
350 {
351 name: "try delta build after dropping 'main' branch from index ",
352 addedDocuments: branchToDocumentMap{
353 "release": []zoekt.Document{fruitV4},
354 },
355 optFn: func(t *testing.T, o *Options) {
356 o.Branches = []string{"HEAD", "release", "dev"} // a bit of a hack to override it this way, but it gets the job done
357 o.BuildOptions.IsDelta = true
358 },
359
360 expectedFallbackToNormalBuild: true,
361 expectedDocuments: []zoekt.Document{fruitV3, fruitV4},
362 },
363 },
364 },
365 {
366 name: "should fallback to normal build if one or more index options updates requires a full build",
367 branches: []string{"main"},
368 steps: []step{
369 {
370 name: "setup",
371 addedDocuments: branchToDocumentMap{
372 "main": []zoekt.Document{fruitV1},
373 },
374
375 expectedDocuments: []zoekt.Document{fruitV1},
376 },
377 {
378 name: "try delta build after updating Disable CTags index option",
379 addedDocuments: branchToDocumentMap{
380 "main": []zoekt.Document{fruitV2},
381 },
382 optFn: func(t *testing.T, o *Options) {
383 o.BuildOptions.IsDelta = true
384 o.BuildOptions.DisableCTags = true
385 },
386
387 expectedFallbackToNormalBuild: true,
388 expectedDocuments: []zoekt.Document{fruitV2},
389 },
390 {
391 name: "try delta build after reverting Disable CTags index option",
392 addedDocuments: branchToDocumentMap{
393 "main": []zoekt.Document{fruitV3},
394 },
395 optFn: func(t *testing.T, o *Options) {
396 o.BuildOptions.IsDelta = true
397 o.BuildOptions.DisableCTags = false
398 },
399
400 expectedFallbackToNormalBuild: true,
401 expectedDocuments: []zoekt.Document{fruitV3},
402 },
403 },
404 },
405 {
406 name: "should successfully perform multiple delta builds after disabling symbols",
407 branches: []string{"main"},
408 steps: []step{
409 {
410 name: "setup",
411 addedDocuments: branchToDocumentMap{
412 "main": []zoekt.Document{fruitV1},
413 },
414
415 expectedDocuments: []zoekt.Document{fruitV1},
416 },
417 {
418 name: "try delta build after updating Disable CTags index option",
419 addedDocuments: branchToDocumentMap{
420 "main": []zoekt.Document{fruitV2},
421 },
422 optFn: func(t *testing.T, o *Options) {
423 o.BuildOptions.IsDelta = true
424 o.BuildOptions.DisableCTags = true
425 },
426
427 expectedFallbackToNormalBuild: true,
428 expectedDocuments: []zoekt.Document{fruitV2},
429 },
430 {
431 name: "try another delta build while CTags is still disabled",
432 addedDocuments: branchToDocumentMap{
433 "main": []zoekt.Document{fruitV3},
434 },
435 optFn: func(t *testing.T, o *Options) {
436 o.BuildOptions.IsDelta = true
437 o.BuildOptions.DisableCTags = true
438 },
439
440 expectedDocuments: []zoekt.Document{fruitV3},
441 },
442 },
443 },
444 {
445 name: "should fallback to normal build if repository has unsupported Sourcegraph ignore file",
446 branches: []string{"main"},
447 steps: []step{
448 {
449 name: "setup",
450 addedDocuments: branchToDocumentMap{
451 "main": []zoekt.Document{emptySourcegraphIgnore},
452 },
453
454 expectedDocuments: []zoekt.Document{emptySourcegraphIgnore},
455 },
456 {
457 name: "attempt delta build after modifying ignore file",
458 addedDocuments: branchToDocumentMap{
459 "main": []zoekt.Document{sourcegraphIgnoreWithContent},
460 },
461 optFn: func(t *testing.T, o *Options) {
462 o.BuildOptions.IsDelta = true
463 },
464
465 expectedFallbackToNormalBuild: true,
466 expectedDocuments: []zoekt.Document{sourcegraphIgnoreWithContent},
467 },
468 },
469 },
470 {
471 name: "should fallback to a full, normal build if the repository has more than the specified threshold of shards",
472 branches: []string{"main"},
473 steps: []step{
474 {
475 name: "setup: first shard",
476 addedDocuments: branchToDocumentMap{
477 "main": []zoekt.Document{foo},
478 },
479
480 expectedDocuments: []zoekt.Document{foo},
481 },
482 {
483 name: "setup: second shard (delta)",
484 addedDocuments: branchToDocumentMap{
485 "main": []zoekt.Document{fruitV1},
486 },
487 optFn: func(t *testing.T, o *Options) {
488 o.BuildOptions.IsDelta = true
489 },
490
491 expectedDocuments: []zoekt.Document{foo, fruitV1},
492 },
493 {
494 name: "setup: third shard (delta)",
495 addedDocuments: branchToDocumentMap{
496 "main": []zoekt.Document{helloWorld},
497 },
498 optFn: func(t *testing.T, o *Options) {
499 o.BuildOptions.IsDelta = true
500 },
501
502 expectedDocuments: []zoekt.Document{foo, fruitV1, helloWorld},
503 },
504 {
505 name: "attempt another delta build after we already blew past the shard threshold",
506 addedDocuments: branchToDocumentMap{
507 "main": []zoekt.Document{fruitV2InFolder},
508 },
509 optFn: func(t *testing.T, o *Options) {
510 o.DeltaShardNumberFallbackThreshold = 2
511 o.BuildOptions.IsDelta = true
512 },
513
514 expectedFallbackToNormalBuild: true,
515 expectedDocuments: []zoekt.Document{foo, fruitV1, helloWorld, fruitV2InFolder},
516 },
517 },
518 },
519 } {
520 test := test
521
522 t.Run(test.name, func(t *testing.T) {
523 t.Parallel()
524
525 indexDir := t.TempDir()
526 repositoryDir := t.TempDir()
527
528 // setup: initialize the repository and all of its branches
529 runScript(t, repositoryDir, "git init -b master")
530 runScript(t, repositoryDir, fmt.Sprintf("git config user.email %q", "you@example.com"))
531 runScript(t, repositoryDir, fmt.Sprintf("git config user.name %q", "Your Name"))
532
533 for _, b := range test.branches {
534 runScript(t, repositoryDir, fmt.Sprintf("git checkout -b %q", b))
535 runScript(t, repositoryDir, fmt.Sprintf("git commit --allow-empty -m %q", "empty commit"))
536 }
537
538 for _, step := range test.steps {
539 t.Run(step.name, func(t *testing.T) {
540 for _, b := range test.branches {
541 // setup: for each branch, process any document deletions / additions and commit those changes
542
543 hadChange := false
544
545 runScript(t, repositoryDir, fmt.Sprintf("git checkout %q", b))
546
547 for _, d := range step.deletedDocuments[b] {
548 hadChange = true
549
550 file := filepath.Join(repositoryDir, d.Name)
551
552 err := os.Remove(file)
553 if err != nil {
554 t.Fatalf("deleting file %q: %s", d.Name, err)
555 }
556
557 runScript(t, repositoryDir, fmt.Sprintf("git add %q", file))
558 }
559
560 for _, d := range step.addedDocuments[b] {
561 hadChange = true
562
563 file := filepath.Join(repositoryDir, d.Name)
564
565 err := os.MkdirAll(filepath.Dir(file), 0o755)
566 if err != nil {
567 t.Fatalf("ensuring that folders exist for file %q: %s", file, err)
568 }
569
570 err = os.WriteFile(file, d.Content, 0o644)
571 if err != nil {
572 t.Fatalf("writing file %q: %s", d.Name, err)
573 }
574
575 runScript(t, repositoryDir, fmt.Sprintf("git add %q", file))
576 }
577
578 if !hadChange {
579 continue
580 }
581
582 runScript(t, repositoryDir, fmt.Sprintf("git commit -m %q", step.name))
583 }
584
585 // setup: prepare indexOptions with given overrides
586 buildOptions := build.Options{
587 IndexDir: indexDir,
588 RepositoryDescription: zoekt.Repository{
589 Name: "repository",
590 },
591 IsDelta: false,
592 }
593 buildOptions.SetDefaults()
594
595 branches := append([]string{"HEAD"}, test.branches...)
596
597 options := Options{
598 RepoDir: filepath.Join(repositoryDir, ".git"),
599 BuildOptions: buildOptions,
600 Branches: branches,
601 }
602
603 if step.optFn != nil {
604 step.optFn(t, &options)
605 }
606
607 // setup: prepare spy versions of prepare delta / normal build so that we can observe
608 // whether they were called appropriately
609 deltaBuildCalled := false
610 prepareDeltaSpy := func(options Options, repository *git.Repository) (repos map[fileKey]BlobLocation, branchMap map[fileKey][]string, branchVersions map[string]map[string]plumbing.Hash, changedOrDeletedPaths []string, err error) {
611 deltaBuildCalled = true
612 return prepareDeltaBuild(options, repository)
613 }
614
615 normalBuildCalled := false
616 prepareNormalSpy := func(options Options, repository *git.Repository) (repos map[fileKey]BlobLocation, branchMap map[fileKey][]string, branchVersions map[string]map[string]plumbing.Hash, err error) {
617 normalBuildCalled = true
618 return prepareNormalBuild(options, repository)
619 }
620
621 // run test
622 err := indexGitRepo(options, gitIndexConfig{
623 prepareDeltaBuild: prepareDeltaSpy,
624 prepareNormalBuild: prepareNormalSpy,
625 })
626 if err != nil {
627 t.Fatalf("IndexGitRepo: %s", err)
628 }
629
630 if options.BuildOptions.IsDelta != deltaBuildCalled {
631 // We should always try a delta build if we request it in the options.
632 t.Fatalf("expected deltaBuildCalled to be %t, got %t", options.BuildOptions.IsDelta, deltaBuildCalled)
633 }
634
635 if options.BuildOptions.IsDelta && (step.expectedFallbackToNormalBuild != normalBuildCalled) {
636 // We only check the normal spy on delta builds because it's only considered a "fallback" if we
637 // asked for a delta build in the first place.
638 t.Fatalf("expected normalBuildCalled to be %t, got %t", step.expectedFallbackToNormalBuild, normalBuildCalled)
639 }
640
641 // examine outcome: load shards into a searcher instance and run a dummy search query
642 // that returns every document contained in the shards
643 //
644 // then, compare returned set of documents with the expected set for the step and see if they agree
645
646 ss, err := shards.NewDirectorySearcher(indexDir)
647 if err != nil {
648 t.Fatalf("NewDirectorySearcher(%s): %s", indexDir, err)
649 }
650 defer ss.Close()
651
652 searchOpts := &zoekt.SearchOptions{Whole: true}
653 result, err := ss.Search(context.Background(), &query.Const{Value: true}, searchOpts)
654 if err != nil {
655 t.Fatalf("Search: %s", err)
656 }
657
658 var receivedDocuments []zoekt.Document
659 for _, f := range result.Files {
660 receivedDocuments = append(receivedDocuments, zoekt.Document{
661 Name: f.FileName,
662 Content: f.Content,
663 })
664 }
665
666 for _, docs := range [][]zoekt.Document{step.expectedDocuments, receivedDocuments} {
667 sort.Slice(docs, func(i, j int) bool {
668 a, b := docs[i], docs[j]
669
670 // first compare names, then fallback to contents if the names are equal
671
672 if a.Name < b.Name {
673 return true
674 }
675
676 if a.Name > b.Name {
677 return false
678 }
679
680 return bytes.Compare(a.Content, b.Content) < 0
681 })
682 }
683
684 compareOptions := []cmp.Option{
685 cmpopts.IgnoreFields(zoekt.Document{}, "Branches"),
686 cmpopts.EquateEmpty(),
687 }
688
689 if diff := cmp.Diff(step.expectedDocuments, receivedDocuments, compareOptions...); diff != "" {
690 t.Errorf("diff in received documents (-want +got):%s\n:", diff)
691 }
692 })
693 }
694 })
695 }
696}
697
698func TestRepoPathRanks(t *testing.T) {
699 pathRanks := repoPathRanks{
700 Paths: map[string]float64{
701 "search.go": 10.23,
702 "internal/index.go": 5.5,
703 "internal/scratch.go": 0.0,
704 "backend/search_test.go": 2.1,
705 },
706 MeanRank: 3.3,
707 }
708 cases := []struct {
709 name string
710 path string
711 rank float64
712 }{
713 {
714 name: "rank for standard file",
715 path: "search.go",
716 rank: 10.23,
717 },
718 {
719 name: "file with rank 0",
720 path: "internal/scratch.go",
721 rank: 0.0,
722 },
723 {
724 name: "rank for test file",
725 path: "backend/search_test.go",
726 rank: 2.1,
727 },
728 {
729 name: "file with missing rank",
730 path: "internal/docs.md",
731 rank: 3.3,
732 },
733 {
734 name: "test file with missing rank",
735 path: "backend/index_test.go",
736 rank: 0.0,
737 },
738 {
739 name: "third-party file with missing rank",
740 path: "node_modules/search/index.js",
741 rank: 0.0,
742 },
743 }
744
745 for _, tt := range cases {
746 t.Run(tt.name, func(t *testing.T) {
747 got := pathRanks.rank(tt.path)
748 if got != tt.rank {
749 t.Errorf("expected file '%s' to have rank %f, but got %f", tt.path, tt.rank, got)
750 }
751 })
752 }
753}
754
755func runScript(t *testing.T, cwd string, script string) {
756 err := os.MkdirAll(cwd, 0o755)
757 if err != nil {
758 t.Fatalf("ensuring path %q exists: %s", cwd, err)
759 }
760
761 cmd := exec.Command("sh", "-euxc", script)
762 cmd.Dir = cwd
763
764 if out, err := cmd.CombinedOutput(); err != nil {
765 t.Fatalf("execution error: %v, output %s", err, out)
766 }
767}