def test_job_group_created(self): j = AlgorithmJobFactory() assert j.viewers is not None assert j.viewers.name.startswith("algorithms_job_") assert j.viewers.name.endswith("_viewers")
def test_creator_in_viewers_group(self): j = AlgorithmJobFactory() assert {*j.viewers.user_set.all()} == {j.creator}
def test_algorithm_jobs_list_view(client): # This view is a bit special, everyone should be able to # view it, but the results should be filtered alg_set = TwoAlgorithms() extra_user1, extra_user2 = UserFactory(), UserFactory() alg_set.alg1.add_user(extra_user1) alg_set.alg2.add_user(extra_user2) j1, j2 = ( AlgorithmJobFactory( algorithm_image__algorithm=alg_set.alg1, creator=extra_user1, status=Job.SUCCESS, ), AlgorithmJobFactory( algorithm_image__algorithm=alg_set.alg2, creator=extra_user2, status=Job.SUCCESS, ), ) all_jobs = {j1, j2} tests = ( (None, alg_set.alg1, 200, set()), (None, alg_set.alg2, 200, set()), (alg_set.creator, alg_set.alg1, 200, set()), (alg_set.creator, alg_set.alg2, 200, set()), (alg_set.editor1, alg_set.alg1, 200, {j1}), (alg_set.editor1, alg_set.alg2, 200, set()), (alg_set.user1, alg_set.alg1, 200, set()), (alg_set.user1, alg_set.alg2, 200, set()), (alg_set.editor2, alg_set.alg1, 200, set()), (alg_set.editor2, alg_set.alg2, 200, {j2}), (alg_set.user2, alg_set.alg1, 200, set()), (alg_set.user2, alg_set.alg2, 200, set()), (alg_set.u, alg_set.alg1, 200, set()), (alg_set.u, alg_set.alg2, 200, set()), (extra_user1, alg_set.alg1, 200, {j1}), (extra_user1, alg_set.alg2, 200, set()), (extra_user2, alg_set.alg1, 200, set()), (extra_user2, alg_set.alg2, 200, {j2}), ) for test in tests: response = get_view_for_user( viewname="algorithms:jobs-list", reverse_kwargs={"slug": test[1].slug}, client=client, user=test[0], data={ "length": 50, "draw": 1 }, **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}, ) assert response.status_code == test[2] # Check that the results are filtered if response.status_code == 200: expected_jobs = test[3] excluded_jobs = all_jobs - expected_jobs data = response.json()["data"] assert all(str(j.pk) in str(data) for j in expected_jobs) assert all(str(j.pk) not in str(data) for j in excluded_jobs)
def test_average_duration(): alg = AlgorithmFactory() now = timezone.now() assert alg.average_duration is None j = AlgorithmJobFactory(algorithm_image__algorithm=alg) j.started_at = now - timedelta(minutes=5) j.completed_at = now j.status = j.SUCCESS j.save() assert alg.average_duration == timedelta(minutes=5) # Unsuccessful jobs should not count j = AlgorithmJobFactory(algorithm_image__algorithm=alg) j.started_at = now - timedelta(minutes=10) j.completed_at = now j.status = j.FAILURE j.save() assert alg.average_duration == timedelta(minutes=5) # Nor should jobs for other algorithms j = AlgorithmJobFactory() j.started_at = now - timedelta(minutes=15) j.completed_at = now j.status = j.SUCCESS j.save() assert alg.average_duration == timedelta(minutes=5)
def test_algorithm_jobs_list_view(client): editor = UserFactory() alg = AlgorithmFactory(public=True) alg.add_editor(editor) im = AlgorithmImageFactory(algorithm=alg) for x in range(50): created = timezone.now() - datetime.timedelta(days=x + 365) job = AlgorithmJobFactory(algorithm_image=im, status=Job.SUCCESS) job.created = created job.save() job.viewer_groups.add(alg.editors_group) response = get_view_for_user( viewname="algorithms:job-list", reverse_kwargs={"slug": slugify(alg.slug)}, client=client, user=editor, method=client.get, follow=True, ) assert response.status_code == 200 response = get_view_for_user( viewname="algorithms:job-list", reverse_kwargs={"slug": slugify(alg.slug)}, client=client, user=editor, method=client.get, follow=True, data={ "length": 10, "draw": 1, "order[0][dir]": "desc", "order[0][column]": 0, }, **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}, ) resp = response.json() assert resp["recordsTotal"] == 50 assert len(resp["data"]) == 10 response = get_view_for_user( viewname="algorithms:job-list", reverse_kwargs={"slug": slugify(alg.slug)}, client=client, user=editor, method=client.get, follow=True, data={ "length": 50, "draw": 1, "order[0][dir]": "desc", "order[0][column]": 0, }, **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}, ) resp = response.json() assert resp["recordsTotal"] == 50 assert len(resp["data"]) == 50 response = get_view_for_user( viewname="algorithms:job-list", reverse_kwargs={"slug": slugify(alg.slug)}, client=client, user=editor, method=client.get, follow=True, data={ "length": 50, "draw": 1, "order[0][dir]": "asc", "order[0][column]": 0, }, **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}, ) resp_new = response.json() assert resp_new["recordsTotal"] == 50 assert resp_new["data"] == resp["data"][::-1] response = get_view_for_user( viewname="algorithms:job-list", reverse_kwargs={"slug": slugify(alg.slug)}, client=client, user=editor, method=client.get, follow=True, data={ "length": 50, "draw": 1, "search[value]": job.creator.username, "order[0][column]": 0, }, **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}, ) resp = response.json() assert resp["recordsTotal"] == 50 assert resp["recordsFiltered"] == 1 assert len(resp["data"]) == 1
def test_permission_required_views(self, client): ai = AlgorithmImageFactory(ready=True) s = RawImageUploadSessionFactory() u = UserFactory() j = AlgorithmJobFactory(algorithm_image=ai) p = AlgorithmPermissionRequestFactory(algorithm=ai.algorithm) VerificationFactory(user=u, is_verified=True) for view_name, kwargs, permission, obj, redirect in [ ("create", {}, "algorithms.add_algorithm", None, None), ( "detail", { "slug": ai.algorithm.slug }, "view_algorithm", ai.algorithm, reverse( "algorithms:permission-request-create", kwargs={"slug": ai.algorithm.slug}, ), ), ( "update", { "slug": ai.algorithm.slug }, "change_algorithm", ai.algorithm, None, ), ( "image-create", { "slug": ai.algorithm.slug }, "change_algorithm", ai.algorithm, None, ), ( "image-detail", { "slug": ai.algorithm.slug, "pk": ai.pk }, "view_algorithmimage", ai, None, ), ( "image-update", { "slug": ai.algorithm.slug, "pk": ai.pk }, "change_algorithmimage", ai, None, ), ( "execution-session-create", { "slug": ai.algorithm.slug }, "execute_algorithm", ai.algorithm, None, ), ( "execution-session-create-new", { "slug": ai.algorithm.slug }, "execute_algorithm", ai.algorithm, None, ), ( "execution-session-detail", { "slug": ai.algorithm.slug, "pk": s.pk }, "view_rawimageuploadsession", s, None, ), ( "job-experiment-detail", { "slug": ai.algorithm.slug, "pk": j.pk }, "view_job", j, None, ), ( "job-detail", { "slug": ai.algorithm.slug, "pk": j.pk }, "view_job", j, None, ), ( "job-update", { "slug": ai.algorithm.slug, "pk": j.pk }, "change_job", j, None, ), ( "job-viewers-update", { "slug": ai.algorithm.slug, "pk": j.pk }, "change_job", j, None, ), ( "editors-update", { "slug": ai.algorithm.slug }, "change_algorithm", ai.algorithm, None, ), ( "users-update", { "slug": ai.algorithm.slug }, "change_algorithm", ai.algorithm, None, ), ( "permission-request-update", { "slug": ai.algorithm.slug, "pk": p.pk }, "change_algorithm", ai.algorithm, None, ), ]: def _get_view(): return get_view_for_user( client=client, viewname=f"algorithms:{view_name}", reverse_kwargs=kwargs, user=u, ) response = _get_view() if redirect is not None: assert response.status_code == 302 assert response.url == redirect else: assert response.status_code == 403 assign_perm(permission, u, obj) response = _get_view() assert response.status_code == 200 remove_perm(permission, u, obj)
def test_outputs_are_set(): j = AlgorithmJobFactory() j.create_result(result={"dsaf": 35421}) outputs = j.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"dsaf": 35421} job = AlgorithmJobFactory() job.create_result(result={"foo": 13.37}) outputs = job.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"foo": 13.37} job.create_result(result={"bar": 13.37}) job.refresh_from_db() outputs = job.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"bar": 13.37} # the original job should not be modified j.refresh_from_db() outputs = j.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"dsaf": 35421}
def test_user_can_download_images(client, reverse): alg_set = TwoAlgorithms() j1_creator, j2_creator = UserFactory(), UserFactory() alg1_job = AlgorithmJobFactory(algorithm_image__algorithm=alg_set.alg1, creator=j1_creator) alg2_job = AlgorithmJobFactory(algorithm_image__algorithm=alg_set.alg2, creator=j2_creator) alg1_job.viewer_groups.add(alg_set.alg1.editors_group) alg2_job.viewer_groups.add(alg_set.alg2.editors_group) iv1, iv2, iv3, iv4 = ( ComponentInterfaceValueFactory(image=ImageFactory()), ComponentInterfaceValueFactory(image=ImageFactory()), ComponentInterfaceValueFactory(image=ImageFactory()), ComponentInterfaceValueFactory(image=ImageFactory()), ) if reverse: for im in [iv1, iv2, iv3, iv4]: im.algorithms_jobs_as_output.add(alg1_job, alg2_job) for im in [iv3, iv4]: im.algorithms_jobs_as_output.remove(alg1_job, alg2_job) for im in [iv1, iv2]: im.algorithms_jobs_as_output.remove(alg2_job) else: # Test that adding images works alg1_job.outputs.add(iv1, iv2, iv3, iv4) # Test that removing images works alg1_job.outputs.remove(iv3, iv4) tests = ( (None, 200, []), (alg_set.creator, 200, []), ( alg_set.editor1, 200, [ *[i.image.pk for i in alg1_job.inputs.all()], iv1.image.pk, iv2.image.pk, ], ), (alg_set.user1, 200, []), ( j1_creator, 200, [ *[i.image.pk for i in alg1_job.inputs.all()], iv1.image.pk, iv2.image.pk, ], ), ( alg_set.editor2, 200, [i.image.pk for i in alg2_job.inputs.all()], ), (alg_set.user2, 200, []), (j2_creator, 200, [i.image.pk for i in alg2_job.inputs.all()]), (alg_set.u, 200, []), ) for test in tests: response = get_view_for_user( viewname="api:image-list", client=client, user=test[0], content_type="application/json", ) assert response.status_code == test[1] assert response.json()["count"] == len(test[2]) pks = {obj["pk"] for obj in response.json()["results"]} assert {str(pk) for pk in test[2]} == pks # Test clearing if reverse: iv1.algorithms_jobs_as_output.clear() iv2.algorithms_jobs_as_output.clear() else: alg1_job.outputs.clear() response = get_view_for_user( viewname="api:image-list", client=client, user=j1_creator, content_type="application/json", ) assert response.status_code == 200 assert response.json()["count"] == 1
def test_viewer_group_in_m2m(self): j = AlgorithmJobFactory() assert {*j.viewer_groups.all()} == {j.viewers}
def test_user_can_download_input_images(client, reverse): alg_set = TwoAlgorithms() j1_creator, j2_creator = UserFactory(), UserFactory() alg1_job = AlgorithmJobFactory( algorithm_image__algorithm=alg_set.alg1, creator=j1_creator ) alg2_job = AlgorithmJobFactory( algorithm_image__algorithm=alg_set.alg2, creator=j2_creator ) iv1, iv2, iv3, iv4 = ( ComponentInterfaceValueFactory(image=ImageFactory()), ComponentInterfaceValueFactory(image=ImageFactory()), ComponentInterfaceValueFactory(image=ImageFactory()), ComponentInterfaceValueFactory(image=ImageFactory()), ) alg1_origin_input = [i.image.pk for i in alg1_job.inputs.all()] alg2_origin_input = [i.image.pk for i in alg2_job.inputs.all()] if reverse: for iv in [iv1, iv2, iv3, iv4]: iv.algorithms_jobs_as_input.add(alg1_job, alg2_job) for iv in [iv3, iv4]: iv.algorithms_jobs_as_input.remove(alg1_job, alg2_job) for iv in [iv1, iv2]: iv.algorithms_jobs_as_input.remove(alg2_job) else: # Test that adding images works alg1_job.inputs.add(iv1, iv2, iv3, iv4) # Test that removing images works alg1_job.inputs.remove(iv3, iv4) tests = ( (None, 401, []), (alg_set.creator, 200, []), ( alg_set.editor1, 200, [*alg1_origin_input, iv1.image.pk, iv2.image.pk], ), (alg_set.user1, 200, []), (j1_creator, 200, [*alg1_origin_input, iv1.image.pk, iv2.image.pk],), (alg_set.editor2, 200, alg2_origin_input), (alg_set.user2, 200, []), (j2_creator, 200, alg2_origin_input), (alg_set.u, 200, []), ) for test in tests: response = get_view_for_user( viewname="api:image-list", client=client, user=test[0], content_type="application/json", ) assert response.status_code == test[1] if test[1] != 401: # We provided auth details and get a response assert response.json()["count"] == len(test[2]) pks = [obj["pk"] for obj in response.json()["results"]] for pk in test[2]: assert str(pk) in pks # Test clearing if reverse: iv1.algorithms_jobs_as_input.clear() iv2.algorithms_jobs_as_input.clear() else: alg1_job.inputs.clear() response = get_view_for_user( viewname="api:image-list", client=client, user=j1_creator, content_type="application/json", ) assert response.status_code == 200 if reverse: assert response.json()["count"] == 1 else: assert response.json()["count"] == 0
def test_group_deletion_reverse(self): j = AlgorithmJobFactory() g = j.viewers with pytest.raises(ProtectedError): g.delete()
def test_outputs_are_set(): j = AlgorithmJobFactory() j.create_result(result={"dsaf": 35421}) outputs = j.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"dsaf": 35421} job = AlgorithmJobFactory() job.create_result(result={"foo": 13.37}) outputs = job.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"foo": 13.37} job.create_result(result={"bar": 13.37}) job.refresh_from_db() outputs = job.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"bar": 13.37} # the original job should not be modified j.refresh_from_db() outputs = j.outputs.all() assert len(outputs) == 1 assert outputs[0].interface.kind == InterfaceKindChoices.JSON assert outputs[0].value == {"dsaf": 35421} job = AlgorithmJobFactory() job.algorithm_image.algorithm.result_template = ( "foo score: {{result_dict.foo}}") assert job.rendered_result_text == "" job.create_result(result={"foo": 13.37}) assert job.rendered_result_text == "<p>foo score: 13.37</p>" job.algorithm_image.algorithm.result_template = "{% for key, value in dict.metrics.items() -%}{{ key }} {{ value }}{% endfor %}" assert job.rendered_result_text == "Jinja template is invalid" job.algorithm_image.algorithm.result_template = "{{ str.__add__('test')}}" assert job.rendered_result_text == "Jinja template is invalid"
def test_get_metrics(): AlgorithmJobFactory() EvaluationFactory() SessionFactory() s = UploadSessionFactory() s.status = s.REQUEUED s.save() # Note, this is the format expected by CloudWatch, # consult the API when changing this result = _get_metrics() assert result == [ { "Namespace": "testserver/algorithms", "MetricData": [ { "MetricName": "JobsQueued", "Value": 1, "Unit": "Count" }, { "MetricName": "JobsStarted", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsReQueued", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsFailed", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsSucceeded", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsCancelled", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsProvisioning", "Value": 0, "Unit": "Count", }, { "MetricName": "JobsProvisioned", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsExecuting", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsExecuted", "Value": 0, "Unit": "Count" }, { "MetricName": "JobsParsingOutputs", "Value": 0, "Unit": "Count", }, { "MetricName": "JobsExecutingAlgorithm", "Value": 0, "Unit": "Count", }, ], }, { "Namespace": "testserver/evaluation", "MetricData": [ { "MetricName": "EvaluationsQueued", "Value": 1, "Unit": "Count", }, { "MetricName": "EvaluationsStarted", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsReQueued", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsFailed", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsSucceeded", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsCancelled", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsProvisioning", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsProvisioned", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsExecuting", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsExecuted", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsParsingOutputs", "Value": 0, "Unit": "Count", }, { "MetricName": "EvaluationsExecutingAlgorithm", "Value": 0, "Unit": "Count", }, ], }, { "Namespace": "testserver/workstations", "MetricData": [ { "MetricName": "SessionsQueued", "Value": 1, "Unit": "Count" }, { "MetricName": "SessionsStarted", "Value": 0, "Unit": "Count" }, { "MetricName": "SessionsRunning", "Value": 0, "Unit": "Count" }, { "MetricName": "SessionsFailed", "Value": 0, "Unit": "Count" }, { "MetricName": "SessionsStopped", "Value": 0, "Unit": "Count" }, ], }, { "Namespace": "testserver/cases", "MetricData": [ { "MetricName": "RawImageUploadSessionsQueued", "Value": 1, "Unit": "Count", }, { "MetricName": "RawImageUploadSessionsStarted", "Value": 0, "Unit": "Count", }, { "MetricName": "RawImageUploadSessionsReQueued", "Value": 1, "Unit": "Count", }, { "MetricName": "RawImageUploadSessionsFailed", "Value": 0, "Unit": "Count", }, { "MetricName": "RawImageUploadSessionsSucceeded", "Value": 0, "Unit": "Count", }, { "MetricName": "RawImageUploadSessionsCancelled", "Value": 0, "Unit": "Count", }, ], }, ]
def test_algorithm_title_on_job_serializer(rf): job = AlgorithmJobFactory() serializer = HyperlinkedJobSerializer(job, context={"request": rf.get("/foo")}) assert (serializer.data["algorithm_title"] == job.algorithm_image.algorithm.title)
def test_workstation_query(settings): image, overlay = ImageFactory(), ImageFactory() reader_study = ReaderStudyFactory( workstation_config=WorkstationConfigFactory()) algorithm_job = AlgorithmJobFactory() config = WorkstationConfigFactory() archive_item = ArchiveItemFactory() qs = workstation_query(image=image) assert "&" not in qs assert f"{settings.WORKSTATIONS_BASE_IMAGE_QUERY_PARAM}={image.pk}" in qs assert (f"{settings.WORKSTATIONS_OVERLAY_QUERY_PARAM}={overlay.pk}" not in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={config.pk}" not in qs qs = workstation_query(image=image, overlay=overlay) assert "&" in qs assert f"{settings.WORKSTATIONS_BASE_IMAGE_QUERY_PARAM}={image.pk}" in qs assert f"{settings.WORKSTATIONS_OVERLAY_QUERY_PARAM}={overlay.pk}" in qs assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={config.pk}" not in qs qs = workstation_query(image=image, config=config) assert "&" in qs assert f"{settings.WORKSTATIONS_BASE_IMAGE_QUERY_PARAM}={image.pk}" in qs assert (f"{settings.WORKSTATIONS_OVERLAY_QUERY_PARAM}={overlay.pk}" not in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={config.pk}" in qs qs = workstation_query(reader_study=reader_study) assert "&" in qs assert ( f"{settings.WORKSTATIONS_READY_STUDY_QUERY_PARAM}={reader_study.pk}" in qs) assert ( f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={reader_study.workstation_config.pk}" in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={config.pk}" not in qs qs = workstation_query(reader_study=reader_study, config=config) assert "&" in qs assert ( f"{settings.WORKSTATIONS_READY_STUDY_QUERY_PARAM}={reader_study.pk}" in qs) assert ( f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={reader_study.workstation_config.pk}" not in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={config.pk}" in qs reader_study.workstation_config = None qs = workstation_query(reader_study=reader_study) assert "&" not in qs assert ( f"{settings.WORKSTATIONS_READY_STUDY_QUERY_PARAM}={reader_study.pk}" in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}" not in qs qs = workstation_query(algorithm_job=algorithm_job) assert "&" not in qs assert ( f"{settings.WORKSTATIONS_ALGORITHM_JOB_QUERY_PARAM}={algorithm_job.pk}" in qs) qs = workstation_query(algorithm_job=algorithm_job, config=config) assert "&" in qs assert ( f"{settings.WORKSTATIONS_ALGORITHM_JOB_QUERY_PARAM}={algorithm_job.pk}" in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={config.pk}" in qs qs = workstation_query(archive_item=archive_item, config=config) assert "&" in qs assert ( f"{settings.WORKSTATIONS_ARCHIVE_ITEM_QUERY_PARAM}={archive_item.pk}" in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}={config.pk}" in qs qs = workstation_query(archive_item=archive_item) assert "&" not in qs assert ( f"{settings.WORKSTATIONS_ARCHIVE_ITEM_QUERY_PARAM}={archive_item.pk}" in qs) assert f"{settings.WORKSTATIONS_CONFIG_QUERY_PARAM}" not in qs