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, ¶ms) 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, ¶ms) 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, ¶ms) 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, ¶ms) 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 }