Skip to content

Commit 8fb9d19

Browse files
committed
[forward port] #1128
#1128
1 parent 103c981 commit 8fb9d19

File tree

4 files changed

+137
-29
lines changed

4 files changed

+137
-29
lines changed

smart_tests/commands/record/build.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import typer
77
from tabulate import tabulate
88

9-
from smart_tests.commands.record.session import KeyValue, LinkKind, parse_key_value
10-
from smart_tests.utils.link import CIRCLECI_KEY, GITHUB_ACTIONS_KEY, JENKINS_URL_KEY, capture_link
9+
from smart_tests.commands.record.session import KeyValue, parse_key_value
10+
from smart_tests.utils.link import CIRCLECI_KEY, GITHUB_ACTIONS_KEY, JENKINS_URL_KEY, capture_links
1111
from smart_tests.utils.tracking import Tracking, TrackingClient
1212

1313
from ...utils import subprocess
@@ -292,14 +292,7 @@ def send(ws: List[Workspace]) -> str | None:
292292
# TODO(Konboi): port forward #1128
293293
# figure out all the CI links to capture
294294
def compute_links():
295-
_links = capture_link(os.environ)
296-
for k, v in links:
297-
_links.append({
298-
"title": k,
299-
"url": v,
300-
"kind": LinkKind.CUSTOM_LINK.name,
301-
})
302-
return _links
295+
return capture_links(link_options=links, env=os.environ)
303296

304297
try:
305298
lineage = branch or ws[0].branch

smart_tests/commands/record/session.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from smart_tests.utils.commands import Command
1010
from smart_tests.utils.exceptions import print_error_and_die
1111
from smart_tests.utils.fail_fast_mode import set_fail_fast_mode
12-
from smart_tests.utils.link import LinkKind, capture_link
12+
from smart_tests.utils.link import capture_links
1313
from smart_tests.utils.no_build import NO_BUILD_BUILD_NAME
1414
from smart_tests.utils.smart_tests_client import SmartTestsClient
1515
from smart_tests.utils.tracking import Tracking, TrackingClient
@@ -78,24 +78,16 @@ def session(
7878
if is_no_build:
7979
build_name = NO_BUILD_BUILD_NAME
8080

81-
payload = {
82-
"flavors": dict([(f.key, f.value) for f in flavors]),
83-
"isObservation": is_observation,
84-
"noBuild": is_no_build,
85-
"testSuite": test_suite,
86-
"timestamp": parsed_timestamp.isoformat() if parsed_timestamp else None,
87-
}
88-
89-
_links = capture_link(os.environ)
90-
for link in links:
91-
_links.append({
92-
"title": link.key,
93-
"url": link.value,
94-
"kind": LinkKind.CUSTOM_LINK.name,
95-
})
96-
payload["links"] = _links
97-
9881
try:
82+
payload = {
83+
"flavors": dict([(f.key, f.value) for f in flavors]),
84+
"isObservation": is_observation,
85+
"noBuild": is_no_build,
86+
"testSuite": test_suite,
87+
"timestamp": parsed_timestamp.isoformat() if parsed_timestamp else None,
88+
"links": capture_links(link_options=[(link.key, link.value) for link in links], env=os.environ)
89+
}
90+
9991
sub_path = f"builds/{build_name}/test_sessions"
10092
res = client.request("post", sub_path, payload=payload)
10193

smart_tests/utils/link.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import re
12
from enum import Enum
2-
from typing import Dict, List, Mapping
3+
from typing import Dict, List, Mapping, Sequence, Tuple
4+
5+
import typer
36

47
JENKINS_URL_KEY = 'JENKINS_URL'
58
JENKINS_BUILD_URL_KEY = 'BUILD_URL'
@@ -18,6 +21,8 @@
1821
CIRCLECI_BUILD_NUM_KEY = 'CIRCLE_BUILD_NUM'
1922
CIRCLECI_JOB_KEY = 'CIRCLE_JOB'
2023

24+
GITHUB_PR_REGEX = re.compile(r"^https://github\.com/[^/]+/[^/]+/pull/\d+$")
25+
2126

2227
class LinkKind(Enum):
2328

@@ -66,3 +71,77 @@ def capture_link(env: Mapping[str, str]) -> List[Dict[str, str]]:
6671
})
6772

6873
return links
74+
75+
76+
def capture_links_from_options(link_options: Sequence[Tuple[str, str]]) -> List[Dict[str, str]]:
77+
"""
78+
Validate user-provided link options, inferring the kind when not explicitly specified.
79+
80+
Each link option is expected in the format "kind|title=url" or "title=url".
81+
If the kind is not provided, it infers the kind based on the URL pattern.
82+
83+
Returns:
84+
A list of dictionaries, where each dictionary contains the validated title, URL, and kind for each link.
85+
86+
Raises:
87+
click.UsageError: If an invalid kind is provided or URL doesn't match with the specified kind.
88+
"""
89+
links = []
90+
for k, url in link_options:
91+
url = url.strip()
92+
93+
# if k,v in format "kind|title=url"
94+
if '|' in k:
95+
kind, title = (part.strip() for part in k.split('|', 1))
96+
if kind not in _valid_kinds():
97+
msg = f"Invalid kind '{kind}' passed to --link option.\nSupported kinds are {_valid_kinds()}"
98+
raise typer.BadParameter(typer.style(msg, fg=typer.colors.RED))
99+
100+
if not _url_matches_kind(url, kind):
101+
msg = f"Invalid url '{url}' passed to --link option.\nURL doesn't match with the specified kind '{kind}'"
102+
raise typer.BadParameter(typer.style(msg, fg=typer.colors.RED))
103+
# if k,v in format "title=url"
104+
else:
105+
kind = _infer_kinds(url)
106+
title = k.strip()
107+
108+
links.append({
109+
"title": title,
110+
"url": url,
111+
"kind": kind,
112+
})
113+
114+
return links
115+
116+
117+
def capture_links(link_options: Sequence[Tuple[str, str]], env: Mapping[str, str]) -> List[Dict[str, str]]:
118+
links = capture_links_from_options(link_options)
119+
120+
env_links = capture_link(env)
121+
for env_link in env_links:
122+
if not _has_kind(links, env_link['kind']):
123+
links.append(env_link)
124+
125+
return links
126+
127+
128+
def _infer_kinds(url: str) -> str:
129+
if GITHUB_PR_REGEX.match(url):
130+
return LinkKind.GITHUB_PULL_REQUEST.name
131+
132+
return LinkKind.CUSTOM_LINK.name
133+
134+
135+
def _has_kind(input_links: List[Dict[str, str]], kind: str) -> bool:
136+
return any(link for link in input_links if link['kind'] == kind)
137+
138+
139+
def _valid_kinds() -> List[str]:
140+
return [kind.name for kind in LinkKind if kind != LinkKind.LINK_KIND_UNSPECIFIED]
141+
142+
143+
def _url_matches_kind(url: str, kind: str) -> bool:
144+
if kind == LinkKind.GITHUB_PULL_REQUEST.name:
145+
return bool(GITHUB_PR_REGEX.match(url))
146+
147+
return True

tests/commands/record/test_build.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,47 @@ def test_with_link(self):
414414
],
415415
"timestamp": None
416416
}, payload)
417+
418+
# with invalid kind
419+
result = self.cli(
420+
"record", "build", "--build", self.build_name,
421+
'--link', 'UNKNOWN_KIND|PR=https://github.com/cloudbees-oss/smart-tests/pull/1',
422+
# set these options for easy to check payload
423+
"--no-commit-collection",
424+
"--branch", "main",
425+
"--commit", "app=abc12",
426+
)
427+
self.assertIn("Invalid kind 'UNKNOWN_KIND' passed to --link option.", result.output)
428+
429+
# with invalid URL
430+
result = self.cli(
431+
"record", "build", "--build", self.build_name,
432+
'--link', 'GITHUB_PULL_REQUEST|PR=https://smart-tests.test/pull/1/files',
433+
# set these options for easy to check payload
434+
"--no-commit-collection",
435+
"--branch", "main",
436+
"--commit", "app=abc12",
437+
)
438+
self.assertIn("Invalid url 'https://smart-tests.test/pull/1/files' passed to --link option.", result.output)
439+
440+
# with infer kind
441+
result = self.cli(
442+
"record", "build", "--build", self.build_name,
443+
'--link', 'PR=https://smart-tests.test/pull/1',
444+
# set these options for easy to check payload
445+
"--no-commit-collection",
446+
"--branch", "main",
447+
"--commit", "app=abc12",
448+
)
449+
self.assert_success(result)
450+
451+
# with explicit kind
452+
result = self.cli(
453+
"record", "build", "--build", self.build_name,
454+
'--link', 'GITHUB_PULL_REQUEST|PR=https://smart-tests.test/pull/1',
455+
# set these options for easy to check payload
456+
"--no-commit-collection",
457+
"--branch", "main",
458+
"--commit", "app=abc12",
459+
)
460+
self.assert_success(result)

0 commit comments

Comments
 (0)