Skip to content

Commit 394e9af

Browse files
authored
Merge pull request #1180 from cloudbees-oss/session-option
Allow file indirection of a session file
2 parents 985cdc4 + ef2c255 commit 394e9af

File tree

9 files changed

+80
-68
lines changed

9 files changed

+80
-68
lines changed

smart_tests/commands/detect_flakes.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from smart_tests.utils.commands import Command
1313
from smart_tests.utils.env_keys import REPORT_ERROR_KEY
1414
from smart_tests.utils.exceptions import print_error_and_die
15-
from smart_tests.utils.session import get_session
15+
from smart_tests.utils.session import SessionId, get_session
1616
from smart_tests.utils.smart_tests_client import SmartTestsClient
1717
from smart_tests.utils.tracking import Tracking, TrackingClient
1818
from smart_tests.utils.typer_types import ignorable_error
@@ -35,12 +35,7 @@ class DetectFlakes(TestPathWriter):
3535
def __init__(
3636
self,
3737
app: Application,
38-
session: Annotated[str, typer.Option(
39-
"--session",
40-
help="In the format builds/<build-name>/test_sessions/<test-session-id>",
41-
metavar="SESSION",
42-
required=True
43-
)],
38+
session: Annotated[SessionId, SessionId.as_option()],
4439
retry_threshold: Annotated[DetectFlakesRetryThreshold, typer.Option(
4540
"--retry-threshold",
4641
help="Thoroughness of how \"flake\" is detected",
@@ -80,7 +75,7 @@ def run(self):
8075
"detect-flake",
8176
params={
8277
"confidence": self.retry_threshold.value.upper(),
83-
"session-id": os.path.basename(self.session),
78+
"session-id": self.session.test_part,
8479
"test-runner": self.app.test_runner,
8580
})
8681

smart_tests/commands/record/attachment.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import click
44

55
import smart_tests.args4p.typer as typer
6-
from smart_tests.utils.session import get_session
6+
from smart_tests.utils.session import SessionId, get_session
77

88
from ... import args4p
99
from ...app import Application
@@ -14,11 +14,7 @@
1414
@args4p.command(help="Record attachment information")
1515
def attachment(
1616
app: Application,
17-
session: Annotated[str, typer.Option(
18-
"--session",
19-
help="test session name",
20-
required=True
21-
)],
17+
session: Annotated[SessionId, SessionId.as_option()],
2218
attachments: Annotated[List[str], typer.Argument(
2319
multiple=True,
2420
help="Attachment files to upload"
@@ -33,7 +29,7 @@ def attachment(
3329
try:
3430
with open(a, mode='rb') as f:
3531
res = client.request(
36-
"post", f"{session}/attachment", compress=True, payload=f,
32+
"post", session.subpath('attachment'), compress=True, payload=f,
3733
additional_headers={"Content-Disposition": f"attachment;filename=\"{a}\""})
3834
res.raise_for_status()
3935
except OSError as e:

smart_tests/commands/record/tests.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import smart_tests.args4p.typer as typer
1818
from smart_tests.utils.authentication import ensure_org_workspace
1919
from smart_tests.utils.env_keys import REPORT_ERROR_KEY
20-
from smart_tests.utils.session import get_session, parse_session
20+
from smart_tests.utils.session import SessionId, get_session
2121
from smart_tests.utils.tracking import Tracking, TrackingClient
2222

2323
from ...app import Application
@@ -70,8 +70,7 @@ class RecordTests:
7070
test_session_id: int
7171

7272
# session is generated by `smart-tests record session` command
73-
# the session format is `builds/<BUILD_NUMBER>/test_sessions/<TEST_SESSION_ID>`
74-
session: str
73+
session: SessionId
7574

7675
# This function, if supplied, is used to build a test path
7776
# that uniquely identifies a test case
@@ -125,11 +124,7 @@ def parse(report: str) -> Generator[CaseEventType, None, None]:
125124
def __init__(
126125
self,
127126
app: Application,
128-
session: Annotated[str, typer.Option(
129-
"--session",
130-
help="In the format builds/<build-name>/test_sessions/<test-session-id>",
131-
required=True
132-
)],
127+
session: Annotated[SessionId, SessionId.as_option()],
133128
base_path: Annotated[Path | None, typer.Option(
134129
"--base",
135130
help="(Advanced) base directory to make test names portable",
@@ -213,7 +208,7 @@ def __init__(
213208
self.client.print_exception_and_recover(e)
214209
# To prevent users from stopping the CI pipeline, the cli exits with a
215210
# status code of 0, indicating that the program terminated successfully.
216-
build_name, test_session_id = parse_session(session)
211+
build_name, test_session_id = session.build_part, session.test_part
217212
exit(0)
218213

219214
self.reports: List[str] = []
@@ -322,7 +317,7 @@ def payload(
322317

323318
def send(payload: Dict[str, Union[str, List]]) -> None:
324319
res = self.client.request(
325-
"post", f"{self.session}/events", payload=payload, compress=True)
320+
"post", self.session.subpath("events"), payload=payload, compress=True)
326321
res.raise_for_status()
327322

328323
nonlocal is_observation
@@ -444,15 +439,15 @@ def recorded_result() -> Tuple[int, int, int, float]:
444439
INVALID_TIMESTAMP = datetime.datetime.fromtimestamp(0)
445440

446441

447-
def get_record_start_at(session: str, client: SmartTestsClient):
442+
def get_record_start_at(session: SessionId, client: SmartTestsClient):
448443
"""
449444
Determine the baseline timestamp to be used for up-to-date checks of report files.
450445
Only files newer than this timestamp will be collected.
451446
452447
Based on the thinking that if a build doesn't exist tests couldn't have possibly run, we attempt
453448
to use the timestamp of a build, with appropriate fallback.
454449
"""
455-
build_name, _ = parse_session(session)
450+
build_name = session.build_part
456451

457452
sub_path = f"builds/{build_name}"
458453

smart_tests/commands/subset.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from smart_tests.utils.authentication import get_org_workspace
1919
from smart_tests.utils.commands import Command
2020
from smart_tests.utils.exceptions import print_error_and_die
21-
from smart_tests.utils.session import get_session, parse_session
21+
from smart_tests.utils.session import SessionId, get_session
2222
from smart_tests.utils.tracking import Tracking, TrackingClient
2323

2424
from ..app import Application
@@ -98,12 +98,7 @@ class Subset(TestPathWriter):
9898
def __init__(
9999
self,
100100
app: Application,
101-
session: Annotated[str, typer.Option(
102-
"--session",
103-
help="In the format builds/<build-name>/test_sessions/<test-session-id>",
104-
metavar="SESSION",
105-
required=True
106-
)],
101+
session: Annotated[SessionId, SessionId.as_option()],
107102
target: Annotated[Percentage | None, typer.Option(
108103
type=parse_percentage,
109104
help="subsetting target from 0% to 100%"
@@ -213,7 +208,7 @@ def warn(msg: str):
213208
else:
214209
# not to block pipeline, parse session and use it
215210
self.client.print_exception_and_recover(e, "Warning: failed to check test session")
216-
self.build_name, self.session_id = parse_session(session)
211+
self.build_name, self.session_id = session.build_part, session.test_part
217212

218213
if is_get_tests_from_guess and is_get_tests_from_previous_sessions:
219214
print_error_and_die(

smart_tests/utils/fail_fast_mode.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import click
55

66
from .commands import Command
7+
from .session import SessionId
78

89
_fail_fast_mode_cache: Optional[bool] = None
910

@@ -30,7 +31,7 @@ def warn_and_exit_if_fail_fast_mode(message: str):
3031

3132
class FailFastModeValidateParams:
3233
def __init__(self, command: Command, build: Optional[str] = None, is_no_build: bool = False,
33-
test_suite: Optional[str] = None, session: Optional[str] = None,
34+
test_suite: Optional[str] = None, session: Optional[SessionId] = None,
3435
links: Sequence[Tuple[str, str]] = (), is_observation: bool = False,
3536
flavor: Sequence[Tuple[str, str]] = ()):
3637
self.command = command

smart_tests/utils/session.py

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
import re
55
import sys
66
from dataclasses import dataclass
7-
from typing import Tuple
87

98
import click
109
from requests import HTTPError
1110

11+
from smart_tests.args4p import typer
1212
from smart_tests.args4p.exceptions import BadCmdLineException
1313
from smart_tests.utils.smart_tests_client import SmartTestsClient
1414
from smart_tests.utils.tracking import Tracking
@@ -23,18 +23,58 @@ class TestSession:
2323
name: str | None = None
2424

2525

26-
def get_session(session: str, client: SmartTestsClient) -> TestSession:
27-
build_name, test_session_id = parse_session(session)
28-
29-
subpath = f"builds/{build_name}/test_sessions/{test_session_id}"
30-
res = client.request("get", subpath)
26+
class SessionId:
27+
'''Represents the user-specific test session via the --session option.'''
28+
29+
def __init__(self, id: str):
30+
'''This is the method in which we parse the user input, so be defensive'''
31+
if id.startswith('@'):
32+
file_path = id[1:]
33+
try:
34+
with open(file_path, 'r') as f:
35+
id = f.read().strip()
36+
except FileNotFoundError:
37+
raise BadCmdLineException(f"Session file '{file_path}' not found.")
38+
except IOError as e:
39+
raise BadCmdLineException(f"Error reading session file '{file_path}': {e}")
40+
41+
match = re.match(r"builds/([^/]+)/test_sessions/(.+)", id)
42+
43+
if match:
44+
self.id = id
45+
self.build_part = match.group(1)
46+
self.test_part = int(match.group(2))
47+
else:
48+
raise BadCmdLineException(
49+
f"Invalid session ID. Expecting the output from 'smart-tests record session', but got '{id}'")
50+
51+
def __str__(self):
52+
return self.id
53+
54+
def subpath(self, endpoint: str) -> str:
55+
return f"{self.id}/{endpoint}"
56+
57+
@staticmethod
58+
def as_option():
59+
'''To promote consistency of the --session option across commands, use this to define an option.'''
60+
return typer.Option(
61+
"--session",
62+
help="Session ID obtained by calling 'smart-tests record session'. It also accepts '@path/to/file' if the session ID is stored in a file ", # noqa E501
63+
required=True,
64+
metavar="SESSION",
65+
type=SessionId,
66+
)
67+
68+
69+
def get_session(session: SessionId, client: SmartTestsClient) -> TestSession:
70+
res = client.request("get", session.id)
3171

3272
try:
3373
res.raise_for_status()
3474
except HTTPError as e:
3575
if e.response.status_code == 404:
3676
# TODO(Konboi): move subset.print_error_and_die to util and use it
37-
msg = f"Session {session} was not found. Make sure to run `smart-tests record session --build {build_name}` before you run this command" # noqa E501
77+
msg = f"Session {session} was not found."
3878
click.secho(msg, fg='red', err=True)
3979
if client.tracking_client:
4080
client.tracking_client.send_error_event(event_name=Tracking.ErrorEvent.USER_ERROR, stack_trace=msg)
@@ -50,24 +90,3 @@ def get_session(session: str, client: SmartTestsClient) -> TestSession:
5090
observation_mode=test_session.get("isObservation"),
5191
name=test_session.get("name"),
5292
)
53-
54-
55-
def parse_session(session: str) -> Tuple[str, int]:
56-
"""Parse session to extract build name and test session id.
57-
58-
Args:
59-
session: Session in format "builds/{build_name}/test_sessions/{test_session_id}"
60-
61-
Returns:
62-
Tuple of (build_name, test_session_id)
63-
64-
Raises:
65-
ValueError: If session_id format is invalid
66-
"""
67-
match = re.match(r"builds/([^/]+)/test_sessions/(.+)", session)
68-
69-
if match:
70-
return match.group(1), int(match.group(2))
71-
else:
72-
raise BadCmdLineException(
73-
f"Invalid session ID format: {session}. Expected format: builds/{{build_name}}/test_sessions/{{test_session_id}}")

tests/cli_test_case.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def setUp(self):
4545
# not to use cached configuration between tests
4646
responses.reset()
4747

48+
# This is a bad idea -- to have one place where all the mocked responses are defined for all the test cases.
49+
# These fixtures should be moved closer to where they are used, as much as possible. It'll take time to clean
50+
# this up, but at least let's not add new ones here.
51+
# (Kohsuke 2025-11-12)
4852
responses.add(
4953
responses.POST,
5054
f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}"

tests/commands/record/test_tests.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44
import sys
5+
import tempfile
56
from pathlib import Path
67
from unittest import mock
78

@@ -18,7 +19,13 @@ class TestsTest(CliTestCase):
1819
@responses.activate
1920
@mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token})
2021
def test_with_group_name(self):
21-
result = self.cli('record', 'tests', 'maven', '--session', self.session, '--group', 'hoge',
22+
# also testing the use of @session_file syntax here
23+
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
24+
tmp.write(self.session)
25+
tmp.flush()
26+
session_file = tmp.name
27+
28+
result = self.cli('record', 'tests', 'maven', '--session', f'@{session_file}', '--group', 'hoge',
2229
str(self.report_files_dir) + "**/reports/")
2330

2431
self.assert_success(result)

tests/utils/test_session.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import responses
55

66
from smart_tests.utils.http_client import get_base_url
7-
from smart_tests.utils.session import TestSession, get_session
7+
from smart_tests.utils.session import SessionId, TestSession, get_session
88
from smart_tests.utils.smart_tests_client import SmartTestsClient
99
from tests.cli_test_case import CliTestCase
1010

@@ -27,7 +27,7 @@ def test_get_session(self):
2727
},
2828
status=200)
2929

30-
test_session = get_session(self.session, client)
30+
test_session = get_session(SessionId(self.session), client)
3131
self.assertEqual(test_session, TestSession(
3232
id=self.session_id,
3333
build_id=456,
@@ -44,5 +44,5 @@ def test_get_session(self):
4444
status=404)
4545

4646
with self.assertRaises(SystemExit) as cm:
47-
get_session(self.session, client)
47+
get_session(SessionId(self.session), client)
4848
self.assertEqual(cm.exception.code, 1)

0 commit comments

Comments
 (0)