From d63713b23eef63917fe71d6941a6517e9ab3cfb4 Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Wed, 19 Jun 2024 08:18:24 -0600 Subject: [PATCH 1/8] Expanded unit tests --- tests/unit_tests/test_heron.py | 189 ++++++++++++++++++++++++--------- tests/unit_tests/tests | 11 ++ 2 files changed, 149 insertions(+), 51 deletions(-) create mode 100644 tests/unit_tests/tests diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py index 75c15691..c57cb79d 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -1,61 +1,148 @@ +import os +import sys import unittest from unittest.mock import mock_open, patch, 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') - self.assertEqual(len(cashflows), 1) - self.assertEqual(cashflows[0].attrib['name'], 'NewComponent_capex') +class TestMinimalInput(unittest.TestCase): + + 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 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 + cashflows = economics.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 + +class TestExpandedInput(unittest.TestCase): + + def setUp(self): + # Added case and datagenerator nodes (should be transferred blindly) and extra components + self.heron_xml = """ + + + + + + + + + + + + - # 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 + + + + + + + + + + + + + """ + 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(self, mock_open, mock_listdir, mock_parse): + # Set up the 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 + componentNodes = result_tree.findall("./Components/Component") + self.assertEqual(len(componentNodes), 3) + + for comp in componentNodes: + if comp.get('name') == 'ExistingComponent0': + # Verify CashFlow with type + cashflows = comp.findall('./economics/CashFlow') + self.assertEqual(len(cashflows), 1) + self.assertEqual(cashflows[0].attrib['type'], 'one-time') + elif comp.get('name') == 'ExistingComponent1': + # Verify CashFlow with type + cashflows = comp.findall('./economics/CashFlow') + self.assertEqual(len(cashflows), 1) + self.assertEqual(cashflows[0].attrib['type'], 'repeating') + elif comp.get('name') == 'NewComponent': + # Verify reference driver + ref_driver = comp.find('./economics/CashFlow/reference_driver/fixed_value') + self.assertEqual(ref_driver.text, '1000') # Check that mW were not converted + break - # Add more tests here to cover other conditions and edge cases + # Verify DataGenerators node was transferred + with (self.subTest("DataGenerators node has been corrupted")): + dataGens = result_tree.findall('./DataGenerators') + self.assertEqual(len(dataGens), 1) + self.assertIsNotNone(dataGens[0].find('./untouched_content_DG')) 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..980dfbdc --- /dev/null +++ b/tests/unit_tests/tests @@ -0,0 +1,11 @@ +[Tests] + [./TestMinimalInput] + type = GenericExecutable + executable = python test_heron.py + [../] + + [./TestExpandedInput] + type = GenericExecutable + executable = python test_heron.py + [../] +[] \ No newline at end of file From ed39f73faee03eddd5d2528b99a391fe0bcc10dd Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Wed, 19 Jun 2024 19:54:12 -0600 Subject: [PATCH 2/8] Added unit tests for inputs with various missing nodes --- tests/unit_tests/test_heron.py | 140 +++++++++++++++++++++++++++++++++ tests/unit_tests/tests | 17 +++- 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py index c57cb79d..c0df4bc2 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -144,5 +144,145 @@ def test_expanded_input(self, mock_open, mock_listdir, mock_parse): self.assertEqual(len(dataGens), 1) self.assertIsNotNone(dataGens[0].find('./untouched_content_DG')) +@unittest.skip("Waiting for function update") +class TestNoComponentsNode(unittest.TestCase): + + 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 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") + + # FIXME: Check result + +class TestNoComponentNodes(unittest.TestCase): + + 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 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(unittest.TestCase): + + 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 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_twice = unittest.mock.mock_open() + mock_open_twice.side_effect = [ + unittest.mock.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, + unittest.mock.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 unittest.mock.patch('builtins.open', mock_open_twice): + 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.assertEqual(cashflows[0].attrib["name"], "Component0_capex") + ref_driver = cashflows[0].find('./reference_driver/fixed_value') + self.assertEqual(ref_driver.text, "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.assertEqual(cashflows[0].attrib["name"], "Component1_capex") + ref_driver = cashflows[0].find('./reference_driver/fixed_value') + self.assertEqual(ref_driver.text, "2000") + if __name__ == '__main__': unittest.main() diff --git a/tests/unit_tests/tests b/tests/unit_tests/tests index 980dfbdc..39974377 100644 --- a/tests/unit_tests/tests +++ b/tests/unit_tests/tests @@ -8,4 +8,19 @@ type = GenericExecutable executable = python test_heron.py [../] -[] \ No newline at end of file + + [./TestNoComponentsNode] + type = GenericExecutable + executable = python test_heron.py + [../] + + [./TestNoComponentNodes] + type = GenericExecutable + executable = python test_heron.py + [../] + + [./TestMissingSubnodes] + type = GenericExecutable + executable = python test_heron.py + [../] +[] From 4115ee94325b3391625aaf536ff6d1096b521b7b Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Mon, 24 Jun 2024 11:22:57 -0600 Subject: [PATCH 3/8] Further expanded unit tests --- tests/unit_tests/test_heron.py | 294 ++++++++++++++++++++++++++++++--- tests/unit_tests/tests | 17 +- 2 files changed, 284 insertions(+), 27 deletions(-) diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py index c0df4bc2..f7a7df8a 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -22,7 +22,7 @@ def setUp(self): """ - self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) + self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) @patch('xml.etree.ElementTree.parse') @patch('os.listdir') @@ -49,16 +49,24 @@ def test_minimal_input(self, mock_file, mock_listdir, mock_parse): economics = new_components[0].find('economics') self.assertIsNotNone(economics) - # Verify the CashFlow node was created + # 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()) + self.assertIn('mult_target', cashflows[0].keys()) + + # Verify reference price + ref_price_value = cashflows[0].find('./reference_price/fixed_value') + self.assertEqual(ref_price_value.text, '-2000') # 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 + ref_driver_value = cashflows[0].find('./reference_driver/fixed_value') + self.assertEqual(ref_driver_value.text, '1.0') # The driver should have been converted from kW to MW -class TestExpandedInput(unittest.TestCase): +class TestExpandedInput1(unittest.TestCase): def setUp(self): # Added case and datagenerator nodes (should be transferred blindly) and extra components @@ -102,7 +110,7 @@ def setUp(self): "Reference Price (USD)": 2000, "Scaling Factor": 0.5 }""") - def test_expanded_input(self, mock_open, mock_listdir, mock_parse): + def test_expanded_input_1(self, mock_open, mock_listdir, mock_parse): # Set up the mock to return an XML tree mock_parse.return_value = self.tree # No additional data for XML @@ -118,10 +126,10 @@ def test_expanded_input(self, mock_open, mock_listdir, mock_parse): self.assertIsNotNone(cases[0].find('./untouched_content_Case')) # Verify Component nodes were merged - componentNodes = result_tree.findall("./Components/Component") - self.assertEqual(len(componentNodes), 3) + component_nodes = result_tree.findall("./Components/Component") + self.assertEqual(len(component_nodes), 3) - for comp in componentNodes: + for comp in component_nodes: if comp.get('name') == 'ExistingComponent0': # Verify CashFlow with type cashflows = comp.findall('./economics/CashFlow') @@ -136,13 +144,252 @@ def test_expanded_input(self, mock_open, mock_listdir, mock_parse): # Verify reference driver ref_driver = comp.find('./economics/CashFlow/reference_driver/fixed_value') self.assertEqual(ref_driver.text, '1000') # Check that mW were not converted - break # Verify DataGenerators node was transferred with (self.subTest("DataGenerators node has been corrupted")): - dataGens = result_tree.findall('./DataGenerators') - self.assertEqual(len(dataGens), 1) - self.assertIsNotNone(dataGens[0].find('./untouched_content_DG')) + data_gens = result_tree.findall('./DataGenerators') + self.assertEqual(len(data_gens), 1) + self.assertIsNotNone(data_gens[0].find('./untouched_content_DG')) + +class TestExpandedInput2(unittest.TestCase): + + 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 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: + if comp.get('name') == 'Component0': + economics = comp.find('./economics') + + # Verify non-capex cashflow was not corrupted + with self.subTest("Non-capex CashFlow node was corrupted"): + cashflow_non_capex = economics.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.findall('./CashFlow[@name="Component0_capex"]') + self.assertEqual(len(cashflow_capex), 1) + + elif comp.get('name') == 'Component1': + economics = comp.findall('./economics') + self.assertEqual(len(economics), 1) + + # 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) + ref_price = cashflow_capex[0].findall('./reference_price/fixed_value') + self.assertEqual(ref_price[0].text, '-2200') + + elif comp.get('name') == 'Component2': + economics = comp.findall('./economics') + + # Verify ProjectTime was not corrupted + with self.subTest("Non-cashflow child node of economics has been corrupted"): + proj_time = economics[0].findall('./ProjectTime') + self.assertEqual(len(proj_time), 1) + self.assertEqual(proj_time[0].text, '1') + + # Verify cashflow merging + cashflow = economics[0].findall('./CashFlow') + self.assertEqual(len(cashflow), 1) + + # Verify cashflow was updated correctly + + # Attributes + with self.subTest("Attributes of CashFlow were corrupted"): + self.assertIn('npv_exempt', cashflow[0].keys()) + + # Children + with self.subTest("CashFlow children nodes were not updated correctly"): + cf_contents = [e for e in cashflow[0].findall('./') + if not e.tag is ET.Comment] # Filters out ET.Comment elements + self.assertEqual(len(cf_contents), 4) + + # Reference driver + ref_driver = cashflow[0].findall('./reference_driver') + self.assertEqual(len(ref_driver), 1) + ref_driver_value = ref_driver[0].findall('./fixed_value') + self.assertEqual(ref_driver_value.text, '3100') + + # Reference price + ref_price = cashflow[0].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), 1) + ref_price_value = ref_price[0].findall('./fixed_value') + self.assertEqual(ref_price_value[0].text, '-3200') + + # Scaling factor + scaling_factor = cashflow[0].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), 1) + scaling_factor_value = scaling_factor[0].findall('./fixed_value') + self.assertEqual(scaling_factor_value[0].text, '0.3') + + with self.subTest("Existing CashFlow child node was corrupted"): + # Driver node + driver = cashflow[0].findall('./driver') + self.assertEqual(len(driver), 1) + self.assertIsNotNone(driver[0].findall('fixed_value')) + + elif comp.get('name') == 'Component3': + cashflow = comp.findall('./economics/CashFlow') + self.assertEqual(len(cashflow), 1) + + # Verify cashflow was updated correctly + with self.subTest("CashFlow children nodes were not updated correctly"): + cf_contents = [e for e in cashflow[0].findall('./') + if not e.tag is ET.Comment] # Filters out ET.Comment elements + self.assertEqual(len(cf_contents), 3) + + # Reference driver + ref_driver = cashflow[0].findall('./reference_driver') + self.assertEqual(len(ref_driver), 1) + ref_driver_value = ref_driver[0].findall('./fixed_value') + self.assertEqual(ref_driver_value.text, '4100') + + # Reference price + ref_price = cashflow[0].findall('./reference_price') + self.assertEqual(len(ref_price), 1) + ref_price_value = ref_price[0].findall('./fixed_value') + self.assertEqual(ref_price_value[0].text, '4200') + + # Scaling factor + scaling_factor = cashflow[0].findall('./scaling_factor_x') + self.assertEqual(len(scaling_factor), 1) + scaling_factor_value = scaling_factor[0].findall('./fixed_value') + self.assertEqual(scaling_factor_value[0].text, '0.4') @unittest.skip("Waiting for function update") class TestNoComponentsNode(unittest.TestCase): @@ -151,7 +398,7 @@ def setUp(self): # Has no Components node self.heron_xml = """ """ - self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) + self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) @patch('xml.etree.ElementTree.parse') @patch('os.listdir') @@ -172,7 +419,10 @@ def test_no_comps_node(self, mock_file, mock_listdir, mock_parse): # Call the function result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") - # FIXME: Check result + # 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(unittest.TestCase): @@ -182,7 +432,7 @@ def setUp(self): """ - self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) + self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) @patch('xml.etree.ElementTree.parse') @patch('os.listdir') @@ -226,7 +476,7 @@ def setUp(self): """ - self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) + self.tree = ET.ElementTree(ET.fromstring(self.heron_xml)) @patch('xml.etree.ElementTree.parse') @patch('os.listdir') @@ -236,9 +486,9 @@ def test_missing_subnodes(self, mock_listdir, mock_parse): mock_listdir.return_value = ['componentSetFake0.json', 'componentSetFake1.json'] # Set up the open mock to return different files each time it's used - mock_open_twice = unittest.mock.mock_open() + mock_open_twice = mock_open() mock_open_twice.side_effect = [ - unittest.mock.mock_open(read_data = + mock_open(read_data = """{ "Component Set Name": "Component0", "Reference Driver": 1000, @@ -246,7 +496,7 @@ def test_missing_subnodes(self, mock_listdir, mock_parse): "Reference Price (USD)": 1000, "Scaling Factor": 0.1 }""").return_value, - unittest.mock.mock_open(read_data = + mock_open(read_data = """{ "Component Set Name": "Component1", "Reference Driver": 2000, @@ -257,7 +507,7 @@ def test_missing_subnodes(self, mock_listdir, mock_parse): ] # Call the function with patch for open function - with unittest.mock.patch('builtins.open', mock_open_twice): + with patch('builtins.open', mock_open_twice): result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") # Verify component nodes @@ -284,5 +534,7 @@ def test_missing_subnodes(self, mock_listdir, mock_parse): ref_driver = cashflows[0].find('./reference_driver/fixed_value') self.assertEqual(ref_driver.text, "2000") +# 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() diff --git a/tests/unit_tests/tests b/tests/unit_tests/tests index 39974377..b0711ff0 100644 --- a/tests/unit_tests/tests +++ b/tests/unit_tests/tests @@ -1,26 +1,31 @@ [Tests] [./TestMinimalInput] type = GenericExecutable - executable = python test_heron.py + executable = python -m unittest test_heron.TestMinimalInput [../] - [./TestExpandedInput] + [./TestExpandedInput1] type = GenericExecutable - executable = python test_heron.py + executable = python -m unittest test_heron.TestExpandedInput1 + [../] + + [./TestExpandedInput2] + type = GenericExecutable + executable = python -m unittest test_heron.TestExpandedInput2 [../] [./TestNoComponentsNode] type = GenericExecutable - executable = python test_heron.py + executable = python -m unittest test_heron.TestNoComponentsNode [../] [./TestNoComponentNodes] type = GenericExecutable - executable = python test_heron.py + executable = python -m unittest test_heron.TestNoComponentNodes [../] [./TestMissingSubnodes] type = GenericExecutable - executable = python test_heron.py + executable = python -m unittest test_heron.TestMissingSubnodes [../] [] From 8a9a48bfb11f65549f73b6b0c74fa57029b9eba7 Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Tue, 25 Jun 2024 11:06:44 -0600 Subject: [PATCH 4/8] Added heron unit tests for component set input files --- tests/unit_tests/test_heron.py | 287 ++++++++++++++++++++++++++------- 1 file changed, 225 insertions(+), 62 deletions(-) diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py index f7a7df8a..a0615e09 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -1,7 +1,7 @@ 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)) @@ -36,7 +36,7 @@ def setUp(self): "Scaling Factor": 0.5 }""") def test_minimal_input(self, mock_file, mock_listdir, mock_parse): - # Set up the mock to return an XML tree + # Set up the parse mock to return an XML tree mock_parse.return_value = self.tree mock_listdir.return_value = ['componentSetFake.json'] @@ -111,7 +111,7 @@ def setUp(self): "Scaling Factor": 0.5 }""") def test_expanded_input_1(self, mock_open, mock_listdir, mock_parse): - # Set up the mock to return an XML tree + # 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"] @@ -217,47 +217,46 @@ def setUp(self): @patch('xml.etree.ElementTree.parse') @patch('os.listdir') def test_expanded_input_2(self, mock_listdir, mock_parse): - # Set up the mock to return an XML tree + # 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 - ] + 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): @@ -391,7 +390,6 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): scaling_factor_value = scaling_factor[0].findall('./fixed_value') self.assertEqual(scaling_factor_value[0].text, '0.4') -@unittest.skip("Waiting for function update") class TestNoComponentsNode(unittest.TestCase): def setUp(self): @@ -412,7 +410,7 @@ def setUp(self): "Scaling Factor": 0.5 }""") def test_no_comps_node(self, mock_file, mock_listdir, mock_parse): - # Set up the mock to return an XML tree + # Set up the parse mock to return an XML tree mock_parse.return_value = self.tree mock_listdir.return_value = ['componentSetFake.json'] @@ -446,7 +444,7 @@ def setUp(self): "Scaling Factor": 0.5 }""") def test_no_comp_nodes(self, mock_file, mock_listdir, mock_parse): - # Set up the mock to return an XML tree + # Set up the parse mock to return an XML tree mock_parse.return_value = self.tree mock_listdir.return_value = ['componentSetFake.json'] @@ -481,33 +479,31 @@ def setUp(self): @patch('xml.etree.ElementTree.parse') @patch('os.listdir') def test_missing_subnodes(self, mock_listdir, mock_parse): - # Set up the mock to return an XML tree + # 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_twice = mock_open() - mock_open_twice.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 - ] + 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_twice): + with patch('builtins.open', mock_open_mult): result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") # Verify component nodes @@ -534,6 +530,173 @@ def test_missing_subnodes(self, mock_listdir, mock_parse): ref_driver = cashflows[0].find('./reference_driver/fixed_value') self.assertEqual(ref_driver.text, "2000") +class TestEmptyCompSetsFolder(unittest.TestCase): + 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') + +@unittest.skip("Waiting for function update (issue #18)") +class TestCompSetsFolderWithBadJSON(unittest.TestCase): + 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 ('json_read_error_name'): #FIXME: replace with correct error once function is updated + caught_bad_json = True + + with self.subTest("Did not respond correctly to bad component set file content"): + self.assertEqual(caught_bad_json, True) + +@unittest.skip("Waiting for function update (issue #18)") +class TestCompSetsFolderMultFiles(unittest.TestCase): + 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') + def test_compsets_folder_mult_files(self, mock_listdir, mock_parse): + # Set up the parse mock to return an XML tree + mock_parse.return_value = self.tree + # Only the txt and json files whose names start with 'componentSet' should be opened + files_list = ['component.json', 'componentSet.csv', 'componentSet.json', 'componentSetStuff.txt', + 'xcomponentSet.json', 'Set.json', 'compSet.json', 'ComponentSet.json'] + mock_listdir.return_value = files_list + + # Set up the open mock to return different files each time it's used + # It should only be called twice, but it contains data for up to four reads + # So it won't break the function when an extra file or two is opened, and the unit tester can catch the bug + mock_open_mult = mock_open() + mock_open_mult.side_effect = [mock_open(read_data = + """{ + "Component Set Name": "NewComponent0", + "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": "NewComponent1", + "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": "NewComponent2", + "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": "NewComponent3", + "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 open function was called on correct files + for file in files_list: + # if file should have been opened + if file in ['componentSet.json', 'componentSetStuff.txt']: + 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_mult.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_mult.call_args_list) + + components_node = result_tree.find('./Components') + + # Verify component nodes were updated + component_nodes = components_node.findall('./Component') + self.assertEqual(len(component_nodes), 3) + # 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__': From 2e4071b02e63cae75091e3bc462a74f721d1b72d Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Wed, 26 Jun 2024 11:09:58 -0600 Subject: [PATCH 5/8] Added unit testing edits and README --- tests/unit_tests/README.md | 60 +++++++++++++++++++++++++ tests/unit_tests/test_heron.py | 80 ++++++++++------------------------ tests/unit_tests/tests | 15 +++++++ 3 files changed, 98 insertions(+), 57 deletions(-) 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 a0615e09..40fe0cb9 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -336,7 +336,7 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): ref_driver = cashflow[0].findall('./reference_driver') self.assertEqual(len(ref_driver), 1) ref_driver_value = ref_driver[0].findall('./fixed_value') - self.assertEqual(ref_driver_value.text, '3100') + self.assertEqual(ref_driver_value[0].text, '3100') # Reference price ref_price = cashflow[0].findall('./reference_price') @@ -376,13 +376,13 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): ref_driver = cashflow[0].findall('./reference_driver') self.assertEqual(len(ref_driver), 1) ref_driver_value = ref_driver[0].findall('./fixed_value') - self.assertEqual(ref_driver_value.text, '4100') + self.assertEqual(ref_driver_value[0].text, '4100') # Reference price ref_price = cashflow[0].findall('./reference_price') self.assertEqual(len(ref_price), 1) ref_price_value = ref_price[0].findall('./fixed_value') - self.assertEqual(ref_price_value[0].text, '4200') + self.assertEqual(ref_price_value[0].text, '-4200') # Scaling factor scaling_factor = cashflow[0].findall('./scaling_factor_x') @@ -575,7 +575,6 @@ def test_empty_compsets_folder(self, mock_open, mock_listdir, mock_parse): self.assertEqual(len(component_nodes), 1) self.assertEqual(component_nodes[0].attrib['name'], 'ExistingComponent') -@unittest.skip("Waiting for function update (issue #18)") class TestCompSetsFolderWithBadJSON(unittest.TestCase): def setUp(self): # Example of a minimal XML structure @@ -605,13 +604,12 @@ def test_compsets_folder_bad_json(self, mock_open, mock_listdir, mock_parse): caught_bad_json = False try: result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") - except ('json_read_error_name'): #FIXME: replace with correct error once function is updated + 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) -@unittest.skip("Waiting for function update (issue #18)") class TestCompSetsFolderMultFiles(unittest.TestCase): def setUp(self): # Example of a minimal XML structure @@ -629,54 +627,28 @@ def setUp(self): @patch('xml.etree.ElementTree.parse') @patch('os.listdir') - def test_compsets_folder_mult_files(self, mock_listdir, mock_parse): + # 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 # Only the txt and json files whose names start with 'componentSet' should be opened - files_list = ['component.json', 'componentSet.csv', 'componentSet.json', 'componentSetStuff.txt', - 'xcomponentSet.json', 'Set.json', 'compSet.json', 'ComponentSet.json'] + 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 - # Set up the open mock to return different files each time it's used - # It should only be called twice, but it contains data for up to four reads - # So it won't break the function when an extra file or two is opened, and the unit tester can catch the bug - mock_open_mult = mock_open() - mock_open_mult.side_effect = [mock_open(read_data = - """{ - "Component Set Name": "NewComponent0", - "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": "NewComponent1", - "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": "NewComponent2", - "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": "NewComponent3", - "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") + # Call the function + result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") # Verify open function was called on correct files for file in files_list: @@ -684,18 +656,12 @@ def test_compsets_folder_mult_files(self, mock_listdir, mock_parse): if file in ['componentSet.json', 'componentSetStuff.txt']: 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_mult.call_args_list) + 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_mult.call_args_list) - - components_node = result_tree.find('./Components') - - # Verify component nodes were updated - component_nodes = components_node.findall('./Component') - self.assertEqual(len(component_nodes), 3) + 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 diff --git a/tests/unit_tests/tests b/tests/unit_tests/tests index b0711ff0..ac522853 100644 --- a/tests/unit_tests/tests +++ b/tests/unit_tests/tests @@ -28,4 +28,19 @@ type = GenericExecutable executable = python -m unittest test_heron.TestMissingSubnodes [../] + + [./TestEmptyCompSetsFolder] + type = GenericExecutable + executable = python -m unittest test_heron.TestEmptyCompSetsFolder + [../] + + [./TestCompSetsFolderWithBadJSON] + type = GenericExecutable + executable = python -m unittest test_heron.TestCompSetsFolderWithBadJSON + [../] + + [./TestCompSetsFolderMultFiles] + type = GenericExecutable + executable = python -m unittest test_heron.TestCompSetsFolderMultFiles + [../] [] From 126977c59691206120c125938f60baa864aedef9 Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Tue, 2 Jul 2024 16:25:22 -0600 Subject: [PATCH 6/8] Updated unit test running for compatibility with future code coverage --- tests/unit_tests/tests | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/unit_tests/tests b/tests/unit_tests/tests index ac522853..4a340f4e 100644 --- a/tests/unit_tests/tests +++ b/tests/unit_tests/tests @@ -1,46 +1,46 @@ [Tests] [./TestMinimalInput] - type = GenericExecutable - executable = python -m unittest test_heron.TestMinimalInput + type = RavenPython + input = '-m unittest test_heron.TestMinimalInput' [../] [./TestExpandedInput1] - type = GenericExecutable - executable = python -m unittest test_heron.TestExpandedInput1 + type = RavenPython + input = '-m unittest test_heron.TestExpandedInput1' [../] [./TestExpandedInput2] - type = GenericExecutable - executable = python -m unittest test_heron.TestExpandedInput2 + type = RavenPython + input = '-m unittest test_heron.TestExpandedInput2' [../] [./TestNoComponentsNode] - type = GenericExecutable - executable = python -m unittest test_heron.TestNoComponentsNode + type = RavenPython + input = '-m unittest test_heron.TestNoComponentsNode' [../] [./TestNoComponentNodes] - type = GenericExecutable - executable = python -m unittest test_heron.TestNoComponentNodes + type = RavenPython + input = '-m unittest test_heron.TestNoComponentNodes' [../] [./TestMissingSubnodes] - type = GenericExecutable - executable = python -m unittest test_heron.TestMissingSubnodes + type = RavenPython + input = '-m unittest test_heron.TestMissingSubnodes' [../] [./TestEmptyCompSetsFolder] - type = GenericExecutable - executable = python -m unittest test_heron.TestEmptyCompSetsFolder + type = RavenPython + input = '-m unittest test_heron.TestEmptyCompSetsFolder' [../] [./TestCompSetsFolderWithBadJSON] - type = GenericExecutable - executable = python -m unittest test_heron.TestCompSetsFolderWithBadJSON + type = RavenPython + input = '-m unittest test_heron.TestCompSetsFolderWithBadJSON' [../] [./TestCompSetsFolderMultFiles] - type = GenericExecutable - executable = python -m unittest test_heron.TestCompSetsFolderMultFiles + type = RavenPython + input = '-m unittest test_heron.TestCompSetsFolderMultFiles' [../] [] From cdfa1a44deed854456b806619c2d405f34d57b3a Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Thu, 25 Jul 2024 08:43:47 -0600 Subject: [PATCH 7/8] Addressing comments --- tests/unit_tests/test_heron.py | 166 +++++++++++++++------------------ 1 file changed, 73 insertions(+), 93 deletions(-) diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py index 40fe0cb9..6cdaff79 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -8,7 +8,36 @@ sys.path.append(FORCE_LOC) from FORCE.src.heron import create_componentsets_in_HERON -class TestMinimalInput(unittest.TestCase): +class HERONTestCase(unittest.TestCase): + + def check_reference_price(self, cashflow, correct_value, correct_content_length=1): + 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): + 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): + 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 @@ -56,17 +85,12 @@ def test_minimal_input(self, mock_file, mock_listdir, mock_parse): self.assertIn('type', cashflows[0].keys()) self.assertIn('taxable', cashflows[0].keys()) self.assertIn('inflation', cashflows[0].keys()) - self.assertIn('mult_target', cashflows[0].keys()) - # Verify reference price - ref_price_value = cashflows[0].find('./reference_price/fixed_value') - self.assertEqual(ref_price_value.text, '-2000') - - # Verify the reference driver and price updates - ref_driver_value = cashflows[0].find('./reference_driver/fixed_value') - self.assertEqual(ref_driver_value.text, '1.0') # The driver should have been converted from kW to MW + # Verify reference price and reference driver + self.check_reference_driver(cashflows[0], '1.0') + self.check_reference_price(cashflows[0], '-2000') -class TestExpandedInput1(unittest.TestCase): +class TestExpandedInput1(HERONTestCase): def setUp(self): # Added case and datagenerator nodes (should be transferred blindly) and extra components @@ -120,7 +144,7 @@ def test_expanded_input_1(self, mock_open, mock_listdir, mock_parse): 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")): + 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')) @@ -130,28 +154,26 @@ def test_expanded_input_1(self, mock_open, mock_listdir, mock_parse): 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 - cashflows = comp.findall('./economics/CashFlow') self.assertEqual(len(cashflows), 1) self.assertEqual(cashflows[0].attrib['type'], 'one-time') elif comp.get('name') == 'ExistingComponent1': # Verify CashFlow with type - cashflows = comp.findall('./economics/CashFlow') self.assertEqual(len(cashflows), 1) self.assertEqual(cashflows[0].attrib['type'], 'repeating') elif comp.get('name') == 'NewComponent': # Verify reference driver - ref_driver = comp.find('./economics/CashFlow/reference_driver/fixed_value') - self.assertEqual(ref_driver.text, '1000') # Check that mW were not converted + self.check_reference_driver(cashflows[0], '1000') # Verify DataGenerators node was transferred - with (self.subTest("DataGenerators node has been corrupted")): + 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(unittest.TestCase): +class TestExpandedInput2(HERONTestCase): def setUp(self): # Complex subnodes to each component with various and positions and configurations @@ -179,7 +201,7 @@ def setUp(self): - 1 + 1 1234 @@ -267,12 +289,13 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): 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': - economics = comp.find('./economics') - # Verify non-capex cashflow was not corrupted with self.subTest("Non-capex CashFlow node was corrupted"): - cashflow_non_capex = economics.findall('./CashFlow[@name="other"]') + 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 @@ -281,13 +304,10 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): # Verify capex cashflow was added with self.subTest("capex CashFlow was not added correctly"): - cashflow_capex = economics.findall('./CashFlow[@name="Component0_capex"]') + cashflow_capex = economics[0].findall('./CashFlow[@name="Component0_capex"]') self.assertEqual(len(cashflow_capex), 1) elif comp.get('name') == 'Component1': - economics = comp.findall('./economics') - self.assertEqual(len(economics), 1) - # Verify number of cashflows self.assertEqual(len(economics[0].findall('./CashFlow')), 2) @@ -304,93 +324,56 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): with self.subTest("capex CashFlow node was not updated correctly"): cashflow_capex = economics[0].findall('./CashFlow[@name="capex"]') self.assertEqual(len(cashflow_capex), 1) - ref_price = cashflow_capex[0].findall('./reference_price/fixed_value') - self.assertEqual(ref_price[0].text, '-2200') + self.check_reference_price(cashflow_capex[0], '-2200') elif comp.get('name') == 'Component2': - economics = comp.findall('./economics') - - # Verify ProjectTime was not corrupted + # Verify lifetime was not corrupted with self.subTest("Non-cashflow child node of economics has been corrupted"): - proj_time = economics[0].findall('./ProjectTime') + proj_time = economics[0].findall('./lifetime') self.assertEqual(len(proj_time), 1) self.assertEqual(proj_time[0].text, '1') # Verify cashflow merging - cashflow = economics[0].findall('./CashFlow') - self.assertEqual(len(cashflow), 1) + cashflows = economics[0].findall('./CashFlow') + self.assertEqual(len(cashflows), 1) # Verify cashflow was updated correctly # Attributes with self.subTest("Attributes of CashFlow were corrupted"): - self.assertIn('npv_exempt', cashflow[0].keys()) + self.assertIn('npv_exempt', cashflows[0].keys()) # Children with self.subTest("CashFlow children nodes were not updated correctly"): - cf_contents = [e for e in cashflow[0].findall('./') + 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) - # Reference driver - ref_driver = cashflow[0].findall('./reference_driver') - self.assertEqual(len(ref_driver), 1) - ref_driver_value = ref_driver[0].findall('./fixed_value') - self.assertEqual(ref_driver_value[0].text, '3100') - - # Reference price - ref_price = cashflow[0].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), 1) - ref_price_value = ref_price[0].findall('./fixed_value') - self.assertEqual(ref_price_value[0].text, '-3200') - - # Scaling factor - scaling_factor = cashflow[0].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), 1) - scaling_factor_value = scaling_factor[0].findall('./fixed_value') - self.assertEqual(scaling_factor_value[0].text, '0.3') + 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 = cashflow[0].findall('./driver') + driver = cashflows[0].findall('./driver') self.assertEqual(len(driver), 1) self.assertIsNotNone(driver[0].findall('fixed_value')) elif comp.get('name') == 'Component3': - cashflow = comp.findall('./economics/CashFlow') - self.assertEqual(len(cashflow), 1) + 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 cashflow[0].findall('./') + 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) - # Reference driver - ref_driver = cashflow[0].findall('./reference_driver') - self.assertEqual(len(ref_driver), 1) - ref_driver_value = ref_driver[0].findall('./fixed_value') - self.assertEqual(ref_driver_value[0].text, '4100') + self.check_reference_driver(cashflows[0], '4100') + self.check_reference_price(cashflows[0], '-4200') + self.check_scaling_factor(cashflows[0], '0.4') - # Reference price - ref_price = cashflow[0].findall('./reference_price') - self.assertEqual(len(ref_price), 1) - ref_price_value = ref_price[0].findall('./fixed_value') - self.assertEqual(ref_price_value[0].text, '-4200') - - # Scaling factor - scaling_factor = cashflow[0].findall('./scaling_factor_x') - self.assertEqual(len(scaling_factor), 1) - scaling_factor_value = scaling_factor[0].findall('./fixed_value') - self.assertEqual(scaling_factor_value[0].text, '0.4') - -class TestNoComponentsNode(unittest.TestCase): +class TestNoComponentsNode(HERONTestCase): def setUp(self): # Has no Components node @@ -422,7 +405,7 @@ def test_no_comps_node(self, mock_file, mock_listdir, mock_parse): self.assertEqual(len(components), 1) self.assertEqual(len(components[0].findall('./Component[@name="NewComponent"]')), 1) -class TestNoComponentNodes(unittest.TestCase): +class TestNoComponentNodes(HERONTestCase): def setUp(self): # Has no Component nodes @@ -459,7 +442,7 @@ def test_no_comp_nodes(self, mock_file, mock_listdir, mock_parse): # Verify contents have been added self.assertIsNotNone(component_list[0].findall('./economics/CashFlow')) -class TestMissingSubnodes(unittest.TestCase): +class TestMissingSubnodes(HERONTestCase): def setUp(self): # Comp0 has no economics subnode; Comp1 has no CashFlow subnode @@ -518,19 +501,15 @@ def test_missing_subnodes(self, mock_listdir, mock_parse): self.assertEqual(len(economics), 1) cashflows = economics[0].findall('./CashFlow') self.assertEqual(len(cashflows), 1) - self.assertEqual(cashflows[0].attrib["name"], "Component0_capex") - ref_driver = cashflows[0].find('./reference_driver/fixed_value') - self.assertEqual(ref_driver.text, "1000") + 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.assertEqual(cashflows[0].attrib["name"], "Component1_capex") - ref_driver = cashflows[0].find('./reference_driver/fixed_value') - self.assertEqual(ref_driver.text, "2000") + self.check_reference_driver(cashflows[0], '2000') -class TestEmptyCompSetsFolder(unittest.TestCase): +class TestEmptyCompSetsFolder(HERONTestCase): def setUp(self): # Example of a minimal XML structure self.heron_xml = """ @@ -575,7 +554,7 @@ def test_empty_compsets_folder(self, mock_open, mock_listdir, mock_parse): self.assertEqual(len(component_nodes), 1) self.assertEqual(component_nodes[0].attrib['name'], 'ExistingComponent') -class TestCompSetsFolderWithBadJSON(unittest.TestCase): +class TestCompSetsFolderWithBadJSON(HERONTestCase): def setUp(self): # Example of a minimal XML structure self.heron_xml = """ @@ -610,7 +589,7 @@ def test_compsets_folder_bad_json(self, mock_open, mock_listdir, mock_parse): with self.subTest("Did not respond correctly to bad component set file content"): self.assertEqual(caught_bad_json, True) -class TestCompSetsFolderMultFiles(unittest.TestCase): +class TestCompSetsFolderMultFiles(HERONTestCase): def setUp(self): # Example of a minimal XML structure self.heron_xml = """ @@ -641,11 +620,12 @@ def setUp(self): 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 - # Only the txt and json files whose names start with 'componentSet' should be opened 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") @@ -653,7 +633,7 @@ def test_compsets_folder_mult_files(self, mock_open, mock_listdir, mock_parse): # Verify open function was called on correct files for file in files_list: # if file should have been opened - if file in ['componentSet.json', 'componentSetStuff.txt']: + 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) From 83347b2cab636ec9e9941a7c48d28a83a75a96d7 Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Mon, 29 Jul 2024 12:01:22 -0600 Subject: [PATCH 8/8] Added docstrings for HERONTestCase class and methods --- tests/unit_tests/test_heron.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py index 6cdaff79..68320d57 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -9,8 +9,18 @@ from FORCE.src.heron import create_componentsets_in_HERON 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('./') @@ -20,6 +30,13 @@ def check_reference_price(self, cashflow, correct_value, correct_content_length= 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('./') @@ -29,6 +46,13 @@ def check_reference_driver(self, cashflow, correct_value, correct_content_length 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('./')