github.com/google/yamlfmt@v0.12.2-0.20240514121411-7f77800e2681/internal/tempfile/golden.go (about)

     1  // Copyright 2024 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 tempfile
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io/fs"
    21  	"os"
    22  	"path/filepath"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/yamlfmt/internal/collections"
    26  )
    27  
    28  type GoldenCtx struct {
    29  	Dir    string
    30  	Update bool
    31  }
    32  
    33  func (g GoldenCtx) goldenPath(path string) string {
    34  	return filepath.Join(g.Dir, path)
    35  }
    36  
    37  func (g GoldenCtx) CompareGoldenFile(path string, gotContent []byte) error {
    38  	// If we are updating, just rewrite the file.
    39  	if g.Update {
    40  		return os.WriteFile(g.goldenPath(path), gotContent, os.ModePerm)
    41  	}
    42  
    43  	// If we are not updating, check that the content is the same.
    44  	expectedContent, err := os.ReadFile(g.goldenPath(path))
    45  	if err != nil {
    46  		return err
    47  	}
    48  	// Edge case for empty stdout.
    49  	if gotContent == nil {
    50  		gotContent = []byte{}
    51  	}
    52  	diff := cmp.Diff(string(expectedContent), string(gotContent))
    53  	// If there is no diff between the content, nothing to do in either mode.
    54  	if diff == "" {
    55  		return nil
    56  	}
    57  	return &GoldenDiffError{path: path, diff: diff}
    58  }
    59  
    60  func (g GoldenCtx) CompareDirectory(resultPath string) error {
    61  	// If in update mode, clobber the whole directory and recreate it with
    62  	// the result of the test.
    63  	if g.Update {
    64  		return g.updateGoldenDirectory(resultPath)
    65  	}
    66  
    67  	// Compare the two directories by reading all paths.
    68  	resultPaths, err := readAllPaths(resultPath)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	goldenPaths, err := readAllPaths(g.Dir)
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	// If the directories differ in paths then the test has failed.
    78  	if !resultPaths.Equals(goldenPaths) {
    79  		return errors.New("the directories were different")
    80  	}
    81  
    82  	// Compare each file and gather each error.
    83  	compareErrors := collections.Errors{}
    84  	for path := range resultPaths {
    85  		gotContent, err := os.ReadFile(filepath.Join(resultPath, path))
    86  		if err != nil {
    87  			return fmt.Errorf("%s: %w", path, err)
    88  		}
    89  		err = g.CompareGoldenFile(path, gotContent)
    90  		if err != nil {
    91  			return fmt.Errorf("%s: %w", path, err)
    92  		}
    93  	}
    94  	// If there are no errors this will be nil, otherwise will be a
    95  	// combination of every error that occurred.
    96  	return compareErrors.Combine()
    97  }
    98  
    99  func (g GoldenCtx) updateGoldenDirectory(resultPath string) error {
   100  	// Clear the golden directory
   101  	err := os.RemoveAll(g.Dir)
   102  	if err != nil {
   103  		return fmt.Errorf("could not clear golden directory %s: %w", g.Dir, err)
   104  	}
   105  	err = os.Mkdir(g.Dir, os.ModePerm)
   106  	if err != nil {
   107  		return fmt.Errorf("could not recreate golden directory %s: %w", g.Dir, err)
   108  	}
   109  
   110  	// Recreate the goldens directory
   111  	paths, err := ReplicateDirectory(resultPath, g.Dir)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	return paths.CreateAll()
   116  }
   117  
   118  func readAllPaths(dirPath string) (collections.Set[string], error) {
   119  	paths := collections.Set[string]{}
   120  	allNamesButCurrentDirectory := func(path string, d fs.DirEntry, err error) error {
   121  		if path == dirPath {
   122  			return nil
   123  		}
   124  		paths.Add(d.Name())
   125  		return nil
   126  	}
   127  	err := filepath.WalkDir(dirPath, allNamesButCurrentDirectory)
   128  	return paths, err
   129  }
   130  
   131  type GoldenDiffError struct {
   132  	path string
   133  	diff string
   134  }
   135  
   136  func (e *GoldenDiffError) Error() string {
   137  	return fmt.Sprintf("golden: %s differed: %s", e.path, e.diff)
   138  }