Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added an Extended Search button for filtering and navigating parametrized testcase permutations in reports.
32 changes: 29 additions & 3 deletions testplan/common/serialization/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@
import pprint

from datetime import timezone, datetime
from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
Union,
cast,
)

from boltons.iterutils import is_scalar, remap
from lxml import etree
Expand All @@ -24,6 +34,7 @@
# orjson's native int range; outside this it raises
_INT64_MIN = -(2**63)
_UINT64_MAX = 2**64 - 1
_MAX_TEXT_LENGTH = 1000

# pylint: disable=unused-argument

Expand Down Expand Up @@ -140,7 +151,9 @@ def _repr_obj(obj: object) -> str:
return object.__repr__(obj)


def native_or_pformat(value: Any) -> Any:
def native_or_pformat(
value: Any, formatter: Callable[[Any], Any] = pprint.pformat
) -> Any:
"""Generic serialization compatible value formatter."""
if comparison.is_regex(value):
value = "REGEX({})".format(value.pattern)
Expand All @@ -159,11 +172,24 @@ def native_or_pformat(value: Any) -> Any:
elif type(value) in JSON_SAFE_SCALARS:
result = value
else:
result = pprint.pformat(value)
result = formatter(value)

return result


def native_or_text(value: Any) -> Any:
"""Preserve primitive values and render other values as bounded text."""
try:
if isinstance(value, (set, frozenset)):
value = sorted(value, key=repr)
result = native_or_pformat(value, formatter=str)
except Exception: # pylint: disable=broad-except
return "<{}>".format(type(value).__name__)
if isinstance(result, str) and len(result) > _MAX_TEXT_LENGTH:
return result[:_MAX_TEXT_LENGTH] + "..."
return result


def native_or_pformat_dict(value: Dict[str, Any]) -> Dict[str, Any]:
"""
Converter utility for dictionaries,
Expand Down
11 changes: 10 additions & 1 deletion testplan/report/testing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@

import copy
import getpass
import hashlib
import itertools
import platform
import re
Expand All @@ -61,6 +60,7 @@
Status,
)
from testplan.common.report.base import ExceptionLoggerBase
from testplan.common.serialization.fields import native_or_text
from testplan.common.utils.timing import iana_tz
from testplan.testing import tagging
from testplan.testing.common import TEST_PART_PATTERN_FORMAT_STRING
Expand Down Expand Up @@ -524,6 +524,7 @@ def __init__(
name: str,
tags: Optional[Union[Dict[str, Any], str]] = None,
category: str = ReportCategories.TESTCASE,
parametrization_kwargs: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
super(TestCaseReport, self).__init__(name=name, **kwargs)
Expand All @@ -533,6 +534,14 @@ def __init__(
self.attachments: List[Any] = []
self.category = category
self.covered_lines: Optional[dict] = None
self.parametrization_kwargs = (
{
key: native_or_text(value)
for key, value in parametrization_kwargs.items()
}
if parametrization_kwargs
else None
)

def _get_comparison_attrs(self) -> List[str]:
return super(TestCaseReport, self)._get_comparison_attrs() + [
Expand Down
10 changes: 10 additions & 0 deletions testplan/report/testing/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ class TestCaseReportSchema(ReportSchema):
category = fields.String()
counter = fields.Dict(dump_only=True)
tags = TagField()
parametrization_kwargs = fields.Dict(allow_none=True)

@post_dump
def strip_empty_parametrization_kwargs(
self, data: Dict[str, Any], **kwargs: Any
) -> Dict[str, Any]:
"""Omit parametrization data from ordinary testcase reports."""
if data.get("parametrization_kwargs") is None:
data.pop("parametrization_kwargs", None)
return data

@post_load
def make_report(
Expand Down
3 changes: 3 additions & 0 deletions testplan/testing/multitest/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,9 @@ def _new_testcase_report(self, testcase: Any) -> TestCaseReport:
definition_name=testcase.name,
uid=testcase.__name__,
tags=testcase.__tags__,
parametrization_kwargs=getattr(
testcase, "_parametrization_kwargs", None
),
)

def _new_parametrized_group_report(
Expand Down
13 changes: 13 additions & 0 deletions testplan/web_ui/testing/src/Report/BatchReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class BatchReportComponent extends BaseReport {
this.updateTagsDisplay = this.updateTagsDisplay.bind(this);
this.updateFilter = this.updateFilter.bind(this);
this.updatePanelView = this.updatePanelView.bind(this);
this.handleExtendedSearchNavigate = this.handleExtendedSearchNavigate.bind(
this
);

this.state = {
...this.state,
Expand Down Expand Up @@ -243,6 +246,15 @@ class BatchReportComponent extends BaseReport {
this.props.history.push(newUrl);
}

/**
* Handle navigation from extended search.
* @param {Array} uids - Array of UIDs from report root to target entry
*/
handleExtendedSearchNavigate(uids) {
if (!uids || uids.length === 0) return;
const newPath = generateSelectionPath(this.props.match.path, uids);
this.props.history.push(newPath);
}

getSelectedUIDsFromPath() {
const { uid, selection } = this.props.match.params;
Expand Down Expand Up @@ -310,6 +322,7 @@ class BatchReportComponent extends BaseReport {
updateTagsDisplayFunc={this.updateTagsDisplay}
current_pannel={this.state.currentPanelView}
switchPanelViewFunc={this.updatePanelView}
onExtendedSearchNavigate={this.handleExtendedSearchNavigate}
/>
<NavBreadcrumbs entries={selectedEntries} url={this.props.match.path} />
<div
Expand Down
19 changes: 17 additions & 2 deletions testplan/web_ui/testing/src/Report/InteractiveReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class InteractiveReportComponent extends BaseReport {
this.reloadCode = this.reloadCode.bind(this);
this.envCtrlCallback = this.envCtrlCallback.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleExtendedSearchNavigate = this.handleExtendedSearchNavigate.bind(
this
);
this.firstGet = props.firstGet !== undefined
? props.firstGet
: true; // Initialize from props
Expand Down Expand Up @@ -531,8 +534,8 @@ class InteractiveReportComponent extends BaseReport {

shallowEntry.entry_uids = entries.map((entry) => entry.uid);

// the filter text is either "null" or an empty string, use truthy-falsy
if (this.state.filteredReport.filter.text) {
const filter = this.state.filteredReport.filter;
if (filter.text || filter.filters?.length) {
shallowEntry.entries = entries
.map((entry) => this.pruneReportEntry(entry))
.filter((entry) => entry !== null);
Expand Down Expand Up @@ -698,6 +701,17 @@ class InteractiveReportComponent extends BaseReport {
);
}

/**
* Handle navigation from extended search.
* @param {Array} uids - Array of UIDs from report root to target entry
*/
handleExtendedSearchNavigate(uids) {
if (!uids || uids.length === 0) return;
const [uid, ...selection] = uids.map((entryUid) => base64url(entryUid));
const newPath = generatePath(this.props.match.path, { uid, selection });
this.props.history.push(newPath);
}

/**
* Render the InteractiveReport component based on its current state.
*/
Expand Down Expand Up @@ -748,6 +762,7 @@ class InteractiveReportComponent extends BaseReport {
updateFilterFunc={noop}
updateEmptyDisplayFunc={noop}
updateTagsDisplayFunc={noop}
onExtendedSearchNavigate={this.handleExtendedSearchNavigate}
extraButtons={[
<ReloadButton
key="reload-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,22 @@ describe("InteractiveReport", () => {
moxios.uninstall();
});

it("navigates to encoded extended search results", () => {
const interactiveReport = renderInteractiveReport();
const historyPush = jest.spyOn(interactiveReport.instance().props.history, "push");

interactiveReport.instance().handleExtendedSearchNavigate([
"TestplanUID",
"MultiTestUID",
"SuiteUID",
"testcaseUID",
]);

expect(historyPush).toHaveBeenCalledWith(
"/interactive/VGVzdHBsYW5VSUQ/TXVsdGlUZXN0VUlE/U3VpdGVVSUQ/dGVzdGNhc2VVSUQ"
);
});

it("Handles 'full' parameter for report serialization", (done) => {
const interactiveReport = renderInteractiveReport(true);
const report = initialReport();
Expand Down Expand Up @@ -905,7 +921,7 @@ describe("InteractiveReport", () => {
});
});

it("Run filtered tests", (done) => {
it("Run filtered tests with an empty display string", (done) => {
const interactiveReport = renderInteractiveReport();

const report = initialReport();
Expand All @@ -919,7 +935,7 @@ describe("InteractiveReport", () => {
interactiveReport.setState({
filteredReport: {
report: report,
filter: { text: "something" },
filter: { text: "", filters: [{ type: "regexp", search: "case" }] },
},
});
interactiveReport.update();
Expand Down
Loading
Loading