From 38126f03c9160eff470a1e8647c51e244cb4b58b Mon Sep 17 00:00:00 2001 From: psakthivel Date: Mon, 11 May 2026 09:37:21 +0530 Subject: [PATCH] feat(AIENG-453): Add view command to display historical test data and insights --- smart_tests/__main__.py | 2 + smart_tests/commands/view/__init__.py | 13 + smart_tests/commands/view/flaky_tests.py | 107 ++++ smart_tests/commands/view/test_results.py | 135 +++++ tests/commands/test_view_flaky_tests.py | 393 ++++++++++++++ tests/commands/test_view_test_results.py | 596 ++++++++++++++++++++++ 6 files changed, 1246 insertions(+) create mode 100644 smart_tests/commands/view/__init__.py create mode 100644 smart_tests/commands/view/flaky_tests.py create mode 100644 smart_tests/commands/view/test_results.py create mode 100644 tests/commands/test_view_flaky_tests.py create mode 100644 tests/commands/test_view_test_results.py diff --git a/smart_tests/__main__.py b/smart_tests/__main__.py index 08ce06bad..3734f4e93 100644 --- a/smart_tests/__main__.py +++ b/smart_tests/__main__.py @@ -16,6 +16,7 @@ from smart_tests.commands.subset import subset from smart_tests.commands.update import update from smart_tests.commands.verify import verify +from smart_tests.commands.view import view cli = Group(name="cli", callback=Application) cli.add_command(record) @@ -29,6 +30,7 @@ cli.add_command(detect_flakes) cli.add_command(gate) cli.add_command(get) +cli.add_command(view) def _load_test_runners(): diff --git a/smart_tests/commands/view/__init__.py b/smart_tests/commands/view/__init__.py new file mode 100644 index 000000000..f46832009 --- /dev/null +++ b/smart_tests/commands/view/__init__.py @@ -0,0 +1,13 @@ +from ... import args4p +from ...app import Application +from .flaky_tests import flaky_tests +from .test_results import test_results + + +@args4p.group(help="View historical test data and insights") +def view(app: Application): + return app + + +view.add_command(flaky_tests) +view.add_command(test_results) diff --git a/smart_tests/commands/view/flaky_tests.py b/smart_tests/commands/view/flaky_tests.py new file mode 100644 index 000000000..d17b53f59 --- /dev/null +++ b/smart_tests/commands/view/flaky_tests.py @@ -0,0 +1,107 @@ +import json +import re +import sys +from http import HTTPStatus +from typing import Annotated + +import click + +import smart_tests.args4p.typer as typer +from smart_tests.args4p.converters import intType +from smart_tests.args4p.exceptions import BadCmdLineException + +from ... import args4p +from ...app import Application +from ...utils.smart_tests_client import SmartTestsClient +from ...utils.typer_types import DateTimeWithTimezone, parse_datetime_with_timezone + + +def validate_iso_week(value: str) -> str: + """Validate ISO week format YYYY-Www""" + pattern = r'^\d{4}-W\d{2}$' + if not re.match(pattern, value): + raise BadCmdLineException( + f"Invalid year-week format: '{value}'. Expected format: YYYY-Www (e.g., 2026-W15)" + ) + return value + + +@args4p.command(help="View flaky test data with weekly scores") +def flaky_tests( + app: Application, + year_week: Annotated[str | None, typer.Option( + "--year-week", + help="Specific ISO week for flaky tests (e.g., '2026-W15')", + type=validate_iso_week, + metavar="YYYY-Www" + )] = None, + weeks: Annotated[int | None, typer.Option( + "--weeks", + help="Number of weeks to retrieve (default: 1, max: 12)", + type=intType(min=1, max=12), + metavar="N" + )] = None, + from_date: Annotated[DateTimeWithTimezone | None, typer.Option( + "--from", + help="Start date/time (ISO 8601 format, e.g., '2026-04-08' or '2026-04-08T00:00:00Z')", + type=parse_datetime_with_timezone, + metavar="DATE" + )] = None, + to_date: Annotated[DateTimeWithTimezone | None, typer.Option( + "--to", + help="End date/time (ISO 8601 format, e.g., '2026-04-14' or '2026-04-14T23:59:59Z')", + type=parse_datetime_with_timezone, + metavar="DATE" + )] = None, + test_suite: Annotated[str | None, typer.Option( + "--test-suite", + help="Test suite name filter (e.g., 'unit-tests')", + metavar="NAME" + )] = None, + limit: Annotated[int | None, typer.Option( + "--limit", + help="Max results to return per week (default: 50, max: 500)", + type=intType(min=1, max=500), + metavar="N" + )] = None, +): + """View flaky tests with weekly scores and trends""" + client = SmartTestsClient(app=app) + + # Build query parameters + params = {} + if year_week: + params["year-week"] = year_week + if weeks: + params["weeks"] = weeks + if from_date: + params["from"] = str(from_date) + if to_date: + params["to"] = str(to_date) + if test_suite: + params["test-suite"] = test_suite + if limit: + params["limit"] = limit + + try: + res = client.request("get", "view/flaky-tests", params=params) + + if res.status_code == HTTPStatus.NOT_FOUND: + click.secho( + "No flaky test data found. Check your filters and try again.", + fg='yellow', err=True + ) + sys.exit(1) + + res.raise_for_status() + response_json = res.json() + + # Output JSON format + click.echo(json.dumps(response_json, indent=2)) + + except Exception as e: + client.print_exception_and_recover( + e, + "Warning: failed to retrieve flaky tests from server" + ) + sys.exit(1) diff --git a/smart_tests/commands/view/test_results.py b/smart_tests/commands/view/test_results.py new file mode 100644 index 000000000..fd3ea0ced --- /dev/null +++ b/smart_tests/commands/view/test_results.py @@ -0,0 +1,135 @@ +import json +import sys +from enum import Enum +from http import HTTPStatus +from typing import Annotated + +import click + +import smart_tests.args4p.typer as typer +from smart_tests.args4p.converters import intType +from smart_tests.args4p.exceptions import BadCmdLineException + +from ... import args4p +from ...app import Application +from ...utils.smart_tests_client import SmartTestsClient +from ...utils.typer_types import DateTimeWithTimezone, parse_datetime_with_timezone + + +class TestStatus(str, Enum): + """Test execution status""" + PASSED = "PASSED" + FAILED = "FAILED" + SKIPPED = "SKIPPED" + FLAKE = "FLAKE" + + @staticmethod + def from_str(value: str) -> "TestStatus": + """Parse status from string""" + for member in TestStatus: + if member.value.lower() == value.lower(): + return member + raise BadCmdLineException( + f"Invalid status: '{value}'. Valid values: PASSED, FAILED, SKIPPED, FLAKE" + ) + + +@args4p.command(help="View detailed test execution results") +def test_results( + app: Application, + test_path: Annotated[str | None, typer.Option( + "--test-path", + help="Filter by test path (exact match)", + metavar="PATH" + )] = None, + status: Annotated[TestStatus | None, typer.Option( + "--status", + help="Filter by test status (PASSED, FAILED, SKIPPED, FLAKE)", + type=TestStatus.from_str, + metavar="STATUS" + )] = None, + branch: Annotated[str | None, typer.Option( + "--branch", + help="Filter by branch/lineage (exact match)", + metavar="BRANCH" + )] = None, + test_suite: Annotated[str | None, typer.Option( + "--test-suite", + help="Filter by test suite name (e.g., 'unit-tests')", + metavar="NAME" + )] = None, + from_date: Annotated[DateTimeWithTimezone | None, typer.Option( + "--from", + help="Start date/time (ISO 8601 format, e.g., '2026-04-08' or '2026-04-08T00:00:00Z')", + type=parse_datetime_with_timezone, + metavar="DATE" + )] = None, + to_date: Annotated[DateTimeWithTimezone | None, typer.Option( + "--to", + help="End date/time (ISO 8601 format, e.g., '2026-04-14' or '2026-04-14T23:59:59Z')", + type=parse_datetime_with_timezone, + metavar="DATE" + )] = None, + limit: Annotated[int | None, typer.Option( + "--limit", + help="Max results to return (default: 50, max: 500)", + type=intType(min=1, max=500), + metavar="N" + )] = None, + offset: Annotated[int | None, typer.Option( + "--offset", + help="Pagination offset (default: 0)", + type=intType(min=0), + metavar="N" + )] = None, + logs: Annotated[bool, typer.Option( + "--logs", + help="Include full stdout/stderr from failed test case executions" + )] = False, +): + """View detailed test execution results with filters""" + client = SmartTestsClient(app=app) + + # Build query parameters + params = {} + if test_path: + params["test-path"] = test_path + if status: + params["status"] = status.value + if branch: + params["branch"] = branch + if test_suite: + params["test-suite"] = test_suite + if from_date: + params["from"] = str(from_date) + if to_date: + params["to"] = str(to_date) + if logs: + params["include-logs"] = "true" + if limit: + params["limit"] = limit + if offset: + params["offset"] = offset + + try: + res = client.request("get", "view/test-results", params=params) + + if res.status_code == HTTPStatus.NOT_FOUND: + click.secho( + "No test results found. Check your filters and try again.", + fg='yellow', err=True + ) + sys.exit(1) + + res.raise_for_status() + response_json = res.json() + + # Output JSON format + click.echo(json.dumps(response_json, indent=2)) + + except Exception as e: + client.print_exception_and_recover( + e, + "Warning: failed to retrieve test results from server" + ) + sys.exit(1) diff --git a/tests/commands/test_view_flaky_tests.py b/tests/commands/test_view_flaky_tests.py new file mode 100644 index 000000000..210735ca0 --- /dev/null +++ b/tests/commands/test_view_flaky_tests.py @@ -0,0 +1,393 @@ +import json +import os +from unittest import mock + +import responses + +from smart_tests.utils.http_client import get_base_url +from tests.cli_test_case import CliTestCase + + +class ViewFlakyTestsTest(CliTestCase): + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_basic(self): + """Test basic flaky tests query with default parameters""" + mock_json_response = { + "data": { + "weeks": [ + { + "weekDate": "2026-W19", + "calculationStatus": "CALCULATED", + "calculationTime": "2026-05-11T10:30:00Z", + "flakyTests": [ + { + "testPath": [ + {"type": "class", "name": "TestClass"}, + {"type": "testCase", "name": "testMethod"} + ], + "score": 0.45, + "weeklyScoreDelta": 0.12, + "runtimeDuration": 5200, + "weeklyRuntimeDurationDelta": -300 + }, + { + "testPath": [ + {"type": "class", "name": "AnotherTest"}, + {"type": "testCase", "name": "anotherMethod"} + ], + "score": 0.38, + "weeklyScoreDelta": -0.05, + "runtimeDuration": 3400, + "weeklyRuntimeDurationDelta": 200 + } + ], + "flakyTestCount": 2 + } + ] + }, + "metadata": { + "weeksRequested": 1, + "weeksReturned": 1, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "flaky-tests", mix_stderr=False) + self.assert_success(result) + + # Verify JSON output + output_json = json.loads(result.stdout) + self.assertEqual(output_json["data"]["weeks"][0]["weekDate"], "2026-W19") + self.assertEqual(len(output_json["data"]["weeks"][0]["flakyTests"]), 2) + self.assertEqual(output_json["metadata"]["weeksRequested"], 1) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_with_year_week(self): + """Test flaky tests query with specific year-week parameter""" + mock_json_response = { + "data": { + "weeks": [ + { + "weekDate": "2026-W15", + "calculationStatus": "CALCULATED", + "calculationTime": "2026-04-12T10:30:00Z", + "flakyTests": [ + { + "testPath": [ + {"type": "class", "name": "TestClass"}, + {"type": "testCase", "name": "testMethod"} + ], + "score": 0.67, + "weeklyScoreDelta": 0.23, + "runtimeDuration": 8200, + "weeklyRuntimeDurationDelta": 1000 + } + ], + "flakyTestCount": 1 + } + ] + }, + "metadata": { + "weeksRequested": 1, + "weeksReturned": 1, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "flaky-tests", "--year-week", "2026-W15", mix_stderr=False) + self.assert_success(result) + + # Verify the request was made and query parameter was passed + self.assertEqual(len(responses.calls), 1) + self.assertIn("/view/flaky-tests", responses.calls[0].request.url) + self.assertIn("year-week=2026-W15", responses.calls[0].request.url) + + # Verify output + output_json = json.loads(result.stdout) + self.assertEqual(output_json["data"]["weeks"][0]["weekDate"], "2026-W15") + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_with_multiple_weeks(self): + """Test flaky tests query with multiple weeks parameter""" + mock_json_response = { + "data": { + "weeks": [ + { + "weekDate": "2026-W19", + "calculationStatus": "CALCULATED", + "calculationTime": "2026-05-11T10:30:00Z", + "flakyTests": [], + "flakyTestCount": 0 + }, + { + "weekDate": "2026-W18", + "calculationStatus": "CALCULATED", + "calculationTime": "2026-05-04T10:30:00Z", + "flakyTests": [], + "flakyTestCount": 0 + } + ] + }, + "metadata": { + "weeksRequested": 2, + "weeksReturned": 2, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "flaky-tests", "--weeks", "2", mix_stderr=False) + self.assert_success(result) + + # Verify the request was made and query parameter was passed + self.assertGreater(len(responses.calls), 0) + self.assertIn("/view/flaky-tests", responses.calls[0].request.url) + self.assertIn("weeks=2", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_with_date_range(self): + """Test flaky tests query with from/to date parameters""" + mock_json_response = { + "data": { + "weeks": [] + }, + "metadata": { + "weeksRequested": 1, + "weeksReturned": 0, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli( + "view", "flaky-tests", + "--from", "2026-04-08", + "--to", "2026-04-14", + mix_stderr=False + ) + self.assert_success(result) + + # Verify the request was made and query parameters were passed + self.assertGreater(len(responses.calls), 0) + self.assertIn("/view/flaky-tests", responses.calls[0].request.url) + self.assertIn("from=", responses.calls[0].request.url) + self.assertIn("to=", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_with_test_suite(self): + """Test flaky tests query with test-suite filter""" + mock_json_response = { + "data": { + "weeks": [ + { + "weekDate": "2026-W19", + "calculationStatus": "CALCULATED", + "calculationTime": "2026-05-11T10:30:00Z", + "flakyTests": [], + "flakyTestCount": 0 + } + ] + }, + "metadata": { + "weeksRequested": 1, + "weeksReturned": 1, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "flaky-tests", "--test-suite", "unit-tests", mix_stderr=False) + self.assert_success(result) + + # Verify the request was made and query parameter was passed + self.assertGreater(len(responses.calls), 0) + self.assertIn("/view/flaky-tests", responses.calls[0].request.url) + self.assertIn("test-suite=unit-tests", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_with_limit(self): + """Test flaky tests query with limit parameter""" + mock_json_response = { + "data": { + "weeks": [ + { + "weekDate": "2026-W19", + "calculationStatus": "CALCULATED", + "calculationTime": "2026-05-11T10:30:00Z", + "flakyTests": [], + "flakyTestCount": 0 + } + ] + }, + "metadata": { + "weeksRequested": 1, + "weeksReturned": 1, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "flaky-tests", "--limit", "100", mix_stderr=False) + self.assert_success(result) + + # Verify the request was made and query parameter was passed + self.assertGreater(len(responses.calls), 0) + self.assertIn("/view/flaky-tests", responses.calls[0].request.url) + self.assertIn("limit=100", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_not_found(self): + """Test flaky tests query when data is not found""" + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json={}, + status=404, + ) + + result = self.cli("view", "flaky-tests", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("No flaky test data found", result.stderr) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_api_error(self): + """Test flaky tests query when API returns error""" + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + status=500, + ) + + result = self.cli("view", "flaky-tests", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("Error", result.stderr) + + def test_flaky_tests_invalid_year_week(self): + """Test flaky tests query with invalid year-week format""" + result = self.cli("view", "flaky-tests", "--year-week", "2026W15", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("Invalid year-week format", result.stderr) + self.assertIn("Expected format: YYYY-Www", result.stderr) + + def test_flaky_tests_invalid_limit(self): + """Test flaky tests query with out-of-range limit""" + result = self.cli("view", "flaky-tests", "--limit", "600", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("cannot be larger than 500", result.stderr) + + def test_flaky_tests_invalid_weeks(self): + """Test flaky tests query with out-of-range weeks""" + result = self.cli("view", "flaky-tests", "--weeks", "15", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("cannot be larger than 12", result.stderr) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_empty_response(self): + """Test flaky tests query with empty weeks data""" + mock_json_response = { + "data": { + "weeks": [] + }, + "metadata": { + "weeksRequested": 1, + "weeksReturned": 0, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "flaky-tests", mix_stderr=False) + self.assert_success(result) + + # Verify output contains empty weeks array + output_json = json.loads(result.stdout) + self.assertEqual(len(output_json["data"]["weeks"]), 0) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_flaky_tests_not_ready_status(self): + """Test flaky tests query with NOT_READY calculation status""" + mock_json_response = { + "data": { + "weeks": [ + { + "weekDate": "2026-W19", + "calculationStatus": "NOT_READY", + "flakyTests": [], + "flakyTestCount": 0 + } + ] + }, + "metadata": { + "weeksRequested": 1, + "weeksReturned": 1, + "latestWeek": "2026-W19" + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/flaky-tests", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "flaky-tests", mix_stderr=False) + self.assert_success(result) + + # Verify output includes NOT_READY status + output_json = json.loads(result.stdout) + self.assertEqual(output_json["data"]["weeks"][0]["calculationStatus"], "NOT_READY") diff --git a/tests/commands/test_view_test_results.py b/tests/commands/test_view_test_results.py new file mode 100644 index 000000000..096637c4c --- /dev/null +++ b/tests/commands/test_view_test_results.py @@ -0,0 +1,596 @@ +import json +import os +from unittest import mock + +import responses + +from smart_tests.utils.http_client import get_base_url +from tests.cli_test_case import CliTestCase + + +class ViewTestResultsTest(CliTestCase): + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_basic(self): + """Test basic test results query with default parameters""" + mock_json_response = { + "data": { + "results": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "testPath": "com.example.TestClass#testMethod", + "status": "FAILED", + "totalDuration": 5200, + "passed": 0, + "failed": 1, + "skipped": 0, + "session": { + "id": 12345, + "buildId": 67890, + "lineage": "main", + "createdAt": "2026-05-11T10:30:00Z" + }, + "createdAt": "2026-05-11T10:30:00Z" + }, + { + "id": "890a1234-e29b-41d4-a716-446655440001", + "testPath": "com.example.TestClass#testMethod2", + "status": "PASSED", + "totalDuration": 3400, + "passed": 1, + "failed": 0, + "skipped": 0, + "session": { + "id": 12346, + "buildId": 67891, + "lineage": "main", + "createdAt": "2026-05-11T11:30:00Z" + }, + "createdAt": "2026-05-11T11:30:00Z" + } + ] + }, + "metadata": { + "totalCount": 145, + "limit": 50, + "offset": 0, + "hasMore": True, + "nextOffset": 50 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", mix_stderr=False) + self.assert_success(result) + + # Verify JSON output + output_json = json.loads(result.stdout) + self.assertEqual(len(output_json["data"]["results"]), 2) + self.assertEqual(output_json["metadata"]["totalCount"], 145) + self.assertTrue(output_json["metadata"]["hasMore"]) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_test_path(self): + """Test test results query with test-path filter""" + mock_json_response = { + "data": { + "results": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "testPath": "com.example.TestClass#testMethod", + "status": "FLAKE", + "totalDuration": 5200, + "passed": 1, + "failed": 1, + "skipped": 0, + "session": { + "id": 12345, + "buildId": 67890, + "lineage": "main", + "createdAt": "2026-05-11T10:30:00Z" + }, + "createdAt": "2026-05-11T10:30:00Z" + } + ] + }, + "metadata": { + "totalCount": 1, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli( + "view", "test-results", + "--test-path", "com.example.TestClass#testMethod", + mix_stderr=False + ) + self.assert_success(result) + + # Verify the request was made and query parameter was passed + self.assertGreater(len(responses.calls), 0) + self.assertIn("/view/test-results", responses.calls[0].request.url) + self.assertIn("test-path=com.example.TestClass%23testMethod", responses.calls[0].request.url) + + # Verify output + output_json = json.loads(result.stdout) + self.assertEqual(output_json["data"]["results"][0]["testPath"], "com.example.TestClass#testMethod") + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_status_failed(self): + """Test test results query with status=FAILED filter""" + mock_json_response = { + "data": { + "results": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "testPath": "com.example.FailingTest#testMethod", + "status": "FAILED", + "totalDuration": 5200, + "passed": 0, + "failed": 1, + "skipped": 0, + "session": { + "id": 12345, + "buildId": 67890, + "lineage": "main", + "createdAt": "2026-05-11T10:30:00Z" + }, + "createdAt": "2026-05-11T10:30:00Z" + } + ] + }, + "metadata": { + "totalCount": 25, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", "--status", "FAILED", mix_stderr=False) + self.assert_success(result) + + # Verify the query parameter was passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("status=FAILED", responses.calls[0].request.url) + + # Verify output + output_json = json.loads(result.stdout) + self.assertEqual(output_json["data"]["results"][0]["status"], "FAILED") + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_status_passed(self): + """Test test results query with status=PASSED filter""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 0, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", "--status", "PASSED", mix_stderr=False) + self.assert_success(result) + + # Verify the query parameter was passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("status=PASSED", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_branch(self): + """Test test results query with branch filter""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 0, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", "--branch", "develop", mix_stderr=False) + self.assert_success(result) + + # Verify the query parameter was passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("branch=develop", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_test_suite(self): + """Test test results query with test-suite filter""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 0, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", "--test-suite", "integration-tests", mix_stderr=False) + self.assert_success(result) + + # Verify the query parameter was passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("test-suite=integration-tests", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_date_range(self): + """Test test results query with from/to date parameters""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 0, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli( + "view", "test-results", + "--from", "2026-04-01", + "--to", "2026-04-14", + mix_stderr=False + ) + self.assert_success(result) + + # Verify the query parameters were passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("from=", responses.calls[0].request.url) + self.assertIn("to=", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_limit(self): + """Test test results query with limit parameter""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 0, + "limit": 100, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", "--limit", "100", mix_stderr=False) + self.assert_success(result) + + # Verify the query parameter was passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("limit=100", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_logs(self): + """Test test results query with logs parameter""" + mock_json_response = { + "data": { + "results": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "testPath": "com.example.TestClass#testMethod", + "status": "FAILED", + "totalDuration": 5200, + "passed": 0, + "failed": 1, + "skipped": 0, + "logs": [ + { + "stdout": "Test execution started\nAssertion failed", + "stderr": "AssertionError: Expected 5 but got 3", + "status": "FAILURE", + "createdAt": "2026-05-11T10:30:01Z" + } + ], + "session": { + "id": 12345, + "buildId": 67890, + "lineage": "main", + "createdAt": "2026-05-11T10:30:00Z" + }, + "createdAt": "2026-05-11T10:30:00Z" + } + ] + }, + "metadata": { + "totalCount": 1, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", "--logs", mix_stderr=False) + self.assert_success(result) + + # Verify the query parameter was passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("include-logs=true", responses.calls[0].request.url) + + # Verify logs are in output + output_json = json.loads(result.stdout) + self.assertIn("logs", output_json["data"]["results"][0]) + self.assertEqual(len(output_json["data"]["results"][0]["logs"]), 1) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_multiple_filters(self): + """Test test results query with multiple filters combined""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 0, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli( + "view", "test-results", + "--status", "FAILED", + "--branch", "main", + "--limit", "200", + mix_stderr=False + ) + self.assert_success(result) + + # Verify all query parameters were passed + # Check request was made + self.assertGreater(len(responses.calls), 0) + self.assertIn("status=FAILED", responses.calls[0].request.url) + self.assertIn("branch=main", responses.calls[0].request.url) + self.assertIn("limit=200", responses.calls[0].request.url) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_not_found(self): + """Test test results query when data is not found""" + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json={}, + status=404, + ) + + result = self.cli("view", "test-results", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("No test results found", result.stderr) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_api_error(self): + """Test test results query when API returns error""" + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + status=500, + ) + + result = self.cli("view", "test-results", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("Error", result.stderr) + + def test_test_results_invalid_status(self): + """Test test results query with invalid status value""" + result = self.cli("view", "test-results", "--status", "INVALID", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("Invalid status", result.stderr) + self.assertIn("PASSED, FAILED, SKIPPED, FLAKE", result.stderr) + + def test_test_results_invalid_limit(self): + """Test test results query with out-of-range limit""" + result = self.cli("view", "test-results", "--limit", "600", mix_stderr=False) + self.assert_exit_code(result, 1) + self.assertIn("cannot be larger than 500", result.stderr) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_empty_response(self): + """Test test results query with empty results""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 0, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", mix_stderr=False) + self.assert_success(result) + + # Verify output contains empty results array + output_json = json.loads(result.stdout) + self.assertEqual(len(output_json["data"]["results"]), 0) + self.assertEqual(output_json["metadata"]["totalCount"], 0) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_offset(self): + """Test test results query with offset parameter for pagination""" + mock_json_response = { + "data": { + "results": [] + }, + "metadata": { + "totalCount": 100, + "limit": 50, + "offset": 50, + "hasMore": False, + "nextOffset": 100 + } + } + + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json=mock_json_response, + status=200, + ) + + result = self.cli("view", "test-results", "--offset", "50", mix_stderr=False) + self.assert_success(result) + + # Verify the request was made and query parameter was passed + self.assertGreater(len(responses.calls), 0) + self.assertIn("/view/test-results", responses.calls[0].request.url) + self.assertIn("offset=50", responses.calls[0].request.url) + + # Verify output + output_json = json.loads(result.stdout) + self.assertEqual(output_json["metadata"]["offset"], 50) + + @responses.activate + @mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token}) + def test_test_results_with_all_statuses(self): + """Test test results query with all different status types""" + for status in ["PASSED", "FAILED", "SKIPPED", "FLAKE"]: + responses.add( + responses.GET, + f"{get_base_url()}/intake/organizations/{self.organization}/workspaces/{self.workspace}/view/test-results", + json={ + "data": {"results": []}, + "metadata": { + "totalCount": 0, + "limit": 50, + "offset": 0, + "hasMore": False, + "nextOffset": 0 + } + }, + status=200, + ) + + result = self.cli("view", "test-results", "--status", status, mix_stderr=False) + self.assert_success(result) + + # Verify the query parameter was passed + self.assertGreater(len(responses.calls), 0) + self.assertIn(f"status={status}", responses.calls[0].request.url) + + # Reset for next iteration + responses.reset() + responses.mock.start()