github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/automation/workflow/wftest/wftest.go (about) 1 package wftest 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "testing" 11 "time" 12 13 "github.com/jinzhu/copier" 14 "github.com/qri-io/dataset" 15 "github.com/qri-io/qri/automation/workflow" 16 "github.com/qri-io/qri/base/dsfs/dstest" 17 "github.com/qri-io/qri/lib" 18 "github.com/qri-io/qri/profile" 19 ) 20 21 const ( 22 // InputWorkflowFilename is the filename to use for an input workflow 23 InputWorkflowFilename = "input.workflow.json" 24 // ExpectWorkflowFilename is the filename to use to compare expected outputs 25 ExpectWorkflowFilename = "expect.workflow.json" 26 ) 27 28 var ( 29 defaultTestCasesDir = "" 30 testCaseCache = []*TestCase{} 31 ) 32 33 // TestCase is a workflow test case, usually built from a 34 // directory of files for use in tests 35 // Each workflow for each test case must have an associated 36 // dataset 37 type TestCase struct { 38 // Path to the directory on the local filesystem this test case is loaded from 39 Path string 40 // Name is the casename, should match the directory name 41 Name string 42 // Input is intended file for test input 43 // loads from input.workflow.json 44 Input *workflow.Workflow 45 // Expect should match the test output 46 // loads from expect.workflow.json 47 Expect *workflow.Workflow 48 Dataset *dstest.TestCase 49 } 50 51 // LoadDefaultTestCases loads test cases from this package 52 // the OwnerID is the first key in the `auth/key/test/keys.go`, which is the 53 // primary key used in the default test configs 54 func LoadDefaultTestCases() ([]*TestCase, error) { 55 created, err := time.Parse(time.RFC3339, "2021-08-03T10:33:36.23224-04:00") 56 if err != nil { 57 return nil, err 58 } 59 return []*TestCase{ 60 { 61 Name: "no_change", 62 Input: &workflow.Workflow{ 63 ID: "9e45m9ll-b366-0945-2743-8mm90731jl72", 64 OwnerID: "QmeL2mdVka1eahKENjehK6tBxkkpk5dNQ1qMcgWi7Hrb4B", 65 Created: &created, 66 Active: true, 67 }, 68 Expect: &workflow.Workflow{ 69 ID: "9e45m9ll-b366-0945-2743-8mm90731jl72", 70 OwnerID: "QmeL2mdVka1eahKENjehK6tBxkkpk5dNQ1qMcgWi7Hrb4B", 71 Created: &created, 72 Active: true, 73 }, 74 Dataset: &dstest.TestCase{ 75 Name: "no_change", 76 Input: &dataset.Dataset{ 77 Transform: &dataset.Transform{ 78 Steps: []*dataset.TransformStep{ 79 { 80 Name: "transform", 81 Syntax: "starlark", 82 Script: "load(\"dataframe.star\", \"dataframe\")\nds = dataset.latest()\nbody = '''a,b,c\n1,2,3\n4,5,6\n'''\nds.body = dataframe.parse_csv(body)\ndataset.commit(ds)", 83 }, 84 }, 85 }, 86 }, 87 }, 88 }, 89 { 90 Name: "now", 91 Input: &workflow.Workflow{ 92 ID: "1d79b0ff-a133-4731-9892-5ee01842ca81", 93 OwnerID: "QmeL2mdVka1eahKENjehK6tBxkkpk5dNQ1qMcgWi7Hrb4B", 94 Created: &created, 95 Active: true, 96 }, 97 Expect: &workflow.Workflow{ 98 ID: "1d79b0ff-a133-4731-9892-5ee01842ca81", 99 OwnerID: "QmeL2mdVka1eahKENjehK6tBxkkpk5dNQ1qMcgWi7Hrb4B", 100 Created: &created, 101 Active: true, 102 }, 103 Dataset: &dstest.TestCase{ 104 Name: "now", 105 Input: &dataset.Dataset{ 106 Transform: &dataset.Transform{ 107 Steps: []*dataset.TransformStep{ 108 { 109 Syntax: "starlark", 110 Name: "setup", 111 Script: "load(\"time.star\", \"time\")\nload(\"dataframe.star\", \"dataframe\")\nds = dataset.latest()", 112 }, 113 { 114 Syntax: "starlark", 115 Name: "transform", 116 Script: "currentTime = time.now()\nbody = [\n ['timestamp']\n ]\nbody.append([str(currentTime)])\nds.body = body\ndataset.commit(ds)", 117 }, 118 }, 119 }, 120 }, 121 }, 122 }, 123 }, nil 124 } 125 126 // LoadTestCases loads a directory of case directories 127 func LoadTestCases(dir string) (tcs []*TestCase, err error) { 128 tcs = []*TestCase{} 129 fis, err := ioutil.ReadDir(dir) 130 if err != nil { 131 return 132 } 133 for _, fi := range fis { 134 if fi.IsDir() { 135 tc, err := NewTestCaseFromDir(filepath.Join(dir, fi.Name())) 136 if err != nil { 137 return nil, err 138 } 139 tcs = append(tcs, tc) 140 } 141 } 142 return 143 } 144 145 // NewTestCaseFromDir creates a test case from a directory of static test files 146 // dir should be the path to the directory to check 147 func NewTestCaseFromDir(dir string) (tc *TestCase, err error) { 148 dsTestCase, err := dstest.NewTestCaseFromDir(dir) 149 if err != nil { 150 return nil, err 151 } 152 153 tc = &TestCase{ 154 Path: dir, 155 Name: filepath.Base(dir), 156 Dataset: &dsTestCase, 157 } 158 159 tc.Input, err = ReadWorkflow(filepath.Join(dir, InputWorkflowFilename)) 160 if err != nil { 161 return nil, fmt.Errorf("%s reading input workflow: %s", tc.Name, err) 162 } 163 164 tc.Expect, err = ReadWorkflow(filepath.Join(dir, ExpectWorkflowFilename)) 165 if err != nil { 166 if os.IsNotExist(err) { 167 err = nil 168 } else { 169 return nil, fmt.Errorf("%s: error loading expect workflow: %s", tc.Name, err) 170 } 171 } 172 173 preserve := TestCase{} 174 copier.Copy(&preserve, &tc) 175 testCaseCache = append(testCaseCache, tc) 176 return 177 } 178 179 // ReadWorkflow grabs a workflow from a given filepath 180 func ReadWorkflow(filepath string) (*workflow.Workflow, error) { 181 data, err := ioutil.ReadFile(filepath) 182 if err != nil { 183 return nil, err 184 } 185 186 wf := &workflow.Workflow{} 187 return wf, json.Unmarshal(data, wf) 188 } 189 190 // A TestRunner give you all the information needed to save workflow 191 type TestRunner interface { 192 Instance() *lib.Instance 193 Owner() *profile.Profile 194 Context() context.Context 195 WorkflowStore() workflow.Store 196 } 197 198 // MustAddWorkflowsFromDir adds workflows and their associated datasets, 199 // that have been loaded from a directory, to the test runner's instance 200 // and workflow store 201 func MustAddWorkflowsFromDir(t *testing.T, tr TestRunner, dir string) { 202 tcs, err := LoadTestCases(dir) 203 if err != nil { 204 t.Fatal(err) 205 } 206 if err := addWorkflowsToTestRunner(tr, tcs); err != nil { 207 t.Fatal(err) 208 } 209 } 210 211 // MustAddDefaultWorkflows adds the default workflows and their associated 212 // datasets to the test runner's instance and workflow store 213 func MustAddDefaultWorkflows(t *testing.T, tr TestRunner) { 214 tcs, err := LoadDefaultTestCases() 215 if err != nil { 216 t.Fatal(err) 217 } 218 if err := addWorkflowsToTestRunner(tr, tcs); err != nil { 219 t.Fatal(err) 220 } 221 } 222 223 // addWorkflowToTestRunner adds workflows and their associated datasets 224 // to the test runner's instance and workflow.Store 225 func addWorkflowsToTestRunner(tr TestRunner, tcs []*TestCase) error { 226 var err error 227 owner := tr.Owner() 228 inst := tr.Instance() 229 wfs := tr.WorkflowStore() 230 ctx := tr.Context() 231 232 if owner == nil { 233 return fmt.Errorf("missing profile") 234 } 235 if inst == nil { 236 return fmt.Errorf("missing instance") 237 } 238 if wfs == nil { 239 return fmt.Errorf("missing workflow store") 240 } 241 242 for _, tc := range tcs { 243 ds := tc.Dataset.Input 244 wf := tc.Input 245 wf.OwnerID = owner.ID 246 ref := fmt.Sprintf("%s/%s", owner.Peername, tc.Name) 247 248 // TODO(ramfox): when we can save with out a body or structure, remove the `Apply` flag 249 ds, err = inst.Dataset().Save(ctx, &lib.SaveParams{Ref: ref, Dataset: ds, Apply: true}) 250 if err != nil { 251 return fmt.Errorf("saving dataset %q: %w", ref, err) 252 } 253 wf.InitID = ds.ID 254 wf, err = wfs.Put(ctx, wf) 255 if err != nil { 256 return fmt.Errorf("adding workflow for dataset %q: %w", ref, err) 257 } 258 } 259 return nil 260 }