Skip to content

Commit

Permalink
Merge branch 'main' into re-test-naming
Browse files Browse the repository at this point in the history
  • Loading branch information
zhenyu-ms committed Oct 27, 2023
2 parents fb3cd6f + 5b1e10c commit 42328d6
Show file tree
Hide file tree
Showing 17 changed files with 250 additions and 180 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Environment control button on the interactive UI now provides visual feedback on ongoing start and stop processes.
12 changes: 12 additions & 0 deletions testplan/common/entity/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Optional,
Tuple,
Union,
Any,
)

import psutil
Expand Down Expand Up @@ -741,6 +742,17 @@ def filter_locals(cls, local_vars):
if key not in EXCLUDE and value is not None
}

def context_input(self) -> Dict[str, Any]:
"""All attr of self in a dict for context resolution"""
ctx = {}
for attr in dir(self):
if attr == "env":
ctx["env"] = self._env
elif attr:
ctx[attr] = getattr(self, attr)
return ctx
# return {attr: getattr(self, attr) for attr in dir(self)}


class RunnableStatus(EntityStatus):
"""
Expand Down
36 changes: 1 addition & 35 deletions testplan/common/utils/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,43 +97,9 @@ def expand(value, contextobj, constructor=None):
return value


def expand_env(
orig: Dict[str, str],
overrides: Dict[str, Union[str, ContextValue]],
contextobj,
):
"""
Copies the `orig` dict of environment variables.
Applies specified overrides.
Removes keys that have value of None as override.
Expands context values as strings.
Returns as a copy.
:param orig: The initial environment variables. Usually `os.environ` is to
be passed in. This will not be modified.
:type orig: ``dict`` of ``str`` to ``str``
:param overrides: Keys and values to be overriden. Values can be strings or
context objects.
:type overrides: ``dict`` of ``str`` to either ``str`` or ``ContextValue``
:param contextobj: The context object that can be used to expand context
values.
:type contextobj: ``object``
:return: Copied, overridden and expanded environment variables
:rtype: ``dict``
"""
env = orig.copy()
env.update(overrides)
return {
key: expand(val, contextobj, str)
for key, val in env.items()
if val is not None
}


def render(template: Union[Template, TempitaTemplate, str], context) -> str:
"""
Renders the template with the given context, tthat used for expression resolution.
Renders the template with the given context, that used for expression resolution.
:param template: A template in str, Jinja2 or Tempita form that will be rendered with the context
:type template: Union[Template, TempitaTemplate, str]
Expand Down
71 changes: 38 additions & 33 deletions testplan/runnable/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,10 +592,13 @@ def _get_tasks(
- time_info["setup_time"]
)
)
if num_of_parts < 1:
raise RuntimeError(
f"Calculated num_of_parts for {uid} is {num_of_parts},"
" check the input runtime_data and auto_part_runtime_limit"
)
if "weight" not in _task_arguments:
_task_arguments["weight"] = _task_arguments.get(
"weight", None
) or (
_task_arguments["weight"] = (
math.ceil(
(time_info["execution_time"] / num_of_parts)
+ time_info["setup_time"]
Expand Down Expand Up @@ -724,6 +727,21 @@ def discover(

return [_cache_task_info(task_info) for task_info in tasks]

def calculate_pool_size(self) -> None:
"""
Calculate the right size of the pool based on the weight (runtime) of the tasks,
so that runtime of all tasks meets the plan_runtime_target.
"""
for executor in self.resources:
if isinstance(executor, Pool) and executor.is_auto_size:
pool_size = self.calculate_pool_size_by_tasks(
list(executor.added_items.values())
)
self.logger.user_info(
f"Set pool size to {pool_size} for {executor.cfg.name}"
)
executor.size = pool_size

def calculate_pool_size_by_tasks(self, tasks: Collection[Task]) -> int:
"""
Calculate the right size of the pool based on the weight (runtime) of the tasks,
Expand Down Expand Up @@ -774,8 +792,8 @@ def schedule(
:param task: Input task, if it is None, a new Task will be constructed
using the options parameter.
:type task: :py:class:`~testplan.runners.pools.tasks.base.Task`
:param resource: Name of the target executor, which is usually a remote Pool,
the by default None is indicating using local executor.
:param resource: Name of the target executor, which is usually a Pool,
default value None indicates using local executor.
:type resource: ``str`` or ``NoneType``
:param options: Task input options.
:type options: ``dict``
Expand All @@ -801,20 +819,15 @@ def schedule_all(
:type path: ``str``
:param name_pattern: a regex pattern to match the file name.
:type name_pattern: ``str``
:param resource: Name of the target executor, which is usually a remote Pool,
the by default None is indicating using local executor.
:param resource: Name of the target executor, which is usually a Pool,
default value None indicates using local executor.
:type resource: ``str`` or ``NoneType``
"""

tasks = self.discover(path=path, name_pattern=name_pattern)

for task in tasks:
self.add(task, resource=resource)
pool: Pool = self.resources[resource]
if pool.is_auto_size:
pool_size = self.calculate_pool_size_by_tasks(
list(pool.added_items.values())
)
pool.size = pool_size

def add(
self,
Expand All @@ -830,26 +843,16 @@ def add(
:param target: Test target.
:type target: :py:class:`~testplan.common.entity.base.Runnable` or
:py:class:`~testplan.runners.pools.tasks.base.Task` or ``callable``
:param resource: Name of the target executor, which is usually a remote Pool,
the by default None is indicating using local executor.
:param resource: Name of the target executor, which is usually a Pool,
default value None indicates using local executor.
:type resource: ``str`` or ``NoneType``
:return: Assigned uid for test.
:rtype: ``str`` or ```NoneType``
"""

# Get the real test entity and verify if it should be added
task_info = self._collect_task_info(target)

local_runner = self.resources.first()
resource: str = resource or local_runner

if resource not in self.resources:
raise RuntimeError(
'Resource "{}" does not exist.'.format(resource)
)

self._verify_task_info(task_info)

uid = task_info.uid

# let see if it is filtered
Expand All @@ -862,15 +865,16 @@ def add(
self.cfg.test_lister.log_test_info(task_info.materialized_test)
return None

if resource is None or self._is_interactive_run():
# use local runner for interactive
resource = self.resources.first()
# just enqueue the materialized test
target = task_info.materialized_test
else:
target = task_info.target

if self._is_interactive_run():
self._register_task_for_interactive(task_info)
# for interactive always use the local runner
resource = local_runner

target = task_info.target
# if running in the local runner we can just enqueue the materialized test
if resource == local_runner:
target = task_info.materialized_test

self._register_task(
resource, target, uid, task_info.materialized_test.get_metadata()
Expand All @@ -891,7 +895,7 @@ def _collect_task_info(self, target: TestTask) -> TaskInformation:
elif isinstance(target, Task):

# First check if there is a cached task info
# that is an optimization flew where task info
# that is an optimization flow where task info
# need to be created at discover, but the already defined api
# need to pass Task, so we attach task_info to the task itself
# and here we remove it
Expand Down Expand Up @@ -1000,6 +1004,7 @@ def pre_resource_steps(self):
self._add_step(self._record_start)
self._add_step(self.make_runpath_dirs)
self._add_step(self._configure_file_logger)
self._add_step(self.calculate_pool_size)

def main_batch_steps(self):
"""Runnable steps to be executed while resources are running."""
Expand Down
64 changes: 46 additions & 18 deletions testplan/testing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from testplan.common.remote.remote_driver import RemoteDriver
from testplan.common.utils import strings
from testplan.common.utils.context import render
from testplan.common.utils.process import (
enforce_timeout,
kill_process,
Expand Down Expand Up @@ -475,7 +476,9 @@ class ProcessRunnerTest(Test):
:type binary: ``str``
:param description: Description of test instance.
:type description: ``str``
:param proc_env: Environment overrides for ``subprocess.Popen``.
:param proc_env: Environment overrides for ``subprocess.Popen``;
context value (when referring to other driver) and jinja2 template (when
referring to self) will be resolved.
:type proc_env: ``dict``
:param proc_cwd: Directory override for ``subprocess.Popen``.
:type proc_cwd: ``str``
Expand Down Expand Up @@ -515,22 +518,14 @@ class ProcessRunnerTest(Test):
_MAX_RETAINED_LOG_SIZE = 4096

def __init__(self, **options):
proc_env = os.environ.copy()
if options.get("proc_env"):
proc_env.update(options["proc_env"])
options["proc_env"] = proc_env
super(ProcessRunnerTest, self).__init__(**options)

self._test_context = None
self._test_process = None # will be set by `self.run_tests`
self._test_process_retcode = None # will be set by `self.run_tests`
self._test_process_killed = False
self._test_has_run = False

# Need to use the binary's absolute path if `proc_cwd` is specified,
# otherwise won't be able to find the binary.
if self.cfg.proc_cwd:
self.cfg._options["binary"] = os.path.abspath(self.cfg.binary)
self._resolved_bin = None # resolved binary path

@property
def stderr(self) -> str:
Expand All @@ -548,6 +543,25 @@ def timeout_log(self) -> str:
def report_path(self) -> str:
return os.path.join(self._runpath, "report.xml")

@property
def resolved_bin(self) -> str:
if not self._resolved_bin:
self._resolved_bin = self.prepare_binary()

return self._resolved_bin

def prepare_binary(self):
"""
Resolve the real binary path to run
"""
# Need to use the binary's absolute path if `proc_cwd` is specified,
# otherwise won't be able to find the binary.
if self.cfg.proc_cwd:
return os.path.abspath(self.cfg.binary)
# use user-specified binary as-is, override if more sophisticated binary resolution is needed.
else:
return self.cfg.binary

def test_command(self) -> List[str]:
"""
Add custom arguments before and after the executable if they are defined.
Expand All @@ -569,7 +583,7 @@ def _test_command(self) -> List[str]:
:return: Command to run test process
:rtype: ``list`` of ``str``
"""
return [self.cfg.binary]
return [self.resolved_bin]

def list_command(self) -> Optional[List[str]]:
"""
Expand Down Expand Up @@ -666,12 +680,19 @@ def timeout_callback(self):
)

def get_proc_env(self):
self._json_ouput = os.path.join(self.runpath, "output.json")
self.logger.debug("Json output: %s", self._json_ouput)
env = {"JSON_REPORT": self._json_ouput}
env.update(
{key.upper(): val for key, val in self.cfg.proc_env.items()}
)
"""
Fabricate the env var for subprocess.
Precedence: user-specified > hardcoded > system env
"""

# start with system env
env = os.environ.copy()

# override with hardcoded values
json_ouput = os.path.join(self.runpath, "output.json")
self.logger.debug("Json output: %s", json_ouput)
env["JSON_REPORT"] = json_ouput

for driver in self.resources:
driver_name = driver.uid()
Expand All @@ -686,6 +707,13 @@ def get_proc_env(self):
).upper()
] = str(value)

# override with user specified values
proc_env = {
key.upper(): render(val, self.context_input())
for key, val in self.cfg.proc_env.items()
}
env.update(proc_env)

return env

def run_tests(self):
Expand Down Expand Up @@ -763,7 +791,7 @@ def get_process_check_report(self, retcode, stdout, stderr):
"""
assertion_content = "\n".join(
[
"Process: {}".format(self.cfg.binary),
"Process: {}".format(self.resolved_bin),
"Exit code: {}".format(retcode),
]
)
Expand Down
4 changes: 2 additions & 2 deletions testplan/testing/cpp/cppunit.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def report_path(self):
return os.path.join(self._runpath, "report.xml")

def _test_command(self):
cmd = [self.cfg.binary]
cmd = [self.resolved_bin]
if self.cfg.filtering_flag and self.cfg.cppunit_filter:
cmd.extend([self.cfg.filtering_flag, self.cfg.cppunit_filter])
if self.cfg.file_output_flag:
Expand All @@ -119,7 +119,7 @@ def _test_command(self):

def _list_command(self):
if self.cfg.listing_flag:
return [self.cfg.binary, self.cfg.listing_flag]
return [self.resolved_bin, self.cfg.listing_flag]
else:
return super(Cppunit, self)._list_command()

Expand Down
2 changes: 1 addition & 1 deletion testplan/testing/cpp/gtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def __init__(
super(GTest, self).__init__(**options)

def base_command(self):
cmd = [self.cfg.binary]
cmd = [self.resolved_bin]
if self.cfg.gtest_filter:
cmd.append("--gtest_filter={}".format(self.cfg.gtest_filter))
return cmd
Expand Down
2 changes: 1 addition & 1 deletion testplan/testing/junit.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def __init__(

def _test_command(self):
return (
[self.cfg.binary]
[self.resolved_bin]
+ (self.cfg.junit_args or [])
+ (self.cfg.junit_filter or [])
)
Expand Down
Loading

0 comments on commit 42328d6

Please sign in to comment.