github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/test/runner/runner.go (about)

     1  // Copyright 2021 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 runner
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"os/exec"
    21  	"path/filepath"
    22  	"regexp"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/GoogleContainerTools/kpt/internal/fnruntime"
    27  	"github.com/google/go-cmp/cmp"
    28  )
    29  
    30  // Runner runs an e2e test
    31  type Runner struct {
    32  	pkgName       string
    33  	testCase      TestCase
    34  	cmd           string
    35  	t             *testing.T
    36  	initialCommit string
    37  	kptBin        string
    38  }
    39  
    40  func getKptBin() (string, error) {
    41  	p, err := exec.Command("which", "kpt").CombinedOutput()
    42  	if err != nil {
    43  		return "", fmt.Errorf("cannot find command 'kpt' in $PATH: %w", err)
    44  	}
    45  	return strings.TrimSpace(string(p)), nil
    46  }
    47  
    48  const (
    49  	// If this env is set to "true", this e2e test framework will update the
    50  	// expected diff and results if they already exist. If will not change
    51  	// config.yaml.
    52  	updateExpectedEnv string = "KPT_E2E_UPDATE_EXPECTED"
    53  
    54  	expectedDir         string = ".expected"
    55  	expectedResultsFile string = "results.yaml"
    56  	expectedDiffFile    string = "diff.patch"
    57  	expectedConfigFile  string = "config.yaml"
    58  	outDir              string = "out"
    59  	setupScript         string = "setup.sh"
    60  	teardownScript      string = "teardown.sh"
    61  	execScript          string = "exec.sh"
    62  	CommandFnEval       string = "eval"
    63  	CommandFnRender     string = "render"
    64  
    65  	allowWasmFlag string = "--allow-alpha-wasm"
    66  )
    67  
    68  // NewRunner returns a new runner for pkg
    69  func NewRunner(t *testing.T, testCase TestCase, c string) (*Runner, error) {
    70  	info, err := os.Stat(testCase.Path)
    71  	if err != nil {
    72  		return nil, fmt.Errorf("cannot open path %s: %w", testCase.Path, err)
    73  	}
    74  	if !info.IsDir() {
    75  		return nil, fmt.Errorf("path %s is not a directory", testCase.Path)
    76  	}
    77  	kptBin, err := getKptBin()
    78  	if err != nil {
    79  		t.Logf("failed to find kpt binary: %v", err)
    80  	}
    81  	if kptBin != "" {
    82  		t.Logf("Using kpt binary: %s", kptBin)
    83  	}
    84  	return &Runner{
    85  		pkgName:  filepath.Base(testCase.Path),
    86  		testCase: testCase,
    87  		cmd:      c,
    88  		t:        t,
    89  		kptBin:   kptBin,
    90  	}, nil
    91  }
    92  
    93  // Run runs the test.
    94  func (r *Runner) Run() error {
    95  	switch r.cmd {
    96  	case CommandFnEval:
    97  		return r.runFnEval()
    98  	case CommandFnRender:
    99  		return r.runFnRender()
   100  	default:
   101  		return fmt.Errorf("invalid command %s", r.cmd)
   102  	}
   103  }
   104  
   105  // runSetupScript runs the setup script if the test has it
   106  func (r *Runner) runSetupScript(pkgPath string) error {
   107  	p, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, setupScript))
   108  	if err != nil {
   109  		return err
   110  	}
   111  	if _, err := os.Stat(p); os.IsNotExist(err) {
   112  		return nil
   113  	}
   114  	cmd := getCommand(pkgPath, "bash", []string{p})
   115  	r.t.Logf("running setup script: %q", cmd.String())
   116  	if output, err := cmd.CombinedOutput(); err != nil {
   117  		return fmt.Errorf("failed to run setup script %q.\nOutput: %q\n: %w", p, string(output), err)
   118  	}
   119  	return nil
   120  }
   121  
   122  // runTearDownScript runs the teardown script if the test has it
   123  func (r *Runner) runTearDownScript(pkgPath string) error {
   124  	p, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, teardownScript))
   125  	if err != nil {
   126  		return err
   127  	}
   128  	if _, err := os.Stat(p); os.IsNotExist(err) {
   129  		return nil
   130  	}
   131  	cmd := getCommand(pkgPath, "bash", []string{p})
   132  	r.t.Logf("running teardown script: %q", cmd.String())
   133  	if output, err := cmd.CombinedOutput(); err != nil {
   134  		return fmt.Errorf("failed to run teardown script %q.\nOutput: %q\n: %w", p, string(output), err)
   135  	}
   136  	return nil
   137  }
   138  
   139  func (r *Runner) runFnEval() error {
   140  	r.t.Logf("Running test against package %s\n", r.pkgName)
   141  	tmpDir, err := os.MkdirTemp("", "kpt-fn-e2e-*")
   142  	if err != nil {
   143  		return fmt.Errorf("failed to create temporary dir: %w", err)
   144  	}
   145  	pkgPath := filepath.Join(tmpDir, r.pkgName)
   146  
   147  	if r.testCase.Config.Debug {
   148  		fmt.Printf("Running test against package %s in dir %s \n", r.pkgName, pkgPath)
   149  	}
   150  	if !r.testCase.Config.Debug {
   151  		// if debug is true, keep the test directory around for debugging
   152  		defer os.RemoveAll(tmpDir)
   153  	}
   154  	var resultsDir, destDir string
   155  
   156  	if r.IsFnResultExpected() {
   157  		resultsDir = filepath.Join(tmpDir, "results")
   158  	}
   159  
   160  	if r.IsOutOfPlace() {
   161  		destDir = filepath.Join(pkgPath, outDir)
   162  	}
   163  
   164  	// copy package to temp directory
   165  	err = copyDir(r.testCase.Path, pkgPath)
   166  	if err != nil {
   167  		return fmt.Errorf("failed to copy package: %w", err)
   168  	}
   169  
   170  	// init and commit package files
   171  	err = r.preparePackage(pkgPath)
   172  	if err != nil {
   173  		return fmt.Errorf("failed to prepare package: %w", err)
   174  	}
   175  
   176  	// run function
   177  	for i := 0; i < r.testCase.Config.RunCount(); i++ {
   178  		err = r.runSetupScript(pkgPath)
   179  		if err != nil {
   180  			return err
   181  		}
   182  
   183  		var cmd *exec.Cmd
   184  		execScriptPath, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, execScript))
   185  		if err != nil {
   186  			return err
   187  		}
   188  
   189  		if _, err := os.Stat(execScriptPath); err == nil {
   190  			cmd = getCommand(pkgPath, "bash", []string{execScriptPath})
   191  		} else {
   192  			kptArgs := []string{"fn", "eval", pkgPath}
   193  
   194  			if resultsDir != "" {
   195  				kptArgs = append(kptArgs, "--results-dir", resultsDir)
   196  			}
   197  			if destDir != "" {
   198  				kptArgs = append(kptArgs, "-o", destDir)
   199  			}
   200  			if r.testCase.Config.AllowWasm {
   201  				kptArgs = append(kptArgs, allowWasmFlag)
   202  			}
   203  			if r.testCase.Config.ImagePullPolicy != "" {
   204  				kptArgs = append(kptArgs, "--image-pull-policy", r.testCase.Config.ImagePullPolicy)
   205  			}
   206  			if r.testCase.Config.EvalConfig.Network {
   207  				kptArgs = append(kptArgs, "--network")
   208  			}
   209  			if r.testCase.Config.EvalConfig.Image != "" {
   210  				kptArgs = append(kptArgs, "--image", r.testCase.Config.EvalConfig.Image)
   211  			} else if !r.testCase.Config.EvalConfig.execUniquePath.Empty() {
   212  				kptArgs = append(kptArgs, "--exec", string(r.testCase.Config.EvalConfig.execUniquePath))
   213  			}
   214  			if !r.testCase.Config.EvalConfig.fnConfigUniquePath.Empty() {
   215  				kptArgs = append(kptArgs, "--fn-config", string(r.testCase.Config.EvalConfig.fnConfigUniquePath))
   216  			}
   217  			if r.testCase.Config.EvalConfig.IncludeMetaResources {
   218  				kptArgs = append(kptArgs, "--include-meta-resources")
   219  			}
   220  			// args must be appended last
   221  			if len(r.testCase.Config.EvalConfig.Args) > 0 {
   222  				kptArgs = append(kptArgs, "--")
   223  				for k, v := range r.testCase.Config.EvalConfig.Args {
   224  					kptArgs = append(kptArgs, fmt.Sprintf("%s=%s", k, v))
   225  				}
   226  			}
   227  			cmd = getCommand("", r.kptBin, kptArgs)
   228  		}
   229  		r.t.Logf("running command: %v=%v %v", fnruntime.ContainerRuntimeEnv, os.Getenv(fnruntime.ContainerRuntimeEnv), cmd.String())
   230  		stdout, stderr, fnErr := runCommand(cmd)
   231  		if fnErr != nil {
   232  			r.t.Logf("kpt error, stdout: %s; stderr: %s", stdout, stderr)
   233  		}
   234  		// Update the diff file or results file if updateExpectedEnv is set.
   235  		if strings.ToLower(os.Getenv(updateExpectedEnv)) == "true" {
   236  			return r.updateExpected(pkgPath, resultsDir, filepath.Join(r.testCase.Path, expectedDir))
   237  		}
   238  
   239  		// compare results
   240  		err = r.compareResult(i, fnErr, stdout, sanitizeTimestamps(stderr), pkgPath, resultsDir)
   241  		if err != nil {
   242  			return err
   243  		}
   244  		// we passed result check, now we should break if the command error
   245  		// is expected
   246  		if fnErr != nil {
   247  			break
   248  		}
   249  
   250  		err = r.runTearDownScript(pkgPath)
   251  		if err != nil {
   252  			return err
   253  		}
   254  	}
   255  
   256  	return nil
   257  }
   258  
   259  func sanitizeTimestamps(stderr string) string {
   260  	// Output will have non-deterministic output timestamps. We will replace these to static message for
   261  	// stable comparison in tests.
   262  	var sanitized []string
   263  	for _, line := range strings.Split(stderr, "\n") {
   264  		// [PASS] \"gcr.io/kpt-fn/set-namespace:v0.1.3\" in 2.0s
   265  		if strings.HasPrefix(line, "[PASS]") || strings.HasPrefix(line, "[FAIL]") {
   266  			tokens := strings.Fields(line)
   267  			if len(tokens) == 4 && tokens[2] == "in" {
   268  				tokens[3] = "0s"
   269  				line = strings.Join(tokens, " ")
   270  			}
   271  		}
   272  		sanitized = append(sanitized, line)
   273  	}
   274  	return strings.Join(sanitized, "\n")
   275  }
   276  
   277  // IsFnResultExpected determines if function results are expected for this testcase.
   278  func (r *Runner) IsFnResultExpected() bool {
   279  	_, err := os.ReadFile(filepath.Join(r.testCase.Path, expectedDir, expectedResultsFile))
   280  	return err == nil
   281  }
   282  
   283  // IsOutOfPlace determines if command output is saved in a different directory (out-of-place).
   284  func (r *Runner) IsOutOfPlace() bool {
   285  	_, err := os.ReadDir(filepath.Join(r.testCase.Path, outDir))
   286  	return err == nil
   287  }
   288  
   289  func (r *Runner) runFnRender() error {
   290  	r.t.Logf("Running test against package %s\n", r.pkgName)
   291  	tmpDir, err := os.MkdirTemp("", "kpt-pipeline-e2e-*")
   292  	if err != nil {
   293  		return fmt.Errorf("failed to create temporary dir: %w", err)
   294  	}
   295  	if r.testCase.Config.Debug {
   296  		fmt.Printf("Running test against package %s in dir %s \n", r.pkgName, tmpDir)
   297  	}
   298  	if !r.testCase.Config.Debug {
   299  		// if debug is true, keep the test directory around for debugging
   300  		defer os.RemoveAll(tmpDir)
   301  	}
   302  	pkgPath := filepath.Join(tmpDir, r.pkgName)
   303  	// create dir to store untouched pkg to compare against
   304  	origPkgPath := filepath.Join(tmpDir, "original")
   305  	err = os.Mkdir(origPkgPath, 0755)
   306  	if err != nil {
   307  		return fmt.Errorf("failed to create original dir %s: %w", origPkgPath, err)
   308  	}
   309  
   310  	var resultsDir, destDir string
   311  
   312  	if r.IsFnResultExpected() {
   313  		resultsDir = filepath.Join(tmpDir, "results")
   314  	}
   315  
   316  	if r.IsOutOfPlace() {
   317  		destDir = filepath.Join(pkgPath, outDir)
   318  	}
   319  
   320  	// copy package to temp directory
   321  	err = copyDir(r.testCase.Path, pkgPath)
   322  	if err != nil {
   323  		return fmt.Errorf("failed to copy package: %w", err)
   324  	}
   325  	err = copyDir(r.testCase.Path, origPkgPath)
   326  	if err != nil {
   327  		return fmt.Errorf("failed to copy package: %w", err)
   328  	}
   329  
   330  	// init and commit package files
   331  	err = r.preparePackage(pkgPath)
   332  	if err != nil {
   333  		return fmt.Errorf("failed to prepare package: %w", err)
   334  	}
   335  
   336  	// run function
   337  	for i := 0; i < r.testCase.Config.RunCount(); i++ {
   338  		err = r.runSetupScript(pkgPath)
   339  		if err != nil {
   340  			return err
   341  		}
   342  
   343  		var cmd *exec.Cmd
   344  
   345  		execScriptPath, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, execScript))
   346  		if err != nil {
   347  			return err
   348  		}
   349  
   350  		if _, err := os.Stat(execScriptPath); err == nil {
   351  			cmd = getCommand(pkgPath, "bash", []string{execScriptPath})
   352  		} else {
   353  			kptArgs := []string{"fn", "render", pkgPath}
   354  
   355  			if resultsDir != "" {
   356  				kptArgs = append(kptArgs, "--results-dir", resultsDir)
   357  			}
   358  
   359  			if destDir != "" {
   360  				kptArgs = append(kptArgs, "-o", destDir)
   361  			}
   362  
   363  			if r.testCase.Config.ImagePullPolicy != "" {
   364  				kptArgs = append(kptArgs, "--image-pull-policy", r.testCase.Config.ImagePullPolicy)
   365  			}
   366  
   367  			if r.testCase.Config.AllowExec {
   368  				kptArgs = append(kptArgs, "--allow-exec")
   369  			}
   370  
   371  			if r.testCase.Config.AllowWasm {
   372  				kptArgs = append(kptArgs, allowWasmFlag)
   373  			}
   374  
   375  			if r.testCase.Config.DisableOutputTruncate {
   376  				kptArgs = append(kptArgs, "--truncate-output=false")
   377  			}
   378  			cmd = getCommand("", r.kptBin, kptArgs)
   379  		}
   380  		r.t.Logf("running command: %v=%v %v", fnruntime.ContainerRuntimeEnv, os.Getenv(fnruntime.ContainerRuntimeEnv), cmd.String())
   381  		stdout, stderr, fnErr := runCommand(cmd)
   382  		// Update the diff file or results file if updateExpectedEnv is set.
   383  		if strings.ToLower(os.Getenv(updateExpectedEnv)) == "true" {
   384  			return r.updateExpected(pkgPath, resultsDir, filepath.Join(r.testCase.Path, expectedDir))
   385  		}
   386  
   387  		if fnErr != nil {
   388  			r.t.Logf("kpt error, stdout: %s; stderr: %s", stdout, stderr)
   389  		}
   390  		// compare results
   391  		err = r.compareResult(i, fnErr, stdout, sanitizeTimestamps(stderr), pkgPath, resultsDir)
   392  		if err != nil {
   393  			return err
   394  		}
   395  		// we passed result check, now we should run teardown script and break
   396  		// if the command error is expected
   397  		err = r.runTearDownScript(pkgPath)
   398  		if err != nil {
   399  			return err
   400  		}
   401  		if fnErr != nil {
   402  			break
   403  		}
   404  	}
   405  	return nil
   406  }
   407  
   408  func (r *Runner) preparePackage(pkgPath string) error {
   409  	err := gitInit(pkgPath)
   410  	if err != nil {
   411  		return err
   412  	}
   413  
   414  	err = gitAddAll(pkgPath)
   415  	if err != nil {
   416  		return err
   417  	}
   418  
   419  	err = gitCommit(pkgPath, "first")
   420  	if err != nil {
   421  		return err
   422  	}
   423  
   424  	r.initialCommit, err = getCommitHash(pkgPath)
   425  	return err
   426  }
   427  
   428  func (r *Runner) compareResult(cnt int, exitErr error, stdout string, stderr string, tmpPkgPath, resultsPath string) error {
   429  	expected, err := newExpected(tmpPkgPath)
   430  	if err != nil {
   431  		return err
   432  	}
   433  	// get exit code
   434  	exitCode := 0
   435  	if e, ok := exitErr.(*exec.ExitError); ok {
   436  		exitCode = e.ExitCode()
   437  	} else if exitErr != nil {
   438  		return fmt.Errorf("cannot get exit code, received error '%w'", exitErr)
   439  	}
   440  
   441  	if exitCode != r.testCase.Config.ExitCode {
   442  		return fmt.Errorf("actual exit code %d doesn't match expected %d", exitCode, r.testCase.Config.ExitCode)
   443  	}
   444  
   445  	// we only check output and results for the first iteration of running because
   446  	// idempotency is only applied to changes in file system.
   447  	if cnt == 0 {
   448  		err = r.compareOutput(stdout, stderr)
   449  		if err != nil {
   450  			return err
   451  		}
   452  
   453  		// compare results
   454  		actual, err := readActualResults(resultsPath)
   455  		if err != nil {
   456  			return fmt.Errorf("failed to read actual results: %w", err)
   457  		}
   458  		diffOfResult, err := diffStrings(actual, expected.Results)
   459  		if err != nil {
   460  			return fmt.Errorf("error when run diff of results: %w: %s", err, diffOfResult)
   461  		}
   462  		if actual != expected.Results {
   463  			return fmt.Errorf("actual results doesn't match expected\nActual\n===\n%s\nDiff of Results\n===\n%s",
   464  				actual, diffOfResult)
   465  		}
   466  	}
   467  
   468  	// compare diff
   469  	actual, err := readActualDiff(tmpPkgPath, r.initialCommit)
   470  	if err != nil {
   471  		return fmt.Errorf("failed to read actual diff: %w", err)
   472  	}
   473  	if actual != expected.Diff {
   474  		diffOfDiff, err := diffStrings(actual, expected.Diff)
   475  		if err != nil {
   476  			return fmt.Errorf("error when run diff of diff: %w: %s", err, diffOfDiff)
   477  		}
   478  		return fmt.Errorf("actual diff doesn't match expected\nActual\n===\n%s\nDiff of Diff\n===\n%s",
   479  			actual, diffOfDiff)
   480  	}
   481  	return nil
   482  }
   483  
   484  // check stdout and stderr against expected
   485  func (r *Runner) compareOutput(stdout string, stderr string) error {
   486  	expectedStderr := r.testCase.Config.StdErr
   487  	if !strings.Contains(stderr, expectedStderr) {
   488  		r.t.Logf("stderr diff is %s", cmp.Diff(expectedStderr, stderr))
   489  		return fmt.Errorf("wanted stderr %q, got %q", expectedStderr, stderr)
   490  	}
   491  	stdErrRegEx := r.testCase.Config.StdErrRegEx
   492  	if stdErrRegEx != "" {
   493  		r, err := regexp.Compile(stdErrRegEx)
   494  		if err != nil {
   495  			return fmt.Errorf("unable to compile the regular expression %q: %w", stdErrRegEx, err)
   496  		}
   497  		if !r.MatchString(stderr) {
   498  			return fmt.Errorf("unable to match regular expression %q, got %v", stdErrRegEx, stderr)
   499  		}
   500  	}
   501  	expectedStdout := r.testCase.Config.StdOut
   502  	if !strings.Contains(stdout, expectedStdout) {
   503  		r.t.Logf("stdout diff is %s", cmp.Diff(expectedStdout, stdout))
   504  		return fmt.Errorf("wanted stdout %q, got %q", expectedStdout, stdout)
   505  	}
   506  	return nil
   507  }
   508  
   509  func (r *Runner) Skip() bool {
   510  	return r.testCase.Config.Skip
   511  }
   512  
   513  func readActualResults(resultsPath string) (string, error) {
   514  	// no results
   515  	if resultsPath == "" {
   516  		return "", nil
   517  	}
   518  	l, err := os.ReadDir(resultsPath)
   519  	if err != nil {
   520  		return "", fmt.Errorf("failed to get files in results dir: %w", err)
   521  	}
   522  	if len(l) > 1 {
   523  		return "", fmt.Errorf("unexpected results files number %d, should be 0 or 1", len(l))
   524  	}
   525  	if len(l) == 0 {
   526  		// no result file
   527  		return "", nil
   528  	}
   529  	resultsFile := l[0].Name()
   530  	actualResults, err := os.ReadFile(filepath.Join(resultsPath, resultsFile))
   531  	if err != nil {
   532  		return "", fmt.Errorf("failed to read actual results: %w", err)
   533  	}
   534  	return strings.TrimSpace(string(actualResults)), nil
   535  }
   536  
   537  func readActualDiff(path, origHash string) (string, error) {
   538  	err := gitAddAll(path)
   539  	if err != nil {
   540  		return "", err
   541  	}
   542  	err = gitCommit(path, "second")
   543  	if err != nil {
   544  		return "", err
   545  	}
   546  	// diff with first commit
   547  	actualDiff, err := gitDiff(path, origHash, "HEAD")
   548  	if err != nil {
   549  		return "", err
   550  	}
   551  	return strings.TrimSpace(actualDiff), nil
   552  }
   553  
   554  // expected contains the expected result for the function running
   555  type expected struct {
   556  	Results string
   557  	Diff    string
   558  }
   559  
   560  func newExpected(path string) (expected, error) {
   561  	e := expected{}
   562  	// get expected results
   563  	expectedResults, err := os.ReadFile(filepath.Join(path, expectedDir, expectedResultsFile))
   564  	switch {
   565  	case os.IsNotExist(err):
   566  		e.Results = ""
   567  	case err != nil:
   568  		return e, fmt.Errorf("failed to read expected results: %w", err)
   569  	default:
   570  		e.Results = strings.TrimSpace(string(expectedResults))
   571  	}
   572  
   573  	// get expected diff
   574  	expectedDiff, err := os.ReadFile(filepath.Join(path, expectedDir, expectedDiffFile))
   575  	switch {
   576  	case os.IsNotExist(err):
   577  		e.Diff = ""
   578  	case err != nil:
   579  		return e, fmt.Errorf("failed to read expected diff: %w", err)
   580  	default:
   581  		e.Diff = strings.TrimSpace(string(expectedDiff))
   582  	}
   583  
   584  	return e, nil
   585  }
   586  
   587  func (r *Runner) updateExpected(tmpPkgPath, resultsPath, sourceOfTruthPath string) error {
   588  	if resultsPath != "" {
   589  		// We update results directory only when a result file already exists.
   590  		l, err := os.ReadDir(resultsPath)
   591  		if err != nil {
   592  			return err
   593  		}
   594  		if len(l) > 0 {
   595  			actualResults, err := readActualResults(resultsPath)
   596  			if err != nil {
   597  				return err
   598  			}
   599  			if actualResults != "" {
   600  				if err := os.WriteFile(filepath.Join(sourceOfTruthPath, expectedResultsFile), []byte(actualResults+"\n"), 0666); err != nil {
   601  					return err
   602  				}
   603  			}
   604  		}
   605  	}
   606  	actualDiff, err := readActualDiff(tmpPkgPath, r.initialCommit)
   607  	if err != nil {
   608  		return err
   609  	}
   610  	if actualDiff != "" {
   611  		if err := os.WriteFile(filepath.Join(sourceOfTruthPath, expectedDiffFile), []byte(actualDiff+"\n"), 0666); err != nil {
   612  			return err
   613  		}
   614  	}
   615  
   616  	return nil
   617  }