Skip to content

Commit

Permalink
Merge pull request #537 from launchableinc/no-build-option
Browse files Browse the repository at this point in the history
Support --no-build option
  • Loading branch information
Konboi authored Feb 9, 2023
2 parents 9ad7509 + 085d518 commit 6391d7c
Show file tree
Hide file tree
Showing 29 changed files with 213 additions and 44 deletions.
18 changes: 18 additions & 0 deletions launchable/commands/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import click

from launchable.utils.no_build import NO_BUILD_BUILD_NAME

from ..utils.http_client import LaunchableClient
from ..utils.session import read_build, read_session

Expand All @@ -13,6 +15,7 @@ def find_or_create_session(
flavor=[],
is_observation: bool = False,
links: List[str] = [],
is_no_build: bool = False,
) -> Optional[str]:
"""Determine the test session ID to be used.
Expand All @@ -32,6 +35,20 @@ def find_or_create_session(
_check_observation_mode_status(session, is_observation)
return session

if is_no_build:
context.invoke(
session_command,
build_name=NO_BUILD_BUILD_NAME,
save_session_file=True,
print_session=False,
flavor=flavor,
is_observation=is_observation,
links=links,
is_no_build=is_no_build,
)
saved_build_name = read_build()
return read_session(str(saved_build_name))

saved_build_name = read_build()
if not saved_build_name:
raise click.UsageError(
Expand Down Expand Up @@ -65,6 +82,7 @@ def find_or_create_session(
flavor=flavor,
is_observation=is_observation,
links=links,
is_no_build=is_no_build,
)
return read_session(saved_build_name)

Expand Down
31 changes: 28 additions & 3 deletions launchable/commands/record/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from ...utils.click import KeyValueType
from ...utils.env_keys import REPORT_ERROR_KEY
from ...utils.http_client import LaunchableClient
from ...utils.session import write_session
from ...utils.no_build import NO_BUILD_BUILD_NAME
from ...utils.session import read_build, write_session

LAUNCHABLE_SESSION_DIR_KEY = 'LAUNCHABLE_SESSION_DIR'

Expand All @@ -21,7 +22,6 @@
'--build',
'build_name',
help='build name',
required=True,
type=str,
metavar='BUILD_NAME'
)
Expand Down Expand Up @@ -54,6 +54,13 @@
default=[],
cls=KeyValueType,
)
@click.option(
"--no-build",
"is_no_build",
help="If you want to only send test reports, please use this option",
is_flag=True,
hidden=True,
)
@click.pass_context
def session(
ctx: click.core.Context,
Expand All @@ -63,6 +70,7 @@ def session(
flavor: List[str] = [],
is_observation: bool = False,
links: List[str] = [],
is_no_build: bool = False,
):
"""
print_session is for barckward compatibility.
Expand All @@ -73,13 +81,26 @@ def session(
you should set print_session = False because users don't expect to print session ID to the subset output.
"""

if not is_no_build and not build_name:
raise click.UsageError("Error: Missing option '--build'")

if is_no_build:
build = read_build()
if build and build != "":
raise click.UsageError(
'The cli already created `.launchable file`. If you want to use `--no-build option`, please remove `.launchable` file before executing.') # noqa: E501

if is_no_build:
build_name = NO_BUILD_BUILD_NAME

flavor_dict = {}
for f in normalize_key_value_types(flavor):
flavor_dict[f[0]] = f[1]

payload = {
"flavors": flavor_dict,
"isObservation": is_observation,
"noBuild": is_no_build,
}

_links = capture_link(os.environ)
Expand Down Expand Up @@ -110,7 +131,11 @@ def session(
sys.exit(1)

res.raise_for_status()
session_id = res.json()['id']

session_id = res.json().get('id', None)
if is_no_build:
build_name = res.json().get("buildNumber", "")
sub_path = "builds/{}/test_sessions".format(build_name)

if save_session_file:
write_session(build_name, "{}/{}".format(sub_path, session_id))
Expand Down
85 changes: 74 additions & 11 deletions launchable/commands/record/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ...utils.exceptions import InvalidJUnitXMLException
from ...utils.http_client import LaunchableClient
from ...utils.logger import Logger
from ...utils.no_build import NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID
from ...utils.session import parse_session, read_build
from ..helper import find_or_create_session
from .case_event import CaseEvent, CaseEventType
Expand Down Expand Up @@ -128,6 +129,13 @@ def _validate_group(ctx, param, value):
default=[],
cls=KeyValueType,
)
@click.option(
'--no-build',
'is_no_build',
help="If you want to only send test reports, please use this option",
is_flag=True,
hidden=True,
)
@click.pass_context
def tests(
context: click.core.Context,
Expand All @@ -142,6 +150,7 @@ def tests(
group: str,
is_allow_test_before_build: bool,
links: List[str] = [],
is_no_build: bool = False,
):
logger = Logger()

Expand All @@ -153,18 +162,26 @@ def tests(

file_path_normalizer = FilePathNormalizer(base_path, no_base_path_inference=no_base_path_inference)

if is_no_build and (read_build() and read_build() != ""):
raise click.UsageError(
'The cli already created `.launchable` file. If you want to use `--no-build` option, please remove `.launchable` file before executing.') # noqa: E501

try:
if subsetting_id:
if is_no_build:
session_id = "builds/{}/test_sessions/{}".format(NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID)
record_start_at = INVALID_TIMESTAMP
elif subsetting_id:
result = get_session_and_record_start_at_from_subsetting_id(subsetting_id, client)
session_id = result["session"]
record_start_at = result["start_at"]
else:
session_id = find_or_create_session(
# The session_id must be back, so cast to str
session_id = str(find_or_create_session(
context=context,
session=session,
build_name=build_name,
flavor=flavor,
links=links)
links=links))
build_name = read_build()
record_start_at = get_record_start_at(session_id, client)

Expand Down Expand Up @@ -208,6 +225,40 @@ def parse_func(self) -> ParseFunc:
def parse_func(self, f: ParseFunc):
self._parse_func = f

@property
def build_name(self) -> str:
return self._build_name

@build_name.setter
def build_name(self, build_name: str):
self._build_name = build_name

@property
def test_session_id(self) -> int:
return self._test_session_id

@test_session_id.setter
def test_session_id(self, test_session_id: int):
self._test_session_id = test_session_id

# session is generated by `launchable record session` command
# the session format is `builds/<BUILD_NUMBER>/test_sessions/<TEST_SESSION_ID>`
@property
def session(self) -> str:
return self._session

@session.setter
def session(self, session: str):
self._session = session

@property
def is_no_build(self) -> bool:
return self._is_no_build

@is_no_build.setter
def is_no_build(self, is_no_build: bool):
self._is_no_build = is_no_build

# setter only property that sits on top of the parse_func property
def set_junitxml_parse_func(self, f: JUnitXmlParseFunc):
"""
Expand Down Expand Up @@ -267,6 +318,10 @@ def __init__(self, dry_run=False):
self.dry_run = dry_run
self.no_base_path_inference = no_base_path_inference
self.is_allow_test_before_build = is_allow_test_before_build
self.build_name = build_name
self.test_session_id = test_session_id
self.session = session_id
self.is_no_build = is_no_build

def make_file_path_component(self, filepath) -> TestPathComponent:
"""Create a single TestPathComponent from the given file path"""
Expand All @@ -278,7 +333,8 @@ def report(self, junit_report_file: str):
ctime = datetime.datetime.fromtimestamp(
os.path.getctime(junit_report_file))

if not self.is_allow_test_before_build and (self.check_timestamp and ctime.timestamp() < record_start_at.timestamp()):
if not self.is_allow_test_before_build and not self.is_no_build and (
self.check_timestamp and ctime.timestamp() < record_start_at.timestamp()):
format = "%Y-%m-%d %H:%M:%S"
logger.warning("skip: {} is too old to report. start_record_at: {} file_created_at: {}".format(
junit_report_file, record_start_at.strftime(format), ctime.strftime(format)))
Expand Down Expand Up @@ -317,7 +373,7 @@ def testcases(reports: List[str]) -> Generator[CaseEventType, None, None]:

# generator that creates the payload incrementally
def payload(cases: Generator[TestCase, None, None],
test_runner, group: str) -> Tuple[Dict[str, Union[str, List]], List[Exception]]:
test_runner, group: str) -> Tuple[Dict[str, Union[str, List, bool]], List[Exception]]:
nonlocal count
cs = []
exs = []
Expand All @@ -331,11 +387,11 @@ def payload(cases: Generator[TestCase, None, None],
exs.append(ex)

count += len(cs)
return {"events": cs, "testRunner": test_runner, "group": group}, exs
return {"events": cs, "testRunner": test_runner, "group": group, "noBuild": self.is_no_build}, exs

def send(payload: Dict[str, Union[str, List]]) -> None:
res = client.request(
"post", "{}/events".format(session_id), payload=payload, compress=True)
"post", "{}/events".format(self.session), payload=payload, compress=True)

if res.status_code == HTTPStatus.NOT_FOUND:
if session:
Expand All @@ -362,6 +418,13 @@ def send(payload: Dict[str, Union[str, List]]) -> None:

res.raise_for_status()

# If don’t override build, test session and session_id, build and test session will be made per chunk request.
if is_no_build:
self.build_name = res.json().get("build", {}).get("build", NO_BUILD_BUILD_NAME)
self.test_session_id = res.json().get("testSession", {}).get("id", NO_BUILD_TEST_SESSION_ID)
self.session = "builds/{}/test_sessions/{}".format(self.build_name, self.test_session_id)
self.is_no_build = False

def recorded_result() -> Tuple[int, int, int, float]:
test_count = 0
success_count = 0
Expand Down Expand Up @@ -395,7 +458,7 @@ def recorded_result() -> Tuple[int, int, int, float]:
send(p)
exceptions.extend(es)

res = client.request("patch", "{}/close".format(session_id),
res = client.request("patch", "{}/close".format(self.session),
payload={"metadata": get_env_values(client)})
res.raise_for_status()
is_observation = res.json().get("isObservation", False)
Expand Down Expand Up @@ -430,8 +493,8 @@ def recorded_result() -> Tuple[int, int, int, float]:

click.echo(
"Launchable recorded tests for build {} (test session {}) to workspace {}/{} from {} files:".format(
build_name,
test_session_id,
self.build_name,
self.test_session_id,
org,
workspace,
file_count,
Expand All @@ -454,7 +517,7 @@ def recorded_result() -> Tuple[int, int, int, float]:
.format(
organization=org,
workspace=workspace,
test_session_id=test_session_id,
test_session_id=self.test_session_id,
))

context.obj = RecordTests(dry_run=context.obj.dry_run)
Expand Down
9 changes: 9 additions & 0 deletions launchable/commands/subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@
default=[],
cls=KeyValueType,
)
@click.option(
"--no-build",
"is_no_build",
help="If you want to only send test reports, please use this option",
is_flag=True,
hidden=True,
)
@click.pass_context
def subset(
context: click.core.Context,
Expand All @@ -153,6 +160,7 @@ def subset(
is_output_exclusion_rules: bool,
ignore_flaky_tests_above: Optional[float],
links: List[str] = [],
is_no_build: bool = False,
):

if is_observation and is_get_tests_from_previous_sessions:
Expand All @@ -171,6 +179,7 @@ def subset(
flavor=flavor,
is_observation=is_observation,
links=links,
is_no_build=is_no_build,
)
file_path_normalizer = FilePathNormalizer(base_path, no_base_path_inference=no_base_path_inference)

Expand Down
2 changes: 1 addition & 1 deletion launchable/commands/test_path_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class TestPathWriter(object):
base_path = Optional[str]
base_path = None # type: Optional[str]

def __init__(self, dry_run=False):
self._formatter = TestPathWriter.default_formatter
Expand Down
2 changes: 2 additions & 0 deletions launchable/utils/no_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NO_BUILD_BUILD_NAME = "nameless"
NO_BUILD_TEST_SESSION_ID = 0
7 changes: 4 additions & 3 deletions tests/commands/record/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_run_session_without_flavor(self):
self.assertEqual(result.exit_code, 0)

payload = json.loads(responses.calls[0].request.body.decode())
self.assert_json_orderless_equal({"flavors": {}, "isObservation": False, "links": []}, payload)
self.assert_json_orderless_equal({"flavors": {}, "isObservation": False, "links": [], "noBuild": False}, payload)

@responses.activate
@mock.patch.dict(os.environ, {
Expand All @@ -46,7 +46,8 @@ def test_run_session_with_flavor(self):
"k e y": "v a l u e",
},
"isObservation": False,
"links": []
"links": [],
"noBuild": False,
}, payload)

with self.assertRaises(ValueError):
Expand All @@ -65,4 +66,4 @@ def test_run_session_with_observation(self):
self.assertEqual(result.exit_code, 0)

payload = json.loads(responses.calls[0].request.body.decode())
self.assert_json_orderless_equal({"flavors": {}, "isObservation": True, "links": []}, payload)
self.assert_json_orderless_equal({"flavors": {}, "isObservation": True, "links": [], "noBuild": False}, payload)
Loading

0 comments on commit 6391d7c

Please sign in to comment.