def test_valid_schema_ok(): i = ComponentInterfaceFactory( schema={"type": "object"}, relative_path="test.json", kind=InterfaceKindChoices.ANY, ) i.full_clean()
def test_input_prefixes(tmp_path, settings): interfaces = [ ComponentInterfaceFactory( kind=InterfaceKindChoices.BOOL, relative_path="test/bool.json" ), ComponentInterfaceFactory( kind=InterfaceKindChoices.IMAGE, relative_path="images/test-image" ), ComponentInterfaceFactory( kind=InterfaceKindChoices.CSV, relative_path="test.csv" ), ] civs = [ ComponentInterfaceValueFactory(interface=interfaces[0], value=True), ComponentInterfaceValueFactory( interface=interfaces[1], image=ImageFileFactory( file__from_path=Path(__file__).parent.parent / "algorithms_tests" / "resources" / "input_file.tif" ).image, ), ComponentInterfaceValueFactory(interface=interfaces[2]), ] settings.COMPONENTS_AMAZON_ECS_NFS_MOUNT_POINT = tmp_path executor = AmazonECSExecutorStub( job_id="algorithms-job-00000000-0000-0000-0000-000000000000", exec_image_sha256="", exec_image_repo_tag="", memory_limit=4, time_limit=60, requires_gpu=False, ) executor.provision( input_civs=civs, input_prefixes={ str(civs[0].pk): "first/output/", str(civs[1].pk): "second/output", }, ) assert {str(f.relative_to(tmp_path)) for f in tmp_path.glob("**/*")} == { "algorithms", "algorithms/job", "algorithms/job/00000000-0000-0000-0000-000000000000", "algorithms/job/00000000-0000-0000-0000-000000000000/input", "algorithms/job/00000000-0000-0000-0000-000000000000/input/test.csv", "algorithms/job/00000000-0000-0000-0000-000000000000/input/first", "algorithms/job/00000000-0000-0000-0000-000000000000/input/first/output", "algorithms/job/00000000-0000-0000-0000-000000000000/input/first/output/test", "algorithms/job/00000000-0000-0000-0000-000000000000/input/first/output/test/bool.json", "algorithms/job/00000000-0000-0000-0000-000000000000/input/second", "algorithms/job/00000000-0000-0000-0000-000000000000/input/second/output", "algorithms/job/00000000-0000-0000-0000-000000000000/input/second/output/images", "algorithms/job/00000000-0000-0000-0000-000000000000/input/second/output/images/test-image", "algorithms/job/00000000-0000-0000-0000-000000000000/input/second/output/images/test-image/input_file.tif", "algorithms/job/00000000-0000-0000-0000-000000000000/output", }
def test_invalid_schema_raises_error(): i = ComponentInterfaceFactory(schema={"type": "whatevs"}) with pytest.raises(ValidationError) as e: i.full_clean() assert str(e.value).startswith( "{'schema': [\"Invalid schema: 'whatevs' is not valid under any of the given schemas" )
def test_unmatched_interface_filter(self): ai = AlgorithmImageFactory() cis = ComponentInterfaceFactory.create_batch(2) ai.algorithm.inputs.set(cis) civ_sets = [ {}, # No interfaces {ComponentInterfaceValueFactory(interface=cis[0]) }, # Missing interface { # OK ComponentInterfaceValueFactory(interface=cis[0]), ComponentInterfaceValueFactory(interface=cis[1]), }, { # Unmatched interface ComponentInterfaceValueFactory(interface=cis[0]), ComponentInterfaceValueFactory( interface=ComponentInterfaceFactory()), }, ] filtered_civ_sets = filter_civs_for_algorithm(civ_sets=civ_sets, algorithm_image=ai) assert filtered_civ_sets == [civ_sets[2]]
def test_no_uuid_validation(): # For multi job inputs we add uuid prefixes, so check that the relative # path does not contain a UUID i = ComponentInterfaceFactory( relative_path=f"{uuid.uuid4()}/whatever.json", kind=InterfaceKindChoices.ANY, ) with pytest.raises(ValidationError) as e: i.full_clean() assert str(e.value) == "{'relative_path': ['Enter a valid value.']}"
def test_save_in_object_store(kind, object_store_required): ci = ComponentInterfaceFactory(kind=kind, store_in_database=True) if object_store_required: assert ci.save_in_object_store is True ci.store_in_database = False assert ci.save_in_object_store is True else: assert ci.save_in_object_store is False ci.store_in_database = False assert ci.save_in_object_store is True
def test_algorithm_job_post_serializer_validations( title, add_user, image_ready, algorithm_interface_titles, job_interface_slugs, error_message, rf, ): # setup user = UserFactory() interfaces = { "TestInterface 1": ComponentInterfaceFactory(kind=ComponentInterface.Kind.STRING, title="TestInterface 1"), "TestInterface 2": ComponentInterfaceFactory(kind=ComponentInterface.Kind.STRING, title="TestInterface 2"), } algorithm_image = AlgorithmImageFactory(ready=image_ready) algorithm_image.algorithm.title = title algorithm_image.algorithm.inputs.set( [interfaces[title] for title in algorithm_interface_titles]) if add_user: algorithm_image.algorithm.add_editor(user) algorithm_image.algorithm.save() job = { "algorithm": algorithm_image.algorithm.api_url, "inputs": [{ "interface": interface, "value": "dummy" } for interface in job_interface_slugs], } # test request = rf.get("/foo") request.user = user serializer = JobPostSerializer(data=job, context={"request": request}) # verify assert serializer.is_valid() == (error_message is None) if error_message: assert error_message in str(serializer.errors) else: assert len(Job.objects.all()) == 0 serializer.create(serializer.validated_data) assert len(Job.objects.all()) == 1 job = Job.objects.first() assert job.status == job.PENDING assert len(job.inputs.all()) == 2
def test_algorithm_job_post_serializer_create(rf): # setup user = UserFactory() upload, upload_2 = ( RawImageUploadSessionFactory(creator=user), RawImageUploadSessionFactory(creator=user), ) image = ImageFactory() upload_2.image_set.set([image]) assign_perm("view_image", user, image) assert user.has_perm("view_image", image) algorithm_image = AlgorithmImageFactory(ready=True) interfaces = { ComponentInterfaceFactory( kind=ComponentInterface.Kind.STRING, title="TestInterface 1", default_value="default", ), ComponentInterfaceFactory(kind=ComponentInterface.Kind.IMAGE, title="TestInterface 2"), ComponentInterfaceFactory(kind=ComponentInterface.Kind.IMAGE, title="TestInterface 3"), } algorithm_image.algorithm.inputs.set(interfaces) algorithm_image.algorithm.add_editor(user) algorithm_image.algorithm.save() job = {"algorithm": algorithm_image.algorithm.api_url, "inputs": []} job["inputs"].append({ "interface": "testinterface-2", "upload_session": upload.api_url }) job["inputs"].append({ "interface": "testinterface-3", "image": image.api_url }) # test request = rf.get("/foo") request.user = user serializer = JobPostSerializer(data=job, context={"request": request}) # verify assert serializer.is_valid() serializer.create(serializer.validated_data) assert len(Job.objects.all()) == 1 job = Job.objects.first() assert job.creator == user assert len(job.inputs.all()) == 3
def test_view_content_mixin(): form = DummyForm() form.cleaned_data["view_content"] = {"main": ["test"]} form.clean_view_content() assert "Unkown slugs in view_content: test" in form.errors["view_content"] form.errors = {"view_content": []} form.cleaned_data["hanging_protocol"] = HangingProtocolFactory( json=[{ "viewport_name": "main" }]) form.cleaned_data["view_content"] = {"secondary": ["test"]} form.clean_view_content() assert ( "Image ports in view_content do not match those in the selected hanging protocol." in form.errors["view_content"]) assert "Unkown slugs in view_content: test" in form.errors["view_content"] ComponentInterfaceFactory(title="Test") form.errors = {"view_content": []} form.cleaned_data["hanging_protocol"] = HangingProtocolFactory( json=[{ "viewport_name": "main" }]) form.cleaned_data["view_content"] = {"main": ["test"]} form.clean_view_content() assert form.errors["view_content"] == []
def test_disjoint_interfaces(): i = ComponentInterfaceFactory() form = AlgorithmForm( user=UserFactory(), data={"inputs": [i.pk], "outputs": [i.pk]} ) assert form.is_valid() is False assert "The sets of Inputs and Outputs must be unique" in str(form.errors)
def test_assert_modification_allowed(): rs = ReaderStudyFactory(use_display_sets=False) ci = ComponentInterfaceFactory( kind=InterfaceKind.InterfaceKindChoices.BOOL) civ = ComponentInterfaceValueFactory(interface=ci, value=True) ds = DisplaySetFactory(reader_study=rs) ds.values.add(civ) del ds.is_editable civ2 = ComponentInterfaceValueFactory(interface=ci, value=True) ds.values.remove(civ) ds.values.add(civ2) assert ds.values.count() == 1 assert ds.values.first() == civ2 q = QuestionFactory(reader_study=rs) AnswerFactory(question=q, display_set=ds) del ds.is_editable with pytest.raises(ValidationError): with transaction.atomic(): ds.values.remove(civ2) assert ds.values.count() == 1 assert ds.values.first() == civ2
def test_ecs_unzip(tmp_path, settings, submission_file): interface = ComponentInterfaceFactory( kind=InterfaceKindChoices.ZIP, relative_path="preds.zip" ) civ = ComponentInterfaceValueFactory(interface=interface) with open(submission_file, "rb") as f: civ.file.save("my_submission.zip", File(f)) settings.COMPONENTS_AMAZON_ECS_NFS_MOUNT_POINT = tmp_path executor = AmazonECSExecutorStub( job_id="algorithms-job-00000000-0000-0000-0000-000000000000", exec_image_sha256="", exec_image_repo_tag="", memory_limit=4, time_limit=60, requires_gpu=False, ) executor.provision(input_civs=[civ], input_prefixes={}) assert {str(f.relative_to(tmp_path)) for f in tmp_path.glob("**/*")} == { "algorithms", "algorithms/job", "algorithms/job/00000000-0000-0000-0000-000000000000", "algorithms/job/00000000-0000-0000-0000-000000000000/input", "algorithms/job/00000000-0000-0000-0000-000000000000/input/submission.csv", "algorithms/job/00000000-0000-0000-0000-000000000000/input/images", "algorithms/job/00000000-0000-0000-0000-000000000000/input/images/image10x10x10.mhd", "algorithms/job/00000000-0000-0000-0000-000000000000/input/images/image10x10x10.zraw", "algorithms/job/00000000-0000-0000-0000-000000000000/output", }
def test_api_archive_item_update_permissions(client, settings, add_to_group, status): # Override the celery settings settings.task_eager_propagates = (True, ) settings.task_always_eager = (True, ) archive = ArchiveFactory() user = UserFactory() item = ArchiveItemFactory(archive=archive) if add_to_group: add_to_group(archive, user) ci = ComponentInterfaceFactory( kind=InterfaceKind.InterfaceKindChoices.BOOL) with capture_on_commit_callbacks(execute=True): response = get_view_for_user( viewname="api:archives-item-detail", reverse_kwargs={"pk": item.pk}, data={"values": [{ "interface": ci.slug, "value": True }]}, user=user, client=client, method=client.patch, content_type="application/json", HTTP_X_FORWARDED_PROTO="https", ) assert response.status_code == status
def test_existing_jobs(self): ai = AlgorithmImageFactory() cis = ComponentInterfaceFactory.create_batch(2) ai.algorithm.inputs.set(cis) civs = [ComponentInterfaceValueFactory(interface=c) for c in cis] j = AlgorithmJobFactory(algorithm_image=ai) j.inputs.set(civs) civ_sets = [ civs, # Job already exists { # New values ComponentInterfaceValueFactory(interface=cis[0]), ComponentInterfaceValueFactory(interface=cis[1]), }, { # Changed values civs[0], ComponentInterfaceValueFactory(interface=cis[1]), }, ] filtered_civ_sets = filter_civs_for_algorithm(civ_sets=civ_sets, algorithm_image=ai) assert filtered_civ_sets == civ_sets[1:]
def test_algorithm_with_invalid_output(client, algorithm_image, settings): # Override the celery settings settings.task_eager_propagates = (True, ) settings.task_always_eager = (True, ) assert Job.objects.count() == 0 # Create the algorithm image algorithm_container, sha256 = algorithm_image alg = AlgorithmImageFactory(image__from_path=algorithm_container, image_sha256=sha256, ready=True) # Make sure the job fails when trying to upload an invalid file detection_interface = ComponentInterfaceFactory( store_in_database=False, relative_path="some_text.txt", slug="detection-json-file", kind=ComponentInterface.Kind.JSON, ) alg.algorithm.outputs.add(detection_interface) alg.save() image_file = ImageFileFactory(file__from_path=Path(__file__).parent / "resources" / "input_file.tif") with capture_on_commit_callbacks(execute=True): execute_jobs(algorithm_image=alg, images=[image_file.image]) jobs = Job.objects.filter(algorithm_image=alg, inputs__image=image_file.image, status=Job.FAILURE).all() assert len(jobs) == 1 assert jobs.first().error_message == "Invalid filetype." assert len(jobs[0].outputs.all()) == 2
def test_disjoint_interfaces(): i = ComponentInterfaceFactory() form = PhaseAdmin.form(data={ "algorithm_inputs": [i.pk], "algorithm_outputs": [i.pk] }) assert form.is_valid() is False assert ("The sets of Algorithm Inputs and Algorithm Outputs must be unique" in str(form.errors))
def test_unmatched_interface_filter_subset(self): ai = AlgorithmImageFactory() cis = ComponentInterfaceFactory.create_batch(2) ai.algorithm.inputs.set(cis) civ_sets = [{ # Extra interface ComponentInterfaceValueFactory(interface=cis[0]), ComponentInterfaceValueFactory(interface=cis[1]), ComponentInterfaceValueFactory( interface=ComponentInterfaceFactory()), }] filtered_civ_sets = filter_civs_for_algorithm(civ_sets=civ_sets, algorithm_image=ai) assert len(filtered_civ_sets) == 1 assert {civ.interface for civ in filtered_civ_sets[0]} == {*cis}
def test_extra_schema_validation(kind, value, invalidation_schema, use_file): i = ComponentInterfaceFactory(kind=kind, store_in_database=not use_file) if use_file: kwargs = { "file": ContentFile( json.dumps(value).encode("utf-8"), name="test.json" ) } else: kwargs = {"value": value} v = ComponentInterfaceValue(interface=i, **kwargs) v.full_clean() i.schema = invalidation_schema with pytest.raises(ValidationError): v.full_clean()
def test_api_archive_item_add_and_update_value(client, settings): # Override the celery settings settings.task_eager_propagates = (True, ) settings.task_always_eager = (True, ) archive = ArchiveFactory() editor = UserFactory() archive.add_editor(editor) item = ArchiveItemFactory(archive=archive) ci = ComponentInterfaceFactory( kind=InterfaceKind.InterfaceKindChoices.BOOL) # add civ with capture_on_commit_callbacks(execute=True): response = get_view_for_user( viewname="api:archives-item-detail", reverse_kwargs={"pk": item.pk}, data={"values": [{ "interface": ci.slug, "value": True }]}, user=editor, client=client, method=client.patch, content_type="application/json", HTTP_X_FORWARDED_PROTO="https", ) assert response.status_code == 200 assert response.json()["pk"] == str(item.pk) item.refresh_from_db() assert item.values.count() == 1 civ = item.values.get() assert civ.interface.slug == ci.slug assert civ.value # update civ with capture_on_commit_callbacks(execute=True): response = get_view_for_user( viewname="api:archives-item-detail", reverse_kwargs={"pk": item.pk}, data={"values": [{ "interface": ci.slug, "value": False }]}, user=editor, client=client, method=client.patch, content_type="application/json", HTTP_X_FORWARDED_PROTO="https", ) assert response.status_code == 200 assert response.json()["pk"] == str(item.pk) item.refresh_from_db() assert item.values.count() == 1 new_civ = item.values.get() assert new_civ.interface.slug == ci.slug assert new_civ != civ
def test_civ_post_image_or_upload_required_validation(kind): # setup interface = ComponentInterfaceFactory(kind=kind) civ = {"interface": interface.slug} # test serializer = ComponentInterfaceValuePostSerializer(data=civ) # verify assert not serializer.is_valid() assert (f"upload_session or image are required for interface kind {kind}" in serializer.errors["non_field_errors"])
def test_civ_post_value_required(kind): # setup interface = ComponentInterfaceFactory(kind=kind) civ = {"interface": interface.slug} # test serializer = ComponentInterfaceValuePostSerializer(data=civ) # verify assert not serializer.is_valid() assert "JSON does not fulfill schema: None is not of type" in str( serializer.errors["__all__"][0])
def test_multi_value_fails(kind, image, file, value): if image: image = ImageFactory() if file: file = ContentFile(json.dumps(True).encode("utf-8"), name="test.csv") i = ComponentInterfaceFactory(kind=kind) v = ComponentInterfaceValue( interface=i, image=image, file=file, value=value ) with pytest.raises(ValidationError): v.full_clean()
def test_api_archive_item_interface_type_update(client, settings): # Override the celery settings settings.task_eager_propagates = (True, ) settings.task_always_eager = (True, ) archive = ArchiveFactory() editor = UserFactory() archive.add_editor(editor) item = ArchiveItemFactory(archive=archive) session, _ = create_raw_upload_image_session(images=["image10x10x10.mha"], user=editor) session.refresh_from_db() im = session.image_set.get() ci = ComponentInterfaceFactory( kind=InterfaceKind.InterfaceKindChoices.IMAGE) civ = ComponentInterfaceValueFactory(interface=ci, image=im) item.values.add(civ) civ.image.update_viewer_groups_permissions() assert item.values.count() == 1 # change interface type from generic medical image to generic overlay # for the already uploaded image with capture_on_commit_callbacks(execute=True): response = get_view_for_user( viewname="api:archives-item-detail", reverse_kwargs={"pk": item.pk}, data={ "values": [{ "interface": "generic-overlay", "image": im.api_url }] }, user=editor, client=client, method=client.patch, content_type="application/json", HTTP_X_FORWARDED_PROTO="https", ) assert response.status_code == 200 assert response.json()["pk"] == str(item.pk) item.refresh_from_db() # check that the old item was removed and a new one was added with the same # image but the new interface type assert item.values.count() == 1 new_civ = item.values.get() assert new_civ.interface.slug == "generic-overlay" assert new_civ.image == im assert new_civ != civ
def setUp(self) -> None: self.method = MethodFactory(ready=True, phase__archive=ArchiveFactory()) self.algorithm_image = AlgorithmImageFactory() interface = ComponentInterfaceFactory() self.algorithm_image.algorithm.inputs.set([interface]) self.images = ImageFactory.create_batch(3) for image in self.images[:2]: civ = ComponentInterfaceValueFactory(image=image, interface=interface) ai = ArchiveItemFactory(archive=self.method.phase.archive) ai.values.add(civ)
def test_default_validation(kind, value, expectation, use_file): i = ComponentInterfaceFactory(kind=kind, store_in_database=not use_file) if use_file: kwargs = { "file": ContentFile( json.dumps(value).encode("utf-8"), name="test.json" ) } else: kwargs = {"value": value} v = ComponentInterfaceValue(interface=i, **kwargs) with expectation: v.full_clean()
def test_civ_post_image_valid(kind, rf): # setup user = UserFactory() upload = UploadSessionFactory(status=RawImageUploadSession.PENDING, creator=user) interface = ComponentInterfaceFactory(kind=kind) civ = {"interface": interface.slug, "upload_session": upload.api_url} # test request = rf.get("/foo") request.user = user serializer = ComponentInterfaceValuePostSerializer( data=civ, context={"request": request}) # verify assert serializer.is_valid()
def test_relative_path_file_ending(kind): if kind in InterfaceKind.interface_type_json(): good_suffix = "json" else: good_suffix = kind.lower() i = ComponentInterfaceFactory( kind=kind, relative_path=f"foo/bar.{good_suffix}", store_in_database=False, ) i.full_clean() i.relative_path = "foo/bar" with pytest.raises(ValidationError): i.full_clean()
def test_civ_post_value_validation(kind): # setup interface = ComponentInterfaceFactory(kind=kind) for test in TEST_DATA: civ = {"interface": interface.slug, "value": TEST_DATA[test]} # test serializer = ComponentInterfaceValuePostSerializer(data=civ) # verify assert serializer.is_valid() == ( kind == test or ( # Ints are valid for float types kind == "FLT" and test == "INT")) if not serializer.is_valid(): assert "JSON does not fulfill schema: " in str( serializer.errors["__all__"][0])
def test_civ_post_upload_permission_validation(kind, rf): # setup user = UserFactory() upload = UploadSessionFactory() interface = ComponentInterfaceFactory(kind=kind) civ = {"interface": interface.slug, "upload_session": upload.api_url} # test request = rf.get("/foo") request.user = user serializer = ComponentInterfaceValuePostSerializer( data=civ, context={"request": request}) # verify assert not serializer.is_valid() assert ("Invalid hyperlink - Object does not exist" in serializer.errors["upload_session"][0])
def test_user_upload_to_archive_item_with_new_interface(client, settings): # Override the celery settings settings.task_eager_propagates = (True, ) settings.task_always_eager = (True, ) user = UserFactory() archive = ArchiveFactory() archive.add_editor(user=user) ci = ComponentInterfaceFactory(kind=ComponentInterface.Kind.STRING, title="Test") civ = ComponentInterfaceValueFactory(interface=ci) item = ArchiveItemFactory(archive=archive) item.values.add(civ) assert item.values.count() == 1 upload = create_upload_from_file( file_path=Path(__file__).parent / "resources" / "image10x10x10.mha", creator=user, ) with capture_on_commit_callbacks(execute=True): response = get_view_for_user( viewname="api:upload-session-list", user=user, client=client, method=client.post, content_type="application/json", data={ "uploads": [upload.api_url], "archive_item": item.pk, "interface": "generic-overlay", }, HTTP_X_FORWARDED_PROTO="https", ) assert response.status_code == 201 upload_session = response.json() assert upload_session["uploads"] == [upload.api_url] item.refresh_from_db() assert item.values.count() == 2 assert "generic-overlay" in [ item.interface.slug for item in item.values.all() ]