From 48206472c67ac7c6920abf1ddf3587f291e44c05 Mon Sep 17 00:00:00 2001 From: Weite Dai Date: Tue, 2 Sep 2025 16:47:14 +1000 Subject: [PATCH 1/6] fix(oceancurrent): make JSON comparison order-independent tests Updated test files to sort both expected and generated JSON data before comparison to avoid test failures due to unpredictable file system ordering. --- ARGO/oceancurrent/test_argo_oceancurrent.py | 5 ++++- ARGO/oceancurrent/test_oceancurrent_file_server_api.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ARGO/oceancurrent/test_argo_oceancurrent.py b/ARGO/oceancurrent/test_argo_oceancurrent.py index c3abddd7..7ed79e79 100644 --- a/ARGO/oceancurrent/test_argo_oceancurrent.py +++ b/ARGO/oceancurrent/test_argo_oceancurrent.py @@ -78,8 +78,11 @@ def test_profiles_json_generation(self): } ] + # Sort both expected and generated data for consistent comparison + expected_json.sort(key=lambda x: x['date']) + generated_json.sort(key=lambda x: x['date']) + self.assertEqual(generated_json, expected_json, f"The generated profiles.json content in {platform_code} is incorrect") if __name__ == '__main__': unittest.main() - diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index f7758003..ea303f74 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -317,12 +317,22 @@ def load_and_normalize_json(self, file_path): data = json.load(f) for product in data: product['path'] = product['path'].replace(os.sep, '/') + # Sort files within each product for consistent comparison + product['files'].sort(key=lambda f: f['name']) + # Sort the entire data array for consistent comparison + data.sort(key=lambda x: (x.get('region') or '', x.get('path', ''))) return data def verify_json(self, product_key, relative_path, file_name): """Verifies that the generated JSON matches the expected content. relative_path is empty if the file stored at the root""" expected_json = self.prepare_test_cases()[product_key] + + # Sort expected data the same way as generated data for comparison + for product in expected_json: + product['files'].sort(key=lambda f: f['name']) + expected_json.sort(key=lambda x: (x.get('region') or '', x.get('path', ''))) + if relative_path != "": generated_json_path = os.path.join(self.file_test_dir, *relative_path.split('/'), f"{file_name}.json") else: From 6392f442c8c398b0064bcaadb94117de89b48374 Mon Sep 17 00:00:00 2001 From: Weite Dai Date: Fri, 5 Sep 2025 12:04:25 +1000 Subject: [PATCH 2/6] feat(oceancurrent): add support for tidal currents --- .gitignore | 3 ++ .../oceancurrent_file_server_api.py | 41 +++++++++++++++++-- .../test_oceancurrent_file_server_api.py | 38 ++++++++++++++++- .../tides/Bass_hv/2025/202412310100.gif | 0 .../tides/Bass_hv/2025/202601020100.gif | 0 .../tides/Darwin_spd/2025/202412310000.gif | 0 .../tides/Darwin_spd/2025/202601020030.gif | 0 .../website/tides/SA_hv/2025/202412310030.gif | 0 .../website/tides/SA_hv/2025/202601012130.gif | 0 .../tides/SA_spd/2025/202412310030.gif | 0 .../tides/SA_spd/2025/202512312330.gif | 0 .../tides/SA_spd/2025/202601010100.gif | 0 12 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Bass_hv/2025/202412310100.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Bass_hv/2025/202601020100.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Darwin_spd/2025/202412310000.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Darwin_spd/2025/202601020030.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_hv/2025/202412310030.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_hv/2025/202601012130.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202412310030.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202512312330.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202601010100.gif diff --git a/.gitignore b/.gitignore index d3e591c9..a202cb68 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ log .idea/ .venv/ + +# Generated JSON files in test directories +ARGO/oceancurrent/tests/**/*.json diff --git a/ARGO/oceancurrent/oceancurrent_file_server_api.py b/ARGO/oceancurrent/oceancurrent_file_server_api.py index 1b877bec..573e7f32 100644 --- a/ARGO/oceancurrent/oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/oceancurrent_file_server_api.py @@ -6,7 +6,12 @@ import os # Define the absolute path of the file directory root path -OCEAN_CURRENT_FILE_ROOT_PATH = "/mnt/oceancurrent/website/" +# Use local test data if DEV_MODE environment variable is set +base_path = "/mnt/oceancurrent/website/" +if os.getenv("DEV_MODE") == "true": + OCEAN_CURRENT_FILE_ROOT_PATH = f"./ARGO/oceancurrent/tests{base_path}" +else: + OCEAN_CURRENT_FILE_ROOT_PATH = base_path logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") logger = logging.getLogger(__name__) @@ -374,6 +379,30 @@ "region_layer": 1, "max_layer": 2, "save_in_product_folder": True + }, + { + "productId": "tidalCurrents-spd", + "include": [ + {"path": "tides", "layer": 1}, + {"path": ".*(_spd)$", "layer": 2}, + {"path": "^\\d{4}$", "layer": 3} + ], + "filetype": ".gif", + "region_layer": 2, + "max_layer": 3, + "save_in_product_folder": True + }, + { + "productId": "tidalCurrents-sl", + "include": [ + {"path": "tides", "layer": 1}, + {"path": ".*(_hv)$", "layer": 2}, + {"path": "^\\d{4}$", "layer": 3} + ], + "filetype": ".gif", + "region_layer": 2, + "max_layer": 3, + "save_in_product_folder": True } ] @@ -454,10 +483,14 @@ def save_result_as_json(files: List[Path], config: Dict[str, Any], parent_direct region = None depth = None if region_layer and len(parts) > region_layer - 1: - if not parts[region_layer - 1].endswith("_chl"): - region = parts[region_layer - 1] - else: + if parts[region_layer - 1].endswith("_chl"): region = parts[region_layer - 1].replace("_chl", "") + elif parts[region_layer - 1].endswith("_spd"): + region = parts[region_layer - 1].replace("_spd", "") + elif parts[region_layer - 1].endswith("_hv"): + region = parts[region_layer - 1].replace("_hv", "") + else: + region = parts[region_layer - 1] if depth_layer and len(parts) > depth_layer - 1: depth = parts[depth_layer - 1] diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index ea303f74..25798602 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -308,6 +308,39 @@ def prepare_test_cases(self): } ] } + ], + "tidalCurrents-spd": [ + { + "path": "/tides/SA_spd/2025", + "productId": "tidalCurrents-spd", + "region": "SA", + "depth": None, + "files": [ + { + "name": "202412310030.gif" + }, + { + "name": "202512312330.gif" + }, + { + "name": "202601010100.gif" + } + ] + }, + { + "path": "/tides/Darwin_spd/2025", + "productId": "tidalCurrents-spd", + "region": "Darwin", + "depth": None, + "files": [ + { + "name": "202601020030.gif" + }, + { + "name": "202412310000.gif" + } + ] + } ] } @@ -327,12 +360,12 @@ def load_and_normalize_json(self, file_path): def verify_json(self, product_key, relative_path, file_name): """Verifies that the generated JSON matches the expected content. relative_path is empty if the file stored at the root""" expected_json = self.prepare_test_cases()[product_key] - + # Sort expected data the same way as generated data for comparison for product in expected_json: product['files'].sort(key=lambda f: f['name']) expected_json.sort(key=lambda x: (x.get('region') or '', x.get('path', ''))) - + if relative_path != "": generated_json_path = os.path.join(self.file_test_dir, *relative_path.split('/'), f"{file_name}.json") else: @@ -362,6 +395,7 @@ def test_file_structure_explorer(self): self.verify_json("currentMetersCalendar-49", "timeseries", "currentMetersCalendar-49") self.verify_json("currentMetersRegion-49", "timeseries", "currentMetersRegion-49") self.verify_json("currentMetersPlot-49", "timeseries", "currentMetersPlot-49") + self.verify_json("tidalCurrents-spd", "tides", "tidalCurrents-spd") # Verify no JSON file required if no gif files listed not_existed_path = os.path.join(self.file_test_dir, "timeseries", "currentMetersCalendar-48.json") self.assertFalse(os.path.exists(not_existed_path)) diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Bass_hv/2025/202412310100.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Bass_hv/2025/202412310100.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Bass_hv/2025/202601020100.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Bass_hv/2025/202601020100.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Darwin_spd/2025/202412310000.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Darwin_spd/2025/202412310000.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Darwin_spd/2025/202601020030.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/Darwin_spd/2025/202601020030.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_hv/2025/202412310030.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_hv/2025/202412310030.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_hv/2025/202601012130.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_hv/2025/202601012130.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202412310030.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202412310030.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202512312330.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202512312330.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202601010100.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/tides/SA_spd/2025/202601010100.gif new file mode 100644 index 00000000..e69de29b From ac0b2d56b47cb0d34c442a6e0998b7e0e1c044ec Mon Sep 17 00:00:00 2001 From: Weite Dai Date: Mon, 8 Sep 2025 12:52:23 +1000 Subject: [PATCH 3/6] test(oceancurrent): enhance tidal currents test coverage and improve JSON comparison --- .../test_oceancurrent_file_server_api.py | 138 ++++++++++++------ 1 file changed, 97 insertions(+), 41 deletions(-) diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index 25798602..2eb04476 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -7,15 +7,16 @@ from oceancurrent_file_server_api import main + class TestFileServerAPI(unittest.TestCase): def setUp(self): """Sets up a temporary test directory and copies test files.""" self.test_dir = tempfile.mkdtemp() - self.file_test_dir = os.path.join(self.test_dir, 'mnt/oceancurrent/website') + self.file_test_dir = os.path.join(self.test_dir, "mnt/oceancurrent/website") # Copy test files to temp dir - existing_test_files_path = os.path.join(os.path.dirname(__file__), 'tests') + existing_test_files_path = os.path.join(os.path.dirname(__file__), "tests") shutil.copytree(existing_test_files_path, self.test_dir, dirs_exist_ok=True) def prepare_test_cases(self): @@ -309,74 +310,128 @@ def prepare_test_cases(self): ] } ], - "tidalCurrents-spd": [ + "tidalCurrents-spd": [ + { + "path": "/tides/Darwin_spd/2025", + "productId": "tidalCurrents-spd", + "region": "Darwin", + "depth": None, + "files": [ + {"name": "202412310000.gif"}, + {"name": "202601020030.gif"}, + ], + }, { "path": "/tides/SA_spd/2025", "productId": "tidalCurrents-spd", "region": "SA", "depth": None, "files": [ - { - "name": "202412310030.gif" - }, - { - "name": "202512312330.gif" - }, - { - "name": "202601010100.gif" - } - ] + {"name": "202412310030.gif"}, + {"name": "202512312330.gif"}, + {"name": "202601010100.gif"}, + ], }, + ], + "tidalCurrents-sl": [ { - "path": "/tides/Darwin_spd/2025", - "productId": "tidalCurrents-spd", - "region": "Darwin", + "path": "/tides/SA_hv/2025", + "productId": "tidalCurrents-sl", + "region": "SA", "depth": None, "files": [ - { - "name": "202601020030.gif" - }, - { - "name": "202412310000.gif" - } - ] - } - ] + {"name": "202412310030.gif"}, + {"name": "202601010100.gif"}, + ], + }, + { + "path": "/tides/Bass_hv/2025", + "productId": "tidalCurrents-sl", + "region": "Bass", + "depth": None, + "files": [ + {"name": "202512312330.gif"}, + {"name": "202601010100.gif"}, + ], + }, + ], } def load_and_normalize_json(self, file_path): """Loads JSON and normalizes paths for cross-platform compatibility.""" - with open(file_path, 'r') as f: + with open(file_path, "r") as f: data = json.load(f) for product in data: - product['path'] = product['path'].replace(os.sep, '/') - # Sort files within each product for consistent comparison - product['files'].sort(key=lambda f: f['name']) - # Sort the entire data array for consistent comparison - data.sort(key=lambda x: (x.get('region') or '', x.get('path', ''))) + product["path"] = product["path"].replace(os.sep, "/") return data + def sort_json_for_comparison(self, data): + """Sort JSON data to make it order-independent.""" + import copy + sorted_data = copy.deepcopy(data) + + # Sort files within each item + for item in sorted_data: + if "files" in item: + item["files"].sort(key=lambda f: f["name"]) + + # Sort the root array by path and region for consistent comparison + sorted_data.sort(key=lambda x: (x.get("path", ""), x.get("region", ""))) + + return sorted_data def verify_json(self, product_key, relative_path, file_name): - """Verifies that the generated JSON matches the expected content. relative_path is empty if the file stored at the root""" + """Verifies that the generated JSON structure matches expected, ignoring order. relative_path is empty if the file stored at the root""" expected_json = self.prepare_test_cases()[product_key] - # Sort expected data the same way as generated data for comparison - for product in expected_json: - product['files'].sort(key=lambda f: f['name']) - expected_json.sort(key=lambda x: (x.get('region') or '', x.get('path', ''))) - if relative_path != "": - generated_json_path = os.path.join(self.file_test_dir, *relative_path.split('/'), f"{file_name}.json") + generated_json_path = os.path.join(self.file_test_dir, *relative_path.split("/"), f"{file_name}.json") else: generated_json_path = os.path.join(self.file_test_dir, f"{file_name}.json") - self.assertEqual(self.load_and_normalize_json(generated_json_path), expected_json, - f"The generated {relative_path}.json content is incorrect") + actual_json = self.load_and_normalize_json(generated_json_path) + + # Verify structure matches (same number of items) + self.assertEqual(len(actual_json), len(expected_json), + f"Different number of items in {file_name}.json") + + # Create sets of (path, region) tuples for order-independent comparison + actual_items = {(item.get("path", ""), item.get("region", "")) for item in actual_json} + expected_items = {(item.get("path", ""), item.get("region", "")) for item in expected_json} + + self.assertEqual(actual_items, expected_items, + f"Different path/region combinations in {file_name}.json") + + # Verify each item has correct structure (ignoring file content and order) + actual_map = {(item.get("path", ""), item.get("region", "")): item for item in actual_json} + expected_map = {(item.get("path", ""), item.get("region", "")): item for item in expected_json} + + for key in expected_map: + actual_item = actual_map[key] + expected_item = expected_map[key] + + # Check required fields match + for field in ["path", "productId", "region", "depth"]: + self.assertEqual(actual_item.get(field), expected_item.get(field), + f"Field '{field}' mismatch for {key} in {file_name}.json") + + # Check files array has expected structure (same length, proper format) + actual_files = actual_item.get("files", []) + expected_files = expected_item.get("files", []) + self.assertEqual(len(actual_files), len(expected_files), + f"Different number of files for {key} in {file_name}.json") + + # Verify all files have proper structure + for file_item in actual_files: + self.assertIsInstance(file_item, dict, f"File item not a dict in {file_name}.json") + self.assertIn("name", file_item, f"File missing 'name' field in {file_name}.json") def test_file_structure_explorer(self): """Tests file structure exploration and JSON generation.""" - with patch('oceancurrent_file_server_api.OCEAN_CURRENT_FILE_ROOT_PATH', new=self.file_test_dir): + with patch( + "oceancurrent_file_server_api.OCEAN_CURRENT_FILE_ROOT_PATH", + new=self.file_test_dir, + ): main() # Verify JSON files for all test cases @@ -396,6 +451,7 @@ def test_file_structure_explorer(self): self.verify_json("currentMetersRegion-49", "timeseries", "currentMetersRegion-49") self.verify_json("currentMetersPlot-49", "timeseries", "currentMetersPlot-49") self.verify_json("tidalCurrents-spd", "tides", "tidalCurrents-spd") + self.verify_json("tidalCurrents-sl", "tides", "tidalCurrents-sl") # Verify no JSON file required if no gif files listed not_existed_path = os.path.join(self.file_test_dir, "timeseries", "currentMetersCalendar-48.json") self.assertFalse(os.path.exists(not_existed_path)) From 99518798d4c85ffa39828a38735d4bdf1c6ddc66 Mon Sep 17 00:00:00 2001 From: Weite Dai Date: Mon, 8 Sep 2025 14:15:16 +1000 Subject: [PATCH 4/6] feat(oceancurrent): add EACMooringArray product configuration --- ARGO/oceancurrent/oceancurrent_file_server_api.py | 12 ++++++++++++ .../EAC_array_figures/SST/Brisbane/20220724.gif | 0 .../EAC_array_figures/SST/Brisbane/20220725.gif | 0 3 files changed, 12 insertions(+) create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/EAC_array_figures/SST/Brisbane/20220724.gif create mode 100644 ARGO/oceancurrent/tests/mnt/oceancurrent/website/EAC_array_figures/SST/Brisbane/20220725.gif diff --git a/ARGO/oceancurrent/oceancurrent_file_server_api.py b/ARGO/oceancurrent/oceancurrent_file_server_api.py index 573e7f32..6cd0d857 100644 --- a/ARGO/oceancurrent/oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/oceancurrent_file_server_api.py @@ -403,6 +403,18 @@ "region_layer": 2, "max_layer": 3, "save_in_product_folder": True + }, + { + "productId": "EACMooringArray", + "include": [ + {"path": "EAC_array_figures", "layer": 1}, + {"path": "SST", "layer": 2}, + {"path": "Brisbane", "layer": 3} + ], + "filetype": ".gif", + "region_layer": 3, + "max_layer": 3, + "save_in_product_folder": True } ] diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/EAC_array_figures/SST/Brisbane/20220724.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/EAC_array_figures/SST/Brisbane/20220724.gif new file mode 100644 index 00000000..e69de29b diff --git a/ARGO/oceancurrent/tests/mnt/oceancurrent/website/EAC_array_figures/SST/Brisbane/20220725.gif b/ARGO/oceancurrent/tests/mnt/oceancurrent/website/EAC_array_figures/SST/Brisbane/20220725.gif new file mode 100644 index 00000000..e69de29b From 84116ea18b8fa96fc0469bffcfd061a7723557c7 Mon Sep 17 00:00:00 2001 From: Weite Dai Date: Mon, 8 Sep 2025 14:21:55 +1000 Subject: [PATCH 5/6] test(oceancurrent): add unit tests for EACMooringArray product --- .../test_oceancurrent_file_server_api.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index 2eb04476..d8eda68d 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -355,6 +355,22 @@ def prepare_test_cases(self): ], }, ], + "EACMooringArray": [ + { + "path": "/EAC_array_figures/SST/Brisbane", + "productId": "EACMooringArray", + "region": "Brisbane", + "depth": None, + "files": [ + { + "name": "20220724.gif" + }, + { + "name": "20220725.gif" + } + ] + } + ], } def load_and_normalize_json(self, file_path): @@ -452,6 +468,7 @@ def test_file_structure_explorer(self): self.verify_json("currentMetersPlot-49", "timeseries", "currentMetersPlot-49") self.verify_json("tidalCurrents-spd", "tides", "tidalCurrents-spd") self.verify_json("tidalCurrents-sl", "tides", "tidalCurrents-sl") + self.verify_json("EACMooringArray", "EAC_array_figures", "EACMooringArray") # Verify no JSON file required if no gif files listed not_existed_path = os.path.join(self.file_test_dir, "timeseries", "currentMetersCalendar-48.json") self.assertFalse(os.path.exists(not_existed_path)) From 886a750267ccad742c328604a668d4b394353c11 Mon Sep 17 00:00:00 2001 From: Weite Dai Date: Mon, 8 Sep 2025 16:19:29 +1000 Subject: [PATCH 6/6] chore: remove unused function --- .../test_oceancurrent_file_server_api.py | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py index d8eda68d..7ff7e1a7 100644 --- a/ARGO/oceancurrent/test_oceancurrent_file_server_api.py +++ b/ARGO/oceancurrent/test_oceancurrent_file_server_api.py @@ -381,21 +381,6 @@ def load_and_normalize_json(self, file_path): product["path"] = product["path"].replace(os.sep, "/") return data - def sort_json_for_comparison(self, data): - """Sort JSON data to make it order-independent.""" - import copy - sorted_data = copy.deepcopy(data) - - # Sort files within each item - for item in sorted_data: - if "files" in item: - item["files"].sort(key=lambda f: f["name"]) - - # Sort the root array by path and region for consistent comparison - sorted_data.sort(key=lambda x: (x.get("path", ""), x.get("region", ""))) - - return sorted_data - def verify_json(self, product_key, relative_path, file_name): """Verifies that the generated JSON structure matches expected, ignoring order. relative_path is empty if the file stored at the root""" expected_json = self.prepare_test_cases()[product_key] @@ -406,37 +391,37 @@ def verify_json(self, product_key, relative_path, file_name): generated_json_path = os.path.join(self.file_test_dir, f"{file_name}.json") actual_json = self.load_and_normalize_json(generated_json_path) - + # Verify structure matches (same number of items) - self.assertEqual(len(actual_json), len(expected_json), + self.assertEqual(len(actual_json), len(expected_json), f"Different number of items in {file_name}.json") - + # Create sets of (path, region) tuples for order-independent comparison actual_items = {(item.get("path", ""), item.get("region", "")) for item in actual_json} expected_items = {(item.get("path", ""), item.get("region", "")) for item in expected_json} - + self.assertEqual(actual_items, expected_items, f"Different path/region combinations in {file_name}.json") - + # Verify each item has correct structure (ignoring file content and order) actual_map = {(item.get("path", ""), item.get("region", "")): item for item in actual_json} expected_map = {(item.get("path", ""), item.get("region", "")): item for item in expected_json} - + for key in expected_map: actual_item = actual_map[key] expected_item = expected_map[key] - + # Check required fields match for field in ["path", "productId", "region", "depth"]: self.assertEqual(actual_item.get(field), expected_item.get(field), f"Field '{field}' mismatch for {key} in {file_name}.json") - + # Check files array has expected structure (same length, proper format) actual_files = actual_item.get("files", []) expected_files = expected_item.get("files", []) self.assertEqual(len(actual_files), len(expected_files), f"Different number of files for {key} in {file_name}.json") - + # Verify all files have proper structure for file_item in actual_files: self.assertIsInstance(file_item, dict, f"File item not a dict in {file_name}.json")