Skip to content

Commit

Permalink
test infra
Browse files Browse the repository at this point in the history
  • Loading branch information
yuwenma committed Oct 11, 2022
1 parent 5fa523b commit b77cbf5
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 2 deletions.
2 changes: 1 addition & 1 deletion go/fn/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ func NewFromTypedObject(v interface{}) (*KubeObject, error) {
}

// String serializes the object in yaml format.
func (o *KubeObject) String() string {
func (o *SubObject) String() string {
doc := internal.NewDoc([]*yaml.Node{o.obj.Node()}...)
s, _ := doc.ToYAML()
return string(s)
Expand Down
10 changes: 9 additions & 1 deletion go/fn/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,15 @@ func AsMain(input interface{}) error {

// Run evaluates the function. input must be a resourceList in yaml format. An
// updated resourceList will be returned.
func Run(p ResourceListProcessor, input []byte) (out []byte, err error) {
func Run(p ResourceListProcessor, input []byte) ([]byte, error) {
switch input := p.(type) {
case runnerProcessor:
p = input
case ResourceListProcessorFunc:
p = input
default:
return nil, fmt.Errorf("unknown input type %T", input)
}
rl, err := ParseResourceList(input)
if err != nil {
return nil, err
Expand Down
244 changes: 244 additions & 0 deletions go/fn/testhelpers/golden.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package testhelpers

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"testing"

"github.com/GoogleContainerTools/kpt-functions-sdk/go/fn"
"github.com/google/go-cmp/cmp"
)

// RunGoldenTests provides the test infra to run golden test.
// - "basedir" should be the parent directory, under where the sub-directories contains test data.
// For example, the "testdata" is the basedir. It contains two cases "test1" and "test2"
// └── testdata
// └── test1
// ├── _expected.yaml
// ├── _fnconfig.yaml
// └── resources.yaml
// └── test2
// ├── _expected.yaml
// ├── _fnconfig.yaml
// └── resources.yaml
// - "krmFunction" should be your ResourceListProcessor implementation.
func RunGoldenTests(t *testing.T, basedir string, krmFunction fn.ResourceListProcessor) {
dirEntries, err := os.ReadDir(basedir)
if err != nil {
t.Fatalf("ReadDir(%q) failed: %v", basedir, err)
}

for _, dirEntry := range dirEntries {
dir := filepath.Join(basedir, dirEntry.Name())
if !dirEntry.IsDir() {
t.Errorf("expected directory, found %s", dir)
continue
}

t.Run(dir, func(t *testing.T) {
files, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("failed to read directory %q: %v", basedir, err)
}
sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() })
var items []*fn.KubeObject
for _, f := range files {
if strings.HasPrefix(f.Name(), "_") {
continue
}
fileItems := mustParseFile(t, filepath.Join(dir, f.Name()))
items = append(items, fileItems...)
}

config := mustParseFile(t, filepath.Join(dir, "_fnconfig.yaml"))

var functionConfig *fn.KubeObject
if len(config) == 0 {
functionConfig = nil
} else if len(config) == 1 {
functionConfig = config[0]
} else {
t.Fatalf("found multiple config objects in %s", filepath.Join(dir, "_fnconfig.yaml"))
}

rl := &fn.ResourceList{Items: items, FunctionConfig: functionConfig}
success, err := krmFunction.Process(rl)
if err != nil {
t.Fatalf("run failed unexpectedly: %v", err)
}
if !success {
t.Fatalf("run did not succeed")
}

rlYAML, err := rl.ToYAML()
if err != nil {
t.Fatalf("failed to convert resource list to yaml: %v", err)
}

p := filepath.Join(dir, "_expected.yaml")
CompareGoldenFile(t, p, rlYAML)
})
}
}

// MustReadFile reads the data from "expectedPath"
func MustReadFile(t *testing.T, expectedPath string) []byte {
b, err := os.ReadFile(expectedPath)
if err != nil {
t.Fatalf("failed to read file %q: %v", expectedPath, err)
}
return b
}

// CompareGoldenFile compares the "got" data with the data stored in a "expectedPath".
func CompareGoldenFile(t *testing.T, expectedPath string, got []byte) {
if os.Getenv("WRITE_GOLDEN_OUTPUT") != "" {
// Short-circuit when the output is correct
b, err := os.ReadFile(expectedPath)
if err == nil && bytes.Equal(b, got) {
return
}

if err := os.WriteFile(expectedPath, got, 0600); err != nil {
t.Fatalf("failed to write golden output %s: %v", expectedPath, err)
}
t.Errorf("wrote output to %s", expectedPath)
} else {
want := MustReadFile(t, expectedPath)
if diff := cmp.Diff(string(want), string(got)); diff != "" {
t.Errorf("unexpected diff in %s: %s", expectedPath, diff)
}
}
}

// CopyDir copies an entire directory from "src" to "dest"
func CopyDir(src, dest string) error {
srcFiles, err := os.ReadDir(src)
if err != nil {
return fmt.Errorf("ReadDir(%q) failed: %w", src, err)
}
for _, srcFile := range srcFiles {
srcPath := filepath.Join(src, srcFile.Name())
destPath := filepath.Join(dest, srcFile.Name())
if srcFile.IsDir() {
if err = CopyDir(srcPath, destPath); err != nil {
return err
}
} else {
if err = CopyFile(srcPath, destPath); err != nil {
return err
}
}
}
return nil
}

// CopyFile copies a single file from "src" to "dest"
func CopyFile(src, dest string) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("OpenFile(%q) failed: %w", src, err)
}
defer in.Close()

out, err := os.Create(dest)
if err != nil {
return fmt.Errorf("create(%q) failed: %w", dest, err)
}

if _, err := io.Copy(out, in); err != nil {
out.Close()
return fmt.Errorf("byte copy from %s to %s failed: %w", src, dest, err)
}

if err := out.Close(); err != nil {
return fmt.Errorf("close(%q) failed: %w", dest, err)
}

return nil
}

// CompareDir compares the contents of two directories
// only compare KRM YAML resources?
func CompareDir(t *testing.T, expectDir, actualDir string) {
expectFiles, err := os.ReadDir(expectDir)
if err != nil {
t.Fatalf("failed to read expectation directory %q: %v", expectDir, err)
}
expectFileMap := make(map[string]os.DirEntry)
for _, expectFile := range expectFiles {
expectFileMap[expectFile.Name()] = expectFile
}

actualFiles, err := os.ReadDir(actualDir)
if err != nil {
t.Fatalf("failed to read actual directory %q: %v", actualDir, err)
}
actualFileMap := make(map[string]os.DirEntry)
for _, actualFile := range actualFiles {
actualFileMap[actualFile.Name()] = actualFile
}

for _, expectFile := range expectFiles {
name := expectFile.Name()
actualFile := actualFileMap[name]
if actualFile == nil {
t.Errorf("expected file %s not found", name)
continue
}

if expectFile.IsDir() {
if !actualFile.IsDir() {
t.Errorf("expected file %s was not a directory", name)
continue
}
CompareDir(t, filepath.Join(expectDir, name), filepath.Join(actualDir, name))
} else {
if actualFile.IsDir() {
t.Errorf("expected file %s was not a file", name)
continue
}
CompareFile(t, expectDir, actualDir, name)
}
}

for _, actualFile := range actualFiles {
name := actualFile.Name()
expectFile := expectFileMap[name]
if expectFile == nil {
t.Errorf("additional file %s found in output", name)
continue
}
}
}

// CompareFile compares a single file of the same relative path between "expectDir" and "actualDir"
func CompareFile(t *testing.T, expectDir, actualDir string, relPath string) {
expectAbs := filepath.Join(expectDir, relPath)

actualAbs := filepath.Join(actualDir, relPath)
actualBytes, err := os.ReadFile(actualAbs)
if err != nil {
if os.IsNotExist(err) {
t.Errorf("expected file %s not found", relPath)
} else {
t.Fatalf("error reading file %s: %v", actualAbs, err)
}
}

CompareGoldenFile(t, expectAbs, actualBytes)
}

func mustParseFile(t *testing.T, path string) fn.KubeObjects {
b := MustReadFile(t, path)
objects, err := fn.ParseKubeObjects(b)
if err != nil {
t.Fatalf("failed to parse objects from file %q: %v", path, err)
}
return objects
}

0 comments on commit b77cbf5

Please sign in to comment.