Skip to content

Conversation

@max-sixty
Copy link
Collaborator

Summary

This adds support for the INSTA_PENDING_DIR environment variable, which allows pending snapshots to be written to a separate directory while keeping the source tree read-only. This is particularly useful for hermetic build systems like Bazel.

When INSTA_PENDING_DIR is set:

  • Pending snapshots (.snap.new and .pending-snap files) are written to the specified directory, preserving the same relative structure as the workspace
  • cargo-insta correctly discovers and maps these pending snapshots back to their target locations in the source tree
  • External test paths (e.g., path = "../tests/lib.rs") are rejected with a clear error, as they would escape the pending directory

Key changes

  • insta: Add get_pending_dir() and pending_snapshot_path() in env.rs
  • insta: Update runtime.rs to use pending_snapshot_path for all snapshot file operations
  • insta: Create parent directories in snapshot.rs when pending dir is set
  • cargo-insta: Update find_pending_snapshots() in walk.rs to support path mapping between pending_root and target_root
  • cargo-insta: Simplify load_snapshot_containers() in cli.rs to use unified loop for both hermetic and default modes
  • cargo-insta: Add comprehensive tests in pending_dir.rs (9 new tests)

Test plan

  • All 93 cargo-insta tests pass
  • New pending_dir tests cover: file snapshots, inline snapshots, accept, reject, auto-creation, update existing, check mode, outside workspace, and external test path rejection
  • Clippy clean
  • Pre-commit passes

🤖 Generated with Claude Code

This adds support for the INSTA_PENDING_DIR environment variable, which
allows pending snapshots to be written to a separate directory while
keeping the source tree read-only. This is particularly useful for
hermetic build systems like Bazel.

When INSTA_PENDING_DIR is set:
- Pending snapshots (.snap.new and .pending-snap files) are written to
  the specified directory, preserving the same relative structure as
  the workspace
- cargo-insta correctly discovers and maps these pending snapshots back
  to their target locations in the source tree
- External test paths (e.g., path = "../tests/lib.rs") are rejected
  with a clear error, as they would escape the pending directory

Key changes:
- insta: Add get_pending_dir() and pending_snapshot_path() in env.rs
- insta: Update runtime.rs to use pending_snapshot_path for all
  snapshot file operations
- insta: Create parent directories in snapshot.rs for pending dir
- cargo-insta: Update find_pending_snapshots() in walk.rs to support
  path mapping between pending_root and target_root
- cargo-insta: Simplify load_snapshot_containers() in cli.rs to use
  unified loop for both hermetic and default modes
- cargo-insta: Add comprehensive tests in pending_dir.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
max-sixty and others added 6 commits December 30, 2025 23:52
On Windows, paths from different sources may have different formats
(e.g., \\?\ prefix from canonicalize vs. regular paths). This caused
strip_prefix to fail when comparing workspace and snapshot paths.

Fix by canonicalizing both paths before strip_prefix. Fall back to
original paths if canonicalization fails (e.g., for test scenarios
with non-existent paths).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The previous canonicalization approach failed on Windows because:
1. Canonicalize() fails for non-existent files (new snapshots)
2. When it succeeds, it adds \\?\ extended-length prefix
3. strip_prefix fails when comparing paths with/without the prefix

The fix adds normalize_path() helper that:
- Tries to canonicalize the full path
- Falls back to canonicalizing parent + filename (for env.rs)
- Strips the \\?\ prefix on Windows for consistent comparison

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The previous fix didn't handle the case where a non-existent file's
parent directories also don't exist yet. On Windows, if part of the
path contains 8.3 short names (e.g., RUNNER~1 for runneradmin),
canonicalization of the existing workspace directory would convert
to long names while the snapshot path stayed in short name form.

The fix walks up the path tree to find the first existing ancestor,
canonicalizes it (which resolves 8.3 names to long names), then
appends the remaining path components.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove the complex path normalization code and try direct strip_prefix.
If both paths come from consistent sources, they should match without
any normalization. Let's see if Windows CI passes without it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…equired

Clarify in doc comments that this complexity exists because Windows paths
from different sources have incompatible formats, and without normalization
strip_prefix silently fails causing snapshots to go to wrong locations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@kormide
Copy link

kormide commented Jan 3, 2026

I'm testing this on my Bazel example but running into a couple of issues:

  1. Running tests under bazel with the following env vars, without sandboxed execution for simplicity:
env = {
    "INSTA_FORCE_PASS": "1",
    "INSTA_OUTPUT": "diff",
    "INSTA_UPDATE": "new",
    "INSTA_PENDING_DIR": "/".join(["$(RULEDIR)", "__{}_snapshots".format(name)]),
},

The value of INSTA_PENDING_DIR refers to some arbitrary location in the output tree for the .snap.new files to go.

I get the following error:

INSTA_PENDING_DIR is set but snapshot path "/home/derek/.cache/bazel/_bazel_derek/d2c61ba2af7a2993fb270be3e171b2ac/execroot/_main/src/snapshots/test__hello_world.snap" is outside workspace "/home/derek/.cache/bazel/_bazel_derek/d2c61ba2af7a2993fb270be3e171b2ac/execroot/_main". External test paths (e.g., path = "../tests/lib.rs" in Cargo.toml) are not compatible with INSTA_PENDING_DIR because the relative path would escape the pending directory.

Note that it detects /home/derek/.cache/bazel/_bazel_derek/d2c61ba2af7a2993fb270be3e171b2ac/execroot/_main as the workspace. The execroot is a tree of readonly symlinks that bazel creates mimicking the source tree and it's the directory where actions are run. This seems correct based on how I've laid out the test.

It detects /home/derek/.cache/bazel/_bazel_derek/d2c61ba2af7a2993fb270be3e171b2ac/execroot/_main/src/snapshots/test__hello_world.snap as incorrectly being outside of the workspace. My guess is that it's following symlinks back to the real source tree and detecting that as the root.

  1. In an attempt to work around the above, I tried setting INSTA_WORKSPACE_ROOT to ".", hoping it will detect the execroot directory as the root. I get:

thread 'test_foo_bar' panicked at external/rules_rust++crate+crate_index__insta-1.45.1/src/env.rs:688:21: INSTA_PENDING_DIR is set but snapshot path "./src/snapshots/test__foo_bar.snap" is outside workspace ".". External test paths (e.g., path = "../tests/lib.rs" in Cargo.toml) are not compatible with INSTA_PENDING_DIR because the relative path would escape the pending directory.

  1. Same as 2, but tried setting an empty string "" (not sure if this is valid in insta).

The .snap.new file is created, but it's created in the execroot symlink tree rather than in INSTA_PENDING_DIR, causing it to also appear in the real source tree (snapshot folder must also be a symlink). A build action writing files to the source tree wouldn't be idiomatic under Bazel, hence why we want to throw it in the output directory defined in INSTA_PENDING_DIR.

Let me know if I can clarify or test anything further.

max-sixty and others added 2 commits January 3, 2026 00:16
The previous implementation used canonicalize() as a fallback for
strip_prefix, but this caused issues with Bazel's symlinked execroot.

Testing shows direct strip_prefix works without the normalization
fallback. If Windows CI fails, we can add back a targeted fix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…allback

This preserves symlinks for Bazel's execroot (where direct strip works)
while still handling Windows path format differences (where normalization
is needed). The key insight is: try direct first, normalize only if that fails.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty
Copy link
Collaborator Author

@kormide have another look at the latest changes; I went back to using .canoncalize, and it seems to pass it the repro on bazel-examples at first glance (but am not confident)

I would need to fix for (non-bazel) Windows tests before merging; they seem to not like the symlinks...

@kormide
Copy link

kormide commented Jan 3, 2026

@kormide have another look at the latest changes; I went back to using .canoncalize, and it seems to pass it the repro on bazel-examples at first glance (but am not confident)

I would need to fix for (non-bazel) Windows tests before merging; they seem to not like the symlinks...

I updated my example in aspect-build/bazel-examples#504 to use your changes and it works great! It also vastly simplified the implementation as I no longer have to rewrite paths or do any magic in the insta wrapper script for reviewing.

One nice-to-have would be a way to remove unreferenced snapshots using cargo insta review, as removing them during tests with --unreferenced wouldn't work under Bazel. I think this would mean that an unreferenced test would need to be somehow recorded in a new file output to the pending snapshot dir, e.g., test__foo_bar.snap.unreferenced. This is not critical though as the referenced snaps could be manually deleted.

I'm not sure about the Windows issues, but is it possible that symlinks aren't enabled on your Windows ci runner? IIRC symlinks are opt-in on Windows.

max-sixty and others added 7 commits January 3, 2026 13:41
This test creates a symlinked workspace (like Bazel's execroot) and
verifies that INSTA_PENDING_DIR works correctly without following
symlinks. This would have caught the bug reported in PR mitsuhiko#852.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Document the limitation that unreferenced snapshot detection requires
direct access to the source tree, which doesn't work in Bazel's sandbox.
Reference kormide's suggestion for a potential solution using marker files.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add backticks around `INSTA_PENDING_DIR`
- Wrap URL in angle brackets

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty
Copy link
Collaborator Author

super!

I added the unreferenced issue as a TODO

I'll release this and let's see if there's more feedback on this working bazel, let's see how we do

thanks for suggesting & testing

@max-sixty max-sixty merged commit 3aa59d6 into mitsuhiko:master Jan 3, 2026
15 checks passed
@max-sixty max-sixty deleted the bazel branch January 4, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants