From b6d3c774fbef03481cf684dc20d66a988d6f10f0 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 15 Apr 2026 21:05:39 -0600 Subject: [PATCH 1/4] update to catch cases where the table rows have been reordered --- energyplus_regressions/diffs/table_diff.py | 112 +++++++++++------- .../tests/diffs/test_table_diff.py | 23 ++++ 2 files changed, 93 insertions(+), 42 deletions(-) diff --git a/energyplus_regressions/diffs/table_diff.py b/energyplus_regressions/diffs/table_diff.py index 6930d1b..5fb92a6 100755 --- a/energyplus_regressions/diffs/table_diff.py +++ b/energyplus_regressions/diffs/table_diff.py @@ -35,6 +35,7 @@ __license__ = "GNU General Public License Version 3" from pathlib import Path +from collections import Counter, defaultdict, deque import sys import getopt import os.path @@ -144,6 +145,71 @@ def get_table_unique_heading(table): return None +def normalize_row_match_value(value): + """Normalize a cell value for row matching without changing actual diff output.""" + text = str(value).replace('\xa0', ' ') + return ' '.join(text.split()).casefold() + + +def row_cells_for_match(trow): + return [normalize_row_match_value(tcol.get_text(' ', strip=True)) for tcol in trow('td')] + + +def reorder_rows_to_match(base_keys, search_keys, search_rows): + rows_by_key = defaultdict(deque) + for key, row in zip(search_keys, search_rows): + rows_by_key[tuple(key)].append(row) + + reordered_rows = [] + for key in base_keys: + normalized_key = tuple(key) + if not rows_by_key[normalized_key]: + return search_rows + reordered_rows.append(rows_by_key[normalized_key].popleft()) + return reordered_rows + + +def match_search_rows_to_base_rows(base_rows, search_rows): + """ + Reorder search_rows to match base_rows when row order is not semantically meaningful. + + First, try a whole-row match using normalized values so case-only formatting changes do + not block reorder detection. If rows have real diffs, fall back to the shortest unique + leading-column key that exists in both tables. This lets us align rows like coil sizing + outputs where the stable row identifier is early in the row, but values later in the row + may legitimately differ. + """ + if not base_rows or not search_rows: + return search_rows + + base_keys = [row_cells_for_match(trow) for trow in base_rows] + search_keys = [row_cells_for_match(trow) for trow in search_rows] + + if base_keys == search_keys: + return search_rows + + if Counter(map(tuple, base_keys)) == Counter(map(tuple, search_keys)): + return reorder_rows_to_match(base_keys, search_keys, search_rows) + + max_prefix_len = min( + min((len(key) for key in base_keys), default=0), + min((len(key) for key in search_keys), default=0), + ) + + for prefix_len in range(1, max_prefix_len + 1): + base_prefixes = [tuple(key[:prefix_len]) for key in base_keys] + if len(set(base_prefixes)) != len(base_prefixes): + continue + + search_prefixes = [tuple(key[:prefix_len]) for key in search_keys] + if Counter(base_prefixes) != Counter(search_prefixes): + continue + + return reorder_rows_to_match(base_prefixes, search_prefixes, search_rows) + + return search_rows + + def hdict2soup(soup, heading, num, hdict, tdict, horder): """Create soup table (including anchor and heading) from header dictionary and error dictionary""" # Append table anchor @@ -243,50 +309,12 @@ def table2hdict_horder(table, table_a=None): # Assume we are going to just loop over the rows and compare the data search_rows = trows[1:] - # But we can handle it specially if we passed in table_a and it's just a valid reorder - # There are some weird things to consider here though. For example, some tables have multiple entirely blank - # rows, just there for visual spacing. Also there are tables where the far left entry is not unique. - # Consider the End Uses by Subcategory table. One row starts with "Heating" and then "General". - # The next row then has nothing in the first column, but the second column is "Boiler". - # This implies that "Heating" was a grouping, and "General" or "Boiler" is the actual subcategory. - # I think the only way to handle this robustly would be to use the entire - # row as the key, which is annoying, but should work well. + # But we can handle it specially if we passed in table_a and the rows are just reordered. + # Prefer whole-row matching first, but if a row has actual value diffs we can still align it + # by the shortest unique leading-column key that exists in both tables. if table_a: - # process the rows of the "base" table_a that was provided into a list of search keys trows_a = table_a('tr') - table_a_row_order = [] - for trow in trows_a[1:]: - search_key = [] - for tcol in trow('td'): - if tcol.contents: - search_key.append(tcol.contents[0]) - else: # pragma: no cover - # I really don't think we can make it here while searching, but I don't want to accidentally crash - search_key.append("") - table_a_row_order.append(search_key) - # process the rows of the "mod" table that was provided into a list of search keys - found_table_b_row_order = [] - for trow in trows[1:]: - search_key = [] - for tcol in trow('td'): - if tcol.contents: - search_key.append(tcol.contents[0]) - else: # pragma: no cover - # I really don't think we can make it here while searching, but I don't want to accidentally crash - search_key.append("") - found_table_b_row_order.append(search_key) - # it's the same order exactly, skip any searching and just run with search_rows as-is - if table_a_row_order == found_table_b_row_order: - pass - # if not exactly the same but overall the same stuff, it's reordered and we can match things up - elif sorted(table_a_row_order) == sorted(found_table_b_row_order): - # now just build the list of trows to search by index based on table a order - search_rows = [] - for to_find_val in table_a_row_order: - for search_row_index, trow in enumerate(trows[1:]): - if found_table_b_row_order[search_row_index] == to_find_val: - search_rows.append(trow) - break + search_rows = match_search_rows_to_base_rows(trows_a[1:], search_rows) # whether it was reordered or just using the literal order, build out the hdict instance to pass back for trow in search_rows: diff --git a/energyplus_regressions/tests/diffs/test_table_diff.py b/energyplus_regressions/tests/diffs/test_table_diff.py index 20910e4..ffa01c4 100644 --- a/energyplus_regressions/tests/diffs/test_table_diff.py +++ b/energyplus_regressions/tests/diffs/test_table_diff.py @@ -577,6 +577,29 @@ def test_reordering_is_ok_sometimes(self): self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 + def test_reordering_with_case_only_key_column_changes_and_real_value_diff(self): + # Coil sizing tables can reorder rows while also changing the presentation case of an + # identifier column. We still want to align rows by their stable leading key and only + # report the actual value diff. + response = table_diff( + self.thresh_dict, + os.path.join(self.diff_files_dir, 'eplustbl_row_reorder_case_change_base.htm'), + os.path.join(self.diff_files_dir, 'eplustbl_row_reorder_case_change_mod.htm'), + os.path.join(self.temp_output_dir, 'abs_diff.htm'), + os.path.join(self.temp_output_dir, 'rel_diff.htm'), + os.path.join(self.temp_output_dir, 'math_diff.log'), + os.path.join(self.temp_output_dir, 'summary.htm'), + ) + self.assertEqual('', response[0]) # diff status + self.assertEqual(1, response[1]) # count_of_tables + self.assertEqual(1, response[2]) # big diffs + self.assertEqual(0, response[3]) # small diffs + self.assertEqual(7, response[4]) # equals + self.assertEqual(0, response[5]) # string diffs + self.assertEqual(0, response[6]) # size errors + self.assertEqual(0, response[7]) # in file 2 but not in file 1 + self.assertEqual(0, response[8]) # in file 1 but not in file 2 + # it seems like this is something that table_diff just cannot handle. The duplicate empty column heading is causing # major problems. I'm going to skip this test for now, but leave the two table diff resource files in place # so that we could try to investigate later if we ever wanted. From 4561fddbae3acfe4221c08466affe0de1dc87c83 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 15 Apr 2026 21:57:07 -0600 Subject: [PATCH 2/4] case sensitive table string diffs --- energyplus_regressions/diffs/table_diff.py | 63 ++++++++++++-- .../tests/diffs/test_table_diff.py | 84 +++++++++++++++---- 2 files changed, 125 insertions(+), 22 deletions(-) diff --git a/energyplus_regressions/diffs/table_diff.py b/energyplus_regressions/diffs/table_diff.py index 5fb92a6..4f33b82 100755 --- a/energyplus_regressions/diffs/table_diff.py +++ b/energyplus_regressions/diffs/table_diff.py @@ -84,6 +84,9 @@ td.table_size_error { background-color: #FCFF97; } +td.stringdiff { + background-color: #F6D8AE; +} .big { background-color: #FF969D; } @@ -92,6 +95,10 @@ background-color: #FFBE84; } +.stringdiff { + background-color: #F6D8AE; +} + """ @@ -112,8 +119,8 @@ def thresh_abs_rel_diff(abs_thresh: float, rel_thresh: float, x: str, y: str) -> diff = 'small' return abs_diff, rel_diff, diff except ValueError: - # if we couldn't get a float out of it, we are doing string comparison, check case-insensitively before leaving - if x.lower().strip() == y.lower().strip(): + # if we couldn't get a float out of it, do a string comparison after trimming edge whitespace + if x.strip() == y.strip(): return 0, 0, 'equal' else: return f'{x} vs {y}', f'{x} vs {y}', 'stringdiff' @@ -151,6 +158,14 @@ def normalize_row_match_value(value): return ' '.join(text.split()).casefold() +def should_ignore_table_diff_field(column_heading, row_label=None): + if column_heading == 'Version ID': + return True + if row_label and str(row_label).strip() == 'Program Version and Build': + return True + return False + + def row_cells_for_match(trow): return [normalize_row_match_value(tcol.get_text(' ', strip=True)) for tcol in trow('td')] @@ -267,7 +282,7 @@ def hdict2soup(soup, heading, num, hdict, tdict, horder): if h not in hdict: tdtag = Tag(soup, name='td', attrs=[("class", "big")]) tdtag.append('ColumnHeadingDifference') - elif h == 'DummyPlaceholder' or h == 'Subcategory': + elif h == 'Subcategory': # Some tables such as the Source Energy End Use Components # have a blank row full of ` ` which won't be # decoded nicely @@ -278,6 +293,23 @@ def hdict2soup(soup, heading, num, hdict, tdict, horder): except Exception: # pragma: no cover val = val.encode('ascii', 'ignore').decode('ascii') tdtag.append(str(val)) + elif h == 'DummyPlaceholder': + val = hdict[h][i] + if isinstance(val, tuple) and len(val) == 2: + diff, which = val + tdtag = Tag(soup, name='td', attrs=[('class', which)]) + try: + tdtag.append(str(diff)) + except Exception: # pragma: no cover + diff = diff.encode('ascii', 'ignore').decode('ascii') + tdtag.append(str(diff)) + else: + tdtag = Tag(soup, name='td') + try: + tdtag.append(str(val)) + except Exception: # pragma: no cover + val = val.encode('ascii', 'ignore').decode('ascii') + tdtag.append(str(val)) else: (diff, which) = hdict[h][i] tdtag = Tag(soup, name='td', attrs=[('class', which)]) @@ -531,6 +563,7 @@ def table_diff( # but for all other tables, we can use the first table as a baseline to carefully match up the rows else: hdict2, horder2 = table2hdict_horder(table2, table1) + compare_row_label_fields = not any(k in uheading1 for k in row_order_dependent_table_keys) # honestly, if the column headings have changed, this should be an indicator to all reviewers that this needs # up close investigation. As such, we are going to trigger the following things: @@ -554,7 +587,18 @@ def table_diff( for h in horder1: if h == 'DummyPlaceholder': - diff_dict[h] = hdict1[h] + if h not in horder2 or not compare_row_label_fields: + diff_dict[h] = hdict1[h] + else: + diff_dict[h] = [] + for x, y in zip(hdict1[h], hdict2[h]): + diff_result = thresh_abs_rel_diff(0, 0, x, y) + if diff_result[2] == 'stringdiff': + diff_dict[h].append((diff_result[0], diff_result[2])) + table_string_diff += 1 + count_of_string_diff += 1 + else: + diff_dict[h].append(x) else: if h not in horder2: diff_dict[h] = [[0, 0, 'big']] * (len(table1('tr')) - 1) @@ -562,13 +606,18 @@ def table_diff( (abs_thresh, rel_thresh) = thresh_dict.lookup(h) h_thresh_dict[h] = (abs_thresh, rel_thresh) diff_dict[h] = [] - for x, y in zip(hdict1[h], hdict2[h]): - diff_dict[h].append(thresh_abs_rel_diff(abs_thresh, rel_thresh, x, y)) + row_labels = hdict1.get('DummyPlaceholder', []) + for row_index, (x, y) in enumerate(zip(hdict1[h], hdict2[h])): + row_label = row_labels[row_index] if row_index < len(row_labels) else None + if should_ignore_table_diff_field(h, row_label): + diff_dict[h].append((0, 0, 'equal')) + else: + diff_dict[h].append(thresh_abs_rel_diff(abs_thresh, rel_thresh, x, y)) # Statistics local to this table for diff_result in diff_dict[h]: diff_type = diff_result[2] - if h == 'Version ID': + if should_ignore_table_diff_field(h): table_equal += 1 count_of_equal += 1 elif diff_type == 'small': diff --git a/energyplus_regressions/tests/diffs/test_table_diff.py b/energyplus_regressions/tests/diffs/test_table_diff.py index ffa01c4..68c8be1 100644 --- a/energyplus_regressions/tests/diffs/test_table_diff.py +++ b/energyplus_regressions/tests/diffs/test_table_diff.py @@ -1,6 +1,7 @@ import os import tempfile import unittest +from pathlib import Path from energyplus_regressions.diffs.table_diff import table_diff from energyplus_regressions.diffs.thresh_dict import ThreshDict @@ -235,11 +236,12 @@ def test_small_numeric_diff(self): self.assertEqual(0, response[8]) # in file 1 but not in file 2 def test_string_diff(self): + abs_diff = os.path.join(self.temp_output_dir, 'abs_diff.htm') response = table_diff( self.thresh_dict, os.path.join(self.diff_files_dir, 'eplustbl_has_string_diff_base.htm'), os.path.join(self.diff_files_dir, 'eplustbl_has_string_diff_mod.htm'), - os.path.join(self.temp_output_dir, 'abs_diff.htm'), + abs_diff, os.path.join(self.temp_output_dir, 'rel_diff.htm'), os.path.join(self.temp_output_dir, 'math_diff.log'), os.path.join(self.temp_output_dir, 'summary.htm'), @@ -253,14 +255,38 @@ def test_string_diff(self): self.assertEqual(0, response[6]) # size errors self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 + self.assertIn('HELLO vs WORLD', Path(abs_diff).read_text()) + + def test_row_label_string_diff(self): + abs_diff = os.path.join(self.temp_output_dir, 'abs_diff.htm') + response = table_diff( + self.thresh_dict, + os.path.join(self.diff_files_dir, 'eplustbl_row_label_string_diff_base.htm'), + os.path.join(self.diff_files_dir, 'eplustbl_row_label_string_diff_mod.htm'), + abs_diff, + os.path.join(self.temp_output_dir, 'rel_diff.htm'), + os.path.join(self.temp_output_dir, 'math_diff.log'), + os.path.join(self.temp_output_dir, 'summary.htm'), + ) + self.assertEqual('', response[0]) # diff status + self.assertEqual(1, response[1]) # count_of_tables + self.assertEqual(0, response[2]) # big diffs + self.assertEqual(0, response[3]) # small diffs + self.assertEqual(2, response[4]) # equals + self.assertEqual(1, response[5]) # string diffs + self.assertEqual(0, response[6]) # size errors + self.assertEqual(0, response[7]) # in file 2 but not in file 1 + self.assertEqual(0, response[8]) # in file 1 but not in file 2 + self.assertIn('Alpha vs Alpha Renamed', Path(abs_diff).read_text()) def test_string_diff_case_change_only(self): - # should be no diffs for case-insensitive comparison + # case-only text changes should still surface as string diffs + abs_diff = os.path.join(self.temp_output_dir, 'abs_diff.htm') response = table_diff( self.thresh_dict, os.path.join(self.diff_files_dir, 'eplustbl_has_string_diff_base.htm'), os.path.join(self.diff_files_dir, 'eplustbl_has_string_diff_case_only.htm'), - os.path.join(self.temp_output_dir, 'abs_diff.htm'), + abs_diff, os.path.join(self.temp_output_dir, 'rel_diff.htm'), os.path.join(self.temp_output_dir, 'math_diff.log'), os.path.join(self.temp_output_dir, 'summary.htm'), @@ -269,11 +295,12 @@ def test_string_diff_case_change_only(self): self.assertEqual(3, response[1]) # count_of_tables self.assertEqual(0, response[2]) # big diffs self.assertEqual(0, response[3]) # small diffs - self.assertEqual(17, response[4]) # equals - self.assertEqual(0, response[5]) # string diffs + self.assertEqual(16, response[4]) # equals + self.assertEqual(1, response[5]) # string diffs self.assertEqual(0, response[6]) # size errors self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 + self.assertIn('HELLO vs hello', Path(abs_diff).read_text()) def test_malformed_table_heading_in_file_1(self): response = table_diff( @@ -352,8 +379,8 @@ def test_weird_unicode_issue(self): self.assertEqual(155, response[1]) # count_of_tables self.assertEqual(334, response[2]) # big diffs self.assertEqual(67, response[3]) # small diffs - self.assertEqual(3756, response[4]) # equals - self.assertEqual(21, response[5]) # string diffs + self.assertEqual(3757, response[4]) # equals + self.assertEqual(20, response[5]) # string diffs self.assertEqual(0, response[6]) # size errors self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 @@ -373,8 +400,8 @@ def test_unicode_but_not_utf8_encoded_table(self): self.assertEqual(155, response[1]) # count_of_tables self.assertEqual(334, response[2]) # big diffs self.assertEqual(67, response[3]) # small diffs - self.assertEqual(3756, response[4]) # equals - self.assertEqual(21, response[5]) # string diffs + self.assertEqual(3757, response[4]) # equals + self.assertEqual(20, response[5]) # string diffs self.assertEqual(0, response[6]) # size errors self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 @@ -512,13 +539,34 @@ def test_ignore_version_diff(self): self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 + def test_ignore_program_version_and_build_diff(self): + response = table_diff( + self.thresh_dict, + os.path.join(self.diff_files_dir, 'eplustbl_program_versiondiff_base.htm'), + os.path.join(self.diff_files_dir, 'eplustbl_program_versiondiff_mod.htm'), + os.path.join(self.temp_output_dir, 'abs_diff.htm'), + os.path.join(self.temp_output_dir, 'rel_diff.htm'), + os.path.join(self.temp_output_dir, 'math_diff.log'), + os.path.join(self.temp_output_dir, 'summary.htm'), + ) + self.assertEqual('', response[0]) # diff status + self.assertEqual(1, response[1]) # count_of_tables + self.assertEqual(0, response[2]) # big diffs + self.assertEqual(0, response[3]) # small diffs + self.assertEqual(2, response[4]) # equals + self.assertEqual(0, response[5]) # string diffs + self.assertEqual(0, response[6]) # size errors + self.assertEqual(0, response[7]) # in file 2 but not in file 1 + self.assertEqual(0, response[8]) # in file 1 but not in file 2 + def test_catching_object_name_diff(self): # The two files have differences in the string diffs, why aren't they handled? + abs_diff = os.path.join(self.temp_output_dir, 'abs_diff.htm') response = table_diff( self.thresh_dict, os.path.join(self.diff_files_dir, 'eplustbl_objname_base.htm'), os.path.join(self.diff_files_dir, 'eplustbl_objname_mod.htm'), - os.path.join(self.temp_output_dir, 'abs_diff.htm'), + abs_diff, os.path.join(self.temp_output_dir, 'rel_diff.htm'), os.path.join(self.temp_output_dir, 'math_diff.log'), os.path.join(self.temp_output_dir, 'summary.htm'), @@ -532,6 +580,7 @@ def test_catching_object_name_diff(self): self.assertEqual(0, response[6]) # size errors self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 + self.assertIn('Unknown vs SPACE1-1 ATU', Path(abs_diff).read_text()) def test_odd_column_heading_mismatch_diff(self): # The eplustbl output has two tables with duplicate names but different column header data @@ -579,13 +628,14 @@ def test_reordering_is_ok_sometimes(self): def test_reordering_with_case_only_key_column_changes_and_real_value_diff(self): # Coil sizing tables can reorder rows while also changing the presentation case of an - # identifier column. We still want to align rows by their stable leading key and only - # report the actual value diff. + # identifier column. We still want to align rows by their stable leading key while + # reporting both the case-only string diffs and the real numeric diff. + abs_diff = os.path.join(self.temp_output_dir, 'abs_diff.htm') response = table_diff( self.thresh_dict, os.path.join(self.diff_files_dir, 'eplustbl_row_reorder_case_change_base.htm'), os.path.join(self.diff_files_dir, 'eplustbl_row_reorder_case_change_mod.htm'), - os.path.join(self.temp_output_dir, 'abs_diff.htm'), + abs_diff, os.path.join(self.temp_output_dir, 'rel_diff.htm'), os.path.join(self.temp_output_dir, 'math_diff.log'), os.path.join(self.temp_output_dir, 'summary.htm'), @@ -594,11 +644,15 @@ def test_reordering_with_case_only_key_column_changes_and_real_value_diff(self): self.assertEqual(1, response[1]) # count_of_tables self.assertEqual(1, response[2]) # big diffs self.assertEqual(0, response[3]) # small diffs - self.assertEqual(7, response[4]) # equals - self.assertEqual(0, response[5]) # string diffs + self.assertEqual(3, response[4]) # equals + self.assertEqual(4, response[5]) # string diffs self.assertEqual(0, response[6]) # size errors self.assertEqual(0, response[7]) # in file 2 but not in file 1 self.assertEqual(0, response[8]) # in file 1 but not in file 2 + self.assertIn( + 'COIL:COOLING:DX:VARIABLEREFRIGERANTFLOW vs Coil:Cooling:DX:VariableRefrigerantFlow', + Path(abs_diff).read_text() + ) # it seems like this is something that table_diff just cannot handle. The duplicate empty column heading is causing # major problems. I'm going to skip this test for now, but leave the two table diff resource files in place From 878d29a5c496feb857dfbc759d92e4981a060c88 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 15 Apr 2026 21:57:48 -0600 Subject: [PATCH 3/4] add test files --- .../eplustbl_program_versiondiff_base.htm | 18 +++++++++++ .../eplustbl_program_versiondiff_mod.htm | 18 +++++++++++ .../eplustbl_row_label_string_diff_base.htm | 18 +++++++++++ .../eplustbl_row_label_string_diff_mod.htm | 18 +++++++++++ .../eplustbl_row_reorder_case_change_base.htm | 31 +++++++++++++++++++ .../eplustbl_row_reorder_case_change_mod.htm | 31 +++++++++++++++++++ 6 files changed, 134 insertions(+) create mode 100644 energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_base.htm create mode 100644 energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_mod.htm create mode 100644 energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_base.htm create mode 100644 energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_mod.htm create mode 100644 energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_base.htm create mode 100644 energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_mod.htm diff --git a/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_base.htm b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_base.htm new file mode 100644 index 0000000..5822f7a --- /dev/null +++ b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_base.htm @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + +
Value
Program Version and BuildEnergyPlus, Version 9.0.1-a7c9cc14ce, YMD=2018.11.20 17:26
RunPeriodSUMMER RUN
+ + diff --git a/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_mod.htm b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_mod.htm new file mode 100644 index 0000000..1050249 --- /dev/null +++ b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_program_versiondiff_mod.htm @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + +
Value
Program Version and BuildEnergyPlus, Version 9.0.1-deadbeef42, YMD=2018.11.20 17:26
RunPeriodSUMMER RUN
+ + diff --git a/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_base.htm b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_base.htm new file mode 100644 index 0000000..9c06fea --- /dev/null +++ b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_base.htm @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + +
Value
Alpha1
Beta2
+ + diff --git a/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_mod.htm b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_mod.htm new file mode 100644 index 0000000..6566960 --- /dev/null +++ b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_label_string_diff_mod.htm @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + +
Value
Alpha Renamed1
Beta2
+ + diff --git a/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_base.htm b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_base.htm new file mode 100644 index 0000000..4dec528 --- /dev/null +++ b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_base.htm @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Coil TypeCoil Final Gross Total Capacity [W]
TU1 VRF DX COOLING COILCOIL:COOLING:DX:VARIABLEREFRIGERANTFLOW100.0
TU2 VRF DX COOLING COILCOIL:COOLING:DX:VARIABLEREFRIGERANTFLOW200.0
TU1 VRF DX HEATING COILCOIL:HEATING:DX:VARIABLEREFRIGERANTFLOW300.0
TU2 VRF DX HEATING COILCOIL:HEATING:DX:VARIABLEREFRIGERANTFLOW400.0
+ + diff --git a/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_mod.htm b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_mod.htm new file mode 100644 index 0000000..cdac17a --- /dev/null +++ b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_row_reorder_case_change_mod.htm @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Coil TypeCoil Final Gross Total Capacity [W]
TU1 VRF DX COOLING COILCoil:Cooling:DX:VariableRefrigerantFlow100.0
TU1 VRF DX HEATING COILCoil:Heating:DX:VariableRefrigerantFlow300.0
TU2 VRF DX COOLING COILCoil:Cooling:DX:VariableRefrigerantFlow250.0
TU2 VRF DX HEATING COILCoil:Heating:DX:VariableRefrigerantFlow400.0
+ + From bad954ed26b079a8605fb3bb03f7ce6a28839add Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Thu, 16 Apr 2026 07:49:08 -0600 Subject: [PATCH 4/4] add more test to get to 100% coverage --- .../tests/diffs/test_table_diff.py | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/energyplus_regressions/tests/diffs/test_table_diff.py b/energyplus_regressions/tests/diffs/test_table_diff.py index 68c8be1..4fd184d 100644 --- a/energyplus_regressions/tests/diffs/test_table_diff.py +++ b/energyplus_regressions/tests/diffs/test_table_diff.py @@ -3,7 +3,15 @@ import unittest from pathlib import Path -from energyplus_regressions.diffs.table_diff import table_diff +from bs4 import BeautifulSoup + +from energyplus_regressions.diffs.table_diff import ( + hdict2soup, + match_search_rows_to_base_rows, + reorder_rows_to_match, + table_diff, + thresh_abs_rel_diff, +) from energyplus_regressions.diffs.thresh_dict import ThreshDict @@ -15,6 +23,51 @@ def setUp(self): self.temp_output_dir = tempfile.mkdtemp() self.thresh_dict = ThreshDict(os.path.join(self.diff_files_dir, 'test_table_diff.config')) + @staticmethod + def _rows_from_table(html_table): + soup = BeautifulSoup(html_table, 'html.parser') + return soup('tr')[1:] + + def test_thresh_abs_rel_diff_string_equal_after_trim(self): + self.assertEqual((0, 0, 'equal'), thresh_abs_rel_diff(0.1, 0.1, ' Value ', 'Value')) + + def test_reorder_rows_to_match_returns_original_rows_on_missing_key(self): + search_rows = ['row-a'] + reordered = reorder_rows_to_match([['a'], ['missing']], [['a']], search_rows) + self.assertIs(reordered, search_rows) + + def test_match_search_rows_to_base_rows_skips_non_unique_short_prefix(self): + base_rows = self._rows_from_table(""" + + + + +
GroupItemValue
EquipmentCoil A10
EquipmentCoil B20
+ """) + search_rows = self._rows_from_table(""" + + + + +
GroupItemValue
EquipmentCoil B21
EquipmentCoil A10
+ """) + reordered = match_search_rows_to_base_rows(base_rows, search_rows) + self.assertEqual('Coil A', reordered[0]('td')[1].get_text(strip=True)) + self.assertEqual('Coil B', reordered[1]('td')[1].get_text(strip=True)) + + def test_hdict2soup_renders_subcategory_as_plain_text(self): + soup = BeautifulSoup('', 'html.parser') + hdict2soup( + soup, + 'Heading', + 1, + {'Subcategory': ['Display Text'], 'Value': [(0, 'equal')]}, + {'Value': (0.001, 0.005)}, + ['Subcategory', 'Value'], + ) + rendered = soup.prettify() + self.assertIn('Display Text', rendered) + def test_identical_files(self): response = table_diff( self.thresh_dict,