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
32 changes: 23 additions & 9 deletions launchable/commands/record/case_event.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import sys
import uuid
from typing import Any, Callable, Dict, List, Optional, Tuple

import dateutil.parser
Expand All @@ -18,6 +19,13 @@
RERUN_TAGS = ("rerunFailure", "rerunError")
FLAKY_TAGS = ("flakyFailure", "flakyError")

# Internal-only key tagging which original <testcase> an event came from (see
# from_case_and_suite). Used by summarize_case_events to group retry/flaky attempts of the
# SAME testcase without relying on testPath (which can be lossy, e.g. Maven's nested-class
# $-stripping, or missing/extra attributes from the raw runner's free-form TestPathComponent).
# Must be stripped before a payload is uploaded (see tests.py's payload()).
BATCH_ID_KEY = "testcaseBatchId"

CaseEventType = Dict[str, Any]


Expand Down Expand Up @@ -196,17 +204,23 @@ def child_text(tag: str) -> Optional[str]:

test_path = path_canonicalizer(path_builder(case, suite, report_file))
data = data_builder(case)
# One id per original <testcase>, shared by every attempt derived from it here, so
# summarize_case_events can group retries correctly without relying on test_path.
batch_id = uuid.uuid4()

return [
CaseEvent.create(
test_path=test_path,
duration_secs=attempt_duration,
status=attempt_status,
stdout=attempt_stdout,
stderr=attempt_stderr,
timestamp=suite.timestamp,
data=data,
)
{
**CaseEvent.create(
test_path=test_path,
duration_secs=attempt_duration,
status=attempt_status,
stdout=attempt_stdout,
stderr=attempt_stderr,
timestamp=suite.timestamp,
data=data,
),
BATCH_ID_KEY: batch_id,
}
for attempt_status, attempt_stdout, attempt_stderr, attempt_duration in attempts
]

Expand Down
35 changes: 25 additions & 10 deletions launchable/commands/record/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import xml.etree.ElementTree as ET
from concurrent.futures import ThreadPoolExecutor, as_completed
from http import HTTPStatus
from typing import Callable, Dict, Generator, Iterable, List, Optional, Sequence, Tuple, Union
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Sequence, Tuple, Union

import click
from dateutil.parser import parse
Expand All @@ -27,7 +27,7 @@
from ...utils.no_build import NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID
from ...utils.session import parse_session, read_build
from ..helper import find_or_create_session, time_ns
from .case_event import CaseEvent, CaseEventType
from .case_event import BATCH_ID_KEY, CaseEvent, CaseEventType

GROUP_NAME_RULE = re.compile("^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
RESERVED_GROUP_NAMES = ["group", "groups", "nogroup", "nogroups"]
Expand Down Expand Up @@ -490,7 +490,7 @@ def testcases(reports: List[str]) -> Generator[CaseEventType, None, None]:

# generator that creates the payload incrementally
def payload(
cases: Generator[TestCase, None, None],
cases: Generator[CaseEventType, None, None],
test_runner, group: str,
test_suite_name: str,
flavors: Dict[str, str]) -> Tuple[Dict[str, Union[str, List, dict, bool]], List[Exception]]:
Expand All @@ -500,11 +500,15 @@ def payload(

while True:
try:
cs.append(next(cases))
tc = next(cases)
except StopIteration:
break
except Exception as ex:
exs.append(ex)
continue
# internal-only; never leaves the process
tc.pop(BATCH_ID_KEY, None)
cs.append(tc)

count += len(cs)
return {
Expand Down Expand Up @@ -665,16 +669,27 @@ def summarize_case_events(events: Iterable[CaseEventType]) -> Tuple[int, int, in
Summarizes a stream of case events into (test_count, success_count, fail_count, duration_minutes),
for the "Tests found / Tests passed / Tests failed" table printed after `record tests` runs.

Grouped by testPath rather than counted per event: a flaky/rerun testcase produces multiple
events (see CaseEvent.from_case_and_suite), but it's still a single test. A test that ever
passed counts as passed (even if it also failed on earlier attempts); a test that only ever
failed counts as failed.
Grouped by which original testcase an event came from, not counted per event: a flaky/rerun
testcase produces multiple events (see CaseEvent.from_case_and_suite), but it's still a
single test. A test that ever passed counts as passed (even if it also failed on earlier
attempts); a test that only ever failed counts as failed.

JUnit-based events carry BATCH_ID_KEY (one id per original <testcase>, see
from_case_and_suite), which is used as the grouping key directly. Other runners (e.g. raw)
build events without going through from_case_and_suite, so they never have that key; for
those, fall back to grouping by testPath's complete attribute set. This fallback never
assumes a component has "type"/"name" specifically -- TestPathComponent is an arbitrary
bag of string attributes (see raw's free-form testPath/testPathComponents) -- and keeps
every attribute, not just type/name, so two components differing only in some other
attribute are never treated as the same path.
"""
tests: Dict[Tuple, Dict[str, bool]] = {}
tests: Dict[Any, Dict[str, bool]] = {}
duration = float(0)

for tc in events:
key = tuple((p["type"], p["name"]) for p in tc.get("testPath", []))
key = tc.get(BATCH_ID_KEY)
if key is None:
key = tuple(tuple(sorted(p.items())) for p in tc.get("testPath", []))
Comment thread
jothikumar-CB marked this conversation as resolved.
Resolved
entry = tests.setdefault(key, {"passed": False, "failed": False})
status = tc.get("status")
if status == 1:
Expand Down
Loading