diff --git a/doc/newsfragments/2991_changed.auto_parts_refine.rst b/doc/newsfragments/2991_changed.auto_parts_refine.rst new file mode 100644 index 000000000..f3d7e7b6e --- /dev/null +++ b/doc/newsfragments/2991_changed.auto_parts_refine.rst @@ -0,0 +1,2 @@ +Refine Auto-Part feature to handle corner cases when zero/negative/overly large number of parts are calculated. + diff --git a/testplan/runnable/base.py b/testplan/runnable/base.py index fbfb28ee2..1a7df2720 100644 --- a/testplan/runnable/base.py +++ b/testplan/runnable/base.py @@ -592,19 +592,46 @@ def _get_tasks( ) num_of_parts = 1 else: - num_of_parts = math.ceil( + # the setup time shall take no more than 50% of runtime + cap = math.ceil( time_info["execution_time"] - / ( - self.cfg.auto_part_runtime_limit - - time_info["setup_time"] - ) + / self.cfg.auto_part_runtime_limit + * 2 ) + formula = f""" + num_of_parts = math.ceil( + time_info["execution_time"] {time_info["execution_time"]} + / ( + self.cfg.auto_part_runtime_limit {self.cfg.auto_part_runtime_limit} + - time_info["setup_time"] {time_info["setup_time"]} + ) + ) +""" + try: + num_of_parts = math.ceil( + time_info["execution_time"] + / ( + self.cfg.auto_part_runtime_limit + - time_info["setup_time"] + ) + ) + except ZeroDivisionError: + self.logger.error( + f"ZeroDivisionError occurred when calculating num_of_parts for {uid}, set to 1. {formula}" + ) + num_of_parts = 1 + if num_of_parts < 1: + self.logger.error( + f"Calculated num_of_parts for {uid} is {num_of_parts}, set to 1. {formula}" + ) num_of_parts = 1 + + if num_of_parts > cap: self.logger.error( - f"Calculated num_of_parts for {uid} is {num_of_parts}," - " check the input runtime_data and auto_part_runtime_limit" + f"Calculated num_of_parts for {uid} is {num_of_parts} > cap {cap}, set to {cap}. {formula}" ) + num_of_parts = cap if "weight" not in _task_arguments: _task_arguments["weight"] = ( math.ceil( diff --git a/tests/functional/testplan/runners/pools/test_auto_part.py b/tests/functional/testplan/runners/pools/test_auto_part.py index 6d244fe59..f1f614033 100755 --- a/tests/functional/testplan/runners/pools/test_auto_part.py +++ b/tests/functional/testplan/runners/pools/test_auto_part.py @@ -97,3 +97,63 @@ def test_auto_weight_discover(): assert task.weight == 140 mockplan.run() assert pool.size == 1 + + +def test_auto_parts_zero_neg_parts(): + with tempfile.TemporaryDirectory() as runpath: + mockplan = TestplanMock( + "plan", + runpath=runpath, + merge_scheduled_parts=True, + auto_part_runtime_limit=45, + plan_runtime_target=200, + runtime_data={ + "Proj1-suite": { + "execution_time": 50, + "setup_time": 50, # setup_time > runtime_limit -> negtive num_of_parts + } + }, + ) + pool = ProcessPool(name="MyPool", size="auto") + mockplan.add_resource(pool) + current_folder = os.path.dirname(os.path.realpath(__file__)) + mockplan.schedule_all( + path=f"{current_folder}/discover_tasks", + name_pattern=r".*auto_parts_tasks\.py$", + resource="MyPool", + ) + assert len(pool.added_items) == 1 + for task in pool.added_items.values(): + assert task.weight == 100 + mockplan.run() + assert pool.size == 1 + + +def test_auto_parts_cap_parts(): + with tempfile.TemporaryDirectory() as runpath: + mockplan = TestplanMock( + "plan", + runpath=runpath, + merge_scheduled_parts=True, + auto_part_runtime_limit=45, + plan_runtime_target=200, + runtime_data={ + "Proj1-suite": { + "execution_time": 60, + "setup_time": 44, + } + }, + ) + pool = ProcessPool(name="MyPool", size="auto") + mockplan.add_resource(pool) + current_folder = os.path.dirname(os.path.realpath(__file__)) + mockplan.schedule_all( + path=f"{current_folder}/discover_tasks", + name_pattern=r".*auto_parts_tasks\.py$", + resource="MyPool", + ) + assert len(pool.added_items) == 3 + for task in pool.added_items.values(): + assert task.weight == 64 + mockplan.run() + assert pool.size == 1