flaky-detector-cli 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: flaky-detector-cli
3
+ Version: 0.1.0
4
+ Summary: Upload JUnit XML test reports to a flaky-detector project from the command line or CI.
5
+ Author: Your Name
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/YOURNAME/flaky-detector
8
+ Project-URL: Repository, https://github.com/YOURNAME/flaky-detector
9
+ Keywords: testing,pytest,junit,ci,flaky-tests
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # flaky-detector-cli
22
+
23
+ A tiny, zero-dependency command-line tool that uploads a JUnit XML test
24
+ report to a [flaky-detector](https://github.com/YOURNAME/flaky-detector)
25
+ project — the same thing the project's GitHub Actions snippet does with
26
+ `curl`, packaged as a real command.
27
+
28
+ ```bash
29
+ pip install flaky-detector-cli
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ pytest --junitxml=report.xml
36
+ flaky-detector submit report.xml
37
+ ```
38
+
39
+ By default it reads two environment variables — the same two names your
40
+ flaky-detector project's CI snippet already uses:
41
+
42
+ - `FLAKY_DETECTOR_URL` — your backend's base URL
43
+ - `FLAKY_DETECTOR_TOKEN` — the project's upload token
44
+
45
+ Everything else is auto-detected. In GitHub Actions, it reads `GITHUB_SHA`,
46
+ `GITHUB_REF_NAME`, `GITHUB_RUN_ID`, and `GITHUB_RUN_ATTEMPT` automatically.
47
+ Running locally, it falls back to your current `git` checkout for the
48
+ commit and branch. You can override any of it explicitly:
49
+
50
+ ```bash
51
+ flaky-detector submit report.xml \
52
+ --url https://your-backend.example.com \
53
+ --token YOUR_TOKEN \
54
+ --commit abc1234 \
55
+ --branch main \
56
+ --run-id 42 \
57
+ --run-attempt 1
58
+ ```
59
+
60
+ ## Why not `curl`?
61
+
62
+ `curl` works fine and is what the project's own onboarding flow suggests
63
+ first. This exists for people who'd rather not hand-build a multipart
64
+ `curl` command in CI YAML, and for local use: run your suite, then
65
+ `flaky-detector submit report.xml` to see the result on your dashboard
66
+ without touching CI at all.
67
+
68
+ ## Exit codes
69
+
70
+ - `0` — uploaded successfully
71
+ - `1` — the server rejected the upload (bad token, malformed report, etc.);
72
+ stderr has the reason
73
+ - `2` — a required argument is missing (no URL/token, and none could be
74
+ auto-detected)
75
+
76
+ ## Development
77
+
78
+ ```bash
79
+ git clone https://github.com/YOURNAME/flaky-detector
80
+ cd flaky-detector/cli
81
+ pip install -e .
82
+ ```
83
+
84
+ No dependencies to install for either running or developing this tool —
85
+ only the Python standard library.
@@ -0,0 +1,65 @@
1
+ # flaky-detector-cli
2
+
3
+ A tiny, zero-dependency command-line tool that uploads a JUnit XML test
4
+ report to a [flaky-detector](https://github.com/YOURNAME/flaky-detector)
5
+ project — the same thing the project's GitHub Actions snippet does with
6
+ `curl`, packaged as a real command.
7
+
8
+ ```bash
9
+ pip install flaky-detector-cli
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ pytest --junitxml=report.xml
16
+ flaky-detector submit report.xml
17
+ ```
18
+
19
+ By default it reads two environment variables — the same two names your
20
+ flaky-detector project's CI snippet already uses:
21
+
22
+ - `FLAKY_DETECTOR_URL` — your backend's base URL
23
+ - `FLAKY_DETECTOR_TOKEN` — the project's upload token
24
+
25
+ Everything else is auto-detected. In GitHub Actions, it reads `GITHUB_SHA`,
26
+ `GITHUB_REF_NAME`, `GITHUB_RUN_ID`, and `GITHUB_RUN_ATTEMPT` automatically.
27
+ Running locally, it falls back to your current `git` checkout for the
28
+ commit and branch. You can override any of it explicitly:
29
+
30
+ ```bash
31
+ flaky-detector submit report.xml \
32
+ --url https://your-backend.example.com \
33
+ --token YOUR_TOKEN \
34
+ --commit abc1234 \
35
+ --branch main \
36
+ --run-id 42 \
37
+ --run-attempt 1
38
+ ```
39
+
40
+ ## Why not `curl`?
41
+
42
+ `curl` works fine and is what the project's own onboarding flow suggests
43
+ first. This exists for people who'd rather not hand-build a multipart
44
+ `curl` command in CI YAML, and for local use: run your suite, then
45
+ `flaky-detector submit report.xml` to see the result on your dashboard
46
+ without touching CI at all.
47
+
48
+ ## Exit codes
49
+
50
+ - `0` — uploaded successfully
51
+ - `1` — the server rejected the upload (bad token, malformed report, etc.);
52
+ stderr has the reason
53
+ - `2` — a required argument is missing (no URL/token, and none could be
54
+ auto-detected)
55
+
56
+ ## Development
57
+
58
+ ```bash
59
+ git clone https://github.com/YOURNAME/flaky-detector
60
+ cd flaky-detector/cli
61
+ pip install -e .
62
+ ```
63
+
64
+ No dependencies to install for either running or developing this tool —
65
+ only the Python standard library.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flaky-detector-cli"
7
+ version = "0.1.0"
8
+ description = "Upload JUnit XML test reports to a flaky-detector project from the command line or CI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Your Name" }]
13
+ keywords = ["testing", "pytest", "junit", "ci", "flaky-tests"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Software Development :: Testing",
21
+ ]
22
+ # Deliberately zero runtime dependencies: argparse, urllib, subprocess are
23
+ # all standard library. Keeps `pip install` instant and avoids ever needing
24
+ # a dependency-conflict fix for a tool this small.
25
+ dependencies = []
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/YOURNAME/flaky-detector"
29
+ Repository = "https://github.com/YOURNAME/flaky-detector"
30
+
31
+ [project.scripts]
32
+ flaky-detector = "flaky_detector_cli.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,201 @@
1
+ """
2
+ flaky-detector-cli: a tiny, zero-dependency command that uploads a JUnit XML
3
+ report to a flaky-detector project.
4
+
5
+ flaky-detector submit report.xml
6
+
7
+ Reads FLAKY_DETECTOR_URL and FLAKY_DETECTOR_TOKEN from the environment by
8
+ default (the same two names your GitHub Actions workflow secret/variable
9
+ already use), or pass --url/--token explicitly. Everything else (commit sha,
10
+ branch, run id/attempt) is auto-detected: from GitHub Actions' own env vars
11
+ when running in CI, falling back to your local `git` checkout otherwise.
12
+
13
+ No third-party dependencies: just the standard library.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import mimetypes
20
+ import os
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ import urllib.error
25
+ import urllib.request
26
+ import uuid
27
+ from pathlib import Path
28
+
29
+ from . import __version__
30
+
31
+ DEFAULT_TIMEOUT = 30
32
+
33
+
34
+ def _git(*args: str) -> str | None:
35
+ try:
36
+ out = subprocess.run(
37
+ ["git", *args], capture_output=True, text=True, timeout=5, check=True
38
+ )
39
+ value = out.stdout.strip()
40
+ return value or None
41
+ except Exception:
42
+ return None
43
+
44
+
45
+ def detect_commit_sha() -> str | None:
46
+ return os.environ.get("GITHUB_SHA") or _git("rev-parse", "HEAD")
47
+
48
+
49
+ def detect_branch() -> str | None:
50
+ ref = os.environ.get("GITHUB_REF_NAME")
51
+ if ref:
52
+ return ref
53
+ branch = _git("rev-parse", "--abbrev-ref", "HEAD")
54
+ return None if branch == "HEAD" else branch # "HEAD" means a detached checkout, not a real branch
55
+
56
+
57
+ def detect_run_id() -> str:
58
+ return os.environ.get("GITHUB_RUN_ID") or f"local-{int(time.time())}"
59
+
60
+
61
+ def detect_run_attempt() -> str:
62
+ return os.environ.get("GITHUB_RUN_ATTEMPT") or "1"
63
+
64
+
65
+ def encode_multipart(fields: dict[str, str], file_field: str, file_path: Path) -> tuple[bytes, str]:
66
+ """Hand-rolled multipart/form-data body (stdlib only, no `requests`)."""
67
+ boundary = uuid.uuid4().hex
68
+ parts: list[bytes] = []
69
+
70
+ for name, value in fields.items():
71
+ parts.append(
72
+ f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'.encode()
73
+ )
74
+
75
+ content_type = mimetypes.guess_type(file_path.name)[0] or "application/xml"
76
+ parts.append(
77
+ (
78
+ f'--{boundary}\r\nContent-Disposition: form-data; name="{file_field}"; '
79
+ f'filename="{file_path.name}"\r\nContent-Type: {content_type}\r\n\r\n'
80
+ ).encode()
81
+ + file_path.read_bytes()
82
+ + b"\r\n"
83
+ )
84
+ parts.append(f"--{boundary}--\r\n".encode())
85
+
86
+ body = b"".join(parts)
87
+ return body, f"multipart/form-data; boundary={boundary}"
88
+
89
+
90
+ class SubmitError(RuntimeError):
91
+ pass
92
+
93
+
94
+ def submit_report(
95
+ *,
96
+ url: str,
97
+ token: str,
98
+ report_path: Path,
99
+ commit_sha: str,
100
+ branch: str,
101
+ run_id: str,
102
+ run_attempt: str,
103
+ job_name: str = "",
104
+ ) -> dict:
105
+ if not report_path.exists():
106
+ raise SubmitError(f"Report file not found: {report_path}")
107
+
108
+ fields = {
109
+ "run_id": run_id,
110
+ "run_attempt": run_attempt,
111
+ "job_name": job_name,
112
+ "commit_sha": commit_sha,
113
+ "branch": branch,
114
+ }
115
+ body, content_type = encode_multipart(fields, "report", report_path)
116
+
117
+ request = urllib.request.Request(
118
+ url.rstrip("/") + "/api/ingest/",
119
+ data=body,
120
+ method="POST",
121
+ headers={
122
+ "Authorization": f"Bearer {token}",
123
+ "Content-Type": content_type,
124
+ },
125
+ )
126
+ try:
127
+ with urllib.request.urlopen(request, timeout=DEFAULT_TIMEOUT) as resp:
128
+ return json.loads(resp.read().decode())
129
+ except urllib.error.HTTPError as exc:
130
+ detail = exc.read().decode(errors="replace")
131
+ raise SubmitError(f"Server rejected the upload ({exc.code}): {detail}") from exc
132
+ except urllib.error.URLError as exc:
133
+ raise SubmitError(f"Could not reach {url}: {exc.reason}") from exc
134
+ except OSError as exc:
135
+ raise SubmitError(f"Connection to {url} failed: {exc}") from exc
136
+
137
+
138
+ def build_parser() -> argparse.ArgumentParser:
139
+ parser = argparse.ArgumentParser(prog="flaky-detector", description=__doc__.split("\n\n")[0])
140
+ parser.add_argument("--version", action="version", version=f"flaky-detector-cli {__version__}")
141
+ sub = parser.add_subparsers(dest="command", required=True)
142
+
143
+ submit = sub.add_parser("submit", help="Upload a JUnit XML report")
144
+ submit.add_argument("report", type=Path, help="Path to the JUnit XML report, e.g. report.xml")
145
+ submit.add_argument("--url", default=os.environ.get("FLAKY_DETECTOR_URL"),
146
+ help="Backend base URL (env: FLAKY_DETECTOR_URL)")
147
+ submit.add_argument("--token", default=os.environ.get("FLAKY_DETECTOR_TOKEN"),
148
+ help="Project upload token (env: FLAKY_DETECTOR_TOKEN)")
149
+ submit.add_argument("--commit", default=None, help="Commit SHA (auto-detected if omitted)")
150
+ submit.add_argument("--branch", default=None, help="Branch name (auto-detected if omitted)")
151
+ submit.add_argument("--run-id", default=None, help="CI run id (auto-detected if omitted)")
152
+ submit.add_argument("--run-attempt", default=None, help="CI run attempt (auto-detected if omitted)")
153
+ submit.add_argument("--job-name", default="", help="Job name, for matrix builds")
154
+ return parser
155
+
156
+
157
+ def main(argv: list[str] | None = None) -> int:
158
+ args = build_parser().parse_args(argv)
159
+
160
+ if args.command == "submit":
161
+ if not args.url:
162
+ print("error: no backend URL given (pass --url or set FLAKY_DETECTOR_URL)", file=sys.stderr)
163
+ return 2
164
+ if not args.token:
165
+ print("error: no token given (pass --token or set FLAKY_DETECTOR_TOKEN)", file=sys.stderr)
166
+ return 2
167
+
168
+ commit_sha = args.commit or detect_commit_sha()
169
+ branch = args.branch or detect_branch()
170
+ if not commit_sha:
171
+ print("error: could not determine commit SHA (pass --commit)", file=sys.stderr)
172
+ return 2
173
+ if not branch:
174
+ print("error: could not determine branch (pass --branch)", file=sys.stderr)
175
+ return 2
176
+
177
+ try:
178
+ result = submit_report(
179
+ url=args.url,
180
+ token=args.token,
181
+ report_path=args.report,
182
+ commit_sha=commit_sha,
183
+ branch=branch,
184
+ run_id=args.run_id or detect_run_id(),
185
+ run_attempt=args.run_attempt or detect_run_attempt(),
186
+ job_name=args.job_name,
187
+ )
188
+ except SubmitError as exc:
189
+ print(f"error: {exc}", file=sys.stderr)
190
+ return 1
191
+
192
+ verb = "Created" if result.get("created") else "Already recorded"
193
+ print(f"{verb} run {result.get('run')}: "
194
+ f"{result.get('tests_in_report', 0)} tests, {result.get('tests_scored', 0)} scored.")
195
+ return 0
196
+
197
+ return 2
198
+
199
+
200
+ if __name__ == "__main__":
201
+ raise SystemExit(main())
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: flaky-detector-cli
3
+ Version: 0.1.0
4
+ Summary: Upload JUnit XML test reports to a flaky-detector project from the command line or CI.
5
+ Author: Your Name
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/YOURNAME/flaky-detector
8
+ Project-URL: Repository, https://github.com/YOURNAME/flaky-detector
9
+ Keywords: testing,pytest,junit,ci,flaky-tests
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # flaky-detector-cli
22
+
23
+ A tiny, zero-dependency command-line tool that uploads a JUnit XML test
24
+ report to a [flaky-detector](https://github.com/YOURNAME/flaky-detector)
25
+ project — the same thing the project's GitHub Actions snippet does with
26
+ `curl`, packaged as a real command.
27
+
28
+ ```bash
29
+ pip install flaky-detector-cli
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ pytest --junitxml=report.xml
36
+ flaky-detector submit report.xml
37
+ ```
38
+
39
+ By default it reads two environment variables — the same two names your
40
+ flaky-detector project's CI snippet already uses:
41
+
42
+ - `FLAKY_DETECTOR_URL` — your backend's base URL
43
+ - `FLAKY_DETECTOR_TOKEN` — the project's upload token
44
+
45
+ Everything else is auto-detected. In GitHub Actions, it reads `GITHUB_SHA`,
46
+ `GITHUB_REF_NAME`, `GITHUB_RUN_ID`, and `GITHUB_RUN_ATTEMPT` automatically.
47
+ Running locally, it falls back to your current `git` checkout for the
48
+ commit and branch. You can override any of it explicitly:
49
+
50
+ ```bash
51
+ flaky-detector submit report.xml \
52
+ --url https://your-backend.example.com \
53
+ --token YOUR_TOKEN \
54
+ --commit abc1234 \
55
+ --branch main \
56
+ --run-id 42 \
57
+ --run-attempt 1
58
+ ```
59
+
60
+ ## Why not `curl`?
61
+
62
+ `curl` works fine and is what the project's own onboarding flow suggests
63
+ first. This exists for people who'd rather not hand-build a multipart
64
+ `curl` command in CI YAML, and for local use: run your suite, then
65
+ `flaky-detector submit report.xml` to see the result on your dashboard
66
+ without touching CI at all.
67
+
68
+ ## Exit codes
69
+
70
+ - `0` — uploaded successfully
71
+ - `1` — the server rejected the upload (bad token, malformed report, etc.);
72
+ stderr has the reason
73
+ - `2` — a required argument is missing (no URL/token, and none could be
74
+ auto-detected)
75
+
76
+ ## Development
77
+
78
+ ```bash
79
+ git clone https://github.com/YOURNAME/flaky-detector
80
+ cd flaky-detector/cli
81
+ pip install -e .
82
+ ```
83
+
84
+ No dependencies to install for either running or developing this tool —
85
+ only the Python standard library.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/flaky_detector_cli/__init__.py
5
+ src/flaky_detector_cli/__main__.py
6
+ src/flaky_detector_cli/cli.py
7
+ src/flaky_detector_cli.egg-info/PKG-INFO
8
+ src/flaky_detector_cli.egg-info/SOURCES.txt
9
+ src/flaky_detector_cli.egg-info/dependency_links.txt
10
+ src/flaky_detector_cli.egg-info/entry_points.txt
11
+ src/flaky_detector_cli.egg-info/top_level.txt
12
+ src/tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flaky-detector = flaky_detector_cli.cli:main
@@ -0,0 +1,2 @@
1
+ flaky_detector_cli
2
+ tests
@@ -0,0 +1,149 @@
1
+ import io
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ from contextlib import redirect_stderr, redirect_stdout
8
+ from pathlib import Path
9
+ from unittest.mock import MagicMock, patch
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
12
+
13
+ from flaky_detector_cli.cli import (
14
+ build_parser,
15
+ detect_branch,
16
+ detect_commit_sha,
17
+ detect_run_attempt,
18
+ detect_run_id,
19
+ encode_multipart,
20
+ main,
21
+ submit_report,
22
+ )
23
+
24
+
25
+ def test_encode_multipart_contains_all_fields_and_file_content(tmp_path=None):
26
+ tmp = Path(tempfile.mkdtemp())
27
+ report = tmp / "report.xml"
28
+ report.write_text("<testsuite></testsuite>")
29
+
30
+ body, content_type = encode_multipart(
31
+ {"run_id": "42", "branch": "main"}, "report", report
32
+ )
33
+ text = body.decode()
34
+
35
+ assert content_type.startswith("multipart/form-data; boundary=")
36
+ boundary = content_type.split("boundary=")[1]
37
+ assert text.count(f"--{boundary}") == 4 # 2 field parts + 1 file part + 1 closing boundary
38
+ assert text.strip().endswith(f"--{boundary}--")
39
+ assert 'name="run_id"' in text and "\r\n42\r\n" in text
40
+ assert 'name="branch"' in text and "\r\nmain\r\n" in text
41
+ assert 'name="report"; filename="report.xml"' in text
42
+ assert "<testsuite></testsuite>" in text
43
+
44
+
45
+ def test_detect_commit_sha_prefers_github_env():
46
+ with patch.dict(os.environ, {"GITHUB_SHA": "deadbeef"}, clear=False):
47
+ assert detect_commit_sha() == "deadbeef"
48
+
49
+
50
+ def test_detect_branch_prefers_github_env():
51
+ with patch.dict(os.environ, {"GITHUB_REF_NAME": "main"}, clear=False):
52
+ assert detect_branch() == "main"
53
+
54
+
55
+ def test_detect_branch_falls_back_to_git_in_a_real_repo():
56
+ tmp = Path(tempfile.mkdtemp())
57
+ subprocess.run(["git", "init", "-q"], cwd=tmp, check=True)
58
+ subprocess.run(["git", "config", "user.email", "a@b.c"], cwd=tmp, check=True)
59
+ subprocess.run(["git", "config", "user.name", "a"], cwd=tmp, check=True)
60
+ (tmp / "f.txt").write_text("x")
61
+ subprocess.run(["git", "add", "."], cwd=tmp, check=True)
62
+ subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=tmp, check=True)
63
+
64
+ env = {k: v for k, v in os.environ.items() if k != "GITHUB_REF_NAME"}
65
+ cwd = os.getcwd()
66
+ try:
67
+ os.chdir(tmp)
68
+ with patch.dict(os.environ, env, clear=True):
69
+ branch = detect_branch()
70
+ sha = detect_commit_sha()
71
+ finally:
72
+ os.chdir(cwd)
73
+ assert branch in ("main", "master") # depends on the machine's git default
74
+ assert sha is not None and len(sha) == 40
75
+
76
+
77
+ def test_detect_run_id_falls_back_to_a_local_placeholder():
78
+ env = {k: v for k, v in os.environ.items() if k != "GITHUB_RUN_ID"}
79
+ with patch.dict(os.environ, env, clear=True):
80
+ assert detect_run_id().startswith("local-")
81
+
82
+
83
+ def test_detect_run_attempt_defaults_to_one():
84
+ env = {k: v for k, v in os.environ.items() if k != "GITHUB_RUN_ATTEMPT"}
85
+ with patch.dict(os.environ, env, clear=True):
86
+ assert detect_run_attempt() == "1"
87
+
88
+
89
+ def test_submit_report_raises_on_missing_file():
90
+ try:
91
+ submit_report(
92
+ url="https://x", token="t", report_path=Path("/no/such/file.xml"),
93
+ commit_sha="a", branch="main", run_id="1", run_attempt="1",
94
+ )
95
+ raise AssertionError("expected SubmitError")
96
+ except Exception as e:
97
+ assert "not found" in str(e)
98
+
99
+
100
+ def test_build_parser_requires_a_subcommand():
101
+ parser = build_parser()
102
+ try:
103
+ parser.parse_args([])
104
+ raise AssertionError("expected SystemExit")
105
+ except SystemExit:
106
+ pass
107
+
108
+
109
+ def test_main_errors_cleanly_with_no_token():
110
+ tmp = Path(tempfile.mkdtemp())
111
+ report = tmp / "r.xml"
112
+ report.write_text("<testsuite></testsuite>")
113
+ env = {k: v for k, v in os.environ.items() if k != "FLAKY_DETECTOR_TOKEN"}
114
+ err = io.StringIO()
115
+ with patch.dict(os.environ, env, clear=True), redirect_stderr(err):
116
+ code = main(["submit", str(report), "--url", "https://x"])
117
+ assert code == 2
118
+ assert "token" in err.getvalue()
119
+
120
+
121
+ @patch("flaky_detector_cli.cli.urllib.request.urlopen")
122
+ def test_main_success_path_prints_summary(mock_urlopen):
123
+ tmp = Path(tempfile.mkdtemp())
124
+ report = tmp / "r.xml"
125
+ report.write_text("<testsuite></testsuite>")
126
+
127
+ response = MagicMock()
128
+ response.read.return_value = json.dumps(
129
+ {"run": 7, "created": True, "tests_in_report": 3, "tests_scored": 3}
130
+ ).encode()
131
+ mock_urlopen.return_value.__enter__.return_value = response
132
+
133
+ out = io.StringIO()
134
+ with redirect_stdout(out):
135
+ code = main([
136
+ "submit", str(report),
137
+ "--url", "https://api.example.com",
138
+ "--token", "sekret",
139
+ "--commit", "a" * 40,
140
+ "--branch", "main",
141
+ "--run-id", "1",
142
+ ])
143
+ assert code == 0
144
+ assert "Created run 7" in out.getvalue()
145
+ assert "3 tests, 3 scored" in out.getvalue()
146
+
147
+ sent_request = mock_urlopen.call_args[0][0]
148
+ assert sent_request.full_url == "https://api.example.com/api/ingest/"
149
+ assert sent_request.get_header("Authorization") == "Bearer sekret"