diff --git a/tests/unit_tests/README.md b/tests/unit_tests/README.md
index e69de29b..7bfbb374 100644
--- a/tests/unit_tests/README.md
+++ b/tests/unit_tests/README.md
@@ -0,0 +1,60 @@
+# Unit Testing
+## HERON
+The `test_heron.py` file contains unit tests for the `FORCE/src/heron/create_componentsets_in_HERON()` function. These are designed to help developers identify bugs and isolate their causes. To this end, a brief overview of the most important unique scenarios each unit test addresses is listed below.
+
+### TestMinimalInput
+This test is designed to check main functionality with minimal edge cases. It checks that:
+- New Component node was added and updated with content, including:
+ - economics node
+ - capex CashFlow, with all attributes
+ - reference price
+ - reference driver, with conversion from kW to mW
+
+### TestExpandedInput1
+This test considers a more complex HERON input XML. It checks that:
+- Case node is transferred uncorrupted
+- Contents of multiple existing components that should not be updated are uncorrupted
+- New component's reference driver value is not converted if input is in mW
+- DataGenerators node is transferred uncorrupted
+
+### TestExpandedInput2
+This test focuses on ensuring correct updating of economics and CashFlow nodes and subnodes. It checks that:
+- Multiple components that need updating can be merged correctly with new components
+- Existing CashFlow of type non-capex is uncorrupted
+- capex CashFlow is added when a non-capex CashFlow exists but a capex CashFlow does not
+- economics node is found successfully when non-economics subnode of the component precedes the economics subnode
+- CashFlows are updated correctly when both a non-capex and a capex CashFlow exists
+- Non-CashFlow subnode of economics node is uncorrupted and CashFlow is still found
+- Existing capex CashFlow has
+ - Uncorrupted attributes
+ - Uncorrupted subnode that does not need updating
+ - All three existing subnodes that need updating correctly replaced
+- CashFlow subnodes are updated correctly when two of the three types that need updating exist
+
+### TestNoComponentsNode
+This test examines a single edge case where the Components node is missing from the HERON input XML script. It checks that:
+- A new Components node is created if one does not exist
+- The new Component node is placed within this new Components node
+
+### TestNoComponentNodes
+This test examines the edge case where a Components node exists, but no Component nodes. It checks that:
+- The new component is added, with content
+
+### TestMissingSubnodes
+This test considers scenarios where a component that needs to be updated is missing an economics or CashFlow subnode. It checks that:
+- If the economics node is missing, it is created with correct content
+- If the CashFlow node is missing, it is created with correct content
+
+### TestEmptyCompSetsFolder
+This test checks the function's behavior when the provided component set folder is empty. It checks that:
+- The open function was not called (e.g., no file was opened)
+- The existing component was not corrupted
+
+### TestCompSetsFolderWithBadJSON
+This test ensures a correct response to a component set file that is not is proper JSON format. It checks that:
+- The function throws a ValueError
+
+### TestCompSetsFolderMultFiles
+This test checks the function's filtering system regarding which component set files it should open. It checks that:
+- Only files whose names start with "componentSet" are opened
+- Only files of type .txt or .json are opened
\ No newline at end of file
diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py
index 75c15691..68320d57 100644
--- a/tests/unit_tests/test_heron.py
+++ b/tests/unit_tests/test_heron.py
@@ -1,61 +1,673 @@
+import os
+import sys
import unittest
-from unittest.mock import mock_open, patch, MagicMock
+from unittest.mock import mock_open, patch, call, MagicMock
import xml.etree.ElementTree as ET
+FORCE_LOC = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
+sys.path.append(FORCE_LOC)
from FORCE.src.heron import create_componentsets_in_HERON
-class TestCreateComponentSetsInHERON(unittest.TestCase):
-
- def setUp(self):
- # Example of a minimal XML structure
- self.heron_xml = """
-
-
-
-
- 100
-
-
-
-
- """
- self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
-
- @patch('xml.etree.ElementTree.parse')
- @patch('os.listdir')
- @patch('builtins.open',
- new_callable=mock_open,
- read_data="""{
- "Component Set Name": "NewComponent",
- "Reference Driver": 1000,
- "Reference Driver Power Units": "kW",
- "Reference Price (USD)": 2000,
- "Scaling Factor": 0.5
- }""")
- def test_new_component_creation(self, mock_file, mock_listdir, mock_parse):
- # Setup the mock to return an XML tree
- mock_parse.return_value = self.tree
- mock_listdir.return_value = ['componentSet1.json']
-
- # Call the function
- result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
-
- # Verify the XML was updated correctly
- components = result_tree.findall('.//Component[@name="NewComponent"]')
- self.assertEqual(len(components), 1)
- economics = components[0].find('economics')
- self.assertIsNotNone(economics)
-
- # Verify the CashFlow node was created
- cashflows = economics.findall('CashFlow')
+class HERONTestCase(unittest.TestCase):
+ """
+ Allows for the addition of more complex checks than the `assert` options in the unittest.TestCase class
+ """
+
+ def check_reference_price(self, cashflow, correct_value, correct_content_length=1):
+ """
+ Checks that the reference price of the given cashflow was correctly updated
+ @ In, cashflow, ET.Element, a xml node within which to check the reference price
+ @ In, correct_value, string, the expected value for the reference price's subnode
+ @ In, correct_content_length, int, optional, the expected total number of subnodes to
+ @ Out, None
+ """
+ ref_price = cashflow.findall('./reference_price')
+ self.assertEqual(len(ref_price), 1)
+ ref_price_contents = [e for e in ref_price[0].findall('./')
+ if not e.tag is ET.Comment] # Filters out ET.Comment elements
+ self.assertEqual(len(ref_price_contents), correct_content_length)
+ ref_price_value = ref_price[0].findall('./fixed_value')
+ self.assertEqual(ref_price_value[0].text, correct_value)
+
+ def check_reference_driver(self, cashflow, correct_value, correct_content_length=1):
+ """
+ Checks that the reference driver of the given cashflow was correctly updated
+ @ In, cashflow, ET.Element, a xml node within which to check the reference driver
+ @ In, correct_value, string, the expected value for the reference driver's subnode
+ @ In, correct_content_length, int, optional, the expected total number of subnodes to
+ @ Out, None
+ """
+ ref_driver = cashflow.findall('./reference_driver')
+ self.assertEqual(len(ref_driver), 1)
+ ref_driver_contents = [e for e in ref_driver[0].findall('./')
+ if not e.tag is ET.Comment] # Filters out ET.Comment elements
+ self.assertEqual(len(ref_driver_contents), correct_content_length)
+ ref_driver_value = ref_driver[0].findall('./fixed_value')
+ self.assertEqual(ref_driver_value[0].text, correct_value)
+
+ def check_scaling_factor(self, cashflow, correct_value, correct_content_length=1):
+ """
+ Checks that the scaling factor of the given cashflow was correctly updated
+ @ In, cashflow, ET.Element, a xml node within which to check the scaling factor
+ @ In, correct_value, string, the expected value for the scaling factor's subnode
+ @ In, correct_content_length, int, optional, the expected total number of subnodes to
+ @ Out, None
+ """
+ scaling_factor = cashflow.findall('./scaling_factor_x')
+ self.assertEqual(len(scaling_factor), 1)
+ scaling_factor_contents = [e for e in scaling_factor[0].findall('./')
+ if not e.tag is ET.Comment] # Filters out ET.Comment elements
+ self.assertEqual(len(scaling_factor_contents), correct_content_length)
+ scaling_factor_value = scaling_factor[0].findall('./fixed_value')
+ self.assertEqual(scaling_factor_value[0].text, correct_value)
+
+class TestMinimalInput(HERONTestCase):
+
+ def setUp(self):
+ # Example of a minimal XML structure
+ self.heron_xml = """
+
+
+
+
+
+
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ @patch('builtins.open',
+ new_callable=mock_open,
+ read_data="""{
+ "Component Set Name": "NewComponent",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "kW",
+ "Reference Price (USD)": 2000,
+ "Scaling Factor": 0.5
+ }""")
+ def test_minimal_input(self, mock_file, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ mock_listdir.return_value = ['componentSetFake.json']
+
+ # Call the function
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+
+ # Verify the XML was updated correctly
+ new_components = result_tree.findall('.//Component[@name="NewComponent"]')
+ self.assertEqual(len(new_components), 1)
+ economics = new_components[0].find('economics')
+ self.assertIsNotNone(economics)
+
+ # Verify the CashFlow node was created with attribs
+ cashflows = economics.findall('CashFlow')
+ self.assertEqual(len(cashflows), 1)
+ self.assertEqual(cashflows[0].attrib['name'], 'NewComponent_capex')
+ self.assertIn('type', cashflows[0].keys())
+ self.assertIn('taxable', cashflows[0].keys())
+ self.assertIn('inflation', cashflows[0].keys())
+
+ # Verify reference price and reference driver
+ self.check_reference_driver(cashflows[0], '1.0')
+ self.check_reference_price(cashflows[0], '-2000')
+
+class TestExpandedInput1(HERONTestCase):
+
+ def setUp(self):
+ # Added case and datagenerator nodes (should be transferred blindly) and extra components
+ self.heron_xml = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ @patch('builtins.open',
+ new_callable=mock_open,
+ read_data="""{
+ "Component Set Name": "NewComponent",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 2000,
+ "Scaling Factor": 0.5
+ }""")
+ def test_expanded_input_1(self, mock_open, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ # No additional data for XML
+ mock_listdir.return_value = ["componentSetFake.json"]
+
+ # Call the function
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+
+ # Verify Case node was transferred
+ with self.subTest("Case node has been corrupted"):
+ cases = result_tree.findall('./Case')
+ self.assertEqual(len(cases), 1)
+ self.assertIsNotNone(cases[0].find('./untouched_content_Case'))
+
+ # Verify Component nodes were merged
+ component_nodes = result_tree.findall("./Components/Component")
+ self.assertEqual(len(component_nodes), 3)
+
+ for comp in component_nodes:
+ cashflows = comp.findall('./economics/CashFlow')
+ if comp.get('name') == 'ExistingComponent0':
+ # Verify CashFlow with type
+ self.assertEqual(len(cashflows), 1)
+ self.assertEqual(cashflows[0].attrib['type'], 'one-time')
+ elif comp.get('name') == 'ExistingComponent1':
+ # Verify CashFlow with type
+ self.assertEqual(len(cashflows), 1)
+ self.assertEqual(cashflows[0].attrib['type'], 'repeating')
+ elif comp.get('name') == 'NewComponent':
+ # Verify reference driver
+ self.check_reference_driver(cashflows[0], '1000')
+
+ # Verify DataGenerators node was transferred
+ with self.subTest("DataGenerators node has been corrupted"):
+ data_gens = result_tree.findall('./DataGenerators')
+ self.assertEqual(len(data_gens), 1)
+ self.assertIsNotNone(data_gens[0].find('./untouched_content_DG'))
+
+class TestExpandedInput2(HERONTestCase):
+
+ def setUp(self):
+ # Complex subnodes to each component with various and positions and configurations
+ self.heron_xml = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ 1234
+
+
+ more_rom_stuff
+
+
+
+
+ 8
+ 0
+
+
+
+
+ 1234
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ def test_expanded_input_2(self, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ # No additional data for XML
+ mock_listdir.return_value = ["componentSetFake0.json", "componentSetFake1.json", "componentSetFake2.json", "componentSetFake3.json"]
+
+ # Set up the open mock to return different files each time it's used
+ mock_open_mult = mock_open()
+ mock_open_mult.side_effect = [mock_open(read_data =
+ """{
+ "Component Set Name": "Component0",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 1000,
+ "Scaling Factor": 0.1
+ }""").return_value,
+ mock_open(read_data =
+ """{
+ "Component Set Name": "Component1",
+ "Reference Driver": 2100,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 2200,
+ "Scaling Factor": 0.2
+ }""").return_value,
+ mock_open(read_data =
+ """{
+ "Component Set Name": "Component2",
+ "Reference Driver": 3100,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 3200,
+ "Scaling Factor": 0.3
+ }""").return_value,
+ mock_open(read_data =
+ """{
+ "Component Set Name": "Component3",
+ "Reference Driver": 4100,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 4200,
+ "Scaling Factor": 0.4
+ }""").return_value]
+
+
+ # Call the function with patch for open function
+ with patch('builtins.open', mock_open_mult):
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+
+ # Verify Component nodes were merged
+ component_nodes = result_tree.findall("./Components/Component")
+ self.assertEqual(len(component_nodes), 4)
+
+ for comp in component_nodes:
+ economics = comp.findall('./economics')
+ self.assertEqual(len(economics), 1)
+
+ if comp.get('name') == 'Component0':
+ # Verify non-capex cashflow was not corrupted
+ with self.subTest("Non-capex CashFlow node was corrupted"):
+ cashflow_non_capex = economics[0].findall('./CashFlow[@name="other"]')
+ self.assertEqual(len(cashflow_non_capex), 1)
+ cf_noncap_contents = [e for e in cashflow_non_capex[0].findall('./')
+ if not e.tag is ET.Comment] # Filters out ET.Comment elements
+ self.assertEqual(len(cf_noncap_contents), 1)
+ self.assertIsNotNone(cf_noncap_contents[0].tag, 'untouched_content')
+
+ # Verify capex cashflow was added
+ with self.subTest("capex CashFlow was not added correctly"):
+ cashflow_capex = economics[0].findall('./CashFlow[@name="Component0_capex"]')
+ self.assertEqual(len(cashflow_capex), 1)
+
+ elif comp.get('name') == 'Component1':
+ # Verify number of cashflows
+ self.assertEqual(len(economics[0].findall('./CashFlow')), 2)
+
+ # Verify non-capex cashflow was not corrupted
+ with self.subTest("Non-capex CashFlow node was corrupted"):
+ cashflow_non_capex = economics[0].findall('./CashFlow[@name="other"]')
+ self.assertEqual(len(cashflow_non_capex), 1)
+ self.assertEqual(cashflow_non_capex[0].attrib['type'], 'one-time')
+ cashflow_noncap_contents = [e for e in cashflow_non_capex[0].findall('./')
+ if not e.tag is ET.Comment] # Filters out ET.Comment elements
+ self.assertEqual(len(cashflow_noncap_contents), 0)
+
+ # Verify capex cashflow was updated
+ with self.subTest("capex CashFlow node was not updated correctly"):
+ cashflow_capex = economics[0].findall('./CashFlow[@name="capex"]')
+ self.assertEqual(len(cashflow_capex), 1)
+ self.check_reference_price(cashflow_capex[0], '-2200')
+
+ elif comp.get('name') == 'Component2':
+ # Verify lifetime was not corrupted
+ with self.subTest("Non-cashflow child node of economics has been corrupted"):
+ proj_time = economics[0].findall('./lifetime')
+ self.assertEqual(len(proj_time), 1)
+ self.assertEqual(proj_time[0].text, '1')
+
+ # Verify cashflow merging
+ cashflows = economics[0].findall('./CashFlow')
self.assertEqual(len(cashflows), 1)
- self.assertEqual(cashflows[0].attrib['name'], 'NewComponent_capex')
- # Verify the reference driver and price updates
- ref_driver = cashflows[0].find('./reference_driver/fixed_value')
- self.assertEqual(ref_driver.text, '1.0') # The driver should have been converted from kW to MW
+ # Verify cashflow was updated correctly
+
+ # Attributes
+ with self.subTest("Attributes of CashFlow were corrupted"):
+ self.assertIn('npv_exempt', cashflows[0].keys())
+
+ # Children
+ with self.subTest("CashFlow children nodes were not updated correctly"):
+ cf_contents = [e for e in cashflows[0].findall('./')
+ if not e.tag is ET.Comment] # Filters out ET.Comment elements
+ self.assertEqual(len(cf_contents), 4)
+
+ self.check_reference_driver(cashflows[0], '3100')
+ self.check_reference_price(cashflows[0], '-3200')
+ self.check_scaling_factor(cashflows[0], '0.3')
+
+ with self.subTest("Existing CashFlow child node was corrupted"):
+ # Driver node
+ driver = cashflows[0].findall('./driver')
+ self.assertEqual(len(driver), 1)
+ self.assertIsNotNone(driver[0].findall('fixed_value'))
+
+ elif comp.get('name') == 'Component3':
+ cashflows = comp.findall('./economics/CashFlow')
+ self.assertEqual(len(cashflows), 1)
+
+ # Verify cashflow was updated correctly
+ with self.subTest("CashFlow children nodes were not updated correctly"):
+ cf_contents = [e for e in cashflows[0].findall('./')
+ if not e.tag is ET.Comment] # Filters out ET.Comment elements
+ self.assertEqual(len(cf_contents), 3)
+
+ self.check_reference_driver(cashflows[0], '4100')
+ self.check_reference_price(cashflows[0], '-4200')
+ self.check_scaling_factor(cashflows[0], '0.4')
+
+class TestNoComponentsNode(HERONTestCase):
+
+ def setUp(self):
+ # Has no Components node
+ self.heron_xml = """
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ @patch('builtins.open',
+ new_callable=mock_open,
+ read_data="""{
+ "Component Set Name": "NewComponent",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 2000,
+ "Scaling Factor": 0.5
+ }""")
+ def test_no_comps_node(self, mock_file, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ mock_listdir.return_value = ['componentSetFake.json']
+
+ # Call the function
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+
+ # Verify components node was created with contents
+ components = result_tree.findall('./Components')
+ self.assertEqual(len(components), 1)
+ self.assertEqual(len(components[0].findall('./Component[@name="NewComponent"]')), 1)
+
+class TestNoComponentNodes(HERONTestCase):
+
+ def setUp(self):
+ # Has no Component nodes
+ self.heron_xml = """
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ @patch('builtins.open',
+ new_callable=mock_open,
+ read_data="""{
+ "Component Set Name": "NewComponent",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "kW",
+ "Reference Price (USD)": 2000,
+ "Scaling Factor": 0.5
+ }""")
+ def test_no_comp_nodes(self, mock_file, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ mock_listdir.return_value = ['componentSetFake.json']
+
+ # Call the function
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+
+ # Verify component node was created
+ component_list = result_tree.findall('./Components/Component')
+ self.assertEqual(len(component_list), 1)
+ self.assertEqual(component_list[0].attrib['name'], 'NewComponent')
+
+ # Verify contents have been added
+ self.assertIsNotNone(component_list[0].findall('./economics/CashFlow'))
+
+class TestMissingSubnodes(HERONTestCase):
+
+ def setUp(self):
+ # Comp0 has no economics subnode; Comp1 has no CashFlow subnode
+ self.heron_xml = """
+
+
+
+
+
+
+
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ def test_missing_subnodes(self, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ mock_listdir.return_value = ['componentSetFake0.json', 'componentSetFake1.json']
+
+ # Set up the open mock to return different files each time it's used
+ mock_open_mult = mock_open()
+ mock_open_mult.side_effect = [mock_open(read_data =
+ """{
+ "Component Set Name": "Component0",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 1000,
+ "Scaling Factor": 0.1
+ }""").return_value,
+ mock_open(read_data =
+ """{
+ "Component Set Name": "Component1",
+ "Reference Driver": 2000,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 2000,
+ "Scaling Factor": 0.2
+ }""").return_value]
+
+ # Call the function with patch for open function
+ with patch('builtins.open', mock_open_mult):
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+
+ # Verify component nodes
+ comp0 = result_tree.findall('./Components/Component[@name="Component0"]')
+ self.assertEqual(len(comp0), 1)
+ comp1 = result_tree.findall('./Components/Component[@name="Component1"]')
+ self.assertEqual(len(comp1), 1)
+
+ # Verify comp0 updated correctly
+ with self.subTest("economics node and subnodes were not added correctly"):
+ economics = comp0[0].findall('./economics')
+ self.assertEqual(len(economics), 1)
+ cashflows = economics[0].findall('./CashFlow')
+ self.assertEqual(len(cashflows), 1)
+ self.check_reference_driver(cashflows[0], '1000')
+
+ # Verify comp1 updated correctly
+ with self.subTest("cashflow node and subnodes were not added correctly"):
+ cashflows = comp1[0].findall('./economics/CashFlow')
+ self.assertEqual(len(cashflows), 1)
+ self.check_reference_driver(cashflows[0], '2000')
+
+class TestEmptyCompSetsFolder(HERONTestCase):
+ def setUp(self):
+ # Example of a minimal XML structure
+ self.heron_xml = """
+
+
+
+
+
+
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ # This mock_open should not be called in the function
+ @patch('builtins.open',
+ new_callable=mock_open,
+ read_data="""{
+ "Component Set Name": "NewComponent",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "kW",
+ "Reference Price (USD)": 2000,
+ "Scaling Factor": 0.5
+ }""")
+ def test_empty_compsets_folder(self, mock_open, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ mock_listdir.return_value = []
+
+ # Call the function
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+
+ # Verify open function was not called
+ mock_open.assert_not_called()
+
+ components_nodes = result_tree.findall('./Components')
+
+ # Verify component node was not corrupted
+ component_nodes = components_nodes[0].findall('./Component')
+ self.assertEqual(len(component_nodes), 1)
+ self.assertEqual(component_nodes[0].attrib['name'], 'ExistingComponent')
+
+class TestCompSetsFolderWithBadJSON(HERONTestCase):
+ def setUp(self):
+ # Example of a minimal XML structure
+ self.heron_xml = """
+
+
+
+
+
+
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ @patch('builtins.open',
+ new_callable=mock_open,
+ read_data="Example of bad format")
+ def test_compsets_folder_bad_json(self, mock_open, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ mock_listdir.return_value = ['componentSetFake.json']
+
+ # Call the function and check for error
+ caught_bad_json = False
+ try:
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
+ except ValueError:
+ caught_bad_json = True
+
+ with self.subTest("Did not respond correctly to bad component set file content"):
+ self.assertEqual(caught_bad_json, True)
+
+class TestCompSetsFolderMultFiles(HERONTestCase):
+ def setUp(self):
+ # Example of a minimal XML structure
+ self.heron_xml = """
+
+
+
+
+
+
+
+
+ """
+ self.tree = ET.ElementTree(ET.fromstring(self.heron_xml))
+
+ @patch('xml.etree.ElementTree.parse')
+ @patch('os.listdir')
+ # Open mock will return the same read_data each time it is called
+ # This is acceptable only because the content of the result tree is untested
+ @patch('builtins.open',
+ new_callable=mock_open,
+ read_data="""{
+ "Component Set Name": "NewComponent",
+ "Reference Driver": 1000,
+ "Reference Driver Power Units": "mW",
+ "Reference Price (USD)": 2000,
+ "Scaling Factor": 0.5
+ }""")
+ def test_compsets_folder_mult_files(self, mock_open, mock_listdir, mock_parse):
+ # Set up the parse mock to return an XML tree
+ mock_parse.return_value = self.tree
+ files_list = ['component.json', 'README', 'componentSet.csv', 'xcomponentSet.json',
+ 'componentSet.json', 'componentSetStuff.txt',
+ 'aFolder', 'Set.json', 'compSet.json', 'ComponentSet.json']
+ mock_listdir.return_value = files_list
+ # Only the txt and json files whose names start with 'componentSet' should be opened
+ acceptable_files = ['componentSet.json', 'componentSetStuff.txt']
+
+ # Call the function
+ result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml")
- # Add more tests here to cover other conditions and edge cases
+ # Verify open function was called on correct files
+ for file in files_list:
+ # if file should have been opened
+ if file in acceptable_files:
+ with self.subTest(msg="File was not opened and should have been", file = file):
+ # Verify file was opened
+ self.assertIn(call('/fake/folder/'+file), mock_open.call_args_list)
+ # if file should not have been opened
+ else:
+ with self.subTest(msg="File was opened and should not have been", file = file):
+ # Verify file was not opened
+ self.assertNotIn(call('/fake/folder/'+file), mock_open.call_args_list)
+# This is not needed for running tests through FORCE/run_tests
+# It does allow tests to be run via the unit tester when test_heron is run directly
if __name__ == '__main__':
- unittest.main()
+ unittest.main()
diff --git a/tests/unit_tests/tests b/tests/unit_tests/tests
new file mode 100644
index 00000000..4a340f4e
--- /dev/null
+++ b/tests/unit_tests/tests
@@ -0,0 +1,46 @@
+[Tests]
+ [./TestMinimalInput]
+ type = RavenPython
+ input = '-m unittest test_heron.TestMinimalInput'
+ [../]
+
+ [./TestExpandedInput1]
+ type = RavenPython
+ input = '-m unittest test_heron.TestExpandedInput1'
+ [../]
+
+ [./TestExpandedInput2]
+ type = RavenPython
+ input = '-m unittest test_heron.TestExpandedInput2'
+ [../]
+
+ [./TestNoComponentsNode]
+ type = RavenPython
+ input = '-m unittest test_heron.TestNoComponentsNode'
+ [../]
+
+ [./TestNoComponentNodes]
+ type = RavenPython
+ input = '-m unittest test_heron.TestNoComponentNodes'
+ [../]
+
+ [./TestMissingSubnodes]
+ type = RavenPython
+ input = '-m unittest test_heron.TestMissingSubnodes'
+ [../]
+
+ [./TestEmptyCompSetsFolder]
+ type = RavenPython
+ input = '-m unittest test_heron.TestEmptyCompSetsFolder'
+ [../]
+
+ [./TestCompSetsFolderWithBadJSON]
+ type = RavenPython
+ input = '-m unittest test_heron.TestCompSetsFolderWithBadJSON'
+ [../]
+
+ [./TestCompSetsFolderMultFiles]
+ type = RavenPython
+ input = '-m unittest test_heron.TestCompSetsFolderMultFiles'
+ [../]
+[]