Skip to content

Commit 9fd7b97

Browse files
committed
sync v1 branch
2 parents dbc2d9e + c229fd7 commit 9fd7b97

File tree

6 files changed

+259
-38
lines changed

6 files changed

+259
-38
lines changed

.tagpr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@
4141
releaseBranch = main
4242
versionFile = -
4343
changelog = false
44+
fixedMajorVersion = v1

smart_tests/commands/record/attachment.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import fnmatch
2+
import os
23
import tarfile
34
import zipfile
45
from io import BytesIO
5-
from typing import Annotated, List, Tuple
6+
from typing import Annotated, List, Optional, Set, Tuple
67

78
import click
9+
from tabulate import tabulate
810

911
import smart_tests.args4p.typer as typer
1012
from smart_tests.utils.session import SessionId, get_session
1113

1214
from ... import args4p
1315
from ...app import Application
14-
from ...utils.fail_fast_mode import warn_and_exit_if_fail_fast_mode
1516
from ...utils.smart_tests_client import SmartTestsClient
16-
from tabulate import tabulate
1717

1818

1919
class AttachmentStatus:
2020
SUCCESS = "✓ Recorded successfully"
2121
FAILED = "⚠ Failed to record"
2222
SKIPPED_NON_TEXT = "⚠ Skipped: not a valid text file"
23+
SKIPPED_DUPLICATE = "⚠ Skipped: duplicate"
2324

2425

2526
@args4p.command(help="Record attachment information")
@@ -39,6 +40,8 @@ def attachment(
3940
):
4041
client = SmartTestsClient(app=app)
4142
summary_rows = []
43+
used_filenames: Set[str] = set()
44+
4245
try:
4346
# Note: Call get_session method to check test session exists
4447
_ = get_session(session, client)
@@ -60,9 +63,15 @@ def attachment(
6063
[zip_info.filename, AttachmentStatus.SKIPPED_NON_TEXT])
6164
continue
6265

66+
file_name = get_unique_filename(zip_info.filename, used_filenames)
67+
if not file_name:
68+
summary_rows.append(
69+
[zip_info.filename, AttachmentStatus.SKIPPED_DUPLICATE])
70+
continue
71+
6372
status = post_attachment(
64-
client, session, file_content, zip_info.filename)
65-
summary_rows.append([zip_info.filename, status])
73+
client, session, file_content, file_name)
74+
summary_rows.append([file_name, status])
6675

6776
# If tar file (tar, tar.gz, tar.bz2, tgz, etc.)
6877
elif tarfile.is_tarfile(a):
@@ -85,9 +94,15 @@ def attachment(
8594
[tar_info.name, AttachmentStatus.SKIPPED_NON_TEXT])
8695
continue
8796

97+
file_name = get_unique_filename(tar_info.name, used_filenames)
98+
if not file_name:
99+
summary_rows.append(
100+
[tar_info.name, AttachmentStatus.SKIPPED_DUPLICATE])
101+
continue
102+
88103
status = post_attachment(
89-
client, session, file_content, tar_info.name)
90-
summary_rows.append([tar_info.name, status])
104+
client, session, file_content, file_name)
105+
summary_rows.append([file_name, status])
91106

92107
else:
93108
with open(a, mode='rb') as f:
@@ -98,15 +113,55 @@ def attachment(
98113
[a, AttachmentStatus.SKIPPED_NON_TEXT])
99114
continue
100115

101-
status = post_attachment(client, session, file_content, a)
102-
summary_rows.append([a, status])
116+
file_name = get_unique_filename(a, used_filenames)
117+
if not file_name:
118+
summary_rows.append(
119+
[a, AttachmentStatus.SKIPPED_DUPLICATE])
120+
continue
121+
122+
status = post_attachment(client, session, file_content, file_name)
123+
summary_rows.append([file_name, status])
124+
103125
except Exception as e:
104126
client.print_exception_and_recover(e)
105127

106128
display_summary_as_table(summary_rows)
107129

108130

109-
def matches_include_patterns(filename: str, include_patterns: List[str]) -> bool:
131+
def get_unique_filename(filepath: str, used_filenames: Set[str]) -> Optional[str]:
132+
"""
133+
Get a unique filename by extracting the basename and prepending parent folders if needed.
134+
Strategy:
135+
1. First occurrence: use basename (e.g., app.log)
136+
2. Duplicate: prepend parent directories until unique
137+
"""
138+
# Normalize path separators to forward slash (archives always use forward slash in both linux, and windows)
139+
normalized_path = filepath.replace(os.sep, '/')
140+
normalized_path = normalize_filename(normalized_path)
141+
142+
basename = normalized_path.split('/')[-1]
143+
144+
# If basename is not used, return it
145+
if basename not in used_filenames:
146+
used_filenames.add(basename)
147+
return basename
148+
149+
# Try prepending parents from nearest to farthest
150+
path_parts = normalized_path.split('/')
151+
parent_parts = [p for p in path_parts[:-1] if p]
152+
153+
prefixed_name = basename
154+
for parent in reversed(parent_parts):
155+
prefixed_name = f"{parent}/{prefixed_name}"
156+
157+
if prefixed_name not in used_filenames:
158+
used_filenames.add(prefixed_name)
159+
return prefixed_name
160+
161+
return None
162+
163+
164+
def matches_include_patterns(filename: str, include_patterns: Tuple[str, ...]) -> bool:
110165
"""
111166
Check if a file should be included based on the include patterns.
112167
If no patterns are specified, all files are included.
@@ -121,6 +176,13 @@ def matches_include_patterns(filename: str, include_patterns: List[str]) -> bool
121176
return False
122177

123178

179+
def normalize_filename(filename: str) -> str:
180+
"""
181+
Normalize filename by replacing whitespace with dashes.
182+
"""
183+
return filename.replace(' ', '-')
184+
185+
124186
def valid_utf8_file(file_content: bytes) -> bool:
125187
# Check for null bytes (binary files)
126188
if b'\x00' in file_content:

smart_tests/test_runners/maven.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import glob
22
import os
33
import re
4-
from typing import Annotated
5-
from typing import Dict, List, Tuple
4+
from typing import Annotated, Dict, List
65

76
import click
87

98
import smart_tests.args4p.typer as typer
109
from smart_tests.utils import glob as uglob
1110
from smart_tests.utils.java import junit5_nested_class_path_builder
12-
from . import smart_tests
11+
1312
from ..args4p.exceptions import BadCmdLineException
1413
from ..commands.record.tests import RecordTests
1514
from ..commands.subset import Subset
15+
from . import smart_tests
1616

1717
# Surefire has the default inclusion pattern
1818
# https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#includes
@@ -169,4 +169,12 @@ def record_tests(
169169
)],
170170
):
171171
client.path_builder = junit5_nested_class_path_builder(client.path_builder)
172+
original_report = client.report
173+
IGNORED_FILES = {'failsafe-summary.xml', 'testng-results.xml'}
174+
175+
def report_with_filter(junit_report_file: str):
176+
if not any(junit_report_file.endswith(f) for f in IGNORED_FILES):
177+
original_report(junit_report_file)
178+
179+
client.report = report_with_filter
172180
smart_tests.CommonRecordTestImpls.load_report_files(client=client, source_roots=reports)

smart_tests/test_runners/pytest.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import os
44
import pathlib
55
import subprocess
6-
from typing import Annotated, Generator, Iterable, List
6+
from datetime import datetime, timezone
7+
from typing import Annotated, Generator, Iterable, List, Optional
78

89
import click
910
from junitparser import Properties, TestCase # type: ignore
@@ -17,7 +18,6 @@
1718
from ..commands.subset import Subset
1819
from . import smart_tests
1920

20-
2121
# Please specify junit_family=legacy for pytest report format. if using pytest version 6 or higher.
2222
# - pytest has changed its default test report format from xunit1 to xunit2 since version 6.
2323
# - https://docs.pytest.org/en/latest/deprecations.html#junit-family-default-value-change-to-xunit2
@@ -40,6 +40,15 @@
4040
# <testcase classname="tests.test_mod.TestClass" name="test__can_print_aaa" file="tests/test_mod.py"
4141
# line="3" time="0.001" />
4242
#
43+
44+
45+
def _timestamp_to_iso(ts: Optional[float]) -> Optional[str]:
46+
# convert to ISO-8601 formatted date
47+
if ts is None:
48+
return None
49+
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
50+
51+
4352
@smart_tests.subset
4453
def subset(
4554
client: Subset,
@@ -344,15 +353,39 @@ def parse_func(
344353
else:
345354
props = None
346355

356+
# extract raw timestamps
357+
start_timestamp = data.get("start")
358+
stop_timestamp = data.get("stop")
359+
360+
# convert to ISO-8601
361+
start_timestamp_iso_format = _timestamp_to_iso(start_timestamp)
362+
end_timestamp_iso_format = _timestamp_to_iso(stop_timestamp)
363+
364+
print(
365+
f"DEBUG pytest timing start={start_timestamp_iso_format}, stop={end_timestamp_iso_format}"
366+
)
367+
368+
event_data = {}
369+
if start_timestamp_iso_format:
370+
event_data["start_timestamp"] = start_timestamp_iso_format
371+
if end_timestamp_iso_format:
372+
event_data["stop_timestamp"] = end_timestamp_iso_format
373+
347374
test_path = _parse_pytest_nodeid(nodeid)
348375
for path in test_path:
349376
if path.get("type") == "file":
350377
path["name"] = pathlib.Path(path["name"]).as_posix()
351378

379+
data_payload = event_data if event_data else None
380+
381+
# end_timestamp_iso_format is being passed as timestamp as it reflects event finalization
382+
# start + duration = stop
383+
# sending both start and stop time in the event data field
352384
yield CaseEvent.create(
353385
test_path=test_path,
354386
duration_secs=data.get("duration", 0),
355387
status=status,
356388
stdout=stdout,
357389
stderr=stderr,
358-
data=props)
390+
timestamp=end_timestamp_iso_format,
391+
data=data_payload)

tests/commands/compare/test_subsets.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,16 @@ def test_subsets_subset_ids(self):
201201
mix_stderr=False)
202202

203203
self.assert_success(result)
204-
expect = """PTS subset change summary:
205-
────────────────────────────────
206-
-> 3 tests analyzed | 1 ↑ promoted | 1 ↓ demoted
207-
-> Code files affected: bbb.py, ccc.py, ddd.py
208-
────────────────────────────────
209-
210-
Δ Rank Subset Rank Test Name Reason Density
211-
-------- ------------- ----------- -------------------- ---------
212-
NEW 1 file=ddd.py Changed file: ddd.py 0.9
213-
↑1 2 file=ccc.py Changed file: ccc.py 0.7
214-
↓1 3 file=bbb.py Changed file: bbb.py 0.5
215-
DELETED - file=aaa.py
216-
"""
217-
self.assertEqual(result.stdout, expect)
204+
output = result.stdout
205+
self.assertIn("3 tests analyzed | 1 ↑ promoted | 1 ↓ demoted", output)
206+
self.assertIn("Code files affected: bbb.py, ccc.py, ddd.py", output)
207+
self.assertIn("Δ Rank", output)
208+
self.assertIn("Density", output)
209+
for expected_row in [
210+
("NEW", "1", "file=ddd.py", "Changed file: ddd.py", "0.9"),
211+
("↑1", "2", "file=ccc.py", "Changed file: ccc.py", "0.7"),
212+
("↓1", "3", "file=bbb.py", "Changed file: bbb.py", "0.5"),
213+
("DELETED", "-", "file=aaa.py"),
214+
]:
215+
for cell in expected_row:
216+
self.assertIn(cell, output)

0 commit comments

Comments
 (0)