diff --git a/energyplus_regressions/__init__.py b/energyplus_regressions/__init__.py index 262e46c..caaa52c 100644 --- a/energyplus_regressions/__init__.py +++ b/energyplus_regressions/__init__.py @@ -1,2 +1,2 @@ NAME = 'energyplus_regressions' -VERSION = '2.1.2' +VERSION = '2.1.3' diff --git a/energyplus_regressions/builds/base.py b/energyplus_regressions/builds/base.py index 7117a5d..411f67c 100644 --- a/energyplus_regressions/builds/base.py +++ b/energyplus_regressions/builds/base.py @@ -87,10 +87,10 @@ def should_keep(file_path: Path): filtered_list = filter(should_keep, all_idfs_relative_path) return set(filtered_list) - def set_build_directory(self, build_directory: Path): + def set_build_directory(self, build_directory: Path) -> None: raise NotImplementedError('Must implement set_build_directory(str) in derived classes') - def verify(self): + def verify(self) -> list[tuple[str, Path, bool]]: results: list[tuple[str, Path, bool]] = [] if not self.build_directory: raise Exception('Build directory has not been set with set_build_directory()') @@ -140,5 +140,5 @@ def verify(self): def get_build_tree(self) -> BuildTree: raise NotImplementedError('Must implement get_build_tree() in derived classes') - def get_idf_directory(self): + def get_idf_directory(self) -> Path: raise NotImplementedError() diff --git a/energyplus_regressions/builds/install.py b/energyplus_regressions/builds/install.py index 225a1d8..81d02d2 100644 --- a/energyplus_regressions/builds/install.py +++ b/energyplus_regressions/builds/install.py @@ -21,7 +21,7 @@ def set_build_directory(self, build_directory: Path): # For an E+ install, the source directory is kinda just the root repo self.source_directory = build_directory - def get_idf_directory(self): + def get_idf_directory(self) -> Path: if not self.build_directory: raise Exception('Build directory has not been set with set_build_directory()') return self.source_directory / 'ExampleFiles' diff --git a/energyplus_regressions/builds/makefile.py b/energyplus_regressions/builds/makefile.py index b914a2f..4091283 100644 --- a/energyplus_regressions/builds/makefile.py +++ b/energyplus_regressions/builds/makefile.py @@ -30,7 +30,7 @@ def set_build_directory(self, build_directory: Path): else: raise Exception('Could not find source directory spec in the CMakeCache file') - def get_idf_directory(self): + def get_idf_directory(self) -> Path: if not self.build_directory: raise Exception('Build directory has not been set with set_build_directory()') return self.source_directory / 'testfiles' diff --git a/energyplus_regressions/builds/visualstudio.py b/energyplus_regressions/builds/visualstudio.py index fceeeb6..607879e 100644 --- a/energyplus_regressions/builds/visualstudio.py +++ b/energyplus_regressions/builds/visualstudio.py @@ -1,6 +1,8 @@ from pathlib import Path +from typing import Callable from energyplus_regressions.builds.base import BaseBuildDirectoryStructure, BuildTree +from energyplus_regressions.structures import ConfigType class CMakeCacheVisualStudioBuildDirectory(BaseBuildDirectoryStructure): @@ -11,12 +13,27 @@ class CMakeCacheVisualStudioBuildDirectory(BaseBuildDirectoryStructure): def __init__(self): super(CMakeCacheVisualStudioBuildDirectory, self).__init__() - self.build_mode: str = 'Release' + self.build_mode: str = ConfigType.RELEASE.value - def set_build_mode(self, debug): - self.build_mode = 'Debug' if debug else 'Release' + def set_build_mode(self, config: ConfigType, error_callback: Callable[[str], None] | None = None) -> None: + build_mode_folder = config.value + desired_build_directory = self.build_directory / 'Products' / build_mode_folder + if desired_build_directory.exists(): + self.build_mode = config.value + else: + if error_callback: + error_callback( + f"Attempted to set build mode as {config.value} but did not detect build dir, use caution! :)" + ) + build_mode_folder = 'Release' + release_folder = self.build_directory / 'Products' / build_mode_folder + release_folder_exists = release_folder.exists() + if release_folder_exists: + self.build_mode = ConfigType.RELEASE.value + else: # Finally, if we can't find release either, just set it to debug and let the user deal with it + self.build_mode = ConfigType.DEBUG.value - def set_build_directory(self, build_directory: Path): + def set_build_directory(self, build_directory: Path) -> None: """ This method takes a build directory, and updates any dependent member variables, in this case the source dir. This method *does* allow an invalid build_directory, as could happen during program initialization @@ -37,15 +54,8 @@ def set_build_directory(self, build_directory: Path): break else: raise Exception('Could not find source directory spec in the CMakeCache file') - build_mode_folder = 'Release' - release_folder = self.build_directory / 'Products' / build_mode_folder - release_folder_exists = release_folder.exists() - if release_folder_exists: - self.set_build_mode(debug=False) - else: - self.set_build_mode(debug=True) - def get_idf_directory(self): + def get_idf_directory(self) -> Path: if not self.build_directory: raise Exception('Build directory has not been set with set_build_directory()') return self.source_directory / 'testfiles' diff --git a/energyplus_regressions/diffs/table_diff.py b/energyplus_regressions/diffs/table_diff.py index d1e3e82..b45e8a8 100755 --- a/energyplus_regressions/diffs/table_diff.py +++ b/energyplus_regressions/diffs/table_diff.py @@ -121,8 +121,12 @@ def thresh_abs_rel_diff(abs_thresh, rel_thresh, x, y): # else: # diff = 'equal' return abs_diff, rel_diff, diff - except: - return '%s vs %s' % (x, y), '%s vs %s' % (x, y), 'stringdiff' + 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(): + return 0, 0, 'equal' + else: + return '%s vs %s' % (x, y), '%s vs %s' % (x, y), 'stringdiff' def prev_sib(entity): diff --git a/energyplus_regressions/energyplus.py b/energyplus_regressions/energyplus.py index f687606..70d7f27 100755 --- a/energyplus_regressions/energyplus.py +++ b/energyplus_regressions/energyplus.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python import glob from os import chdir, getcwd, rename, environ from pathlib import Path @@ -8,8 +7,6 @@ from energyplus_regressions.builds.base import BuildTree from energyplus_regressions.structures import ForceRunType -script_dir = Path(__file__).resolve().parent - class ExecutionArguments: def __init__(self, build_tree: BuildTree, entry_name: str, test_run_directory: Path, @@ -24,17 +21,17 @@ def __init__(self, build_tree: BuildTree, entry_name: str, test_run_directory: P # noinspection PyBroadException -def execute_energyplus(e_args: ExecutionArguments): +def execute_energyplus(e_args: ExecutionArguments) -> tuple[Path, str, bool, bool, str]: # set up a few paths energyplus = e_args.build_tree.energyplus basement = e_args.build_tree.basement idd_path = e_args.build_tree.idd_path slab = e_args.build_tree.slab - basementidd = e_args.build_tree.basementidd - slabidd = e_args.build_tree.slabidd - expandobjects = e_args.build_tree.expandobjects - epmacro = e_args.build_tree.epmacro - readvars = e_args.build_tree.readvars + basement_idd = e_args.build_tree.basementidd + slab_idd = e_args.build_tree.slabidd + expand_objects = e_args.build_tree.expandobjects + ep_macro = e_args.build_tree.epmacro + read_vars = e_args.build_tree.readvars parametric = e_args.build_tree.parametric # Save the current path so we can go back here @@ -61,7 +58,7 @@ def execute_energyplus(e_args: ExecutionArguments): imf_path = e_args.test_run_directory / 'in.imf' ght_file = e_args.test_run_directory / 'GHTIn.idf' basement_file = e_args.test_run_directory / 'BasementGHTIn.idf' - epjson_file = e_args.test_run_directory / 'in.epJSON' + ep_json_file = e_args.test_run_directory / 'in.epJSON' rvi_file = e_args.test_run_directory / 'in.rvi' mvi_file = e_args.test_run_directory / 'in.mvi' @@ -79,7 +76,7 @@ def execute_energyplus(e_args: ExecutionArguments): for line in newlines: f.write(line) macro_run = subprocess.Popen( - str(epmacro), shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE + str(ep_macro), shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) o, e = macro_run.communicate() std_out += o @@ -102,12 +99,13 @@ def execute_energyplus(e_args: ExecutionArguments): idf_file.unlink() rename(file_to_run_here, idf_file) else: - return [e_args.build_tree.build_dir, e_args.entry_name, False, False, "Issue with Parametrics"] + return e_args.build_tree.build_dir, e_args.entry_name, False, False, "Issue with Parametric" # Run ExpandObjects and process as necessary, but not for epJSON files! if idf_file.exists(): expand_objects_run = subprocess.Popen( - str(expandobjects), shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE + str(expand_objects), shell=True, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE ) o, e = expand_objects_run.communicate() std_out += o @@ -118,7 +116,7 @@ def execute_energyplus(e_args: ExecutionArguments): rename(expanded_file, idf_file) if basement_file.exists(): - shutil.copy(basementidd, e_args.test_run_directory) + shutil.copy(basement_idd, e_args.test_run_directory) basement_environment = environ.copy() basement_environment['CI_BASEMENT_NUMYEARS'] = '2' basement_run = subprocess.Popen( @@ -140,7 +138,7 @@ def execute_energyplus(e_args: ExecutionArguments): (e_args.test_run_directory / 'BasementGHT.idd').unlink() if ght_file.exists(): - shutil.copy(slabidd, e_args.test_run_directory) + shutil.copy(slab_idd, e_args.test_run_directory) slab_run = subprocess.Popen( str(slab), shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) @@ -184,7 +182,7 @@ def execute_energyplus(e_args: ExecutionArguments): # Execute EnergyPlus try: command_line = str(energyplus) - if epjson_file.exists(): + if ep_json_file.exists(): command_line += ' in.epJSON' std_out += subprocess.check_output( command_line, shell=True, stdin=subprocess.DEVNULL, stderr=subprocess.PIPE @@ -193,23 +191,23 @@ def execute_energyplus(e_args: ExecutionArguments): ... # so I can verify that I hit this during the test_case_b_crash test, but if I just have the return in # here alone, it shows as missing on the coverage...wonky - return [e_args.build_tree.build_dir, e_args.entry_name, False, False, str(e)] + return e_args.build_tree.build_dir, e_args.entry_name, False, False, str(e) - # Execute readvars + # Execute read-vars if rvi_file.exists(): csv_run = subprocess.Popen( - str(readvars) + ' in.rvi', shell=True, stdin=subprocess.DEVNULL, + str(read_vars) + ' in.rvi', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) else: csv_run = subprocess.Popen( - str(readvars), shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + str(read_vars), shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE) o, e = csv_run.communicate() std_out += o std_err += e if mvi_file.exists(): mtr_run = subprocess.Popen( - str(readvars) + ' in.mvi', shell=True, stdin=subprocess.DEVNULL, + str(read_vars) + ' in.mvi', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) else: @@ -217,7 +215,7 @@ def execute_energyplus(e_args: ExecutionArguments): f.write("eplusout.mtr\n") f.write("eplusmtr.csv\n") mtr_run = subprocess.Popen( - str(readvars) + ' in.mvi', shell=True, stdin=subprocess.DEVNULL, + str(read_vars) + ' in.mvi', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) o, e = mtr_run.communicate() @@ -232,11 +230,10 @@ def execute_energyplus(e_args: ExecutionArguments): f.write(std_err.decode('utf-8')) new_idd_path.unlink() - return [e_args.build_tree.build_dir, e_args.entry_name, True, False] + chdir(start_path) + return e_args.build_tree.build_dir, e_args.entry_name, True, False, "" except Exception as e: print("**" + str(e)) - return [e_args.build_tree.build_dir, e_args.entry_name, False, False, str(e)] - - finally: chdir(start_path) + return e_args.build_tree.build_dir, e_args.entry_name, False, False, str(e) diff --git a/energyplus_regressions/runtests.py b/energyplus_regressions/runtests.py index 4e6789d..182f204 100755 --- a/energyplus_regressions/runtests.py +++ b/energyplus_regressions/runtests.py @@ -17,7 +17,7 @@ from difflib import unified_diff # python's own diff library -from energyplus_regressions.builds.base import BuildTree +from energyplus_regressions.builds.base import BuildTree, BaseBuildDirectoryStructure from energyplus_regressions.diffs import math_diff, table_diff, thresh_dict as td from energyplus_regressions.energyplus import ExecutionArguments, execute_energyplus from energyplus_regressions.structures import ( @@ -41,8 +41,9 @@ class TestRunConfiguration: __test__ = False # so that PyTest doesn't try to run this as a class fixture - def __init__(self, force_run_type, num_threads, report_freq, build_a, build_b, single_test_run=False, - force_output_sql: ForceOutputSQL = ForceOutputSQL.NOFORCE, + def __init__(self, force_run_type: str, num_threads: int, report_freq: str, + build_a: BaseBuildDirectoryStructure, build_b: BaseBuildDirectoryStructure, + single_test_run: bool = False, force_output_sql: ForceOutputSQL = ForceOutputSQL.NOFORCE, force_output_sql_unitconv: ForceOutputSQLUnitConversion = ForceOutputSQLUnitConversion.NOFORCE): self.force_run_type = force_run_type self.TestOneFile = single_test_run @@ -57,7 +58,7 @@ def __init__(self, force_run_type, num_threads, report_freq, build_a, build_b, s class TestCaseCompleted: __test__ = False # so that PyTest doesn't try to run this as a class fixture - def __init__(self, run_directory, case_name, run_status, error_msg_reported_already, extra_message=""): + def __init__(self, run_directory: str, case_name: str, run_status, error_msg_reported_already, extra_message=""): self.run_directory = run_directory self.case_name = case_name self.run_success = run_status @@ -68,7 +69,7 @@ def __init__(self, run_directory, case_name, run_status, error_msg_reported_alre # the actual main test suite run class class SuiteRunner: - def __init__(self, run_config, these_entries, mute=False): + def __init__(self, run_config: TestRunConfiguration, these_entries, mute=False): # initialize the master mute button -- this is overridden by registering callbacks self.mute = mute @@ -446,7 +447,7 @@ def run_build(self, build_tree: BuildTree): local_run_type, self.min_reporting_freq, parametric_file, - epw_path + str(epw_path) ) ) diff --git a/energyplus_regressions/structures.py b/energyplus_regressions/structures.py index 554306a..5950090 100755 --- a/energyplus_regressions/structures.py +++ b/energyplus_regressions/structures.py @@ -52,6 +52,11 @@ class ForceOutputSQLUnitConversion(Enum): InchPound = 'InchPound' +class ConfigType(Enum): + RELEASE = "Release" + DEBUG = "Debug" + + class Results: def __init__(self): self.descriptions = {} diff --git a/energyplus_regressions/tests/builds/test_visualstudio.py b/energyplus_regressions/tests/builds/test_visualstudio.py index ccdb3cb..24d4ad9 100644 --- a/energyplus_regressions/tests/builds/test_visualstudio.py +++ b/energyplus_regressions/tests/builds/test_visualstudio.py @@ -5,6 +5,7 @@ from energyplus_regressions.builds.base import BuildTree from energyplus_regressions.builds.visualstudio import CMakeCacheVisualStudioBuildDirectory +from energyplus_regressions.structures import ConfigType class TestVisualStudioBuildMethods(unittest.TestCase): @@ -13,6 +14,7 @@ def setUp(self): self.build = CMakeCacheVisualStudioBuildDirectory() self.run_dir = Path(tempfile.mkdtemp()) self.dummy_source_dir = Path('/dummy/source/dir') + self.output_message: str = "" def set_cache_file(self): with open(os.path.join(self.run_dir, 'CMakeCache.txt'), 'w') as f: @@ -77,3 +79,31 @@ def test_get_idf_dir(self): self.build.set_build_directory(self.run_dir) idf_dir = self.build.get_idf_directory() self.assertEqual(self.dummy_source_dir / 'testfiles', idf_dir) + + def test_set_build_mode_folder_exists(self): + self.set_cache_file() + self.build.set_build_directory(self.run_dir) + os.makedirs(os.path.join(self.run_dir, 'Products', 'Release')) + self.build.set_build_mode(ConfigType.RELEASE) + self.assertEqual(ConfigType.RELEASE.value, self.build.build_mode) + os.makedirs(os.path.join(self.run_dir, 'Products', 'Debug')) + self.build.set_build_mode(ConfigType.DEBUG) + self.assertEqual(ConfigType.DEBUG.value, self.build.build_mode) + + def _message_capture(self, s: str): + self.output_message = s + + def test_set_build_debug_does_not_exist(self): + self.set_cache_file() + self.build.set_build_directory(self.run_dir) + self.build.set_build_mode(ConfigType.DEBUG, self._message_capture) + self.assertIn("caution", self.output_message.lower()) + self.assertEqual(ConfigType.DEBUG.value, self.build.build_mode) # should fall to debug type + + def test_set_build_debug_does_not_exist_release_does(self): + self.set_cache_file() + self.build.set_build_directory(self.run_dir) + os.makedirs(os.path.join(self.run_dir, 'Products', 'Release')) + self.build.set_build_mode(ConfigType.DEBUG, self._message_capture) + self.assertIn("caution", self.output_message.lower()) + self.assertEqual(ConfigType.RELEASE.value, self.build.build_mode) # should fall to release type diff --git a/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_has_string_diff_case_only.htm b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_has_string_diff_case_only.htm new file mode 100644 index 0000000..4ed2df1 --- /dev/null +++ b/energyplus_regressions/tests/diffs/tbl_resources/eplustbl_has_string_diff_case_only.htm @@ -0,0 +1,100 @@ + + + + Bldg DENVER CENTENNIAL ANN CLG 1% CONDNS DB=>MWB ** + 2018-11-20 + 17:26:37 + - EnergyPlus + + + +

Table of Contents

+ +

Program Version:EnergyPlus, Version 9.0.1-a7c9cc14ce, YMD=2018.11.20 17:26

+

Tabular Output Report in Format: HTML

+

Building: Bldg

+

Environment: DENVER CENTENNIAL ANN CLG 1% CONDNS DB=>MWB **

+

Simulation Timestamp: 2018-11-20 + 17:26:37

+
+

Table of Contents

+ +

Report: Annual Building Utility Performance Summary

+

For: Entire Facility

+

Timestamp: 2018-11-20 + 17:26:37

+Values gathered over 0.00 hours

+WARNING: THE REPORT DOES NOT REPRESENT A FULL ANNUAL SIMULATION.

+

+Site and Source Energy

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Total Energy [GJ]Energy Per Total Building Area [MJ/m2]Energy Per Conditioned Building Area [MJ/m2]
Total Site Energy hello 0.00 0.00
Net Site Energy 0.00 0.00 0.00
Total Source Energy 0.00 0.00 0.00
Net Source Energy 0.00 0.00 0.00
+

+Site to Source Energy Conversion Factors

+ + + + + + + + + + + + + +
Site=>Source Conversion Factor
Electricity 3.167
Natural Gas 1.084
+

+Building Area

+ + + + + + + + + + + + + + + + + +
Area [m2]
Total Building Area 232.26
Net Conditioned Building Area 232.26
Unconditioned Building Area 0.00
+

+ + diff --git a/energyplus_regressions/tests/diffs/test_table_diff.py b/energyplus_regressions/tests/diffs/test_table_diff.py index e9c9b1e..6747d3c 100644 --- a/energyplus_regressions/tests/diffs/test_table_diff.py +++ b/energyplus_regressions/tests/diffs/test_table_diff.py @@ -194,6 +194,27 @@ def test_string_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_string_diff_case_change_only(self): + # should be no diffs for case-insensitive comparison + 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'), + 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(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(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_malformed_table_heading_in_file_1(self): response = table_diff( self.thresh_dict, diff --git a/energyplus_regressions/tk_window.py b/energyplus_regressions/tk_window.py index ddae5d7..cf7bddb 100644 --- a/energyplus_regressions/tk_window.py +++ b/energyplus_regressions/tk_window.py @@ -35,6 +35,7 @@ from energyplus_regressions.runtests import TestRunConfiguration, SuiteRunner from energyplus_regressions.structures import ( CompletedStructure, + ConfigType, ForceOutputSQL, ForceOutputSQLUnitConversion, ForceRunType, @@ -185,6 +186,8 @@ def __init__(self): self.force_output_sql.set(ForceOutputSQL.NOFORCE.value) self.force_output_sql_unitconv = StringVar() self.force_output_sql_unitconv.set(ForceOutputSQLUnitConversion.NOFORCE.value) + self.preferred_build_type = StringVar() + self.preferred_build_type.set(ConfigType.RELEASE.value) self.num_threads_var = StringVar() # widgets that we might want to access later @@ -192,23 +195,17 @@ def __init__(self): self.build_dir_2_button = None self.run_button = None self.stop_button = None - self.build_dir_1_label = None if system() == 'Windows': - self.build_dir_1_var.set(r'C:\EnergyPlus\repos\1eplus\builds\VS64') # "") + self.build_dir_1_var.set('/Users/elee/eplus/repos/1eplus/builds/r') + self.build_dir_2_var.set('/Users/elee/eplus/repos/2eplus/builds/r') elif system() == 'Linux': - self.build_dir_1_var.set('/eplus/repos/1eplus/builds/r') # "") - self.build_dir_2_label = None - if system() == 'Windows': - self.build_dir_2_var.set(r'C:\EnergyPlus\repos\2eplus\builds\VS64') # "") - elif system() == 'Linux': - self.build_dir_2_var.set('/eplus/repos/2eplus/builds/r') # "") self.progress = None self.log_message_listbox = None @@ -227,17 +224,18 @@ def __init__(self): self.reporting_frequency_option_menu = None self.force_output_sql_option_menu = None self.force_output_sql_unitconv_option_menu = None + self.set_preferred_build_type = None self.idf_select_from_containing_button = None # some data holders self.tree_folders = dict() self.valid_idfs_in_listing = False - self.build_1 = None - self.build_2 = None + self.build_1: BaseBuildDirectoryStructure | None = None + self.build_2: BaseBuildDirectoryStructure | None = None self.last_results = None self.auto_saving = False self.manually_saving = False - self.save_interval = 10000 # ms, so 1 minute + self.save_interval_ms = 60_000 # ms, so 1 minute # initialize the GUI self.main_notebook = None @@ -247,7 +245,10 @@ def __init__(self): self.client_open(auto_open=True) # PyCharm is relentlessly complaining the unused *args parameter to root.after, when it's not needed # noinspection PyTypeChecker - self.root.after(self.save_interval, self.auto_save) + self.root.after(self.save_interval_ms, self.auto_save) + + # set up any Var traces here after the init is all done + self.preferred_build_type.trace_add("write", self.refresh_builds_for_build_type_change) # wire up the background thread pub.subscribe(self.print_handler, PubSubMessageTypes.PRINT) @@ -259,7 +260,7 @@ def __init__(self): pub.subscribe(self.cancelled_handler, PubSubMessageTypes.CANCELLED) # on Linux, initialize the notification class instance - self.notification = None + self.notification: Notification | None = None if system() == 'Linux': self.notification_icon = Path(self.icon_path) self.notification = Notification('energyplus_regression_runner') @@ -267,7 +268,7 @@ def __init__(self): def init_window(self): # changing the title of our master widget self.root.title("EnergyPlus Regression Tool") - self.root.protocol("WM_DELETE_WINDOW", self.client_exit) + self.root.protocol("WM_DELETE_WINDOW", self.app_exit) # create the menu menu = Menu(self.root) @@ -275,7 +276,7 @@ def init_window(self): file_menu = Menu(menu) file_menu.add_command(label="Open Project...", command=self.client_open) file_menu.add_command(label="Save Project...", command=self.client_save) - file_menu.add_command(label="Exit", command=self.client_exit) + file_menu.add_command(label="Exit", command=self.app_exit) menu.add_cascade(label="File", menu=file_menu) help_menu = Menu(menu) help_menu.add_command(label="Open Documentation...", command=self.open_documentation) @@ -299,40 +300,49 @@ def init_window(self): self.build_dir_1_button = ttk.Button(group_build_dir_1, text="Change...", command=self.client_build_dir_1, style="C.TButton") self.build_dir_1_button.grid(row=1, column=1, sticky=W) - self.build_dir_1_label = Label(group_build_dir_1, textvariable=self.build_dir_1_var) - self.build_dir_1_label.grid(row=1, column=2, sticky=E) + build_dir_1_label = Label(group_build_dir_1, textvariable=self.build_dir_1_var) + build_dir_1_label.grid(row=1, column=2, sticky=E) group_build_dir_2 = LabelFrame(pane_run, text="Build Directory 2") group_build_dir_2.pack(fill=X, padx=5) self.build_dir_2_button = ttk.Button(group_build_dir_2, text="Change...", command=self.client_build_dir_2, style="C.TButton") self.build_dir_2_button.grid(row=1, column=1, sticky=W) - self.build_dir_2_label = Label(group_build_dir_2, textvariable=self.build_dir_2_var) - self.build_dir_2_label.grid(row=1, column=2, sticky=E) + build_dir_2_label = Label(group_build_dir_2, textvariable=self.build_dir_2_var) + build_dir_2_label.grid(row=1, column=2, sticky=E) group_run_options = LabelFrame(pane_run, text="Run Options") group_run_options.pack(fill=X, padx=5) + # row 1 Label(group_run_options, text="Number of threads for suite: ").grid(row=1, column=1, sticky=E) self.num_threads_spinner = Spinbox(group_run_options, from_=1, to=48, textvariable=self.num_threads_var) self.num_threads_spinner.grid(row=1, column=2, sticky=W) + # row 2 Label(group_run_options, text="Test suite run configuration: ").grid(row=2, column=1, sticky=E) self.run_period_option_menu = OptionMenu(group_run_options, self.run_period_option, *ForceRunType.get_all()) self.run_period_option_menu.grid(row=2, column=2, sticky=W) + # row 3 Label(group_run_options, text="Minimum reporting frequency: ").grid(row=3, column=1, sticky=E) self.reporting_frequency_option_menu = OptionMenu( group_run_options, self.reporting_frequency, *ReportingFreq.get_all() ) self.reporting_frequency_option_menu.grid(row=3, column=2, sticky=W) - + # row 4 Label(group_run_options, text="Force Output SQL: ").grid(row=4, column=1, sticky=E) self.force_output_sql_option_menu = OptionMenu( group_run_options, self.force_output_sql, *[x.value for x in ForceOutputSQL] ) self.force_output_sql_option_menu.grid(row=4, column=2, sticky=W) - + # row 5 Label(group_run_options, text="Force Output SQL UnitConv: ").grid(row=5, column=1, sticky=E) self.force_output_sql_unitconv_option_menu = OptionMenu( group_run_options, self.force_output_sql_unitconv, *[x.value for x in ForceOutputSQLUnitConversion] ) self.force_output_sql_unitconv_option_menu.grid(row=5, column=2, sticky=W) + # row 6 + Label(group_run_options, text="Multi-configuration Build Preference: ").grid(row=6, column=1, sticky=E) + self.set_preferred_build_type = OptionMenu( + group_run_options, self.preferred_build_type, *[x.value for x in ConfigType] + ) + self.set_preferred_build_type.grid(row=6, column=2, sticky=W) self.main_notebook.add(pane_run, text='Configuration') @@ -483,13 +493,17 @@ def client_open(self, auto_open=False): self.reporting_frequency.set(data['report_freq']) self.force_output_sql.set(data['force_output_sql']) self.force_output_sql_unitconv.set(data['force_output_sql_unitconv']) - - status = self.try_to_set_build_1_to_dir(Path(data['build_1_build_dir'])) + if 'preferred_build_type' in data: # it's initialized to RELEASE in the window __init__, override if found + self.preferred_build_type.set(data['preferred_build_type']) + # try to set build 1 object, where it will try to use the preferred build type + status = self.try_to_set_build_1_to_dir(Path(data['build_1_build_dir']), init_mode=True) if status: self.build_dir_1_var.set(data['build_1_build_dir']) - status = self.try_to_set_build_2_to_dir(Path(data['build_2_build_dir'])) + # try to set build 2 object, where it will try to use the preferred build type + status = self.try_to_set_build_2_to_dir(Path(data['build_2_build_dir']), init_mode=True) if status: self.build_dir_2_var.set(data['build_2_build_dir']) + # at this point we should have build dirs, but it's OK if they are invalid self.build_idf_listing(False, data['idfs']) self.add_to_log("Project settings loaded") except Exception: @@ -503,7 +517,7 @@ def auto_save(self): self.client_save(auto_save=True) # PyCharm is relentlessly complaining the unused *args parameter to root.after, when it's not needed # noinspection PyTypeChecker - self.root.after(self.save_interval, self.auto_save) + self.root.after(self.save_interval_ms, self.auto_save) def client_save(self, auto_save=False): # we shouldn't come into this function from the auto_save if any other saving is going on already @@ -534,6 +548,7 @@ def client_save(self, auto_save=False): 'build_1_build_dir': str(self.build_1.build_directory), 'build_2_build_dir': str(self.build_2.build_directory), 'last_results': these_results, + 'preferred_build_type': self.preferred_build_type.get(), } except Exception as e: # if we hit an exception, our action depends on whether we are manually saving or auto-saving @@ -583,7 +598,7 @@ def results_popup(self, event): if title.startswith('Case '): if title.endswith('(0)'): context_menu = Menu(self, tearoff=0) - context_menu.add_command(label="Selected Node Has No Children", command=self.dummy) + context_menu.add_command(label="Selected Node Has No Children", command=lambda: None) context_menu.post(event.x_root, event.y_root) else: tags = self.results_tree.item(iid, "tags") @@ -598,9 +613,6 @@ def copy_lambda(): # ignoring anything but the tree root nodes pass - def dummy(self): - pass - def copy_selected_node(self, tags): string = ';'.join(tags) self.root.clipboard_clear() @@ -762,7 +774,7 @@ def build_results_tree(self, results: CompletedStructure = None): ) self.last_results = results - def add_to_log(self, message): + def add_to_log(self, message: str): if self.log_message_listbox: self.log_message_listbox.insert(END, f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]: {message}") self.log_message_listbox.yview(END) @@ -947,11 +959,21 @@ def set_gui_status_for_run(self, is_running: bool): self.reporting_frequency_option_menu.configure(state=run_button_state) self.force_output_sql_option_menu.configure(state=run_button_state) self.force_output_sql_unitconv_option_menu.configure(state=run_button_state) + self.set_preferred_build_type.configure(state=run_button_state) self.num_threads_spinner.configure(state=run_button_state) self.stop_button.configure(state=stop_button_state) self.main_notebook.tab(3, text=results_tab_title) - def try_to_set_build_1_to_dir(self, selected_dir: Path) -> bool: + def refresh_builds_for_build_type_change(self, *_): + # just try to refresh both build directories, it will warn if the debug/release folders aren't there + self.try_to_set_build_1_to_dir(self.build_1.build_directory) + self.try_to_set_build_2_to_dir(self.build_2.build_directory) + + def add_to_log_and_alert(self, message: str): + self.add_to_log(message) + messagebox.showerror("Error", message) + + def try_to_set_build_1_to_dir(self, selected_dir: Path, init_mode: bool = False) -> bool: probable_build_dir_type = autodetect_build_dir_type(selected_dir) if probable_build_dir_type == KnownBuildTypes.Unknown: self.add_to_log("Could not detect build 1 type") @@ -964,6 +986,10 @@ def try_to_set_build_1_to_dir(self, selected_dir: Path) -> bool: self.add_to_log("Build 1 type detected as a Visual Studio build") self.build_1 = CMakeCacheVisualStudioBuildDirectory() self.build_1.set_build_directory(selected_dir) + if init_mode: + self.build_1.set_build_mode(ConfigType(self.preferred_build_type.get()), self.add_to_log) + else: + self.build_1.set_build_mode(ConfigType(self.preferred_build_type.get()), self.add_to_log_and_alert) elif probable_build_dir_type == KnownBuildTypes.Makefile: self.add_to_log("Build 1 type detected as a Makefile-style build") self.build_1 = CMakeCacheMakeFileBuildDirectory() @@ -986,7 +1012,7 @@ def client_build_dir_1(self): self.build_dir_1_var.set(str(selected_dir)) self.build_idf_listing() - def try_to_set_build_2_to_dir(self, selected_dir: Path) -> bool: + def try_to_set_build_2_to_dir(self, selected_dir: Path, init_mode: bool = False) -> bool: probable_build_dir_type = autodetect_build_dir_type(selected_dir) if probable_build_dir_type == KnownBuildTypes.Unknown: self.add_to_log("Could not detect build 2 type") @@ -999,6 +1025,10 @@ def try_to_set_build_2_to_dir(self, selected_dir: Path) -> bool: self.add_to_log("Build 2 type detected as a Visual Studio build") self.build_2 = CMakeCacheVisualStudioBuildDirectory() self.build_2.set_build_directory(selected_dir) + if init_mode: + self.build_2.set_build_mode(ConfigType(self.preferred_build_type.get()), self.add_to_log) + else: + self.build_2.set_build_mode(ConfigType(self.preferred_build_type.get()), self.add_to_log_and_alert) elif probable_build_dir_type == KnownBuildTypes.Makefile: self.add_to_log("Build 2 type detected as a Makefile-style build") self.build_2 = CMakeCacheMakeFileBuildDirectory() @@ -1034,7 +1064,9 @@ def client_run(self): return ok_or_cancel_msg = "Press OK to continue anyway (risky!), or press Cancel to abort" build_1_valid = self.build_1.verify() - build_1_problem_files = [b[1] for b in build_1_valid if not b[2]] + if isinstance(self.build_1, CMakeCacheVisualStudioBuildDirectory): + self.build_1.set_build_mode(ConfigType(self.preferred_build_type.get()), self.add_to_log_and_alert) + build_1_problem_files = [str(b[1]) for b in build_1_valid if not b[2]] if len(build_1_problem_files): missing_files = '\n'.join(build_1_problem_files) r = messagebox.askokcancel("Build folder 1 problem", f"Missing files:\n{missing_files}\n{ok_or_cancel_msg}") @@ -1044,7 +1076,9 @@ def client_run(self): messagebox.showerror("Build folder 2 problem", "Select a valid build folder 2 prior to running") return build_2_valid = self.build_2.verify() - build_2_problem_files = [b[1] for b in build_2_valid if not b[2]] + if isinstance(self.build_2, CMakeCacheVisualStudioBuildDirectory): + self.build_2.set_build_mode(ConfigType(self.preferred_build_type.get()), self.add_to_log_and_alert) + build_2_problem_files = [str(b[1]) for b in build_2_valid if not b[2]] if len(build_2_problem_files): missing_files = '\n'.join(build_2_problem_files) r = messagebox.askokcancel("Build folder 2 problem", f"Missing files:\n{missing_files}\n{ok_or_cancel_msg}") @@ -1175,10 +1209,11 @@ def client_stop(self): self.label_string.set("Attempting to cancel...") self.background_operator.interrupt_please() - def client_exit(self): + def app_exit(self): if self.long_thread: messagebox.showerror("Uh oh!", "Cannot exit program while operations are running; abort them then exit") return + self.client_save(auto_save=True) sys.exit() def client_done(self): diff --git a/requirements.txt b/requirements.txt index 57e2b54..1ad7e04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ pypubsub beautifulsoup4==4.12.3 # for running tests +coverage coveralls flake8 pytest