github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/datasets_test.go (about)

     1  package lib
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"os"
    13  	"path"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"testing"
    18  
    19  	"github.com/ghodss/yaml"
    20  	"github.com/google/go-cmp/cmp"
    21  	cmpopts "github.com/google/go-cmp/cmp/cmpopts"
    22  	"github.com/qri-io/dataset"
    23  	"github.com/qri-io/dataset/dsio"
    24  	"github.com/qri-io/dataset/dstest"
    25  	"github.com/qri-io/dataset/preview"
    26  	"github.com/qri-io/qfs"
    27  	"github.com/qri-io/qri/base"
    28  	"github.com/qri-io/qri/base/dsfs"
    29  	"github.com/qri-io/qri/base/params"
    30  	testcfg "github.com/qri-io/qri/config/test"
    31  	"github.com/qri-io/qri/dsref"
    32  	"github.com/qri-io/qri/event"
    33  	"github.com/qri-io/qri/p2p"
    34  	p2ptest "github.com/qri-io/qri/p2p/test"
    35  	reporef "github.com/qri-io/qri/repo/ref"
    36  	testrepo "github.com/qri-io/qri/repo/test"
    37  )
    38  
    39  func TestDatasetRequestsSave(t *testing.T) {
    40  	ctx, done := context.WithCancel(context.Background())
    41  	defer done()
    42  
    43  	mr, err := testrepo.NewTestRepo()
    44  	if err != nil {
    45  		t.Fatalf("error allocating test repo: %s", err.Error())
    46  	}
    47  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
    48  	if err != nil {
    49  		t.Fatal(err.Error())
    50  	}
    51  
    52  	jobsBodyPath, err := dstest.BodyFilepath("testdata/jobs_by_automation")
    53  	if err != nil {
    54  		t.Fatal(err.Error())
    55  	}
    56  
    57  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    58  		res := `city,pop,avg_age,in_usa
    59  	toronto,40000000,55.5,false
    60  	new york,8500000,44.4,true
    61  	chicago,300000,44.4,true
    62  	chatham,35000,65.25,true
    63  	raleigh,250000,50.65,true
    64  	sarnia,550000,55.65,false
    65  `
    66  		w.Write([]byte(res))
    67  	}))
    68  
    69  	badDataS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    70  		w.Write([]byte(`\\\{"json":"data"}`))
    71  	}))
    72  
    73  	citiesMetaOnePath := tempDatasetFile(t, "*-cities_meta_1.json", &dataset.Dataset{Meta: &dataset.Meta{Title: "updated name of movies dataset"}})
    74  	citiesMetaTwoPath := tempDatasetFile(t, "*-cities_meta_2.json", &dataset.Dataset{Meta: &dataset.Meta{Description: "Description, b/c bodies are the same thing"}})
    75  	defer func() {
    76  		os.RemoveAll(citiesMetaOnePath)
    77  		os.RemoveAll(citiesMetaTwoPath)
    78  	}()
    79  
    80  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
    81  
    82  	privateErrMsg := "option to make dataset private not yet implemented, refer to https://github.com/qri-io/qri/issues/291 for updates"
    83  	_, err = inst.Dataset().Save(ctx, &SaveParams{Private: true})
    84  	if err == nil {
    85  		t.Errorf("expected datset to error")
    86  	} else if err.Error() != privateErrMsg {
    87  		t.Errorf("private flag error mismatch: expected: '%s', got: '%s'", privateErrMsg, err.Error())
    88  	}
    89  
    90  	good := []struct {
    91  		description string
    92  		params      SaveParams
    93  		res         *reporef.DatasetRef
    94  	}{
    95  		{"body file", SaveParams{Ref: "me/jobs_ranked_by_automation_prob", BodyPath: jobsBodyPath}, nil},
    96  		{"no body", SaveParams{Ref: "me/no_body_dataset", Dataset: &dataset.Dataset{Meta: &dataset.Meta{Title: "big things cooking"}}}, nil},
    97  		{"meta set title", SaveParams{Ref: "me/cities", FilePaths: []string{citiesMetaOnePath}}, nil},
    98  		{"meta set description, supply same body", SaveParams{Ref: "me/cities", FilePaths: []string{citiesMetaTwoPath}, BodyPath: s.URL + "/body.csv"}, nil},
    99  	}
   100  
   101  	for i, c := range good {
   102  		got, err := inst.Dataset().Save(ctx, &c.params)
   103  		if err != nil {
   104  			t.Errorf("case %d: '%s' unexpected error: %s", i, c.description, err.Error())
   105  			continue
   106  		}
   107  
   108  		if got != nil && c.res != nil {
   109  			expect := c.res.Dataset
   110  			if diff := dstest.CompareDatasets(expect, got); diff != "" {
   111  				t.Errorf("case %d ds mistmatch (-want +got):\n%s", i, diff)
   112  				continue
   113  			}
   114  		}
   115  	}
   116  
   117  	bad := []struct {
   118  		description string
   119  		params      SaveParams
   120  		err         string
   121  	}{
   122  
   123  		{"empty params", SaveParams{}, "no changes to save"},
   124  		{"", SaveParams{Ref: "me/bad", BodyPath: badDataS.URL + "/data.json"}, "determining dataset structure: invalid json data"},
   125  	}
   126  
   127  	for i, c := range bad {
   128  		_, err := inst.Dataset().Save(ctx, &c.params)
   129  		if err == nil {
   130  			t.Errorf("case %d: '%s' returned no error", i, c.description)
   131  		}
   132  		if err.Error() != c.err {
   133  			t.Errorf("case %d: '%s' error mismatch. expected:\n'%s'\ngot:\n'%s'", i, c.description, c.err, err.Error())
   134  		}
   135  	}
   136  }
   137  
   138  func tempDatasetFile(t *testing.T, fileName string, ds *dataset.Dataset) (path string) {
   139  	f, err := ioutil.TempFile("", fileName)
   140  	if err != nil {
   141  		t.Fatal(err)
   142  	}
   143  	if err := json.NewEncoder(f).Encode(ds); err != nil {
   144  		t.Fatal(err)
   145  	}
   146  	return f.Name()
   147  }
   148  
   149  func TestDatasetRequestsForceSave(t *testing.T) {
   150  	ctx, done := context.WithCancel(context.Background())
   151  	defer done()
   152  
   153  	node := newTestQriNode(t)
   154  	ref := addCitiesDataset(t, node)
   155  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   156  
   157  	_, err := inst.Dataset().Save(ctx, &SaveParams{Ref: ref.Alias()})
   158  	if err == nil {
   159  		t.Error("expected empty save without force flag to error")
   160  	}
   161  
   162  	_, err = inst.Dataset().Save(ctx, &SaveParams{
   163  		Ref:   ref.Alias(),
   164  		Force: true,
   165  	})
   166  	if err != nil {
   167  		t.Errorf("expected empty save with force flag to not error. got: %q", err.Error())
   168  	}
   169  }
   170  
   171  func TestDatasetRequestsSaveZip(t *testing.T) {
   172  	ctx, done := context.WithCancel(context.Background())
   173  	defer done()
   174  
   175  	mr, err := testrepo.NewTestRepo()
   176  	if err != nil {
   177  		t.Fatalf("error allocating test repo: %s", err.Error())
   178  	}
   179  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
   180  	if err != nil {
   181  		t.Fatal(err.Error())
   182  	}
   183  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   184  
   185  	// TODO (b5): import.zip has a ref.txt file that specifies test_user/test_repo as the dataset name,
   186  	// save now requires a string reference. we need to pick a behaviour here & write a test that enforces it
   187  	res, err := inst.Dataset().Save(ctx, &SaveParams{Ref: "me/huh", FilePaths: []string{"testdata/import.zip"}})
   188  	if err != nil {
   189  		t.Fatal(err.Error())
   190  	}
   191  
   192  	if res.Commit.Title != "Test Title" {
   193  		t.Fatalf("Expected 'Test Title', got '%s'", res.Commit.Title)
   194  	}
   195  	if res.Meta.Title != "Test Repo" {
   196  		t.Fatalf("Expected 'Test Repo', got '%s'", res.Meta.Title)
   197  	}
   198  }
   199  
   200  func TestDatasetRequestsSaveApply(t *testing.T) {
   201  	run := newTestRunner(t)
   202  	defer run.Delete()
   203  
   204  	// Trying to save using apply without a transform is an error
   205  	_, err := run.SaveWithParams(&SaveParams{
   206  		Ref:      "me/cities_ds",
   207  		BodyPath: "testdata/cities_2/body.csv",
   208  		Apply:    true,
   209  	})
   210  	if err == nil {
   211  		t.Fatal("expected an error, did not get one")
   212  	}
   213  	expectErr := `cannot apply while saving without a transform`
   214  	if diff := cmp.Diff(expectErr, err.Error()); diff != "" {
   215  		t.Errorf("error mismatch (-want +got):%s\n", diff)
   216  	}
   217  
   218  	// Save using apply and a transform, for a new dataset
   219  	_, err = run.SaveWithParams(&SaveParams{
   220  		Ref:       "me/hello",
   221  		FilePaths: []string{"testdata/tf/transform.star"},
   222  		Apply:     true,
   223  	})
   224  	if err != nil {
   225  		t.Error(err)
   226  	}
   227  
   228  	// Save another dataset with a body
   229  	_, err = run.SaveWithParams(&SaveParams{
   230  		Ref:      "me/existing_ds",
   231  		BodyPath: "testdata/cities_2/body.csv",
   232  	})
   233  	if err != nil {
   234  		t.Error(err)
   235  	}
   236  
   237  	ds := run.MustGet(t, "me/existing_ds")
   238  	bodyPath := ds.BodyPath
   239  
   240  	// Save using apply and a transform, for dataset that already exists
   241  	_, err = run.SaveWithParams(&SaveParams{
   242  		Ref:       "me/existing_ds",
   243  		FilePaths: []string{"testdata/cities_2/add_city.star"},
   244  		Apply:     true,
   245  	})
   246  	if err != nil {
   247  		t.Error(err)
   248  	}
   249  
   250  	ds = run.MustGet(t, "me/existing_ds")
   251  	if ds.BodyPath == bodyPath {
   252  		t.Error("expected body path to change, but it did not change")
   253  	}
   254  
   255  	// Save another dataset with a body
   256  	_, err = run.SaveWithParams(&SaveParams{
   257  		Ref:      "me/another_ds",
   258  		BodyPath: "testdata/cities_2/body.csv",
   259  	})
   260  	if err != nil {
   261  		t.Error(err)
   262  	}
   263  
   264  	ds = run.MustGet(t, "me/another_ds")
   265  	bodyPath = ds.BodyPath
   266  
   267  	// Save by adding a transform, but do not apply it. Body is unchanged.
   268  	_, err = run.SaveWithParams(&SaveParams{
   269  		Ref:       "me/another_ds",
   270  		FilePaths: []string{"testdata/tf/transform.star"},
   271  	})
   272  	if err != nil {
   273  		t.Error(err)
   274  	}
   275  
   276  	ds = run.MustGet(t, "me/another_ds")
   277  	if ds.BodyPath != bodyPath {
   278  		t.Error("unexpected: body path changed")
   279  	}
   280  }
   281  
   282  func TestGet(t *testing.T) {
   283  	ctx, done := context.WithCancel(context.Background())
   284  	defer done()
   285  
   286  	mr, err := testrepo.NewTestRepo()
   287  	if err != nil {
   288  		t.Fatalf("error allocating test repo: %s", err.Error())
   289  	}
   290  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
   291  	if err != nil {
   292  		t.Fatal(err.Error())
   293  	}
   294  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   295  
   296  	ref, err := mr.GetRef(reporef.DatasetRef{Peername: "peer", Name: "movies"})
   297  	if err != nil {
   298  		t.Fatalf("error getting path: %s", err.Error())
   299  	}
   300  
   301  	moviesDs, err := dsfs.LoadDataset(ctx, mr.Filesystem(), ref.Path)
   302  	if err != nil {
   303  		t.Fatalf("error loading dataset: %s", err.Error())
   304  	}
   305  
   306  	moviesDs.OpenBodyFile(ctx, node.Repo.Filesystem())
   307  	moviesBodyFile := moviesDs.BodyFile()
   308  	reader, err := dsio.NewCSVReader(moviesDs.Structure, moviesBodyFile)
   309  	if err != nil {
   310  		t.Fatalf("creating CSV reader: %s", err)
   311  	}
   312  	moviesBody := mustBeArray(base.ReadEntries(reader))
   313  
   314  	moviesPreviewDs, err := dsfs.LoadDataset(ctx, mr.Filesystem(), ref.Path)
   315  	if err != nil {
   316  		t.Fatalf("error loading dataset: %s", err.Error())
   317  	}
   318  	base.OpenDataset(ctx, node.Repo.Filesystem(), moviesPreviewDs)
   319  	moviesPreview, err := preview.Create(ctx, moviesPreviewDs)
   320  	if err != nil {
   321  		t.Fatalf("creating preview: %s", err)
   322  	}
   323  
   324  	cases := []struct {
   325  		description string
   326  		params      *GetParams
   327  		expect      interface{}
   328  	}{
   329  
   330  		{"empty ref",
   331  			&GetParams{Ref: "", Selector: "body"}, `"" is not a valid dataset reference: empty reference`},
   332  
   333  		{"invalid ref",
   334  			&GetParams{Ref: "peer/ABC@abc"}, `"peer/ABC@abc" is not a valid dataset reference: unexpected character at position 8: '@'`},
   335  
   336  		{"ref without path",
   337  			&GetParams{Ref: "peer/movies"},
   338  			setDatasetName(moviesPreview, "peer/movies")},
   339  
   340  		{"ref with path",
   341  			&GetParams{Ref: fmt.Sprintf("peer/movies@%s", ref.Path)},
   342  			setDatasetName(moviesPreview, "peer/movies")},
   343  
   344  		{"commit component",
   345  			&GetParams{Ref: "peer/movies", Selector: "commit"},
   346  			moviesDs.Commit},
   347  
   348  		{"structure component",
   349  			&GetParams{Ref: "peer/movies", Selector: "structure"},
   350  			moviesDs.Structure},
   351  
   352  		{"title field of commit component",
   353  			&GetParams{Ref: "peer/movies", Selector: "commit.title"}, "initial commit"},
   354  
   355  		{"body",
   356  			&GetParams{Ref: "peer/movies", Selector: "body"}, moviesBody[:0]},
   357  
   358  		{"body with limit and offfset",
   359  			&GetParams{Ref: "peer/movies", Selector: "body",
   360  				List: params.List{Limit: 5, Offset: 0}, All: false}, moviesBody[:5]},
   361  
   362  		{"body with invalid limit and offset",
   363  			&GetParams{Ref: "peer/movies", Selector: "body",
   364  				List: params.List{Limit: -5, Offset: -100}, All: false}, "invalid limit / offset settings"},
   365  
   366  		{"body with all flag ignores invalid limit and offset",
   367  			&GetParams{Ref: "peer/movies", Selector: "body",
   368  				List: params.List{Limit: -5, Offset: -100}, All: true}, moviesBody},
   369  
   370  		{"body with all flag",
   371  			&GetParams{Ref: "peer/movies", Selector: "body",
   372  				List: params.List{Limit: 0, Offset: 0}, All: true}, moviesBody},
   373  
   374  		{"body with limit and non-zero offset",
   375  			&GetParams{Ref: "peer/movies", Selector: "body",
   376  				List: params.List{Limit: 2, Offset: 10}, All: false}, moviesBody[10:12]},
   377  	}
   378  
   379  	for _, c := range cases {
   380  		t.Run(c.description, func(t *testing.T) {
   381  			got, err := inst.Dataset().Get(ctx, c.params)
   382  			if err != nil {
   383  				if err.Error() != c.expect {
   384  					t.Errorf("error mismatch: expected: %s, got: %s", c.expect, err)
   385  				}
   386  				return
   387  			}
   388  			if ds, ok := got.Value.(*dataset.Dataset); ok {
   389  				if ds.ID == "" {
   390  					t.Errorf("returned dataset should have a non-empty ID field")
   391  				}
   392  			}
   393  			if diff := cmp.Diff(c.expect, got.Value, cmpopts.IgnoreUnexported(
   394  				dataset.Dataset{},
   395  				dataset.Meta{},
   396  				dataset.Commit{},
   397  				dataset.Structure{},
   398  				dataset.Viz{},
   399  				dataset.Readme{},
   400  				dataset.Transform{},
   401  			),
   402  				cmpopts.IgnoreFields(dataset.Dataset{}, "ID"),
   403  			); diff != "" {
   404  				t.Errorf("get output (-want +got):\n%s", diff)
   405  			}
   406  		})
   407  	}
   408  }
   409  
   410  func TestGetParamsValidate(t *testing.T) {
   411  	p := &GetParams{}
   412  	p.Selector = "test+selector"
   413  	expectErr := fmt.Errorf("could not parse request: invalid selector")
   414  	if err := p.Validate(); err.Error() != expectErr.Error() {
   415  		t.Errorf("GetParams.Validate error mismatch, expected %s, got %s", expectErr, err)
   416  	}
   417  }
   418  
   419  func TestGetParamsSetNonZeroDefaults(t *testing.T) {
   420  	gotParams := &GetParams{
   421  		Selector: "body",
   422  		List: params.List{
   423  			Offset: -1,
   424  		},
   425  	}
   426  	expectParams := &GetParams{
   427  		Selector: "body",
   428  		List: params.List{
   429  			Limit:  25,
   430  			Offset: 0,
   431  		},
   432  	}
   433  	gotParams.SetNonZeroDefaults()
   434  	if diff := cmp.Diff(expectParams, gotParams); diff != "" {
   435  		t.Errorf("output mismatch (-want +got):\n%s", diff)
   436  	}
   437  }
   438  
   439  func TestGetZip(t *testing.T) {
   440  	ctx, done := context.WithCancel(context.Background())
   441  	defer done()
   442  
   443  	mr, err := testrepo.NewTestRepo()
   444  	if err != nil {
   445  		t.Fatalf("error allocating test repo: %s", err.Error())
   446  	}
   447  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
   448  	if err != nil {
   449  		t.Fatal(err.Error())
   450  	}
   451  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   452  
   453  	p := &GetParams{Ref: "peer/movies"}
   454  	zipResults, err := inst.Dataset().GetZip(ctx, p)
   455  	if err != nil {
   456  		t.Fatalf("TestGetZip unexpected error: %s", err)
   457  	}
   458  	tempDir, err := ioutil.TempDir("", "get_zip_test")
   459  	defer os.RemoveAll(tempDir)
   460  
   461  	filename := path.Join(tempDir, "dataset.zip")
   462  	if err := ioutil.WriteFile(filename, zipResults.Bytes, 0644); err != nil {
   463  		t.Fatalf("error writing zip: %s", err)
   464  	}
   465  	expectedFiles := []string{
   466  		"commit.json",
   467  		"meta.json",
   468  		"structure.json",
   469  		"body.csv",
   470  		"qri-ref.txt",
   471  	}
   472  	r, err := zip.OpenReader(filename)
   473  	if err != nil {
   474  		t.Fatalf("error reading zip: %s", err)
   475  	}
   476  	gotFiles := []string{}
   477  	for _, f := range r.File {
   478  		gotFiles = append(gotFiles, f.Name)
   479  	}
   480  
   481  	if diff := cmp.Diff(expectedFiles, gotFiles); diff != "" {
   482  		t.Errorf("expected zip files (-want +got):\n%s", diff)
   483  	}
   484  }
   485  
   486  func TestGetCSV(t *testing.T) {
   487  	ctx, done := context.WithCancel(context.Background())
   488  	defer done()
   489  
   490  	mr, err := testrepo.NewTestRepo()
   491  	if err != nil {
   492  		t.Fatalf("error allocating test repo: %s", err.Error())
   493  	}
   494  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
   495  	if err != nil {
   496  		t.Fatal(err.Error())
   497  	}
   498  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   499  
   500  	ref, err := mr.GetRef(reporef.DatasetRef{Peername: "peer", Name: "movies"})
   501  	if err != nil {
   502  		t.Fatalf("error getting path: %s", err.Error())
   503  	}
   504  	moviesDs, err := dsfs.LoadDataset(ctx, mr.Filesystem(), ref.Path)
   505  	if err != nil {
   506  		t.Fatalf("error loading dataset: %s", err.Error())
   507  	}
   508  	moviesDs.OpenBodyFile(ctx, node.Repo.Filesystem())
   509  	moviesBodyFile := moviesDs.BodyFile()
   510  	expectedBytes, err := ioutil.ReadAll(moviesBodyFile)
   511  	if err != nil {
   512  		t.Fatalf("error reading body file: %s", err)
   513  	}
   514  
   515  	// the body file has `movie_title` for the first column, but the schema has the first column title as `title`
   516  	expectedBytes = bytes.Replace(expectedBytes, []byte(`movie_title,duration`), []byte(`title,duration`), 1)
   517  
   518  	gotBytes, err := inst.Dataset().GetCSV(ctx, &GetParams{Ref: "peer/movies", All: true})
   519  	if err != nil {
   520  		t.Fatalf("error getting csv: %s", err)
   521  	}
   522  	if diff := cmp.Diff(expectedBytes, gotBytes); diff != "" {
   523  		t.Errorf("csv body bytes (-want +got):\n%s", diff)
   524  	}
   525  }
   526  
   527  func TestGetBodySize(t *testing.T) {
   528  	run := newTestRunner(t)
   529  	defer run.Delete()
   530  
   531  	prevMaxBodySize := maxBodySizeToGetAll
   532  	maxBodySizeToGetAll = 160
   533  	defer func() {
   534  		maxBodySizeToGetAll = prevMaxBodySize
   535  	}()
   536  
   537  	// Save a dataset with a body smaller than our test limit
   538  	_, err := run.SaveWithParams(&SaveParams{
   539  		Ref:      "me/small_ds",
   540  		BodyPath: "testdata/cities_2/body.csv",
   541  	})
   542  	if err != nil {
   543  		t.Fatal(err)
   544  	}
   545  
   546  	// Save a dataset with a body larger than our test limit
   547  	_, err = run.SaveWithParams(&SaveParams{
   548  		Ref:      "me/large_ds",
   549  		BodyPath: "testdata/cities_2/body_more.csv",
   550  	})
   551  	if err != nil {
   552  		t.Fatal(err)
   553  	}
   554  
   555  	inst := run.Instance
   556  	ctx := run.Ctx
   557  
   558  	// Get the small dataset's body, which is okay
   559  	params := GetParams{Ref: "me/small_ds", Selector: "body", List: params.List{Limit: -1}, All: true}
   560  	_, err = inst.Dataset().Get(ctx, &params)
   561  	if err != nil {
   562  		t.Errorf("%s", err)
   563  	}
   564  
   565  	// Get the large dataset's body, which will return an error
   566  	params.Ref = "me/large_ds"
   567  	_, err = inst.Dataset().Get(ctx, &params)
   568  	if err == nil {
   569  		t.Errorf("expected error, did not get one")
   570  	}
   571  	expectErr := `body is too large to get all: 217 larger than 160`
   572  	if err.Error() != expectErr {
   573  		t.Errorf("error mismatch, expected: %s, got: %s", expectErr, err)
   574  	}
   575  
   576  	// Get the small dataset's body in CSV format, which is okay
   577  	params.Ref = "me/small_ds"
   578  	_, err = inst.Dataset().GetCSV(ctx, &params)
   579  	if err != nil {
   580  		t.Errorf("%s", err)
   581  	}
   582  
   583  	// Get the large dataset's body in CSV format, which will return an error
   584  	params.Ref = "me/large_ds"
   585  	_, err = inst.Dataset().GetCSV(ctx, &params)
   586  	if err == nil {
   587  		t.Errorf("expected error, did not get one")
   588  	}
   589  	if err.Error() != expectErr {
   590  		t.Errorf("error mismatch, expected: %s, got: %s", expectErr, err)
   591  	}
   592  }
   593  
   594  func setDatasetName(ds *dataset.Dataset, name string) *dataset.Dataset {
   595  	parts := strings.Split(name, "/")
   596  	ds.Peername = parts[0]
   597  	ds.Name = parts[1]
   598  	return ds
   599  }
   600  
   601  func componentToString(component interface{}, format string) string {
   602  	switch format {
   603  	case "json":
   604  		bytes, err := json.MarshalIndent(component, "", " ")
   605  		if err != nil {
   606  			return err.Error()
   607  		}
   608  		return string(bytes)
   609  	case "non-pretty json":
   610  		bytes, err := json.Marshal(component)
   611  		if err != nil {
   612  			return err.Error()
   613  		}
   614  		return string(bytes)
   615  	case "yaml":
   616  		bytes, err := yaml.Marshal(component)
   617  		if err != nil {
   618  			return err.Error()
   619  		}
   620  		return string(bytes)
   621  	default:
   622  		return "Unknown format"
   623  	}
   624  }
   625  
   626  func bodyToString(component interface{}) string {
   627  	bytes, err := json.Marshal(component)
   628  	if err != nil {
   629  		return err.Error()
   630  	}
   631  	return string(bytes)
   632  }
   633  
   634  func bodyToPrettyString(component interface{}) string {
   635  	bytes, err := json.MarshalIndent(component, "", " ")
   636  	if err != nil {
   637  		return err.Error()
   638  	}
   639  	return string(bytes)
   640  }
   641  
   642  func TestDatasetRequestsGetP2p(t *testing.T) {
   643  	ctx, done := context.WithCancel(context.Background())
   644  	defer done()
   645  
   646  	// Matches what is used to generated test peers.
   647  	datasets := []string{"movies", "cities", "counter", "craigslist", "sitemap"}
   648  
   649  	factory := p2ptest.NewTestNodeFactory(p2p.NewTestableQriNode)
   650  	testPeers, err := p2ptest.NewTestNetwork(ctx, factory, 5)
   651  	if err != nil {
   652  		t.Errorf("error creating network: %s", err.Error())
   653  		return
   654  	}
   655  
   656  	if err := p2ptest.ConnectNodes(ctx, testPeers); err != nil {
   657  		t.Errorf("error connecting peers: %s", err.Error())
   658  	}
   659  
   660  	// Convert from test nodes to non-test nodes.
   661  	peers := make([]*p2p.QriNode, len(testPeers))
   662  	for i, node := range testPeers {
   663  		peers[i] = node.(*p2p.QriNode)
   664  	}
   665  
   666  	var wg sync.WaitGroup
   667  	for _, p1 := range peers {
   668  		wg.Add(1)
   669  		go func(node *p2p.QriNode) {
   670  			defer wg.Done()
   671  			// Get number from end of peername, use that to create dataset name.
   672  			profile := node.Repo.Profiles().Owner(ctx)
   673  			num := profile.Peername[len(profile.Peername)-1:]
   674  			index, _ := strconv.ParseInt(num, 10, 32)
   675  			name := datasets[index]
   676  			ref := reporef.DatasetRef{Peername: profile.Peername, Name: name}
   677  
   678  			inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   679  			// TODO (b5) - we're using "JSON" here b/c the "craigslist" test dataset
   680  			// is tripping up the YAML serializer
   681  			got, err := inst.Dataset().Get(ctx, &GetParams{Ref: fmt.Sprintf("%s/%s", profile.Peername, name)})
   682  			if err != nil {
   683  				t.Errorf("error getting dataset for %q: %s", ref, err.Error())
   684  			}
   685  
   686  			if got.Value == nil {
   687  				t.Errorf("failed to get dataset for ref %q", ref)
   688  			}
   689  			// TODO: Test contents of Dataset.
   690  		}(p1)
   691  	}
   692  
   693  	wg.Wait()
   694  }
   695  
   696  func TestDatasetRequestsRename(t *testing.T) {
   697  	ctx, done := context.WithCancel(context.Background())
   698  	defer done()
   699  
   700  	mr, err := testrepo.NewTestRepo()
   701  	if err != nil {
   702  		t.Fatalf("error allocating test repo: %s", err.Error())
   703  	}
   704  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
   705  	if err != nil {
   706  		t.Fatal(err.Error())
   707  	}
   708  
   709  	bad := []struct {
   710  		p   *RenameParams
   711  		err string
   712  	}{
   713  		{&RenameParams{}, "current name is required to rename a dataset"},
   714  		{&RenameParams{Current: "peer/movies", Next: "peer/new movies"}, fmt.Sprintf("destination name: %s", dsref.ErrDescribeValidName.Error())},
   715  		{&RenameParams{Current: "peer/cities", Next: "peer/sitemap"}, `dataset "peer/sitemap" already exists`},
   716  	}
   717  
   718  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   719  	for i, c := range bad {
   720  		t.Run(fmt.Sprintf("bad_%d", i), func(t *testing.T) {
   721  			_, err := inst.WithSource("local").Dataset().Rename(ctx, c.p)
   722  
   723  			if err == nil {
   724  				t.Fatalf("test didn't error")
   725  			}
   726  
   727  			if c.err != err.Error() {
   728  				t.Errorf("error mismatch: expected: %s, got: %s", c.err, err)
   729  			}
   730  		})
   731  	}
   732  
   733  	log, err := mr.Logbook().DatasetRef(ctx, dsref.Ref{Username: "peer", Name: "movies"})
   734  	if err != nil {
   735  		t.Errorf("error getting logbook head reference: %s", err)
   736  	}
   737  
   738  	p := &RenameParams{
   739  		Current: "peer/movies",
   740  		Next:    "peer/new_movies",
   741  	}
   742  
   743  	res, err := inst.WithSource("local").Dataset().Rename(ctx, p)
   744  	if err != nil {
   745  		t.Errorf("unexpected error renaming: %s", err)
   746  	}
   747  
   748  	expect := &dsref.Ref{Username: "peer", Name: "new_movies"}
   749  	if expect.Alias() != res.Alias() {
   750  		t.Errorf("response mismatch. expected: %s, got: %s", expect.Alias(), res.Alias())
   751  	}
   752  
   753  	// get log by id this time
   754  	after, err := mr.Logbook().Log(ctx, log.ID())
   755  	if err != nil {
   756  		t.Errorf("getting log by ID: %s", err)
   757  	}
   758  
   759  	if expect.Name != after.Name() {
   760  		t.Errorf("rename log mismatch. expected: %s, got: %s", expect.Name, after.Name())
   761  	}
   762  }
   763  
   764  func TestDatasetRequestsRemove(t *testing.T) {
   765  	ctx, done := context.WithCancel(context.Background())
   766  	defer done()
   767  
   768  	mr, err := testrepo.NewTestRepo()
   769  	if err != nil {
   770  		t.Fatalf("error allocating test repo: %s", err.Error())
   771  	}
   772  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
   773  	if err != nil {
   774  		t.Fatal(err.Error())
   775  	}
   776  
   777  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   778  	allRevs := &dsref.Rev{Field: "ds", Gen: -1}
   779  
   780  	// create datasets working directory
   781  	datasetsDir, err := ioutil.TempDir("", "QriTestDatasetRequestsRemove")
   782  	if err != nil {
   783  		t.Fatal(err)
   784  	}
   785  	defer os.RemoveAll(datasetsDir)
   786  
   787  	// add a commit to craigslist
   788  	_, err = inst.Dataset().Save(ctx, &SaveParams{Ref: "peer/craigslist", Dataset: &dataset.Dataset{Meta: &dataset.Meta{Title: "oh word"}}})
   789  	if err != nil {
   790  		t.Fatal(err)
   791  	}
   792  
   793  	badCases := []struct {
   794  		err    string
   795  		params RemoveParams
   796  	}{
   797  		{`"" is not a valid dataset reference: empty reference`, RemoveParams{Ref: "", Revision: allRevs}},
   798  		{"reference not found", RemoveParams{Ref: "abc/not_found", Revision: allRevs}},
   799  		{"can only remove whole dataset versions, not individual components", RemoveParams{Ref: "abc/not_found", Revision: &dsref.Rev{Field: "st", Gen: -1}}},
   800  		{"invalid number of revisions to delete: 0", RemoveParams{Ref: "peer/movies", Revision: &dsref.Rev{Field: "ds", Gen: 0}}},
   801  	}
   802  
   803  	for i, c := range badCases {
   804  		t.Run(fmt.Sprintf("bad_case_%s", c.err), func(t *testing.T) {
   805  			_, err := inst.WithSource("local").Dataset().Remove(ctx, &c.params)
   806  
   807  			if err == nil {
   808  				t.Errorf("case %d: expected error. got nil", i)
   809  				return
   810  			} else if c.err != err.Error() {
   811  				t.Errorf("case %d: error mismatch: expected: %s, got: %s", i, c.err, err)
   812  			}
   813  		})
   814  	}
   815  
   816  	goodCases := []struct {
   817  		description string
   818  		params      RemoveParams
   819  		res         RemoveResponse
   820  	}{
   821  		{"all generations of peer/movies",
   822  			RemoveParams{Ref: "peer/movies", Revision: allRevs},
   823  			RemoveResponse{NumDeleted: -1},
   824  		},
   825  		{"all generations, specifying more revs than log length",
   826  			RemoveParams{Ref: "peer/counter", Revision: &dsref.Rev{Field: "ds", Gen: 20}},
   827  			RemoveResponse{NumDeleted: -1},
   828  		},
   829  	}
   830  
   831  	for _, c := range goodCases {
   832  		t.Run(fmt.Sprintf("good_case_%s", c.description), func(t *testing.T) {
   833  			res, err := inst.WithSource("local").Dataset().Remove(ctx, &c.params)
   834  
   835  			if err != nil {
   836  				t.Errorf("unexpected error: %s", err)
   837  				return
   838  			}
   839  			if c.res.NumDeleted != res.NumDeleted {
   840  				t.Errorf("res.NumDeleted mismatch. want %d, got %d", c.res.NumDeleted, res.NumDeleted)
   841  			}
   842  			if c.res.Unlinked != res.Unlinked {
   843  				t.Errorf("res.Unlinked mismatch. want %t, got %t", c.res.Unlinked, res.Unlinked)
   844  			}
   845  		})
   846  	}
   847  }
   848  
   849  func TestDatasetRequestsPull(t *testing.T) {
   850  	ctx, done := context.WithCancel(context.Background())
   851  	defer done()
   852  
   853  	bad := []struct {
   854  		p   PullParams
   855  		err string
   856  	}{
   857  		{PullParams{Ref: "abc/hash###"}, "node is not online and no registry is configured"},
   858  	}
   859  
   860  	mr, err := testrepo.NewTestRepo()
   861  	if err != nil {
   862  		t.Fatalf("error allocating test repo: %s", err.Error())
   863  	}
   864  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
   865  	if err != nil {
   866  		t.Fatal(err.Error())
   867  	}
   868  
   869  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
   870  	for i, c := range bad {
   871  		t.Run(fmt.Sprintf("bad_case_%d", i), func(t *testing.T) {
   872  			_, err := inst.Dataset().Pull(ctx, &c.p)
   873  			if err == nil {
   874  				t.Fatal("expected error, got nil")
   875  			}
   876  
   877  			if err.Error() == c.err {
   878  				t.Errorf("case %d error mismatch: expected: %s, got: %s", i, c.err, err)
   879  			}
   880  		})
   881  	}
   882  }
   883  
   884  func TestDatasetRequestsAddP2P(t *testing.T) {
   885  	t.Skip("TODO (b5)")
   886  	ctx, done := context.WithCancel(context.Background())
   887  	defer done()
   888  
   889  	// Matches what is used to generate the test peers.
   890  	datasets := []string{"movies", "cities", "counter", "craigslist", "sitemap"}
   891  
   892  	// Create test nodes.
   893  	factory := p2ptest.NewTestNodeFactory(p2p.NewTestableQriNode)
   894  	testPeers, err := p2ptest.NewTestNetwork(ctx, factory, 5)
   895  	if err != nil {
   896  		t.Errorf("error creating network: %s", err.Error())
   897  		return
   898  	}
   899  
   900  	// Peers exchange Qri profile information.
   901  	if err := p2ptest.ConnectNodes(ctx, testPeers); err != nil {
   902  		t.Errorf("error upgrading to qri connections: %s", err.Error())
   903  		return
   904  	}
   905  
   906  	// Convert from test nodes to non-test nodes.
   907  	peers := make([]*p2p.QriNode, len(testPeers))
   908  	for i, node := range testPeers {
   909  		peers[i] = node.(*p2p.QriNode)
   910  	}
   911  
   912  	// Connect in memory Mapstore's behind the scene to simulate IPFS like behavior.
   913  	for i, s0 := range peers {
   914  		for _, s1 := range peers[i+1:] {
   915  			m0 := (s0.Repo.Filesystem().Filesystem("mem")).(*qfs.MemFS)
   916  			m1 := (s1.Repo.Filesystem().Filesystem("mem")).(*qfs.MemFS)
   917  			m0.AddConnection(m1)
   918  		}
   919  	}
   920  
   921  	var wg sync.WaitGroup
   922  	for i, p0 := range peers {
   923  		for _, p1 := range peers[i+1:] {
   924  			wg.Add(1)
   925  			go func(p0, p1 *p2p.QriNode) {
   926  				defer wg.Done()
   927  
   928  				// Get ref to dataset that peer2 has.
   929  				profile := p1.Repo.Profiles().Owner(ctx)
   930  				num := profile.Peername[len(profile.Peername)-1:]
   931  				index, _ := strconv.ParseInt(num, 10, 32)
   932  				name := datasets[index]
   933  				ref := reporef.DatasetRef{Peername: profile.Peername, Name: name}
   934  				p := &PullParams{
   935  					Ref: ref.AliasString(),
   936  				}
   937  
   938  				// Build requests for peer1 to peer2.
   939  				inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), p0)
   940  
   941  				_, err := inst.Dataset().Pull(ctx, p)
   942  				if err != nil {
   943  					pro1 := p0.Repo.Profiles().Owner(ctx)
   944  					pro2 := p1.Repo.Profiles().Owner(ctx)
   945  					t.Errorf("error adding dataset for %s from %s to %s: %s",
   946  						ref.Name, pro2.Peername, pro1.Peername, err.Error())
   947  				}
   948  			}(p0, p1)
   949  		}
   950  	}
   951  	wg.Wait()
   952  
   953  	// TODO: Validate that p1 has added data from p2.
   954  }
   955  
   956  func TestDatasetRequestsValidate(t *testing.T) {
   957  	ctx, done := context.WithCancel(context.Background())
   958  	defer done()
   959  
   960  	run := newTestRunner(t)
   961  	defer run.Delete()
   962  
   963  	movieb := `Avatar ,178
   964  Pirates of the Caribbean: At World's End ,169
   965  Pirates of the Caribbean: At World's End ,foo
   966  `
   967  	schemaB := `{
   968  	  "type": "array",
   969  	  "items": {
   970  	    "type": "array",
   971  	    "items": [
   972  	      {
   973  	        "title": "title",
   974  	        "type": "string"
   975  	      },
   976  	      {
   977  	        "title": "duration",
   978  	        "type": "number"
   979  	      }
   980  	    ]
   981  	  }
   982  	}`
   983  
   984  	bodyFilename := run.MakeTmpFilename("data.csv")
   985  	schemaFilename := run.MakeTmpFilename("schema.json")
   986  	run.MustWriteFile(t, bodyFilename, movieb)
   987  	run.MustWriteFile(t, schemaFilename, schemaB)
   988  
   989  	cases := []struct {
   990  		p         ValidateParams
   991  		numErrors int
   992  		err       string
   993  		isNil     bool
   994  	}{
   995  		{ValidateParams{Ref: ""}, 0, "bad arguments provided", true},
   996  		{ValidateParams{Ref: "me"}, 0, "\"me\" is not a valid dataset reference: need username separated by '/' from dataset name", true},
   997  		{ValidateParams{Ref: "me/movies"}, 4, "", false},
   998  		{ValidateParams{Ref: "me/movies", BodyFilename: bodyFilename}, 1, "", false},
   999  		{ValidateParams{Ref: "me/movies", SchemaFilename: schemaFilename}, 5, "", false},
  1000  		{ValidateParams{SchemaFilename: schemaFilename, BodyFilename: bodyFilename}, 1, "", false},
  1001  	}
  1002  
  1003  	mr, err := testrepo.NewTestRepo()
  1004  	if err != nil {
  1005  		t.Fatalf("error allocating test repo: %s", err.Error())
  1006  	}
  1007  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
  1008  	if err != nil {
  1009  		t.Fatal(err.Error())
  1010  	}
  1011  
  1012  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
  1013  	for i, c := range cases {
  1014  		res, err := inst.WithSource("local").Dataset().Validate(ctx, &c.p)
  1015  		if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
  1016  			t.Errorf("case %d error mismatch: expected: %s, got: %s", i, c.err, err.Error())
  1017  			continue
  1018  		}
  1019  
  1020  		if res == nil && !c.isNil {
  1021  			t.Errorf("case %d error result was nil: expected result to not be nil", i)
  1022  			continue
  1023  		}
  1024  
  1025  		if res != nil && len(res.Errors) != c.numErrors {
  1026  			t.Errorf("case %d error count mismatch. expected: %d, got: %d", i, c.numErrors, len(res.Errors))
  1027  			continue
  1028  		}
  1029  	}
  1030  }
  1031  
  1032  func TestDatasetRequestsStats(t *testing.T) {
  1033  	ctx, done := context.WithCancel(context.Background())
  1034  	defer done()
  1035  
  1036  	mr, err := testrepo.NewTestRepo()
  1037  	if err != nil {
  1038  		t.Fatalf("error allocating test repo: %s", err.Error())
  1039  	}
  1040  	node, err := p2p.NewQriNode(mr, testcfg.DefaultP2PForTesting(), event.NilBus, nil)
  1041  	if err != nil {
  1042  		t.Fatal(err.Error())
  1043  	}
  1044  
  1045  	inst := NewInstanceFromConfigAndNode(ctx, testcfg.DefaultConfigForTesting(), node)
  1046  
  1047  	badCases := []struct {
  1048  		description string
  1049  		ref         string
  1050  		expectedErr string
  1051  	}{
  1052  		{"empty reference", "", `"" is not a valid dataset reference: empty reference`},
  1053  		{"bad reference", "!", `"!" is not a valid dataset reference: unexpected character at position 0: '!'`},
  1054  		{"dataset does not exist", "me/dataset_does_not_exist", "reference not found"},
  1055  	}
  1056  	for _, c := range badCases {
  1057  		t.Run(c.description, func(t *testing.T) {
  1058  			_, err = inst.WithSource("local").Dataset().Get(ctx, &GetParams{Ref: c.ref, Selector: "stats"})
  1059  			if c.expectedErr != err.Error() {
  1060  				t.Errorf("error mismatch, expected: %q, got: %q", c.expectedErr, err.Error())
  1061  			}
  1062  		})
  1063  	}
  1064  
  1065  	// TODO (ramfox): see if there is a better way to verify the stat bytes then
  1066  	// just inputing them in the cases struct
  1067  	goodCases := []struct {
  1068  		description string
  1069  		ref         string
  1070  		expectPath  string
  1071  	}{
  1072  		{"csv: me/cities", "me/cities", "./testdata/cities.stats.json"},
  1073  		{"json: me/sitemap", "me/sitemap", `./testdata/sitemap.stats.json`},
  1074  	}
  1075  	for _, c := range goodCases {
  1076  		t.Run(c.description, func(t *testing.T) {
  1077  			res, err := inst.WithSource("local").Dataset().Get(ctx, &GetParams{Ref: c.ref, Selector: "stats"})
  1078  			if err != nil {
  1079  				t.Fatalf("unexpected error: %q", err.Error())
  1080  			}
  1081  			expectData, err := ioutil.ReadFile(c.expectPath)
  1082  			if err != nil {
  1083  				t.Fatal(err)
  1084  			}
  1085  
  1086  			expect := []interface{}{}
  1087  			if err = json.Unmarshal(expectData, &expect); err != nil {
  1088  				t.Fatal(err)
  1089  			}
  1090  			if diff := cmp.Diff(expect, res.Value); diff != "" {
  1091  				t.Errorf("result mismatch (-want +got):%s\n", diff)
  1092  				output, _ := json.Marshal(res.Value)
  1093  				fmt.Println(string(output))
  1094  			}
  1095  		})
  1096  	}
  1097  }
  1098  
  1099  func TestDatasetWhatChanged(t *testing.T) {
  1100  	run := newTestRunner(t)
  1101  	defer run.Delete()
  1102  
  1103  	// Save a first version, with just a body
  1104  	run.MustSaveFromBody(t, "cities_ds", "testdata/cities_2/body.csv")
  1105  
  1106  	// Save a second version, with a meta.title
  1107  	ref, err := run.SaveWithParams(&SaveParams{
  1108  		Ref: "me/cities_ds",
  1109  		Dataset: &dataset.Dataset{
  1110  			Meta: &dataset.Meta{
  1111  				Title: "city data",
  1112  			},
  1113  		},
  1114  	})
  1115  	if err != nil {
  1116  		t.Fatal(err)
  1117  	}
  1118  	version2 := ref.String()
  1119  
  1120  	// Save a third version, with a different meta.title, changed body, added readme
  1121  	ref, err = run.SaveWithParams(&SaveParams{
  1122  		Ref: "me/cities_ds",
  1123  		Dataset: &dataset.Dataset{
  1124  			Meta: &dataset.Meta{
  1125  				Title: "city data 2",
  1126  			},
  1127  			Readme: &dataset.Readme{
  1128  				Text: "# About\n\nThis is a test dataset",
  1129  			},
  1130  		},
  1131  		BodyPath: "testdata/cities_2/body_more.csv",
  1132  	})
  1133  	if err != nil {
  1134  		t.Fatal(err)
  1135  	}
  1136  	version3 := ref.String()
  1137  
  1138  	// Check what changed for version 2: a meta was added
  1139  	items := run.MustWhatChanged(t, version2)
  1140  	expectItems := []base.StatusItem{
  1141  		{Component: "meta", Type: "add"},
  1142  		{Component: "structure", Type: "unmodified"},
  1143  		{Component: "body", Type: "unmodified"},
  1144  	}
  1145  	if diff := cmp.Diff(expectItems, items); diff != "" {
  1146  		t.Errorf("error mismatch (-want +got):%s\n", diff)
  1147  	}
  1148  
  1149  	// Check what changed for version 3: meta and body changed, readme added
  1150  	items = run.MustWhatChanged(t, version3)
  1151  	expectItems = []base.StatusItem{
  1152  		{Component: "meta", Type: "modified"},
  1153  		{Component: "structure", Type: "unmodified"},
  1154  		{Component: "readme", Type: "add"},
  1155  		{Component: "body", Type: "modified"},
  1156  	}
  1157  	if diff := cmp.Diff(expectItems, items); diff != "" {
  1158  		t.Errorf("error mismatch (-want +got):%s\n", diff)
  1159  	}
  1160  }
  1161  
  1162  // Convert the interface value into an array, or panic if not possible
  1163  func mustBeArray(i interface{}, err error) []interface{} {
  1164  	if err != nil {
  1165  		panic(err)
  1166  	}
  1167  	return i.([]interface{})
  1168  }