just 1.45.0

🤖 Just a command runner
Documentation
use {super::*, nix::sys::signal::Signal, nix::unistd::Pid, std::process::Child};

fn kill(child: &Child, signal: Signal) {
  nix::sys::signal::kill(Pid::from_raw(child.id().try_into().unwrap()), signal).unwrap();
}

fn interrupt_test(arguments: &[&str], justfile: &str) {
  let tmp = tempdir();
  let mut justfile_path = tmp.path().to_path_buf();
  justfile_path.push("justfile");
  fs::write(justfile_path, unindent(justfile)).unwrap();

  let start = Instant::now();

  let mut child = Command::new(executable_path("just"))
    .current_dir(&tmp)
    .args(arguments)
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn()
    .expect("just invocation failed");

  while start.elapsed() < Duration::from_millis(500) {}

  kill(&child, Signal::SIGINT);

  let status = child.wait().unwrap();

  let elapsed = start.elapsed();

  assert!(
    elapsed <= Duration::from_secs(2),
    "process returned too late: {elapsed:?}"
  );

  assert!(
    elapsed >= Duration::from_millis(100),
    "process returned too early : {elapsed:?}"
  );

  assert_eq!(status.code(), Some(130));
}

#[test]
#[ignore]
fn interrupt_shebang() {
  interrupt_test(
    &[],
    "
        default:
          #!/usr/bin/env sh
          sleep 1
      ",
  );
}

#[test]
#[ignore]
fn interrupt_line() {
  interrupt_test(
    &[],
    "
        default:
          @sleep 1
      ",
  );
}

#[test]
#[ignore]
fn interrupt_backtick() {
  interrupt_test(
    &[],
    "
        foo := `sleep 1`

        default:
          @echo {{foo}}
      ",
  );
}

#[test]
#[ignore]
fn interrupt_command() {
  interrupt_test(&["--command", "sleep", "1"], "");
}

// This test is ignored because it is sensitive to the process signal mask.
// Programs like `watchexec` and `cargo-watch` change the signal mask to ignore
// `SIGHUP`, which causes this test to fail.
#[test]
#[ignore]
fn forwarding() {
  let just = executable_path("just");

  let tempdir = tempdir();

  fs::write(
    tempdir.path().join("justfile"),
    "foo:\n @{{just_executable()}} --request '\"signal\"'",
  )
  .unwrap();

  for signal in [Signal::SIGINT, Signal::SIGQUIT, Signal::SIGHUP] {
    let mut child = Command::new(&just)
      .current_dir(&tempdir)
      .stdout(Stdio::piped())
      .stderr(Stdio::piped())
      .spawn()
      .unwrap();

    // wait for child to start
    thread::sleep(Duration::from_millis(500));

    // send non-forwarded signal
    kill(&child, signal);

    // wait for child to receive signal
    thread::sleep(Duration::from_millis(500));

    // assert that child does not exit, because signal is not forwarded
    assert!(child.try_wait().unwrap().is_none());

    // send forwarded signal
    kill(&child, Signal::SIGTERM);

    // child exits
    let output = child.wait_with_output().unwrap();

    let status = output.status;
    let stderr = str::from_utf8(&output.stderr).unwrap();
    let stdout = str::from_utf8(&output.stdout).unwrap();

    let mut failures = 0;

    if status.code() != Some(128 + signal as i32) {
      failures += 1;
      eprintln!("unexpected status: {status}");
    }

    // just reports that it was interrupted by first, non-forwarded signal
    if stderr != format!("error: Interrupted by {signal}\n") {
      failures += 1;
      eprintln!("unexpected stderr: {stderr}");
    }

    // child reports that it was terminated by forwarded signal
    if stdout != r#"{"signal":"SIGTERM"}"# {
      failures += 1;
      eprintln!("unexpected stdout: {stdout}");
    }

    assert!(failures == 0, "{failures} failures");
  }
}

#[test]
#[ignore]
#[cfg(any(
  target_os = "dragonfly",
  target_os = "freebsd",
  target_os = "ios",
  target_os = "macos",
  target_os = "netbsd",
  target_os = "openbsd",
))]
fn siginfo_prints_current_process() {
  let just = executable_path("just");

  let tempdir = tempdir();

  fs::write(tempdir.path().join("justfile"), "foo:\n @sleep 1").unwrap();

  let child = Command::new(&just)
    .current_dir(&tempdir)
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn()
    .unwrap();

  thread::sleep(Duration::from_millis(500));

  kill(&child, Signal::SIGINFO);

  let output = child.wait_with_output().unwrap();

  let status = output.status;
  let stderr = str::from_utf8(&output.stderr).unwrap();
  let stdout = str::from_utf8(&output.stdout).unwrap();

  let mut failures = 0;

  if !status.success() {
    failures += 1;
    eprintln!("unexpected status: {status}");
  }

  let re =
    Regex::new(r#"just \d+: 1 child process:\n\d+: cd ".*" && "sh" "-cu" "sleep 1"\n"#).unwrap();

  if !re.is_match(stderr) {
    failures += 1;
    eprintln!("unexpected stderr: {stderr}");
  }

  if !stdout.is_empty() {
    failures += 1;
    eprintln!("unexpected stdout: {stdout}");
  }

  assert!(failures == 0, "{failures} failures");
}