xray-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xray_cli/__init__.py +3 -0
- xray_cli/auth.py +88 -0
- xray_cli/cli.py +153 -0
- xray_cli/commands/__init__.py +0 -0
- xray_cli/commands/_shared.py +23 -0
- xray_cli/commands/execution.py +97 -0
- xray_cli/commands/folder.py +39 -0
- xray_cli/commands/precondition.py +30 -0
- xray_cli/commands/results.py +82 -0
- xray_cli/commands/run.py +111 -0
- xray_cli/commands/test.py +80 -0
- xray_cli/commands/test_plan.py +30 -0
- xray_cli/commands/test_set.py +30 -0
- xray_cli/config.py +177 -0
- xray_cli/errors.py +78 -0
- xray_cli/formatter.py +273 -0
- xray_cli/graphql_client.py +97 -0
- xray_cli/log.py +37 -0
- xray_cli/resolver.py +87 -0
- xray_cli/rest_client.py +109 -0
- xray_cli/service.py +619 -0
- xray_cli-0.1.0.dist-info/LICENSE +21 -0
- xray_cli-0.1.0.dist-info/METADATA +260 -0
- xray_cli-0.1.0.dist-info/RECORD +27 -0
- xray_cli-0.1.0.dist-info/WHEEL +5 -0
- xray_cli-0.1.0.dist-info/entry_points.txt +2 -0
- xray_cli-0.1.0.dist-info/top_level.txt +1 -0
xray_cli/__init__.py
ADDED
xray_cli/auth.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Token service for Xray Cloud API authentication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
|
|
7
|
+
from xray_cli.errors import AuthError, TransportError
|
|
8
|
+
from xray_cli import log
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenService:
|
|
12
|
+
"""Acquires and caches Xray API tokens using Client ID and Secret."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, auth_url, client_id, client_secret, timeout=30):
|
|
15
|
+
self._auth_url = auth_url
|
|
16
|
+
self._client_id = client_id
|
|
17
|
+
self._client_secret = client_secret
|
|
18
|
+
self._timeout = timeout
|
|
19
|
+
self._token = None
|
|
20
|
+
|
|
21
|
+
def get_token(self):
|
|
22
|
+
"""Return a cached token, or acquire a new one."""
|
|
23
|
+
if self._token is not None:
|
|
24
|
+
return self._token
|
|
25
|
+
self._token = self._authenticate()
|
|
26
|
+
return self._token
|
|
27
|
+
|
|
28
|
+
def invalidate_token(self):
|
|
29
|
+
"""Clear the cached token so the next call re-authenticates."""
|
|
30
|
+
self._token = None
|
|
31
|
+
|
|
32
|
+
def _authenticate(self):
|
|
33
|
+
"""Exchange Client ID/Secret for an auth token."""
|
|
34
|
+
payload = json.dumps({
|
|
35
|
+
"client_id": self._client_id,
|
|
36
|
+
"client_secret": self._client_secret,
|
|
37
|
+
}).encode("utf-8")
|
|
38
|
+
|
|
39
|
+
req = urllib.request.Request(
|
|
40
|
+
self._auth_url,
|
|
41
|
+
data=payload,
|
|
42
|
+
headers={"Content-Type": "application/json"},
|
|
43
|
+
method="POST",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
log.debug("Authenticating against %s", self._auth_url)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
50
|
+
body = resp.read().decode("utf-8")
|
|
51
|
+
# The Xray auth endpoint returns the token as a bare JSON string
|
|
52
|
+
token = json.loads(body)
|
|
53
|
+
if isinstance(token, str):
|
|
54
|
+
log.debug("Token acquired successfully")
|
|
55
|
+
return token
|
|
56
|
+
# Some responses may wrap it differently
|
|
57
|
+
if isinstance(token, dict) and "token" in token:
|
|
58
|
+
log.debug("Token acquired successfully")
|
|
59
|
+
return token["token"]
|
|
60
|
+
raise AuthError(
|
|
61
|
+
"Unexpected auth response format",
|
|
62
|
+
detail=f"Response body: {body[:200]}",
|
|
63
|
+
)
|
|
64
|
+
except urllib.error.HTTPError as exc:
|
|
65
|
+
status = exc.code
|
|
66
|
+
try:
|
|
67
|
+
detail = exc.read().decode("utf-8", errors="replace")[:500]
|
|
68
|
+
except Exception:
|
|
69
|
+
detail = None
|
|
70
|
+
if status in (401, 403):
|
|
71
|
+
raise AuthError(
|
|
72
|
+
"Authentication failed: invalid Client ID or Client Secret",
|
|
73
|
+
detail=detail,
|
|
74
|
+
)
|
|
75
|
+
raise AuthError(
|
|
76
|
+
f"Authentication failed with HTTP {status}",
|
|
77
|
+
detail=detail,
|
|
78
|
+
)
|
|
79
|
+
except urllib.error.URLError as exc:
|
|
80
|
+
raise TransportError(
|
|
81
|
+
f"Cannot reach auth endpoint: {exc.reason}",
|
|
82
|
+
detail=str(exc),
|
|
83
|
+
)
|
|
84
|
+
except (OSError, ValueError) as exc:
|
|
85
|
+
raise TransportError(
|
|
86
|
+
f"Auth request failed: {exc}",
|
|
87
|
+
detail=str(exc),
|
|
88
|
+
)
|
xray_cli/cli.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""CLI entry point and argparse command tree for xray-cli."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from xray_cli import __version__
|
|
7
|
+
from xray_cli.config import load_config
|
|
8
|
+
from xray_cli.errors import XrayCliError, EXIT_USAGE_ERROR
|
|
9
|
+
from xray_cli import log
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _build_parser():
|
|
13
|
+
"""Build the top-level argument parser with global flags."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="xray",
|
|
16
|
+
description="Command-line interface for Xray Cloud test management.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--config", metavar="PATH", default=None,
|
|
23
|
+
help="Path to config file (default: auto-discover .xray-cli.ini)",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--json", action="store_true", default=False, dest="json_output",
|
|
27
|
+
help="Output in JSON format instead of human-readable text",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--verbose", "-v", action="store_true", default=False,
|
|
31
|
+
help="Enable verbose/debug output",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--quiet", "-q", action="store_true", default=False,
|
|
35
|
+
help="Suppress all non-error output",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--timeout", type=int, default=None, metavar="SECONDS",
|
|
39
|
+
help="Request timeout in seconds (default: from config or 30)",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
subparsers = parser.add_subparsers(dest="command", title="commands")
|
|
43
|
+
|
|
44
|
+
# Register command families
|
|
45
|
+
from xray_cli.commands import execution, test, run, results
|
|
46
|
+
from xray_cli.commands import precondition, test_set, test_plan, folder
|
|
47
|
+
execution.register(subparsers)
|
|
48
|
+
test.register(subparsers)
|
|
49
|
+
run.register(subparsers)
|
|
50
|
+
results.register(subparsers)
|
|
51
|
+
precondition.register(subparsers)
|
|
52
|
+
test_set.register(subparsers)
|
|
53
|
+
test_plan.register(subparsers)
|
|
54
|
+
folder.register(subparsers)
|
|
55
|
+
|
|
56
|
+
return parser
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _collect_cli_overrides(args):
|
|
60
|
+
"""Extract CLI-level overrides to pass into config loading."""
|
|
61
|
+
overrides = {}
|
|
62
|
+
if args.json_output:
|
|
63
|
+
overrides["output"] = "json"
|
|
64
|
+
if args.timeout is not None:
|
|
65
|
+
overrides["timeout_seconds"] = str(args.timeout)
|
|
66
|
+
return overrides
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_GLOBAL_FLAGS = {
|
|
70
|
+
"--json": 0, # 0 = flag takes no argument
|
|
71
|
+
"--verbose": 0,
|
|
72
|
+
"-v": 0,
|
|
73
|
+
"--quiet": 0,
|
|
74
|
+
"-q": 0,
|
|
75
|
+
"--config": 1, # 1 = flag takes one argument
|
|
76
|
+
"--timeout": 1,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _hoist_global_flags(argv):
|
|
81
|
+
"""Move global flags that appear after a subcommand to the front.
|
|
82
|
+
|
|
83
|
+
argparse only recognises top-level flags when they precede the
|
|
84
|
+
subcommand. This helper lets users write them anywhere, e.g.
|
|
85
|
+
``xray test get OTR-1 --json`` instead of ``xray --json test get OTR-1``.
|
|
86
|
+
"""
|
|
87
|
+
hoisted = []
|
|
88
|
+
rest = []
|
|
89
|
+
i = 0
|
|
90
|
+
while i < len(argv):
|
|
91
|
+
token = argv[i]
|
|
92
|
+
if token in _GLOBAL_FLAGS:
|
|
93
|
+
n_args = _GLOBAL_FLAGS[token]
|
|
94
|
+
hoisted.append(token)
|
|
95
|
+
if n_args and i + 1 < len(argv):
|
|
96
|
+
i += 1
|
|
97
|
+
hoisted.append(argv[i])
|
|
98
|
+
else:
|
|
99
|
+
rest.append(token)
|
|
100
|
+
i += 1
|
|
101
|
+
return hoisted + rest
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main(argv=None):
|
|
105
|
+
"""Main entry point for xray-cli."""
|
|
106
|
+
parser = _build_parser()
|
|
107
|
+
raw = argv if argv is not None else sys.argv[1:]
|
|
108
|
+
args = parser.parse_args(_hoist_global_flags(raw))
|
|
109
|
+
|
|
110
|
+
# Configure logging
|
|
111
|
+
log.configure_verbosity(verbose=args.verbose, quiet=args.quiet)
|
|
112
|
+
|
|
113
|
+
# If no command given, print help
|
|
114
|
+
if not args.command:
|
|
115
|
+
parser.print_help(sys.stderr)
|
|
116
|
+
return EXIT_USAGE_ERROR
|
|
117
|
+
|
|
118
|
+
# Load config
|
|
119
|
+
try:
|
|
120
|
+
cli_overrides = _collect_cli_overrides(args)
|
|
121
|
+
config = load_config(
|
|
122
|
+
explicit_path=args.config,
|
|
123
|
+
cli_overrides=cli_overrides,
|
|
124
|
+
)
|
|
125
|
+
log.debug("Config loaded: %s", config)
|
|
126
|
+
except XrayCliError as exc:
|
|
127
|
+
log.error("%s: %s", exc.category, exc)
|
|
128
|
+
if exc.detail:
|
|
129
|
+
log.debug("Detail: %s", exc.detail)
|
|
130
|
+
return exc.exit_code
|
|
131
|
+
|
|
132
|
+
# Dispatch to subcommand handler
|
|
133
|
+
handler = getattr(args, "handler", None)
|
|
134
|
+
if handler is None:
|
|
135
|
+
parser.print_help(sys.stderr)
|
|
136
|
+
return EXIT_USAGE_ERROR
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
return handler(args, config)
|
|
140
|
+
except XrayCliError as exc:
|
|
141
|
+
log.error("%s: %s", exc.category, exc)
|
|
142
|
+
if exc.detail:
|
|
143
|
+
log.debug("Detail: %s", exc.detail)
|
|
144
|
+
return exc.exit_code
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cli():
|
|
148
|
+
"""Console script entry point — calls main() and exits."""
|
|
149
|
+
sys.exit(main() or 0)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
cli()
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Shared helpers for command handlers."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def build_service(config):
|
|
5
|
+
"""Build a fully wired XrayService from config."""
|
|
6
|
+
from xray_cli.auth import TokenService
|
|
7
|
+
from xray_cli.graphql_client import GraphQLClient
|
|
8
|
+
from xray_cli.rest_client import RestClient
|
|
9
|
+
from xray_cli.service import XrayService
|
|
10
|
+
|
|
11
|
+
token_service = TokenService(
|
|
12
|
+
config.auth_url, config.client_id, config.client_secret,
|
|
13
|
+
timeout=config.timeout_seconds,
|
|
14
|
+
)
|
|
15
|
+
gql_client = GraphQLClient(
|
|
16
|
+
config.graphql_url, token_service,
|
|
17
|
+
timeout=config.timeout_seconds,
|
|
18
|
+
)
|
|
19
|
+
rest_client = RestClient(
|
|
20
|
+
config.import_base_url, token_service,
|
|
21
|
+
timeout=config.timeout_seconds,
|
|
22
|
+
)
|
|
23
|
+
return XrayService(gql_client, rest_client=rest_client)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Command handlers for execution operations."""
|
|
2
|
+
|
|
3
|
+
from xray_cli import formatter
|
|
4
|
+
from xray_cli.errors import ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(subparsers):
|
|
8
|
+
"""Register execution subcommands."""
|
|
9
|
+
exec_parser = subparsers.add_parser("execution", help="Test execution operations")
|
|
10
|
+
exec_sub = exec_parser.add_subparsers(dest="execution_command", title="execution commands")
|
|
11
|
+
|
|
12
|
+
# execution list
|
|
13
|
+
list_parser = exec_sub.add_parser("list", help="List test executions")
|
|
14
|
+
list_parser.add_argument(
|
|
15
|
+
"--jql", default=None,
|
|
16
|
+
help="JQL filter for executions",
|
|
17
|
+
)
|
|
18
|
+
list_parser.add_argument(
|
|
19
|
+
"--limit", type=int, default=None,
|
|
20
|
+
help="Maximum results per page (1-100, default: from config)",
|
|
21
|
+
)
|
|
22
|
+
list_parser.add_argument(
|
|
23
|
+
"--start", type=int, default=0,
|
|
24
|
+
help="Start offset for pagination (default: 0)",
|
|
25
|
+
)
|
|
26
|
+
list_parser.set_defaults(handler=handle_execution_list)
|
|
27
|
+
|
|
28
|
+
# execution create
|
|
29
|
+
create_parser = exec_sub.add_parser("create", help="Create a test execution")
|
|
30
|
+
create_parser.add_argument(
|
|
31
|
+
"--project", required=True, dest="project_key",
|
|
32
|
+
help="Jira project key (e.g. XSP)",
|
|
33
|
+
)
|
|
34
|
+
create_parser.add_argument(
|
|
35
|
+
"--summary", required=True,
|
|
36
|
+
help="Summary for the new test execution",
|
|
37
|
+
)
|
|
38
|
+
create_parser.add_argument(
|
|
39
|
+
"--test", dest="test_keys", action="append", default=None,
|
|
40
|
+
help="Test issue key to include (repeatable)",
|
|
41
|
+
)
|
|
42
|
+
create_parser.set_defaults(handler=handle_execution_create)
|
|
43
|
+
|
|
44
|
+
# Set a fallback handler for bare "execution" with no subcommand
|
|
45
|
+
exec_parser.set_defaults(handler=lambda args, config: _show_help(exec_parser))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _show_help(parser):
|
|
49
|
+
parser.print_help()
|
|
50
|
+
return 2
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _build_service(config):
|
|
54
|
+
from xray_cli.auth import TokenService
|
|
55
|
+
from xray_cli.graphql_client import GraphQLClient
|
|
56
|
+
from xray_cli.service import XrayService
|
|
57
|
+
|
|
58
|
+
token_service = TokenService(
|
|
59
|
+
config.auth_url, config.client_id, config.client_secret,
|
|
60
|
+
timeout=config.timeout_seconds,
|
|
61
|
+
)
|
|
62
|
+
gql_client = GraphQLClient(
|
|
63
|
+
config.graphql_url, token_service,
|
|
64
|
+
timeout=config.timeout_seconds,
|
|
65
|
+
)
|
|
66
|
+
return XrayService(gql_client)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def handle_execution_list(args, config):
|
|
70
|
+
"""Handle 'execution list' command."""
|
|
71
|
+
limit = args.limit if args.limit is not None else config.page_limit
|
|
72
|
+
if limit < 1 or limit > 100:
|
|
73
|
+
raise ValidationError("--limit must be between 1 and 100")
|
|
74
|
+
|
|
75
|
+
service = _build_service(config)
|
|
76
|
+
result = service.list_executions(
|
|
77
|
+
jql=args.jql,
|
|
78
|
+
limit=limit,
|
|
79
|
+
start=args.start,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
output_mode = "json" if args.json_output else config.output
|
|
83
|
+
formatter.render(result, mode=output_mode)
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def handle_execution_create(args, config):
|
|
88
|
+
"""Handle 'execution create' command."""
|
|
89
|
+
service = _build_service(config)
|
|
90
|
+
result = service.create_execution(
|
|
91
|
+
project_key=args.project_key,
|
|
92
|
+
summary=args.summary,
|
|
93
|
+
test_keys=args.test_keys,
|
|
94
|
+
)
|
|
95
|
+
output_mode = "json" if args.json_output else config.output
|
|
96
|
+
formatter.render(result, mode=output_mode)
|
|
97
|
+
return 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Command handlers for folder operations."""
|
|
2
|
+
|
|
3
|
+
from xray_cli import formatter
|
|
4
|
+
from xray_cli.errors import ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(subparsers):
|
|
8
|
+
"""Register folder subcommands."""
|
|
9
|
+
parser = subparsers.add_parser("folder", help="Folder operations")
|
|
10
|
+
sub = parser.add_subparsers(dest="folder_command", title="folder commands")
|
|
11
|
+
|
|
12
|
+
list_parser = sub.add_parser("list", help="List folders")
|
|
13
|
+
list_parser.add_argument(
|
|
14
|
+
"--project", required=True, dest="project_key",
|
|
15
|
+
help="Jira project key (required)",
|
|
16
|
+
)
|
|
17
|
+
list_parser.add_argument("--path", default="/", help="Folder path (default: root /)")
|
|
18
|
+
list_parser.add_argument("--limit", type=int, default=None, help="Page size (1-100)")
|
|
19
|
+
list_parser.add_argument("--start", type=int, default=0, help="Pagination offset")
|
|
20
|
+
list_parser.set_defaults(handler=handle_list)
|
|
21
|
+
|
|
22
|
+
parser.set_defaults(handler=lambda args, config: (parser.print_help(), 2)[1])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def handle_list(args, config):
|
|
26
|
+
from xray_cli.commands._shared import build_service
|
|
27
|
+
limit = args.limit if args.limit is not None else config.page_limit
|
|
28
|
+
if limit < 1 or limit > 100:
|
|
29
|
+
raise ValidationError("--limit must be between 1 and 100")
|
|
30
|
+
service = build_service(config)
|
|
31
|
+
result = service.list_folders(
|
|
32
|
+
project_key=args.project_key,
|
|
33
|
+
path=args.path,
|
|
34
|
+
limit=limit,
|
|
35
|
+
start=args.start,
|
|
36
|
+
)
|
|
37
|
+
output_mode = "json" if args.json_output else config.output
|
|
38
|
+
formatter.render(result, mode=output_mode)
|
|
39
|
+
return 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Command handlers for precondition operations."""
|
|
2
|
+
|
|
3
|
+
from xray_cli import formatter
|
|
4
|
+
from xray_cli.errors import ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(subparsers):
|
|
8
|
+
"""Register precondition subcommands."""
|
|
9
|
+
parser = subparsers.add_parser("precondition", help="Precondition operations")
|
|
10
|
+
sub = parser.add_subparsers(dest="precondition_command", title="precondition commands")
|
|
11
|
+
|
|
12
|
+
list_parser = sub.add_parser("list", help="List preconditions")
|
|
13
|
+
list_parser.add_argument("--jql", default=None, help="JQL filter")
|
|
14
|
+
list_parser.add_argument("--limit", type=int, default=None, help="Page size (1-100)")
|
|
15
|
+
list_parser.add_argument("--start", type=int, default=0, help="Pagination offset")
|
|
16
|
+
list_parser.set_defaults(handler=handle_list)
|
|
17
|
+
|
|
18
|
+
parser.set_defaults(handler=lambda args, config: (parser.print_help(), 2)[1])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handle_list(args, config):
|
|
22
|
+
from xray_cli.commands._shared import build_service
|
|
23
|
+
limit = args.limit if args.limit is not None else config.page_limit
|
|
24
|
+
if limit < 1 or limit > 100:
|
|
25
|
+
raise ValidationError("--limit must be between 1 and 100")
|
|
26
|
+
service = build_service(config)
|
|
27
|
+
result = service.list_preconditions(jql=args.jql, limit=limit, start=args.start)
|
|
28
|
+
output_mode = "json" if args.json_output else config.output
|
|
29
|
+
formatter.render(result, mode=output_mode)
|
|
30
|
+
return 0
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Command handlers for results import operations."""
|
|
2
|
+
|
|
3
|
+
from xray_cli import formatter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register(subparsers):
|
|
7
|
+
"""Register results subcommands."""
|
|
8
|
+
results_parser = subparsers.add_parser("results", help="Test results operations")
|
|
9
|
+
results_sub = results_parser.add_subparsers(dest="results_command", title="results commands")
|
|
10
|
+
|
|
11
|
+
# results import
|
|
12
|
+
import_parser = results_sub.add_parser("import", help="Import test results")
|
|
13
|
+
import_sub = import_parser.add_subparsers(dest="import_format", title="import formats")
|
|
14
|
+
|
|
15
|
+
# results import xray-json
|
|
16
|
+
xray_json_parser = import_sub.add_parser("xray-json", help="Import Xray JSON results")
|
|
17
|
+
xray_json_parser.add_argument("file", help="Path to Xray JSON results file")
|
|
18
|
+
xray_json_parser.add_argument("--project", dest="project_key", default=None, help="Project key")
|
|
19
|
+
xray_json_parser.add_argument("--execution", dest="execution_key", default=None, help="Existing execution key")
|
|
20
|
+
xray_json_parser.set_defaults(handler=handle_import_xray_json)
|
|
21
|
+
|
|
22
|
+
# results import junit
|
|
23
|
+
junit_parser = import_sub.add_parser("junit", help="Import JUnit XML results")
|
|
24
|
+
junit_parser.add_argument("file", help="Path to JUnit XML results file")
|
|
25
|
+
junit_parser.add_argument("--project", dest="project_key", default=None, help="Project key")
|
|
26
|
+
junit_parser.add_argument("--execution", dest="execution_key", default=None, help="Existing execution key")
|
|
27
|
+
junit_parser.set_defaults(handler=handle_import_junit)
|
|
28
|
+
|
|
29
|
+
import_parser.set_defaults(handler=lambda args, config: _show_help(import_parser))
|
|
30
|
+
results_parser.set_defaults(handler=lambda args, config: _show_help(results_parser))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _show_help(parser):
|
|
34
|
+
parser.print_help()
|
|
35
|
+
return 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_service(config):
|
|
39
|
+
from xray_cli.auth import TokenService
|
|
40
|
+
from xray_cli.graphql_client import GraphQLClient
|
|
41
|
+
from xray_cli.rest_client import RestClient
|
|
42
|
+
from xray_cli.service import XrayService
|
|
43
|
+
|
|
44
|
+
token_service = TokenService(
|
|
45
|
+
config.auth_url, config.client_id, config.client_secret,
|
|
46
|
+
timeout=config.timeout_seconds,
|
|
47
|
+
)
|
|
48
|
+
gql_client = GraphQLClient(
|
|
49
|
+
config.graphql_url, token_service,
|
|
50
|
+
timeout=config.timeout_seconds,
|
|
51
|
+
)
|
|
52
|
+
rest_client = RestClient(
|
|
53
|
+
config.import_base_url, token_service,
|
|
54
|
+
timeout=config.timeout_seconds,
|
|
55
|
+
)
|
|
56
|
+
return XrayService(gql_client, rest_client=rest_client)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def handle_import_xray_json(args, config):
|
|
60
|
+
"""Handle 'results import xray-json <file>' command."""
|
|
61
|
+
service = _build_service(config)
|
|
62
|
+
result = service.import_xray_json(
|
|
63
|
+
args.file,
|
|
64
|
+
project_key=args.project_key or config.project_key,
|
|
65
|
+
execution_key=args.execution_key,
|
|
66
|
+
)
|
|
67
|
+
output_mode = "json" if args.json_output else config.output
|
|
68
|
+
formatter.render(result, mode=output_mode)
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def handle_import_junit(args, config):
|
|
73
|
+
"""Handle 'results import junit <file>' command."""
|
|
74
|
+
service = _build_service(config)
|
|
75
|
+
result = service.import_junit_xml(
|
|
76
|
+
args.file,
|
|
77
|
+
project_key=args.project_key or config.project_key,
|
|
78
|
+
execution_key=args.execution_key,
|
|
79
|
+
)
|
|
80
|
+
output_mode = "json" if args.json_output else config.output
|
|
81
|
+
formatter.render(result, mode=output_mode)
|
|
82
|
+
return 0
|
xray_cli/commands/run.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Command handlers for run operations."""
|
|
2
|
+
|
|
3
|
+
from xray_cli import formatter
|
|
4
|
+
from xray_cli.errors import ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(subparsers):
|
|
8
|
+
"""Register run subcommands."""
|
|
9
|
+
run_parser = subparsers.add_parser("run", help="Test run operations")
|
|
10
|
+
run_sub = run_parser.add_subparsers(dest="run_command", title="run commands")
|
|
11
|
+
|
|
12
|
+
# run list
|
|
13
|
+
list_parser = run_sub.add_parser("list", help="List test runs")
|
|
14
|
+
list_parser.add_argument(
|
|
15
|
+
"--test", dest="test_keys", action="append", default=None,
|
|
16
|
+
help="Filter by test issue key (repeatable)",
|
|
17
|
+
)
|
|
18
|
+
list_parser.add_argument(
|
|
19
|
+
"--execution", dest="execution_keys", action="append", default=None,
|
|
20
|
+
help="Filter by execution issue key (repeatable)",
|
|
21
|
+
)
|
|
22
|
+
list_parser.add_argument(
|
|
23
|
+
"--status", default=None,
|
|
24
|
+
help="Filter by run status name",
|
|
25
|
+
)
|
|
26
|
+
list_parser.add_argument(
|
|
27
|
+
"--failed-only", action="store_true", default=False,
|
|
28
|
+
help="Show only failed/aborted runs",
|
|
29
|
+
)
|
|
30
|
+
list_parser.add_argument(
|
|
31
|
+
"--limit", type=int, default=None,
|
|
32
|
+
help="Maximum results per page (1-100, default: from config)",
|
|
33
|
+
)
|
|
34
|
+
list_parser.add_argument(
|
|
35
|
+
"--start", type=int, default=0,
|
|
36
|
+
help="Start offset for pagination (default: 0)",
|
|
37
|
+
)
|
|
38
|
+
list_parser.set_defaults(handler=handle_run_list)
|
|
39
|
+
|
|
40
|
+
# run update-status
|
|
41
|
+
update_parser = run_sub.add_parser("update-status", help="Update a test run status")
|
|
42
|
+
update_parser.add_argument("run_id", help="Test run ID")
|
|
43
|
+
update_parser.add_argument("status", help="New status (e.g. PASS, FAIL, TODO)")
|
|
44
|
+
update_parser.set_defaults(handler=handle_run_update_status)
|
|
45
|
+
|
|
46
|
+
# run add-evidence
|
|
47
|
+
evidence_parser = run_sub.add_parser("add-evidence", help="Add evidence to a test run")
|
|
48
|
+
evidence_parser.add_argument("run_id", help="Test run ID")
|
|
49
|
+
evidence_parser.add_argument("file", help="Path to evidence file")
|
|
50
|
+
evidence_parser.set_defaults(handler=handle_run_add_evidence)
|
|
51
|
+
|
|
52
|
+
run_parser.set_defaults(handler=lambda args, config: _show_help(run_parser))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _show_help(parser):
|
|
56
|
+
parser.print_help()
|
|
57
|
+
return 2
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_service(config):
|
|
61
|
+
from xray_cli.auth import TokenService
|
|
62
|
+
from xray_cli.graphql_client import GraphQLClient
|
|
63
|
+
from xray_cli.service import XrayService
|
|
64
|
+
|
|
65
|
+
token_service = TokenService(
|
|
66
|
+
config.auth_url, config.client_id, config.client_secret,
|
|
67
|
+
timeout=config.timeout_seconds,
|
|
68
|
+
)
|
|
69
|
+
gql_client = GraphQLClient(
|
|
70
|
+
config.graphql_url, token_service,
|
|
71
|
+
timeout=config.timeout_seconds,
|
|
72
|
+
)
|
|
73
|
+
return XrayService(gql_client)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_run_list(args, config):
|
|
77
|
+
"""Handle 'run list' command."""
|
|
78
|
+
limit = args.limit if args.limit is not None else config.page_limit
|
|
79
|
+
if limit < 1 or limit > 100:
|
|
80
|
+
raise ValidationError("--limit must be between 1 and 100")
|
|
81
|
+
|
|
82
|
+
service = _build_service(config)
|
|
83
|
+
result = service.list_runs(
|
|
84
|
+
test_keys=args.test_keys,
|
|
85
|
+
execution_keys=args.execution_keys,
|
|
86
|
+
status=args.status,
|
|
87
|
+
failed_only=args.failed_only,
|
|
88
|
+
limit=limit,
|
|
89
|
+
start=args.start,
|
|
90
|
+
)
|
|
91
|
+
output_mode = "json" if args.json_output else config.output
|
|
92
|
+
formatter.render(result, mode=output_mode)
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def handle_run_update_status(args, config):
|
|
97
|
+
"""Handle 'run update-status <run-id> <status>' command."""
|
|
98
|
+
service = _build_service(config)
|
|
99
|
+
result = service.update_run_status(args.run_id, args.status)
|
|
100
|
+
output_mode = "json" if args.json_output else config.output
|
|
101
|
+
formatter.render(result, mode=output_mode)
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def handle_run_add_evidence(args, config):
|
|
106
|
+
"""Handle 'run add-evidence <run-id> <file>' command."""
|
|
107
|
+
service = _build_service(config)
|
|
108
|
+
result = service.add_run_evidence(args.run_id, args.file)
|
|
109
|
+
output_mode = "json" if args.json_output else config.output
|
|
110
|
+
formatter.render(result, mode=output_mode)
|
|
111
|
+
return 0
|