Coverage for src/domain/validation/spec_workspace.py: 38%

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

2

3 This module provides helpers for managing the workspace context during

4 validation runs, including:

5- Log directory setup and run ID generation

6- Baseline coverage refresh before worktree creation

7- Worktree creation and cleanup lifecycle

8

9 The SpecRunWorkspace dataclass captures all workspace state needed for

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

11"""

12

13 from __future__ import annotations

14

15 import tempfile

16 import uuid

17 from contextlib import contextmanager

18 from dataclasses import dataclass

19 from pathlib import Path

20 from typing import TYPE_CHECKING

21

22 from .coverage import BaselineCoverageService

23 from .spec import ValidationArtifacts

24 from .worktree import (

25 WorktreeConfig,

26 WorktreeState,

27 create_worktree,

28 remove_worktree,

29)

30

31 if 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

40 class 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

55 class 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

84 def 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

191 def 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

224 def 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)