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

Feature/skip remaining tests being executed #1029

Merged
merged 18 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions doc/en/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,17 @@ Command line
--trace-tests-output
Specify output file for tests impacted by change in Testplan pattern format (see --trace-tests). Will be ignored if --trace-tests is not specified. Default to standard output.
--xfail-tests Read a list of known to fail testcases from a JSON file with each entry looks like: {"<Multitest>:<TestSuite>:<testcase>": {"reason": <value>, "strict": <value>} }

--runtime-data PATH Historical runtime data which will be used for Multitest auto-part and weight-based Task smart-scheduling with entries looks like:

{
"<Multitest>": {
"execution_time": 199.99,
"setup_time": 39.99,
},
......
}
--skip-remaining {cases-on-failed,cases-on-error,suites-on-failed,suites-on-error,tests-on-failed,tests-on-error}
Skip the remaining Testcases/Testsuites/Multitests being executed when a Testcase has failed or raised exception.
Filtering:
--patterns Test filter, supports glob notation & multiple arguments.

Expand Down Expand Up @@ -427,7 +437,7 @@ Command line
--report-tags-all <tag_name_1> --report-tags-all <tag_name 2>

--report-tags-all <tag_name_1> <tag_category_1>=<tag_name_2>
--file-log-level {exporter_info,test_info,driver_info,critical,error,warning,info,debug,none}
--file-log-level {USER_INFO,CRITICAL,ERROR,WARNING,INFO,DEBUG,NONE}

Specify log level for file logs. Set to None to disable file logging.

Expand Down
3 changes: 3 additions & 0 deletions doc/newsfragments/2540_new.skip_remaining.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added a new command line argument ``--skip-remaining``, a new argument ``skip_strategy`` to MultiTest, allowing remaining Testcases/Testsuites/MultiTests being skipped from execution when a Testcase has failed or raised exeception.

Argument ``uid`` of :py:meth:`Testplan.add_resource <testplan.base.Testplan.add_resource>` should now match the uid of the ``resource`` argument.
4 changes: 2 additions & 2 deletions testplan/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,10 @@ def __init__(
)

# By default, a LocalRunner is added to store and execute the tests.
self._runnable.add_resource(LocalRunner(), uid="local_runner")
self._runnable.add_resource(LocalRunner())

# Stores independent environments.
self._runnable.add_resource(Environments(), uid="environments")
self._runnable.add_resource(Environments())

@property
def parser(self):
Expand Down
22 changes: 12 additions & 10 deletions testplan/common/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,7 @@ def __getattr__(self, name):
)

def __repr__(self):
return "{}{}".format(
self.__class__.__name__, self._cfg_input or self._options
)
return "{}{}".format(self.__class__.__name__, self._options)

def get_local(self, name, default=None):
"""Returns a local config setting (not from container)"""
Expand All @@ -184,6 +182,11 @@ def get_local(self, name, default=None):
else:
return default

def set_local(self, name, value):
"""set without any check"""
options = self.__getattribute__("_options")
options[name] = value

@property
def parent(self):
"""Returns the parent configuration."""
Expand All @@ -206,20 +209,19 @@ def denormalize(self):
new_options = {}
for key in self._options:
value = getattr(self, key)
if inspect.isclass(value) or inspect.isroutine(value):
# Skipping non-serializable classes and routines.
logger.TESTPLAN_LOGGER.debug(
"Skip denormalizing option: %s", key
)
continue
try:
new_options[copy.deepcopy(key)] = copy.deepcopy(value)
except Exception as exc:
logger.TESTPLAN_LOGGER.warning(
"Failed to denormalize option: {} - {}".format(key, exc)
)

new = self.__class__(**new_options)
# XXX: we have transformed options, which should not be validated
# XXX: against schema again
new = object.__new__(self.__class__)
setattr(new, "_parent", None)
setattr(new, "_cfg_input", new_options)
setattr(new, "_options", new_options)
return new

@classmethod
Expand Down
29 changes: 19 additions & 10 deletions testplan/common/entity/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from collections import OrderedDict, deque
from contextlib import suppress
from typing import (
Any,
Callable,
Deque,
Dict,
Expand All @@ -20,20 +21,19 @@
Optional,
Tuple,
Union,
Any,
)

import psutil
from schema import Or

from testplan.common.config import Config, ConfigOption
from testplan.common.report.base import EventRecorder
from testplan.common.utils import logger
from testplan.common.utils.path import default_runpath, makedirs, makeemptydirs
from testplan.common.utils.strings import slugify, uuid4
from testplan.common.utils.thread import execute_as_thread, interruptible_join
from testplan.common.utils.timing import wait
from testplan.common.utils.validation import is_subclass
from testplan.common.report.base import EventRecorder


class Environment:
Expand Down Expand Up @@ -149,6 +149,9 @@ def __repr__(self):
def __len__(self):
return len(self._resources)

def items(self):
return self._resources.items()

def all_status(self, target) -> bool:
"""
Checks whether all resources have target status.
Expand All @@ -165,7 +168,7 @@ def _record_resource_exception(self, message, resource, msg_store):
fetch_msg = "\n".join(resource.fetch_error_log())

msg = message.format(
resource_name=resource.cfg.name,
resource=resource,
traceback_exc=traceback.format_exc(),
fetch_msg=fetch_msg,
)
Expand All @@ -186,7 +189,8 @@ def start(self):
resource.start()
except Exception:
self._record_resource_exception(
message="While starting resource [{resource_name}]\n{traceback_exc}\n{fetch_msg}",
message="While starting resource {resource}:\n"
"{traceback_exc}\n{fetch_msg}",
resource=resource,
msg_store=self.start_exceptions,
)
Expand All @@ -207,7 +211,8 @@ def start(self):
resource.wait(resource.STATUS.STARTED)
except Exception:
self._record_resource_exception(
message="While waiting for resource [{resource_name}] to start\n{traceback_exc}\n{fetch_msg}",
message="While waiting for resource {resource} to start:\n"
"{traceback_exc}\n{fetch_msg}",
resource=resource,
msg_store=self.start_exceptions,
)
Expand Down Expand Up @@ -273,7 +278,8 @@ def stop(self, is_reversed=False):
resource.stop()
except Exception:
self._record_resource_exception(
message="While stopping resource [{resource_name}]\n{traceback_exc}\n{fetch_msg}",
message="While stopping resource {resource}:"
"\n{traceback_exc}\n{fetch_msg}",
resource=resource,
msg_store=self.stop_exceptions,
)
Expand Down Expand Up @@ -596,8 +602,7 @@ def abort(self):

self._should_abort = True
for dep in self.abort_dependencies():
if dep is not None:
self._abort_entity(dep)
self._abort_entity(dep)

self.logger.info("Aborting %s", self)
self.aborting()
Expand Down Expand Up @@ -631,7 +636,6 @@ def _abort_entity(self, entity, wait_timeout=None):
self.logger.error(traceback.format_exc())
self.logger.error("Exception on aborting %s - %s", entity, exc)
else:

if (
wait(
predicate=lambda: entity.aborted is True,
Expand All @@ -640,7 +644,11 @@ def _abort_entity(self, entity, wait_timeout=None):
)
is False
):
self.logger.error("Timeout on waiting to abort %s.", entity)
self.logger.error(
"Timeout on waiting to abort %s after %d seconds.",
entity,
timeout,
)

def aborting(self):
"""
Expand Down Expand Up @@ -1000,6 +1008,7 @@ def _post_run_checks(self, start_threads, start_procs):
:param start_procs: processes before run
:type start_procs: ``list`` of ``Process``
"""
# XXX: do we want to suppress process/thread check for tests?
Copy link
Contributor

Choose a reason for hiding this comment

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

what does XXX mean?

Copy link
Contributor Author

@zhenyu-ms zhenyu-ms Jan 12, 2024

Choose a reason for hiding this comment

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

i use XXX for something that's not TODO and not FIXME and not NOTE

suppression is from intuition, use XXX instead of FIXME here

end_threads = threading.enumerate()
if start_threads != end_threads:
new_threads = [
Expand Down
24 changes: 15 additions & 9 deletions testplan/common/utils/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
- Test progress information (e.g. Pass / Fail status)
- Exporter statuses
"""
import logging
import os
import sys
import logging

from testplan.common.utils.strings import Color, uuid4
from testplan.report import Status
Expand Down Expand Up @@ -74,16 +74,22 @@ def user_info(self, msg, *args, **kwargs):
"""Log 'msg % args' with severity 'USER_INFO'"""
self._custom_log(USER_INFO, msg, *args, **kwargs)

def log_test_status(self, name, status, indent=0, level=USER_INFO):
def log_test_status(
self,
name: str,
status: Status,
indent: int = 0,
level: int = USER_INFO,
):
"""Shortcut to log a pass/fail status for a test."""
if Status.STATUS_CATEGORY[status] == Status.PASSED:
pass_label = Color.green(status.title())
elif Status.STATUS_CATEGORY[status] in [Status.FAILED, Status.ERROR]:
pass_label = Color.red(status.title())
elif Status.STATUS_CATEGORY[status] == Status.UNSTABLE:
pass_label = Color.yellow(status.title())
if status.normalised() == Status.PASSED:
pass_label = Color.green(status.name.title())
elif status <= Status.FAILED:
pass_label = Color.red(status.name.title())
elif status.normalised() == Status.UNSTABLE:
pass_label = Color.yellow(status.name.title())
else: # unknown
pass_label = status
pass_label = status.name.title()

indent_str = indent * " "
msg = self._TEST_STATUS_FORMAT
Expand Down
6 changes: 4 additions & 2 deletions testplan/common/utils/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

import functools
import platform
from signal import Signals
import subprocess
import threading
import psutil
import time
import warnings
from enum import Enum, auto
from signal import Signals
from typing import IO, Any, Callable, List, Union

import psutil

from testplan.common.utils.logger import TESTPLAN_LOGGER
from testplan.common.utils.timing import exponential_interval, get_sleeper

Expand Down Expand Up @@ -253,6 +254,7 @@ def execute_cmd(

if isinstance(cmd, list):
cmd = [str(a) for a in cmd]
# FIXME: not good enough, need shell escaping
cmd_string = " ".join(cmd) # for logging, easy to copy and execute
else:
cmd_string = cmd
Expand Down
Loading
Loading