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
2 changes: 2 additions & 0 deletions launchable/commands/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def find_or_create_session(
session_id = read_session(saved_build_name)
if session_id:
_check_observation_mode_status(session_id, is_observation, tracking_client=tracking_client, app=context.obj)
if links:
click.echo(click.style("WARNING: --link option is ignored since session already exists."), err=True)
return session_id

context.invoke(
Expand Down
11 changes: 2 additions & 9 deletions launchable/commands/record/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import click
from tabulate import tabulate

from launchable.utils.link import CIRCLECI_KEY, GITHUB_ACTIONS_KEY, JENKINS_URL_KEY, LinkKind, capture_link
from launchable.utils.link import CIRCLECI_KEY, GITHUB_ACTIONS_KEY, JENKINS_URL_KEY, capture_links
from launchable.utils.tracking import Tracking, TrackingClient

from ...utils import subprocess
Expand Down Expand Up @@ -316,14 +316,7 @@ def synthesize_workspaces() -> List[Workspace]:
def send(ws: List[Workspace]) -> Optional[str]:
# figure out all the CI links to capture
def compute_links():
_links = capture_link(os.environ)
for k, v in links:
_links.append({
"title": k,
"url": v,
"kind": LinkKind.CUSTOM_LINK.name,
})
return _links
return capture_links(links, os.environ)

try:
payload = {
Expand Down
30 changes: 11 additions & 19 deletions launchable/commands/record/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import click

from launchable.utils.click import DATETIME_WITH_TZ, validate_past_datetime
from launchable.utils.link import LinkKind, capture_link
from launchable.utils.link import capture_links
from launchable.utils.tracking import Tracking, TrackingClient

from ...utils.click import KEY_VALUE
Expand Down Expand Up @@ -181,25 +181,17 @@ def session(

flavor_dict = dict(flavor)

payload = {
"flavors": flavor_dict,
"isObservation": is_observation,
"noBuild": is_no_build,
"lineage": lineage,
"testSuite": test_suite,
"timestamp": timestamp.isoformat() if timestamp else None,
}

_links = capture_link(os.environ)
for link in links:
_links.append({
"title": link[0],
"url": link[1],
"kind": LinkKind.CUSTOM_LINK.name,
})
payload["links"] = _links

try:
payload = {
"flavors": flavor_dict,
"isObservation": is_observation,
"noBuild": is_no_build,
"lineage": lineage,
"testSuite": test_suite,
"timestamp": timestamp.isoformat() if timestamp else None,
"links": capture_links(links, os.environ),
}

sub_path = "builds/{}/test_sessions".format(build_name)
res = client.request("post", sub_path, payload=payload)

Expand Down
5 changes: 5 additions & 0 deletions launchable/commands/record/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ def tests(

is_no_build = False

if session and links:
warn_and_exit_if_fail_fast_mode(
"WARNING: `--link` and `--session` are set together.\n--link option can't be used with existing sessions."
)

try:
if is_no_build:
session_id = "builds/{}/test_sessions/{}".format(NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID)
Expand Down
85 changes: 84 additions & 1 deletion launchable/utils/link.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import re
from enum import Enum
from typing import Dict, List, Mapping
from typing import Dict, List, Mapping, Sequence, Tuple

import click

JENKINS_URL_KEY = 'JENKINS_URL'
JENKINS_BUILD_URL_KEY = 'BUILD_URL'
Expand All @@ -18,6 +21,8 @@
CIRCLECI_BUILD_NUM_KEY = 'CIRCLE_BUILD_NUM'
CIRCLECI_JOB_KEY = 'CIRCLE_JOB'

GITHUB_PR_REGEX = re.compile(r"^https://github\.com/[^/]+/[^/]+/pull/\d+$")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the named group? Here is the example:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are not extracting any parts of the URL, I think it's better to keep it simple.



class LinkKind(Enum):

Expand Down Expand Up @@ -71,3 +76,81 @@ def capture_link(env: Mapping[str, str]) -> List[Dict[str, str]]:
})

return links


def capture_links_from_options(link_options: Sequence[Tuple[str, str]]) -> List[Dict[str, str]]:
"""
Validate user-provided link options, inferring the kind when not explicitly specified.

Each link option is expected in the format "kind|title=url" or "title=url".
If the kind is not provided, it infers the kind based on the URL pattern.

Returns:
A list of dictionaries, where each dictionary contains the validated title, URL, and kind for each link.

Raises:
click.UsageError: If an invalid kind is provided or URL doesn't match with the specified kind.
"""
links = []
for k, url in link_options:
url = url.strip()

# if k,v in format "kind|title=url"
if '|' in k:
kind, title = (part.strip() for part in k.split('|', 1))
if kind not in _valid_kinds():
msg = ("Invalid kind '{}' passed to --link option.\n"
"Supported kinds are: {}".format(kind, _valid_kinds()))
raise click.UsageError(click.style(msg, fg="red"))

if not _url_matches_kind(url, kind):
msg = ("Invalid url '{}' passed to --link option.\n"
"URL doesn't match with the specified kind: {}".format(url, kind))
raise click.UsageError(click.style(msg, fg="red"))

# if k,v in format "title=url"
else:
kind = _infer_kind(url)
title = k.strip()

links.append({
"title": title,
"url": url,
"kind": kind,
})

return links


def capture_links(link_options: Sequence[Tuple[str, str]], env: Mapping[str, str]) -> List[Dict[str, str]]:

links = capture_links_from_options(link_options)

env_links = capture_link(env)
for env_link in env_links:
if not _has_kind(links, env_link['kind']):
links.append(env_link)

return links


def _infer_kind(url: str) -> str:
if GITHUB_PR_REGEX.match(url):
return LinkKind.GITHUB_PULL_REQUEST.name

return LinkKind.CUSTOM_LINK.name


def _has_kind(input_links: List[Dict[str, str]], kind: str) -> bool:
return any(link for link in input_links if link['kind'] == kind)


def _valid_kinds() -> List[str]:
return [kind.name for kind in LinkKind if kind != LinkKind.LINK_KIND_UNSPECIFIED]


def _url_matches_kind(url: str, kind: str) -> bool:
if kind == LinkKind.GITHUB_PULL_REQUEST.name:
return bool(GITHUB_PR_REGEX.match(url))

return True
47 changes: 47 additions & 0 deletions tests/commands/record/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,50 @@ def test_with_timestamp(self, mock_check_output):
}, payload)

self.assertEqual(read_build(), self.build_name)

@responses.activate
@mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
def test_build_with_links(self):
# Invalid kind
result = self.cli(
"record",
"build",
"--no-commit-collection",
"--link",
"UNKNOWN_KIND|PR=https://github.com/launchableinc/cli/pull/1",
"--name",
self.build_name)
self.assertIn("Invalid kind 'UNKNOWN_KIND' passed to --link option", result.output)

# Invalid URL
result = self.cli(
"record",
"build",
"--no-commit-collection",
"--link",
"GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/1/files",
"--name",
self.build_name)
self.assertIn("Invalid url 'https://github.com/launchableinc/cli/pull/1/files' passed to --link option", result.output)

# Infer kind
result = self.cli(
"record",
"build",
"--no-commit-collection",
"--link",
"PR=https://github.com/launchableinc/cli/pull/1",
"--name",
self.build_name)
self.assert_success(result)

# Explicit kind
result = self.cli(
"record",
"build",
"--no-commit-collection",
"--link",
"GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/1",
"--name",
self.build_name)
self.assert_success(result)
84 changes: 84 additions & 0 deletions tests/commands/record/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import responses # type: ignore

from launchable.utils.http_client import get_base_url
from launchable.utils.link import LinkKind
from tests.cli_test_case import CliTestCase


Expand Down Expand Up @@ -188,3 +189,86 @@ def test_run_session_with_timestamp(self):
"testSuite": None,
"timestamp": "2023-10-01T12:00:00+00:00",
}, payload)

@responses.activate
@mock.patch.dict(os.environ, {
"LAUNCHABLE_TOKEN": CliTestCase.launchable_token,
'LANG': 'C.UTF-8',
"GITHUB_PULL_REQUEST_URL": "https://github.com/launchableinc/cli/pull/1",
}, clear=True)
def test_run_session_with_links(self):
# Endpoint to assert
endpoint = "{}/intake/organizations/{}/workspaces/{}/builds/{}/test_sessions".format(
get_base_url(),
self.organization,
self.workspace,
self.build_name)

# Capture from environment
result = self.cli("record", "session", "--build", self.build_name)
self.assert_success(result)
payload = json.loads(self.find_request(endpoint, 0).request.body.decode())
self.assertEqual([{
"kind": LinkKind.GITHUB_PULL_REQUEST.name,
"title": "",
"url": "https://github.com/launchableinc/cli/pull/1",
}], payload["links"])

# Priority check
result = self.cli("record", "session", "--build", self.build_name, "--link",
"GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2")
self.assert_success(result)
payload = json.loads(self.find_request(endpoint, 1).request.body.decode())
self.assertEqual([{
"kind": LinkKind.GITHUB_PULL_REQUEST.name,
"title": "PR",
"url": "https://github.com/launchableinc/cli/pull/2",
}], payload["links"])

# Infer kind
result = self.cli("record", "session", "--build", self.build_name, "--link",
"PR=https://github.com/launchableinc/cli/pull/2")
self.assert_success(result)
payload = json.loads(self.find_request(endpoint, 2).request.body.decode())
self.assertEqual([{
"kind": LinkKind.GITHUB_PULL_REQUEST.name,
"title": "PR",
"url": "https://github.com/launchableinc/cli/pull/2",
}], payload["links"])

# Explicit kind
result = self.cli("record", "session", "--build", self.build_name, "--link",
"GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2")
self.assert_success(result)
payload = json.loads(self.find_request(endpoint, 3).request.body.decode())
self.assertEqual([{
"kind": LinkKind.GITHUB_PULL_REQUEST.name,
"title": "PR",
"url": "https://github.com/launchableinc/cli/pull/2",
}], payload["links"])

# Multiple kinds
result = self.cli("record", "session", "--build", self.build_name, "--link",
"GITHUB_ACTIONS|=https://github.com/launchableinc/mothership/actions/runs/3747451612")
self.assert_success(result)
payload = json.loads(self.find_request(endpoint, 4).request.body.decode())
self.assertEqual([{
"kind": LinkKind.GITHUB_ACTIONS.name,
"title": "",
"url": "https://github.com/launchableinc/mothership/actions/runs/3747451612",
},
{
"kind": LinkKind.GITHUB_PULL_REQUEST.name,
"title": "",
"url": "https://github.com/launchableinc/cli/pull/1",
}], payload["links"])

# Invalid kind
result = self.cli("record", "session", "--build", self.build_name, "--link",
"UNKNOWN_KIND|PR=https://github.com/launchableinc/cli/pull/2")
self.assertIn("Invalid kind 'UNKNOWN_KIND' passed to --link option", result.output)

# Invalid URL
result = self.cli("record", "session", "--build", self.build_name, "--link",
"GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2/files")
self.assertIn("Invalid url 'https://github.com/launchableinc/cli/pull/2/files' passed to --link option", result.output)
Loading
Loading