Skip to content

Commit 63a10f6

Browse files
committed
Add 'gate' command
1 parent d0d6ecc commit 63a10f6

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

launchable/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from .commands.compare import compare
1313
from .commands.detect_flakes import detect_flakes
14+
from .commands.gate import gate
1415
from .commands.inspect import inspect
1516
from .commands.record import record
1617
from .commands.split_subset import split_subset
@@ -93,6 +94,7 @@ def main(ctx, log_level, plugin_dir, dry_run, skip_cert_verification):
9394
main.add_command(stats)
9495
main.add_command(compare)
9596
main.add_command(detect_flakes, "detect-flakes")
97+
main.add_command(gate)
9698

9799
if __name__ == '__main__':
98100
main()

launchable/commands/gate.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import json
2+
import os
3+
import sys
4+
from http import HTTPStatus
5+
6+
import click
7+
from requests import Response
8+
from tabulate import tabulate
9+
10+
from launchable.commands.helper import find_or_create_session
11+
from launchable.utils.click import ignorable_error
12+
from launchable.utils.env_keys import REPORT_ERROR_KEY
13+
from launchable.utils.tracking import Tracking, TrackingClient
14+
15+
from ..utils.commands import Command
16+
from ..utils.launchable_client import LaunchableClient
17+
18+
19+
@click.command()
20+
@click.option(
21+
'--session',
22+
'session',
23+
help='In the format builds/<build-name>/test_sessions/<test-session-id>',
24+
type=str,
25+
required=True
26+
)
27+
@click.option(
28+
'--json',
29+
'is_json_format',
30+
help='display JSON format',
31+
is_flag=True
32+
)
33+
@click.pass_context
34+
def gate(ctx: click.core.Context, session: str, is_json_format: bool):
35+
tracking_client = TrackingClient(Command.DETECT_FLAKE, app=ctx.obj)
36+
client = LaunchableClient(app=ctx.obj)
37+
session_id = None
38+
try:
39+
session_id = find_or_create_session(
40+
context=ctx,
41+
session=session,
42+
build_name=None,
43+
tracking_client=tracking_client
44+
)
45+
except click.UsageError as e:
46+
click.echo(click.style(str(e), fg="red"), err=True)
47+
sys.exit(1)
48+
except Exception as e:
49+
tracking_client.send_error_event(
50+
event_name=Tracking.ErrorEvent.INTERNAL_CLI_ERROR,
51+
stack_trace=str(e),
52+
)
53+
if os.getenv(REPORT_ERROR_KEY):
54+
raise e
55+
else:
56+
click.echo(ignorable_error(e), err=True)
57+
if session_id is None:
58+
return
59+
try:
60+
res: Response = client.request("get", "gate", params={"session-id": os.path.basename(session_id)})
61+
62+
if res.status_code == HTTPStatus.NOT_FOUND:
63+
click.echo(click.style(
64+
"Gate data currently not available for this workspace.", 'yellow'), err=True)
65+
sys.exit()
66+
67+
res.raise_for_status()
68+
69+
res_json = res.json()
70+
71+
if is_json_format:
72+
display_as_json(res)
73+
else:
74+
display_as_table(res)
75+
76+
# Exit with failure status if gate failed
77+
if res_json.get('status') == 'FAILED':
78+
sys.exit(1)
79+
80+
except Exception as e:
81+
client.print_exception_and_recover(e, "Warning: failed to fetch gate status")
82+
83+
84+
def display_as_json(res: Response):
85+
res_json = res.json()
86+
click.echo(json.dumps(res_json, indent=2))
87+
88+
89+
def display_as_table(res: Response):
90+
headers = ["Status", "Quarantined (Ignored)", "Actionable Failures"]
91+
res_json = res.json()
92+
93+
status_icon = "PASSED" if res_json.get('status') == 'PASSED' else "FAILED"
94+
95+
rows = [[
96+
status_icon,
97+
res_json.get('quarantinedFailures', 0),
98+
res_json.get('actionableFailures', 0)
99+
]]
100+
101+
click.echo(tabulate(rows, headers, tablefmt="github"))

tests/commands/test_gate.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import json
2+
import os
3+
from unittest import mock
4+
5+
import responses
6+
7+
from launchable.utils.http_client import get_base_url
8+
from tests.cli_test_case import CliTestCase
9+
10+
11+
class GateTest(CliTestCase):
12+
@responses.activate
13+
@mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
14+
def test_gate_passed(self):
15+
"""Test gate command exits with 0 when status is PASSED"""
16+
responses.add(
17+
responses.GET,
18+
"{}/intake/organizations/{}/workspaces/{}/gate".format(
19+
get_base_url(),
20+
self.organization,
21+
self.workspace),
22+
json={
23+
'status': 'PASSED',
24+
'quarantinedFailures': 5,
25+
'actionableFailures': 0
26+
},
27+
status=200)
28+
29+
result = self.cli('gate', '--session', self.session)
30+
self.assert_success(result)
31+
self.assertIn('PASSED', result.output)
32+
33+
@responses.activate
34+
@mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
35+
def test_gate_failed(self):
36+
"""Test gate command exits with 1 when status is FAILED"""
37+
responses.add(
38+
responses.GET,
39+
"{}/intake/organizations/{}/workspaces/{}/gate".format(
40+
get_base_url(),
41+
self.organization,
42+
self.workspace),
43+
json={
44+
'status': 'FAILED',
45+
'quarantinedFailures': 2,
46+
'actionableFailures': 3
47+
},
48+
status=200)
49+
50+
result = self.cli('gate', '--session', self.session)
51+
self.assert_exit_code(result, 1)
52+
self.assertIn('FAILED', result.output)
53+
54+
@responses.activate
55+
@mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
56+
def test_gate_passed_json_format(self):
57+
"""Test gate command with --json flag when status is PASSED"""
58+
gate_data = {
59+
'status': 'PASSED',
60+
'quarantinedFailures': 5,
61+
'actionableFailures': 0
62+
}
63+
64+
responses.add(
65+
responses.GET,
66+
"{}/intake/organizations/{}/workspaces/{}/gate".format(
67+
get_base_url(),
68+
self.organization,
69+
self.workspace),
70+
json=gate_data,
71+
status=200)
72+
73+
result = self.cli('gate', '--session', self.session, '--json')
74+
self.assert_success(result)
75+
76+
# Verify JSON output
77+
output_json = json.loads(result.output)
78+
self.assertEqual(output_json['status'], 'PASSED')
79+
self.assertEqual(output_json['quarantinedFailures'], 5)
80+
self.assertEqual(output_json['actionableFailures'], 0)
81+
82+
@responses.activate
83+
@mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
84+
def test_gate_failed_json_format(self):
85+
"""Test gate command with --json flag when status is FAILED"""
86+
gate_data = {
87+
'status': 'FAILED',
88+
'quarantinedFailures': 2,
89+
'actionableFailures': 3
90+
}
91+
92+
responses.add(
93+
responses.GET,
94+
"{}/intake/organizations/{}/workspaces/{}/gate".format(
95+
get_base_url(),
96+
self.organization,
97+
self.workspace),
98+
json=gate_data,
99+
status=200)
100+
101+
result = self.cli('gate', '--session', self.session, '--json')
102+
self.assert_exit_code(result, 1)
103+
104+
# Verify JSON output
105+
output_json = json.loads(result.output)
106+
self.assertEqual(output_json['status'], 'FAILED')
107+
self.assertEqual(output_json['quarantinedFailures'], 2)
108+
self.assertEqual(output_json['actionableFailures'], 3)
109+
110+
@responses.activate
111+
@mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
112+
def test_gate_not_found(self):
113+
"""Test gate command when gate data is not available"""
114+
responses.add(
115+
responses.GET,
116+
"{}/intake/organizations/{}/workspaces/{}/gate".format(
117+
get_base_url(),
118+
self.organization,
119+
self.workspace),
120+
json={},
121+
status=404)
122+
123+
result = self.cli('gate', '--session', self.session)
124+
# Should exit with 0 when gate data is not available (non-error case)
125+
self.assert_success(result)
126+
self.assertIn('Gate data currently not available', result.output)

0 commit comments

Comments
 (0)