deploynator 1.3.0__tar.gz → 1.3.3__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.
- {deploynator-1.3.0 → deploynator-1.3.3}/PKG-INFO +6 -39
- {deploynator-1.3.0 → deploynator-1.3.3}/README.md +5 -38
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/__init__.py +1 -1
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_app.py +30 -1
- deploynator-1.3.3/deploynator/_toml.py +287 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/example.deploynator.toml +4 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_runner.py +14 -0
- deploynator-1.3.3/tests/test_toml.py +216 -0
- deploynator-1.3.0/deploynator/_toml.py +0 -100
- deploynator-1.3.0/tests/test_toml.py +0 -81
- {deploynator-1.3.0 → deploynator-1.3.3}/.gitignore +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/LICENSE +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/__main__.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_changelog.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_config.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_deploy.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_docs.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_git.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_mcp_server.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_models.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_platforms.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_pypi.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_readme.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_runner.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_semver.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/projects/deploynator.yaml +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/projects/lazyown.yaml +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/projects/template.yaml +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/pyproject.toml +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/specs/deploynator_toml_pipeline.spec.md +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/__init__.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/bdd_test_deploy.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_changelog.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_cli_mcp.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_config.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_deploy.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_init.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_install_example.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_models.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_path_deploy.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_pipeline.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_pypi.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_readme_footer.py +0 -0
- {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_semver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: deploynator
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: Multi-platform deployer for GitHub, GitLab, and Bitbucket
|
|
5
5
|
Project-URL: Homepage, https://github.com/grisuno/deploynator
|
|
6
6
|
Project-URL: Repository, https://github.com/grisuno/deploynator
|
|
@@ -260,55 +260,22 @@ Deployed with [deploynator](https://github.com/grisuno/deploynator)
|
|
|
260
260
|
## Knowledge Base
|
|
261
261
|
|
|
262
262
|
This project has been analyzed by [ReadMenator](https://github.com/grisuno/ReadMenator),
|
|
263
|
-
a zero-token polyglot static analysis tool.
|
|
264
|
-
`pip install readmenator && readmenator . --rebuild`. Analysis outputs:
|
|
263
|
+
a zero-token polyglot static analysis tool. Analysis outputs are available:
|
|
265
264
|
|
|
266
265
|
- **[KNOWLEDGE_BASE.md](./KNOWLEDGE_BASE.md)** -- Full architecture reference with all
|
|
267
266
|
classes, functions, imports, dependency graphs, UML class diagrams, security
|
|
268
|
-
audit findings, community analysis,
|
|
267
|
+
audit findings, community analysis, and more.
|
|
269
268
|
- **[readmenator-agent/](./readmenator-agent/)** -- Agent-friendly, grep-optimized index.
|
|
270
269
|
- `INDEX.md` -- Quick reference: what each file does
|
|
271
|
-
- `SYMBOLS.md` -- Every symbol with file:line
|
|
272
|
-
- `ARCHITECTURE.md` -- Internal dependency pairs + external imports
|
|
273
270
|
- `API.md` -- Public function contracts
|
|
274
|
-
- `GOTCHAS.md` -- Change warnings
|
|
275
|
-
- `SECURITY.md` -- Findings by severity
|
|
276
|
-
- `MEMORY.md` -- Rules, workflow, style, deliverables, session log
|
|
277
|
-
- `MANIFEST.json` -- Freshness (git_commit + source_fingerprint) + read order
|
|
278
|
-
- `KB_<subsystem>.md` -- Per-subsystem context
|
|
279
|
-
- `recipes/*.md` -- Grounded task guides (add-function, fix-cycle, ...)
|
|
271
|
+
- `GOTCHAS.md` -- Change warnings
|
|
272
|
+
- `SECURITY.md` -- Findings by severity
|
|
280
273
|
- **[readmenator-wiki/](./readmenator-wiki/)** -- Navigable wiki (start here for the big picture).
|
|
281
274
|
- `index.md` -- Entry point: overview, reading order, god nodes, connections
|
|
282
275
|
- `community_*.md` -- One synthesis page per code community
|
|
283
|
-
- `connections.json` -- Typed bridges between communities
|
|
284
|
-
- `queries.md` -- Suggested questions + answer log
|
|
285
276
|
- `REPORT.md` -- Honest audit: coverage, confidence, limits
|
|
286
|
-
- **[readmenator-graphrag/](./readmenator-graphrag/)** -- Zero-token GraphRAG index.
|
|
287
|
-
- `index.json` + `REPORTS.md` -- Query with `readmenator . ask "<question>"`
|
|
288
|
-
- **[readmenator-apidocs/](./readmenator-apidocs/)** -- API reference (Sphinx/Doxygen style).
|
|
289
|
-
- `index.md` + `modules/*.md` -- Signatures, parameters, returns, raises, examples, call graphs
|
|
290
|
-
- `architecture.md` -- Tiered architecture + execution flows (control flow, call tree, sequence)
|
|
291
|
-
- `classes.md`, `genindex.md`, `COVERAGE.md` -- Hierarchy, A-Z index, doc coverage + warnings
|
|
292
|
-
- `index.html` + `api/` -- Offline searchable browser and machine-readable model (`api/index.json`, `api/modules/*.jsonl` one entity per line)
|
|
293
|
-
- **[readmenator-maps/](./readmenator-maps/)** -- Interactive gallery (`index.html`).
|
|
294
|
-
- 5 system maps: architecture, workflow, sequence, dataflow, lifecycle
|
|
295
|
-
- `graph-force.html` -- Force-graph explorer (2D/3D, reach, paths, lenses)
|
|
296
|
-
- `graph-bundle.html` -- Edge bundle explorer (circle + sphere)
|
|
297
|
-
- `components.html` -- Component diagrams
|
|
298
|
-
- **[docs/](./docs/)** -- Static site (GitHub Pages ready).
|
|
299
|
-
- Gallery copy + `llms.txt` (agent entry point) + `md/` docs + video
|
|
300
|
-
- **[readmenator-diagrams/](./readmenator-diagrams/)** -- Component diagrams.
|
|
301
|
-
- `analysis.json` + `graph LR` markdown + per-component pages + `index.html`
|
|
302
|
-
- **[readmenator-rules/](./readmenator-rules/)** -- Suggested Semgrep rules (`*.yml`),
|
|
303
|
-
`yara_rules.yar` (YARA-lite) + `exclusions.yaml` (false-positive blocklist)
|
|
304
|
-
- **[readmenator.mp4](./readmenator.mp4)** -- Cinematic synthwave overview video
|
|
305
|
-
- **[.cursorrules](./.cursorrules)** -- Generated AI assistant rules
|
|
306
|
-
- **[readmenator_audit.sarif](./readmenator_audit.sarif)** -- SARIF v2.1.0 audit (when security scan is on)
|
|
307
|
-
|
|
308
|
-
Cache (`.readmenator_cache/`), packaged agent skills (`.claude/skills/`), and the
|
|
309
|
-
GitHub wiki mirror are also produced on `run`/`--rebuild`.
|
|
310
277
|
|
|
311
278
|
AI agents: Read `readmenator-wiki/index.md` first for the big picture, then `readmenator-agent/INDEX.md` for grep-friendly lookup.
|
|
312
|
-
Developers: Read `KNOWLEDGE_BASE.md` for full architecture reference
|
|
279
|
+
Developers: Read `KNOWLEDGE_BASE.md` for full architecture reference.
|
|
313
280
|
<!-- /readmenator-kb-link -->
|
|
314
281
|
|
|
@@ -226,55 +226,22 @@ Deployed with [deploynator](https://github.com/grisuno/deploynator)
|
|
|
226
226
|
## Knowledge Base
|
|
227
227
|
|
|
228
228
|
This project has been analyzed by [ReadMenator](https://github.com/grisuno/ReadMenator),
|
|
229
|
-
a zero-token polyglot static analysis tool.
|
|
230
|
-
`pip install readmenator && readmenator . --rebuild`. Analysis outputs:
|
|
229
|
+
a zero-token polyglot static analysis tool. Analysis outputs are available:
|
|
231
230
|
|
|
232
231
|
- **[KNOWLEDGE_BASE.md](./KNOWLEDGE_BASE.md)** -- Full architecture reference with all
|
|
233
232
|
classes, functions, imports, dependency graphs, UML class diagrams, security
|
|
234
|
-
audit findings, community analysis,
|
|
233
|
+
audit findings, community analysis, and more.
|
|
235
234
|
- **[readmenator-agent/](./readmenator-agent/)** -- Agent-friendly, grep-optimized index.
|
|
236
235
|
- `INDEX.md` -- Quick reference: what each file does
|
|
237
|
-
- `SYMBOLS.md` -- Every symbol with file:line
|
|
238
|
-
- `ARCHITECTURE.md` -- Internal dependency pairs + external imports
|
|
239
236
|
- `API.md` -- Public function contracts
|
|
240
|
-
- `GOTCHAS.md` -- Change warnings
|
|
241
|
-
- `SECURITY.md` -- Findings by severity
|
|
242
|
-
- `MEMORY.md` -- Rules, workflow, style, deliverables, session log
|
|
243
|
-
- `MANIFEST.json` -- Freshness (git_commit + source_fingerprint) + read order
|
|
244
|
-
- `KB_<subsystem>.md` -- Per-subsystem context
|
|
245
|
-
- `recipes/*.md` -- Grounded task guides (add-function, fix-cycle, ...)
|
|
237
|
+
- `GOTCHAS.md` -- Change warnings
|
|
238
|
+
- `SECURITY.md` -- Findings by severity
|
|
246
239
|
- **[readmenator-wiki/](./readmenator-wiki/)** -- Navigable wiki (start here for the big picture).
|
|
247
240
|
- `index.md` -- Entry point: overview, reading order, god nodes, connections
|
|
248
241
|
- `community_*.md` -- One synthesis page per code community
|
|
249
|
-
- `connections.json` -- Typed bridges between communities
|
|
250
|
-
- `queries.md` -- Suggested questions + answer log
|
|
251
242
|
- `REPORT.md` -- Honest audit: coverage, confidence, limits
|
|
252
|
-
- **[readmenator-graphrag/](./readmenator-graphrag/)** -- Zero-token GraphRAG index.
|
|
253
|
-
- `index.json` + `REPORTS.md` -- Query with `readmenator . ask "<question>"`
|
|
254
|
-
- **[readmenator-apidocs/](./readmenator-apidocs/)** -- API reference (Sphinx/Doxygen style).
|
|
255
|
-
- `index.md` + `modules/*.md` -- Signatures, parameters, returns, raises, examples, call graphs
|
|
256
|
-
- `architecture.md` -- Tiered architecture + execution flows (control flow, call tree, sequence)
|
|
257
|
-
- `classes.md`, `genindex.md`, `COVERAGE.md` -- Hierarchy, A-Z index, doc coverage + warnings
|
|
258
|
-
- `index.html` + `api/` -- Offline searchable browser and machine-readable model (`api/index.json`, `api/modules/*.jsonl` one entity per line)
|
|
259
|
-
- **[readmenator-maps/](./readmenator-maps/)** -- Interactive gallery (`index.html`).
|
|
260
|
-
- 5 system maps: architecture, workflow, sequence, dataflow, lifecycle
|
|
261
|
-
- `graph-force.html` -- Force-graph explorer (2D/3D, reach, paths, lenses)
|
|
262
|
-
- `graph-bundle.html` -- Edge bundle explorer (circle + sphere)
|
|
263
|
-
- `components.html` -- Component diagrams
|
|
264
|
-
- **[docs/](./docs/)** -- Static site (GitHub Pages ready).
|
|
265
|
-
- Gallery copy + `llms.txt` (agent entry point) + `md/` docs + video
|
|
266
|
-
- **[readmenator-diagrams/](./readmenator-diagrams/)** -- Component diagrams.
|
|
267
|
-
- `analysis.json` + `graph LR` markdown + per-component pages + `index.html`
|
|
268
|
-
- **[readmenator-rules/](./readmenator-rules/)** -- Suggested Semgrep rules (`*.yml`),
|
|
269
|
-
`yara_rules.yar` (YARA-lite) + `exclusions.yaml` (false-positive blocklist)
|
|
270
|
-
- **[readmenator.mp4](./readmenator.mp4)** -- Cinematic synthwave overview video
|
|
271
|
-
- **[.cursorrules](./.cursorrules)** -- Generated AI assistant rules
|
|
272
|
-
- **[readmenator_audit.sarif](./readmenator_audit.sarif)** -- SARIF v2.1.0 audit (when security scan is on)
|
|
273
|
-
|
|
274
|
-
Cache (`.readmenator_cache/`), packaged agent skills (`.claude/skills/`), and the
|
|
275
|
-
GitHub wiki mirror are also produced on `run`/`--rebuild`.
|
|
276
243
|
|
|
277
244
|
AI agents: Read `readmenator-wiki/index.md` first for the big picture, then `readmenator-agent/INDEX.md` for grep-friendly lookup.
|
|
278
|
-
Developers: Read `KNOWLEDGE_BASE.md` for full architecture reference
|
|
245
|
+
Developers: Read `KNOWLEDGE_BASE.md` for full architecture reference.
|
|
279
246
|
<!-- /readmenator-kb-link -->
|
|
280
247
|
|
|
@@ -128,6 +128,8 @@ ensure_pkg() {
|
|
|
128
128
|
|
|
129
129
|
ensure_pkg deploynator deploynator "git+https://github.com/grisuno/deploynator.git"
|
|
130
130
|
ensure_pkg readmenator readmenator readmenator
|
|
131
|
+
ensure_pkg build build build
|
|
132
|
+
ensure_pkg twine twine twine
|
|
131
133
|
|
|
132
134
|
if ! python3 -c "import deploynator" 2>/dev/null; then
|
|
133
135
|
echo "[!] deploynator not available and install failed (offline?)." >&2
|
|
@@ -195,8 +197,35 @@ version:
|
|
|
195
197
|
|
|
196
198
|
toml_path = os.path.join(base, "deploynator.toml")
|
|
197
199
|
if not os.path.isfile(toml_path):
|
|
200
|
+
from deploynator._toml import detect_test_commands, _has_python_tests
|
|
201
|
+
inferred: list[str] | None = None
|
|
202
|
+
try:
|
|
203
|
+
detected = detect_test_commands(base)
|
|
204
|
+
if detected is not None and not (
|
|
205
|
+
detected != ["make test-host"]
|
|
206
|
+
and detected[0].startswith("make")
|
|
207
|
+
and _has_python_tests(base)
|
|
208
|
+
):
|
|
209
|
+
inferred = detected
|
|
210
|
+
except ValueError:
|
|
211
|
+
inferred = None
|
|
212
|
+
if inferred is not None:
|
|
213
|
+
commands_repr = "[" + ", ".join(f'"{c}"' for c in inferred) + "]"
|
|
214
|
+
timeout = 900
|
|
215
|
+
else:
|
|
216
|
+
commands_repr = '["python -m pytest tests/ -q"]'
|
|
217
|
+
timeout = 300
|
|
218
|
+
content = self.DEPLOY_TOML_TEMPLATE.format(name=project_name)
|
|
219
|
+
content = content.replace(
|
|
220
|
+
'commands = ["python -m pytest tests/ -q"]',
|
|
221
|
+
f"commands = {commands_repr}",
|
|
222
|
+
).replace(
|
|
223
|
+
"timeout_s = 300",
|
|
224
|
+
f"timeout_s = {timeout}",
|
|
225
|
+
1,
|
|
226
|
+
)
|
|
198
227
|
with open(toml_path, "w") as f:
|
|
199
|
-
f.write(
|
|
228
|
+
f.write(content)
|
|
200
229
|
created.append(toml_path)
|
|
201
230
|
|
|
202
231
|
script_path = os.path.join(base, "deploy.sh")
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from deploynator._config import Config
|
|
7
|
+
|
|
8
|
+
TOML_FILENAME = "deploynator.toml"
|
|
9
|
+
DEFAULT_TEST_COMMANDS: list[str] = ["python -m pytest tests/ -q"]
|
|
10
|
+
DEFAULT_DOCS_COMMAND = "python -m readmenator . --rebuild"
|
|
11
|
+
|
|
12
|
+
# Auto-detección C: nombres de Makefile y targets preferidos.
|
|
13
|
+
# `test-host` va primero porque en proyectos como miniOS `test`
|
|
14
|
+
# requiere QEMU/imagen (lento), mientras `test-host` corre las
|
|
15
|
+
# suites unitarias rápidas en el host (contrato CI).
|
|
16
|
+
MAKEFILE_NAMES: tuple[str, ...] = ("Makefile", "makefile", "GNUmakefile")
|
|
17
|
+
MAKE_TEST_PRIORITY: list[str] = ["test-host", "test", "check", "tests"]
|
|
18
|
+
C_TEST_TIMEOUT_S: int = 900
|
|
19
|
+
|
|
20
|
+
_TARGET_RE = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _target_regex(): # type: ignore[no-untyped-def]
|
|
24
|
+
global _TARGET_RE
|
|
25
|
+
if _TARGET_RE is None:
|
|
26
|
+
import re
|
|
27
|
+
_TARGET_RE = re.compile(r"^([A-Za-z0-9_][A-Za-z0-9_.\-]*)\s*:(?!=)")
|
|
28
|
+
return _TARGET_RE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list_make_targets(project_path: str) -> list[str]:
|
|
32
|
+
"""Lee el Makefile y devuelve sus targets sin ejecutar nada.
|
|
33
|
+
|
|
34
|
+
Seguro: rechaza symlinks, limita tamaño (Config.max_file_size_bytes).
|
|
35
|
+
"""
|
|
36
|
+
makefile = ""
|
|
37
|
+
for name in MAKEFILE_NAMES:
|
|
38
|
+
candidate = os.path.join(project_path, name)
|
|
39
|
+
if os.path.islink(candidate):
|
|
40
|
+
raise ValueError(f"Symlink rejected: {candidate}")
|
|
41
|
+
if os.path.isfile(candidate):
|
|
42
|
+
makefile = candidate
|
|
43
|
+
break
|
|
44
|
+
if not makefile:
|
|
45
|
+
return []
|
|
46
|
+
if os.path.getsize(makefile) > Config.create().max_file_size_bytes:
|
|
47
|
+
raise ValueError(f"File too large: {makefile}")
|
|
48
|
+
targets: list[str] = []
|
|
49
|
+
try:
|
|
50
|
+
with open(makefile, "r", errors="replace") as f:
|
|
51
|
+
for line in f:
|
|
52
|
+
# Solo targets al inicio de línea (sin indentación = no receta).
|
|
53
|
+
if line[:1] in (" ", "\t"):
|
|
54
|
+
continue
|
|
55
|
+
m = _target_regex().match(line.strip())
|
|
56
|
+
if m and m.group(1) not in targets:
|
|
57
|
+
if not m.group(1).startswith("."):
|
|
58
|
+
targets.append(m.group(1))
|
|
59
|
+
except OSError as exc:
|
|
60
|
+
raise ValueError(f"Cannot read Makefile: {exc}") from exc
|
|
61
|
+
return targets
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _has_python_tests(project_path: str) -> bool:
|
|
65
|
+
tests_dir = os.path.join(project_path, "tests")
|
|
66
|
+
if not os.path.isdir(tests_dir):
|
|
67
|
+
return False
|
|
68
|
+
try:
|
|
69
|
+
for entry in os.listdir(tests_dir):
|
|
70
|
+
if entry.endswith(".py"):
|
|
71
|
+
return True
|
|
72
|
+
except OSError:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def looks_like_packaging_project(project_path: str) -> bool:
|
|
77
|
+
"""Inferencia runtime (multi-proyecto): ¿es un paquete PyPI?
|
|
78
|
+
|
|
79
|
+
Espejo liviano de PyPIRunner.is_packaging_project sin importar
|
|
80
|
+
_pypi (evita ciclo). El gate final sigue en PyPIRunner al
|
|
81
|
+
desplegar; aquí solo se usa para el default de pypi_enabled.
|
|
82
|
+
"""
|
|
83
|
+
pyproject = os.path.join(project_path, "pyproject.toml")
|
|
84
|
+
if os.path.isfile(pyproject) and not os.path.islink(pyproject):
|
|
85
|
+
try:
|
|
86
|
+
import tomllib
|
|
87
|
+
with open(pyproject, "rb") as f:
|
|
88
|
+
data = tomllib.load(f)
|
|
89
|
+
project = data.get("project", {})
|
|
90
|
+
if isinstance(project, dict) and project.get("name"):
|
|
91
|
+
return True
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
for candidate in ("setup.py", "setup.cfg"):
|
|
95
|
+
candidate_path = os.path.join(project_path, candidate)
|
|
96
|
+
if os.path.isfile(candidate_path) and not os.path.islink(candidate_path):
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def detect_test_commands(project_path: str) -> list[str] | None:
|
|
103
|
+
"""Detecta comandos de test para proyectos C/autotools sin toml.
|
|
104
|
+
|
|
105
|
+
Runtime multi-proyecto, sin hardcodear nombres de proyecto:
|
|
106
|
+
Makefile (test-host canónico solo; si no, TODOS los targets de
|
|
107
|
+
test en una sola invocación `make test test-* ...`, p. ej.
|
|
108
|
+
`make test test-dsp test-transport`) → CMake (build+ctest) →
|
|
109
|
+
meson (setup+test).
|
|
110
|
+
Devuelve None si no hay nada detectable (caller usa pytest default).
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
targets = list_make_targets(project_path)
|
|
114
|
+
except ValueError:
|
|
115
|
+
targets = []
|
|
116
|
+
if targets:
|
|
117
|
+
# `test-host` es la suite canónica (drivers internos propios):
|
|
118
|
+
# va sola, manda sobre todo (incluso sobre tests/*.py).
|
|
119
|
+
if "test-host" in targets:
|
|
120
|
+
return ["make test-host"]
|
|
121
|
+
combined: list[str] = []
|
|
122
|
+
for preferred in MAKE_TEST_PRIORITY[1:]:
|
|
123
|
+
# `tests` desnudo suele ser helper mkdir (dependencia order-only
|
|
124
|
+
# `| tests`), no runner: se excluye si hay otro target de test.
|
|
125
|
+
if preferred == "tests" and any(
|
|
126
|
+
t in targets for t in ("test", "check")
|
|
127
|
+
):
|
|
128
|
+
continue
|
|
129
|
+
if preferred in targets and preferred not in combined:
|
|
130
|
+
combined.append(preferred)
|
|
131
|
+
for extra in sorted(t for t in targets
|
|
132
|
+
if (t.startswith("test-") or t.startswith("test_"))
|
|
133
|
+
and t not in combined):
|
|
134
|
+
combined.append(extra)
|
|
135
|
+
if combined:
|
|
136
|
+
return [f"make {' '.join(combined)}"]
|
|
137
|
+
# Makefile sin target de test: al menos verifica que compila.
|
|
138
|
+
if "all" in targets:
|
|
139
|
+
return ["make"]
|
|
140
|
+
return ["make"]
|
|
141
|
+
if os.path.isfile(os.path.join(project_path, "CMakeLists.txt")):
|
|
142
|
+
return [
|
|
143
|
+
"cmake -S . -B build",
|
|
144
|
+
"cmake --build build",
|
|
145
|
+
"ctest --test-dir build",
|
|
146
|
+
]
|
|
147
|
+
if os.path.isfile(os.path.join(project_path, "meson.build")):
|
|
148
|
+
return ["meson setup build", "meson test -C build"]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _auto_toml_config(project_path: str) -> TomlConfig | None:
|
|
153
|
+
"""Config auto para proyectos sin deploynator.toml (C primero).
|
|
154
|
+
|
|
155
|
+
Multi-proyecto: pypi_enabled se infiere al vuelo de los ficheros
|
|
156
|
+
de packaging (pyproject/setup.py/setup.cfg); el gate final sigue
|
|
157
|
+
en PyPIRunner.is_packaging_project al desplegar.
|
|
158
|
+
"""
|
|
159
|
+
detected = detect_test_commands(project_path)
|
|
160
|
+
if detected is None:
|
|
161
|
+
return None
|
|
162
|
+
# `make test-host` es la suite canónica C (incluye sus propios
|
|
163
|
+
# drivers python internos): manda incluso si hay tests/*.py.
|
|
164
|
+
# Para el resto, si hay tests Python reales se conserva pytest
|
|
165
|
+
# (evita romper proyectos Python con Makefile auxiliar).
|
|
166
|
+
if detected != ["make test-host"] and detected[0].startswith("make") and _has_python_tests(project_path):
|
|
167
|
+
return None
|
|
168
|
+
name = os.path.basename(os.path.abspath(project_path))
|
|
169
|
+
return TomlConfig(
|
|
170
|
+
project_name=name,
|
|
171
|
+
test_commands=detected,
|
|
172
|
+
test_timeout_s=C_TEST_TIMEOUT_S,
|
|
173
|
+
docs_auto=True,
|
|
174
|
+
pypi_enabled=looks_like_packaging_project(project_path),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True)
|
|
179
|
+
class TomlConfig:
|
|
180
|
+
project_name: str = ""
|
|
181
|
+
version_file: str = "version.json"
|
|
182
|
+
test_commands: list[str] = None # type: ignore[assignment]
|
|
183
|
+
test_timeout_s: int = 300
|
|
184
|
+
allow_no_tests: bool = False
|
|
185
|
+
docs_auto: bool = True
|
|
186
|
+
docs_command: str = DEFAULT_DOCS_COMMAND
|
|
187
|
+
docs_timeout_s: int = 300
|
|
188
|
+
deploy_branch: str = "main"
|
|
189
|
+
deploy_remote: str = "origin"
|
|
190
|
+
pypi_enabled: bool = True
|
|
191
|
+
pypi_repo: str = "pypi"
|
|
192
|
+
pypi_clean_dist: bool = True
|
|
193
|
+
pypi_timeout_s: int = 600
|
|
194
|
+
|
|
195
|
+
def __post_init__(self) -> None:
|
|
196
|
+
if self.test_commands is None:
|
|
197
|
+
object.__setattr__(self, "test_commands", list(DEFAULT_TEST_COMMANDS))
|
|
198
|
+
object.__setattr__(self, "test_timeout_s", _clamp_timeout(self.test_timeout_s))
|
|
199
|
+
object.__setattr__(self, "docs_timeout_s", _clamp_timeout(self.docs_timeout_s))
|
|
200
|
+
object.__setattr__(self, "pypi_timeout_s", _clamp_timeout(self.pypi_timeout_s))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _clamp_timeout(value: int) -> int:
|
|
204
|
+
try:
|
|
205
|
+
v = int(value)
|
|
206
|
+
except (TypeError, ValueError):
|
|
207
|
+
return 300
|
|
208
|
+
return max(1, min(3600, v))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def load_toml_config(project_path: str) -> TomlConfig:
|
|
212
|
+
toml_path = os.path.join(project_path, TOML_FILENAME)
|
|
213
|
+
if os.path.islink(toml_path):
|
|
214
|
+
raise ValueError(f"Symlink rejected: {toml_path}")
|
|
215
|
+
if not os.path.isfile(toml_path):
|
|
216
|
+
auto = _auto_toml_config(project_path)
|
|
217
|
+
if auto is not None:
|
|
218
|
+
return auto
|
|
219
|
+
return TomlConfig(project_name=os.path.basename(os.path.abspath(project_path)))
|
|
220
|
+
if os.path.getsize(toml_path) > Config.create().max_file_size_bytes:
|
|
221
|
+
raise ValueError(f"File too large: {toml_path}")
|
|
222
|
+
try:
|
|
223
|
+
import tomllib
|
|
224
|
+
except ImportError as exc:
|
|
225
|
+
raise ValueError(f"tomllib unavailable: {exc}") from exc
|
|
226
|
+
try:
|
|
227
|
+
with open(toml_path, "rb") as f:
|
|
228
|
+
data = tomllib.load(f)
|
|
229
|
+
except Exception as exc:
|
|
230
|
+
raise ValueError(f"Invalid {TOML_FILENAME}: {exc}") from exc
|
|
231
|
+
if not isinstance(data, dict):
|
|
232
|
+
raise ValueError(f"Invalid {TOML_FILENAME}: root must be a table")
|
|
233
|
+
|
|
234
|
+
project_raw = data.get("project", {}) or {}
|
|
235
|
+
tests_raw = data.get("tests", {}) or {}
|
|
236
|
+
docs_raw = data.get("docs", {}) or {}
|
|
237
|
+
deploy_raw = data.get("deploy", {}) or {}
|
|
238
|
+
pypi_raw = data.get("pypi", {}) or {}
|
|
239
|
+
|
|
240
|
+
if not isinstance(project_raw, dict) or not isinstance(tests_raw, dict):
|
|
241
|
+
raise ValueError(f"Invalid {TOML_FILENAME}: bad sections")
|
|
242
|
+
if not isinstance(docs_raw, dict) or not isinstance(deploy_raw, dict):
|
|
243
|
+
raise ValueError(f"Invalid {TOML_FILENAME}: bad sections")
|
|
244
|
+
if not isinstance(pypi_raw, dict):
|
|
245
|
+
raise ValueError(f"Invalid {TOML_FILENAME}: bad sections")
|
|
246
|
+
|
|
247
|
+
_sentinel: list[str] = [] # type: ignore[assignment]
|
|
248
|
+
commands = tests_raw.get("commands", _sentinel)
|
|
249
|
+
if commands is _sentinel:
|
|
250
|
+
# Sin [tests]commands explícito: auto-detect C, si no pytest.
|
|
251
|
+
detected = detect_test_commands(project_path)
|
|
252
|
+
if detected is not None and not (
|
|
253
|
+
detected != ["make test-host"]
|
|
254
|
+
and detected[0].startswith("make")
|
|
255
|
+
and _has_python_tests(project_path)
|
|
256
|
+
):
|
|
257
|
+
commands = detected
|
|
258
|
+
default_timeout = C_TEST_TIMEOUT_S
|
|
259
|
+
else:
|
|
260
|
+
commands = list(DEFAULT_TEST_COMMANDS)
|
|
261
|
+
default_timeout = 300
|
|
262
|
+
elif commands is None:
|
|
263
|
+
commands = list(DEFAULT_TEST_COMMANDS)
|
|
264
|
+
default_timeout = 300
|
|
265
|
+
else:
|
|
266
|
+
default_timeout = 300
|
|
267
|
+
if not isinstance(commands, list) or not all(isinstance(c, str) for c in commands):
|
|
268
|
+
raise ValueError(f"Invalid {TOML_FILENAME}: [tests] commands must be list[str]")
|
|
269
|
+
|
|
270
|
+
raw_timeout = tests_raw.get("timeout_s", default_timeout)
|
|
271
|
+
|
|
272
|
+
return TomlConfig(
|
|
273
|
+
project_name=str(project_raw.get("name", "") or os.path.basename(os.path.abspath(project_path))),
|
|
274
|
+
version_file=str(project_raw.get("version_file", "version.json")),
|
|
275
|
+
test_commands=list(commands),
|
|
276
|
+
test_timeout_s=_clamp_timeout(raw_timeout),
|
|
277
|
+
allow_no_tests=bool(tests_raw.get("allow_no_tests", False)),
|
|
278
|
+
docs_auto=bool(docs_raw.get("auto", True)),
|
|
279
|
+
docs_command=str(docs_raw.get("command", DEFAULT_DOCS_COMMAND)),
|
|
280
|
+
docs_timeout_s=_clamp_timeout(docs_raw.get("timeout_s", 300)),
|
|
281
|
+
deploy_branch=str(deploy_raw.get("branch", "main")),
|
|
282
|
+
deploy_remote=str(deploy_raw.get("remote", "origin")),
|
|
283
|
+
pypi_enabled=bool(pypi_raw.get("enabled", True)),
|
|
284
|
+
pypi_repo=str(pypi_raw.get("repo", "pypi")),
|
|
285
|
+
pypi_clean_dist=bool(pypi_raw.get("clean_dist", True)),
|
|
286
|
+
pypi_timeout_s=_clamp_timeout(pypi_raw.get("timeout_s", 600)),
|
|
287
|
+
)
|
|
@@ -9,6 +9,10 @@ version_file = "version.json"
|
|
|
9
9
|
[tests]
|
|
10
10
|
# Comandos en orden. Se ejecutan con shell=False (shlex), cwd=project_path.
|
|
11
11
|
# Default si falta el archivo: ["python -m pytest tests/ -q"]
|
|
12
|
+
# Proyectos C sin toml: auto-detecta el Makefile (test-host > test > check,
|
|
13
|
+
# o primer test-*; si no hay target de test usa `make`). CMake/meson
|
|
14
|
+
# también se detectan. Ejemplo C explícito:
|
|
15
|
+
# commands = ["make test-host"]
|
|
12
16
|
commands = ["python -m pytest tests/ -q"]
|
|
13
17
|
timeout_s = 300
|
|
14
18
|
# Si true y commands=[] → pasa sin tests. Si false y sin tests/ → falla.
|
|
@@ -101,6 +101,20 @@ class TestTestRunner:
|
|
|
101
101
|
ok, out = r.run(tmp, _toml([""]))
|
|
102
102
|
assert ok is False
|
|
103
103
|
|
|
104
|
+
def test_make_target_runs(self):
|
|
105
|
+
from deploynator._runner import TestRunner
|
|
106
|
+
from deploynator._config import Config
|
|
107
|
+
import shutil
|
|
108
|
+
if shutil.which("make") is None:
|
|
109
|
+
pytest.skip("make not available")
|
|
110
|
+
r = TestRunner(Config.create())
|
|
111
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
112
|
+
with open(os.path.join(tmp, "Makefile"), "w") as f:
|
|
113
|
+
f.write("test-host:\n\techo host-ok\n")
|
|
114
|
+
ok, out = r.run(tmp, _toml(["make test-host"]))
|
|
115
|
+
assert ok is True
|
|
116
|
+
assert "host-ok" in out
|
|
117
|
+
|
|
104
118
|
|
|
105
119
|
class TestDocsRunner:
|
|
106
120
|
def test_passing_docs(self):
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""RED: TomlConfig contract — deploynator.toml per-project."""
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestTomlConfig:
|
|
8
|
+
def test_load_missing_returns_defaults(self):
|
|
9
|
+
from deploynator._toml import load_toml_config
|
|
10
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
11
|
+
cfg = load_toml_config(tmp)
|
|
12
|
+
assert cfg.project_name == os.path.basename(tmp) or cfg.project_name == ""
|
|
13
|
+
assert cfg.test_commands == ["python -m pytest tests/ -q"]
|
|
14
|
+
assert cfg.test_timeout_s == 300
|
|
15
|
+
assert cfg.docs_auto is True
|
|
16
|
+
assert "readmenator" in cfg.docs_command
|
|
17
|
+
|
|
18
|
+
def test_load_valid_toml(self):
|
|
19
|
+
from deploynator._toml import load_toml_config
|
|
20
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
21
|
+
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
22
|
+
f.write(
|
|
23
|
+
'[project]\nname = "my-app"\nversion_file = "VERSION"\n\n'
|
|
24
|
+
'[tests]\ncommands = ["pytest -q", "ruff check ."]\ntimeout_s = 60\n'
|
|
25
|
+
'[docs]\nauto = false\n'
|
|
26
|
+
'[deploy]\nbranch = "develop"\nremote = "upstream"\n'
|
|
27
|
+
)
|
|
28
|
+
cfg = load_toml_config(tmp)
|
|
29
|
+
assert cfg.project_name == "my-app"
|
|
30
|
+
assert cfg.version_file == "VERSION"
|
|
31
|
+
assert cfg.test_commands == ["pytest -q", "ruff check ."]
|
|
32
|
+
assert cfg.test_timeout_s == 60
|
|
33
|
+
assert cfg.docs_auto is False
|
|
34
|
+
assert cfg.deploy_branch == "develop"
|
|
35
|
+
assert cfg.deploy_remote == "upstream"
|
|
36
|
+
|
|
37
|
+
def test_symlink_rejected(self):
|
|
38
|
+
from deploynator._toml import load_toml_config
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
40
|
+
real = os.path.join(tmp, "real.toml")
|
|
41
|
+
with open(real, "w") as f:
|
|
42
|
+
f.write('[project]\nname="x"\n')
|
|
43
|
+
link = os.path.join(tmp, "deploynator.toml")
|
|
44
|
+
os.symlink(real, link)
|
|
45
|
+
with pytest.raises(ValueError, match="Symlink"):
|
|
46
|
+
load_toml_config(tmp)
|
|
47
|
+
|
|
48
|
+
def test_oversize_rejected(self):
|
|
49
|
+
from deploynator._toml import load_toml_config
|
|
50
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
51
|
+
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
52
|
+
f.write("#" * (10_485_760 + 1))
|
|
53
|
+
with pytest.raises(ValueError, match="too large"):
|
|
54
|
+
load_toml_config(tmp)
|
|
55
|
+
|
|
56
|
+
def test_malformed_toml_raises(self):
|
|
57
|
+
from deploynator._toml import load_toml_config
|
|
58
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
59
|
+
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
60
|
+
f.write("[tests\ncommands = [[[invalid")
|
|
61
|
+
with pytest.raises(ValueError):
|
|
62
|
+
load_toml_config(tmp)
|
|
63
|
+
|
|
64
|
+
def test_timeout_clamped(self):
|
|
65
|
+
from deploynator._toml import load_toml_config
|
|
66
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
67
|
+
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
68
|
+
f.write("[tests]\ntimeout_s = 999999\n")
|
|
69
|
+
cfg = load_toml_config(tmp)
|
|
70
|
+
assert cfg.test_timeout_s <= 3600
|
|
71
|
+
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
72
|
+
f.write("[tests]\ntimeout_s = 0\n")
|
|
73
|
+
cfg = load_toml_config(tmp)
|
|
74
|
+
assert cfg.test_timeout_s >= 1
|
|
75
|
+
|
|
76
|
+
def test_frozen(self):
|
|
77
|
+
from deploynator._toml import load_toml_config
|
|
78
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
79
|
+
cfg = load_toml_config(tmp)
|
|
80
|
+
with pytest.raises(Exception):
|
|
81
|
+
cfg.project_name = "mutated"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestCMakefileDetection:
|
|
85
|
+
def _mk(self, tmp, content="all:\n\techo hi\ntest-host:\n\techo host\ntest:\n\techo t\n"):
|
|
86
|
+
with open(os.path.join(tmp, "Makefile"), "w") as f:
|
|
87
|
+
f.write(content)
|
|
88
|
+
|
|
89
|
+
def test_list_targets_ignores_recipes(self):
|
|
90
|
+
from deploynator._toml import list_make_targets
|
|
91
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
92
|
+
self._mk(tmp, "all:\n\techo hi\ntest-host:\n\techo host\n")
|
|
93
|
+
targets = list_make_targets(tmp)
|
|
94
|
+
assert "all" in targets
|
|
95
|
+
assert "test-host" in targets
|
|
96
|
+
assert "echo hi" not in targets
|
|
97
|
+
|
|
98
|
+
def test_detect_prefers_test_host(self):
|
|
99
|
+
from deploynator._toml import detect_test_commands
|
|
100
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
101
|
+
self._mk(tmp)
|
|
102
|
+
assert detect_test_commands(tmp) == ["make test-host"]
|
|
103
|
+
|
|
104
|
+
def test_detect_falls_back_to_test(self):
|
|
105
|
+
from deploynator._toml import detect_test_commands
|
|
106
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
107
|
+
self._mk(tmp, "all:\n\techo hi\ntest:\n\techo t\n")
|
|
108
|
+
assert detect_test_commands(tmp) == ["make test"]
|
|
109
|
+
|
|
110
|
+
def test_detect_falls_back_to_check(self):
|
|
111
|
+
from deploynator._toml import detect_test_commands
|
|
112
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
113
|
+
self._mk(tmp, "all:\n\techo hi\ncheck:\n\techo c\n")
|
|
114
|
+
assert detect_test_commands(tmp) == ["make check"]
|
|
115
|
+
|
|
116
|
+
def test_detect_cmake(self):
|
|
117
|
+
from deploynator._toml import detect_test_commands
|
|
118
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
119
|
+
with open(os.path.join(tmp, "CMakeLists.txt"), "w") as f:
|
|
120
|
+
f.write("cmake_minimum_required(VERSION 3.10)\n")
|
|
121
|
+
cmds = detect_test_commands(tmp)
|
|
122
|
+
assert cmds is not None and any("ctest" in c for c in cmds)
|
|
123
|
+
|
|
124
|
+
def test_load_missing_makefile_uses_make(self):
|
|
125
|
+
from deploynator._toml import load_toml_config
|
|
126
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
127
|
+
self._mk(tmp, "test-host:\n\techo host\n")
|
|
128
|
+
cfg = load_toml_config(tmp)
|
|
129
|
+
assert cfg.test_commands == ["make test-host"]
|
|
130
|
+
assert cfg.test_timeout_s == 900
|
|
131
|
+
assert cfg.pypi_enabled is False
|
|
132
|
+
|
|
133
|
+
def test_python_with_aux_makefile_keeps_pytest(self):
|
|
134
|
+
from deploynator._toml import load_toml_config
|
|
135
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
136
|
+
self._mk(tmp, "test:\n\techo t\n")
|
|
137
|
+
os.makedirs(os.path.join(tmp, "tests"))
|
|
138
|
+
with open(os.path.join(tmp, "tests", "test_x.py"), "w") as f:
|
|
139
|
+
f.write("def test_x(): pass\n")
|
|
140
|
+
cfg = load_toml_config(tmp)
|
|
141
|
+
assert cfg.test_commands == ["python -m pytest tests/ -q"]
|
|
142
|
+
|
|
143
|
+
def test_test_host_wins_over_python_tests(self):
|
|
144
|
+
from deploynator._toml import load_toml_config
|
|
145
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
146
|
+
self._mk(tmp, "test-host:\n\techo host\ntest:\n\techo t\n")
|
|
147
|
+
os.makedirs(os.path.join(tmp, "tests"))
|
|
148
|
+
with open(os.path.join(tmp, "tests", "test_x.py"), "w") as f:
|
|
149
|
+
f.write("def test_x(): pass\n")
|
|
150
|
+
cfg = load_toml_config(tmp)
|
|
151
|
+
assert cfg.test_commands == ["make test-host"]
|
|
152
|
+
|
|
153
|
+
def test_makefile_symlink_rejected(self):
|
|
154
|
+
from deploynator._toml import list_make_targets
|
|
155
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
156
|
+
real = os.path.join(tmp, "RealMakefile")
|
|
157
|
+
with open(real, "w") as f:
|
|
158
|
+
f.write("test:\n\techo t\n")
|
|
159
|
+
os.symlink(real, os.path.join(tmp, "Makefile"))
|
|
160
|
+
with pytest.raises(ValueError, match="Symlink"):
|
|
161
|
+
list_make_targets(tmp)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestMultiTargetAndPyPIInference:
|
|
165
|
+
"""Runtime multi-proyecto: todos los targets test en una invocación
|
|
166
|
+
y pypi_enabled inferido de los ficheros de packaging."""
|
|
167
|
+
|
|
168
|
+
def _mk(self, tmp, content):
|
|
169
|
+
with open(os.path.join(tmp, "Makefile"), "w") as f:
|
|
170
|
+
f.write(content)
|
|
171
|
+
|
|
172
|
+
def test_detect_combines_test_targets(self):
|
|
173
|
+
from deploynator._toml import detect_test_commands
|
|
174
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
175
|
+
self._mk(tmp, "test:\n\techo t\ntest-dsp:\n\techo d\n"
|
|
176
|
+
"test-transport:\n\techo r\ntests:\n\tmkdir -p tests\n")
|
|
177
|
+
assert detect_test_commands(tmp) == ["make test test-dsp test-transport"]
|
|
178
|
+
|
|
179
|
+
def test_detect_test_host_alone_wins(self):
|
|
180
|
+
from deploynator._toml import detect_test_commands
|
|
181
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
182
|
+
self._mk(tmp, "test-host:\n\techo h\ntest:\n\techo t\n"
|
|
183
|
+
"test-dsp:\n\techo d\n")
|
|
184
|
+
assert detect_test_commands(tmp) == ["make test-host"]
|
|
185
|
+
|
|
186
|
+
def test_detect_bare_tests_alone_kept(self):
|
|
187
|
+
from deploynator._toml import detect_test_commands
|
|
188
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
189
|
+
self._mk(tmp, "tests:\n\techo t\n")
|
|
190
|
+
assert detect_test_commands(tmp) == ["make tests"]
|
|
191
|
+
|
|
192
|
+
def test_auto_pypi_enabled_for_packaging(self):
|
|
193
|
+
from deploynator._toml import load_toml_config
|
|
194
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
195
|
+
self._mk(tmp, "test:\n\techo t\n")
|
|
196
|
+
with open(os.path.join(tmp, "pyproject.toml"), "w") as f:
|
|
197
|
+
f.write('[project]\nname = "x"\nversion = "0.1.0"\n')
|
|
198
|
+
cfg = load_toml_config(tmp)
|
|
199
|
+
assert cfg.test_commands == ["make test"]
|
|
200
|
+
assert cfg.pypi_enabled is True
|
|
201
|
+
|
|
202
|
+
def test_auto_pypi_disabled_for_plain_c(self):
|
|
203
|
+
from deploynator._toml import load_toml_config
|
|
204
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
205
|
+
self._mk(tmp, "test:\n\techo t\n")
|
|
206
|
+
cfg = load_toml_config(tmp)
|
|
207
|
+
assert cfg.test_commands == ["make test"]
|
|
208
|
+
assert cfg.pypi_enabled is False
|
|
209
|
+
|
|
210
|
+
def test_looks_like_packaging_setup_py(self):
|
|
211
|
+
from deploynator._toml import looks_like_packaging_project
|
|
212
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
213
|
+
assert looks_like_packaging_project(tmp) is False
|
|
214
|
+
with open(os.path.join(tmp, "setup.py"), "w") as f:
|
|
215
|
+
f.write("from setuptools import setup\nsetup(name='x')\n")
|
|
216
|
+
assert looks_like_packaging_project(tmp) is True
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
|
|
6
|
-
from deploynator._config import Config
|
|
7
|
-
|
|
8
|
-
TOML_FILENAME = "deploynator.toml"
|
|
9
|
-
DEFAULT_TEST_COMMANDS: list[str] = ["python -m pytest tests/ -q"]
|
|
10
|
-
DEFAULT_DOCS_COMMAND = "python -m readmenator . --rebuild"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass(frozen=True)
|
|
14
|
-
class TomlConfig:
|
|
15
|
-
project_name: str = ""
|
|
16
|
-
version_file: str = "version.json"
|
|
17
|
-
test_commands: list[str] = None # type: ignore[assignment]
|
|
18
|
-
test_timeout_s: int = 300
|
|
19
|
-
allow_no_tests: bool = False
|
|
20
|
-
docs_auto: bool = True
|
|
21
|
-
docs_command: str = DEFAULT_DOCS_COMMAND
|
|
22
|
-
docs_timeout_s: int = 300
|
|
23
|
-
deploy_branch: str = "main"
|
|
24
|
-
deploy_remote: str = "origin"
|
|
25
|
-
pypi_enabled: bool = True
|
|
26
|
-
pypi_repo: str = "pypi"
|
|
27
|
-
pypi_clean_dist: bool = True
|
|
28
|
-
pypi_timeout_s: int = 600
|
|
29
|
-
|
|
30
|
-
def __post_init__(self) -> None:
|
|
31
|
-
if self.test_commands is None:
|
|
32
|
-
object.__setattr__(self, "test_commands", list(DEFAULT_TEST_COMMANDS))
|
|
33
|
-
object.__setattr__(self, "test_timeout_s", _clamp_timeout(self.test_timeout_s))
|
|
34
|
-
object.__setattr__(self, "docs_timeout_s", _clamp_timeout(self.docs_timeout_s))
|
|
35
|
-
object.__setattr__(self, "pypi_timeout_s", _clamp_timeout(self.pypi_timeout_s))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _clamp_timeout(value: int) -> int:
|
|
39
|
-
try:
|
|
40
|
-
v = int(value)
|
|
41
|
-
except (TypeError, ValueError):
|
|
42
|
-
return 300
|
|
43
|
-
return max(1, min(3600, v))
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def load_toml_config(project_path: str) -> TomlConfig:
|
|
47
|
-
toml_path = os.path.join(project_path, TOML_FILENAME)
|
|
48
|
-
if os.path.islink(toml_path):
|
|
49
|
-
raise ValueError(f"Symlink rejected: {toml_path}")
|
|
50
|
-
if not os.path.isfile(toml_path):
|
|
51
|
-
return TomlConfig(project_name=os.path.basename(os.path.abspath(project_path)))
|
|
52
|
-
if os.path.getsize(toml_path) > Config.create().max_file_size_bytes:
|
|
53
|
-
raise ValueError(f"File too large: {toml_path}")
|
|
54
|
-
try:
|
|
55
|
-
import tomllib
|
|
56
|
-
except ImportError as exc:
|
|
57
|
-
raise ValueError(f"tomllib unavailable: {exc}") from exc
|
|
58
|
-
try:
|
|
59
|
-
with open(toml_path, "rb") as f:
|
|
60
|
-
data = tomllib.load(f)
|
|
61
|
-
except Exception as exc:
|
|
62
|
-
raise ValueError(f"Invalid {TOML_FILENAME}: {exc}") from exc
|
|
63
|
-
if not isinstance(data, dict):
|
|
64
|
-
raise ValueError(f"Invalid {TOML_FILENAME}: root must be a table")
|
|
65
|
-
|
|
66
|
-
project_raw = data.get("project", {}) or {}
|
|
67
|
-
tests_raw = data.get("tests", {}) or {}
|
|
68
|
-
docs_raw = data.get("docs", {}) or {}
|
|
69
|
-
deploy_raw = data.get("deploy", {}) or {}
|
|
70
|
-
pypi_raw = data.get("pypi", {}) or {}
|
|
71
|
-
|
|
72
|
-
if not isinstance(project_raw, dict) or not isinstance(tests_raw, dict):
|
|
73
|
-
raise ValueError(f"Invalid {TOML_FILENAME}: bad sections")
|
|
74
|
-
if not isinstance(docs_raw, dict) or not isinstance(deploy_raw, dict):
|
|
75
|
-
raise ValueError(f"Invalid {TOML_FILENAME}: bad sections")
|
|
76
|
-
if not isinstance(pypi_raw, dict):
|
|
77
|
-
raise ValueError(f"Invalid {TOML_FILENAME}: bad sections")
|
|
78
|
-
|
|
79
|
-
commands = tests_raw.get("commands", list(DEFAULT_TEST_COMMANDS))
|
|
80
|
-
if commands is None:
|
|
81
|
-
commands = list(DEFAULT_TEST_COMMANDS)
|
|
82
|
-
if not isinstance(commands, list) or not all(isinstance(c, str) for c in commands):
|
|
83
|
-
raise ValueError(f"Invalid {TOML_FILENAME}: [tests] commands must be list[str]")
|
|
84
|
-
|
|
85
|
-
return TomlConfig(
|
|
86
|
-
project_name=str(project_raw.get("name", "") or os.path.basename(os.path.abspath(project_path))),
|
|
87
|
-
version_file=str(project_raw.get("version_file", "version.json")),
|
|
88
|
-
test_commands=list(commands),
|
|
89
|
-
test_timeout_s=_clamp_timeout(tests_raw.get("timeout_s", 300)),
|
|
90
|
-
allow_no_tests=bool(tests_raw.get("allow_no_tests", False)),
|
|
91
|
-
docs_auto=bool(docs_raw.get("auto", True)),
|
|
92
|
-
docs_command=str(docs_raw.get("command", DEFAULT_DOCS_COMMAND)),
|
|
93
|
-
docs_timeout_s=_clamp_timeout(docs_raw.get("timeout_s", 300)),
|
|
94
|
-
deploy_branch=str(deploy_raw.get("branch", "main")),
|
|
95
|
-
deploy_remote=str(deploy_raw.get("remote", "origin")),
|
|
96
|
-
pypi_enabled=bool(pypi_raw.get("enabled", True)),
|
|
97
|
-
pypi_repo=str(pypi_raw.get("repo", "pypi")),
|
|
98
|
-
pypi_clean_dist=bool(pypi_raw.get("clean_dist", True)),
|
|
99
|
-
pypi_timeout_s=_clamp_timeout(pypi_raw.get("timeout_s", 600)),
|
|
100
|
-
)
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"""RED: TomlConfig contract — deploynator.toml per-project."""
|
|
2
|
-
import os
|
|
3
|
-
import tempfile
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestTomlConfig:
|
|
8
|
-
def test_load_missing_returns_defaults(self):
|
|
9
|
-
from deploynator._toml import load_toml_config
|
|
10
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
11
|
-
cfg = load_toml_config(tmp)
|
|
12
|
-
assert cfg.project_name == os.path.basename(tmp) or cfg.project_name == ""
|
|
13
|
-
assert cfg.test_commands == ["python -m pytest tests/ -q"]
|
|
14
|
-
assert cfg.test_timeout_s == 300
|
|
15
|
-
assert cfg.docs_auto is True
|
|
16
|
-
assert "readmenator" in cfg.docs_command
|
|
17
|
-
|
|
18
|
-
def test_load_valid_toml(self):
|
|
19
|
-
from deploynator._toml import load_toml_config
|
|
20
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
21
|
-
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
22
|
-
f.write(
|
|
23
|
-
'[project]\nname = "my-app"\nversion_file = "VERSION"\n\n'
|
|
24
|
-
'[tests]\ncommands = ["pytest -q", "ruff check ."]\ntimeout_s = 60\n'
|
|
25
|
-
'[docs]\nauto = false\n'
|
|
26
|
-
'[deploy]\nbranch = "develop"\nremote = "upstream"\n'
|
|
27
|
-
)
|
|
28
|
-
cfg = load_toml_config(tmp)
|
|
29
|
-
assert cfg.project_name == "my-app"
|
|
30
|
-
assert cfg.version_file == "VERSION"
|
|
31
|
-
assert cfg.test_commands == ["pytest -q", "ruff check ."]
|
|
32
|
-
assert cfg.test_timeout_s == 60
|
|
33
|
-
assert cfg.docs_auto is False
|
|
34
|
-
assert cfg.deploy_branch == "develop"
|
|
35
|
-
assert cfg.deploy_remote == "upstream"
|
|
36
|
-
|
|
37
|
-
def test_symlink_rejected(self):
|
|
38
|
-
from deploynator._toml import load_toml_config
|
|
39
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
40
|
-
real = os.path.join(tmp, "real.toml")
|
|
41
|
-
with open(real, "w") as f:
|
|
42
|
-
f.write('[project]\nname="x"\n')
|
|
43
|
-
link = os.path.join(tmp, "deploynator.toml")
|
|
44
|
-
os.symlink(real, link)
|
|
45
|
-
with pytest.raises(ValueError, match="Symlink"):
|
|
46
|
-
load_toml_config(tmp)
|
|
47
|
-
|
|
48
|
-
def test_oversize_rejected(self):
|
|
49
|
-
from deploynator._toml import load_toml_config
|
|
50
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
51
|
-
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
52
|
-
f.write("#" * (10_485_760 + 1))
|
|
53
|
-
with pytest.raises(ValueError, match="too large"):
|
|
54
|
-
load_toml_config(tmp)
|
|
55
|
-
|
|
56
|
-
def test_malformed_toml_raises(self):
|
|
57
|
-
from deploynator._toml import load_toml_config
|
|
58
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
59
|
-
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
60
|
-
f.write("[tests\ncommands = [[[invalid")
|
|
61
|
-
with pytest.raises(ValueError):
|
|
62
|
-
load_toml_config(tmp)
|
|
63
|
-
|
|
64
|
-
def test_timeout_clamped(self):
|
|
65
|
-
from deploynator._toml import load_toml_config
|
|
66
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
67
|
-
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
68
|
-
f.write("[tests]\ntimeout_s = 999999\n")
|
|
69
|
-
cfg = load_toml_config(tmp)
|
|
70
|
-
assert cfg.test_timeout_s <= 3600
|
|
71
|
-
with open(os.path.join(tmp, "deploynator.toml"), "w") as f:
|
|
72
|
-
f.write("[tests]\ntimeout_s = 0\n")
|
|
73
|
-
cfg = load_toml_config(tmp)
|
|
74
|
-
assert cfg.test_timeout_s >= 1
|
|
75
|
-
|
|
76
|
-
def test_frozen(self):
|
|
77
|
-
from deploynator._toml import load_toml_config
|
|
78
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
79
|
-
cfg = load_toml_config(tmp)
|
|
80
|
-
with pytest.raises(Exception):
|
|
81
|
-
cfg.project_name = "mutated"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|