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.
Files changed (44) hide show
  1. {deploynator-1.3.0 → deploynator-1.3.3}/PKG-INFO +6 -39
  2. {deploynator-1.3.0 → deploynator-1.3.3}/README.md +5 -38
  3. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/__init__.py +1 -1
  4. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_app.py +30 -1
  5. deploynator-1.3.3/deploynator/_toml.py +287 -0
  6. {deploynator-1.3.0 → deploynator-1.3.3}/example.deploynator.toml +4 -0
  7. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_runner.py +14 -0
  8. deploynator-1.3.3/tests/test_toml.py +216 -0
  9. deploynator-1.3.0/deploynator/_toml.py +0 -100
  10. deploynator-1.3.0/tests/test_toml.py +0 -81
  11. {deploynator-1.3.0 → deploynator-1.3.3}/.gitignore +0 -0
  12. {deploynator-1.3.0 → deploynator-1.3.3}/LICENSE +0 -0
  13. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/__main__.py +0 -0
  14. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_changelog.py +0 -0
  15. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_config.py +0 -0
  16. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_deploy.py +0 -0
  17. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_docs.py +0 -0
  18. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_git.py +0 -0
  19. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_mcp_server.py +0 -0
  20. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_models.py +0 -0
  21. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_platforms.py +0 -0
  22. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_pypi.py +0 -0
  23. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_readme.py +0 -0
  24. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_runner.py +0 -0
  25. {deploynator-1.3.0 → deploynator-1.3.3}/deploynator/_semver.py +0 -0
  26. {deploynator-1.3.0 → deploynator-1.3.3}/projects/deploynator.yaml +0 -0
  27. {deploynator-1.3.0 → deploynator-1.3.3}/projects/lazyown.yaml +0 -0
  28. {deploynator-1.3.0 → deploynator-1.3.3}/projects/template.yaml +0 -0
  29. {deploynator-1.3.0 → deploynator-1.3.3}/pyproject.toml +0 -0
  30. {deploynator-1.3.0 → deploynator-1.3.3}/specs/deploynator_toml_pipeline.spec.md +0 -0
  31. {deploynator-1.3.0 → deploynator-1.3.3}/tests/__init__.py +0 -0
  32. {deploynator-1.3.0 → deploynator-1.3.3}/tests/bdd_test_deploy.py +0 -0
  33. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_changelog.py +0 -0
  34. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_cli_mcp.py +0 -0
  35. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_config.py +0 -0
  36. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_deploy.py +0 -0
  37. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_init.py +0 -0
  38. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_install_example.py +0 -0
  39. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_models.py +0 -0
  40. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_path_deploy.py +0 -0
  41. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_pipeline.py +0 -0
  42. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_pypi.py +0 -0
  43. {deploynator-1.3.0 → deploynator-1.3.3}/tests/test_readme_footer.py +0 -0
  44. {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.0
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. Regenerate with
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, taint paths, hotspots, cycles, and more.
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 (blast radius, cycles, hotspots)
275
- - `SECURITY.md` -- Findings by severity with fix hints
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, open `readmenator-maps/index.html` for the visual gallery.
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. Regenerate with
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, taint paths, hotspots, cycles, and more.
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 (blast radius, cycles, hotspots)
241
- - `SECURITY.md` -- Findings by severity with fix hints
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, open `readmenator-maps/index.html` for the visual gallery.
245
+ Developers: Read `KNOWLEDGE_BASE.md` for full architecture reference.
279
246
  <!-- /readmenator-kb-link -->
280
247
 
@@ -1,4 +1,4 @@
1
1
  from deploynator._app import DeploynatorApp
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.3.3"
4
4
  __all__ = ["DeploynatorApp"]
@@ -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(self.DEPLOY_TOML_TEMPLATE.format(name=project_name))
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