Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/commands/land/v2/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,10 +674,14 @@ fn pinned_roots(step: &Value, roots: &Roots, journal: &Journal, phase: &str) ->
.get(&repo)
.context("pinned checkout binding missing")?;
let rev = rev.as_str().context("revision must be string")?;
// Keep the checkout directory short: this path sits deep under the
// workspace, and a full 64-character hash pushes it past Windows'
// 260-character limit (`fatal: '$GIT_DIR' too big`). Twelve hex
// characters still separate every repo and revision in one run.
let checkout = journal
.path
.with_extension("checkouts")
.join(canonical_hash(&json!([repo, rev])));
.join(&canonical_hash(&json!([repo, rev]))[..12]);
if !checkout.exists() {
if git(root, &["cat-file", "-e", &format!("{rev}^{{commit}}")]).is_err() {
let identity = super::mergeability::repo_identity(&snapshot["sourceBundle"], &repo);
Expand Down
40 changes: 40 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,39 @@ pub fn isolated_git_config_global() -> String {
dir.join("gitconfig").to_string_lossy().to_string()
}

/// A stable temp directory private to one workspace path: the same cwd always
/// maps to the same directory, and different workspaces map apart.
fn workspace_temp_dir(cwd: &Path) -> PathBuf {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
cwd.hash(&mut hasher);
let dir = dunce_canonicalize_or(std::env::temp_dir()).join(format!(
"knit-test-{}-{:x}",
std::process::id(),
hasher.finish()
));
fs::create_dir_all(&dir).unwrap();
dir
}

/// Give a spawned `knit` process a temp directory private to its workspace.
///
/// Landing ownership locks live in `std::env::temp_dir()` and are keyed only
/// by project id, which most fixtures share (`demo`): parallel tests that land
/// at the same time would otherwise fight over one lock file (`create_new`
/// fails while another test holds it, `os error 80` on Windows). A temp dir
/// keyed by workspace keeps locks shared only inside a single test. Windows
/// resolves the temp dir from `TMP`/`TEMP` and Unix from `TMPDIR`, so set all
/// three. Applied before test-provided env, a test can still override.
pub fn private_temp_env(cwd: &Path, command: &mut Command) {
let temp = workspace_temp_dir(cwd).into_os_string();
command
.env("TMPDIR", &temp)
.env("TMP", &temp)
.env("TEMP", &temp);
}

/// Ambient identity overrides (exported by editor/agent harnesses such as a
/// T3 Code session) would silently override the per-repo `git config` identity
/// every fixture sets and inject actor attribution, breaking actor/author
Expand Down Expand Up @@ -470,6 +503,7 @@ where
.env("GIT_CONFIG_GLOBAL", isolated_git_config_global())
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION");
private_temp_env(cwd, &mut command);
scrub_ambient_git_identity(&mut command);
run(command)
}
Expand All @@ -487,6 +521,7 @@ where
.env("GIT_CONFIG_GLOBAL", isolated_git_config_global())
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION");
private_temp_env(cwd, &mut command);
scrub_ambient_git_identity(&mut command);
// Test-provided env wins, so a test can still point KNIT_HOME at its own dir.
for (key, value) in env {
Expand All @@ -508,6 +543,7 @@ where
.env("GIT_CONFIG_GLOBAL", isolated_git_config_global())
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION");
private_temp_env(cwd, &mut command);
scrub_ambient_git_identity(&mut command);
let output = command.output().unwrap();
assert!(!output.status.success());
Expand All @@ -531,6 +567,7 @@ where
.env("GIT_CONFIG_GLOBAL", isolated_git_config_global())
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION");
private_temp_env(cwd, &mut command);
scrub_ambient_git_identity(&mut command);
for (key, value) in env {
command.env(key, value);
Expand Down Expand Up @@ -578,6 +615,7 @@ where
.env_remove("KNIT_SESSION")
.env("PATH", path)
.env("GH_FAKE_DIR", fake_gh_dir);
private_temp_env(cwd, &mut command);
scrub_ambient_git_identity(&mut command);
for (key, value) in env {
command.env(key, value);
Expand Down Expand Up @@ -624,6 +662,7 @@ where
.env_remove("KNIT_SESSION")
.env("PATH", path)
.env("GH_FAKE_DIR", fake_gh_dir);
private_temp_env(cwd, &mut command);
scrub_ambient_git_identity(&mut command);
for (key, value) in env {
command.env(key, value);
Expand Down Expand Up @@ -663,6 +702,7 @@ where
.env_remove("KNIT_SESSION")
.env("PATH", path)
.env("FORGE_FAKE_DIR", fake_dir);
private_temp_env(cwd, &mut command);
scrub_ambient_forge_env(&mut command);
scrub_ambient_git_identity(&mut command);
for (key, value) in env {
Expand Down
9 changes: 5 additions & 4 deletions tests/land.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,16 +1283,17 @@ fn terminating_knit_cancels_deployment_tree_and_records_failure() {

knit(&workspace, ["bundle", "cancelled deploy"]);
knit(&workspace, ["land", "plan"]);
let child = Command::new(env!("CARGO_BIN_EXE_knit"))
let mut child = Command::new(env!("CARGO_BIN_EXE_knit"));
child
.args(["land", "apply"])
.current_dir(&workspace)
.env("KNIT_HOME", isolated_knit_home())
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
.stderr(Stdio::piped());
private_temp_env(&workspace, &mut child);
let child = child.spawn().unwrap();

let marker_deadline = Instant::now() + Duration::from_secs(5);
while !child_pid_path.exists() && Instant::now() < marker_deadline {
Expand Down
9 changes: 5 additions & 4 deletions tests/land_fork_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ impl Fixture {
}
}
fn cmd(&self, args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_knit"))
let mut command = Command::new(env!("CARGO_BIN_EXE_knit"));
command
.current_dir(&self.root)
.args(args)
.env_remove("KNIT_BUNDLE")
Expand All @@ -198,9 +199,9 @@ impl Fixture {
.env("GIT_CONFIG_GLOBAL", isolated_git_config_global())
.env("GH_TOKEN", "synthetic")
.env("KNIT_GITHUB_API_TRANSPORT", "native")
.env("KNIT_GITHUB_API_BASE", &self.api)
.output()
.unwrap()
.env("KNIT_GITHUB_API_BASE", &self.api);
private_temp_env(&self.root, &mut command);
command.output().unwrap()
}
fn ok(&self, args: &[&str]) -> String {
let o = self.cmd(args);
Expand Down
9 changes: 5 additions & 4 deletions tests/landing_gates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ else: raise Exception('unexpected forge call '+str(a))
.chain(std::env::split_paths(&std::env::var_os("PATH").unwrap())),
)
.unwrap();
Command::new(env!("CARGO_BIN_EXE_knit"))
let mut command = Command::new(env!("CARGO_BIN_EXE_knit"));
command
.current_dir(&self.workspace)
.args(args)
.env("PATH", path)
Expand All @@ -142,9 +143,9 @@ else: raise Exception('unexpected forge call '+str(a))
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION")
.env_remove("GH_TOKEN")
.env_remove("GITHUB_TOKEN")
.output()
.unwrap()
.env_remove("GITHUB_TOKEN");
private_temp_env(&self.workspace, &mut command);
command.output().unwrap()
}
fn run_path(&self) -> PathBuf {
fs::read_dir(self.workspace.join(".knit/land-runs"))
Expand Down
9 changes: 5 additions & 4 deletions tests/landing_sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,15 @@ impl Fixture {
}

fn cmd(&self, args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_knit"))
let mut command = Command::new(env!("CARGO_BIN_EXE_knit"));
command
.current_dir(&self.root)
.args(args)
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION")
.env("KNIT_HOME", self.root.join("home"))
.output()
.unwrap()
.env("KNIT_HOME", self.root.join("home"));
private_temp_env(&self.root, &mut command);
command.output().unwrap()
}

fn plan(&self) -> Value {
Expand Down
13 changes: 8 additions & 5 deletions tests/landing_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,15 @@ elif sys.argv[1]=='verify': assert state.read_text()==json.loads(os.environ['KNI
}
}
fn cmd(&self, args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_knit"))
let mut command = Command::new(env!("CARGO_BIN_EXE_knit"));
command
.current_dir(&self.root)
.args(args)
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION")
.env("KNIT_HOME", self.root.join("home"))
.output()
.unwrap()
.env("KNIT_HOME", self.root.join("home"));
private_temp_env(&self.root, &mut command);
command.output().unwrap()
}
}
impl Drop for Fixture {
Expand Down Expand Up @@ -181,7 +182,9 @@ fn ordinary_commands_stream_before_exit_and_keep_receipts_in_both_output_modes()
drop(send);
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let deadline = Instant::now() + Duration::from_secs(5);
// Windows runners start `sh`/Python slowly enough that five seconds
// was flaky; the step's own timeout still bounds the command.
let deadline = Instant::now() + Duration::from_secs(30);
while !(String::from_utf8_lossy(if json_mode { &stderr } else { &stdout })
.contains("LIVE-STDOUT")
&& String::from_utf8_lossy(&stderr).contains("LIVE-STDERR"))
Expand Down
1 change: 1 addition & 0 deletions tests/revert_fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ impl Landed {
.env("GIT_CONFIG_GLOBAL", isolated_git_config_global())
.env_remove("KNIT_BUNDLE")
.env_remove("KNIT_SESSION");
private_temp_env(&self.workspace, &mut command);
scrub_ambient_forge_env(&mut command);
for (key, value) in &self.env {
command.env(key, value);
Expand Down
40 changes: 3 additions & 37 deletions tests/revert_landed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,9 @@ use std::path::{Path, PathBuf};
const BUNDLE: &str = "venue-capacity";

// Landing ownership locks live in the temp directory and are keyed by project,
// and every test here uses project `demo`; a private temp directory keeps
// parallel tests apart. Windows takes it from TMP/TEMP rather than TMPDIR.
fn private_tmp(fake_bin: &Path) -> String {
let dir = fake_bin.parent().unwrap().join("tmp");
fs::create_dir_all(&dir).unwrap();
dir.to_string_lossy().into_owned()
}

fn knit_with_fake_gh<I, S>(cwd: &Path, args: I, fake_bin: &Path, fake_gh_dir: &Path) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
let tmp = private_tmp(fake_bin);
knit_with_fake_gh_env(
cwd,
args,
fake_bin,
fake_gh_dir,
&[("TMPDIR", &tmp), ("TMP", &tmp), ("TEMP", &tmp)],
)
}

fn knit_fails_with_fake_gh<I, S>(cwd: &Path, args: I, fake_bin: &Path, fake_gh_dir: &Path) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
let tmp = private_tmp(fake_bin);
knit_fails_with_fake_gh_env(
cwd,
args,
fake_bin,
fake_gh_dir,
&[("TMPDIR", &tmp), ("TMP", &tmp), ("TEMP", &tmp)],
)
}
// and every test here uses project `demo`; the common test harness gives each
// workspace its own temp directory (TMP/TEMP/TMPDIR), keeping parallel tests
// apart on every platform.

/// Two-repo project whose landing merges backend before frontend, published
/// as `venue-capacity`.
Expand Down
Loading