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 smart_tests/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, Generator, 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]
CaseEventGenerator = Generator[CaseEventType, None, None]

Expand Down Expand Up @@ -197,17 +205,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 smart_tests/commands/record/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from time import time_ns
from typing import Annotated, Callable, Dict, Generator, Iterable, List, Tuple, Union
from typing import Annotated, Any, Callable, Dict, Generator, Iterable, List, Tuple, Union

import click
from dateutil.parser import ParserError, parse
Expand All @@ -32,7 +32,7 @@
set_fail_fast_mode, warn_and_exit_if_fail_fast_mode)
from ...utils.logger import Logger
from ...utils.smart_tests_client import SmartTestsClient
from .case_event import CaseEvent, CaseEventGenerator, CaseEventType, DataBuilder, TestPathBuilder
from .case_event import BATCH_ID_KEY, CaseEvent, CaseEventGenerator, CaseEventType, DataBuilder, TestPathBuilder

# Together with the workers sending events, this stays within urllib3's default pool size (10 connections per host)
MAX_RAW_FILE_UPLOAD_WORKERS = 5
Expand Down Expand Up @@ -352,7 +352,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 @@ -362,11 +362,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 @@ -505,16 +509,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