charisma-cli 0.2.2__tar.gz → 0.3.1__tar.gz
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.
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/.gitignore +4 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/PKG-INFO +23 -7
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/README.md +22 -6
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/pyproject.toml +1 -1
- charisma_cli-0.3.1/src/charisma_cli/__init__.py +7 -0
- charisma_cli-0.3.1/src/charisma_cli/config.py +149 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/main.py +130 -17
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/models.py +55 -18
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/uploader.py +327 -70
- charisma_cli-0.3.1/tests/test_cli.py +186 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_config.py +177 -0
- charisma_cli-0.3.1/tests/test_open_launch_payload.py +152 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_uploader.py +557 -32
- charisma_cli-0.2.2/src/charisma_cli/__init__.py +0 -3
- charisma_cli-0.2.2/src/charisma_cli/config.py +0 -86
- charisma_cli-0.2.2/tests/test_cli.py +0 -33
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/launch_url.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/parser.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/retry.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/subprocess_mgr.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/src/charisma_cli/watcher.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/__init__.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/conftest.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_integration.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_launch_url.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_models.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_parser.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_retry.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_silent_mode.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_subprocess_mgr.py +0 -0
- {charisma_cli-0.2.2 → charisma_cli-0.3.1}/tests/test_watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: charisma-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: CLI tool that watches allure-results and streams test results + attachments to Charisma.
|
|
5
5
|
Author: Charisma Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -75,15 +75,31 @@ charismactl watch --results allure-results -- uv run pytest -n 4 tests/
|
|
|
75
75
|
| `CHARISMA_TOKEN` | Yes | Service or CI bearer token |
|
|
76
76
|
| `CHARISMA_PROJECT_ID` | Yes | Project alias for result routing |
|
|
77
77
|
| `CHARISMA_RESULTS` | No | Path to allure-results (default: `allure-results`) |
|
|
78
|
+
| `CHARISMA_RUN_KEY` | No | Run identity / merge key (`--run-key` fallback) |
|
|
79
|
+
| `BUILD_ID` | No | CI build identifier (`--build-id` fallback; `CHARISMA_BUILD_ID` also honored) |
|
|
80
|
+
| `BUILD_PLAN_KEY` | No | Build plan key (`--build-plan-key` fallback; `CHARISMA_BUILD_PLAN_KEY` also honored) |
|
|
81
|
+
| `ENVIRONMENT` | No | Environment code (`--environment` fallback; `CHARISMA_ENVIRONMENT` also honored) |
|
|
82
|
+
| `COMPONENT` | No | Component alias (`--component` fallback; `CHARISMA_COMPONENT` also honored) |
|
|
83
|
+
| `SOURCE_URL` | No | CI/CD build link (`--source-url` fallback; `CHARISMA_SOURCE_URL` also honored) |
|
|
78
84
|
|
|
79
85
|
## Flags
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
| `--
|
|
86
|
-
| `--
|
|
87
|
+
Both `upload` and `watch` accept the identical option set.
|
|
88
|
+
|
|
89
|
+
| Flag | Description |
|
|
90
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
91
|
+
| `--endpoint URL` | Charisma API endpoint (overrides `CHARISMA_ENDPOINT`) |
|
|
92
|
+
| `--token TOKEN` | Bearer token (overrides `CHARISMA_TOKEN`) |
|
|
93
|
+
| `--results PATH` | Allure results directory (overrides env var) |
|
|
94
|
+
| `--project ID` | Project alias (overrides env var) |
|
|
95
|
+
| `--silent` | Don't fail pipeline if upload errors occur |
|
|
96
|
+
| `--skip-too-big` | Skip result files larger than 2MB |
|
|
97
|
+
| `--component ALIAS` | Component alias within the project |
|
|
98
|
+
| `--source-url URL` | CI/CD build link for this run |
|
|
99
|
+
| `--build-plan-key K` | Explicit build plan key for resolution (forwarded as `buildPlanKey`) |
|
|
100
|
+
| `--build-id ID` | CI build identifier for this run (forwarded as `buildId`) |
|
|
101
|
+
| `--environment ENV` | Environment code (e.g. `qat`, `prod`), stored on the launch |
|
|
102
|
+
| `--run-key KEY` | Run identity / merge key (reruns + parallel workers share it) |
|
|
87
103
|
|
|
88
104
|
## Launch URL in CI
|
|
89
105
|
|
|
@@ -50,15 +50,31 @@ charismactl watch --results allure-results -- uv run pytest -n 4 tests/
|
|
|
50
50
|
| `CHARISMA_TOKEN` | Yes | Service or CI bearer token |
|
|
51
51
|
| `CHARISMA_PROJECT_ID` | Yes | Project alias for result routing |
|
|
52
52
|
| `CHARISMA_RESULTS` | No | Path to allure-results (default: `allure-results`) |
|
|
53
|
+
| `CHARISMA_RUN_KEY` | No | Run identity / merge key (`--run-key` fallback) |
|
|
54
|
+
| `BUILD_ID` | No | CI build identifier (`--build-id` fallback; `CHARISMA_BUILD_ID` also honored) |
|
|
55
|
+
| `BUILD_PLAN_KEY` | No | Build plan key (`--build-plan-key` fallback; `CHARISMA_BUILD_PLAN_KEY` also honored) |
|
|
56
|
+
| `ENVIRONMENT` | No | Environment code (`--environment` fallback; `CHARISMA_ENVIRONMENT` also honored) |
|
|
57
|
+
| `COMPONENT` | No | Component alias (`--component` fallback; `CHARISMA_COMPONENT` also honored) |
|
|
58
|
+
| `SOURCE_URL` | No | CI/CD build link (`--source-url` fallback; `CHARISMA_SOURCE_URL` also honored) |
|
|
53
59
|
|
|
54
60
|
## Flags
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
| `--
|
|
61
|
-
| `--
|
|
62
|
+
Both `upload` and `watch` accept the identical option set.
|
|
63
|
+
|
|
64
|
+
| Flag | Description |
|
|
65
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
66
|
+
| `--endpoint URL` | Charisma API endpoint (overrides `CHARISMA_ENDPOINT`) |
|
|
67
|
+
| `--token TOKEN` | Bearer token (overrides `CHARISMA_TOKEN`) |
|
|
68
|
+
| `--results PATH` | Allure results directory (overrides env var) |
|
|
69
|
+
| `--project ID` | Project alias (overrides env var) |
|
|
70
|
+
| `--silent` | Don't fail pipeline if upload errors occur |
|
|
71
|
+
| `--skip-too-big` | Skip result files larger than 2MB |
|
|
72
|
+
| `--component ALIAS` | Component alias within the project |
|
|
73
|
+
| `--source-url URL` | CI/CD build link for this run |
|
|
74
|
+
| `--build-plan-key K` | Explicit build plan key for resolution (forwarded as `buildPlanKey`) |
|
|
75
|
+
| `--build-id ID` | CI build identifier for this run (forwarded as `buildId`) |
|
|
76
|
+
| `--environment ENV` | Environment code (e.g. `qat`, `prod`), stored on the launch |
|
|
77
|
+
| `--run-key KEY` | Run identity / merge key (reruns + parallel workers share it) |
|
|
62
78
|
|
|
63
79
|
## Launch URL in CI
|
|
64
80
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""charisma-cli: Stream allure results to Charisma in real time."""
|
|
2
|
+
|
|
3
|
+
# Single source of truth for the CLI version. At build time the wheel metadata
|
|
4
|
+
# is generated from pyproject.toml [project].version, so keeping this literal
|
|
5
|
+
# equal to pyproject guarantees `--version` reports the published version. The
|
|
6
|
+
# prepare-commit step owns the PATCH bump — do not hand-bump here.
|
|
7
|
+
__version__ = "0.3.1"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Configuration dataclass with env var + CLI flag resolution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _resolve_env(bare_name: str) -> str | None:
|
|
10
|
+
"""Resolve an env var by its bare name, falling back to the CHARISMA_ prefix.
|
|
11
|
+
|
|
12
|
+
Bare-name-first (e.g. ``BUILD_ID``) keeps parity with CI systems that export
|
|
13
|
+
generic build metadata, while the ``CHARISMA_`` prefixed form (e.g.
|
|
14
|
+
``CHARISMA_BUILD_ID``) is honored as a namespaced fallback. Returns the first
|
|
15
|
+
non-empty value, or None.
|
|
16
|
+
"""
|
|
17
|
+
for name in (bare_name, f"CHARISMA_{bare_name}"):
|
|
18
|
+
value = os.getenv(name)
|
|
19
|
+
if value and value.strip():
|
|
20
|
+
return value.strip()
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Config:
|
|
26
|
+
"""Resolved configuration for charismactl.
|
|
27
|
+
|
|
28
|
+
Fields are populated via from_env() which merges CLI flag overrides
|
|
29
|
+
with environment variable fallbacks.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
endpoint: str
|
|
33
|
+
token: str
|
|
34
|
+
project: str
|
|
35
|
+
results_dir: str
|
|
36
|
+
silent: bool
|
|
37
|
+
skip_too_big: bool
|
|
38
|
+
drain_timeout: int
|
|
39
|
+
build_id: str | None
|
|
40
|
+
commit_sha: str | None
|
|
41
|
+
branch: str | None
|
|
42
|
+
component: str | None = None
|
|
43
|
+
source_url: str | None = None
|
|
44
|
+
build_plan_key: str | None = None
|
|
45
|
+
environment: str | None = None
|
|
46
|
+
name: str | None = None
|
|
47
|
+
tags: list[str] | None = None
|
|
48
|
+
run_key: str | None = None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_env(
|
|
52
|
+
cls,
|
|
53
|
+
*,
|
|
54
|
+
endpoint: str | None = None,
|
|
55
|
+
token: str | None = None,
|
|
56
|
+
project: str | None = None,
|
|
57
|
+
results: str | None = None,
|
|
58
|
+
silent: bool = False,
|
|
59
|
+
skip_too_big: bool = False,
|
|
60
|
+
drain_timeout: int = 30,
|
|
61
|
+
component: str | None = None,
|
|
62
|
+
source_url: str | None = None,
|
|
63
|
+
build_plan_key: str | None = None,
|
|
64
|
+
build_id: str | None = None,
|
|
65
|
+
environment: str | None = None,
|
|
66
|
+
run_key: str | None = None,
|
|
67
|
+
) -> "Config":
|
|
68
|
+
"""Build Config by resolving CLI flags over environment variables.
|
|
69
|
+
|
|
70
|
+
Resolution order: CLI flag (if not None) > env var > default.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
endpoint: --endpoint flag override
|
|
74
|
+
token: --token flag override
|
|
75
|
+
project: --project flag override
|
|
76
|
+
results: --results flag override
|
|
77
|
+
silent: --silent flag
|
|
78
|
+
skip_too_big: --skip-too-big flag
|
|
79
|
+
drain_timeout: drain timeout in seconds
|
|
80
|
+
component: --component flag override (component alias within the
|
|
81
|
+
project). Env: COMPONENT, fallback CHARISMA_COMPONENT.
|
|
82
|
+
source_url: --source-url flag override (CI/CD build link). Env:
|
|
83
|
+
SOURCE_URL, fallback CHARISMA_SOURCE_URL.
|
|
84
|
+
build_plan_key: --build-plan-key flag override (explicit build plan
|
|
85
|
+
key). Env: BUILD_PLAN_KEY, fallback CHARISMA_BUILD_PLAN_KEY.
|
|
86
|
+
build_id: --build-id flag override (CI build identifier). Env:
|
|
87
|
+
BUILD_ID, fallback CHARISMA_BUILD_ID. Flag wins over env.
|
|
88
|
+
environment: --environment flag override (env code, e.g. "qat").
|
|
89
|
+
Persisted to the first-class launch environment column. Env:
|
|
90
|
+
ENVIRONMENT, fallback CHARISMA_ENVIRONMENT.
|
|
91
|
+
run_key: --run-key flag override. Client-supplied merge key: the
|
|
92
|
+
backend derives the launch id as uuid5(project/run_key) and
|
|
93
|
+
get-or-creates, so reruns and parallel producers sharing a key
|
|
94
|
+
land in one launch. Empty/whitespace resolves to None (a new
|
|
95
|
+
random launch, unchanged behavior).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Fully resolved Config instance.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
click.UsageError: If required fields (endpoint, token) are missing.
|
|
102
|
+
"""
|
|
103
|
+
# Connection config resolves from the CHARISMA_-prefixed name ONLY (no
|
|
104
|
+
# bare form): a generic ENDPOINT/TOKEN/PROJECT_ID/RUN_KEY in the
|
|
105
|
+
# environment must not silently reconfigure the client. Build/run
|
|
106
|
+
# metadata below uses _resolve_env (bare-first, prefixed fallback) so it
|
|
107
|
+
# picks up generic CI variables. Do NOT "unify" these — the split is
|
|
108
|
+
# intentional and mirrors pytest-charisma (bare_env_var=None for config).
|
|
109
|
+
resolved_endpoint = (endpoint or os.getenv("CHARISMA_ENDPOINT", "")).strip()
|
|
110
|
+
resolved_token = (token or os.getenv("CHARISMA_TOKEN", "")).strip()
|
|
111
|
+
resolved_project = (project or os.getenv("CHARISMA_PROJECT_ID", "")).strip()
|
|
112
|
+
resolved_results = results or os.getenv("CHARISMA_RESULTS", "allure-results")
|
|
113
|
+
resolved_run_key = (run_key or os.getenv("CHARISMA_RUN_KEY", "")).strip() or None
|
|
114
|
+
resolved_component = (component or _resolve_env("COMPONENT") or "").strip() or None
|
|
115
|
+
resolved_source_url = (source_url or _resolve_env("SOURCE_URL") or "").strip() or None
|
|
116
|
+
resolved_build_plan_key = (build_plan_key or _resolve_env("BUILD_PLAN_KEY") or "").strip() or None
|
|
117
|
+
resolved_build_id = (build_id or _resolve_env("BUILD_ID") or "").strip() or None
|
|
118
|
+
resolved_environment = (environment or _resolve_env("ENVIRONMENT") or "").strip() or None
|
|
119
|
+
# Human-readable launch name (LAUNCH_NAME) and comma-separated tags
|
|
120
|
+
# (TAGS, e.g. "CI/CD,performance") shown on the Launches page.
|
|
121
|
+
resolved_name = (_resolve_env("LAUNCH_NAME") or "").strip() or None
|
|
122
|
+
resolved_tags = [t.strip() for t in (_resolve_env("TAGS") or "").split(",") if t.strip()] or None
|
|
123
|
+
|
|
124
|
+
if not resolved_endpoint:
|
|
125
|
+
raise click.UsageError(
|
|
126
|
+
"Missing required configuration: endpoint. Set CHARISMA_ENDPOINT or pass --endpoint."
|
|
127
|
+
)
|
|
128
|
+
if not resolved_token:
|
|
129
|
+
raise click.UsageError("Missing required configuration: token. Set CHARISMA_TOKEN or pass --token.")
|
|
130
|
+
|
|
131
|
+
return cls(
|
|
132
|
+
endpoint=resolved_endpoint,
|
|
133
|
+
token=resolved_token,
|
|
134
|
+
project=resolved_project,
|
|
135
|
+
results_dir=resolved_results,
|
|
136
|
+
silent=silent,
|
|
137
|
+
skip_too_big=skip_too_big,
|
|
138
|
+
drain_timeout=drain_timeout,
|
|
139
|
+
build_id=resolved_build_id,
|
|
140
|
+
commit_sha=_resolve_env("COMMIT_SHA"),
|
|
141
|
+
branch=_resolve_env("BRANCH"),
|
|
142
|
+
component=resolved_component,
|
|
143
|
+
source_url=resolved_source_url,
|
|
144
|
+
build_plan_key=resolved_build_plan_key,
|
|
145
|
+
environment=resolved_environment,
|
|
146
|
+
name=resolved_name,
|
|
147
|
+
tags=resolved_tags,
|
|
148
|
+
run_key=resolved_run_key,
|
|
149
|
+
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import signal
|
|
4
4
|
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from queue import PriorityQueue
|
|
7
8
|
|
|
@@ -39,6 +40,12 @@ def _resolve_config(
|
|
|
39
40
|
results: str,
|
|
40
41
|
silent: bool,
|
|
41
42
|
skip_too_big: bool,
|
|
43
|
+
component: str | None = None,
|
|
44
|
+
source_url: str | None = None,
|
|
45
|
+
build_plan_key: str | None = None,
|
|
46
|
+
build_id: str | None = None,
|
|
47
|
+
environment: str | None = None,
|
|
48
|
+
run_key: str | None = None,
|
|
42
49
|
) -> Config:
|
|
43
50
|
"""Resolve CLI config from flags + env vars. Re-raises UsageError on failure."""
|
|
44
51
|
return Config.from_env(
|
|
@@ -48,6 +55,12 @@ def _resolve_config(
|
|
|
48
55
|
results=results,
|
|
49
56
|
silent=silent,
|
|
50
57
|
skip_too_big=skip_too_big,
|
|
58
|
+
component=component,
|
|
59
|
+
source_url=source_url,
|
|
60
|
+
build_plan_key=build_plan_key,
|
|
61
|
+
build_id=build_id,
|
|
62
|
+
environment=environment,
|
|
63
|
+
run_key=run_key,
|
|
51
64
|
)
|
|
52
65
|
|
|
53
66
|
|
|
@@ -133,13 +146,50 @@ def _print_summary(uploader: Uploader) -> None:
|
|
|
133
146
|
)
|
|
134
147
|
|
|
135
148
|
|
|
149
|
+
def _common_options(func: Callable) -> Callable:
|
|
150
|
+
"""Apply the shared connection + launch-metadata options to a command.
|
|
151
|
+
|
|
152
|
+
`watch` and `upload` accept the identical option set; defining it once here
|
|
153
|
+
keeps them from drifting (a new launch field is added in one place, not two).
|
|
154
|
+
Decorators apply bottom-up, so the visible order on the command matches the
|
|
155
|
+
order below.
|
|
156
|
+
"""
|
|
157
|
+
options = [
|
|
158
|
+
click.option("--endpoint", envvar="CHARISMA_ENDPOINT", default=None, help="Charisma API endpoint."),
|
|
159
|
+
click.option("--token", envvar="CHARISMA_TOKEN", default=None, help="Bearer token."),
|
|
160
|
+
click.option(
|
|
161
|
+
"--results", envvar="CHARISMA_RESULTS", default="allure-results", help="Allure results directory."
|
|
162
|
+
),
|
|
163
|
+
click.option("--project", envvar="CHARISMA_PROJECT_ID", required=True, help="Project alias."),
|
|
164
|
+
click.option("--silent", is_flag=True, default=False, help="Don't fail if upload errors occur."),
|
|
165
|
+
click.option("--skip-too-big", is_flag=True, default=False, help="Skip result files larger than 2MB."),
|
|
166
|
+
click.option("--component", default=None, help="Component alias within the project."),
|
|
167
|
+
click.option("--source-url", default=None, help="CI/CD build link for this run."),
|
|
168
|
+
click.option("--build-plan-key", default=None, help="Explicit build plan key for resolution."),
|
|
169
|
+
click.option("--build-id", default=None, help="CI build identifier for this run (forwarded as buildId)."),
|
|
170
|
+
click.option(
|
|
171
|
+
"--environment",
|
|
172
|
+
default=None,
|
|
173
|
+
help="Environment code (e.g. 'qat', 'prod'). Stored as the launch environment.",
|
|
174
|
+
),
|
|
175
|
+
click.option(
|
|
176
|
+
"--run-key",
|
|
177
|
+
envvar="CHARISMA_RUN_KEY",
|
|
178
|
+
default=None,
|
|
179
|
+
help="Run identity / merge key (NOT the build id). Producers sharing this key "
|
|
180
|
+
"stream into one launch (launch id = uuid5(project/run-key)) — powers "
|
|
181
|
+
"rerun-in-place and parallel workers; reruns record retry attempts. Use your "
|
|
182
|
+
"CI run/pipeline id (e.g. GITHUB_RUN_ID) so retries merge but separate runs "
|
|
183
|
+
"stay separate. Omit for a new launch.",
|
|
184
|
+
),
|
|
185
|
+
]
|
|
186
|
+
for option in reversed(options):
|
|
187
|
+
func = option(func)
|
|
188
|
+
return func
|
|
189
|
+
|
|
190
|
+
|
|
136
191
|
@cli.command()
|
|
137
|
-
@
|
|
138
|
-
@click.option("--token", envvar="CHARISMA_TOKEN", default=None, help="Bearer token.")
|
|
139
|
-
@click.option("--results", envvar="CHARISMA_RESULTS", default="allure-results", help="Allure results directory.")
|
|
140
|
-
@click.option("--project", envvar="CHARISMA_PROJECT_ID", required=True, help="Project alias.")
|
|
141
|
-
@click.option("--silent", is_flag=True, default=False, help="Don't fail if upload errors occur.")
|
|
142
|
-
@click.option("--skip-too-big", is_flag=True, default=False, help="Skip result files larger than 2MB.")
|
|
192
|
+
@_common_options
|
|
143
193
|
@click.argument("command", nargs=-1, required=True)
|
|
144
194
|
def watch(
|
|
145
195
|
endpoint: str | None,
|
|
@@ -148,13 +198,32 @@ def watch(
|
|
|
148
198
|
project: str,
|
|
149
199
|
silent: bool,
|
|
150
200
|
skip_too_big: bool,
|
|
201
|
+
component: str | None,
|
|
202
|
+
source_url: str | None,
|
|
203
|
+
build_plan_key: str | None,
|
|
204
|
+
build_id: str | None,
|
|
205
|
+
environment: str | None,
|
|
206
|
+
run_key: str | None,
|
|
151
207
|
command: tuple[str, ...],
|
|
152
208
|
) -> None:
|
|
153
209
|
"""Watch allure-results and stream to Charisma while running COMMAND.
|
|
154
210
|
|
|
155
211
|
Usage: charismactl watch --project my-project -- pytest -n 4 tests/
|
|
156
212
|
"""
|
|
157
|
-
config = _resolve_config(
|
|
213
|
+
config = _resolve_config(
|
|
214
|
+
endpoint,
|
|
215
|
+
token,
|
|
216
|
+
project,
|
|
217
|
+
results,
|
|
218
|
+
silent,
|
|
219
|
+
skip_too_big,
|
|
220
|
+
component,
|
|
221
|
+
source_url,
|
|
222
|
+
build_plan_key,
|
|
223
|
+
build_id,
|
|
224
|
+
environment,
|
|
225
|
+
run_key,
|
|
226
|
+
)
|
|
158
227
|
|
|
159
228
|
queue: PriorityQueue[FileEvent] = PriorityQueue()
|
|
160
229
|
uploader = Uploader(config, queue)
|
|
@@ -207,7 +276,12 @@ def watch(
|
|
|
207
276
|
|
|
208
277
|
# Drain and close — stop the watcher AFTER drain so the consumer's final
|
|
209
278
|
# queue flush still sees anything the observer enqueued during drain.
|
|
210
|
-
|
|
279
|
+
# drain() is progress-based and returns False if it stalled or any send
|
|
280
|
+
# failed — same partial-send guard as `upload`. In watch mode a dropped
|
|
281
|
+
# upload must not be masked by a passing test run (exit 0), so an incomplete
|
|
282
|
+
# drain forces a non-zero exit while still preserving a failing test's own
|
|
283
|
+
# exit code.
|
|
284
|
+
fully_drained = uploader.drain(timeout=config.drain_timeout)
|
|
211
285
|
watcher.stop()
|
|
212
286
|
uploader.stop()
|
|
213
287
|
|
|
@@ -219,16 +293,22 @@ def watch(
|
|
|
219
293
|
|
|
220
294
|
_warn_if_results_unsent(uploader, config)
|
|
221
295
|
_print_summary(uploader)
|
|
296
|
+
|
|
297
|
+
if not fully_drained:
|
|
298
|
+
click.echo(
|
|
299
|
+
"Error: upload did not finish — the consumer stalled or results "
|
|
300
|
+
"failed to send; some results were dropped. Exiting non-zero so this "
|
|
301
|
+
"run is not treated as a successful upload.",
|
|
302
|
+
err=True,
|
|
303
|
+
)
|
|
304
|
+
# Preserve a failing test's exit code; otherwise surface the upload
|
|
305
|
+
# failure as exit 1 instead of letting a passing test mask it.
|
|
306
|
+
sys.exit(exit_code if exit_code != 0 else 1)
|
|
222
307
|
sys.exit(exit_code)
|
|
223
308
|
|
|
224
309
|
|
|
225
310
|
@cli.command()
|
|
226
|
-
@
|
|
227
|
-
@click.option("--token", envvar="CHARISMA_TOKEN", default=None, help="Bearer token.")
|
|
228
|
-
@click.option("--results", envvar="CHARISMA_RESULTS", default="allure-results", help="Allure results directory.")
|
|
229
|
-
@click.option("--project", envvar="CHARISMA_PROJECT_ID", required=True, help="Project alias.")
|
|
230
|
-
@click.option("--silent", is_flag=True, default=False, help="Don't fail if upload errors occur.")
|
|
231
|
-
@click.option("--skip-too-big", is_flag=True, default=False, help="Skip result files larger than 2MB.")
|
|
311
|
+
@_common_options
|
|
232
312
|
def upload(
|
|
233
313
|
endpoint: str | None,
|
|
234
314
|
token: str | None,
|
|
@@ -236,12 +316,31 @@ def upload(
|
|
|
236
316
|
project: str,
|
|
237
317
|
silent: bool,
|
|
238
318
|
skip_too_big: bool,
|
|
319
|
+
component: str | None,
|
|
320
|
+
source_url: str | None,
|
|
321
|
+
build_plan_key: str | None,
|
|
322
|
+
build_id: str | None,
|
|
323
|
+
environment: str | None,
|
|
324
|
+
run_key: str | None,
|
|
239
325
|
) -> None:
|
|
240
326
|
"""Upload completed allure-results to Charisma (post-execution).
|
|
241
327
|
|
|
242
328
|
Usage: charismactl upload --project my-project --results allure-results
|
|
243
329
|
"""
|
|
244
|
-
config = _resolve_config(
|
|
330
|
+
config = _resolve_config(
|
|
331
|
+
endpoint,
|
|
332
|
+
token,
|
|
333
|
+
project,
|
|
334
|
+
results,
|
|
335
|
+
silent,
|
|
336
|
+
skip_too_big,
|
|
337
|
+
component,
|
|
338
|
+
source_url,
|
|
339
|
+
build_plan_key,
|
|
340
|
+
build_id,
|
|
341
|
+
environment,
|
|
342
|
+
run_key,
|
|
343
|
+
)
|
|
245
344
|
|
|
246
345
|
queue: PriorityQueue[FileEvent] = PriorityQueue()
|
|
247
346
|
uploader = Uploader(config, queue)
|
|
@@ -268,9 +367,14 @@ def upload(
|
|
|
268
367
|
if category is not None:
|
|
269
368
|
queue.put(FileEvent(path=filepath, category=category))
|
|
270
369
|
|
|
271
|
-
# Process and drain
|
|
370
|
+
# Process and drain. drain() is progress-based: it runs to completion for a
|
|
371
|
+
# healthy upload of any size and only returns False if the consumer STALLS
|
|
372
|
+
# (no progress for drain_timeout seconds). A False return means results were
|
|
373
|
+
# dropped — we must still close the launch and print the summary, then exit
|
|
374
|
+
# non-zero so CI sees a failure instead of a green build with a partial send
|
|
375
|
+
# (the upload results_sent=700/10158-queued bug).
|
|
272
376
|
uploader.start()
|
|
273
|
-
uploader.drain(timeout=config.drain_timeout)
|
|
377
|
+
fully_drained = uploader.drain(timeout=config.drain_timeout)
|
|
274
378
|
uploader.stop()
|
|
275
379
|
|
|
276
380
|
# Close launch
|
|
@@ -279,3 +383,12 @@ def upload(
|
|
|
279
383
|
uploader.stop()
|
|
280
384
|
|
|
281
385
|
_print_summary(uploader)
|
|
386
|
+
|
|
387
|
+
if not fully_drained:
|
|
388
|
+
click.echo(
|
|
389
|
+
"Error: upload did not finish — the consumer stalled and some results "
|
|
390
|
+
"were not sent. Exiting non-zero so this run is not treated as a "
|
|
391
|
+
"successful upload.",
|
|
392
|
+
err=True,
|
|
393
|
+
)
|
|
394
|
+
sys.exit(1)
|
|
@@ -4,32 +4,69 @@ from dataclasses import dataclass, field
|
|
|
4
4
|
from enum import IntEnum
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any
|
|
7
|
-
from uuid import NAMESPACE_URL, uuid5
|
|
8
7
|
|
|
8
|
+
# Backend field-size contract (mirror of backend
|
|
9
|
+
# models/testresults.py::MAX_ERROR_MESSAGE_BYTES / MAX_STACK_TRACE_BYTES).
|
|
10
|
+
# The streaming ingestion API rejects the WHOLE batch (400 INVALID_INPUT) if any
|
|
11
|
+
# result's error_message > 10KB or stack_trace > 100KB (UTF-8 bytes). Keep these
|
|
12
|
+
# byte-identical to the backend; oversized fields spill to an attachment rather
|
|
13
|
+
# than truncate (see uploader._spill_oversized_field).
|
|
14
|
+
MAX_ERROR_MESSAGE_BYTES = 10 * 1024 # 10KB
|
|
15
|
+
MAX_STACK_TRACE_BYTES = 100 * 1024 # 100KB
|
|
9
16
|
|
|
10
|
-
def backend_result_id(launch_id: str, test_id: str) -> str:
|
|
11
|
-
"""Derive the result identity the Charisma backend stores and validates against.
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# Cap for a synthesized attachment filename derived from a test's historyId,
|
|
19
|
+
# which is arbitrary client-supplied text (Allure historyId, or fullName
|
|
20
|
+
# fallback) that may contain path separators or be very long.
|
|
21
|
+
MAX_ATTACHMENT_FILENAME_LENGTH = 200
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def safe_attachment_filename(history_id: str, suffix: str) -> str:
|
|
25
|
+
"""Build a safe ``text/plain`` attachment filename from a test identity.
|
|
26
|
+
|
|
27
|
+
``history_id`` is arbitrary client-supplied text (Allure ``historyId``, or
|
|
28
|
+
the ``fullName`` fallback) and can contain path separators (``/``, ``\\``),
|
|
29
|
+
``..``, control characters, or be very long. This strips it to a basename,
|
|
30
|
+
replaces any remaining unsafe characters, and length-caps the stem so the
|
|
31
|
+
multipart filename sent to the backend is well-formed and bounded.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
history_id: The owning result's test identity.
|
|
35
|
+
suffix: A short descriptor appended to the stem (e.g. ``error-message``).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A sanitized ``{stem}-{suffix}.txt`` filename.
|
|
39
|
+
"""
|
|
40
|
+
# Drop any path structure, then keep only filename-safe characters.
|
|
41
|
+
base = history_id.replace("\\", "/").rsplit("/", 1)[-1]
|
|
42
|
+
safe = "".join(c if (c.isalnum() or c in "-_.") else "_" for c in base)
|
|
43
|
+
stem = safe[:MAX_ATTACHMENT_FILENAME_LENGTH] or "result"
|
|
44
|
+
return f"{stem}-{suffix}.txt"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def head_snippet(text: str, max_bytes: int) -> str:
|
|
48
|
+
"""Return the longest UTF-8 prefix of ``text`` that fits in ``max_bytes``.
|
|
49
|
+
|
|
50
|
+
Cuts on a character boundary — never splits a multibyte code point — by
|
|
51
|
+
encoding, slicing the byte string, and decoding with ``errors="ignore"`` to
|
|
52
|
+
drop a trailing partial character. Used to keep an inline head of an
|
|
53
|
+
oversized field (the full value goes to an attachment).
|
|
20
54
|
|
|
21
55
|
Args:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
``test_id`` in the results batch. This is the Allure ``historyId``,
|
|
25
|
-
falling back to ``fullName`` when ``historyId`` is absent (see
|
|
26
|
-
``parser.parse_result_file``). Matching the backend requires only
|
|
27
|
-
that both sides key off this same value, whichever it is.
|
|
56
|
+
text: The source string.
|
|
57
|
+
max_bytes: Maximum size of the returned prefix in UTF-8 bytes.
|
|
28
58
|
|
|
29
59
|
Returns:
|
|
30
|
-
|
|
60
|
+
A prefix of ``text`` whose UTF-8 encoding is ``<= max_bytes`` bytes.
|
|
31
61
|
"""
|
|
32
|
-
|
|
62
|
+
encoded = text.encode("utf-8")
|
|
63
|
+
# Clamp: a non-positive budget must yield an empty prefix, never a Python
|
|
64
|
+
# negative slice (encoded[:-n]) which would return almost the whole string.
|
|
65
|
+
if max_bytes <= 0:
|
|
66
|
+
return ""
|
|
67
|
+
if len(encoded) <= max_bytes:
|
|
68
|
+
return text
|
|
69
|
+
return encoded[:max_bytes].decode("utf-8", errors="ignore")
|
|
33
70
|
|
|
34
71
|
|
|
35
72
|
class FileCategory(IntEnum):
|