From 567e3980575d841939ab8f3e176cc55c665157ed Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Thu, 12 Oct 2023 12:49:19 +0200 Subject: [PATCH 1/8] build: prepare hotfix v2.15.4 (unreleased) --- antarest/__init__.py | 4 ++-- docs/CHANGELOG.md | 4 ++++ setup.py | 2 +- sonar-project.properties | 2 +- webapp/package-lock.json | 2 +- webapp/package.json | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/antarest/__init__.py b/antarest/__init__.py index 8a0831bff2..a73192a58a 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -7,9 +7,9 @@ # Standard project metadata -__version__ = "2.15.3" +__version__ = "2.15.4" __author__ = "RTE, Antares Web Team" -__date__ = "2023-10-12" +__date__ = "unreleased" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1df1b0b313..c53c1a386f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,10 @@ Antares Web Changelog ===================== +v2.15.4 (unreleased) +-------------------- + + v2.15.3 (2023-10-12) -------------------- diff --git a/setup.py b/setup.py index 1fc961fedf..2e504d356a 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="AntaREST", - version="2.15.3", + version="2.15.4", description="Antares Server", long_description=Path("README.md").read_text(encoding="utf-8"), long_description_content_type="text/markdown", diff --git a/sonar-project.properties b/sonar-project.properties index 8742aa5a83..b3c424acb2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,5 +6,5 @@ sonar.exclusions=antarest/gui.py,antarest/main.py sonar.python.coverage.reportPaths=coverage.xml sonar.python.version=3.8 sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info -sonar.projectVersion=2.15.3 +sonar.projectVersion=2.15.4 sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,antarest/singleton_services.py,antarest/worker/archive_worker_service.py,webapp/**/* \ No newline at end of file diff --git a/webapp/package-lock.json b/webapp/package-lock.json index e179a3f1ca..dd4e93dc89 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1,6 +1,6 @@ { "name": "antares-web", - "version": "2.15.3", + "version": "2.15.4", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/webapp/package.json b/webapp/package.json index 77204beb44..e1157e2109 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "antares-web", - "version": "2.15.3", + "version": "2.15.4", "private": true, "engines": { "node": "18.16.1" From 8bd0bdf93c1a9ef0ee12570cb7d398ba7212b2fe Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Sun, 15 Oct 2023 17:59:37 +0200 Subject: [PATCH 2/8] test(commands): refactored study variant command unit tests, improved coverage, and fixed deprecated attribute usage --- antarest/study/business/district_manager.py | 2 +- antarest/study/service.py | 1 - .../business/command_extractor.py | 17 +- .../model/command/create_cluster.py | 31 ++- .../command/create_renewables_cluster.py | 47 ++-- docs/CHANGELOG.md | 5 + tests/integration/test_integration.py | 189 +--------------- .../variant_blueprint/test_variant_manager.py | 188 ++++++++++++++++ tests/variantstudy/conftest.py | 38 +++- .../model/command/test_create_area.py | 8 +- .../model/command/test_create_cluster.py | 205 +++++++++++++----- .../model/command/test_create_link.py | 16 +- .../command/test_create_renewables_cluster.py | 130 ++++++++--- .../test_manage_binding_constraints.py | 11 +- .../model/command/test_manage_district.py | 3 - .../model/command/test_replace_matrix.py | 27 ++- .../variantstudy/model/test_variant_model.py | 19 +- 17 files changed, 590 insertions(+), 347 deletions(-) create mode 100644 tests/integration/variant_blueprint/test_variant_manager.py diff --git a/antarest/study/business/district_manager.py b/antarest/study/business/district_manager.py index de95c39925..5a214c284c 100644 --- a/antarest/study/business/district_manager.py +++ b/antarest/study/business/district_manager.py @@ -100,7 +100,7 @@ def create_district( output=dto.output, comments=dto.comments, base_filter=DistrictBaseFilter.remove_all, - filter_items=areas, + filter_items=list(areas), command_context=self.storage_service.variant_study_service.command_factory.command_context, ) execute_or_add_commands(study, file_study, [command], self.storage_service) diff --git a/antarest/study/service.py b/antarest/study/service.py index 64d5c7d83c..5bf67200b7 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -1397,7 +1397,6 @@ def _create_edit_study_command( elif isinstance(tree_node, RawFileNode): if url.split("/")[-1] == "comments": return UpdateComments( - target=url, comments=data, command_context=context, ) diff --git a/antarest/study/storage/variantstudy/business/command_extractor.py b/antarest/study/storage/variantstudy/business/command_extractor.py index ab1f8b6d42..83b88de09a 100644 --- a/antarest/study/storage/variantstudy/business/command_extractor.py +++ b/antarest/study/storage/variantstudy/business/command_extractor.py @@ -313,14 +313,12 @@ def extract_district(self, study: FileStudy, district_id: str) -> List[ICommand] study_config = study.config study_tree = study.tree district_config = study_config.sets[district_id] + base_filter = DistrictBaseFilter.add_all if district_config.inverted_set else DistrictBaseFilter.remove_all district_fetched_config = study_tree.get(["input", "areas", "sets", district_id]) study_commands.append( CreateDistrict( name=district_config.name, - metadata={}, - base_filter=DistrictBaseFilter.add_all - if district_config.inverted_set - else DistrictBaseFilter.remove_all, + base_filter=base_filter, filter_items=district_config.areas or [], output=district_config.output, comments=district_fetched_config.get("comments", None), @@ -331,9 +329,11 @@ def extract_district(self, study: FileStudy, district_id: str) -> List[ICommand] def extract_comments(self, study: FileStudy) -> List[ICommand]: study_tree = study.tree + content = cast(bytes, study_tree.get(["settings", "comments"])) + comments = content.decode("utf-8") return [ UpdateComments( - comments=study_tree.get(["settings", "comments"]), + comments=comments, command_context=self.command_context, ) ] @@ -392,8 +392,8 @@ def generate_update_comments( self, study_tree: FileStudyTree, ) -> ICommand: - url = ["settings", "comments"] - comments = study_tree.get(url) + content = cast(bytes, study_tree.get(["settings", "comments"])) + comments = content.decode("utf-8") return UpdateComments( comments=comments, command_context=self.command_context, @@ -444,8 +444,7 @@ def generate_update_district( district_config = study_config.sets[district_id] district_fetched_config = study_tree.get(["input", "areas", "sets", district_id]) return UpdateDistrict( - name=district_config.name, - metadata={}, + id=district_config.name, base_filter=DistrictBaseFilter.add_all if district_config.inverted_set else DistrictBaseFilter.remove_all, filter_items=district_config.areas or [], output=district_config.output, diff --git a/antarest/study/storage/variantstudy/model/command/create_cluster.py b/antarest/study/storage/variantstudy/model/command/create_cluster.py index 0c0b58ace3..8097cad542 100644 --- a/antarest/study/storage/variantstudy/model/command/create_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_cluster.py @@ -18,20 +18,30 @@ class CreateCluster(ICommand): + """ + Command used to create a thermal cluster in an area. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.CREATE_THERMAL_CLUSTER + version = 1 + + # Command parameters + # ================== + area_id: str cluster_name: str parameters: Dict[str, str] prepro: Optional[Union[List[List[MatrixData]], str]] = None modulation: Optional[Union[List[List[MatrixData]], str]] = None - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.CREATE_THERMAL_CLUSTER, version=1, **data) - @validator("cluster_name") def validate_cluster_name(cls, val: str) -> str: valid_name = transform_name_to_id(val, lower=False) if valid_name != val: - raise ValueError("Area name must only contains [a-zA-Z0-9],&,-,_,(,) characters") + raise ValueError("Cluster name must only contains [a-zA-Z0-9],&,-,_,(,) characters") return val @validator("prepro", always=True) @@ -57,13 +67,14 @@ def validate_modulation( return validate_matrix(v, values) def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: + # Search the Area in the configuration if self.area_id not in study_data.areas: return ( CommandOutput( status=False, - message=f"Area '{self.area_id}' does not exist", + message=f"Area '{self.area_id}' does not exist in the study configuration.", ), - dict(), + {}, ) cluster_id = transform_name_to_id(self.cluster_name) for cluster in study_data.areas[self.area_id].thermals: @@ -71,15 +82,15 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=False, - message=f"Cluster '{self.cluster_name}' already exist", + message=f"Thermal cluster '{cluster_id}' already exists in the area '{self.area_id}'.", ), - dict(), + {}, ) study_data.areas[self.area_id].thermals.append(Cluster(id=cluster_id, name=self.cluster_name)) return ( CommandOutput( status=True, - message=f"Cluster '{self.cluster_name}' added to area '{self.area_id}'", + message=f"Thermal cluster '{cluster_id}' added to area '{self.area_id}'.", ), {"cluster_id": cluster_id}, ) @@ -123,7 +134,7 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: def to_dto(self) -> CommandDTO: return CommandDTO( - action=CommandName.CREATE_THERMAL_CLUSTER.value, + action=self.command_name.value, args={ "area_id": self.area_id, "cluster_name": self.cluster_name, diff --git a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py index 66964dd68d..59502439e6 100644 --- a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py @@ -16,17 +16,23 @@ class CreateRenewablesCluster(ICommand): + """ + Command used to create a renewable cluster in an area. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.CREATE_RENEWABLES_CLUSTER + version = 1 + + # Command parameters + # ================== + area_id: str cluster_name: str parameters: Dict[str, str] - def __init__(self, **data: Any) -> None: - super().__init__( - command_name=CommandName.CREATE_RENEWABLES_CLUSTER, - version=1, - **data, - ) - @validator("cluster_name") def validate_cluster_name(cls, val: str) -> str: valid_name = transform_name_to_id(val, lower=False) @@ -43,20 +49,33 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, ) return CommandOutput(status=False, message=message), {} + # Search the Area in the configuration if self.area_id not in study_data.areas: - message = f"Area '{self.area_id}' does not exist" - return CommandOutput(status=False, message=message), {} + return ( + CommandOutput( + status=False, + message=f"Area '{self.area_id}' does not exist in the study configuration.", + ), + {}, + ) cluster_id = transform_name_to_id(self.cluster_name) for cluster in study_data.areas[self.area_id].renewables: if cluster.id == cluster_id: - message = f"Renewable cluster '{self.cluster_name}' already exist" - return CommandOutput(status=False, message=message), {} + return ( + CommandOutput( + status=False, + message=f"Renewable cluster '{cluster_id}' already exists in the area '{self.area_id}'.", + ), + {}, + ) study_data.areas[self.area_id].renewables.append(Cluster(id=cluster_id, name=self.cluster_name)) - message = f"Renewable cluster '{self.cluster_name}' added to area '{self.area_id}'" return ( - CommandOutput(status=True, message=message), + CommandOutput( + status=True, + message=f"Renewable cluster '{cluster_id}' added to area '{self.area_id}'.", + ), {"cluster_id": cluster_id}, ) @@ -94,7 +113,7 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: def to_dto(self) -> CommandDTO: return CommandDTO( - action=CommandName.CREATE_RENEWABLES_CLUSTER.value, + action=self.command_name.value, args={ "area_id": self.area_id, "cluster_name": self.cluster_name, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c53c1a386f..8e425ac15a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,11 @@ Antares Web Changelog v2.15.4 (unreleased) -------------------- +### Tests + +* **commands:** refactored study variant command unit tests, improved coverage, and fixed deprecated attribute usage [`5244442`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/52444422ed5e3ef116f514e02b8454b1ae203104) + + v2.15.3 (2023-10-12) -------------------- diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index df4249ca47..dd1d2b3179 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -6,7 +6,6 @@ from starlette.testclient import TestClient from antarest.core.model import PublicMode -from antarest.core.tasks.model import TaskDTO, TaskStatus from antarest.study.business.adequacy_patch_management import PriceTakingOrder from antarest.study.business.area_management import AreaType, LayerInfoDTO from antarest.study.business.areas.properties_management import AdequacyPatchMode @@ -104,17 +103,19 @@ def test_main(client: TestClient, admin_access_token: str, study_id: str) -> Non assert res.json()["description"] == "Not a year by year simulation" # Set new comments - client.put( + res = client.put( f"/v1/studies/{study_id}/comments", headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, json={"comments": comments}, ) + assert res.status_code == 204, res.json() # Get comments res = client.get( f"/v1/studies/{study_id}/comments", headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, ) + assert res.status_code == 200, res.json() assert res.json() == comments # study synthesis @@ -122,7 +123,7 @@ def test_main(client: TestClient, admin_access_token: str, study_id: str) -> Non f"/v1/studies/{study_id}/synthesis", headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, ) - assert res.status_code == 200 + assert res.status_code == 200, res.json() # playlist res = client.post( @@ -1977,188 +1978,6 @@ def test_archive(client: TestClient, admin_access_token: str, study_id: str, tmp assert not (tmp_path / "archive_dir" / f"{study_id}.zip").exists() -def test_variant_manager(client: TestClient, admin_access_token: str, study_id: str) -> None: - admin_headers = {"Authorization": f"Bearer {admin_access_token}"} - - base_study_res = client.post("/v1/studies?name=foo", headers=admin_headers) - - base_study_id = base_study_res.json() - - res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers) - variant_id = res.json() - - client.post(f"/v1/launcher/run/{variant_id}", headers=admin_headers) - - res = client.get(f"v1/studies/{variant_id}/synthesis", headers=admin_headers) - - assert variant_id in res.json()["output_path"] - - client.post(f"/v1/studies/{variant_id}/variants?name=bar", headers=admin_headers) - client.post(f"/v1/studies/{variant_id}/variants?name=baz", headers=admin_headers) - res = client.get(f"/v1/studies/{base_study_id}/variants", headers=admin_headers) - children = res.json() - assert children["node"]["name"] == "foo" - assert len(children["children"]) == 1 - assert children["children"][0]["node"]["name"] == "foo" - assert len(children["children"][0]["children"]) == 2 - assert children["children"][0]["children"][0]["node"]["name"] == "bar" - assert children["children"][0]["children"][1]["node"]["name"] == "baz" - - # George creates a base study - # He creates a variant from this study : assert that no command is created - # The admin creates a variant from the same base study : assert that its author is admin (created via a command) - - client.post( - "/v1/users", - headers=admin_headers, - json={"name": "George", "password": "mypass"}, - ) - res = client.post("/v1/login", json={"username": "George", "password": "mypass"}) - george_credentials = res.json() - base_study_res = client.post( - "/v1/studies?name=foo", - headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, - ) - - base_study_id = base_study_res.json() - res = client.post( - f"/v1/studies/{base_study_id}/variants?name=foo_2", - headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, - ) - variant_id = res.json() - res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) - assert len(res.json()) == 0 - res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers) - variant_id = res.json() - res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) - assert len(res.json()) == 1 - command = res.json()[0] - assert command["action"] == "update_config" - assert command["args"]["target"] == "study" - assert command["args"]["data"]["antares"]["author"] == "admin" - - res = client.get(f"/v1/studies/{variant_id}/parents", headers=admin_headers) - assert len(res.json()) == 1 - assert res.json()[0]["id"] == base_study_id - assert res.status_code == 200 - - res = client.post( - f"/v1/studies/{variant_id}/commands", - json=[ - { - "action": "create_area", - "args": {"area_name": "testZone", "metadata": {}}, - } - ], - headers=admin_headers, - ) - assert res.status_code == 200 - assert len(res.json()) == 1 - - res = client.post( - f"/v1/studies/{variant_id}/commands", - json=[ - { - "action": "create_area", - "args": {"area_name": "testZone2", "metadata": {}}, - } - ], - headers=admin_headers, - ) - assert res.status_code == 200 - - res = client.post( - f"/v1/studies/{variant_id}/command", - json={ - "action": "create_area", - "args": {"area_name": "testZone3", "metadata": {}}, - }, - headers=admin_headers, - ) - assert res.status_code == 200 - - command_id = res.json() - res = client.put( - f"/v1/studies/{variant_id}/commands/{command_id}", - json={ - "action": "create_area", - "args": {"area_name": "testZone4", "metadata": {}}, - }, - headers=admin_headers, - ) - assert res.status_code == 200 - - res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) - assert len(res.json()) == 4 - assert res.status_code == 200 - - res = client.put( - f"/v1/studies/{variant_id}/commands", - json=[ - { - "action": "create_area", - "args": {"area_name": "testZoneReplace1", "metadata": {}}, - }, - { - "action": "create_area", - "args": {"area_name": "testZoneReplace1", "metadata": {}}, - }, - ], - headers=admin_headers, - ) - assert res.status_code == 200 - - res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) - assert len(res.json()) == 2 - assert res.status_code == 200 - - command_id = res.json()[1]["id"] - - res = client.put(f"/v1/studies/{variant_id}/commands/{command_id}/move?index=0", headers=admin_headers) - assert res.status_code == 200 - - res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) - assert res.json()[0]["id"] == command_id - assert res.status_code == 200 - - res = client.delete(f"/v1/studies/{variant_id}/commands/{command_id}", headers=admin_headers) - - assert res.status_code == 200 - - res = client.put(f"/v1/studies/{variant_id}/generate", headers=admin_headers) - assert res.status_code == 200 - - res = client.get(f"/v1/tasks/{res.json()}?wait_for_completion=true", headers=admin_headers) - assert res.status_code == 200 - task_result = TaskDTO.parse_obj(res.json()) - assert task_result.status == TaskStatus.COMPLETED - assert task_result.result.success # type: ignore - - res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers) - assert res.status_code == 200 - - res = client.post(f"/v1/studies/{variant_id}/freeze?name=bar", headers=admin_headers) - assert res.status_code == 500 - - new_study_id = "newid" - - res = client.get(f"/v1/studies/{new_study_id}", headers=admin_headers) - assert res.status_code == 404 - - res = client.delete(f"/v1/studies/{variant_id}/commands", headers=admin_headers) - assert res.status_code == 200 - - res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) - assert res.status_code == 200 - assert len(res.json()) == 0 - - res = client.delete(f"/v1/studies/{variant_id}", headers=admin_headers) - assert res.status_code == 200 - - res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers) - assert res.status_code == 404 - - def test_maintenance(client: TestClient, admin_access_token: str, study_id: str) -> None: admin_headers = {"Authorization": f"Bearer {admin_access_token}"} diff --git a/tests/integration/variant_blueprint/test_variant_manager.py b/tests/integration/variant_blueprint/test_variant_manager.py new file mode 100644 index 0000000000..5af256dbbe --- /dev/null +++ b/tests/integration/variant_blueprint/test_variant_manager.py @@ -0,0 +1,188 @@ +import logging + +from starlette.testclient import TestClient + +from antarest.core.tasks.model import TaskDTO, TaskStatus + + +def test_variant_manager(client: TestClient, admin_access_token: str, study_id: str, caplog) -> None: + with caplog.at_level(level=logging.WARNING): + admin_headers = {"Authorization": f"Bearer {admin_access_token}"} + + base_study_res = client.post("/v1/studies?name=foo", headers=admin_headers) + + base_study_id = base_study_res.json() + + res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers) + variant_id = res.json() + + client.post(f"/v1/launcher/run/{variant_id}", headers=admin_headers) + + res = client.get(f"v1/studies/{variant_id}/synthesis", headers=admin_headers) + + assert variant_id in res.json()["output_path"] + + client.post(f"/v1/studies/{variant_id}/variants?name=bar", headers=admin_headers) + client.post(f"/v1/studies/{variant_id}/variants?name=baz", headers=admin_headers) + res = client.get(f"/v1/studies/{base_study_id}/variants", headers=admin_headers) + children = res.json() + assert children["node"]["name"] == "foo" + assert len(children["children"]) == 1 + assert children["children"][0]["node"]["name"] == "foo" + assert len(children["children"][0]["children"]) == 2 + assert children["children"][0]["children"][0]["node"]["name"] == "bar" + assert children["children"][0]["children"][1]["node"]["name"] == "baz" + + # George creates a base study + # He creates a variant from this study : assert that no command is created + # The admin creates a variant from the same base study : assert that its author is admin (created via a command) + + client.post( + "/v1/users", + headers=admin_headers, + json={"name": "George", "password": "mypass"}, + ) + res = client.post("/v1/login", json={"username": "George", "password": "mypass"}) + george_credentials = res.json() + base_study_res = client.post( + "/v1/studies?name=foo", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + ) + + base_study_id = base_study_res.json() + res = client.post( + f"/v1/studies/{base_study_id}/variants?name=foo_2", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + ) + variant_id = res.json() + res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) + assert len(res.json()) == 0 + res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers) + variant_id = res.json() + res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) + assert len(res.json()) == 1 + command = res.json()[0] + assert command["action"] == "update_config" + assert command["args"]["target"] == "study" + assert command["args"]["data"]["antares"]["author"] == "admin" + + res = client.get(f"/v1/studies/{variant_id}/parents", headers=admin_headers) + assert len(res.json()) == 1 + assert res.json()[0]["id"] == base_study_id + assert res.status_code == 200 + + res = client.post( + f"/v1/studies/{variant_id}/commands", + json=[ + { + "action": "create_area", + "args": {"area_name": "testZone", "metadata": {}}, + } + ], + headers=admin_headers, + ) + assert res.status_code == 200 + assert len(res.json()) == 1 + + res = client.post( + f"/v1/studies/{variant_id}/commands", + json=[ + { + "action": "create_area", + "args": {"area_name": "testZone2", "metadata": {}}, + } + ], + headers=admin_headers, + ) + assert res.status_code == 200 + + res = client.post( + f"/v1/studies/{variant_id}/command", + json={ + "action": "create_area", + "args": {"area_name": "testZone3", "metadata": {}}, + }, + headers=admin_headers, + ) + assert res.status_code == 200 + + command_id = res.json() + res = client.put( + f"/v1/studies/{variant_id}/commands/{command_id}", + json={ + "action": "create_area", + "args": {"area_name": "testZone4", "metadata": {}}, + }, + headers=admin_headers, + ) + assert res.status_code == 200 + + res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) + assert len(res.json()) == 4 + assert res.status_code == 200 + + res = client.put( + f"/v1/studies/{variant_id}/commands", + json=[ + { + "action": "create_area", + "args": {"area_name": "testZoneReplace1", "metadata": {}}, + }, + { + "action": "create_area", + "args": {"area_name": "testZoneReplace1", "metadata": {}}, + }, + ], + headers=admin_headers, + ) + assert res.status_code == 200 + + res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) + assert len(res.json()) == 2 + assert res.status_code == 200 + + command_id = res.json()[1]["id"] + + res = client.put(f"/v1/studies/{variant_id}/commands/{command_id}/move?index=0", headers=admin_headers) + assert res.status_code == 200 + + res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) + assert res.json()[0]["id"] == command_id + assert res.status_code == 200 + + res = client.delete(f"/v1/studies/{variant_id}/commands/{command_id}", headers=admin_headers) + + assert res.status_code == 200 + + res = client.put(f"/v1/studies/{variant_id}/generate", headers=admin_headers) + assert res.status_code == 200 + + res = client.get(f"/v1/tasks/{res.json()}?wait_for_completion=true", headers=admin_headers) + assert res.status_code == 200 + task_result = TaskDTO.parse_obj(res.json()) + assert task_result.status == TaskStatus.COMPLETED + assert task_result.result.success # type: ignore + + res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers) + assert res.status_code == 200 + + res = client.post(f"/v1/studies/{variant_id}/freeze?name=bar", headers=admin_headers) + assert res.status_code == 500 + + new_study_id = "newid" + + res = client.get(f"/v1/studies/{new_study_id}", headers=admin_headers) + assert res.status_code == 404 + + res = client.delete(f"/v1/studies/{variant_id}/commands", headers=admin_headers) + assert res.status_code == 200 + + res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers) + assert res.status_code == 200 + assert len(res.json()) == 0 + + res = client.delete(f"/v1/studies/{variant_id}", headers=admin_headers) + assert res.status_code == 200 + + res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers) + assert res.status_code == 404 diff --git a/tests/variantstudy/conftest.py b/tests/variantstudy/conftest.py index 526d5b53ba..9db21ab220 100644 --- a/tests/variantstudy/conftest.py +++ b/tests/variantstudy/conftest.py @@ -1,11 +1,14 @@ import hashlib +import typing as t import zipfile from pathlib import Path from unittest.mock import Mock import numpy as np +import numpy.typing as npt import pytest +from antarest.matrixstore.model import MatrixDTO from antarest.matrixstore.service import MatrixService from antarest.matrixstore.uri_resolver_service import UriResolverService from antarest.study.repository import StudyMetadataRepository @@ -29,17 +32,50 @@ def matrix_service_fixture() -> MatrixService: An instance of the `SimpleMatrixService` class representing the matrix service. """ - def create(data): + matrix_map: t.Dict[str, npt.NDArray[np.float64]] = {} + + def create(data: t.Union[t.List[t.List[float]], npt.NDArray[np.float64]]) -> str: """ This function calculates a unique ID for each matrix, without storing any data in the file system or the database. """ matrix = data if isinstance(data, np.ndarray) else np.array(data, dtype=np.float64) matrix_hash = hashlib.sha256(matrix.data).hexdigest() + matrix_map[matrix_hash] = matrix return matrix_hash + def get(matrix_id: str) -> MatrixDTO: + """ + This function retrieves the matrix from the map. + """ + data = matrix_map[matrix_id] + return MatrixDTO( + id=matrix_id, + width=data.shape[1], + height=data.shape[0], + index=[str(i) for i in range(data.shape[0])], + columns=[str(i) for i in range(data.shape[1])], + data=data.tolist(), + ) + + def exists(matrix_id: str) -> bool: + """ + This function checks if the matrix exists in the map. + """ + return matrix_id in matrix_map + + def delete(matrix_id: str) -> None: + """ + This function deletes the matrix from the map. + """ + del matrix_map[matrix_id] + matrix_service = Mock(spec=MatrixService) matrix_service.create.side_effect = create + matrix_service.get.side_effect = get + matrix_service.exists.side_effect = exists + matrix_service.delete.side_effect = delete + return matrix_service diff --git a/tests/variantstudy/model/command/test_create_area.py b/tests/variantstudy/model/command/test_create_area.py index 8c23a95f63..62e01aeba4 100644 --- a/tests/variantstudy/model/command/test_create_area.py +++ b/tests/variantstudy/model/command/test_create_area.py @@ -141,13 +141,7 @@ def test_apply( assert output.status - create_area_command: ICommand = CreateArea.parse_obj( - { - "area_name": area_name, - "metadata": {}, - "command_context": command_context, - } - ) + create_area_command: ICommand = CreateArea(area_name=area_name, command_context=command_context, metadata={}) output = create_area_command.apply(study_data=empty_study) assert not output.status diff --git a/tests/variantstudy/model/command/test_create_cluster.py b/tests/variantstudy/model/command/test_create_cluster.py index 06d368a1e0..4fdeb3c488 100644 --- a/tests/variantstudy/model/command/test_create_cluster.py +++ b/tests/variantstudy/model/command/test_create_cluster.py @@ -1,8 +1,14 @@ import configparser +import re + +import numpy as np +import pytest +from pydantic import ValidationError from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter +from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_cluster import CreateCluster from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster @@ -12,22 +18,52 @@ class TestCreateCluster: - def test_validation(self, empty_study: FileStudy): - pass + def test_init(self, command_context: CommandContext): + prepro = np.random.rand(365, 6).tolist() + modulation = np.random.rand(8760, 4).tolist() + cl = CreateCluster( + area_id="foo", + cluster_name="Cluster1", + parameters={"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400}, + command_context=command_context, + prepro=prepro, + modulation=modulation, + ) + + # Check the command metadata + assert cl.command_name == CommandName.CREATE_THERMAL_CLUSTER + assert cl.version == 1 + assert cl.command_context is command_context + + # Check the command data + prepro_id = command_context.matrix_service.create(prepro) + modulation_id = command_context.matrix_service.create(modulation) + assert cl.area_id == "foo" + assert cl.cluster_name == "Cluster1" + assert cl.parameters == {"group": "Nuclear", "nominalcapacity": "2400", "unitcount": "2"} + assert cl.prepro == f"matrix://{prepro_id}" + assert cl.modulation == f"matrix://{modulation_id}" + + def test_validate_cluster_name(self, command_context: CommandContext): + with pytest.raises(ValidationError, match="cluster_name"): + CreateCluster(area_id="fr", cluster_name="%", command_context=command_context, parameters={}) + + def test_validate_prepro(self, command_context: CommandContext): + cl = CreateCluster(area_id="fr", cluster_name="C1", command_context=command_context, parameters={}) + assert cl.prepro == command_context.generator_matrix_constants.get_thermal_prepro_data() + + def test_validate_modulation(self, command_context: CommandContext): + cl = CreateCluster(area_id="fr", cluster_name="C1", command_context=command_context, parameters={}) + assert cl.modulation == command_context.generator_matrix_constants.get_thermal_prepro_modulation() def test_apply(self, empty_study: FileStudy, command_context: CommandContext): study_path = empty_study.config.study_path - area_name = "Area" + area_name = "DE" area_id = transform_name_to_id(area_name, lower=True) - cluster_name = "cluster_name" + cluster_name = "Cluster-1" cluster_id = transform_name_to_id(cluster_name, lower=True) - CreateArea.parse_obj( - { - "area_name": area_name, - "command_context": command_context, - } - ).apply(empty_study) + CreateArea(area_name=area_name, command_context=command_context).apply(empty_study) parameters = { "group": "Other", @@ -37,19 +73,24 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): "market-bid-cost": "30", } - command = CreateCluster.parse_obj( - { - "area_id": area_id, - "cluster_name": cluster_name, - "parameters": parameters, - "prepro": [[0]], - "modulation": [[0]], - "command_context": command_context, - } + prepro = np.random.rand(365, 6).tolist() + modulation = np.random.rand(8760, 4).tolist() + command = CreateCluster( + area_id=area_id, + cluster_name=cluster_name, + parameters=parameters, + prepro=prepro, + modulation=modulation, + command_context=command_context, ) output = command.apply(empty_study) - assert output.status + assert output.status is True + assert re.match( + r"Thermal cluster 'cluster-1' added to area 'de'", + output.message, + flags=re.IGNORECASE, + ) clusters = configparser.ConfigParser() clusters.read(study_path / "input" / "thermal" / "clusters" / area_id / "list.ini") @@ -63,64 +104,106 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): assert (study_path / "input" / "thermal" / "prepro" / area_id / cluster_id / "data.txt.link").exists() assert (study_path / "input" / "thermal" / "prepro" / area_id / cluster_id / "modulation.txt.link").exists() - output = CreateCluster.parse_obj( - { - "area_id": area_id, - "cluster_name": cluster_name, - "parameters": parameters, - "prepro": [[0]], - "modulation": [[0]], - "command_context": command_context, - } + output = CreateCluster( + area_id=area_id, + cluster_name=cluster_name, + parameters=parameters, + prepro=prepro, + modulation=modulation, + command_context=command_context, ).apply(empty_study) - assert not output.status - - output = CreateCluster.parse_obj( - { - "area_id": "non_existent_area", - "cluster_name": cluster_name, - "parameters": parameters, - "prepro": [[0]], - "modulation": [[0]], - "command_context": command_context, - } + assert output.status is False + assert re.match( + r"Thermal cluster 'cluster-1' already exists in the area 'de'", + output.message, + flags=re.IGNORECASE, + ) + + output = CreateCluster( + area_id="non_existent_area", + cluster_name=cluster_name, + parameters=parameters, + prepro=prepro, + modulation=modulation, + command_context=command_context, ).apply(empty_study) - assert not output.status + assert output.status is False + assert re.match( + r"Area 'non_existent_area' does not exist", + output.message, + flags=re.IGNORECASE, + ) + + def test_to_dto(self, command_context: CommandContext): + prepro = np.random.rand(365, 6).tolist() + modulation = np.random.rand(8760, 4).tolist() + command = CreateCluster( + area_id="foo", + cluster_name="Cluster1", + parameters={"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400}, + command_context=command_context, + prepro=prepro, + modulation=modulation, + ) + prepro_id = command_context.matrix_service.create(prepro) + modulation_id = command_context.matrix_service.create(modulation) + dto = command.to_dto() + assert dto.dict() == { + "action": "create_cluster", + "args": { + "area_id": "foo", + "cluster_name": "Cluster1", + "parameters": {"group": "Nuclear", "nominalcapacity": "2400", "unitcount": "2"}, + "prepro": prepro_id, + "modulation": modulation_id, + }, + "id": None, + "version": 1, + } def test_match(command_context: CommandContext): + prepro = np.random.rand(365, 6).tolist() + modulation = np.random.rand(8760, 4).tolist() base = CreateCluster( area_id="foo", cluster_name="foo", parameters={}, - prepro=[[0]], - modulation=[[0]], + prepro=prepro, + modulation=modulation, command_context=command_context, ) other_match = CreateCluster( area_id="foo", cluster_name="foo", parameters={}, - prepro=[[0]], - modulation=[[0]], + prepro=prepro, + modulation=modulation, command_context=command_context, ) other_not_match = CreateCluster( area_id="foo", cluster_name="bar", parameters={}, - prepro=[[0]], - modulation=[[0]], + prepro=prepro, + modulation=modulation, command_context=command_context, ) other_other = RemoveCluster(area_id="id", cluster_id="id", command_context=command_context) assert base.match(other_match) assert not base.match(other_not_match) assert not base.match(other_other) + + assert base.match(other_match, equal=True) + assert not base.match(other_not_match, equal=True) + assert not base.match(other_other, equal=True) + assert base.match_signature() == "create_cluster%foo%foo" + # check the matrices links - matrix_id = command_context.matrix_service.create([[0]]) - assert base.get_inner_matrices() == [matrix_id, matrix_id] + prepro_id = command_context.matrix_service.create(prepro) + modulation_id = command_context.matrix_service.create(modulation) + assert base.get_inner_matrices() == [prepro_id, modulation_id] def test_revert(command_context: CommandContext): @@ -128,8 +211,6 @@ def test_revert(command_context: CommandContext): area_id="foo", cluster_name="foo", parameters={}, - prepro=[[0]], - modulation=[[0]], command_context=command_context, ) assert CommandReverter().revert(base, [], None) == [ @@ -142,36 +223,42 @@ def test_revert(command_context: CommandContext): def test_create_diff(command_context: CommandContext): + prepro_a = np.random.rand(365, 6).tolist() + modulation_a = np.random.rand(8760, 4).tolist() base = CreateCluster( area_id="foo", cluster_name="foo", parameters={}, - prepro="a", - modulation="b", + prepro=prepro_a, + modulation=modulation_a, command_context=command_context, ) + + prepro_b = np.random.rand(365, 6).tolist() + modulation_b = np.random.rand(8760, 4).tolist() other_match = CreateCluster( area_id="foo", cluster_name="foo", - parameters={"a": "b"}, - prepro="c", - modulation="d", + parameters={"nominalcapacity": "2400"}, + prepro=prepro_b, + modulation=modulation_b, command_context=command_context, ) + assert base.create_diff(other_match) == [ ReplaceMatrix( target=f"input/thermal/prepro/foo/foo/data", - matrix="c", + matrix=prepro_b, command_context=command_context, ), ReplaceMatrix( target=f"input/thermal/prepro/foo/foo/modulation", - matrix="d", + matrix=modulation_b, command_context=command_context, ), UpdateConfig( target=f"input/thermal/clusters/foo/list/foo", - data={"a": "b"}, + data={"nominalcapacity": "2400"}, command_context=command_context, ), ] diff --git a/tests/variantstudy/model/command/test_create_link.py b/tests/variantstudy/model/command/test_create_link.py index fdafb2efa6..133d8d2b7d 100644 --- a/tests/variantstudy/model/command/test_create_link.py +++ b/tests/variantstudy/model/command/test_create_link.py @@ -1,5 +1,6 @@ import configparser +import numpy as np import pytest from pydantic import ValidationError @@ -234,14 +235,23 @@ def test_revert(command_context: CommandContext): def test_create_diff(command_context: CommandContext): - base = CreateLink(area1="foo", area2="bar", series="a", command_context=command_context) + series_a = np.random.rand(8760, 8).tolist() + base = CreateLink( + area1="foo", + area2="bar", + series=series_a, + command_context=command_context, + ) + + series_b = np.random.rand(8760, 8).tolist() other_match = CreateLink( area1="foo", area2="bar", parameters={"hurdles-cost": "true"}, - series="b", + series=series_b, command_context=command_context, ) + assert base.create_diff(other_match) == [ UpdateConfig( target=f"input/links/bar/properties/foo", @@ -250,7 +260,7 @@ def test_create_diff(command_context: CommandContext): ), ReplaceMatrix( target=f"@links_series/bar/foo", - matrix="b", + matrix=series_b, command_context=command_context, ), ] diff --git a/tests/variantstudy/model/command/test_create_renewables_cluster.py b/tests/variantstudy/model/command/test_create_renewables_cluster.py index e47e9277e0..fc6ac91afe 100644 --- a/tests/variantstudy/model/command/test_create_renewables_cluster.py +++ b/tests/variantstudy/model/command/test_create_renewables_cluster.py @@ -1,8 +1,13 @@ import configparser +import re + +import pytest +from pydantic import ValidationError from antarest.study.storage.rawstudy.model.filesystem.config.model import ENR_MODELLING, transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter +from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster @@ -11,65 +16,115 @@ class TestCreateRenewablesCluster: - def test_validation(self, empty_study: FileStudy): - pass + def test_init(self, command_context: CommandContext): + cl = CreateRenewablesCluster( + area_id="foo", + cluster_name="Cluster1", + parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400}, + command_context=command_context, + ) + + # Check the command metadata + assert cl.command_name == CommandName.CREATE_RENEWABLES_CLUSTER + assert cl.version == 1 + assert cl.command_context is command_context + + # Check the command data + assert cl.area_id == "foo" + assert cl.cluster_name == "Cluster1" + assert cl.parameters == {"group": "Solar Thermal", "nominalcapacity": "2400", "unitcount": "2"} + + def test_validate_cluster_name(self, command_context: CommandContext): + with pytest.raises(ValidationError, match="cluster_name"): + CreateRenewablesCluster(area_id="fr", cluster_name="%", command_context=command_context, parameters={}) def test_apply(self, empty_study: FileStudy, command_context: CommandContext): empty_study.config.enr_modelling = ENR_MODELLING.CLUSTERS.value study_path = empty_study.config.study_path - area_name = "Area" + area_name = "DE" area_id = transform_name_to_id(area_name, lower=True) - cluster_name = "cluster_name" - cluster_id = transform_name_to_id(cluster_name, lower=True) - - CreateArea.parse_obj( - { - "area_name": area_name, - "command_context": command_context, - } - ).apply(empty_study) + cluster_name = "Cluster-1" + + CreateArea(area_name=area_name, command_context=command_context).apply(empty_study) parameters = { "name": cluster_name, "ts-interpretation": "power-generation", } - command = CreateRenewablesCluster.parse_obj( - { - "area_id": area_id, - "cluster_name": cluster_name, - "parameters": parameters, - "command_context": command_context, - } + command = CreateRenewablesCluster( + area_id=area_id, + cluster_name=cluster_name, + parameters=parameters, + command_context=command_context, ) output = command.apply(empty_study) - assert output.status + assert output.status is True + assert re.match( + r"Renewable cluster 'cluster-1' added to area 'de'", + output.message, + flags=re.IGNORECASE, + ) clusters = configparser.ConfigParser() clusters.read(study_path / "input" / "renewables" / "clusters" / area_id / "list.ini") assert str(clusters[cluster_name]["name"]) == cluster_name assert str(clusters[cluster_name]["ts-interpretation"]) == parameters["ts-interpretation"] - output = CreateRenewablesCluster.parse_obj( - { - "area_id": area_id, - "cluster_name": cluster_name, - "parameters": parameters, - "command_context": command_context, - } + output = CreateRenewablesCluster( + area_id=area_id, + cluster_name=cluster_name, + parameters=parameters, + command_context=command_context, ).apply(empty_study) assert not output.status - output = CreateRenewablesCluster.parse_obj( - { - "area_id": "non_existent_area", - "cluster_name": cluster_name, - "parameters": parameters, - "command_context": command_context, - } + output = CreateRenewablesCluster( + area_id=area_id, + cluster_name=cluster_name, + parameters=parameters, + command_context=command_context, ).apply(empty_study) - assert not output.status + assert output.status is False + + assert re.match( + r"Renewable cluster 'cluster-1' already exists in the area 'de'", + output.message, + flags=re.IGNORECASE, + ) + + output = CreateRenewablesCluster( + area_id="non_existent_area", + cluster_name=cluster_name, + parameters=parameters, + command_context=command_context, + ).apply(empty_study) + assert output.status is False + assert re.match( + r"Area 'non_existent_area' does not exist", + output.message, + flags=re.IGNORECASE, + ) + + def test_to_dto(self, command_context: CommandContext): + command = CreateRenewablesCluster( + area_id="foo", + cluster_name="Cluster1", + parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400}, + command_context=command_context, + ) + dto = command.to_dto() + assert dto.dict() == { + "action": "create_renewables_cluster", # "renewables" with a final "s". + "args": { + "area_id": "foo", + "cluster_name": "Cluster1", + "parameters": {"group": "Solar Thermal", "nominalcapacity": "2400", "unitcount": "2"}, + }, + "id": None, + "version": 1, + } def test_match(command_context: CommandContext): @@ -95,6 +150,11 @@ def test_match(command_context: CommandContext): assert base.match(other_match) assert not base.match(other_not_match) assert not base.match(other_other) + + assert base.match(other_match, equal=True) + assert not base.match(other_not_match, equal=True) + assert not base.match(other_other, equal=True) + assert base.match_signature() == "create_renewables_cluster%foo%foo" assert base.get_inner_matrices() == [] diff --git a/tests/variantstudy/model/command/test_manage_binding_constraints.py b/tests/variantstudy/model/command/test_manage_binding_constraints.py index d22e05ce1e..a1309c2e47 100644 --- a/tests/variantstudy/model/command/test_manage_binding_constraints.py +++ b/tests/variantstudy/model/command/test_manage_binding_constraints.py @@ -1,5 +1,7 @@ from unittest.mock import Mock +import numpy as np + from antarest.study.storage.rawstudy.io.reader import IniReader from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import BindingConstraintFrequency from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy @@ -337,22 +339,25 @@ def test_revert(command_context: CommandContext): def test_create_diff(command_context: CommandContext): + values_a = np.random.rand(365, 3).tolist() base = CreateBindingConstraint( name="foo", enabled=False, time_step=BindingConstraintFrequency.DAILY, operator=BindingConstraintOperator.BOTH, coeffs={"a": [0.3]}, - values="a", + values=values_a, command_context=command_context, ) + + values_b = np.random.rand(8760, 3).tolist() other_match = CreateBindingConstraint( name="foo", enabled=True, time_step=BindingConstraintFrequency.HOURLY, operator=BindingConstraintOperator.EQUAL, coeffs={"b": [0.3]}, - values="b", + values=values_b, command_context=command_context, ) assert base.create_diff(other_match) == [ @@ -362,7 +367,7 @@ def test_create_diff(command_context: CommandContext): time_step=BindingConstraintFrequency.HOURLY, operator=BindingConstraintOperator.EQUAL, coeffs={"b": [0.3]}, - values="b", + values=values_b, command_context=command_context, ) ] diff --git a/tests/variantstudy/model/command/test_manage_district.py b/tests/variantstudy/model/command/test_manage_district.py index d0ca0b2c4c..fee8be82fe 100644 --- a/tests/variantstudy/model/command/test_manage_district.py +++ b/tests/variantstudy/model/command/test_manage_district.py @@ -63,7 +63,6 @@ def test_manage_district(empty_study: FileStudy, command_context: CommandContext create_district2_command: ICommand = CreateDistrict( name="One subtracted zone", - metadata={}, base_filter=DistrictBaseFilter.add_all, filter_items=[area1_id], command_context=command_context, @@ -79,7 +78,6 @@ def test_manage_district(empty_study: FileStudy, command_context: CommandContext update_district2_command: ICommand = UpdateDistrict( id="one subtracted zone", - metadata={}, base_filter=DistrictBaseFilter.remove_all, filter_items=[area2_id], command_context=command_context, @@ -94,7 +92,6 @@ def test_manage_district(empty_study: FileStudy, command_context: CommandContext create_district3_command: ICommand = CreateDistrict( name="Empty district without output", - metadata={}, output=False, command_context=command_context, ) diff --git a/tests/variantstudy/model/command/test_replace_matrix.py b/tests/variantstudy/model/command/test_replace_matrix.py index e3aaad78c4..5436f1e98d 100644 --- a/tests/variantstudy/model/command/test_replace_matrix.py +++ b/tests/variantstudy/model/command/test_replace_matrix.py @@ -1,5 +1,7 @@ from unittest.mock import Mock, patch +import numpy as np + from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter @@ -69,18 +71,33 @@ def test_match(command_context: CommandContext): @patch("antarest.study.storage.variantstudy.business.command_extractor.CommandExtractor.generate_replace_matrix") def test_revert(mock_generate_replace_matrix, command_context: CommandContext): - base = ReplaceMatrix(target="foo", matrix=[[0]], command_context=command_context) + matrix_a = np.random.rand(5, 2).tolist() + base = ReplaceMatrix(target="foo", matrix=matrix_a, command_context=command_context) study = FileStudy(config=Mock(), tree=Mock()) CommandReverter().revert(base, [], study) mock_generate_replace_matrix.assert_called_with(study.tree, ["foo"]) assert CommandReverter().revert( base, - [ReplaceMatrix(target="foo", matrix="b", command_context=command_context)], + [ + ReplaceMatrix( + target="foo", + matrix=matrix_a, + command_context=command_context, + ) + ], study, - ) == [ReplaceMatrix(target="foo", matrix="b", command_context=command_context)] + ) == [ + ReplaceMatrix( + target="foo", + matrix=matrix_a, + command_context=command_context, + ) + ] def test_create_diff(command_context: CommandContext): - base = ReplaceMatrix(target="foo", matrix="c", command_context=command_context) - other_match = ReplaceMatrix(target="foo", matrix="b", command_context=command_context) + matrix_a = np.random.rand(5, 2).tolist() + base = ReplaceMatrix(target="foo", matrix=matrix_a, command_context=command_context) + matrix_b = np.random.rand(5, 2).tolist() + other_match = ReplaceMatrix(target="foo", matrix=matrix_b, command_context=command_context) assert base.create_diff(other_match) == [other_match] diff --git a/tests/variantstudy/model/test_variant_model.py b/tests/variantstudy/model/test_variant_model.py index d3a1760077..27cd732e47 100644 --- a/tests/variantstudy/model/test_variant_model.py +++ b/tests/variantstudy/model/test_variant_model.py @@ -2,7 +2,9 @@ from pathlib import Path from unittest.mock import ANY, Mock +import numpy as np from sqlalchemy import create_engine +from sqlalchemy.engine.base import Engine # type: ignore from antarest.core.cache.business.local_chache import LocalCache from antarest.core.config import Config, StorageConfig, WorkspaceConfig @@ -29,17 +31,11 @@ ) -def test_commands_service(tmp_path: Path, command_factory: CommandFactory): - engine = create_engine( - "sqlite:///:memory:", - echo=False, - connect_args={"check_same_thread": False}, - ) - Base.metadata.create_all(engine) +def test_commands_service(tmp_path: Path, db_engine: Engine, command_factory: CommandFactory): # noinspection SpellCheckingInspection DBSessionMiddleware( None, - custom_engine=engine, + custom_engine=db_engine, session_args={"autocommit": False, "autoflush": False}, ) repository = VariantStudyRepository(LocalCache()) @@ -99,12 +95,13 @@ def test_commands_service(tmp_path: Path, command_factory: CommandFactory): assert len(commands) == 3 # Update command - # note: we use a matrix reference to simplify tests + prepro = np.random.rand(365, 6).tolist() + prepro_id = command_factory.command_context.matrix_service.create(prepro) command_5 = CommandDTO( action="replace_matrix", args={ "target": "some/matrix/path", - "matrix": "matrix://739aa4b6-79ff-4388-8fed-f0d285bfc69f", + "matrix": prepro_id, }, ) service.update_command( @@ -115,7 +112,7 @@ def test_commands_service(tmp_path: Path, command_factory: CommandFactory): ) commands = service.get_commands(saved_id, SADMIN) assert commands[2].action == "replace_matrix" - assert commands[2].args["matrix"] == "matrix://739aa4b6-79ff-4388-8fed-f0d285bfc69f" + assert commands[2].args["matrix"] == prepro_id # Move command service.move_command( From afac2d67fc72e2a39557665a4123e99c9ca57a5f Mon Sep 17 00:00:00 2001 From: MartinBelthle <102529366+MartinBelthle@users.noreply.github.com> Date: Sun, 15 Oct 2023 20:56:26 +0200 Subject: [PATCH 3/8] fix(raw): fix HTTP exception when going on debug view (#1769) Co-authored-by: Laurent LAPORTE --- antarest/study/web/raw_studies_blueprint.py | 5 ++--- .../raw_studies_blueprint/test_fetch_raw_data.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/antarest/study/web/raw_studies_blueprint.py b/antarest/study/web/raw_studies_blueprint.py index 21d19620e2..edf09aff37 100644 --- a/antarest/study/web/raw_studies_blueprint.py +++ b/antarest/study/web/raw_studies_blueprint.py @@ -96,12 +96,11 @@ def get_study( extra={"user": current_user.id}, ) parameters = RequestParameters(user=current_user) - - resource_path = pathlib.PurePosixPath(path) - output = study_service.get(uuid, str(resource_path), depth=depth, formatted=formatted, params=parameters) + output = study_service.get(uuid, path, depth=depth, formatted=formatted, params=parameters) if isinstance(output, bytes): # Guess the suffix form the target data + resource_path = pathlib.PurePosixPath(path) parent_cfg = study_service.get(uuid, str(resource_path.parent), depth=2, formatted=True, params=parameters) child = parent_cfg[resource_path.name] suffix = pathlib.PurePosixPath(child).suffix diff --git a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py index 04a6b3fbfc..3d0177b1f2 100644 --- a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py +++ b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py @@ -1,4 +1,5 @@ import http +import itertools import json import pathlib import shutil @@ -167,3 +168,12 @@ def test_get_study( ) assert res.status_code == 200 assert np.isnan(res.json()["data"][0]).any() + + # Iterate over all possible combinations of path and depth + for path, depth in itertools.product([None, "", "/"], [0, 1, 2]): + res = client.get( + f"/v1/studies/{study_id}/raw", + params={"path": path, "depth": depth}, + headers=headers, + ) + assert res.status_code == 200, f"Error for path={path} and depth={depth}" From 5aaa67a9e85185dee7c7dd729e9cb593f4d1e029 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Sun, 15 Oct 2023 21:12:22 +0200 Subject: [PATCH 4/8] build: enhance the `update_version.py` script to better update `package-lock.json` --- scripts/update_version.py | 2 +- webapp/package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/update_version.py b/scripts/update_version.py index f2d5e32512..a829035ff0 100755 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -148,7 +148,7 @@ def upgrade_version(new_version: str, new_date: str) -> None: if fullpath.is_file(): print(f"- updating '{fullpath.relative_to(PROJECT_DIR)}'...") text = fullpath.read_text(encoding="utf-8") - patched = re.sub(search, replace, text, count=1) + patched = re.sub(search, replace, text, count=2) fullpath.write_text(patched, encoding="utf-8") # Patching release date diff --git a/webapp/package-lock.json b/webapp/package-lock.json index dd4e93dc89..6c474d183a 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "antares-web", - "version": "2.15.1", + "version": "2.15.4", "dependencies": { "@emotion/react": "11.10.6", "@emotion/styled": "11.10.6", From fba859b736c76c2c3e671a6c740ddcd7b31b703d Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Sun, 15 Oct 2023 21:15:52 +0200 Subject: [PATCH 5/8] build: update CHANGELOG.md for release v2.15.4 (unreleased) --- docs/CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8e425ac15a..0db70386b4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,7 +6,18 @@ v2.15.4 (unreleased) ### Tests -* **commands:** refactored study variant command unit tests, improved coverage, and fixed deprecated attribute usage [`5244442`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/52444422ed5e3ef116f514e02b8454b1ae203104) +* **commands:** refactored study variant command unit tests, improved coverage, and fixed deprecated attribute usage ([8bd0bdf](https://github.com/AntaresSimulatorTeam/AntaREST/commit/8bd0bdf93c1a9ef0ee12570cb7d398ba7212b2fe)) + + +### Bug Fixes + +* **raw:** fix HTTP exception when going on debug view (#1769) ([afac2d6](https://github.com/AntaresSimulatorTeam/AntaREST/commit/afac2d67fc72e2a39557665a4123e99c9ca57a5f)) + + +### Contributors + +laurent-laporte-pro, +MartinBelthle From 1dcb3f0600f7b7e2a22dd12b66bb8cb1bb60ce3d Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:10:51 +0200 Subject: [PATCH 6/8] fix(ui-study): remove popup to prevent close after variant creation (#1773) --- webapp/public/locales/en/main.json | 1 - webapp/public/locales/fr/main.json | 1 - .../InformationView/CreateVariantDialog.tsx | 34 +++++---------- webapp/src/components/common/Form/index.tsx | 41 ++++++++++++++----- .../src/components/common/FormTable/index.tsx | 6 +-- .../components/common/dialogs/FormDialog.tsx | 35 ++++++++++------ 6 files changed, 67 insertions(+), 51 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index e966640063..a6c3b22341 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -584,7 +584,6 @@ "studies.variant": "Variant", "variants.createNewVariant": "Create new variant", "variants.newVariant": "New variant", - "variants.error.variantCreation": "Failed to create variant", "variants.newCommand": "Add new command", "variants.commandActionLabel": "Select action", "variants.success.save": "Command updated successfully", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 90a338597b..3f8551f4e7 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -584,7 +584,6 @@ "studies.variant": "Variante", "variants.createNewVariant": "Créer une nouvelle variante", "variants.newVariant": "Nouvelle variante", - "variants.error.variantCreation": "Erreur lors de la création de la variante", "variants.newCommand": "Ajouter une nouvelle commande", "variants.commandActionLabel": "Sélectionnez le type", "variants.success.save": "Commande modifiée avec succès", diff --git a/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx index 3f6e4f8915..e0578828ae 100644 --- a/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx @@ -1,12 +1,10 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router"; import { useTranslation } from "react-i18next"; -import { AxiosError } from "axios"; import AddCircleIcon from "@mui/icons-material/AddCircle"; import { GenericInfo, VariantTree } from "../../../../../common/types"; import { createVariant } from "../../../../../services/api/variant"; import { createListFromTree } from "../../../../../services/utils"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import FormDialog from "../../../../common/dialogs/FormDialog"; import StringFE from "../../../../common/fieldEditors/StringFE"; import Fieldset from "../../../../common/Fieldset"; @@ -24,39 +22,28 @@ function CreateVariantDialog(props: Props) { const { parentId, open, tree, onClose } = props; const [t] = useTranslation(); const navigate = useNavigate(); - const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [sourceList, setSourceList] = useState>([]); + const defaultValues = { name: "", sourceId: parentId }; useEffect(() => { setSourceList(createListFromTree(tree)); }, [tree]); - const defaultValues = { - name: "", - sourceId: parentId, - }; - //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// - const handleSubmit = async ( - data: SubmitHandlerPlus - ) => { + const handleSubmit = (data: SubmitHandlerPlus) => { const { sourceId, name } = data.values; + return createVariant(sourceId, name); + }; - try { - if (sourceId) { - const variantId = await createVariant(sourceId, name); - onClose(); - navigate(`/studies/${variantId}`); - } - } catch (e) { - enqueueErrorSnackbar( - t("variants.error.variantCreation"), - e as AxiosError - ); - } + const handleSubmitSuccessful = async ( + data: SubmitHandlerPlus, + variantId: string + ) => { + onClose(); + navigate(`/studies/${variantId}`); }; //////////////////////////////////////////////////////////////// @@ -70,6 +57,7 @@ function CreateVariantDialog(props: Props) { open={open} onCancel={onClose} onSubmit={handleSubmit} + onSubmitSuccessful={handleSubmitSuccessful} config={{ defaultValues }} > {({ control }) => ( diff --git a/webapp/src/components/common/Form/index.tsx b/webapp/src/components/common/Form/index.tsx index 9e8d6721c2..1c56825c16 100644 --- a/webapp/src/components/common/Form/index.tsx +++ b/webapp/src/components/common/Form/index.tsx @@ -51,14 +51,22 @@ export type AutoSubmitConfig = { enable: boolean; wait?: number }; export interface FormProps< TFieldValues extends FieldValues = FieldValues, - TContext = any -> extends Omit, "onSubmit" | "children"> { + TContext = any, + SubmitReturnValue = any +> extends Omit< + React.HTMLAttributes, + "onSubmit" | "onInvalid" | "children" + > { config?: UseFormProps; onSubmit?: ( data: SubmitHandlerPlus, event?: React.BaseSyntheticEvent - ) => void | Promise; - onSubmitError?: SubmitErrorHandler; + ) => void | Promise; + onSubmitSuccessful?: ( + data: SubmitHandlerPlus, + submitResult: SubmitReturnValue + ) => void; + onInvalid?: SubmitErrorHandler; children: | ((formApi: UseFormReturnPlus) => React.ReactNode) | React.ReactNode; @@ -82,7 +90,8 @@ function Form( const { config, onSubmit, - onSubmitError, + onSubmitSuccessful, + onInvalid, children, submitButtonText, submitButtonIcon, @@ -110,6 +119,8 @@ function Form( [] ); const lastSubmittedData = useRef(); + // eslint-disable-next-line @typescript-eslint/no-empty-function + const submitSuccessfulCb = useRef(() => {}); const preventClose = useRef(false); const contextValue = useMemo( () => ({ isAutoSubmitEnabled: autoSubmitConfig.enable }), @@ -177,6 +188,8 @@ function Form( useEffect( () => { if (isSubmitSuccessful && lastSubmittedData.current) { + submitSuccessfulCb.current(); + const valuesToSetAfterReset = getValues( fieldsChangeDuringAutoSubmitting.current ); @@ -232,11 +245,11 @@ function Form( typeof data >; - const res = []; + const toResolve = []; if (autoSubmitConfig.enable) { const listeners = fieldAutoSubmitListeners.current; - res.push( + toResolve.push( ...Object.keys(listeners) .filter((key) => R.hasPath(stringToPath(key), dirtyValues)) .map((key) => { @@ -246,11 +259,19 @@ function Form( ); } + const dataArg = { values: data, dirtyValues }; + if (onSubmit) { - res.push(onSubmit({ values: data, dirtyValues }, event)); + toResolve.push(onSubmit(dataArg, event)); } - return Promise.all(res) + return Promise.all(toResolve) + .then((values) => { + submitSuccessfulCb.current = () => { + const onSubmitRes = onSubmit ? R.last(values) : undefined; + onSubmitSuccessful?.(dataArg, onSubmitRes); + }; + }) .catch((err) => { enqueueErrorSnackbar(t("form.submit.error"), err); @@ -266,7 +287,7 @@ function Form( .finally(() => { preventClose.current = false; }); - }, onSubmitError); + }, onInvalid); return callback(); }; diff --git a/webapp/src/components/common/FormTable/index.tsx b/webapp/src/components/common/FormTable/index.tsx index 41bd13812f..b89380dd40 100644 --- a/webapp/src/components/common/FormTable/index.tsx +++ b/webapp/src/components/common/FormTable/index.tsx @@ -23,7 +23,7 @@ export interface FormTableProps< > { defaultValues: DefaultValues; onSubmit?: FormProps["onSubmit"]; - onSubmitError?: FormProps["onSubmitError"]; + onInvalid?: FormProps["onInvalid"]; formApiRef?: FormProps["apiRef"]; sx?: SxProps; tableProps?: Omit & { @@ -40,7 +40,7 @@ function FormTable( const { defaultValues, onSubmit, - onSubmitError, + onInvalid, sx, formApiRef, tableProps = {}, @@ -83,7 +83,7 @@ function FormTable(
= Omit< - BasicDialogProps, - "onSubmit" | "children" -> & - Omit, "hideSubmitButton">; +type SuperType< + TFieldValues extends FieldValues, + TContext, + SubmitReturnValue +> = Omit & + Omit< + FormProps, + "hideSubmitButton" + >; export interface FormDialogProps< TFieldValues extends FieldValues = FieldValues, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TContext = any -> extends SuperType { + TContext = any, + SubmitReturnValue = any +> extends SuperType { cancelButtonText?: string; onCancel: VoidFunction; } // TODO: `formState.isSubmitting` doesn't update when auto submit enabled -function FormDialog( - props: FormDialogProps -) { +function FormDialog< + TFieldValues extends FieldValues, + TContext, + SubmitReturnValue +>(props: FormDialogProps) { const { config, onSubmit, - onSubmitError, + onSubmitSuccessful, + onInvalid, children, autoSubmit, onStateChange, @@ -42,7 +50,8 @@ function FormDialog( const formProps = { config, onSubmit, - onSubmitError, + onSubmitSuccessful, + onInvalid, children, autoSubmit, }; From 641c421a8f64759a7b8c7a732dc344693d862ec1 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:48:05 +0200 Subject: [PATCH 7/8] fix(results-ui): display results for a specific year (#1779) --- .../App/Singlestudy/explore/Results/ResultDetails/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts index a2bcdd4af9..f140205498 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts @@ -36,7 +36,7 @@ export function createPath(params: Params): string { const { id, mode } = output; const isYearPeriod = year && year > 0; const periodFolder = isYearPeriod - ? `mc-ind/${Math.max(year, output.nbyears).toString().padStart(5, "0")}` + ? `mc-ind/${Math.min(year, output.nbyears).toString().padStart(5, "0")}` : "mc-all"; const isLink = "area1" in item; const itemType = isLink ? OutputItemType.Links : OutputItemType.Areas; From cdfcb662f8589390866e41b31a039f86598935e0 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Tue, 24 Oct 2023 17:22:10 +0200 Subject: [PATCH 8/8] build: new hot fix release v2.15.4 (2023-10-25) --- antarest/__init__.py | 2 +- docs/CHANGELOG.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/antarest/__init__.py b/antarest/__init__.py index a73192a58a..f04a15b16f 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -9,7 +9,7 @@ __version__ = "2.15.4" __author__ = "RTE, Antares Web Team" -__date__ = "unreleased" +__date__ = "2023-10-25" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0db70386b4..3e6b6a3df2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,7 @@ Antares Web Changelog ===================== -v2.15.4 (unreleased) +v2.15.4 (2023-10-25) -------------------- ### Tests @@ -11,7 +11,9 @@ v2.15.4 (unreleased) ### Bug Fixes -* **raw:** fix HTTP exception when going on debug view (#1769) ([afac2d6](https://github.com/AntaresSimulatorTeam/AntaREST/commit/afac2d67fc72e2a39557665a4123e99c9ca57a5f)) +* **results-ui:** display results for a specific year [`#1779`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1779) +* **ui-study:** remove popup to prevent close after variant creation [`#1773`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1773) +* **raw:** fix HTTP exception when going on debug view [`#1769`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1769) ### Contributors