Skip to content

Commit

Permalink
Merge branch 'main' into code_context
Browse files Browse the repository at this point in the history
  • Loading branch information
rnemes authored Dec 11, 2024
2 parents 27c4cfc + 7e19949 commit 8968c60
Show file tree
Hide file tree
Showing 19 changed files with 118 additions and 131 deletions.
4 changes: 2 additions & 2 deletions .github/actions/pip-cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ runs:
- name: Restore Pip Cache
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ hashFiles('setup.py') }}-${{ hashFiles('requirements.txt') }}
path: ${{ runner.os == 'Linux' && '~/.cache/pip' || '~\AppData\Local\pip\Cache' }}
key: ${{ runner.os }}-${{ hashFiles('pyproject.toml') }}
8 changes: 4 additions & 4 deletions doc/en/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Also find all our downloadable examples :ref:`here <download>`.

Working with the source
-----------------------

You will need a working python 3.7+ interrpreter preferably a venv, and for the interactive ui you need node installed.
We are using `doit <https://pydoit.org/contents.html>`_ as the taskrunner ``doit list`` can show all the commands.

Expand All @@ -85,10 +85,10 @@ We are using `doit <https://pydoit.org/contents.html>`_ as the taskrunner ``doit
git clone https://github.com/morganstanley/testplan.git
cd testplan
# install all dev requirements
pip install -r requirements-txt # this install testplan in editable mode
# install testplan in editable mode & all dev requirements
pip install -e .
#build the interactive UI (if you do not like it is opening a browserwindow remove the `-o`)
# build the interactive UI (if you do not like it is opening a browserwindow remove the `-o`)
doit build_ui -o
Internal tests
Expand Down
1 change: 1 addition & 0 deletions doc/newsfragments/3160_changed.case_name.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Limit the length of parameterization testcase name to 255 characters. If the name length exceeds 255 characters, index-suffixed names (e.g., ``{func_name} 1``, ``{func_name} 2``) will be used.
1 change: 1 addition & 0 deletions doc/newsfragments/3167_changed.json_exporter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``JSONExporter`` will log a "file not found" warning in the log instead of raising an exception.
3 changes: 0 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,9 @@
"setuptools",
"pytest",
"pytest-mock",
"py",
"psutil",
"schema",
"pytz",
"lxml",
"python-dateutil",
"reportlab",
"marshmallow",
"termcolor",
Expand Down
12 changes: 7 additions & 5 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
[pytest]
filterwarnings =
ignore::pytest.PytestWarning
ignore::DeprecationWarning:flask_restx.*:
ignore::DeprecationWarning:jinja2.*:
ignore::DeprecationWarning:jsonschema.*:
ignore::DeprecationWarning:marshmallow.*:
ignore::DeprecationWarning:werkzeug.*:
ignore:.*flask_restx.*:DeprecationWarning
; jsonschema warning from flask_restx
ignore:.*jsonschema.*:DeprecationWarning
ignore:.*load_module.*:DeprecationWarning
ignore:.*LogMatcher.*:UserWarning
; under most cases, included files are not hit
ignore:No data was collected:coverage.exceptions.CoverageWarning

norecursedirs=tests/helpers
44 changes: 0 additions & 44 deletions requirements-basic.txt

This file was deleted.

5 changes: 0 additions & 5 deletions requirements-rtd.txt

This file was deleted.

2 changes: 1 addition & 1 deletion testplan/common/utils/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def instantiate(template, values, destination):
try:
target.write(render(source.read(), values))
except UnicodeDecodeError:
shutil.copy(template, destination)
shutil.copyfile(template, destination)
except Exception as exc:
raise Exception(
"On reading/writing template: {} - of file {}".format(
Expand Down
2 changes: 1 addition & 1 deletion testplan/common/utils/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __new__(cls):
cls.__instance = object.__new__(cls)
return cls.__instance

def __str__(self):
def __repr__(self):
return self.descr


Expand Down
76 changes: 41 additions & 35 deletions testplan/exporters/testing/json/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,6 @@
from ..base import Exporter


def save_attachments(report: TestReport, directory: str) -> Dict[str, str]:
"""
Saves the report attachments to the given directory.
:param report: Testplan report.
:param directory: directory to save attachments in
:return: dictionary of destination paths
"""
moved_attachments = {}
attachments = getattr(report, "attachments", None)
if attachments:
for dst, src in attachments.items():
src = pathlib.Path(src)
dst_path = pathlib.Path(directory) / dst
makedirs(dst_path.parent)
if not src.is_file():
dirname = src.parent
# Try retrieving the file from "_attachments" directory that is
# near to the test report, the downloaded report might be moved
src = pathlib.Path.cwd() / ATTACHMENTS / dst
if not src.is_file():
raise FileNotFoundError(
f'Attachment "{dst}" not found in either {dirname} or'
f' the nearest "{ATTACHMENTS}" directory of test report'
)
copyfile(src=src, dst=dst_path)
moved_attachments[dst] = str(dst_path)

return moved_attachments


def save_resource_data(
report: TestReport, directory: pathlib.Path
) -> pathlib.Path:
Expand Down Expand Up @@ -176,8 +145,9 @@ def export(
with open(assertions_filepath, "w") as json_file:
json_file.write(json_dumps(assertions))

meta["attachments"] = save_attachments(
report=source, directory=attachments_dir
meta["attachments"] = self.save_attachments(
report=source,
directory=attachments_dir,
)
meta["version"] = 2
meta["attachments"][structure_filename] = str(
Expand All @@ -192,8 +162,9 @@ def export(
with open(json_path, "w") as json_file:
json_file.write(json_dumps(meta))
else:
data["attachments"] = save_attachments(
report=source, directory=attachments_dir
data["attachments"] = self.save_attachments(
report=source,
directory=attachments_dir,
)
data["version"] = 1

Expand All @@ -208,6 +179,41 @@ def export(
)
return result

def save_attachments(
self, report: TestReport, directory: str
) -> Dict[str, str]:
"""
Saves the report attachments to the given directory.
:param report: Testplan report.
:param directory: directory to save attachments in
:return: dictionary of destination paths
"""
moved_attachments = {}
attachments = getattr(report, "attachments", None)
if attachments:
for dst, src in attachments.items():
src = pathlib.Path(src)
dst_path = pathlib.Path(directory) / dst
makedirs(dst_path.parent)
if not src.is_file():
dirname = src.parent
# Try retrieving the file from "_attachments" directory that is
# near to the test report, the downloaded report might be moved
src = pathlib.Path.cwd() / ATTACHMENTS / dst
if not src.is_file():
self.logger.warning(
'Attachment "%s" not found in either %s or the nearest "%s" directory of test report',
dst,
dirname,
ATTACHMENTS,
)
continue
copyfile(src=src, dst=dst_path)
moved_attachments[dst] = str(dst_path)

return moved_attachments

@staticmethod
def split_json_report(data):
"""Split a single Json into several parts."""
Expand Down
2 changes: 1 addition & 1 deletion testplan/runners/pools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def rebase_task_path(self, task: Task) -> None:
def discard_running_tasks(self):
self._discard_running.set()

def __str__(self):
def __repr__(self):
return f"{self.__class__.__name__}[{self.cfg.index}]"


Expand Down
2 changes: 1 addition & 1 deletion testplan/testing/multitest/driver/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def send(self, method, api, **kwargs):
args=(method, api, drop_response, self.timeout),
kwargs=kwargs,
)
request_thread.setDaemon(True)
request_thread.daemon = True
request_thread.start()
self.request_threads.append((request_thread, drop_response))

Expand Down
2 changes: 1 addition & 1 deletion testplan/testing/multitest/driver/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ def starting(self):
timeout=self.timeout,
logger=self.logger,
)
self._server_thread.setName(self.name)
self._server_thread.name = self.name
self._server_thread.start()

while not hasattr(self._server_thread.server, "server_port"):
Expand Down
41 changes: 36 additions & 5 deletions testplan/testing/multitest/parametrization.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from testplan.common.utils import callable as callable_utils
from testplan.common.utils import convert, interface
from testplan.testing import tagging
from typing import Callable, Optional

# Although any string will be processed as normal, it's a good
# approach to warn the user if the generated method name is not a
Expand Down Expand Up @@ -206,8 +207,11 @@ def _generated(self, env, result):
)
# Users request the feature that when `name_func` set to `None`,
# then simply append integer suffixes to the names of testcases
_generated.name = (
name_func(name, kwargs) if name_func is not None else f"{name} {idx}"
_generated.name = _parametrization_report_name_func_wrapper(
name_func=name_func,
name=name,
kwargs=kwargs,
index=idx,
)

if hasattr(function, "__xfail__"):
Expand Down Expand Up @@ -267,10 +271,11 @@ def _check_tag_func(tag_func):
)


def _parametrization_name_func_wrapper(func_name, kwargs):
def _parametrization_name_func_wrapper(func_name: str, kwargs: dict):
"""
Make sure that name generation doesn't end up with invalid / unreadable
attribute names/types etc.
attribute names/types etc. The return value can be used as a
method __name__.
If somehow a 'bad' function name is generated, will just return the
original ``func_name`` instead (which will later on be suffixed with an
Expand All @@ -291,6 +296,32 @@ def _parametrization_name_func_wrapper(func_name, kwargs):
return generated_name


def _parametrization_report_name_func_wrapper(
name_func: Optional[Callable], name: str, kwargs: dict, index: int
):
"""
Make sure that generated name is not too long,
if it is, then use index suffixed names e.g. "{func_name} 1", "{func_name} 2", will be used.
The return value is used for reporting purposes, it is not used as a method __name__.
"""
if name_func:
generated_name = name_func(name, kwargs)
if not isinstance(generated_name, str):
raise ValueError(
"The return value of name_func must be a string, "
f"it is of type: {type(generated_name)}, value: {generated_name}"
)
if len(generated_name) <= MAX_METHOD_NAME_LENGTH:
return generated_name
else:
warnings.warn(
f"The name name_func returned ({generated_name}) is too long, using index suffixed names."
)

return f"{name} {index}"


def parametrization_name_func(func_name, kwargs):
"""
Method name generator for parametrized testcases.
Expand Down Expand Up @@ -321,7 +352,7 @@ def default_name_func(func_name, kwargs):
>>> import collections
>>> default_name_func('Test Method',
collections.OrderedDict(('foo', 5), ('bar', 10)))
'Test Method {foo:5, bar:10}'
'Test Method <foo:5, bar:10>'
:param func_name: Name of the parametrization target function.
:type func_name: ``str``
Expand Down
8 changes: 4 additions & 4 deletions testplan/testing/multitest/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,10 @@ def _validate_testcase(func):
raise exc

if len(func.name) > defaults.MAX_TEST_NAME_LENGTH:
warnings.warn(
'Name defined for testcase "{}" is too long,'
' consider customizing testcase name with argument "name_func"'
" in @testcase decorator.".format(func.__name__)
raise ValueError(
f'Name defined for testcase "{func.name}" is longer than {defaults.MAX_TEST_NAME_LENGTH},'
' consider customizing testcase name with argument "name"'
" in @testcase decorator."
)


Expand Down
Loading

0 comments on commit 8968c60

Please sign in to comment.