diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ddb8dc0..aaf5218e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## vTBD +- [BP-1715](https://movai.atlassian.net/browse/BP-1715): Add tests for scopes import/export functionality + ## v3.25.3 - [BP-1704](https://movai.atlassian.net/browse/BP-1704): Optimize and remove unused imports - Use lazy imports for dal/scopes diff --git a/pyproject.toml b/pyproject.toml index 743c1f81..c569c20b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dal = [ line-length = 100 [tool.bumpversion] -current_version = "3.25.3.1" +current_version = "3.25.4.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)?(\\.(?P\\d+))?" serialize = ["{major}.{minor}.{patch}.{build}"] diff --git a/tests/unit/data/valid/metadata/Configuration/delete_me.yaml b/tests/unit/data/valid/metadata/Configuration/delete_me.yaml index e69de29b..b4f46b72 100644 --- a/tests/unit/data/valid/metadata/Configuration/delete_me.yaml +++ b/tests/unit/data/valid/metadata/Configuration/delete_me.yaml @@ -0,0 +1 @@ +key: value diff --git a/tests/unit/data/valid/metadata/Flow/delete_me.json b/tests/unit/data/valid/metadata/Flow/delete_me.json index 0c8b243f..914fff1c 100644 --- a/tests/unit/data/valid/metadata/Flow/delete_me.json +++ b/tests/unit/data/valid/metadata/Flow/delete_me.json @@ -1,7 +1,7 @@ { "Flow": { "delete_me": { - "Description": "", + "Description": "imported flow", "ExposedPorts": {}, "Info": "", "Label": "delete_me", diff --git a/tests/unit/data/valid/metadata/Node/delete_me.json b/tests/unit/data/valid/metadata/Node/delete_me.json index c65774ec..80f08dfb 100644 --- a/tests/unit/data/valid/metadata/Node/delete_me.json +++ b/tests/unit/data/valid/metadata/Node/delete_me.json @@ -1,7 +1,7 @@ { "Node": { "delete_me": { - "Info": null, + "Info": "imported node", "Label": "delete_me", "LastUpdate": { "date": "18/11/2022 at 15:30:55", diff --git a/tests/unit/with_db/scopes/test_node.py b/tests/unit/with_db/scopes/test_node.py index e16b98ad..668552f9 100644 --- a/tests/unit/with_db/scopes/test_node.py +++ b/tests/unit/with_db/scopes/test_node.py @@ -22,7 +22,7 @@ def test_node(self, global_db, metadata_folder): node = Node("delete_me") - assert node.Info is None + assert node.Info == "imported node" assert node.Label == "delete_me" assert node.User == "" assert hasattr(node, "LastUpdate") diff --git a/tests/unit/with_db/test_tools_backup.py b/tests/unit/with_db/test_tools_backup.py index 5d21f45c..cd8bc332 100644 --- a/tests/unit/with_db/test_tools_backup.py +++ b/tests/unit/with_db/test_tools_backup.py @@ -5,6 +5,30 @@ class TestToolsBackup: + """Test suite for the backup tool's import/export functionality. + + This test class covers core scope import/export functions available in data-access-layer: + - Callback (with .py file import) + - Configuration (with .yaml file import) + - Flow (with recursive dependency imports) + - Node (single and multiple imports) + - Package (recursive file imports) + - StateMachine + - Translation (with .po file import) + - Alert + + **Note**: Enterprise scopes (Annotation, GraphicScene, Layout, SharedDataEntry, SharedDataTemplate) + are tested in flow-initiator/tests/unit/with_db/test_enterprise_import_export.py since they require + movai_core_enterprise which is not available in data-access-layer. + + Additional scope-specific tests validate: + - Recursive dependency imports (Flow -> Node, Flow -> Subflow) + - Multiple imports in one operation + - Export validation and file comparison + - Manifest file processing + - Invalid data handling + """ + def test_import_manifest(self, global_db, metadata_folder, manifest_file): from dal.tools.backup import Importer @@ -53,8 +77,8 @@ def test_relative_import(self, global_db, metadata_folder, manifest_file): tool.run(objects) - def test_import_export_translation(self, global_db, metadata_folder, manifest_file, tmp_path): - """Test translation import and export.""" + def test_import_export_alert(self, global_db, metadata_folder, manifest_file, tmp_path): + """Test alert import and export.""" from dal.tools.backup import Importer, Exporter importer = Importer( @@ -66,7 +90,7 @@ def test_import_export_translation(self, global_db, metadata_folder, manifest_fi clean_old_data=True, ) - data = {"Translation": ["delete_me"]} + data = {"Alert": ["delete_me"]} importer.run(data) @@ -78,23 +102,20 @@ def test_import_export_translation(self, global_db, metadata_folder, manifest_fi exporter.run(data) - to_check = [ - "delete_me.json", - "delete_me.pt.po", - "delete_me.fr.po", - ] + imported_file = metadata_folder / "Alert" / "delete_me.json" + exported_file = tmp_path / "Alert" / "delete_me.json" + with open(imported_file, "r") as imported, open(exported_file, "r") as exported: + imported_content = json.load(imported) + exported_content = json.load(exported) - equal, diff, err = cmpfiles( - metadata_folder / "Translation", tmp_path / "Translation", to_check - ) - assert set(equal) == set(to_check) - assert not diff - assert not err + assert imported_content == exported_content - def test_import_export_alert(self, global_db, metadata_folder, manifest_file, tmp_path): - """Test alert import and export.""" + def test_import_export_callback(self, global_db, metadata_folder, tmp_path): + """Test callback import with .py file and export.""" from dal.tools.backup import Importer, Exporter + from dal.scopes.callback import Callback + # Import importer = Importer( metadata_folder, force=True, @@ -104,34 +125,175 @@ def test_import_export_alert(self, global_db, metadata_folder, manifest_file, tm clean_old_data=True, ) - data = {"Alert": ["delete_me"]} - + data = {"Callback": ["delete_me"]} importer.run(data) + # Validate imported data + callback = Callback("delete_me") + assert callback.Label == "delete_me" + assert callback.Code == 'print("hi")\n' + + # Export exporter = Exporter( tmp_path, debug=False, recursive=False, ) + exporter.run(data) + + # Validate files exist + assert (tmp_path / "Callback" / "delete_me.py").exists() + assert (tmp_path / "Callback" / "delete_me.json").exists() + + # Validate code content matches + with open(metadata_folder / "Callback" / "delete_me.py", "r") as original_code, open( + tmp_path / "Callback" / "delete_me.py", "r" + ) as exported_code: + assert original_code.read() == exported_code.read() + + # Validate JSON structure + with open(metadata_folder / "Callback" / "delete_me.json", "r") as original_json, open( + tmp_path / "Callback" / "delete_me.json", "r" + ) as callback_json: + original_content = json.load(original_json) + exported_content = json.load(callback_json) + assert original_content == exported_content + + def test_import_export_configuration(self, global_db, metadata_folder, tmp_path): + """Test configuration import with .yaml file and export.""" + from dal.tools.backup import Importer, Exporter + from dal.scopes.configuration import Configuration + # Import + importer = Importer( + metadata_folder, + force=True, + dry=False, + debug=False, + recursive=False, + clean_old_data=True, + ) + + data = {"Configuration": ["delete_me"]} + importer.run(data) + + # Validate imported data + config = Configuration("delete_me") + assert config.Label == "delete_me" + assert config.Yaml == "key: value\n" + + # Export + exporter = Exporter( + tmp_path, + debug=False, + recursive=False, + ) exporter.run(data) - imported_file = metadata_folder / "Alert" / "delete_me.json" - exported_file = tmp_path / "Alert" / "delete_me.json" - with open(imported_file, "r") as f1, open(exported_file, "r") as f2: - imported_content = json.load(f1) - exported_content = json.load(f2) + # Validate files exist + assert (tmp_path / "Configuration" / "delete_me.yaml").exists() + assert (tmp_path / "Configuration" / "delete_me.json").exists() + + # Validate YAML content matches + with open(metadata_folder / "Configuration" / "delete_me.yaml", "r") as original_yaml, open( + tmp_path / "Configuration" / "delete_me.yaml", "r" + ) as exported_yaml: + assert original_yaml.read() == exported_yaml.read() + + # Validate JSON structure + with open(metadata_folder / "Configuration" / "delete_me.json", "r") as original_json, open( + tmp_path / "Configuration" / "delete_me.json", "r" + ) as exported_json: + original_content = json.load(original_json) + exported_content = json.load(exported_json) + assert original_content == exported_content + + def test_import_export_flow(self, global_db, metadata_folder, tmp_path): + """Test flow import and export.""" + from dal.tools.backup import Importer, Exporter + from dal.scopes.flow import Flow + + # Import + importer = Importer( + metadata_folder, + force=True, + dry=False, + debug=False, + recursive=False, + clean_old_data=True, + ) + data = {"Flow": ["delete_me"]} + importer.run(data) + + # Validate imported data + flow = Flow("delete_me") + assert flow.Label == "delete_me" + assert flow.Description == "imported flow" + assert flow.ExposedPorts == {} + assert "delete_me" in flow.NodeInst + + # Export + exporter = Exporter( + tmp_path, + debug=False, + recursive=False, + ) + exporter.run(data) + + # Compare files + imported_file = metadata_folder / "Flow" / "delete_me.json" + exported_file = tmp_path / "Flow" / "delete_me.json" + with open(imported_file, "r") as imported, open(exported_file, "r") as exported: + imported_content = json.load(imported) + exported_content = json.load(exported) assert imported_content == exported_content - def test_import_invalid_data( - self, global_db, metadata_folder_invalid_data, manifest_file_invalid_data, capsys - ): - """Test import validates and reports invalid data.""" + def test_import_flow_with_dependencies(self, global_db, metadata_folder, tmp_path): + """Test flow import with recursive dependencies (nodes and subflows).""" from dal.tools.backup import Importer + from dal.scopes.flow import Flow + from dal.scopes.node import Node - tool = Importer( - metadata_folder_invalid_data, + # Import with recursive=True to import dependencies + importer = Importer( + metadata_folder, + force=True, + dry=False, + debug=False, + recursive=True, + clean_old_data=True, + ) + + data = {"Flow": ["flow_with_nodes_and_subflow"]} + importer.run(data) + + # Validate main flow imported + flow = Flow("flow_with_nodes_and_subflow") + assert flow.Label == "flow_with_nodes_and_subflow" + assert "pub" in flow.NodeInst + assert "sub" in flow.NodeInst + assert "subflow" in flow.Container + + # Validate node dependencies were imported + node_pub1 = Node("NodePub1") + assert node_pub1.Label == "NodePub1" + + node_sub1 = Node("NodeSub1") + assert node_sub1.Label == "NodeSub1" + + # Validate subflow dependency was imported + subflow = Flow("flow_with_duplicated_subflow") + assert subflow.Label == "flow_with_duplicated_subflow" + + def test_import_export_node(self, global_db, metadata_folder, tmp_path): + """Test node import and export.""" + from dal.tools.backup import Importer, Exporter + from dal.scopes.node import Node + + # Import + importer = Importer( + metadata_folder, force=True, dry=False, debug=False, @@ -139,16 +301,54 @@ def test_import_invalid_data( clean_old_data=True, ) - objects = tool.read_manifest(manifest_file_invalid_data) + data = {"Node": ["delete_me"]} + importer.run(data) - tool.run(objects) + # Validate imported data + node = Node("delete_me") + assert node.Label == "delete_me" + assert node.Info == "imported node" + assert "in" in node.PortsInst - captured = capsys.readouterr() - assert ( - "Failed to import Translation:delete_me - Invalid data for scope Translation" - in captured.out + # Export + exporter = Exporter( + tmp_path, + debug=False, + recursive=False, + ) + exporter.run(data) + + # Compare files + imported_file = metadata_folder / "Node" / "delete_me.json" + exported_file = tmp_path / "Node" / "delete_me.json" + with open(imported_file, "r") as imported, open(exported_file, "r") as exported: + imported_content = json.load(imported) + exported_content = json.load(exported) + assert imported_content == exported_content + + def test_import_node_multiple(self, global_db, metadata_folder, tmp_path): + """Test importing multiple nodes at once.""" + from dal.tools.backup import Importer + from dal.scopes.node import Node + + # Import + importer = Importer( + metadata_folder, + force=True, + dry=False, + debug=False, + recursive=False, + clean_old_data=True, ) + data = {"Node": ["NodePub1", "NodePub2", "NodeSub1", "NodeSub2", "UnusedNode"]} + importer.run(data) + + # Validate all nodes imported + for node_name in ["NodePub1", "NodePub2", "NodeSub1", "NodeSub2", "UnusedNode"]: + node = Node(node_name) + assert node.Label == node_name + def test_import_package(self, global_db, metadata_folder, metadata2_folder): """Test that consecutive imports merge package contents correctly.""" from dal.tools.backup import Importer @@ -186,3 +386,66 @@ def test_import_package(self, global_db, metadata_folder, metadata2_folder): "delete_me2.png", "delete_me2.yaml", } + + def test_import_export_translation(self, global_db, metadata_folder, manifest_file, tmp_path): + """Test translation import and export.""" + from dal.tools.backup import Importer, Exporter + + importer = Importer( + metadata_folder, + force=True, + dry=False, + debug=False, + recursive=False, + clean_old_data=True, + ) + + data = {"Translation": ["delete_me"]} + + importer.run(data) + + exporter = Exporter( + tmp_path, + debug=False, + recursive=False, + ) + + exporter.run(data) + + to_check = [ + "delete_me.json", + "delete_me.pt.po", + "delete_me.fr.po", + ] + + equal, diff, err = cmpfiles( + metadata_folder / "Translation", tmp_path / "Translation", to_check + ) + assert set(equal) == set(to_check) + assert not diff + assert not err + + def test_import_invalid_data( + self, global_db, metadata_folder_invalid_data, manifest_file_invalid_data, capsys + ): + """Test import validates and reports invalid data.""" + from dal.tools.backup import Importer + + tool = Importer( + metadata_folder_invalid_data, + force=True, + dry=False, + debug=False, + recursive=False, + clean_old_data=True, + ) + + objects = tool.read_manifest(manifest_file_invalid_data) + + tool.run(objects) + + captured = capsys.readouterr() + assert ( + "Failed to import Translation:delete_me - Invalid data for scope Translation" + in captured.out + )