Skip to content

Commit 26ccf8e

Browse files
committed
clearer self-test error messages
1 parent 745e504 commit 26ccf8e

File tree

7 files changed

+102
-61
lines changed

7 files changed

+102
-61
lines changed

ROADMAP.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ API
3737
- [ ] can utils/config be made into a single submodule? how does that play with the bundler?
3838
- [ ] do we want a way to dump the schema for documentation purposes?
3939

40-
utils
41-
- [ ] do things that call is_excluded need to check if root exists?
42-
- [ ] improve the self-test to give specific errors along the way
43-
4440
config_validate
4541
- [ ] key in wrong place: "Ignored watch_interval in build #0: applies only at root level (move it above your builds: block)." could use `ROOT_ONLY_HINTS = {"watch_interval": "move it above your builds list"}`
4642
- [ ] type examples for _infer_type_label(), TYPE_EXAMPLES, "key 'include' expected list[str], got int" could add "expected list[str] (e.g. ["src/", "lib/"]), got int"

src/pocket_build/actions.py

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -196,66 +196,92 @@ def get_metadata() -> Metadata:
196196
return Metadata(version, commit)
197197

198198

199-
def run_selftest() -> bool:
199+
def run_selftest() -> bool: # noqa: PLR0915
200200
"""Run a lightweight functional test of the tool itself."""
201201
logger = get_logger()
202202
logger.info("🧪 Running self-test...")
203203

204+
start_time = time.time()
204205
tmp_dir: Path | None = None
206+
205207
try:
206208
tmp_dir = Path(tempfile.mkdtemp(prefix=f"{PROGRAM_SCRIPT}-selftest-"))
207209
src = tmp_dir / "src"
208210
out = tmp_dir / "out"
209211
src.mkdir()
210212

211-
# Create a tiny file to copy
213+
logger.debug("[SELFTEST] Temp dir: %s", tmp_dir)
214+
215+
# --- Phase 1: Create input file ---
216+
test_msg = f"hello {PROGRAM_DISPLAY}!"
212217
file = src / "hello.txt"
213-
file.write_text(f"hello {PROGRAM_DISPLAY}!", encoding="utf-8")
214-
215-
# --- Construct minimal BuildConfigResolved using helpers ---
216-
build_cfg: BuildConfigResolved = {
217-
"include": [make_includeresolved(str(src / "**"), tmp_dir, "code")],
218-
"exclude": [],
219-
"out": make_pathresolved(out, tmp_dir, "code"),
220-
"respect_gitignore": False,
221-
"log_level": "info",
222-
"dry_run": False,
223-
"__meta__": {"cli_root": tmp_dir, "config_root": tmp_dir},
224-
}
218+
file.write_text(test_msg, encoding="utf-8")
219+
# file_write should raise an exception on failure making this check unnecesary
220+
if not file.exists():
221+
xmsg = f"Input file creation failed: {file}"
222+
raise RuntimeError(xmsg) # noqa: TRY301
223+
logger.debug("[SELFTEST] Created input file: %s", file)
224+
225+
# --- Phase 2: Prepare config ---
226+
try:
227+
build_cfg: BuildConfigResolved = {
228+
"include": [make_includeresolved(str(src / "**"), tmp_dir, "code")],
229+
"exclude": [],
230+
"out": make_pathresolved(out, tmp_dir, "code"),
231+
"respect_gitignore": False,
232+
"log_level": "info",
233+
"dry_run": False,
234+
"__meta__": {"cli_root": tmp_dir, "config_root": tmp_dir},
235+
}
236+
except Exception as e:
237+
xmsg = f"Config construction failed: {e}"
238+
raise RuntimeError(xmsg) from e
225239

226240
logger.debug("[SELFTEST] using temp dir: %s", tmp_dir)
227241

228-
# --- Run the build directly ---
242+
# --- Phase 3: Execute build (both dry and real) ---
229243
for dry_run in (True, False):
230244
build_cfg["dry_run"] = dry_run
231-
run_build(build_cfg)
232-
233-
# Verify file copy
245+
logger.debug("[SELFTEST] Running build (dry_run=%s)", dry_run)
246+
try:
247+
run_build(build_cfg)
248+
except Exception as e:
249+
xmsg = f"Build execution failed (dry_run={dry_run}): {e}"
250+
raise RuntimeError(xmsg) from e
251+
252+
# --- Phase 4: Validate results ---
234253
copied = out / "hello.txt"
235-
if (
236-
copied.exists()
237-
and copied.read_text().strip() == f"hello {PROGRAM_DISPLAY}!"
238-
):
239-
logger.info(
240-
"✅ Self-test passed — %s is working correctly.", PROGRAM_DISPLAY
241-
)
242-
return True
243-
244-
logger.error("Self-test failed: output file not found or invalid.")
254+
if not copied.exists():
255+
xmsg = f"Expected output file missing: {copied}"
256+
raise RuntimeError(xmsg) # noqa: TRY301
257+
258+
actual = copied.read_text(encoding="utf-8").strip()
259+
if actual != test_msg:
260+
xmsg = f"Output content mismatch: got '{actual}', expected '{test_msg}'"
261+
raise AssertionError(xmsg) # noqa: TRY301
262+
263+
elapsed = time.time() - start_time
264+
logger.info(
265+
"✅ Self-test passed in %.2fs — %s is working correctly.",
266+
elapsed,
267+
PROGRAM_DISPLAY,
268+
)
269+
return True
270+
271+
except (PermissionError, FileNotFoundError) as e:
272+
logger.error_if_not_debug("Self-test failed due to environment issue: %s", e)
245273
return False
246274

247-
except PermissionError:
248-
logger.error("Self-test failed: insufficient permissions.") # noqa: TRY400
275+
except RuntimeError as e:
276+
logger.error_if_not_debug("Self-test failed: %s", e)
249277
return False
250-
except FileNotFoundError:
251-
logger.error("Self-test failed: missing file or directory.") # noqa: TRY400
278+
279+
except AssertionError as e:
280+
logger.error_if_not_debug("Self-test failed validation: %s", e)
252281
return False
282+
253283
except Exception:
254-
# Unexpected bug — show traceback and ask for a bug report
255-
logger.exception(
256-
"Unexpected self-test failure. "
257-
"Please report this issue with the following traceback:"
258-
)
284+
logger.exception("Unexpected self-test failure. Please report this traceback:")
259285
return False
260286

261287
finally:

src/pocket_build/build.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,15 @@ def copy_item(
201201
"""Copy one file or directory entry, using built-in root info."""
202202
logger = get_logger()
203203
src = Path(src_entry["path"])
204-
root_src = Path(src_entry["root"]).resolve()
205-
src = (root_src / src).resolve() if not src.is_absolute() else src.resolve()
204+
src_root = Path(src_entry["root"]).resolve()
205+
src = (src_root / src).resolve() if not src.is_absolute() else src.resolve()
206206
dest = Path(dest_entry["path"])
207-
root_dest = (dest_entry["root"]).resolve()
208-
dest = (root_dest / dest).resolve() if not dest.is_absolute() else dest.resolve()
207+
dest_root = (dest_entry["root"]).resolve()
208+
dest = (dest_root / dest).resolve() if not dest.is_absolute() else dest.resolve()
209209
origin = src_entry.get("origin", "?")
210210

211211
# Combine output directory with the precomputed dest (if relative)
212-
dest = dest if dest.is_absolute() else (root_dest / dest)
212+
dest = dest if dest.is_absolute() else (dest_root / dest)
213213

214214
exclude_patterns_raw = [str(e["path"]) for e in exclude_patterns]
215215
pattern_str = str(src_entry.get("pattern", src_entry["path"]))
@@ -220,8 +220,8 @@ def copy_item(
220220
)
221221

222222
# Exclusion check relative to its root
223-
if is_excluded_raw(src, exclude_patterns_raw, root_src):
224-
logger.debug("🚫 Skipped (excluded): %s", src.relative_to(root_src))
223+
if is_excluded_raw(src, exclude_patterns_raw, src_root):
224+
logger.debug("🚫 Skipped (excluded): %s", src.relative_to(src_root))
225225
return
226226

227227
# Detect shallow single-star pattern
@@ -233,7 +233,7 @@ def copy_item(
233233
# — copy only the directory itself, not its contents
234234
if src.is_dir() and is_shallow_star:
235235
logger.trace(
236-
f"📁 (shallow from pattern={pattern_str!r}) {src.relative_to(root_src)}",
236+
f"📁 (shallow from pattern={pattern_str!r}) {src.relative_to(src_root)}",
237237
)
238238
if not dry_run:
239239
dest.mkdir(parents=True, exist_ok=True)
@@ -245,14 +245,14 @@ def copy_item(
245245
src,
246246
dest,
247247
exclude_patterns_raw,
248-
src_root=root_src,
248+
src_root=src_root,
249249
dry_run=dry_run,
250250
)
251251
else:
252252
copy_file(
253253
src,
254254
dest,
255-
src_root=root_src,
255+
src_root=src_root,
256256
dry_run=dry_run,
257257
)
258258

src/pocket_build/cli.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -319,20 +319,15 @@ def main(argv: list[str] | None = None) -> int: # noqa: C901, PLR0911, PLR0912,
319319
silent = getattr(e, "silent", False)
320320
if not silent:
321321
try:
322-
logger.error(str(e)) # noqa: TRY400
322+
logger.error_if_not_debug(str(e))
323323
except Exception: # noqa: BLE001
324324
safe_log(f"[FATAL] Logging failed while reporting: {e}")
325325
return getattr(e, "code", 1)
326326

327327
except Exception as e: # noqa: BLE001
328328
# unexpected internal error
329329
try:
330-
if logger.level_name in {"DEBUG", "TRACE"}: # how to get the name?
331-
# Show traceback only in verbose/debug modes
332-
logger.exception("Unexpected internal error:")
333-
else:
334-
# Just a one-line summary for normal users
335-
logger.critical("Unexpected internal error: %s", e)
330+
logger.critical_if_not_debug("Unexpected internal error: %s", e)
336331
except Exception: # noqa: BLE001
337332
safe_log(f"[FATAL] Logging failed while reporting: {e}")
338333

src/pocket_build/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ def is_excluded_raw( # noqa: PLR0911
231231
- Treats 'path' as relative to 'root' unless already absolute.
232232
- If 'root' is a file, match directly.
233233
- Handles absolute or relative glob patterns.
234+
235+
Note:
236+
The function does not require `root` to exist; if it does not,
237+
a debug message is logged and matching is purely path-based.
234238
"""
235239
logger = get_logger()
236240
root = Path(root).resolve()

src/pocket_build/utils_logs.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ def safe_log(msg: str) -> None:
8484

8585

8686
def make_test_trace(icon: str = "🧰") -> Callable[..., Any]:
87-
def local_trace(label: str, *args: object) -> Any:
87+
def local_trace(label: str, *args: Any) -> Any:
8888
return TEST_TRACE(label, *args, icon=icon)
8989

9090
return local_trace
9191

9292

93-
def TEST_TRACE(label: str, *args: object, icon: str = "🧰") -> None: # noqa: N802
93+
def TEST_TRACE(label: str, *args: Any, icon: str = "🧰") -> None: # noqa: N802
9494
"""Emit a synchronized, flush-safe diagnostic line.
9595
9696
Args:
@@ -245,6 +245,26 @@ def level_name(self) -> str:
245245
(see also: logging.getLevelName)."""
246246
return logging.getLevelName(self.getEffectiveLevel())
247247

248+
def error_if_not_debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
249+
"""Logs an exception with the real traceback starting from the caller.
250+
Only shows full traceback if debug/trace is enabled."""
251+
exc_info = kwargs.pop("exc_info", True)
252+
stacklevel = kwargs.pop("stacklevel", 2) # skip helper frame
253+
if self.isEnabledFor(logging.DEBUG):
254+
self.exception(msg, *args, exc_info=exc_info, stacklevel=stacklevel)
255+
else:
256+
self.error(msg, *args)
257+
258+
def critical_if_not_debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
259+
"""Logs an exception with the real traceback starting from the caller.
260+
Only shows full traceback if debug/trace is enabled."""
261+
exc_info = kwargs.pop("exc_info", True)
262+
stacklevel = kwargs.pop("stacklevel", 2) # skip helper frame
263+
if self.isEnabledFor(logging.DEBUG):
264+
self.exception(msg, *args, exc_info=exc_info, stacklevel=stacklevel)
265+
else:
266+
self.critical(msg, *args)
267+
248268
def colorize(
249269
self, text: str, color: str, *, enable_color: bool | None = None
250270
) -> str:

tests/utils/proj_root.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
from pathlib import Path
44

55

6-
PROJ_ROOT = Path(__file__).resolve().parent.parent.parent
6+
PROJ_ROOT = Path(__file__).resolve().parent.parent.parent.resolve()

0 commit comments

Comments
 (0)