Skip to content

Commit

Permalink
Add cuetxtar for test helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
sam boyer committed Sep 27, 2021
1 parent 1e0a72c commit 5e6f5f5
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
cuelang.org/go v0.4.0
github.com/google/go-cmp v0.5.5
github.com/iancoleman/strcase v0.1.3
github.com/rogpeppe/go-internal v1.8.0
golang.org/x/tools v0.0.0-20200612220849-54c614fe050c
gotest.tools v2.2.0+incompatible // indirect
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc/go.mod h1
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.6.2-0.20200830194709-1115b6af0369 h1:wdCVGtPadWC/ZuuLC7Hv58VQ5UF7V98ewE71n5mJfrM=
github.com/rogpeppe/go-internal v1.6.2-0.20200830194709-1115b6af0369/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
Expand Down
338 changes: 338 additions & 0 deletions internal/cuetxtar/cuetxtar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
// Copyright 2020 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cuetxtar

// COPIED FROM CUELANG REPO

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

"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/load"
"github.com/google/go-cmp/cmp"
"github.com/rogpeppe/go-internal/txtar"
)

// A TxTarTest represents a test run that process all CUE tests in the txtar
// format rooted in a given directory.
type TxTarTest struct {
// Run TxTarTest on this directory.
Root string

// Name is a unique name for this test. The golden file for this test is
// derived from the out/<name> file in the .txtar file.
//
// TODO: by default derive from the current base directory name.
Name string

// If Update is true, TestTxTar will update the out/Name file if it differs
// from the original input. The user must set the output in Gold for this
// to be detected.
Update bool

// Skip is a map of tests to skip to their skip message.
Skip map[string]string

// ToDo is a map of tests that should be skipped now, but should be fixed.
ToDo map[string]string
}

// A Test represents a single test based on a .txtar file.
//
// A Test embeds *testing.T and should be used to report errors.
//
// A Test also embeds a *bytes.Buffer which is used to report test results,
// which are compared against the golden file for the test in the TxTar archive.
// If the test fails and the update flag is set to true, the Archive will be
// updated and written to disk.
type Test struct {
// Allow Test to be used as a T.
*testing.T

prefix string
buf *bytes.Buffer // the default buffer
outFiles []file

Archive *txtar.Archive

// The absolute path of the current test directory.
Dir string

hasGold bool
}

func (t *Test) Write(b []byte) (n int, err error) {
if t.buf == nil {
t.buf = &bytes.Buffer{}
t.outFiles = append(t.outFiles, file{t.prefix, t.buf})
}
return t.buf.Write(b)
}

type file struct {
name string
buf *bytes.Buffer
}

func (t *Test) HasTag(key string) bool {
prefix := []byte("#" + key)
s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment))
for s.Scan() {
b := s.Bytes()
if bytes.Equal(bytes.TrimSpace(b), prefix) {
return true
}
}
return false
}

func (t *Test) Value(key string) (value string, ok bool) {
prefix := []byte("#" + key + ":")
s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment))
for s.Scan() {
b := s.Bytes()
if bytes.HasPrefix(b, prefix) {
return string(bytes.TrimSpace(b[len(prefix):])), true
}
}
return "", false
}

// Bool searches for a line starting with #key: value in the comment and
// returns true if the key exists and the value is true.
func (t *Test) Bool(key string) bool {
s, ok := t.Value(key)
return ok && s == "true"
}

// Rel converts filename to a normalized form so that it will given the same
// output across different runs and OSes.
func (t *Test) Rel(filename string) string {
rel, err := filepath.Rel(t.Dir, filename)
if err != nil {
return filepath.Base(filename)
}
return filepath.ToSlash(rel)
}

// WriteErrors writes strings and
func (t *Test) WriteErrors(err errors.Error) {
if err != nil {
errors.Print(t, err, &errors.Config{
Cwd: t.Dir,
ToSlash: true,
})
}
}

// Write file in a directory.
func (t *Test) WriteFile(f *ast.File) {
// TODO: use FileWriter instead in separate CL.
fmt.Fprintln(t, "==", filepath.Base(f.Filename))
_, _ = t.Write(formatNode(t.T, f))
}

// Writer returns a Writer with the given name.
func (t *Test) Writer(name string) io.Writer {
switch name {
case "":
name = t.prefix
default:
name = path.Join(t.prefix, name)
}

for _, f := range t.outFiles {
if f.name == name {
return f.buf
}
}

w := &bytes.Buffer{}
t.outFiles = append(t.outFiles, file{name, w})

if name == t.prefix {
t.buf = w
}

return w
}

func formatNode(t *testing.T, n ast.Node) []byte {
t.Helper()

b, err := format.Node(n)
if err != nil {
t.Fatal(err)
}
return b
}

// ValidInstances returns the valid instances for this .txtar file or skips the
// test if there is an error loading the instances.
func (t *Test) ValidInstances(args ...string) []*build.Instance {
a := t.RawInstances(args...)
for _, i := range a {
if i.Err != nil {
if t.hasGold {
t.Fatal("Parse error: ", i.Err)
}
t.Skip("Parse error: ", i.Err)
}
}
return a
}

// RawInstances returns the intstances represented by this .txtar file. The
// returned instances are not checked for errors.
func (t *Test) RawInstances(args ...string) []*build.Instance {
return Load(t.Archive, t.Dir, args...)
}

// Load loads the intstances of a txtar file. By default, it only loads
// files in the root directory. Relative files in the archive are given an
// absolution location by prefixing it with dir.
func Load(a *txtar.Archive, dir string, args ...string) []*build.Instance {
auto := len(args) == 0
overlay := map[string]load.Source{}
for _, f := range a.Files {
if auto && !strings.Contains(f.Name, "/") {
args = append(args, f.Name)
}
overlay[filepath.Join(dir, f.Name)] = load.FromBytes(f.Data)
}

cfg := &load.Config{
Dir: dir,
Overlay: overlay,
}

return load.Instances(args, cfg)
}

// Run runs tests defined in txtar files in root or its subdirectories.
// Only tests for which an `old/name` test output file exists are run.
func (x *TxTarTest) Run(t *testing.T, f func(tc *Test)) {
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}

root := x.Root

err = filepath.Walk(root, func(fullpath string, info os.FileInfo, err error) error {
if err != nil {
t.Fatal(err)
}

if info.IsDir() || filepath.Ext(fullpath) != ".txtar" {
return nil
}

str := filepath.ToSlash(fullpath)
p := strings.Index(str, "/testdata/")
testName := str[p+len("/testdata/") : len(str)-len(".txtar")]

t.Run(testName, func(t *testing.T) {
a, err := txtar.ParseFile(fullpath)
if err != nil {
t.Fatalf("error parsing txtar file: %v", err)
}

tc := &Test{
T: t,
Archive: a,
Dir: filepath.Dir(filepath.Join(dir, fullpath)),

prefix: path.Join("out", x.Name),
}

for _, f := range a.Files {
// TODO: not entirely correct.
if strings.HasPrefix(f.Name, tc.prefix) {
tc.hasGold = true
}
}

if tc.HasTag("skip") {
t.Skip()
}

if msg, ok := x.Skip[testName]; ok {
t.Skip(msg)
}
if msg, ok := x.ToDo[testName]; ok {
t.Skip(msg)
}

f(tc)

update := false
for _, sub := range tc.outFiles {
var gold *txtar.File
for i, f := range a.Files {
if f.Name == sub.name {
gold = &a.Files[i]
}
}

result := sub.buf.Bytes()

switch {
case gold == nil:
a.Files = append(a.Files, txtar.File{Name: sub.name})
gold = &a.Files[len(a.Files)-1]

case bytes.Equal(gold.Data, result):
continue
}

if x.Update {
update = true
gold.Data = result
continue
}

t.Errorf("result for %s differs:\n%s",
sub.name,
cmp.Diff(string(gold.Data), string(result)))
}

if update {
err = ioutil.WriteFile(fullpath, txtar.Format(a), 0644)
if err != nil {
t.Fatal(err)
}
}
})

return nil
})

if err != nil {
t.Fatal(err)
}
}

0 comments on commit 5e6f5f5

Please sign in to comment.