Coverage for src / domain / validation / spec_workspace.py: 38%
63 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-04 04:43 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-04 04:43 +0000
1"""Workspace setup for spec-based validation.
3This module provides helpers for managing the workspace context during
4validation runs, including:
5- Log directory setup and run ID generation
6- Baseline coverage refresh before worktree creation
7- Worktree creation and cleanup lifecycle
9The SpecRunWorkspace dataclass captures all workspace state needed for
10validation, and the setup/cleanup functions manage the lifecycle.
11"""
13from __future__ import annotations
15import tempfile
16import uuid
17from contextlib import contextmanager
18from dataclasses import dataclass
19from pathlib import Path
20from typing import TYPE_CHECKING
22from .coverage import BaselineCoverageService
23from .spec import ValidationArtifacts
24from .worktree import (
25 WorktreeConfig,
26 WorktreeState,
27 create_worktree,
28 remove_worktree,
29)
31if TYPE_CHECKING:
32 from collections.abc import Generator
34 from src.core.protocols import CommandRunnerPort, EnvConfigPort, LockManagerPort
36 from .spec import ValidationContext, ValidationSpec
37 from .worktree import WorktreeContext
40class SetupError(Exception):
41 """Raised when workspace setup fails.
43 Attributes:
44 reason: Human-readable failure reason.
45 retriable: Whether the failure is potentially retriable.
46 """
48 def __init__(self, reason: str, retriable: bool = False) -> None:
49 super().__init__(reason)
50 self.reason = reason
51 self.retriable = retriable
54@dataclass
55class SpecRunWorkspace:
56 """Workspace context for a validation run.
58 This dataclass captures all the state needed to run validation:
59 - Where to run commands (validation_cwd)
60 - Where to store artifacts (artifacts, log_dir)
61 - Baseline coverage for "no decrease" mode
62 - Optional worktree context for cleanup
64 Attributes:
65 validation_cwd: Working directory for running validation commands.
66 This is either the main repo (in-place validation) or a worktree.
67 artifacts: ValidationArtifacts for tracking logs and outputs.
68 baseline_percent: Baseline coverage percentage for "no decrease" mode.
69 None if using explicit threshold or coverage disabled.
70 run_id: Unique identifier for this validation run.
71 log_dir: Directory for logs and artifacts.
72 worktree_ctx: Optional worktree context if validation uses a worktree.
73 Used for cleanup after validation completes.
74 """
76 validation_cwd: Path
77 artifacts: ValidationArtifacts
78 baseline_percent: float | None
79 run_id: str
80 log_dir: Path
81 worktree_ctx: WorktreeContext | None
84def setup_workspace(
85 spec: ValidationSpec,
86 context: ValidationContext,
87 log_dir: Path | None,
88 step_timeout_seconds: float | None,
89 command_runner: CommandRunnerPort,
90 env_config: EnvConfigPort,
91 lock_manager: LockManagerPort,
92) -> SpecRunWorkspace:
93 """Set up workspace for a validation run.
95 This function:
96 1. Creates/uses log directory
97 2. Generates unique run ID
98 3. Initializes artifacts tracking
99 4. Refreshes baseline coverage if in "no decrease" mode
100 5. Creates worktree if validating a specific commit
102 Args:
103 spec: What validations to run.
104 context: Immutable context for the validation run.
105 log_dir: Directory for logs/artifacts. Uses temp dir if None.
106 step_timeout_seconds: Optional timeout for baseline refresh commands.
107 command_runner: Command runner for executing git commands.
108 env_config: Environment configuration for paths.
109 lock_manager: Lock manager for file locking.
111 Returns:
112 SpecRunWorkspace with all context needed for validation.
114 Raises:
115 SetupError: If baseline refresh or worktree creation fails.
116 """
117 # Set up log directory
118 if log_dir is None:
119 log_dir = Path(tempfile.mkdtemp(prefix="mala-validation-logs-"))
120 log_dir.mkdir(parents=True, exist_ok=True)
122 # Generate unique run ID
123 run_id = f"run-{uuid.uuid4().hex[:8]}"
124 issue_id = context.issue_id or "run-level"
126 # Initialize artifacts
127 artifacts = ValidationArtifacts(log_dir=log_dir)
129 # Check/refresh baseline coverage BEFORE worktree creation
130 # This captures baseline from main repo state
131 baseline_percent: float | None = None
132 if spec.coverage.enabled and spec.coverage.min_percent is None:
133 # "No decrease" mode - need to get baseline via service
134 baseline_service = BaselineCoverageService(
135 context.repo_path,
136 env_config=env_config,
137 command_runner=command_runner,
138 lock_manager=lock_manager,
139 coverage_config=spec.yaml_coverage_config,
140 step_timeout_seconds=step_timeout_seconds,
141 )
142 result = baseline_service.refresh_if_stale(spec)
143 if not result.success:
144 raise SetupError(
145 result.error or "Baseline refresh failed",
146 retriable=False,
147 )
148 baseline_percent = result.percent
150 # Set up worktree if we have a commit to validate
151 worktree_ctx: WorktreeContext | None = None
152 validation_cwd: Path
154 if context.commit_hash:
155 worktree_config = WorktreeConfig(
156 base_dir=log_dir / "worktrees",
157 keep_on_failure=True, # Keep for debugging
158 )
159 worktree_ctx = create_worktree(
160 repo_path=context.repo_path,
161 commit_sha=context.commit_hash,
162 config=worktree_config,
163 run_id=run_id,
164 issue_id=issue_id,
165 attempt=1,
166 command_runner=command_runner,
167 )
169 if worktree_ctx.state == WorktreeState.FAILED:
170 raise SetupError(
171 f"Worktree creation failed: {worktree_ctx.error}",
172 retriable=False,
173 )
175 validation_cwd = worktree_ctx.path
176 artifacts.worktree_path = worktree_ctx.path
177 else:
178 # No commit specified, validate in place
179 validation_cwd = context.repo_path
181 return SpecRunWorkspace(
182 validation_cwd=validation_cwd,
183 artifacts=artifacts,
184 baseline_percent=baseline_percent,
185 run_id=run_id,
186 log_dir=log_dir,
187 worktree_ctx=worktree_ctx,
188 )
191def cleanup_workspace(
192 workspace: SpecRunWorkspace,
193 validation_passed: bool,
194 command_runner: CommandRunnerPort,
195) -> None:
196 """Clean up workspace after validation completes.
198 Handles worktree removal with proper pass/fail status handling:
199 - On success: removes the worktree
200 - On failure: keeps the worktree for debugging
202 Args:
203 workspace: The workspace to clean up.
204 validation_passed: Whether validation succeeded.
205 command_runner: Command runner for executing git commands.
206 """
207 if workspace.worktree_ctx is None:
208 return
210 worktree_ctx = remove_worktree(
211 workspace.worktree_ctx,
212 validation_passed=validation_passed,
213 command_runner=command_runner,
214 )
216 # Update artifacts with worktree state
217 if worktree_ctx.state == WorktreeState.KEPT:
218 workspace.artifacts.worktree_state = "kept"
219 elif worktree_ctx.state == WorktreeState.REMOVED:
220 workspace.artifacts.worktree_state = "removed"
223@contextmanager
224def workspace_context(
225 spec: ValidationSpec,
226 context: ValidationContext,
227 log_dir: Path | None,
228 step_timeout_seconds: float | None,
229 command_runner: CommandRunnerPort,
230 env_config: EnvConfigPort,
231 lock_manager: LockManagerPort,
232) -> Generator[SpecRunWorkspace, None, None]:
233 """Context manager for workspace setup and cleanup.
235 Ensures cleanup is called even if validation raises an exception.
236 On exception, validation_passed=False is used for cleanup.
238 Args:
239 spec: What validations to run.
240 context: Immutable context for the validation run.
241 log_dir: Directory for logs/artifacts. Uses temp dir if None.
242 step_timeout_seconds: Optional timeout for baseline refresh commands.
243 command_runner: Command runner for executing git commands.
244 env_config: Environment configuration for paths.
245 lock_manager: Lock manager for file locking.
247 Yields:
248 SpecRunWorkspace with all context needed for validation.
250 Raises:
251 SetupError: If baseline refresh or worktree creation fails.
252 """
253 workspace = setup_workspace(
254 spec,
255 context,
256 log_dir,
257 step_timeout_seconds,
258 command_runner,
259 env_config,
260 lock_manager,
261 )
262 validation_passed = False
263 try:
264 yield workspace
265 validation_passed = True
266 finally:
267 cleanup_workspace(workspace, validation_passed, command_runner)