Skip to content

Commit f5e883c

Browse files
authored
Refactor/streamline testplan report (#1163)
- remove unused entries from generated report - merge utc_time & machine_time into timestamp field - upgrade pnpm & node in ci; move pytest cfg to pyproject.toml - use unix timestamps & store tz info in test-level report - update frontend test w @testing-library/react - delta encode level info of all dict/fix entries - avoid match status expansion for dict/fix match entries - omit fixed indices of tablelog entry - add timezone field to TestReport, have default values to timezone fields
1 parent 91052f0 commit f5e883c

File tree

85 files changed

+28343
-11886
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+28343
-11886
lines changed

.github/workflows/test_pr.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ jobs:
7676
with:
7777
python-version: 3.11
7878
- name: Set up Node
79-
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
79+
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
8080
with:
81-
node-version: "16.x"
81+
node-version: 20
8282
- name: Restore pip cache
8383
uses: ./.github/actions/pip-cache
8484
- name: Set up PNPM
8585
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
8686
with:
87-
version: 8.10.4
87+
version: 9
8888
- name: Setup
8989
run: |
9090
pip install -r requirements-build.txt -U
@@ -133,7 +133,7 @@ jobs:
133133
- name: Set up Kafka for tests
134134
if: ${{ matrix.os == 'ubuntu-22.04' }}
135135
run: |
136-
wget https://archive.apache.org/dist/kafka/3.5.2/kafka_2.13-3.5.2.tgz -O kafka.tgz
136+
wget https://dlcdn.apache.org/kafka/3.9.0/kafka_2.13-3.9.0.tgz -O kafka.tgz
137137
sudo mkdir /opt/kafka
138138
sudo chown -R $USER:$USER /opt/kafka
139139
tar zxf kafka.tgz -C /opt/kafka --strip-components 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Change Testplan exported JSON report structure to reduce report size.
2+
3+
* Remove unused report entry fields.
4+
* ``fix_spec_path``.
5+
* ``status_override`` and ``status_reason`` in case they are empty.
6+
* ``line_no``, ``code_context`` and ``file_path`` if ``--code`` is not enabled.
7+
* ``env_status``, ``part``, ``strict_order`` and ``host`` depending on report category.
8+
* Remove unused assertion entry fields ``category`` and ``flag`` if they are ``DEFAULT``.
9+
* Merge assertion entry fields ``utc_time`` and ``machine_time`` into a unix timestamp field ``timestamp``, and store timezone info in parent Test-level report under key ``timezone``.
10+
* Replace ISO 8601 time string with unix timestamp in all ``timer`` fields, and add a ``timezone`` field to Testplan-level report as well.
11+
* Update data structure of several serialized assertion entries.
12+
* Delta encode level info of ``flattened_dict`` fields of ``DictLog`` and ``FixLog`` entries.
13+
* Delta encode level info of ``comparison`` fields of ``DictMatch`` and ``FixMatch`` entries.
14+
* Delta encode level info of nested ``comparison`` fields of ``DictMatchAll`` and ``FixMatchAll`` entries, remove extra nesting of ``matches`` as well.
15+
* Preserve abbreviations of match status of ``DictMatch``, ``FixMatch``, ``DictMatchAll`` and ``FixMatchAll`` entries, i.e. ``p`` instead of ``Passed``, ``f`` instead of ``Failed``, ``i`` instead of ``Ignored``.
16+
* Remove ``indices`` field of ``TableLog`` entries.

pyproject.toml

+14-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"pytest-mock",
3535
"psutil",
3636
"schema",
37+
"tzlocal",
3738
"lxml",
3839
"reportlab",
3940
"marshmallow",
@@ -110,7 +111,6 @@
110111
]
111112

112113
[tool.releaseherald]
113-
114114
news_fragments_directory = 'doc/newsfragments'
115115
unreleased = true
116116
news_file = 'doc/news_template.rst'
@@ -121,3 +121,16 @@
121121
[tool.releaseherald.filename_metadata_extractor]
122122
type="re"
123123
pattern='''^(((?P<id>\d+)_?)?((?P<type>changed|new|deprecated|removed))?\.)?.*$'''
124+
125+
[tool.pytest.ini_options]
126+
filterwarnings = [
127+
"ignore::pytest.PytestWarning",
128+
"ignore:.*flask_restx.*:DeprecationWarning",
129+
# jsonschema warning from flask_restx
130+
"ignore:.*jsonschema.*:DeprecationWarning",
131+
"ignore:.*load_module.*:DeprecationWarning",
132+
"ignore:.*LogMatcher.*:UserWarning",
133+
# under most cases, included files are not hit
134+
"ignore:No data was collected:coverage.exceptions.CoverageWarning",
135+
]
136+
norecursedirs = "tests/helpers"

pytest.ini

-12
This file was deleted.

testplan/cli/merger/mergers.py

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def merge(self, reports: Iterable[TestReport]) -> TestReport:
4848
description.append(f"{report.description}\n")
4949

5050
# TODO: what to do with meta, tags_index
51+
# TODO: timer & timezone
5152
result.description = "\n".join(description)
5253

5354
return result

testplan/common/exporters/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from testplan.common.config import Config, Configurable
99
from testplan.common.utils import strings
1010
from testplan.common.utils.comparison import is_regex
11-
from testplan.common.utils.timing import utcnow
11+
from testplan.common.utils.timing import now
1212
from testplan.report import TestReport
1313

1414

@@ -18,7 +18,7 @@ class ExporterResult:
1818
result: Dict = None
1919
traceback: str = None
2020
uid: str = strings.uuid4()
21-
start_time: datetime = utcnow()
21+
start_time: datetime = now()
2222
end_time: datetime = None
2323

2424
@property
@@ -154,7 +154,7 @@ def run_exporter(
154154
except Exception:
155155
exp_result.traceback = traceback.format_exc()
156156
finally:
157-
exp_result.end_time = utcnow()
157+
exp_result.end_time = now()
158158
if not exp_result.success:
159159
exporter.logger.error(exp_result.traceback)
160160
if result:

testplan/common/report/base.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,23 @@ class ReportCategories:
246246
# use for before/after_start/stop, setup, teardown, etc
247247
SYNTHESIZED = "synthesized"
248248

249+
@classmethod
250+
def is_test_level(cls, cat):
251+
return cat in (
252+
cls.MULTITEST,
253+
cls.TASK_RERUN,
254+
cls.GTEST,
255+
cls.CPPUNIT,
256+
cls.BOOST_TEST,
257+
cls.HOBBESTEST,
258+
cls.PYTEST,
259+
cls.PYUNIT,
260+
cls.UNITTEST,
261+
cls.QUNIT,
262+
cls.JUNIT,
263+
cls.ERROR,
264+
)
265+
249266

250267
class Report:
251268
"""
@@ -510,7 +527,6 @@ def __init__(self, name, **kwargs):
510527
super(BaseReportGroup, self).__init__(name=name, **kwargs)
511528

512529
self._index: Dict = {}
513-
self.host: Optional[str] = None
514530
self.children = []
515531

516532
self.build_index()

testplan/common/report/log.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ def emit(self, record):
2828
if hasattr(record, "report_obj_id"):
2929
report = REPORT_MAP.get(record.report_obj_id)
3030
if report is not None:
31-
created = datetime.datetime.utcfromtimestamp(
31+
created = datetime.datetime.fromtimestamp(
3232
record.created
33-
).replace(tzinfo=timezone.utc)
33+
).astimezone()
3434
report.logs.append(
3535
{
3636
"message": self.format(record),

testplan/common/report/schemas.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""
22
Base schemas for report serialization.
33
"""
4-
from marshmallow import Schema, fields, post_load
4+
import datetime
5+
6+
from marshmallow import Schema, fields, post_dump, post_load, pre_load
57
from marshmallow.utils import EXCLUDE
68

79
from testplan.common.report.base import (
@@ -22,8 +24,24 @@
2224
class IntervalSchema(Schema):
2325
"""Schema for ``timer.Interval``"""
2426

25-
start = custom_fields.UTCDateTime()
26-
end = custom_fields.UTCDateTime(allow_none=True)
27+
start = fields.DateTime("timestamp")
28+
end = fields.DateTime("timestamp", allow_none=True)
29+
30+
@pre_load
31+
def accept_old_isoformat(self, data, **kwargs):
32+
try:
33+
if data.get("start", None) and isinstance(data["start"], str):
34+
data["start"] = datetime.datetime.fromisoformat(
35+
data["start"]
36+
).timestamp()
37+
if data.get("end", None) and isinstance(data["end"], str):
38+
data["end"] = datetime.datetime.fromisoformat(
39+
data["end"]
40+
).timestamp()
41+
return data
42+
except ValueError as e:
43+
# no need to defer
44+
raise ValueError("Invalid value when loading Interval.") from e
2745

2846
@post_load
2947
def make_interval(self, data, **kwargs):
@@ -68,7 +86,7 @@ class ReportLogSchema(Schema):
6886
message = fields.String()
6987
levelname = fields.String()
7088
levelno = fields.Integer()
71-
created = custom_fields.UTCDateTime()
89+
created = fields.DateTime("timestamp")
7290
funcName = fields.String()
7391
lineno = fields.Integer()
7492
uid = fields.UUID()
@@ -126,6 +144,14 @@ def make_report(self, data, **kwargs):
126144
rep.timer = timer
127145
return rep
128146

147+
@post_dump
148+
def strip_none(self, data, **kwargs):
149+
if data["status_override"] is None:
150+
del data["status_override"]
151+
if data["status_reason"] is None:
152+
del data["status_reason"]
153+
return data
154+
129155

130156
class BaseReportGroupSchema(ReportSchema):
131157
"""Schema for ``base.BaseReportGroup``."""

testplan/common/utils/convert.py

+34-41
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
"""Conversion utilities."""
22
import itertools
3-
from typing import Union, Tuple, Iterable, Callable, List, Sequence
3+
from typing import Callable, Iterable, List, Optional, Sequence, Tuple, Union
44

55
from .reporting import Absent
66

7-
87
RecursiveListTuple = List[Union[Tuple, Tuple["RecursiveListTuple"]]]
98

109

@@ -77,27 +76,11 @@ def make_iterables(values: Iterable) -> List[Union[List, Tuple]]:
7776
return iterables
7877

7978

80-
def full_status(status: str) -> str:
81-
"""
82-
Human readable status label.
83-
84-
:param status: status label
85-
:return: human-readable status label
86-
"""
87-
if status == "p":
88-
return "Passed"
89-
elif status == "f":
90-
return "Failed"
91-
elif status == "i":
92-
return "Ignored"
93-
return ""
94-
95-
9679
def expand_values(
9780
rows: List[Tuple],
9881
level: int = 0,
9982
ignore_key: bool = False,
100-
key_path: List = None,
83+
key_path: Optional[List] = None,
10184
match: str = "",
10285
):
10386
"""
@@ -145,24 +128,6 @@ def expand_values(
145128
key_path.pop()
146129

147130

148-
# TODO: position parameter is misleading and it allows extracting
149-
# the key or match information as value
150-
# "left" or "right" choices would be enough for clarity and
151-
# would fail earlier upon any change to structure
152-
def extract_values(comparison: List[Tuple], position: int) -> List:
153-
"""
154-
Extracts one-side of a comparison result based on value position.
155-
156-
:param comparison: list of key, match, and value pair quadruples
157-
:param position: index pointing to particular value
158-
:return: list of key, match, and value triples
159-
"""
160-
result = []
161-
for item in comparison:
162-
result.append((item[0], item[1], item[position]))
163-
return result
164-
165-
166131
def flatten_formatted_object(formatted_obj):
167132
"""
168133
Flatten the formatted object which is the result of function
@@ -232,10 +197,10 @@ def flatten_dict_comparison(comparison: List[Tuple]) -> List[List]:
232197
:param comparison: list of comparison results
233198
:return: result table to be used in display
234199
"""
235-
result_table = [] # level, key, left, right, result
200+
result_table = [] # level, key, result, left, right
236201

237-
left = list(expand_values(extract_values(comparison, 2)))
238-
right = list(expand_values(extract_values(comparison, 3)))
202+
left = list(expand_values(map(lambda x: (x[0], x[1], x[2]), comparison)))
203+
right = list(expand_values(map(lambda x: (x[0], x[1], x[3]), comparison)))
239204

240205
while left or right:
241206
lpart, rpart = None, None
@@ -271,7 +236,7 @@ def flatten_dict_comparison(comparison: List[Tuple]) -> List[List]:
271236
level -= 1
272237
# key = '(group)'
273238

274-
status = full_status(lpart[3] if lpart else rpart[3])
239+
status = lpart[3] if lpart else rpart[3]
275240
lval = lpart[4] if lpart else None
276241
rval = rpart[4] if rpart else None
277242
result_table.append(
@@ -295,3 +260,31 @@ def flatten_dict_comparison(comparison: List[Tuple]) -> List[List]:
295260
break
296261

297262
return result_table
263+
264+
265+
def delta_encode_level(homo):
266+
prev = 0
267+
hetero = []
268+
for r in homo:
269+
level = r[0]
270+
res = r[1:]
271+
diff = level - prev
272+
if diff != 0:
273+
hetero.append(diff)
274+
prev = level
275+
hetero.append(res)
276+
277+
return hetero
278+
279+
280+
def delta_decode_level(hetero):
281+
level = 0
282+
homo = []
283+
for r in hetero:
284+
if isinstance(r, int):
285+
level += r
286+
continue
287+
else:
288+
homo.append([level, *r])
289+
290+
return homo

testplan/common/utils/reporting.py

-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ def render(obj, key=None):
7878
ret = (0, None, str(obj))
7979
elif obj is None:
8080
ret = (0, None, None)
81-
elif issubclass(obj_t, (int,)):
82-
ret = (0, obj_t.__name__, str(obj))
8381
elif issubclass(obj_t, NATIVE_TYPES):
8482
ret = (0, obj_t.__name__, obj)
8583
elif isinstance(obj, ContextValue):

0 commit comments

Comments
 (0)