From 2ee580e4700fa69ca0012ee266c324dfe9c94a40 Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Fri, 10 Apr 2026 12:57:15 -0400 Subject: [PATCH] test: add tests for `app_runmacro.py` --- news/add-test.rst | 23 +++++ src/diffpy/apps/app_runmacro.py | 15 +++- tests/test_runmacro.py | 151 +++++++++++++++++++++++++++++--- 3 files changed, 178 insertions(+), 11 deletions(-) create mode 100644 news/add-test.rst diff --git a/news/add-test.rst b/news/add-test.rst new file mode 100644 index 0000000..ddd7070 --- /dev/null +++ b/news/add-test.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: Add more tests fro app_runmacro.py. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/apps/app_runmacro.py b/src/diffpy/apps/app_runmacro.py index 22096fd..47330dd 100644 --- a/src/diffpy/apps/app_runmacro.py +++ b/src/diffpy/apps/app_runmacro.py @@ -10,7 +10,7 @@ grammar = r""" Program: commands*=Command - variable=VariableBlock + variable=VariableBlock? ; Command: @@ -86,9 +86,22 @@ def input_as_list(self, key, value): def load_command_processor(self, command): if command.component == "structure": # TODO: support multiple structures input in the future + if ( + self.inputs.get("initialize_structures.structure_paths") + is not None + ): + raise ValueError( + "Multiple structures are not supported by `runmacro` yet. " + "Please use python script instead." + ) key = "initialize_structures.structure_paths" variable = "structure" elif command.component == "profile": + if self.inputs.get("initialize_profile.profile_path") is not None: + raise ValueError( + "Multiple profiles are not supported by `runmacro`. " + "Please use python script instead." + ) key = "initialize_profile.profile_path" variable = "profile" else: diff --git a/tests/test_runmacro.py b/tests/test_runmacro.py index a33e437..a2545ba 100644 --- a/tests/test_runmacro.py +++ b/tests/test_runmacro.py @@ -1,18 +1,20 @@ from pathlib import Path import numpy +import pytest from helper import make_cmi_recipe from scipy.optimize import least_squares from diffpy.apps.app_runmacro import MacroParser +_STRUCTURE_PATH = str(Path(__file__).parent / "data" / "Ni.cif") +_PROFILE_PATH = str(Path(__file__).parent / "data" / "Ni.gr") + def test_meta_model(): # C1: Run the same fit with pdfadapter and diffpy_cmi # Expect the refined parameters to be the same within 1e-5 # diffpy_cmi fitting - structure_path = Path(__file__).parent / "data" / "Ni.cif" - profile_path = Path(__file__).parent / "data" / "Ni.gr" initial_pv_dict = { "s0": 0.4, "qdamp": 0.04, @@ -30,12 +32,7 @@ def test_meta_model(): "qbroad", ] diffpycmi_recipe = make_cmi_recipe( - str(structure_path), str(profile_path), initial_pv_dict - ) - structure_path = Path(__file__).parent / "data" / "Ni.cif" - profile_path = Path(__file__).parent / "data" / "Ni.gr" - diffpycmi_recipe = make_cmi_recipe( - str(structure_path), str(profile_path), initial_pv_dict + _STRUCTURE_PATH, _PROFILE_PATH, initial_pv_dict ) diffpycmi_recipe.fithooks[0].verbose = 0 diffpycmi_recipe.fix("all") @@ -51,8 +48,8 @@ def test_meta_model(): diffpy_pv_dict[pname] = parameter.value diffpy_dsl = f""" -load structure G1 from "{str(structure_path)}" -load profile exp_ni from "{str(profile_path)}" +load structure G1 from "{_STRUCTURE_PATH}" +load profile exp_ni from "{_PROFILE_PATH}" set G1 spacegroup as auto set exp_ni q_range as 0.1 25 @@ -78,3 +75,137 @@ def test_meta_model(): diffpy_value = diffpy_pv_dict[var_name] interpreter_value = interpreter_results["variables"][var_name]["value"] assert numpy.isclose(diffpy_value, interpreter_value, atol=1e-5) + + +@pytest.mark.parametrize( + "command_string, expected_inputs, expected_variables", + [ + # C1: load structure G1 from "path/to/structure.cif" + # Expect inputs and variables to be set correctly + ( + f'load structure G1 from "{_STRUCTURE_PATH}"', + { + "initialize_structures.structure_paths": _STRUCTURE_PATH, + "initialize_structures.names": ["G1"], + }, + {"G1": "structure"}, + ), + # C2: load profile exp_ni from "path/to/profile.gr" + # Expect inputs and variables to be set correctly + ( + f'load profile exp_ni from "{_PROFILE_PATH}"', + {"initialize_profile.profile_path": _PROFILE_PATH}, + {"exp_ni": "profile"}, + ), + # C3: create equation variables s0 + # Expect variable names to be stored + ( + "create equation variables s0", + {"add_contribution_variables.variable_names": ["s0"]}, + {}, + ), + # C4: save to "results.json" + # Expect result path to be stored + ( + 'save to "results.json"', + {"save_results.result_path": "results.json"}, + {}, + ), + # C5: set equation as "s0*G1" + # Expect equation to be stored + ( + 'set equation as "s0*G1"', + {"initialize_contribution.equation": ["s0*G1"]}, + {}, + ), + # C6: set exp_ni q_range as 0.1 25 + # Expect q_range to be stored + ( + f'load profile exp_ni from "{_PROFILE_PATH}"\n' + "set exp_ni q_range as 0.1 25", + {"initialize_profile.q_range": [0.1, 25]}, + {"exp_ni": "profile"}, + ), + # C7: set exp_ni calculation_range as 1.5 50 0.01 + # Expect calculation_range to be stored + ( + f'load profile exp_ni from "{_PROFILE_PATH}"\n' + "set exp_ni calculation_range as 1.5 50 0.01", + {"initialize_profile.calculation_range": [1.5, 50, 0.01]}, + {"exp_ni": "profile"}, + ), + # C8: set G1 spacegroup as auto + # Expect spacegroup to be stored + ( + f'load structure G1 from "{_STRUCTURE_PATH}"\n' + "set G1 spacegroup as auto", + {"initialize_structures.spacegroups": ["auto"]}, + {"G1": "structure"}, + ), + # C9: variables section with multiple variables + # Expect variable names and values to be stored + ( + """ +variables: +--- +- G1_a: 3.52 +- s0 +- G1_Uiso_0: 0.005 +--- +""", + { + "set_initial_variable_values.variable_name_to_value": { + "G1_a": 3.52, + "G1_Uiso_0": 0.005, + }, + "refine_variables.variable_names": ["G1_a", "s0", "G1_Uiso_0"], + }, + {}, + ), + ], +) +def test_command_processor( + command_string, expected_inputs, expected_variables +): + parser = MacroParser() + parser.parse(command_string) + for key, value in expected_inputs.items(): + assert parser.inputs[key] == value + assert dict(parser.variables) == expected_variables + + +@pytest.mark.parametrize( + "command_string, expected_exception, match", + [ + ( + f'load unknown foo from "{_STRUCTURE_PATH}"', + ValueError, + "Unknown component type: unknown " + "Please use 'structure' or 'profile'.", + ), + ( + 'load structure foo from "/nonexistent/path.cif"', + FileNotFoundError, + "structure /nonexistent/path.cif not found. " + "Please ensure the path is correct and the file exists.", + ), + ( + f'load structure G1 from "{_STRUCTURE_PATH}"\n' + f'load structure G2 from "{_STRUCTURE_PATH}"', + ValueError, + "Multiple structures are not supported by `runmacro` yet. " + "Please use python script instead.", + ), + ( + f'load profile p1 from "{_PROFILE_PATH}"\n' + f'load profile p2 from "{_PROFILE_PATH}"', + ValueError, + "Multiple profiles are not supported by `runmacro`. " + "Please use python script instead.", + ), + ], +) +def test_load_command_processor_bad(command_string, expected_exception, match): + parser = MacroParser() + with pytest.raises(expected_exception, match=match): + parser.parse(command_string)