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 }