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  }