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

1"""Workspace setup for spec-based validation. 

2 

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 

8 

9The SpecRunWorkspace dataclass captures all workspace state needed for 

10validation, and the setup/cleanup functions manage the lifecycle. 

11""" 

12 

13from __future__ import annotations 

14 

15import tempfile 

16import uuid 

17from contextlib import contextmanager 

18from dataclasses import dataclass 

19from pathlib import Path 

20from typing import TYPE_CHECKING 

21 

22from .coverage import BaselineCoverageService 

23from .spec import ValidationArtifacts 

24from .worktree import ( 

25 WorktreeConfig, 

26 WorktreeState, 

27 create_worktree, 

28 remove_worktree, 

29) 

30 

31if TYPE_CHECKING: 

32 from collections.abc import Generator 

33 

34 from src.core.protocols import CommandRunnerPort, EnvConfigPort, LockManagerPort 

35 

36 from .spec import ValidationContext, ValidationSpec 

37 from .worktree import WorktreeContext 

38 

39 

40class SetupError(Exception): 

41 """Raised when workspace setup fails. 

42 

43 Attributes: 

44 reason: Human-readable failure reason. 

45 retriable: Whether the failure is potentially retriable. 

46 """ 

47 

48 def __init__(self, reason: str, retriable: bool = False) -> None: 

49 super().__init__(reason) 

50 self.reason = reason 

51 self.retriable = retriable 

52 

53 

54@dataclass 

55class SpecRunWorkspace: 

56 """Workspace context for a validation run. 

57 

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 

63 

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 """ 

75 

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 

82 

83 

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. 

94 

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 

101 

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. 

110 

111 Returns: 

112 SpecRunWorkspace with all context needed for validation. 

113 

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) 

121 

122 # Generate unique run ID 

123 run_id = f"run-{uuid.uuid4().hex[:8]}" 

124 issue_id = context.issue_id or "run-level" 

125 

126 # Initialize artifacts 

127 artifacts = ValidationArtifacts(log_dir=log_dir) 

128 

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 

149 

150 # Set up worktree if we have a commit to validate 

151 worktree_ctx: WorktreeContext | None = None 

152 validation_cwd: Path 

153 

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 ) 

168 

169 if worktree_ctx.state == WorktreeState.FAILED: 

170 raise SetupError( 

171 f"Worktree creation failed: {worktree_ctx.error}", 

172 retriable=False, 

173 ) 

174 

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 

180 

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 ) 

189 

190 

191def cleanup_workspace( 

192 workspace: SpecRunWorkspace, 

193 validation_passed: bool, 

194 command_runner: CommandRunnerPort, 

195) -> None: 

196 """Clean up workspace after validation completes. 

197 

198 Handles worktree removal with proper pass/fail status handling: 

199 - On success: removes the worktree 

200 - On failure: keeps the worktree for debugging 

201 

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 

209 

210 worktree_ctx = remove_worktree( 

211 workspace.worktree_ctx, 

212 validation_passed=validation_passed, 

213 command_runner=command_runner, 

214 ) 

215 

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" 

221 

222 

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. 

234 

235 Ensures cleanup is called even if validation raises an exception. 

236 On exception, validation_passed=False is used for cleanup. 

237 

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. 

246 

247 Yields: 

248 SpecRunWorkspace with all context needed for validation. 

249 

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)