Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heron unit testing #20

Merged
merged 11 commits into from
Jul 29, 2024
166 changes: 73 additions & 93 deletions tests/unit_tests/test_heron.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you be able to add docstrings to these new methods? For reference: https://github.com/idaholab/raven/wiki/RAVEN-Code-Standards#function-and-class-documentation-strings

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem.

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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'))
Expand All @@ -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':
GabrielSoto-INL marked this conversation as resolved.
Show resolved Hide resolved
# Verify CashFlow with type
cashflows = comp.findall('./economics/CashFlow')
self.assertEqual(len(cashflows), 1)
GabrielSoto-INL marked this conversation as resolved.
Show resolved Hide resolved
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 <economics> and <CashFlows> positions and configurations
Expand Down Expand Up @@ -179,7 +201,7 @@ def setUp(self):

<Component name="Component2">
<economics>
<ProjectTime>1</ProjectTime>
<lifetime>1</lifetime>
<CashFlow name="capex_Comp2" npv_exempt="True">
<reference_driver>
<fixed_value>1234</fixed_value>
Expand Down Expand Up @@ -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:
GabrielSoto-INL marked this conversation as resolved.
Show resolved Hide resolved
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('./')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I'm following this step. are you trying to find all child nodes of the CashFlow[@name="other"] node? would it be better to use the getchildren( ) method or something similar within the ET package?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the purpose of this (lines 277-278) is to filter the children of the CashFlow[@name="other"] node to find all non-comment subelements (i.e., subnodes). This filtering is necessary because the length of the resulting list is checked. A getchildren() method would be cleaner than findall('./'), but there is not one included in the ET package. The iter() or iterfind() methods could be used in place of the findall() method, but neither would simplify syntax; xml pathing (the './' argument) would still be required. Documentation here: https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this working on XML directly with ET, or in the RAVEN-defined heirarchical trees? Because we can use EDDI format trees (from WORKBENCH) as well, I would hesitate to strictly depend on the XML parsing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PaulTalbot-INL This uses ET. I'm pretty confident in the functionality of the parsing for now, but it's not the easiest to work with, and the syntax can be a bit picky and hard to read. So if the EDDI format trees are significantly easier, it might be worthwhile to switch over for the sake of future edits and new tests. What's the best place to get more info on those?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In RAVEN utils we have the TreeStructure, which behaves like XML and mimics ET in terms of methods, but is designed to flexibly load in different heirarchical structures (XML, EDDI, HIT, etc). It may not be too hard to load the XML into the tree structure and use that instead of parsing the XML directly.

See https://github.com/idaholab/raven/blob/devel/ravenframework/utils/TreeStructure.py.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to clarify, is the suggestion here to modify the create_componentsets_in_HERON( ) method to output a TreeStructure object rather than an ElementTree object? I think that could be valuable, though I feel like might be out of scope for this particular PR. might be worth starting a different one to change that functionality.

also wanted to note that the input to the create_componentsets_in_HERON( ) method accepts a 'str' path to some sort of HERON input script, so it could theoretically accept either a .xml or potentially .heron (for Workbench) file which it can modify. to actually write to the new file, this would involve a new integration test or modifying the existing ones...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. I'm fine with using XML and ET for now. We've been wanting to do something to allow InputSpec on-the-fly instantiation for dynamic component building, but we haven't figured out how to make the ends meet on that yet. This is fine with ET.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caleb-sitton-inl seems like it's best to keep as is, but also it might be a good idea to start an issue for Feature Request regarding switching to dynamic TreeStructure elements rather than ET

if not e.tag is ET.Comment] # Filters out ET.Comment elements
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = """<HERON>
Expand Down Expand Up @@ -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 = """<HERON>
Expand Down Expand Up @@ -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 = """<HERON>
Expand Down Expand Up @@ -641,19 +620,20 @@ 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")

# 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)
Expand Down
Loading