Skip to content

Commit 63b708e

Browse files
branchseerclaude
andcommitted
refactor(cache): deduplicate PreRunFingerprint creation and add glob base tests
- Extract PreRunFingerprint::from_cache_metadata() to avoid code duplication in try_hit() and update() methods - Rename base_dir parameter to workspace_root for clarity - Replace expect() calls with proper anyhow errors in spawn.rs - Add e2e tests for glob base directory behavior: - Verify globs are relative to package directory, not task cwd - Test root package and subpackage glob resolution - Test that custom cwd doesn't affect glob base Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5eb6631 commit 63b708e

20 files changed

Lines changed: 519 additions & 52 deletions

crates/vite_task/src/session/cache/mod.rs

Lines changed: 43 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,40 @@ pub struct PreRunFingerprint {
3737
pub globbed_inputs: BTreeMap<RelativePathBuf, u64>,
3838
}
3939

40+
impl PreRunFingerprint {
41+
/// Build a pre-run fingerprint from cache metadata and globbed inputs.
42+
///
43+
/// # Arguments
44+
/// * `cache_metadata` - Cache metadata from plan stage (contains absolute glob_base)
45+
/// * `globbed_inputs` - Hashes of explicit input files computed from positive globs
46+
/// * `workspace_root` - Workspace root for converting absolute glob_base to relative
47+
fn from_cache_metadata(
48+
cache_metadata: &CacheMetadata,
49+
globbed_inputs: BTreeMap<RelativePathBuf, u64>,
50+
workspace_root: &AbsolutePath,
51+
) -> anyhow::Result<Self> {
52+
// Convert absolute glob_base to relative for cache key
53+
let glob_base = cache_metadata
54+
.glob_base
55+
.strip_prefix(workspace_root)
56+
.map_err(|e| anyhow::anyhow!("failed to strip prefix from glob_base: {e}"))?
57+
.ok_or_else(|| {
58+
anyhow::anyhow!(
59+
"glob_base {:?} is not inside workspace {:?}",
60+
cache_metadata.glob_base,
61+
workspace_root
62+
)
63+
})?;
64+
65+
Ok(Self {
66+
spawn_fingerprint: cache_metadata.spawn_fingerprint.clone(),
67+
input_config: cache_metadata.input_config.clone(),
68+
glob_base,
69+
globbed_inputs,
70+
})
71+
}
72+
}
73+
4074
/// Command cache value, for validating post-run fingerprint after the spawn fingerprint is matched,
4175
/// and replaying the std outputs if validated.
4276
#[derive(Debug, Encode, Decode, Serialize)]
@@ -153,45 +187,27 @@ impl ExecutionCache {
153187
/// # Arguments
154188
/// * `cache_metadata` - Cache metadata from plan stage
155189
/// * `globbed_inputs` - Hashes of explicit input files computed from positive globs
156-
/// * `base_dir` - Workspace root for validating post-run fingerprint
190+
/// * `workspace_root` - Workspace root for converting paths and validating fingerprints
157191
#[tracing::instrument(level = "debug", skip_all)]
158192
pub async fn try_hit(
159193
&self,
160194
cache_metadata: &CacheMetadata,
161195
globbed_inputs: BTreeMap<RelativePathBuf, u64>,
162-
base_dir: &AbsolutePath,
196+
workspace_root: &AbsolutePath,
163197
) -> anyhow::Result<Result<CommandCacheValue, CacheMiss>> {
164198
let spawn_fingerprint = &cache_metadata.spawn_fingerprint;
165199
let execution_cache_key = &cache_metadata.execution_cache_key;
166200
let input_config = &cache_metadata.input_config;
167201

168-
// Convert absolute glob_base to relative for cache key
169-
let glob_base = cache_metadata
170-
.glob_base
171-
.strip_prefix(base_dir)
172-
.map_err(|e| anyhow::anyhow!("failed to strip prefix from glob_base: {e}"))?
173-
.ok_or_else(|| {
174-
anyhow::anyhow!(
175-
"glob_base {:?} is not inside workspace {:?}",
176-
cache_metadata.glob_base,
177-
base_dir
178-
)
179-
})?;
180-
181-
// Build pre-run fingerprint combining spawn fingerprint, input config, and globbed inputs
182-
let pre_run_fingerprint = PreRunFingerprint {
183-
spawn_fingerprint: spawn_fingerprint.clone(),
184-
input_config: input_config.clone(),
185-
glob_base,
186-
globbed_inputs,
187-
};
202+
let pre_run_fingerprint =
203+
PreRunFingerprint::from_cache_metadata(cache_metadata, globbed_inputs, workspace_root)?;
188204

189205
// Try to directly find the cache by pre-run fingerprint first
190206
if let Some(cache_value) = self.get_by_pre_run_fingerprint(&pre_run_fingerprint).await? {
191207
// Validate post-run fingerprint (inferred inputs) only if auto inference is enabled
192208
if input_config.includes_auto {
193209
if let Some(post_run_fingerprint_mismatch) =
194-
cache_value.post_run_fingerprint.validate(base_dir)?
210+
cache_value.post_run_fingerprint.validate(workspace_root)?
195211
{
196212
// Found the cache with the same pre-run fingerprint, but the post-run fingerprint mismatches
197213
return Ok(Err(CacheMiss::FingerprintMismatch(
@@ -231,40 +247,21 @@ impl ExecutionCache {
231247
/// # Arguments
232248
/// * `cache_metadata` - Cache metadata from plan stage
233249
/// * `globbed_inputs` - Hashes of explicit input files computed from positive globs
234-
/// * `base_dir` - Workspace root for converting absolute paths to relative
250+
/// * `workspace_root` - Workspace root for converting absolute paths to relative
235251
/// * `cache_value` - The cache value to store (outputs and post-run fingerprint)
236252
#[tracing::instrument(level = "debug", skip_all)]
237253
pub async fn update(
238254
&self,
239255
cache_metadata: &CacheMetadata,
240256
globbed_inputs: BTreeMap<RelativePathBuf, u64>,
241-
base_dir: &AbsolutePath,
257+
workspace_root: &AbsolutePath,
242258
cache_value: CommandCacheValue,
243259
) -> anyhow::Result<()> {
244260
let spawn_fingerprint = &cache_metadata.spawn_fingerprint;
245261
let execution_cache_key = &cache_metadata.execution_cache_key;
246-
let input_config = &cache_metadata.input_config;
247262

248-
// Convert absolute glob_base to relative for cache key
249-
let glob_base = cache_metadata
250-
.glob_base
251-
.strip_prefix(base_dir)
252-
.map_err(|e| anyhow::anyhow!("failed to strip prefix from glob_base: {e}"))?
253-
.ok_or_else(|| {
254-
anyhow::anyhow!(
255-
"glob_base {:?} is not inside workspace {:?}",
256-
cache_metadata.glob_base,
257-
base_dir
258-
)
259-
})?;
260-
261-
// Build pre-run fingerprint combining spawn fingerprint, input config, and globbed inputs
262-
let pre_run_fingerprint = PreRunFingerprint {
263-
spawn_fingerprint: spawn_fingerprint.clone(),
264-
input_config: input_config.clone(),
265-
glob_base,
266-
globbed_inputs,
267-
};
263+
let pre_run_fingerprint =
264+
PreRunFingerprint::from_cache_metadata(cache_metadata, globbed_inputs, workspace_root)?;
268265

269266
self.upsert_pre_run_fingerprint_cache(&pre_run_fingerprint, &cache_value).await?;
270267
self.upsert_execution_key_to_fingerprint(execution_cache_key, spawn_fingerprint).await?;

crates/vite_task/src/session/execute/spawn.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,22 @@ pub async fn spawn_with_tracking(
167167
}
168168
}
169169

170-
let termination = match tracking_state {
171-
TrackingState::FspyEnabled(tracked_child) => Some(tracked_child.wait_handle.await?),
170+
// Wait for process termination and process path accesses if fspy was enabled
171+
let (termination, path_accesses) = match tracking_state {
172+
TrackingState::FspyEnabled(tracked_child) => {
173+
let termination = tracked_child.wait_handle.await?;
174+
// path_accesses must be Some when fspy is enabled (they're set together)
175+
let path_accesses = path_accesses.ok_or_else(|| {
176+
anyhow::anyhow!("internal error: fspy enabled but path_accesses is None")
177+
})?;
178+
(termination, path_accesses)
179+
}
172180
TrackingState::FspyDisabled(mut tokio_child) => {
173181
let exit_status = tokio_child.wait().await?;
174182
return Ok(SpawnResult { exit_status, duration: start.elapsed() });
175183
}
176184
};
177185
let duration = start.elapsed();
178-
179-
// Process path accesses from fspy (only when fspy was enabled)
180-
let termination = termination.expect("fspy enabled but no termination");
181-
let path_accesses = path_accesses.expect("fspy enabled but no path_accesses");
182186
let path_reads = &mut path_accesses.path_reads;
183187
let path_writes = &mut path_accesses.path_writes;
184188

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const rootOther = 'initial';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "glob-base-test",
3+
"private": true
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const other = 'initial';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "sub-pkg",
3+
"private": true
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const sub = 'initial';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"tasks": {
3+
"sub-glob-test": {
4+
"command": "print-file src/sub.ts",
5+
"inputs": ["src/**/*.ts"],
6+
"cache": true
7+
},
8+
"sub-glob-with-cwd": {
9+
"command": "print-file sub.ts",
10+
"cwd": "src",
11+
"inputs": ["src/**/*.ts"],
12+
"cache": true
13+
}
14+
}
15+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- 'packages/*'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Test glob base directory behavior
2+
# Globs are relative to PACKAGE directory, NOT task cwd
3+
4+
# 1. Root package - glob matches files in root's src/
5+
[[e2e]]
6+
name = "root glob - matches src files"
7+
steps = [
8+
"vite run root-glob-test",
9+
# Modify matched file
10+
"replace-file-content src/root.ts initial modified",
11+
# Cache miss: file matches glob
12+
"vite run root-glob-test",
13+
]
14+
15+
[[e2e]]
16+
name = "root glob - ignores other directory"
17+
steps = [
18+
"vite run root-glob-test",
19+
# Modify file outside glob pattern
20+
"replace-file-content other/other.ts initial modified",
21+
# Cache hit: file doesn't match glob
22+
"vite run root-glob-test",
23+
]
24+
25+
[[e2e]]
26+
name = "root glob - ignores subpackage files"
27+
steps = [
28+
"vite run root-glob-test",
29+
# Modify file in subpackage - should NOT match root's src/**
30+
"replace-file-content packages/sub-pkg/src/sub.ts initial modified",
31+
# Cache hit: subpackage files don't match root's glob
32+
"vite run root-glob-test",
33+
]
34+
35+
# 2. Root package with custom cwd - glob still relative to package root
36+
[[e2e]]
37+
name = "root glob with cwd - glob relative to package not cwd"
38+
steps = [
39+
"vite run root-glob-with-cwd",
40+
# Modify file - glob is src/** relative to package root
41+
"replace-file-content src/root.ts initial modified",
42+
# Cache miss: file matches glob (relative to package, not cwd)
43+
"vite run root-glob-with-cwd",
44+
]
45+
46+
# 3. Subpackage - glob matches files in subpackage's src/
47+
[[e2e]]
48+
name = "subpackage glob - matches own src files"
49+
steps = [
50+
"vite run sub-pkg#sub-glob-test",
51+
# Modify matched file in subpackage
52+
"replace-file-content packages/sub-pkg/src/sub.ts initial modified",
53+
# Cache miss: file matches subpackage's glob
54+
"vite run sub-pkg#sub-glob-test",
55+
]
56+
57+
[[e2e]]
58+
name = "subpackage glob - ignores other directory in subpackage"
59+
steps = [
60+
"vite run sub-pkg#sub-glob-test",
61+
# Modify file outside glob pattern
62+
"replace-file-content packages/sub-pkg/other/other.ts initial modified",
63+
# Cache hit: file doesn't match glob
64+
"vite run sub-pkg#sub-glob-test",
65+
]
66+
67+
[[e2e]]
68+
name = "subpackage glob - ignores root src files"
69+
steps = [
70+
"vite run sub-pkg#sub-glob-test",
71+
# Modify file in root - should NOT match subpackage's src/**
72+
"replace-file-content src/root.ts initial modified",
73+
# Cache hit: root files don't match subpackage's glob
74+
"vite run sub-pkg#sub-glob-test",
75+
]
76+
77+
# 4. Subpackage with custom cwd - glob still relative to subpackage root
78+
[[e2e]]
79+
name = "subpackage glob with cwd - glob relative to package not cwd"
80+
steps = [
81+
"vite run sub-pkg#sub-glob-with-cwd",
82+
# Modify file - glob is src/** relative to subpackage root
83+
"replace-file-content packages/sub-pkg/src/sub.ts initial modified",
84+
# Cache miss: file matches glob (relative to subpackage, not cwd)
85+
"vite run sub-pkg#sub-glob-with-cwd",
86+
]

0 commit comments

Comments
 (0)