From f88a4808fc7d0f114673a1e7db863fbe8872a232 Mon Sep 17 00:00:00 2001 From: Kort Travis Date: Thu, 14 Nov 2024 14:23:06 -0500 Subject: [PATCH] Code coverage. --- src/snapred/backend/error/ContinueWarning.py | 3 +- .../backend/error/RecoverableException.py | 3 +- .../recipe/EffectiveInstrumentRecipe.py | 10 ++- .../backend/service/ReductionService.py | 13 +-- .../backend/data/test_LocalDataService.py | 8 ++ .../recipe/test_EffectiveInstrumentRecipe.py | 50 ++++++++++- .../backend/service/test_ReductionService.py | 83 ++++++++++++++++++- 7 files changed, 149 insertions(+), 21 deletions(-) diff --git a/src/snapred/backend/error/ContinueWarning.py b/src/snapred/backend/error/ContinueWarning.py index 69869ae95..7554d1d48 100644 --- a/src/snapred/backend/error/ContinueWarning.py +++ b/src/snapred/backend/error/ContinueWarning.py @@ -30,8 +30,7 @@ def flags(self): return self.model.flags def __init__(self, message: str, flags: "Type" = 0): - ContinueWarning.Model.update_forward_refs() - ContinueWarning.Model.model_rebuild(force=True) + ContinueWarning.Model.model_rebuild(force=True) # replaces: `update_forward_refs` method self.model = ContinueWarning.Model(message=message, flags=flags) super().__init__(message) diff --git a/src/snapred/backend/error/RecoverableException.py b/src/snapred/backend/error/RecoverableException.py index ad96d254a..57007e7f9 100644 --- a/src/snapred/backend/error/RecoverableException.py +++ b/src/snapred/backend/error/RecoverableException.py @@ -37,8 +37,7 @@ def data(self): return self.model.data def __init__(self, message: str, flags: "Type" = 0, data: Optional[Any] = None): - RecoverableException.Model.update_forward_refs() - RecoverableException.Model.model_rebuild(force=True) + RecoverableException.Model.model_rebuild(force=True) # replaces: `update_forward_refs` method self.model = RecoverableException.Model(message=message, flags=flags, data=data) logger.error(f"{extractTrueStacktrace()}") super().__init__(message) diff --git a/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py b/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py index 16642d25c..5e2ed7418 100644 --- a/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py +++ b/src/snapred/backend/recipe/EffectiveInstrumentRecipe.py @@ -27,9 +27,16 @@ def queueAlgos(self): Queues up the processing algorithms for the recipe. Requires: unbagged groceries. """ + # `EditInstrumentGeometry` modifies in-place, so we need to clone if a distinct output workspace is required. + if self.outputWS != self.inputWS: + self.mantidSnapper.CloneWorkspace( + "Clone workspace for reduced instrument", + OutputWorkspace=self.outputWS, + InputWorkspace=self.inputWS + ) self.mantidSnapper.EditInstrumentGeometry( f"Editing instrument geometry for grouping '{self.unmaskedPixelGroup.focusGroup.name}'", - Workspace=self.inputWS, + Workspace=self.outputWS, # TODO: Mantid defect: allow SI units here! L2=np.rad2deg(self.unmaskedPixelGroup.L2), Polar=np.rad2deg(self.unmaskedPixelGroup.twoTheta), @@ -37,7 +44,6 @@ def queueAlgos(self): # InstrumentName=f"SNAP_{self.unmaskedPixelGroup.focusGroup.name}" ) - self.outputWS = self.inputWS def validateInputs(self, ingredients: Ingredients, groceries: Dict[str, WorkspaceName]): pass diff --git a/src/snapred/backend/service/ReductionService.py b/src/snapred/backend/service/ReductionService.py index 512e41929..79a81fd70 100644 --- a/src/snapred/backend/service/ReductionService.py +++ b/src/snapred/backend/service/ReductionService.py @@ -188,7 +188,6 @@ def reduction(self, request: ReductionRequest): groceries = self.fetchReductionGroceries(request) ingredients = self.prepReductionIngredients(request, groceries.get("combinedPixelMask")) - ingredients.artificialNormalizationIngredients = request.artificialNormalizationIngredients # attach the list of grouping workspaces to the grocery dictionary groceries["groupingWorkspaces"] = groupingResults["groupingWorkspaces"] @@ -314,13 +313,7 @@ def prepReductionIngredients(self, request: ReductionRequest, combinedPixelMask: Prepare the needed ingredients for calculating reduction. Requires: - - runNumber - - lite mode flag - - timestamp - - at least one focus group specified - - a smoothing parameter - - a calibrant sample path - - a peak threshold + - reduction request - an optional combined mask workspace :param request: a reduction request @@ -338,7 +331,9 @@ def prepReductionIngredients(self, request: ReductionRequest, combinedPixelMask: versions=request.versions, ) # TODO: Skip calibrant sample if there is no calibrant - return self.sousChef.prepReductionIngredients(farmFresh, combinedPixelMask) + ingredients = self.sousChef.prepReductionIngredients(farmFresh, combinedPixelMask) + ingredients.artificialNormalizationIngredients = request.artificialNormalizationIngredients + return ingredients @FromString def fetchReductionGroceries(self, request: ReductionRequest) -> Dict[str, Any]: diff --git a/tests/unit/backend/data/test_LocalDataService.py b/tests/unit/backend/data/test_LocalDataService.py index 06c71e6eb..b153c9e95 100644 --- a/tests/unit/backend/data/test_LocalDataService.py +++ b/tests/unit/backend/data/test_LocalDataService.py @@ -533,6 +533,14 @@ def test_stateExists(): assert localDataService.stateExists("12345") +def test_stateExists_not(): + # Test that the 'stateExists' method returns False when the state doesn't exist. + localDataService = LocalDataService() + localDataService.constructCalibrationStateRoot = mock.Mock(return_value=Path("a/non-existent/path")) + localDataService.generateStateId = mock.Mock(return_value=(ENDURING_STATE_ID, None)) + assert not localDataService.stateExists("12345") + + @mock.patch(ThisService + "GetIPTS") def test_calibrationFileExists(GetIPTS): # noqa ARG002 localDataService = LocalDataService() diff --git a/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py b/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py index b8537d83e..c4b1dba0f 100644 --- a/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py +++ b/tests/unit/backend/recipe/test_EffectiveInstrumentRecipe.py @@ -72,6 +72,24 @@ def test_unbagGroceries_output_default(self): assert recipe.outputWS == groceries["inputWorkspace"] def test_queueAlgos(self): + recipe = EffectiveInstrumentRecipe() + ingredients = self.ingredients + groceries = {"inputWorkspace": mock.Mock(), "outputWorkspace": mock.Mock()} + recipe.prep(ingredients, groceries) + recipe.queueAlgos() + + queuedAlgos = recipe.mantidSnapper._algorithmQueue + + cloneWorkspaceTuple = queuedAlgos[0] + assert cloneWorkspaceTuple[0] == "CloneWorkspace" + assert cloneWorkspaceTuple[2]["InputWorkspace"] == groceries["inputWorkspace"] + assert cloneWorkspaceTuple[2]["OutputWorkspace"] == groceries["outputWorkspace"] + + editInstrumentGeometryTuple = queuedAlgos[1] + assert editInstrumentGeometryTuple[0] == "EditInstrumentGeometry" + assert editInstrumentGeometryTuple[2]["Workspace"] == groceries["outputWorkspace"] + + def test_queueAlgos_default(self): recipe = EffectiveInstrumentRecipe() ingredients = self.ingredients groceries = {"inputWorkspace": mock.Mock()} @@ -79,12 +97,39 @@ def test_queueAlgos(self): recipe.queueAlgos() queuedAlgos = recipe.mantidSnapper._algorithmQueue - editInstrumentGeometryTuple = queuedAlgos[0] - + + editInstrumentGeometryTuple = queuedAlgos[0] assert editInstrumentGeometryTuple[0] == "EditInstrumentGeometry" assert editInstrumentGeometryTuple[2]["Workspace"] == groceries["inputWorkspace"] def test_cook(self): + utensils = Utensils() + mockSnapper = mock.Mock() + utensils.mantidSnapper = mockSnapper + recipe = EffectiveInstrumentRecipe(utensils=utensils) + ingredients = self.ingredients + groceries = {"inputWorkspace": mock.Mock(), "outputWorkspace": mock.Mock()} + + output = recipe.cook(ingredients, groceries) + + assert output == groceries["outputWorkspace"] + + assert mockSnapper.executeQueue.called + mockSnapper.CloneWorkspace.assert_called_once_with( + "Clone workspace for reduced instrument", + OutputWorkspace=groceries["outputWorkspace"], + InputWorkspace=groceries["inputWorkspace"] + ) + mockSnapper.EditInstrumentGeometry.assert_called_once_with( + f"Editing instrument geometry for grouping '{ingredients.unmaskedPixelGroup.focusGroup.name}'", + Workspace=groceries["outputWorkspace"], + L2=np.rad2deg(ingredients.unmaskedPixelGroup.L2), + Polar=np.rad2deg(ingredients.unmaskedPixelGroup.twoTheta), + Azimuthal=np.rad2deg(ingredients.unmaskedPixelGroup.azimuth), + InstrumentName=f"SNAP_{ingredients.unmaskedPixelGroup.focusGroup.name}" + ) + + def test_cook_default(self): utensils = Utensils() mockSnapper = mock.Mock() utensils.mantidSnapper = mockSnapper @@ -97,6 +142,7 @@ def test_cook(self): assert output == groceries["inputWorkspace"] assert mockSnapper.executeQueue.called + mockSnapper.CloneWorkspace.assert_not_called() mockSnapper.EditInstrumentGeometry.assert_called_once_with( f"Editing instrument geometry for grouping '{ingredients.unmaskedPixelGroup.focusGroup.name}'", Workspace=groceries["inputWorkspace"], diff --git a/tests/unit/backend/service/test_ReductionService.py b/tests/unit/backend/service/test_ReductionService.py index 23e5717a6..efe6b3c40 100644 --- a/tests/unit/backend/service/test_ReductionService.py +++ b/tests/unit/backend/service/test_ReductionService.py @@ -12,10 +12,12 @@ ) from snapred.backend.api.RequestScheduler import RequestScheduler from snapred.backend.dao import WorkspaceMetadata +from snapred.backend.dao.ingredients import ArtificialNormalizationIngredients from snapred.backend.dao.ingredients.ReductionIngredients import ReductionIngredients from snapred.backend.dao.reduction.ReductionRecord import ReductionRecord from snapred.backend.dao.request import ( CreateArtificialNormalizationRequest, + FarmFreshIngredients, ReductionExportRequest, ReductionRequest, ) @@ -74,7 +76,10 @@ def setUp(self): timestamp=self.instance.getUniqueTimestamp(), versions=(1, 2), pixelMasks=[], + keepUnfocused=True, + convertUnitsTo="TOF", focusGroups=[FocusGroup(name="apple", definition="path/to/grouping")], + artificialNormalizationIngredients=mock.Mock(spec=ArtificialNormalizationIngredients) ) def test_name(self): @@ -101,10 +106,22 @@ def test_fetchReductionGroupings(self): def test_prepReductionIngredients(self): # Call the method with the provided parameters - res = self.instance.prepReductionIngredients(self.request) - - assert ReductionIngredients.model_validate(res) - assert res == self.instance.sousChef.prepReductionIngredients(self.request) + result = self.instance.prepReductionIngredients(self.request) + + farmFresh = FarmFreshIngredients( + runNumber=self.request.runNumber, + useLiteMode=self.request.useLiteMode, + timestamp=self.request.timestamp, + focusGroups=self.request.focusGroups, + keepUnfocused=self.request.keepUnfocused, + convertUnitsTo=self.request.convertUnitsTo, + versions=self.request.versions, + ) + expected = self.instance.sousChef.prepReductionIngredients(farmFresh) + expected.artificialNormalizationIngredients = self.request.artificialNormalizationIngredients + + assert ReductionIngredients.model_validate(result) + assert result == expected def test_fetchReductionGroceries(self): self.instance.dataFactoryService.getThisOrLatestCalibrationVersion = mock.Mock(return_value=1) @@ -140,6 +157,64 @@ def test_reduction(self, mockReductionRecipe): mockReductionRecipe.return_value.cook.assert_called_once_with(ingredients, groceries) assert result.record.workspaceNames == mockReductionRecipe.return_value.cook.return_value["outputs"] + @mock.patch(thisService + "ReductionResponse") + @mock.patch(thisService + "ReductionRecipe") + def test_reduction_full_sequence(self, mockReductionRecipe, mockReductionResponse): + mockReductionRecipe.return_value = mock.Mock() + mockResult = { + "result": True, + "outputs": ["one", "two", "three"], + "unfocusedWS": mock.Mock() + } + mockReductionRecipe.return_value.cook = mock.Mock(return_value=mockResult) + self.instance.dataFactoryService.getThisOrLatestCalibrationVersion = mock.Mock(return_value=1) + self.instance.dataFactoryService.stateExists = mock.Mock(return_value=True) + self.instance.dataFactoryService.calibrationExists = mock.Mock(return_value=True) + self.instance.dataFactoryService.getThisOrLatestNormalizationVersion = mock.Mock(return_value=1) + self.instance.dataFactoryService.normalizationExists = mock.Mock(return_value=True) + self.instance._markWorkspaceMetadata = mock.Mock() + + self.instance.fetchReductionGroupings = mock.Mock( + return_value={ + "focusGroups": mock.Mock(), + "groupingWorkspaces": mock.Mock() + } + ) + self.instance.fetchReductionGroceries = mock.Mock( + return_value={ + "combinedPixelMask": mock.Mock() + } + ) + self.instance.prepReductionIngredients = mock.Mock( + return_value=mock.Mock() + ) + self.instance._createReductionRecord = mock.Mock( + return_value=mock.Mock() + ) + + request_ = self.request.model_copy() + result = self.instance.reduction(request_) + + self.instance.fetchReductionGroupings.assert_called_once_with(request_) + assert request_.focusGroups == self.instance.fetchReductionGroupings.return_value["focusGroups"] + self.instance.fetchReductionGroceries.assert_called_once_with(request_) + self.instance.prepReductionIngredients.assert_called_once_with( + request_, + self.instance.fetchReductionGroceries.return_value["combinedPixelMask"] + ) + assert self.instance.fetchReductionGroceries.return_value["groupingWorkspaces"] ==\ + self.instance.fetchReductionGroupings.return_value["groupingWorkspaces"] + + self.instance._createReductionRecord.assert_called_once_with( + request_, + self.instance.prepReductionIngredients.return_value, + mockReductionRecipe.return_value.cook.return_value["outputs"] + ) + mockReductionResponse.assert_called_once_with( + record=self.instance._createReductionRecord.return_value, + unfocusedData=mockReductionRecipe.return_value.cook.return_value["unfocusedWS"] + ) + def test_reduction_noState_withWritePerms(self): mockRequest = mock.Mock() self.instance.dataFactoryService.stateExists = mock.Mock(return_value=False)