github.com/weaviate/weaviate@v1.24.6/usecases/classification/classifier_test.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 package classification 13 14 import ( 15 "context" 16 "encoding/json" 17 "errors" 18 "fmt" 19 "testing" 20 "time" 21 22 "github.com/go-openapi/strfmt" 23 "github.com/sirupsen/logrus" 24 "github.com/sirupsen/logrus/hooks/test" 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/require" 27 "github.com/weaviate/weaviate/entities/models" 28 testhelper "github.com/weaviate/weaviate/test/helper" 29 ) 30 31 func newNullLogger() *logrus.Logger { 32 log, _ := test.NewNullLogger() 33 return log 34 } 35 36 func Test_Classifier_KNN(t *testing.T) { 37 t.Run("with invalid data", func(t *testing.T) { 38 sg := &fakeSchemaGetter{testSchema()} 39 _, err := New(sg, nil, nil, &fakeAuthorizer{}, newNullLogger(), nil). 40 Schedule(context.Background(), nil, models.Classification{}) 41 assert.NotNil(t, err, "should error with invalid user input") 42 }) 43 44 var id strfmt.UUID 45 // so we can reuse it for follow up requests, such as checking the status 46 47 t.Run("with valid data", func(t *testing.T) { 48 sg := &fakeSchemaGetter{testSchema()} 49 repo := newFakeClassificationRepo() 50 authorizer := &fakeAuthorizer{} 51 vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified()) 52 classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil) 53 54 params := models.Classification{ 55 Class: "Article", 56 BasedOnProperties: []string{"description"}, 57 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 58 Settings: map[string]interface{}{ 59 "k": json.Number("1"), 60 }, 61 } 62 63 t.Run("scheduling a classification", func(t *testing.T) { 64 class, err := classifier.Schedule(context.Background(), nil, params) 65 require.Nil(t, err, "should not error") 66 require.NotNil(t, class) 67 68 assert.Len(t, class.ID, 36, "an id was assigned") 69 id = class.ID 70 }) 71 72 t.Run("retrieving the same classification by id", func(t *testing.T) { 73 class, err := classifier.Get(context.Background(), nil, id) 74 require.Nil(t, err) 75 require.NotNil(t, class) 76 assert.Equal(t, id, class.ID) 77 assert.Equal(t, models.ClassificationStatusRunning, class.Status) 78 }) 79 80 // TODO: improve by polling instead 81 time.Sleep(500 * time.Millisecond) 82 83 t.Run("status is now completed", func(t *testing.T) { 84 class, err := classifier.Get(context.Background(), nil, id) 85 require.Nil(t, err) 86 require.NotNil(t, class) 87 assert.Equal(t, models.ClassificationStatusCompleted, class.Status) 88 }) 89 90 t.Run("the classifier updated the actions with the classified references", func(t *testing.T) { 91 vectorRepo.Lock() 92 require.Len(t, vectorRepo.db, 6) 93 vectorRepo.Unlock() 94 95 t.Run("food", func(t *testing.T) { 96 idArticleFoodOne := "06a1e824-889c-4649-97f9-1ed3fa401d8e" 97 idArticleFoodTwo := "6402e649-b1e0-40ea-b192-a64eab0d5e56" 98 99 checkRef(t, vectorRepo, idArticleFoodOne, "exactCategory", idCategoryFoodAndDrink) 100 checkRef(t, vectorRepo, idArticleFoodTwo, "mainCategory", idMainCategoryFoodAndDrink) 101 }) 102 103 t.Run("politics", func(t *testing.T) { 104 idArticlePoliticsOne := "75ba35af-6a08-40ae-b442-3bec69b355f9" 105 idArticlePoliticsTwo := "f850439a-d3cd-4f17-8fbf-5a64405645cd" 106 107 checkRef(t, vectorRepo, idArticlePoliticsOne, "exactCategory", idCategoryPolitics) 108 checkRef(t, vectorRepo, idArticlePoliticsTwo, "mainCategory", idMainCategoryPoliticsAndSociety) 109 }) 110 111 t.Run("society", func(t *testing.T) { 112 idArticleSocietyOne := "a2bbcbdc-76e1-477d-9e72-a6d2cfb50109" 113 idArticleSocietyTwo := "069410c3-4b9e-4f68-8034-32a066cb7997" 114 115 checkRef(t, vectorRepo, idArticleSocietyOne, "exactCategory", idCategorySociety) 116 checkRef(t, vectorRepo, idArticleSocietyTwo, "mainCategory", idMainCategoryPoliticsAndSociety) 117 }) 118 }) 119 }) 120 121 t.Run("when errors occur during classification", func(t *testing.T) { 122 sg := &fakeSchemaGetter{testSchema()} 123 repo := newFakeClassificationRepo() 124 authorizer := &fakeAuthorizer{} 125 vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified()) 126 vectorRepo.errorOnAggregate = errors.New("something went wrong") 127 classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil) 128 129 params := models.Classification{ 130 Class: "Article", 131 BasedOnProperties: []string{"description"}, 132 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 133 Settings: map[string]interface{}{ 134 "k": json.Number("1"), 135 }, 136 } 137 138 t.Run("scheduling a classification", func(t *testing.T) { 139 class, err := classifier.Schedule(context.Background(), nil, params) 140 require.Nil(t, err, "should not error") 141 require.NotNil(t, class) 142 143 assert.Len(t, class.ID, 36, "an id was assigned") 144 id = class.ID 145 }) 146 147 waitForStatusToNoLongerBeRunning(t, classifier, id) 148 149 t.Run("status is now failed", func(t *testing.T) { 150 class, err := classifier.Get(context.Background(), nil, id) 151 require.Nil(t, err) 152 require.NotNil(t, class) 153 assert.Equal(t, models.ClassificationStatusFailed, class.Status) 154 expectedErrStrings := []string{ 155 "classification failed: ", 156 "classify Article/75ba35af-6a08-40ae-b442-3bec69b355f9: something went wrong", 157 "classify Article/f850439a-d3cd-4f17-8fbf-5a64405645cd: something went wrong", 158 "classify Article/a2bbcbdc-76e1-477d-9e72-a6d2cfb50109: something went wrong", 159 "classify Article/069410c3-4b9e-4f68-8034-32a066cb7997: something went wrong", 160 "classify Article/06a1e824-889c-4649-97f9-1ed3fa401d8e: something went wrong", 161 "classify Article/6402e649-b1e0-40ea-b192-a64eab0d5e56: something went wrong", 162 } 163 164 for _, msg := range expectedErrStrings { 165 assert.Contains(t, class.Error, msg) 166 } 167 }) 168 }) 169 170 t.Run("when there is nothing to be classified", func(t *testing.T) { 171 sg := &fakeSchemaGetter{testSchema()} 172 repo := newFakeClassificationRepo() 173 authorizer := &fakeAuthorizer{} 174 vectorRepo := newFakeVectorRepoKNN(nil, testDataAlreadyClassified()) 175 classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil) 176 177 params := models.Classification{ 178 Class: "Article", 179 BasedOnProperties: []string{"description"}, 180 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 181 Settings: map[string]interface{}{ 182 "k": json.Number("1"), 183 }, 184 } 185 186 t.Run("scheduling a classification", func(t *testing.T) { 187 class, err := classifier.Schedule(context.Background(), nil, params) 188 require.Nil(t, err, "should not error") 189 require.NotNil(t, class) 190 191 assert.Len(t, class.ID, 36, "an id was assigned") 192 id = class.ID 193 }) 194 195 waitForStatusToNoLongerBeRunning(t, classifier, id) 196 197 t.Run("status is now failed", func(t *testing.T) { 198 class, err := classifier.Get(context.Background(), nil, id) 199 require.Nil(t, err) 200 require.NotNil(t, class) 201 assert.Equal(t, models.ClassificationStatusFailed, class.Status) 202 expectedErr := "classification failed: " + 203 "no classes to be classified - did you run a previous classification already?" 204 assert.Equal(t, expectedErr, class.Error) 205 }) 206 }) 207 } 208 209 func Test_Classifier_Custom_Classifier(t *testing.T) { 210 var id strfmt.UUID 211 // so we can reuse it for follow up requests, such as checking the status 212 213 t.Run("with unreconginzed custom module classifier name", func(t *testing.T) { 214 sg := &fakeSchemaGetter{testSchema()} 215 repo := newFakeClassificationRepo() 216 authorizer := &fakeAuthorizer{} 217 218 vectorRepo := newFakeVectorRepoContextual(testDataToBeClassified(), testDataPossibleTargets()) 219 logger, _ := test.NewNullLogger() 220 221 // vectorizer := &fakeVectorizer{words: testDataVectors()} 222 modulesProvider := NewFakeModulesProvider() 223 classifier := New(sg, repo, vectorRepo, authorizer, logger, modulesProvider) 224 225 notRecoginzedContextual := "text2vec-contextionary-custom-not-recognized" 226 params := models.Classification{ 227 Class: "Article", 228 BasedOnProperties: []string{"description"}, 229 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 230 Type: notRecoginzedContextual, 231 } 232 233 t.Run("scheduling an unrecognized classification", func(t *testing.T) { 234 class, err := classifier.Schedule(context.Background(), nil, params) 235 require.Nil(t, err, "should not error") 236 require.NotNil(t, class) 237 238 assert.Len(t, class.ID, 36, "an id was assigned") 239 id = class.ID 240 }) 241 242 t.Run("retrieving the same classification by id", func(t *testing.T) { 243 class, err := classifier.Get(context.Background(), nil, id) 244 require.Nil(t, err) 245 require.NotNil(t, class) 246 assert.Equal(t, id, class.ID) 247 }) 248 249 // TODO: improve by polling instead 250 time.Sleep(500 * time.Millisecond) 251 252 t.Run("status is failed", func(t *testing.T) { 253 class, err := classifier.Get(context.Background(), nil, id) 254 require.Nil(t, err) 255 require.NotNil(t, class) 256 assert.Equal(t, models.ClassificationStatusFailed, class.Status) 257 assert.Equal(t, notRecoginzedContextual, class.Type) 258 assert.Contains(t, class.Error, "classifier "+notRecoginzedContextual+" not found") 259 }) 260 }) 261 262 t.Run("with valid data", func(t *testing.T) { 263 sg := &fakeSchemaGetter{testSchema()} 264 repo := newFakeClassificationRepo() 265 authorizer := &fakeAuthorizer{} 266 267 vectorRepo := newFakeVectorRepoContextual(testDataToBeClassified(), testDataPossibleTargets()) 268 logger, _ := test.NewNullLogger() 269 270 modulesProvider := NewFakeModulesProvider() 271 classifier := New(sg, repo, vectorRepo, authorizer, logger, modulesProvider) 272 273 contextual := "text2vec-contextionary-custom-contextual" 274 params := models.Classification{ 275 Class: "Article", 276 BasedOnProperties: []string{"description"}, 277 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 278 Type: contextual, 279 } 280 281 t.Run("scheduling a classification", func(t *testing.T) { 282 class, err := classifier.Schedule(context.Background(), nil, params) 283 require.Nil(t, err, "should not error") 284 require.NotNil(t, class) 285 286 assert.Len(t, class.ID, 36, "an id was assigned") 287 id = class.ID 288 }) 289 290 t.Run("retrieving the same classification by id", func(t *testing.T) { 291 class, err := classifier.Get(context.Background(), nil, id) 292 require.Nil(t, err) 293 require.NotNil(t, class) 294 assert.Equal(t, id, class.ID) 295 }) 296 297 // TODO: improve by polling instead 298 time.Sleep(500 * time.Millisecond) 299 300 t.Run("status is now completed", func(t *testing.T) { 301 class, err := classifier.Get(context.Background(), nil, id) 302 require.Nil(t, err) 303 require.NotNil(t, class) 304 assert.Equal(t, models.ClassificationStatusCompleted, class.Status) 305 }) 306 307 t.Run("the classifier updated the actions with the classified references", func(t *testing.T) { 308 vectorRepo.Lock() 309 require.Len(t, vectorRepo.db, 6) 310 vectorRepo.Unlock() 311 312 t.Run("food", func(t *testing.T) { 313 idArticleFoodOne := "06a1e824-889c-4649-97f9-1ed3fa401d8e" 314 idArticleFoodTwo := "6402e649-b1e0-40ea-b192-a64eab0d5e56" 315 316 checkRef(t, vectorRepo, idArticleFoodOne, "exactCategory", idCategoryFoodAndDrink) 317 checkRef(t, vectorRepo, idArticleFoodTwo, "mainCategory", idMainCategoryFoodAndDrink) 318 }) 319 320 t.Run("politics", func(t *testing.T) { 321 idArticlePoliticsOne := "75ba35af-6a08-40ae-b442-3bec69b355f9" 322 idArticlePoliticsTwo := "f850439a-d3cd-4f17-8fbf-5a64405645cd" 323 324 checkRef(t, vectorRepo, idArticlePoliticsOne, "exactCategory", idCategoryPolitics) 325 checkRef(t, vectorRepo, idArticlePoliticsTwo, "mainCategory", idMainCategoryPoliticsAndSociety) 326 }) 327 328 t.Run("society", func(t *testing.T) { 329 idArticleSocietyOne := "a2bbcbdc-76e1-477d-9e72-a6d2cfb50109" 330 idArticleSocietyTwo := "069410c3-4b9e-4f68-8034-32a066cb7997" 331 332 checkRef(t, vectorRepo, idArticleSocietyOne, "exactCategory", idCategorySociety) 333 checkRef(t, vectorRepo, idArticleSocietyTwo, "mainCategory", idMainCategoryPoliticsAndSociety) 334 }) 335 }) 336 }) 337 338 t.Run("when errors occur during classification", func(t *testing.T) { 339 sg := &fakeSchemaGetter{testSchema()} 340 repo := newFakeClassificationRepo() 341 authorizer := &fakeAuthorizer{} 342 vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified()) 343 vectorRepo.errorOnAggregate = errors.New("something went wrong") 344 logger, _ := test.NewNullLogger() 345 classifier := New(sg, repo, vectorRepo, authorizer, logger, nil) 346 347 params := models.Classification{ 348 Class: "Article", 349 BasedOnProperties: []string{"description"}, 350 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 351 Settings: map[string]interface{}{ 352 "k": json.Number("1"), 353 }, 354 } 355 356 t.Run("scheduling a classification", func(t *testing.T) { 357 class, err := classifier.Schedule(context.Background(), nil, params) 358 require.Nil(t, err, "should not error") 359 require.NotNil(t, class) 360 361 assert.Len(t, class.ID, 36, "an id was assigned") 362 id = class.ID 363 }) 364 365 waitForStatusToNoLongerBeRunning(t, classifier, id) 366 367 t.Run("status is now failed", func(t *testing.T) { 368 class, err := classifier.Get(context.Background(), nil, id) 369 require.Nil(t, err) 370 require.NotNil(t, class) 371 assert.Equal(t, models.ClassificationStatusFailed, class.Status) 372 expectedErrStrings := []string{ 373 "classification failed: ", 374 "classify Article/75ba35af-6a08-40ae-b442-3bec69b355f9: something went wrong", 375 "classify Article/f850439a-d3cd-4f17-8fbf-5a64405645cd: something went wrong", 376 "classify Article/a2bbcbdc-76e1-477d-9e72-a6d2cfb50109: something went wrong", 377 "classify Article/069410c3-4b9e-4f68-8034-32a066cb7997: something went wrong", 378 "classify Article/06a1e824-889c-4649-97f9-1ed3fa401d8e: something went wrong", 379 "classify Article/6402e649-b1e0-40ea-b192-a64eab0d5e56: something went wrong", 380 } 381 for _, msg := range expectedErrStrings { 382 assert.Contains(t, class.Error, msg) 383 } 384 }) 385 }) 386 387 t.Run("when there is nothing to be classified", func(t *testing.T) { 388 sg := &fakeSchemaGetter{testSchema()} 389 repo := newFakeClassificationRepo() 390 authorizer := &fakeAuthorizer{} 391 vectorRepo := newFakeVectorRepoKNN(nil, testDataAlreadyClassified()) 392 logger, _ := test.NewNullLogger() 393 classifier := New(sg, repo, vectorRepo, authorizer, logger, nil) 394 395 params := models.Classification{ 396 Class: "Article", 397 BasedOnProperties: []string{"description"}, 398 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 399 Settings: map[string]interface{}{ 400 "k": json.Number("1"), 401 }, 402 } 403 404 t.Run("scheduling a classification", func(t *testing.T) { 405 class, err := classifier.Schedule(context.Background(), nil, params) 406 require.Nil(t, err, "should not error") 407 require.NotNil(t, class) 408 409 assert.Len(t, class.ID, 36, "an id was assigned") 410 id = class.ID 411 }) 412 413 waitForStatusToNoLongerBeRunning(t, classifier, id) 414 415 t.Run("status is now failed", func(t *testing.T) { 416 class, err := classifier.Get(context.Background(), nil, id) 417 require.Nil(t, err) 418 require.NotNil(t, class) 419 assert.Equal(t, models.ClassificationStatusFailed, class.Status) 420 expectedErr := "classification failed: " + 421 "no classes to be classified - did you run a previous classification already?" 422 assert.Equal(t, expectedErr, class.Error) 423 }) 424 }) 425 } 426 427 func Test_Classifier_WhereFilterValidation(t *testing.T) { 428 t.Run("when invalid whereFilters are received", func(t *testing.T) { 429 sg := &fakeSchemaGetter{testSchema()} 430 repo := newFakeClassificationRepo() 431 authorizer := &fakeAuthorizer{} 432 vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified()) 433 classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil) 434 435 t.Run("with only one of the where filters being set", func(t *testing.T) { 436 whereFilter := &models.WhereFilter{ 437 Path: []string{"id"}, 438 Operator: "Like", 439 ValueText: ptString("*"), 440 } 441 testData := []struct { 442 name string 443 classificationType string 444 classificationFilters *models.ClassificationFilters 445 }{ 446 { 447 name: "Contextual only source where filter set", 448 classificationType: TypeContextual, 449 classificationFilters: &models.ClassificationFilters{ 450 SourceWhere: whereFilter, 451 }, 452 }, 453 { 454 name: "Contextual only target where filter set", 455 classificationType: TypeContextual, 456 classificationFilters: &models.ClassificationFilters{ 457 TargetWhere: whereFilter, 458 }, 459 }, 460 { 461 name: "ZeroShot only source where filter set", 462 classificationType: TypeZeroShot, 463 classificationFilters: &models.ClassificationFilters{ 464 SourceWhere: whereFilter, 465 }, 466 }, 467 { 468 name: "ZeroShot only target where filter set", 469 classificationType: TypeZeroShot, 470 classificationFilters: &models.ClassificationFilters{ 471 TargetWhere: whereFilter, 472 }, 473 }, 474 { 475 name: "KNN only source where filter set", 476 classificationType: TypeKNN, 477 classificationFilters: &models.ClassificationFilters{ 478 SourceWhere: whereFilter, 479 }, 480 }, 481 { 482 name: "KNN only training set where filter set", 483 classificationType: TypeKNN, 484 classificationFilters: &models.ClassificationFilters{ 485 TrainingSetWhere: whereFilter, 486 }, 487 }, 488 } 489 for _, td := range testData { 490 t.Run(td.name, func(t *testing.T) { 491 params := models.Classification{ 492 Class: "Article", 493 BasedOnProperties: []string{"description"}, 494 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 495 Settings: map[string]interface{}{ 496 "k": json.Number("1"), 497 }, 498 Type: td.classificationType, 499 Filters: td.classificationFilters, 500 } 501 class, err := classifier.Schedule(context.Background(), nil, params) 502 assert.Nil(t, err) 503 assert.NotNil(t, class) 504 505 assert.Len(t, class.ID, 36, "an id was assigned") 506 waitForStatusToNoLongerBeRunning(t, classifier, class.ID) 507 }) 508 } 509 }) 510 }) 511 512 t.Run("[deprecated string] when valueString whereFilters are received", func(t *testing.T) { 513 sg := &fakeSchemaGetter{testSchema()} 514 repo := newFakeClassificationRepo() 515 authorizer := &fakeAuthorizer{} 516 vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified()) 517 classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil) 518 519 validFilter := &models.WhereFilter{ 520 Path: []string{"description"}, 521 Operator: "Equal", 522 ValueText: ptString("valueText is valid"), 523 } 524 deprecatedFilter := &models.WhereFilter{ 525 Path: []string{"description"}, 526 Operator: "Equal", 527 ValueString: ptString("valueString is accepted"), 528 } 529 530 t.Run("with deprecated sourceFilter", func(t *testing.T) { 531 params := models.Classification{ 532 Class: "Article", 533 BasedOnProperties: []string{"description"}, 534 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 535 Settings: map[string]interface{}{ 536 "k": json.Number("1"), 537 }, 538 Filters: &models.ClassificationFilters{ 539 SourceWhere: deprecatedFilter, 540 }, 541 Type: TypeContextual, 542 } 543 544 _, err := classifier.Schedule(context.Background(), nil, params) 545 assert.Nil(t, err) 546 }) 547 548 t.Run("with deprecated targetFilter", func(t *testing.T) { 549 params := models.Classification{ 550 Class: "Article", 551 BasedOnProperties: []string{"description"}, 552 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 553 Settings: map[string]interface{}{ 554 "k": json.Number("1"), 555 }, 556 Filters: &models.ClassificationFilters{ 557 SourceWhere: validFilter, 558 TargetWhere: deprecatedFilter, 559 }, 560 Type: TypeContextual, 561 } 562 563 _, err := classifier.Schedule(context.Background(), nil, params) 564 assert.Nil(t, err) 565 }) 566 567 t.Run("with deprecated trainingFilter", func(t *testing.T) { 568 params := models.Classification{ 569 Class: "Article", 570 BasedOnProperties: []string{"description"}, 571 ClassifyProperties: []string{"exactCategory", "mainCategory"}, 572 Settings: map[string]interface{}{ 573 "k": json.Number("1"), 574 }, 575 Filters: &models.ClassificationFilters{ 576 SourceWhere: validFilter, 577 TrainingSetWhere: deprecatedFilter, 578 }, 579 Type: TypeKNN, 580 } 581 582 _, err := classifier.Schedule(context.Background(), nil, params) 583 assert.Nil(t, err) 584 }) 585 }) 586 } 587 588 type genericFakeRepo interface { 589 get(strfmt.UUID) (*models.Object, bool) 590 } 591 592 func checkRef(t *testing.T, repo genericFakeRepo, source, propName, target string) { 593 object, ok := repo.get(strfmt.UUID(source)) 594 require.True(t, ok, "object must be present") 595 596 schema, ok := object.Properties.(map[string]interface{}) 597 require.True(t, ok, "schema must be map") 598 599 prop, ok := schema[propName] 600 require.True(t, ok, "ref prop must be present") 601 602 refs, ok := prop.(models.MultipleRef) 603 require.True(t, ok, "ref prop must be models.MultipleRef") 604 require.Len(t, refs, 1, "refs must have len 1") 605 606 assert.Equal(t, fmt.Sprintf("weaviate://localhost/%s", target), refs[0].Beacon.String(), "beacon must match") 607 } 608 609 func waitForStatusToNoLongerBeRunning(t *testing.T, classifier *Classifier, id strfmt.UUID) { 610 testhelper.AssertEventuallyEqualWithFrequencyAndTimeout(t, true, func() interface{} { 611 class, err := classifier.Get(context.Background(), nil, id) 612 require.Nil(t, err) 613 require.NotNil(t, class) 614 615 return class.Status != models.ClassificationStatusRunning 616 }, 100*time.Millisecond, 20*time.Second, "wait until status in no longer running") 617 }