diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index 956805322..3d2b22ec2 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -296,6 +296,11 @@ Real Vuforia separates these by OAuth scope as well, which the mock does not mod Some Model Target Web API paths remain mock-only in ``tests/mock_vws/test_model_target_web_api.py::TestMockOnlyErrors``. Downloads of still-processing datasets are mock-only because exercising the path against real Vuforia would require creating a dataset on every test run; the mock drives the processing window deterministically. +A download request for a dataset which is not ready reports the dataset's +training status. The mock reports ``not-started`` for the whole processing +window, as real Vuforia does for a dataset which was just created, and +``failed`` for a dataset whose generation failed. The name which real Vuforia +reports for a failed dataset has not been observed. Advanced-dataset creation with more than 20 models is mock-only because the available test account lacks the advanced-dataset scope and real Vuforia rejects the request with a 403 before validating model counts. Cross-dataset-type access is mock-only for the same reason. State-Based Model Target creation and validation are also mock-only because the diff --git a/newsfragments/model-target-failed-download-status.change b/newsfragments/model-target-failed-download-status.change new file mode 100644 index 000000000..74be7dde5 --- /dev/null +++ b/newsfragments/model-target-failed-download-status.change @@ -0,0 +1 @@ +Report the ``failed`` training status when downloading a Model Target dataset whose generation failed, rather than the ``not-started`` status which a still-processing dataset reports. diff --git a/src/mock_vws/_model_target_web_api.py b/src/mock_vws/_model_target_web_api.py index f7e1b8d90..b097a6162 100644 --- a/src/mock_vws/_model_target_web_api.py +++ b/src/mock_vws/_model_target_web_api.py @@ -86,6 +86,18 @@ def remove_model_target_dataset(self, dataset_uuid: str) -> None: "simplify": frozenset({"always", "auto", "never"}), "trackingMode": frozenset({"car", "default", "scan"}), } +# The training status which the download route reports for a dataset which +# is not ready to download, keyed by the status which the status route +# reports. +# +# Real Vuforia reports ``not-started`` for a dataset which was created just +# before the download request, so the mock uses that name for the whole +# processing window. The name for a dataset whose generation failed has not +# been observed. +_TRAINING_STATUSES: dict[str, str] = { + "processing": "not-started", + "failed": "failed", +} # ``realisticAppearance`` is documented as an enumerated model field for # advanced datasets only. _ADVANCED_MODEL_ENUM_FIELD_VALUES: dict[str, frozenset[str]] = { @@ -1057,12 +1069,13 @@ def download_model_target_dataset( if dataset is None: return _unknown_dataset_response(dataset_uuid=dataset_uuid) if dataset.status != "done": + training_status = _TRAINING_STATUSES[dataset.status] return _error_response( status_code=HTTPStatus.UNPROCESSABLE_ENTITY, code="UNSUPPORTED_STATE", message=( f"Training status for dataset {dataset_uuid} is " - "not-started != done" + f"{training_status} != done" ), target=dataset_uuid, details=None, diff --git a/tests/mock_vws/test_model_target_web_api.py b/tests/mock_vws/test_model_target_web_api.py index eb13e1efc..c8357ebc0 100644 --- a/tests/mock_vws/test_model_target_web_api.py +++ b/tests/mock_vws/test_model_target_web_api.py @@ -15,7 +15,7 @@ from beartype import beartype from vws.response import Response -from mock_vws import MockVWS +from mock_vws import MockVWS, ModelTargetGenerationFailure from mock_vws.model_target import ModelTargetDataset, ModelTargetDatasetType from tests.mock_vws.fixtures.model_target_prepared_requests import ( MODEL_TARGET_DATASET_UUID, @@ -1501,6 +1501,50 @@ def test_processing_dataset_cannot_be_downloaded() -> None: ) assert error["target"] == dataset_uuid + @staticmethod + def test_failed_dataset_cannot_be_downloaded() -> None: + """A dataset which failed generation cannot be downloaded, and the + error reports the failed training status rather than the + ``not-started`` status which a still-processing dataset reports. + + Mock-only because a generation failure cannot be provoked on demand + against real Vuforia, so the training status name it reports for a + failed dataset has not been observed. + """ + failure = ModelTargetGenerationFailure(message="CAD model is invalid") + with MockVWS( + processing_time_seconds=0, + model_target_generation_failure=failure, + ): + create_response = requests.post( + url=f"{_VWS_HOST}/modeltargets/datasets", + headers={"Authorization": f"Bearer {_MOCK_BEARER_TOKEN}"}, + json=_UNAUTHENTICATED_DATASET_REQUEST, + timeout=30, + ) + dataset_uuid = create_response.json()["uuid"] + status_response = requests.get( + url=f"{_VWS_HOST}/modeltargets/datasets/{dataset_uuid}/status", + headers={"Authorization": f"Bearer {_MOCK_BEARER_TOKEN}"}, + timeout=30, + ) + response = requests.get( + url=( + f"{_VWS_HOST}/modeltargets/datasets/{dataset_uuid}/dataset" + ), + headers={"Authorization": f"Bearer {_MOCK_BEARER_TOKEN}"}, + timeout=30, + ) + + assert status_response.json()["status"] == "failed" + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + error = response.json()["error"] + assert error["code"] == "UNSUPPORTED_STATE" + assert error["message"] == ( + f"Training status for dataset {dataset_uuid} is failed != done" + ) + assert error["target"] == dataset_uuid + @staticmethod @pytest.mark.parametrize( argnames=("created_path", "other_path"),