github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/testutil/testutil.go (about) 1 // Copyright 2019 Google LLC 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 15 package testutil 16 17 import ( 18 "bytes" 19 "fmt" 20 "os" 21 "os/exec" 22 "path/filepath" 23 "reflect" 24 "runtime" 25 "strings" 26 "testing" 27 28 "github.com/GoogleContainerTools/kpt/internal/gitutil" 29 "github.com/GoogleContainerTools/kpt/internal/pkg" 30 "github.com/GoogleContainerTools/kpt/internal/printer/fake" 31 "github.com/GoogleContainerTools/kpt/internal/util/addmergecomment" 32 "github.com/GoogleContainerTools/kpt/internal/util/git" 33 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 34 "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" 35 toposort "github.com/philopon/go-toposort" 36 "github.com/stretchr/testify/assert" 37 assertnow "gotest.tools/assert" 38 "sigs.k8s.io/kustomize/kyaml/copyutil" 39 "sigs.k8s.io/kustomize/kyaml/errors" 40 "sigs.k8s.io/kustomize/kyaml/filesys" 41 "sigs.k8s.io/kustomize/kyaml/sets" 42 "sigs.k8s.io/kustomize/kyaml/yaml" 43 ) 44 45 const TmpDirPrefix = "test-kpt" 46 47 const ( 48 Dataset1 = "dataset1" 49 Dataset2 = "dataset2" 50 Dataset3 = "dataset3" 51 Dataset4 = "dataset4" // Dataset4 is replica of Dataset2 with different setter values 52 Dataset5 = "dataset5" // Dataset5 is replica of Dataset2 with additional non KRM files 53 Dataset6 = "dataset6" // Dataset6 contains symlinks 54 DatasetMerged = "datasetmerged" 55 DiffOutput = "diff_output" 56 UpdateMergeConflict = "updateMergeConflict" 57 ) 58 59 // TestGitRepo manages a local git repository for testing 60 type TestGitRepo struct { 61 T *testing.T 62 63 // RepoDirectory is the temp directory of the git repo 64 RepoDirectory string 65 66 // DatasetDirectory is the directory of the testdata files 67 DatasetDirectory string 68 69 // RepoName is the name of the repository 70 RepoName string 71 72 // Commits keeps track of the commit shas for the changes 73 // to the repo. 74 Commits []string 75 } 76 77 var AssertNoError = assertnow.NilError 78 79 var KptfileSet = diffSet(kptfilev1.KptFileName) 80 81 func diffSet(path string) sets.String { 82 s := sets.String{} 83 s.Insert(path) 84 return s 85 } 86 87 // AssertEqual verifies the contents of a source package matches the contents of the 88 // destination package it was fetched to. 89 // Excludes comparing the .git directory in the source package. 90 // 91 // While the sourceDir can be the TestGitRepo, because tests change the TestGitRepo 92 // may have been changed after the destDir was copied, it is often better to explicitly 93 // use a set of golden files as the sourceDir rather than the original TestGitRepo 94 // that was copied. 95 func (g *TestGitRepo) AssertEqual(t *testing.T, sourceDir, destDir string, addMergeCommentsToSource bool) bool { 96 diff, err := Diff(sourceDir, destDir, addMergeCommentsToSource) 97 if !assert.NoError(t, err) { 98 return false 99 } 100 diff = diff.Difference(KptfileSet) 101 return assert.Empty(t, diff.List()) 102 } 103 104 // KptfileAwarePkgEqual compares two packages (including any subpackages) 105 // and has special handling of Kptfiles to handle fields that contain 106 // values which cannot easily be specified in the golden package. 107 func KptfileAwarePkgEqual(t *testing.T, pkg1, pkg2 string, addMergeCommentsToSource bool) bool { 108 diff, err := Diff(pkg1, pkg2, addMergeCommentsToSource) 109 if !assert.NoError(t, err) { 110 return false 111 } 112 113 // TODO(mortent): See if we can avoid this. We just need to make sure 114 // we can compare Kptfiles without any formatting issues. 115 for _, s := range diff.List() { 116 if !strings.HasSuffix(s, kptfilev1.KptFileName) { 117 continue 118 } 119 120 pkg1Path := filepath.Join(pkg1, s) 121 pkg1KfExists := kptfileExists(t, pkg1Path) 122 123 pkg2Path := filepath.Join(pkg2, s) 124 pkg2KfExists := kptfileExists(t, pkg2Path) 125 126 if !pkg1KfExists || !pkg2KfExists { 127 continue 128 } 129 130 // Read the Kptfiles and set the Commit field to an empty 131 // string before we compare. 132 pkg1kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, filepath.Dir(pkg1Path)) 133 if !assert.NoError(t, err) { 134 t.FailNow() 135 } 136 pkg2kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, filepath.Dir(pkg2Path)) 137 if !assert.NoError(t, err) { 138 t.FailNow() 139 } 140 141 equal, err := kptfileutil.Equal(pkg1kf, pkg2kf) 142 if !assert.NoError(t, err) { 143 t.FailNow() 144 } 145 // If the two files are considered equal after we have compared 146 // them with Kptfile-specific rules, we remove the path from the 147 // diff set. 148 if equal { 149 diff = diff.Difference(diffSet(s)) 150 } 151 } 152 return assert.Empty(t, diff.List()) 153 } 154 155 func kptfileExists(t *testing.T, path string) bool { 156 _, err := os.Stat(path) 157 if err != nil && !os.IsNotExist(err) { 158 assert.NoError(t, err) 159 t.FailNow() 160 } 161 return !os.IsNotExist(err) 162 } 163 164 // Diff returns a list of files that differ between the source and destination. 165 // 166 // Diff is guaranteed to return a non-empty set if any files differ, but 167 // this set is not guaranteed to contain all differing files. 168 func Diff(sourceDir, destDir string, addMergeCommentsToSource bool) (sets.String, error) { 169 // get set of filenames in the package source 170 var newSourceDir string 171 if addMergeCommentsToSource { 172 dir, clean, err := addmergecomment.ProcessWithCleanup(sourceDir) 173 defer clean() 174 if err != nil { 175 return sets.String{}, err 176 } 177 newSourceDir = dir 178 } 179 if newSourceDir != "" { 180 sourceDir = newSourceDir 181 } 182 upstreamFiles := sets.String{} 183 err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { 184 if err != nil { 185 return err 186 } 187 188 // skip git repo if it exists 189 if strings.Contains(path, ".git") { 190 return nil 191 } 192 193 upstreamFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, sourceDir), string(filepath.Separator))) 194 return nil 195 }) 196 if err != nil { 197 return sets.String{}, err 198 } 199 200 // get set of filenames in the cloned package 201 localFiles := sets.String{} 202 err = filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error { 203 if err != nil { 204 return err 205 } 206 207 // skip git repo if it exists 208 if strings.Contains(path, ".git") { 209 return nil 210 } 211 212 localFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, destDir), string(filepath.Separator))) 213 return nil 214 }) 215 if err != nil { 216 return sets.String{}, err 217 } 218 219 // verify the source and cloned packages have the same set of filenames 220 diff := upstreamFiles.SymmetricDifference(localFiles) 221 222 // verify file contents match 223 for _, f := range upstreamFiles.Intersection(localFiles).List() { 224 fi, err := os.Stat(filepath.Join(destDir, f)) 225 if err != nil { 226 return diff, err 227 } 228 if fi.Mode().IsDir() { 229 // already checked that this directory exists in the local files 230 continue 231 } 232 233 // compare upstreamFiles 234 b1, err := os.ReadFile(filepath.Join(destDir, f)) 235 if err != nil { 236 return diff, err 237 } 238 b2, err := os.ReadFile(filepath.Join(sourceDir, f)) 239 if err != nil { 240 return diff, err 241 } 242 243 s1 := strings.TrimSpace(strings.TrimPrefix(string(b1), trimPrefix)) 244 s2 := strings.TrimSpace(strings.TrimPrefix(string(b2), trimPrefix)) 245 if s1 != s2 { 246 fmt.Println(copyutil.PrettyFileDiff(s1, s2)) 247 diff.Insert(f) 248 } 249 } 250 // return the differing files 251 return diff, nil 252 } 253 254 const trimPrefix = `# Copyright 2019 Google LLC 255 # 256 # Licensed under the Apache License, Version 2.0 (the "License"); 257 # you may not use this file except in compliance with the License. 258 # You may obtain a copy of the License at 259 # 260 # http://www.apache.org/licenses/LICENSE-2.0 261 # 262 # Unless required by applicable law or agreed to in writing, software 263 # distributed under the License is distributed on an "AS IS" BASIS, 264 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 265 # See the License for the specific language governing permissions and 266 # limitations under the License.` 267 268 // AssertKptfile verifies the contents of the KptFile matches the provided value. 269 func (g *TestGitRepo) AssertKptfile(t *testing.T, cloned string, kpkg kptfilev1.KptFile) bool { 270 // read the actual generated KptFile 271 b, err := os.ReadFile(filepath.Join(cloned, kptfilev1.KptFileName)) 272 if !assert.NoError(t, err) { 273 return false 274 } 275 var res bytes.Buffer 276 d := yaml.NewEncoder(&res) 277 if !assert.NoError(t, d.Encode(kpkg)) { 278 return false 279 } 280 return assert.Equal(t, res.String(), string(b)) 281 } 282 283 // CheckoutBranch checks out the git branch in the repo 284 func (g *TestGitRepo) CheckoutBranch(branch string, create bool) error { 285 return checkoutBranch(g.RepoDirectory, branch, create) 286 } 287 288 // DeleteBranch deletes the git branch in the repo 289 func (g *TestGitRepo) DeleteBranch(branch string) error { 290 // checkout the branch 291 cmd := exec.Command("git", []string{"branch", "-D", branch}...) 292 cmd.Dir = g.RepoDirectory 293 _, err := cmd.Output() 294 if err != nil { 295 return err 296 } 297 298 return nil 299 } 300 301 // Commit performs a git commit and returns the SHA for the newly 302 // created commit. 303 func (g *TestGitRepo) Commit(message string) (string, error) { 304 return commit(g.RepoDirectory, message) 305 } 306 307 func (g *TestGitRepo) GetCommit() (string, error) { 308 cmd := exec.Command("git", "rev-parse", "--verify", "HEAD") 309 cmd.Dir = g.RepoDirectory 310 b, err := cmd.Output() 311 if err != nil { 312 return "", err 313 } 314 return strings.TrimSpace(string(b)), nil 315 } 316 317 // RemoveAll deletes the test git repo 318 func (g *TestGitRepo) RemoveAll() error { 319 err := os.RemoveAll(g.RepoDirectory) 320 return err 321 } 322 323 // ReplaceData replaces the data with a new source 324 func (g *TestGitRepo) ReplaceData(data string) error { 325 if !filepath.IsAbs(data) { 326 data = filepath.Join(g.DatasetDirectory, data) 327 } 328 329 return replaceData(g.RepoDirectory, data) 330 } 331 332 // CustomUpdate executes the provided update function and passes in the 333 // path to the directory of the repository. 334 func (g *TestGitRepo) CustomUpdate(f func(string) error) error { 335 return f(g.RepoDirectory) 336 } 337 338 // SetupTestGitRepo initializes a new git repository and populates it with data from a source 339 func (g *TestGitRepo) SetupTestGitRepo(name string, data []Content, repos map[string]*TestGitRepo) error { 340 defaultBranch := "main" 341 if len(data) > 0 && len(data[0].Branch) > 0 { 342 defaultBranch = data[0].Branch 343 } 344 345 err := g.createEmptyGitRepo(defaultBranch) 346 if err != nil { 347 return err 348 } 349 350 // configure the path to the test dataset 351 ds, err := GetTestDataPath() 352 if err != nil { 353 return err 354 } 355 g.DatasetDirectory = ds 356 357 return UpdateGitDir(g.T, name, g, data, repos) 358 } 359 360 func (g *TestGitRepo) createEmptyGitRepo(defaultBranch string) error { 361 dir, err := os.MkdirTemp("", fmt.Sprintf("%s-upstream-", TmpDirPrefix)) 362 if err != nil { 363 return err 364 } 365 g.RepoDirectory = dir 366 g.RepoName = filepath.Base(g.RepoDirectory) 367 368 cmd := exec.Command("git", "init", 369 fmt.Sprintf("--initial-branch=%s", defaultBranch)) 370 cmd.Dir = dir 371 stdoutStderr, err := cmd.CombinedOutput() 372 if err != nil { 373 fmt.Fprintf(os.Stderr, "%s", stdoutStderr) 374 return err 375 } 376 _, err = g.Commit("initial commit") 377 return err 378 } 379 380 func GetTestDataPath() (string, error) { 381 filename, err := getTestUtilGoFilePath() 382 if err != nil { 383 return "", err 384 } 385 ds, err := filepath.Abs(filepath.Join(filepath.Dir(filename), "testdata")) 386 if err != nil { 387 return "", err 388 } 389 return ds, nil 390 } 391 392 func getTestUtilGoFilePath() (string, error) { 393 _, filename, _, ok := runtime.Caller(1) 394 if !ok { 395 return "", errors.Errorf("failed to testutil package location") 396 } 397 return filename, nil 398 } 399 400 // Tag initializes tags the git repository 401 func (g *TestGitRepo) Tag(tagName string) error { 402 return tag(g.RepoDirectory, tagName) 403 } 404 405 // SetupRepoAndWorkspace handles setting up a default repo to clone, and a workspace to clone into. 406 // returns a cleanup function to remove the git repo and workspace. 407 func SetupRepoAndWorkspace(t *testing.T, content Content) (*TestGitRepo, *TestWorkspace, func()) { 408 repos, workspace, cleanup := SetupReposAndWorkspace(t, map[string][]Content{ 409 Upstream: { 410 content, 411 }, 412 }) 413 414 g := repos[Upstream] 415 return g, workspace, cleanup 416 } 417 418 // SetupReposAndWorkspace handles setting up a set of repos as specified by 419 // the reposContent and a workspace to clone into. It returns a cleanup function 420 // that will remove the repos. 421 func SetupReposAndWorkspace(t *testing.T, reposContent map[string][]Content) (map[string]*TestGitRepo, *TestWorkspace, func()) { 422 repos, repoCleanup := SetupRepos(t, reposContent) 423 w, workspaceCleanup := SetupWorkspace(t) 424 return repos, w, func() { 425 repoCleanup() 426 workspaceCleanup() 427 } 428 } 429 430 // SetupWorkspace creates a local workspace which kpt packages can be cloned 431 // into. It returns a cleanup function that will remove the workspace. 432 func SetupWorkspace(t *testing.T) (*TestWorkspace, func()) { 433 // setup the directory to clone to 434 w := &TestWorkspace{} 435 err := w.SetupTestWorkspace() 436 assert.NoError(t, err) 437 438 gr, err := gitutil.NewLocalGitRunner(w.WorkspaceDirectory) 439 if !assert.NoError(t, err) { 440 t.FailNow() 441 } 442 443 rr, err := gr.Run(fake.CtxWithDefaultPrinter(), "init") 444 if !assert.NoError(t, err) { 445 assert.FailNowf(t, "%s %s", rr.Stdout, rr.Stderr) 446 } 447 return w, func() { 448 _ = w.RemoveAll() 449 } 450 } 451 452 // AddKptfileToWorkspace writes the provided Kptfile to the workspace 453 // and makes a commit. 454 func AddKptfileToWorkspace(t *testing.T, w *TestWorkspace, kf *kptfilev1.KptFile) { 455 err := os.MkdirAll(w.FullPackagePath(), 0700) 456 if !assert.NoError(t, err) { 457 t.FailNow() 458 } 459 460 err = kptfileutil.WriteFile(w.FullPackagePath(), kf) 461 if !assert.NoError(t, err) { 462 t.FailNow() 463 } 464 465 gitRunner, err := gitutil.NewLocalGitRunner(w.WorkspaceDirectory) 466 if !assert.NoError(t, err) { 467 t.FailNow() 468 } 469 _, err = gitRunner.Run(fake.CtxWithDefaultPrinter(), "add", ".") 470 if !assert.NoError(t, err) { 471 t.FailNow() 472 } 473 _, err = w.Commit("added Kptfile") 474 if !assert.NoError(t, err) { 475 t.FailNow() 476 } 477 } 478 479 // SetupRepos creates repos and returns a mapping from name to TestGitRepos. 480 // This only creates the first version of each repo as given by the first item 481 // in the repoContent slice. 482 func SetupRepos(t *testing.T, repoContent map[string][]Content) (map[string]*TestGitRepo, func()) { 483 repos := make(map[string]*TestGitRepo) 484 485 cleanupFunc := func() { 486 for _, rp := range repos { 487 _ = os.RemoveAll(rp.RepoDirectory) 488 } 489 } 490 491 ordering, err := findRepoOrdering(repoContent) 492 if !assert.NoError(t, err) { 493 t.FailNow() 494 } 495 496 for _, name := range ordering { 497 data := repoContent[name] 498 if len(data) < 1 { 499 continue 500 } 501 tgr := &TestGitRepo{T: t} 502 repos[name] = tgr 503 if err := tgr.SetupTestGitRepo(name, data[:1], repos); err != nil { 504 return repos, cleanupFunc 505 } 506 } 507 return repos, cleanupFunc 508 } 509 510 // UpdateRepos updates the existing repos with any additional Content 511 // items in the repoContent slice. 512 func UpdateRepos(t *testing.T, repos map[string]*TestGitRepo, repoContent map[string][]Content) error { 513 ordering, err := findRepoOrdering(repoContent) 514 if !assert.NoError(t, err) { 515 t.FailNow() 516 } 517 518 for _, name := range ordering { 519 data := repoContent[name] 520 if len(data) < 1 { 521 continue 522 } 523 524 r := repos[name] 525 err := UpdateGitDir(t, name, r, data[1:], repos) 526 if err != nil { 527 return err 528 } 529 } 530 return nil 531 } 532 533 // findRepoOrdering orders the repos based on their dependencies. So if repo 534 // A includes repo B as a subpackage, we can create repo B before we create 535 // repo B. This is done with a topological sort. If there are any circular 536 // dependencies between the repos, it will return an error. 537 func findRepoOrdering(repoContent map[string][]Content) ([]string, error) { 538 var repoNames []string 539 for n := range repoContent { 540 repoNames = append(repoNames, n) 541 } 542 543 topo := toposort.NewGraph(len(repoNames)) 544 topo.AddNodes(repoNames...) 545 // Keep track of which edges have been added to topo. The library doesn't 546 // handle the same edge added multiple times. 547 added := make(map[string]string) 548 for n, contents := range repoContent { 549 for _, c := range contents { 550 if c.Pkg == nil { 551 continue 552 } 553 pkg := c.Pkg 554 refRepos := pkg.AllReferencedRepos() 555 for _, refRepo := range refRepos { 556 if v, ok := added[refRepo]; ok && v == n { 557 continue 558 } 559 topo.AddEdge(refRepo, n) 560 added[refRepo] = n 561 } 562 } 563 } 564 ordering, ok := topo.Toposort() 565 if !ok { 566 return nil, fmt.Errorf("unable to sort repo references. Cycles are not allowed") 567 } 568 return ordering, nil 569 } 570 571 func checkoutBranch(repo string, branch string, create bool) error { 572 var args []string 573 if create { 574 args = []string{"checkout", "-b", branch} 575 } else { 576 args = []string{"checkout", branch} 577 } 578 579 // checkout the branch 580 cmd := exec.Command("git", args...) 581 cmd.Dir = repo 582 _, err := cmd.CombinedOutput() 583 if err != nil { 584 return err 585 } 586 587 return nil 588 } 589 590 // nolint:gocyclo 591 func replaceData(repo, data string) error { 592 // If the path is absolute we assume it is the full path to the 593 // testdata. If it is relative, we assume it refers to one of the 594 // test data sets. 595 if !filepath.IsAbs(data) { 596 ds, err := GetTestDataPath() 597 if err != nil { 598 return err 599 } 600 data = filepath.Join(ds, data) 601 } 602 // Walk the data directory and copy over all files. We have special 603 // handling of the Kptfile to make sure we don't lose the Upstream data. 604 if err := filepath.Walk(data, func(path string, info os.FileInfo, err error) error { 605 if err != nil { 606 return err 607 } 608 rel, err := filepath.Rel(data, path) 609 if err != nil { 610 return err 611 } 612 613 _, err = os.Stat(filepath.Join(repo, rel)) 614 if err != nil && !os.IsNotExist(err) { 615 return err 616 } 617 618 // If the file/directory doesn't exist in the repo folder, we just 619 // copy it over. 620 if os.IsNotExist(err) { 621 switch { 622 case info.Mode()&os.ModeSymlink != 0: 623 path, err := os.Readlink(path) 624 if err != nil { 625 return err 626 } 627 return os.Symlink(path, filepath.Join(repo, rel)) 628 case info.IsDir(): 629 return os.Mkdir(filepath.Join(repo, rel), 0700) 630 default: 631 return copyutil.SyncFile(path, filepath.Join(repo, rel)) 632 } 633 } 634 635 // If it is a directory and we know it already exists, we don't need 636 // to do anything. 637 if info.IsDir() { 638 return nil 639 } 640 641 // For Kptfiles we want to keep the Upstream section if the Kptfile 642 // in the data directory doesn't already include one. 643 if filepath.Base(path) == "Kptfile" { 644 dataKptfile, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, filepath.Dir(path)) 645 if err != nil { 646 return err 647 } 648 repoKptfileDir := filepath.Dir(filepath.Join(repo, rel)) 649 repoKptfile, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, repoKptfileDir) 650 if err != nil { 651 return err 652 } 653 // Only copy over the Upstream section from the existing 654 // Kptfile if other values hasn't been provided. 655 if dataKptfile.Upstream == nil || reflect.DeepEqual(dataKptfile.Upstream, kptfilev1.Upstream{}) { 656 dataKptfile.Upstream = repoKptfile.Upstream 657 } 658 if dataKptfile.UpstreamLock == nil || reflect.DeepEqual(dataKptfile.UpstreamLock, kptfilev1.UpstreamLock{}) { 659 dataKptfile.UpstreamLock = repoKptfile.UpstreamLock 660 } 661 dataKptfile.Name = repoKptfile.Name 662 err = kptfileutil.WriteFile(repoKptfileDir, dataKptfile) 663 if err != nil { 664 return err 665 } 666 } else { 667 err := copyutil.SyncFile(path, filepath.Join(repo, rel)) 668 if err != nil { 669 return err 670 } 671 } 672 return nil 673 }); err != nil { 674 return err 675 } 676 677 // We then walk the repo folder and remove and files/directories that 678 // exists in the repo, but doesn't exist in the data directory. 679 if err := filepath.Walk(repo, func(path string, info os.FileInfo, err error) error { 680 if os.IsNotExist(err) { 681 return nil 682 } 683 if err != nil { 684 return err 685 } 686 // Find the relative path of the file/directory 687 rel, err := filepath.Rel(repo, path) 688 if err != nil { 689 return err 690 } 691 // We skip anything that is inside the .git folder 692 if strings.HasPrefix(rel, ".git") { 693 return nil 694 } 695 696 // Never delete the Kptfile in the root package. 697 if rel == kptfilev1.KptFileName { 698 return nil 699 } 700 701 // Check if a file/directory exists at the path relative path within the 702 // data directory 703 dataCopy := filepath.Join(data, rel) 704 _, err = os.Stat(dataCopy) 705 if err != nil && !os.IsNotExist(err) { 706 return err 707 } 708 709 // If the file/directory doesn't exist in the data folder, we remove 710 // them from the repo folder. 711 if os.IsNotExist(err) { 712 if info.IsDir() { 713 if err := os.RemoveAll(path); err != nil { 714 return err 715 } 716 } else { 717 if err := os.Remove(path); err != nil { 718 return err 719 } 720 } 721 } 722 return nil 723 }); err != nil { 724 return err 725 } 726 727 // Add the changes to git. 728 cmd := exec.Command("git", "add", ".") 729 cmd.Dir = repo 730 _, err := cmd.CombinedOutput() 731 return err 732 } 733 734 func commit(repo, message string) (string, error) { 735 cmd := exec.Command("git", "commit", "-m", message, "--allow-empty") 736 cmd.Dir = repo 737 stdoutStderr, err := cmd.CombinedOutput() 738 if err != nil { 739 fmt.Fprintf(os.Stderr, "%s", stdoutStderr) 740 return "", err 741 } 742 743 sha, err := git.LookupCommit(repo) 744 if err != nil { 745 return "", err 746 } 747 748 return sha, nil 749 } 750 751 func tag(repo, tag string) error { 752 cmd := exec.Command("git", "tag", tag) 753 cmd.Dir = repo 754 b, err := cmd.Output() 755 if err != nil { 756 fmt.Fprintf(os.Stderr, "%s\n", b) 757 return err 758 } 759 760 return nil 761 } 762 763 type TestWorkspace struct { 764 WorkspaceDirectory string 765 PackageDir string 766 } 767 768 // FullPackagePath returns the full path to the roor package in the 769 // local workspace. 770 func (w *TestWorkspace) FullPackagePath() string { 771 return filepath.Join(w.WorkspaceDirectory, w.PackageDir) 772 } 773 774 func (w *TestWorkspace) SetupTestWorkspace() error { 775 var err error 776 w.WorkspaceDirectory, err = os.MkdirTemp("", "test-kpt-local-") 777 return err 778 } 779 780 func (w *TestWorkspace) RemoveAll() error { 781 return os.RemoveAll(w.WorkspaceDirectory) 782 } 783 784 // CheckoutBranch checks out the git branch in the repo 785 func (w *TestWorkspace) CheckoutBranch(branch string, create bool) error { 786 return checkoutBranch(w.WorkspaceDirectory, branch, create) 787 } 788 789 // ReplaceData replaces the data with a new source 790 func (w *TestWorkspace) ReplaceData(data string) error { 791 return replaceData(filepath.Join(w.WorkspaceDirectory, w.PackageDir), data) 792 } 793 794 // CustomUpdate executes the provided update function and passes in the 795 // path to the directory of the repository. 796 func (w *TestWorkspace) CustomUpdate(f func(string) error) error { 797 return f(w.WorkspaceDirectory) 798 } 799 800 // Commit performs a git commit 801 func (w *TestWorkspace) Commit(message string) (string, error) { 802 return commit(w.WorkspaceDirectory, message) 803 } 804 805 // Tag initializes tags the git repository 806 func (w *TestWorkspace) Tag(tagName string) error { 807 return tag(w.WorkspaceDirectory, tagName) 808 } 809 810 func PrintPackage(paths ...string) error { 811 path := filepath.Join(paths...) 812 return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 813 if err != nil { 814 return err 815 } 816 if strings.Contains(path, "/.git") { 817 return nil 818 } 819 fmt.Println(path) 820 return nil 821 }) 822 } 823 824 func PrintFile(paths ...string) error { 825 path := filepath.Join(paths...) 826 b, err := os.ReadFile(path) 827 if err != nil { 828 return err 829 } 830 fmt.Println(string(b)) 831 return nil 832 } 833 834 func Chdir(t *testing.T, path string) func() { 835 cwd, err := os.Getwd() 836 if !assert.NoError(t, err) { 837 t.FailNow() 838 } 839 revertFunc := func() { 840 if err := os.Chdir(cwd); err != nil { 841 panic(err) 842 } 843 } 844 err = os.Chdir(path) 845 if !assert.NoError(t, err) { 846 defer revertFunc() 847 t.FailNow() 848 } 849 return revertFunc 850 } 851 852 // ConfigureTestKptCache sets up a temporary directory for the kpt git 853 // cache, sets the env variable so it will be used for tests, and cleans 854 // up the directory afterwards. 855 func ConfigureTestKptCache(m *testing.M) int { 856 cacheDir, err := os.MkdirTemp("", "kpt-test-cache-repos-") 857 if err != nil { 858 panic(fmt.Errorf("error creating temp dir for cache: %w", err)) 859 } 860 defer func() { 861 _ = os.RemoveAll(cacheDir) 862 }() 863 if err := os.Setenv(gitutil.RepoCacheDirEnv, cacheDir); err != nil { 864 panic(fmt.Errorf("error setting repo cache env variable: %w", err)) 865 } 866 return m.Run() 867 } 868 869 var EmptyReposInfo = &ReposInfo{} 870 871 func ToReposInfo(repos map[string]*TestGitRepo) *ReposInfo { 872 return &ReposInfo{ 873 repos: repos, 874 } 875 } 876 877 type ReposInfo struct { 878 repos map[string]*TestGitRepo 879 } 880 881 func (ri *ReposInfo) ResolveRepoRef(repoRef string) (string, bool) { 882 repo, found := ri.repos[repoRef] 883 if !found { 884 return "", false 885 } 886 return repo.RepoDirectory, true 887 } 888 889 func (ri *ReposInfo) ResolveCommitIndex(repoRef string, index int) (string, bool) { 890 repo, found := ri.repos[repoRef] 891 if !found { 892 return "", false 893 } 894 commits := repo.Commits 895 if len(commits) <= index { 896 return "", false 897 } 898 return commits[index], true 899 }