Skip to content

Commit 4e3383f

Browse files
committed
tests against module and singlefile
1 parent 16b44a4 commit 4e3383f

File tree

16 files changed

+2447
-45
lines changed

16 files changed

+2447
-45
lines changed

.gitignore

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ __pycache__/
1313
build/
1414
develop-eggs/
1515
dist/
16+
tmp-dist/
17+
tmp/
18+
tmp-*/
1619
downloads/
1720
out/
1821
coverage/
@@ -29,6 +32,12 @@ share/python-wheels/
2932
.installed.cfg
3033
*.egg
3134
MANIFEST
35+
*.tmp
36+
37+
# Prevent recursive test discovery inside build outputs
38+
!tests/
39+
tmp-dist/tests/
40+
dist/tests/
3241

3342
# PyInstaller
3443
# Usually these files are written by a python script from a template
@@ -243,4 +252,4 @@ Desktop.ini
243252
**/_fonts/brand/
244253

245254
# Github configuration
246-
!.github/
255+
!.github/

.pocket-build.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"builds":[{"include":["*"],"out":"dist"}]}

bin/pocket-build.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
# Full text: https://github.com/apathetic-tools/pocket-build/blob/main/LICENSE
55

66
# Version: 0.1.0
7-
# Commit: 1894643
7+
# Commit: 16b44a4
88
# Repo: https://github.com/apathetic-tools/pocket-build
99

1010
"""
1111
Pocket Build — a tiny build system that fits in your pocket.
1212
This single-file version is auto-generated from modular sources.
1313
Version: 0.1.0
14-
Commit: 1894643
14+
Commit: 16b44a4
1515
"""
1616

1717
from __future__ import annotations
@@ -129,6 +129,12 @@ def run_build(
129129
src_pattern = entry_dict.get("src")
130130
assert src_pattern is not None, f"Missing required 'src' in entry: {entry_dict}"
131131

132+
if not src_pattern or src_pattern.strip() in {".", ""}:
133+
print(
134+
f"{YELLOW}⚠️ Skipping invalid include pattern: {src_pattern!r}{RESET}"
135+
)
136+
continue
137+
132138
dest_name = entry_dict.get("dest")
133139
matches = (
134140
list(config_dir.rglob(src_pattern))
@@ -187,3 +193,9 @@ def main(argv: Optional[List[str]] = None) -> int:
187193

188194
print("🎉 All builds complete.")
189195
return 0
196+
197+
198+
if __name__ == "__main__":
199+
import sys
200+
201+
sys.exit(main(sys.argv[1:]))

dev/make_script.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ def strip_redundant_blocks(text: str) -> str:
134134
f"Commit: {commit}\n"
135135
'"""\n\n'
136136
f"{sorted_imports}\n"
137-
"\n" + "\n".join(parts)
137+
"\n" + "\n".join(parts) + "\n\nif __name__ == '__main__':\n"
138+
" import sys\n"
139+
" sys.exit(main(sys.argv[1:]))\n"
138140
)
139141

140142
OUT_FILE.parent.mkdir(parents=True, exist_ok=True)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ strict = true
6565
# --- pytest: testing framework ---
6666
[tool.pytest.ini_options]
6767
pythonpath = ["src"]
68+
addopts = "-ra"
6869

6970
# --- Black: kept for compatibility ---
7071
[tool.black]

pytest.ini

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# pytest.ini
2+
[pytest]
3+
# --------------------------------------------------------------------
4+
# Pocket Build test configuration
5+
# --------------------------------------------------------------------
6+
7+
# Only run tests inside the real test suite
8+
testpaths = tests
9+
10+
# Do NOT recurse into generated or build output directories
11+
norecursedirs =
12+
dist
13+
tmp-dist
14+
tmp
15+
out
16+
bin
17+
build
18+
.git
19+
.venv
20+
.cache
21+
__pycache__
22+
23+
# Show more context in assertion diffs
24+
addopts = -ra
25+

result.txt

Lines changed: 2045 additions & 0 deletions
Large diffs are not rendered by default.

src/pocket_build/__init__.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1+
# src/pocket_build/__init__.py
12
"""
2-
Pocket Build — a tiny build system that fits in your pocket.
3-
Because not everything needs a toolchain.
3+
Pocket Build — modular entrypoint.
4+
Exports the same surface as the single-file bundled version,
5+
so that tests and users can use either interchangeably.
46
"""
57

8+
from .build import copy_directory, copy_file, copy_item, run_build
69
from .cli import main
10+
from .config import parse_builds
11+
from .types import BuildConfig
12+
from .utils import is_excluded, load_jsonc
713

8-
__all__ = ["main"]
14+
__all__ = [
15+
"copy_file",
16+
"copy_directory",
17+
"copy_item",
18+
"run_build",
19+
"main",
20+
"parse_builds",
21+
"is_excluded",
22+
"load_jsonc",
23+
"BuildConfig",
24+
]

src/pocket_build/build.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ def run_build(
5555
src_pattern = entry_dict.get("src")
5656
assert src_pattern is not None, f"Missing required 'src' in entry: {entry_dict}"
5757

58+
if not src_pattern or src_pattern.strip() in {".", ""}:
59+
print(
60+
f"{YELLOW}⚠️ Skipping invalid include pattern: {src_pattern!r}{RESET}"
61+
)
62+
continue
63+
5864
dest_name = entry_dict.get("dest")
5965
matches = (
6066
list(config_dir.rglob(src_pattern))

tests/conftest.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# tests/conftest.py
2+
"""
3+
Shared test fixture for pocket-build.
4+
5+
This lets all tests transparently run against both:
6+
1. The modular package (`src/pocket_build`)
7+
2. The bundled single-file script (`bin/pocket-build.py`)
8+
9+
Each test receives a `pocket_build_env` fixture that behaves like the module.
10+
11+
If the bundled script is missing or older than the source files,
12+
it is automatically rebuilt using `dev/make_script.py`.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import importlib.util
18+
import subprocess
19+
import sys
20+
from pathlib import Path
21+
from typing import Any, Dict, Generator, List, Optional, Protocol
22+
23+
import pytest
24+
25+
# Ensure the src/ folder is on sys.path (so "import pocket_build" works)
26+
ROOT = Path(__file__).resolve().parent.parent
27+
SRC_DIR = ROOT / "src"
28+
if SRC_DIR.exists():
29+
sys.path.insert(0, str(SRC_DIR))
30+
31+
# ruff: noqa: E402 — import after sys.path modification
32+
from pocket_build.types import BuildConfig
33+
34+
# --- Prevent pytest from scanning build output folders ---
35+
# These directories can contain auto-generated code or duplicated tests.
36+
BAD_DIRS = ["dist", "tmp-dist", "bin"]
37+
38+
for bad in BAD_DIRS:
39+
bad_path = Path(bad).resolve()
40+
sys.path = [p for p in sys.path if bad not in p]
41+
42+
43+
# ------------------------------------------------------------
44+
# 🧩 Protocol for type safety & editor autocompletion
45+
# ------------------------------------------------------------
46+
class PocketBuildLike(Protocol):
47+
"""Subset of functions shared by both implementations."""
48+
49+
# --- utils ---
50+
def load_jsonc(self, path: Path) -> Dict[str, Any]: ...
51+
def is_excluded(
52+
self,
53+
path: Path,
54+
exclude_patterns: List[str],
55+
root: Path,
56+
) -> bool: ...
57+
58+
# --- config ---
59+
def parse_builds(self, raw_config: Dict[str, Any]) -> List[BuildConfig]: ...
60+
61+
# --- build ---
62+
def copy_file(self, src: Path, dest: Path, root: Path) -> None: ...
63+
def copy_directory(
64+
self,
65+
src: Path,
66+
dest: Path,
67+
exclude_patterns: List[str],
68+
root: Path,
69+
) -> None: ...
70+
def copy_item(
71+
self,
72+
src: Path,
73+
dest: Path,
74+
exclude_patterns: List[str],
75+
root: Path,
76+
) -> None: ...
77+
def run_build(
78+
self,
79+
build_cfg: BuildConfig, # ✅ use the proper TypedDict
80+
config_dir: Path,
81+
out_override: Optional[str],
82+
) -> None: ...
83+
84+
# --- CLI ---
85+
def main(self, argv: Optional[List[str]] = None) -> int: ...
86+
87+
88+
def pytest_ignore_collect(collection_path: Path, config): # type: ignore[override]
89+
"""
90+
Hook called by pytest for each discovered path.
91+
Returning True tells pytest to skip collecting tests from it.
92+
"""
93+
for bad in BAD_DIRS:
94+
if bad in str(collection_path):
95+
return True
96+
return False
97+
98+
99+
# ------------------------------------------------------------
100+
# ⚙️ Auto-build helper for bundled script
101+
# ------------------------------------------------------------
102+
def ensure_bundled_script_up_to_date(root: Path) -> Path:
103+
"""Rebuild `bin/pocket-build.py` if missing or older than source files."""
104+
bin_path = root / "bin" / "pocket-build.py"
105+
src_dir = root / "src" / "pocket_build"
106+
builder = root / "dev" / "make_script.py"
107+
108+
# If the output file doesn't exist or is older than any source file → rebuild.
109+
needs_rebuild = not bin_path.exists()
110+
if not needs_rebuild:
111+
bin_mtime = bin_path.stat().st_mtime
112+
for src_file in src_dir.rglob("*.py"):
113+
if src_file.stat().st_mtime > bin_mtime:
114+
needs_rebuild = True
115+
break
116+
117+
if needs_rebuild:
118+
print("⚙️ Rebuilding single-file bundle (make_script.py)...")
119+
subprocess.run([sys.executable, str(builder)], check=True)
120+
assert bin_path.exists(), "❌ Failed to generate bundled script."
121+
122+
return bin_path
123+
124+
125+
# ------------------------------------------------------------
126+
# 🔁 Fixture: load either the package or the bundled script
127+
# ------------------------------------------------------------
128+
@pytest.fixture(scope="session", params=["module", "singlefile"])
129+
def pocket_build_env(
130+
request: pytest.FixtureRequest,
131+
) -> Generator[PocketBuildLike, None, None]:
132+
"""Yield a loaded pocket_build environment (module or bundled single-file)."""
133+
root = Path(__file__).resolve().parent.parent
134+
135+
if request.param == "module":
136+
import pocket_build as mod
137+
138+
yield mod # type: ignore[return-value]
139+
140+
else:
141+
bin_path = ensure_bundled_script_up_to_date(root)
142+
143+
spec = importlib.util.spec_from_file_location("pocket_build_single", bin_path)
144+
assert spec and spec.loader, f"Failed to load spec from {bin_path}"
145+
mod = importlib.util.module_from_spec(spec)
146+
sys.modules["pocket_build_single"] = mod
147+
spec.loader.exec_module(mod)
148+
yield mod # type: ignore[return-value]

0 commit comments

Comments
 (0)