github.com/solo-io/cue@v0.4.7/internal/cuetxtar/txtar.go (about)

     1  // Copyright 2020 CUE Authors
     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 cuetxtar
    16  
    17  import (
    18  	"bufio"
    19  	"bytes"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/rogpeppe/go-internal/txtar"
    31  	"github.com/solo-io/cue/cue/ast"
    32  	"github.com/solo-io/cue/cue/build"
    33  	"github.com/solo-io/cue/cue/errors"
    34  	"github.com/solo-io/cue/cue/format"
    35  	"github.com/solo-io/cue/cue/load"
    36  	"github.com/solo-io/cue/internal/cuetest"
    37  )
    38  
    39  // A TxTarTest represents a test run that process all CUE tests in the txtar
    40  // format rooted in a given directory.
    41  type TxTarTest struct {
    42  	// Run TxTarTest on this directory.
    43  	Root string
    44  
    45  	// Name is a unique name for this test. The golden file for this test is
    46  	// derived from the out/<name> file in the .txtar file.
    47  	//
    48  	// TODO: by default derive from the current base directory name.
    49  	Name string
    50  
    51  	// If Update is true, TestTxTar will update the out/Name file if it differs
    52  	// from the original input. The user must set the output in Gold for this
    53  	// to be detected.
    54  	Update bool
    55  
    56  	// Skip is a map of tests to skip to their skip message.
    57  	Skip map[string]string
    58  
    59  	// ToDo is a map of tests that should be skipped now, but should be fixed.
    60  	ToDo map[string]string
    61  }
    62  
    63  // A Test represents a single test based on a .txtar file.
    64  //
    65  // A Test embeds *testing.T and should be used to report errors.
    66  //
    67  // A Test also embeds a *bytes.Buffer which is used to report test results,
    68  // which are compared against the golden file for the test in the TxTar archive.
    69  // If the test fails and the update flag is set to true, the Archive will be
    70  // updated and written to disk.
    71  type Test struct {
    72  	// Allow Test to be used as a T.
    73  	*testing.T
    74  
    75  	prefix   string
    76  	buf      *bytes.Buffer // the default buffer
    77  	outFiles []file
    78  
    79  	Archive *txtar.Archive
    80  
    81  	// The absolute path of the current test directory.
    82  	Dir string
    83  
    84  	hasGold bool
    85  }
    86  
    87  func (t *Test) Write(b []byte) (n int, err error) {
    88  	if t.buf == nil {
    89  		t.buf = &bytes.Buffer{}
    90  		t.outFiles = append(t.outFiles, file{t.prefix, t.buf})
    91  	}
    92  	return t.buf.Write(b)
    93  }
    94  
    95  type file struct {
    96  	name string
    97  	buf  *bytes.Buffer
    98  }
    99  
   100  func (t *Test) HasTag(key string) bool {
   101  	prefix := []byte("#" + key)
   102  	s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment))
   103  	for s.Scan() {
   104  		b := s.Bytes()
   105  		if bytes.Equal(bytes.TrimSpace(b), prefix) {
   106  			return true
   107  		}
   108  	}
   109  	return false
   110  }
   111  
   112  func (t *Test) Value(key string) (value string, ok bool) {
   113  	prefix := []byte("#" + key + ":")
   114  	s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment))
   115  	for s.Scan() {
   116  		b := s.Bytes()
   117  		if bytes.HasPrefix(b, prefix) {
   118  			return string(bytes.TrimSpace(b[len(prefix):])), true
   119  		}
   120  	}
   121  	return "", false
   122  }
   123  
   124  // Bool searches for a line starting with #key: value in the comment and
   125  // returns true if the key exists and the value is true.
   126  func (t *Test) Bool(key string) bool {
   127  	s, ok := t.Value(key)
   128  	return ok && s == "true"
   129  }
   130  
   131  // Rel converts filename to a normalized form so that it will given the same
   132  // output across different runs and OSes.
   133  func (t *Test) Rel(filename string) string {
   134  	rel, err := filepath.Rel(t.Dir, filename)
   135  	if err != nil {
   136  		return filepath.Base(filename)
   137  	}
   138  	return filepath.ToSlash(rel)
   139  }
   140  
   141  // WriteErrors writes strings and
   142  func (t *Test) WriteErrors(err errors.Error) {
   143  	if err != nil {
   144  		errors.Print(t, err, &errors.Config{
   145  			Cwd:     t.Dir,
   146  			ToSlash: true,
   147  		})
   148  	}
   149  }
   150  
   151  // Write file in a directory.
   152  func (t *Test) WriteFile(f *ast.File) {
   153  	// TODO: use FileWriter instead in separate CL.
   154  	fmt.Fprintln(t, "==", filepath.Base(f.Filename))
   155  	_, _ = t.Write(formatNode(t.T, f))
   156  }
   157  
   158  // Writer returns a Writer with the given name.
   159  func (t *Test) Writer(name string) io.Writer {
   160  	switch name {
   161  	case "":
   162  		name = t.prefix
   163  	default:
   164  		name = path.Join(t.prefix, name)
   165  	}
   166  
   167  	for _, f := range t.outFiles {
   168  		if f.name == name {
   169  			return f.buf
   170  		}
   171  	}
   172  
   173  	w := &bytes.Buffer{}
   174  	t.outFiles = append(t.outFiles, file{name, w})
   175  
   176  	if name == t.prefix {
   177  		t.buf = w
   178  	}
   179  
   180  	return w
   181  }
   182  
   183  func formatNode(t *testing.T, n ast.Node) []byte {
   184  	t.Helper()
   185  
   186  	b, err := format.Node(n)
   187  	if err != nil {
   188  		t.Fatal(err)
   189  	}
   190  	return b
   191  }
   192  
   193  // ValidInstances returns the valid instances for this .txtar file or skips the
   194  // test if there is an error loading the instances.
   195  func (t *Test) ValidInstances(args ...string) []*build.Instance {
   196  	a := t.RawInstances(args...)
   197  	for _, i := range a {
   198  		if i.Err != nil {
   199  			if t.hasGold {
   200  				t.Fatal("Parse error: ", i.Err)
   201  			}
   202  			t.Skip("Parse error: ", i.Err)
   203  		}
   204  	}
   205  	return a
   206  }
   207  
   208  // RawInstances returns the intstances represented by this .txtar file. The
   209  // returned instances are not checked for errors.
   210  func (t *Test) RawInstances(args ...string) []*build.Instance {
   211  	return Load(t.Archive, t.Dir, args...)
   212  }
   213  
   214  // Load loads the intstances of a txtar file. By default, it only loads
   215  // files in the root directory. Relative files in the archive are given an
   216  // absolution location by prefixing it with dir.
   217  func Load(a *txtar.Archive, dir string, args ...string) []*build.Instance {
   218  	auto := len(args) == 0
   219  	overlay := map[string]load.Source{}
   220  	for _, f := range a.Files {
   221  		if auto && !strings.Contains(f.Name, "/") {
   222  			args = append(args, f.Name)
   223  		}
   224  		overlay[filepath.Join(dir, f.Name)] = load.FromBytes(f.Data)
   225  	}
   226  
   227  	cfg := &load.Config{
   228  		Dir:     dir,
   229  		Overlay: overlay,
   230  	}
   231  
   232  	return load.Instances(args, cfg)
   233  }
   234  
   235  // Run runs tests defined in txtar files in root or its subdirectories.
   236  // Only tests for which an `old/name` test output file exists are run.
   237  func (x *TxTarTest) Run(t *testing.T, f func(tc *Test)) {
   238  	dir, err := os.Getwd()
   239  	if err != nil {
   240  		t.Fatal(err)
   241  	}
   242  
   243  	root := x.Root
   244  
   245  	err = filepath.Walk(root, func(fullpath string, info os.FileInfo, err error) error {
   246  		if err != nil {
   247  			t.Fatal(err)
   248  		}
   249  
   250  		if info.IsDir() || filepath.Ext(fullpath) != ".txtar" {
   251  			return nil
   252  		}
   253  
   254  		str := filepath.ToSlash(fullpath)
   255  		p := strings.Index(str, "/testdata/")
   256  		testName := str[p+len("/testdata/") : len(str)-len(".txtar")]
   257  
   258  		t.Run(testName, func(t *testing.T) {
   259  			a, err := txtar.ParseFile(fullpath)
   260  			if err != nil {
   261  				t.Fatalf("error parsing txtar file: %v", err)
   262  			}
   263  
   264  			tc := &Test{
   265  				T:       t,
   266  				Archive: a,
   267  				Dir:     filepath.Dir(filepath.Join(dir, fullpath)),
   268  
   269  				prefix: path.Join("out", x.Name),
   270  			}
   271  
   272  			for _, f := range a.Files {
   273  				// TODO: not entirely correct.
   274  				if strings.HasPrefix(f.Name, tc.prefix) {
   275  					tc.hasGold = true
   276  				}
   277  			}
   278  
   279  			if tc.HasTag("skip") {
   280  				t.Skip()
   281  			}
   282  
   283  			if msg, ok := x.Skip[testName]; ok {
   284  				t.Skip(msg)
   285  			}
   286  			if msg, ok := x.ToDo[testName]; ok {
   287  				t.Skip(msg)
   288  			}
   289  
   290  			f(tc)
   291  
   292  			update := false
   293  			for _, sub := range tc.outFiles {
   294  				var gold *txtar.File
   295  				for i, f := range a.Files {
   296  					if f.Name == sub.name {
   297  						gold = &a.Files[i]
   298  					}
   299  				}
   300  
   301  				result := sub.buf.Bytes()
   302  
   303  				switch {
   304  				case gold == nil:
   305  					a.Files = append(a.Files, txtar.File{Name: sub.name})
   306  					gold = &a.Files[len(a.Files)-1]
   307  
   308  				case bytes.Equal(gold.Data, result):
   309  					continue
   310  				}
   311  
   312  				if x.Update || cuetest.UpdateGoldenFiles {
   313  					update = true
   314  					gold.Data = result
   315  					continue
   316  				}
   317  
   318  				t.Errorf("result for %s differs:\n%s",
   319  					sub.name,
   320  					cmp.Diff(string(gold.Data), string(result)))
   321  			}
   322  
   323  			if update {
   324  				err = ioutil.WriteFile(fullpath, txtar.Format(a), 0644)
   325  				if err != nil {
   326  					t.Fatal(err)
   327  				}
   328  			}
   329  		})
   330  
   331  		return nil
   332  	})
   333  
   334  	if err != nil {
   335  		t.Fatal(err)
   336  	}
   337  }