github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/base/dsfs/dstest/dstest.go (about)

     1  // Package dstest defines an interface for reading test cases from static files
     2  // leveraging directories of test dataset input files & expected output files
     3  package dstest
     4  
     5  import (
     6  	"bytes"
     7  	"crypto/sha1"
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  
    15  	logger "github.com/ipfs/go-log"
    16  	"github.com/jinzhu/copier"
    17  	"github.com/qri-io/dataset"
    18  	"github.com/qri-io/qfs"
    19  	"github.com/ugorji/go/codec"
    20  )
    21  
    22  var log = logger.Logger("dstest")
    23  
    24  const (
    25  	// InputDatasetFilename is the filename to use for an input dataset
    26  	InputDatasetFilename = "input.dataset.json"
    27  	// ExpectDatasetFilename is the filename to use to compare expected outputs
    28  	ExpectDatasetFilename = "expect.dataset.json"
    29  	// RenderedFilename is the file that represents an executed viz script
    30  	RenderedFilename = "rendered.html"
    31  )
    32  
    33  // TestCase is a dataset test case, usually built from a
    34  // directory of files for use in tests.
    35  // All files are optional for TestCase, but may be required
    36  // by the test itself.
    37  type TestCase struct {
    38  	// Path to the director on the local filesystem this test case is loaded from
    39  	Path string
    40  	// Name is the casename, should match directory name
    41  	Name string
    42  	// body.csv,body.json, etc
    43  	BodyFilename string
    44  	// test body in expected data format
    45  	Body []byte
    46  	// Filename of Transform Script
    47  	TransformScriptFilename string
    48  	// TransformScript bytes if one exists
    49  	TransformScript []byte
    50  	// Filename of Viz Script
    51  	VizScriptFilename string
    52  	// VizScript bytes if one exists
    53  	VizScript []byte
    54  	// Input is intended file for test input
    55  	// loads from input.dataset.json
    56  	Input *dataset.Dataset
    57  	//  Expect should match test output
    58  	// loads from expect.dataset.json
    59  	Expect *dataset.Dataset
    60  }
    61  
    62  // DatasetChecksum generates a fast, insecure hash of an encoded dataset,
    63  // useful for checking that expected dataset values haven't changed
    64  func DatasetChecksum(ds *dataset.Dataset) string {
    65  	buf := &bytes.Buffer{}
    66  	h := &codec.CborHandle{}
    67  	h.Canonical = true
    68  	if err := codec.NewEncoder(buf, h).Encode(ds); err != nil {
    69  		panic(err)
    70  	}
    71  
    72  	sum := sha1.Sum(buf.Bytes())
    73  	return hex.EncodeToString(sum[:])
    74  }
    75  
    76  var testCaseCache = make(map[string]TestCase)
    77  
    78  // BodyFile creates a new in-memory file from data & filename properties
    79  func (t TestCase) BodyFile() qfs.File {
    80  	return qfs.NewMemfileBytes(t.BodyFilename, t.Body)
    81  }
    82  
    83  // TransformScriptFile creates a qfs.File from testCase transform script data
    84  func (t TestCase) TransformScriptFile() (qfs.File, bool) {
    85  	if t.TransformScript == nil {
    86  		return nil, false
    87  	}
    88  	return qfs.NewMemfileBytes(t.TransformScriptFilename, t.TransformScript), true
    89  }
    90  
    91  // VizScriptFile creates a qfs.File from testCase transform script data
    92  func (t TestCase) VizScriptFile() (qfs.File, bool) {
    93  	if t.VizScript == nil {
    94  		return nil, false
    95  	}
    96  	return qfs.NewMemfileBytes(t.VizScriptFilename, t.VizScript), true
    97  }
    98  
    99  // RenderedFile returns a qfs.File of the rendered file if one exists
   100  func (t TestCase) RenderedFile() (qfs.File, error) {
   101  	path := filepath.Join(t.Path, RenderedFilename)
   102  	f, err := os.Open(path)
   103  	return qfs.NewMemfileReader(RenderedFilename, f), err
   104  }
   105  
   106  // BodyFilepath retuns the path to the first valid data file it can find,
   107  // which is a file named "data" that ends in an extension we support
   108  func BodyFilepath(dir string) (string, error) {
   109  	for _, df := range dataset.SupportedDataFormats() {
   110  		path := fmt.Sprintf("%s/body.%s", dir, df)
   111  		if _, err := os.Stat(path); !os.IsNotExist(err) {
   112  			return path, nil
   113  		}
   114  	}
   115  	return "", os.ErrNotExist
   116  }
   117  
   118  // LoadTestCases loads a directory of case directories
   119  func LoadTestCases(dir string) (tcs map[string]TestCase, err error) {
   120  	tcs = map[string]TestCase{}
   121  	fis, err := ioutil.ReadDir(dir)
   122  	if err != nil {
   123  		return
   124  	}
   125  	for _, fi := range fis {
   126  		if fi.IsDir() {
   127  			if tc, err := NewTestCaseFromDir(filepath.Join(dir, fi.Name())); err == nil {
   128  				tcs[fi.Name()] = tc
   129  			}
   130  		}
   131  	}
   132  	return
   133  }
   134  
   135  // NewTestCaseFromDir creates a test case from a directory of static test files
   136  // dir should be the path to the directory to check, and any parsing errors will
   137  // be logged using t.Log methods
   138  func NewTestCaseFromDir(dir string) (tc TestCase, err error) {
   139  	// TODO (b5): for now we need to disable the cache b/c copier.Copy can't
   140  	// copy unexported fields, which includes script files. We should switch
   141  	// the testcase.Input field to a method that creates new datset instances
   142  	// with fresh files on each call of input, this'll let us restore cache use
   143  	// and speed tests up again
   144  	// if got, ok := testCaseCache[dir]; ok {
   145  	// 	tc = TestCase{}
   146  	// 	copier.Copy(&tc, &got)
   147  	// 	return
   148  	// }
   149  	foundTestData := false
   150  
   151  	tc = TestCase{
   152  		Path: dir,
   153  		Name: filepath.Base(dir),
   154  	}
   155  
   156  	tc.Input, err = ReadDataset(dir, InputDatasetFilename)
   157  	if err != nil {
   158  		if os.IsNotExist(err) {
   159  			err = nil
   160  		} else {
   161  			return tc, fmt.Errorf("%s reading input dataset: %s", tc.Name, err.Error())
   162  		}
   163  	} else {
   164  		foundTestData = true
   165  	}
   166  	if tc.Input == nil {
   167  		tc.Input = &dataset.Dataset{}
   168  	}
   169  	tc.Input.Name = tc.Name
   170  
   171  	if tc.Body, tc.BodyFilename, err = ReadBodyData(dir); err != nil {
   172  		if err == os.ErrNotExist {
   173  			// Body is optional
   174  			err = nil
   175  		} else {
   176  			return tc, fmt.Errorf("error reading test case body for dir: %s: %s", dir, err.Error())
   177  		}
   178  	} else {
   179  		foundTestData = true
   180  		tc.Input.SetBodyFile(qfs.NewMemfileBytes(tc.BodyFilename, tc.Body))
   181  	}
   182  
   183  	if tc.TransformScript, tc.TransformScriptFilename, err = ReadInputTransformScript(dir); err != nil {
   184  		if err == os.ErrNotExist {
   185  			// TransformScript is optional
   186  			err = nil
   187  		} else {
   188  			return tc, fmt.Errorf("reading transform script: %s", err.Error())
   189  		}
   190  	} else {
   191  		foundTestData = true
   192  		if tc.Input.Transform == nil {
   193  			tc.Input.Transform = &dataset.Transform{}
   194  		}
   195  		tc.Input.Transform.SetScriptFile(qfs.NewMemfileBytes(tc.TransformScriptFilename, tc.TransformScript))
   196  	}
   197  
   198  	if tc.VizScript, tc.VizScriptFilename, err = ReadInputVizScript(dir); err != nil {
   199  		if err == os.ErrNotExist {
   200  			// VizScript is optional
   201  			err = nil
   202  		} else {
   203  			return tc, fmt.Errorf("reading viz script: %s", err.Error())
   204  		}
   205  	} else {
   206  		foundTestData = true
   207  		if tc.Input.Viz == nil {
   208  			tc.Input.Viz = &dataset.Viz{}
   209  		}
   210  		tc.Input.Viz.SetScriptFile(qfs.NewMemfileBytes(tc.VizScriptFilename, tc.VizScript))
   211  	}
   212  
   213  	tc.Expect, err = ReadDataset(dir, ExpectDatasetFilename)
   214  	if err != nil {
   215  		if os.IsNotExist(err) {
   216  			err = nil
   217  		} else {
   218  			return tc, fmt.Errorf("%s: error loading expect dataset: %s", tc.Name, err)
   219  		}
   220  	} else {
   221  		foundTestData = true
   222  	}
   223  
   224  	if !foundTestData {
   225  		return tc, fmt.Errorf("%s no test data at path: %s", tc.Name, dir)
   226  	}
   227  
   228  	preserve := TestCase{}
   229  	copier.Copy(&preserve, &tc)
   230  	testCaseCache[dir] = preserve
   231  	return
   232  }
   233  
   234  // ReadDataset grabs a dataset for a given dir for a given filename
   235  func ReadDataset(dir, filename string) (*dataset.Dataset, error) {
   236  	data, err := ioutil.ReadFile(filepath.Join(dir, filename))
   237  	if err != nil {
   238  		log.Info(err.Error())
   239  		return nil, err
   240  	}
   241  
   242  	ds := &dataset.Dataset{}
   243  	return ds, json.Unmarshal(data, ds)
   244  }
   245  
   246  // ReadBodyData grabs input data
   247  func ReadBodyData(dir string) ([]byte, string, error) {
   248  	for _, df := range dataset.SupportedDataFormats() {
   249  		path := fmt.Sprintf("%s/body.%s", dir, df)
   250  		if f, err := os.Open(path); err == nil {
   251  			data, err := ioutil.ReadAll(f)
   252  			return data, fmt.Sprintf("body.%s", df), err
   253  		}
   254  	}
   255  	return nil, "", os.ErrNotExist
   256  }
   257  
   258  // ReadInputTransformScript grabs input transform bytes
   259  func ReadInputTransformScript(dir string) ([]byte, string, error) {
   260  	path := filepath.Join(dir, "transform.star")
   261  	if f, err := os.Open(path); err == nil {
   262  		data, err := ioutil.ReadAll(f)
   263  		return data, "transform.star", err
   264  	}
   265  	return nil, "", os.ErrNotExist
   266  }
   267  
   268  // ReadInputVizScript grabs input viz script bytes
   269  func ReadInputVizScript(dir string) ([]byte, string, error) {
   270  	path := filepath.Join(dir, "template.html")
   271  	if f, err := os.Open(path); err == nil {
   272  		data, err := ioutil.ReadAll(f)
   273  		return data, "template.html", err
   274  	}
   275  	return nil, "", os.ErrNotExist
   276  }