Skip to content

Commit

Permalink
Import scripted test for wait built-in
Browse files Browse the repository at this point in the history
The test script was copied from:
https://github.com/magicant/yash/blob/8ed979daa54384b40728617cc52e78490cb72455/tests/wait-p.tst

The test cases require a pseudo-terminal because they use the job
control facilities. The pseudo-terminal handling code is based on:
https://github.com/magicant/pseudo-terminal-wrapper
  • Loading branch information
magicant committed Dec 27, 2023
1 parent de79f8a commit e7d321e
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions yash/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ yash-syntax = { path = "../yash-syntax", version = "0.7.0" }
[dev-dependencies]
assert_matches = "1.5.0"
fuzed-iterator = "1.0.0"
nix = { version = "0.27.0", features = ["fs", "process", "term"] }
tempfile = "3.8.0"
101 changes: 101 additions & 0 deletions yash/tests/pty/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// This file is part of yash, an extended POSIX shell.
// Copyright (C) 2023 WATANABE Yuki
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Pseudo-terminal handling for scripted tests
//!
//! This module contains functionality to run a test subject in a
//! pseudo-terminal and communicate with it via the master side of the
//! pseudo-terminal. This is used for tests that depends on terminal
//! facilities such as job control.

use crate::run_with_preexec;
use nix::fcntl::{open, OFlag};
use nix::libc;
use nix::pty::{grantpt, posix_openpt, ptsname, unlockpt, PtyMaster};
use nix::sys::stat::Mode;
use nix::unistd::{close, getpgrp, setsid, tcgetpgrp};
use std::ffi::c_int;
use std::os::fd::{AsRawFd as _, FromRawFd as _, OwnedFd};
use std::path::{Path, PathBuf};
use std::sync::Mutex;

/// Runs a test subject in a pseudo-terminal.
pub fn run_with_pty(name: &str) {
let master = prepare_pty_master();
let slave_path = pty_slave_path(&master);
let slave = open_pty_slave(&slave_path);
let raw_master = master.as_raw_fd();
let raw_slave = slave.as_raw_fd();

unsafe {
run_with_preexec(name, move || {
close(raw_master)?;
prepare_as_slave(&slave_path)?;
close(raw_slave)?;
Ok(())
});
}
}

/// Prepares the master side of a pseudo-terminal.
fn prepare_pty_master() -> PtyMaster {
let master = posix_openpt(OFlag::O_RDWR | OFlag::O_NOCTTY).expect("posix_openpt failed");
grantpt(&master).expect("grantpt failed");
unlockpt(&master).expect("unlockpt failed");
master
}

/// Mutex to serialize access to `ptsname`, which is not thread-safe.
static PTSNAME_MUTEX: Mutex<()> = Mutex::new(());

/// Returns the path of the slave side of a pseudo-terminal.
fn pty_slave_path(master: &PtyMaster) -> PathBuf {
let _lock = PTSNAME_MUTEX.lock().expect("PTSNAME_MUTEX poisoned");
unsafe { ptsname(master) }.expect("ptsname failed").into()
}

/// Opens the slave side of a pseudo-terminal.
fn open_pty_slave(path: &Path) -> OwnedFd {
let raw_fd = open(path, OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty()).expect("open failed");
unsafe { OwnedFd::from_raw_fd(raw_fd) }
}

/// Prepares the slave side of a pseudo-terminal.
///
/// No memory allocation or panicking is allowed in this function because it is
/// called in a child process.
fn prepare_as_slave(slave_path: &Path) -> nix::Result<()> {
setsid()?;

// How to become the controlling process of a slave pseudo-terminal is
// implementation-dependent. We support two implementation schemes:
// (1) A process automatically becomes the controlling process when it
// first opens the terminal.
// (2) A process needs to use the TIOCSCTTY ioctl system call.
// There is a race condition in both schemes: an unrelated process could
// become the controlling process before we do, in which case the slave is
// not our controlling terminal and therefore we should abort.
let raw_fd = open(slave_path, OFlag::O_RDWR, Mode::empty())?;
// Although TIOCSCTTY is available in many Unix-like systems, it may not be
// available in some systems. Please report if you encounter such a system.
unsafe { libc::ioctl(raw_fd, libc::TIOCSCTTY, 0 as c_int) };

if tcgetpgrp(raw_fd) == Ok(getpgrp()) {
Ok(())
} else {
Err(nix::Error::ENOTSUP)
}
}
37 changes: 32 additions & 5 deletions yash/tests/scripted_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,43 @@
//! examines the results. Test cases are written in script files named with the
//! `-p.sh` or `-y.sh` suffix.

use pty::run_with_pty;
use std::os::unix::process::CommandExt as _;
use std::path::Path;
use std::process::Command;
use std::process::Stdio;

mod pty;

const BIN: &str = env!("CARGO_BIN_EXE_yash");
const TMPDIR: &str = env!("CARGO_TARGET_TMPDIR");

fn run(name: &str) {
/// Runs a test subject.
///
/// You would normally not use this function directly. Instead, use one of the
/// [`run`] or [`run_with_pty`] functions.
unsafe fn run_with_preexec<F>(name: &str, pre_exec: F)
where
F: FnMut() -> std::io::Result<()> + Send + Sync + 'static,
{
// TODO Reset signal blocking mask

let mut log_file = Path::new(TMPDIR).join(name);
log_file.set_extension("log");

let result = Command::new("sh")
let mut command = Command::new("sh");
command
.env("TMPDIR", TMPDIR)
.current_dir("tests/scripted_test")
.stdin(Stdio::null())
.arg("./run-test.sh")
.arg(BIN)
.arg(name)
.arg(&log_file)
.output()
.unwrap();
.arg(&log_file);
unsafe {
command.pre_exec(pre_exec);
}
let result = command.output().unwrap();
assert!(result.status.success(), "{:?}", result);

// The `run-test.sh` script returns a successful exit status even if there
Expand All @@ -51,6 +65,14 @@ fn run(name: &str) {
assert!(!log.contains("FAILED"), "{}", log);
}

/// Runs a test subject.
///
/// This function runs the test subject in the current session. To run it in a
/// separate session, use [`run_with_pty`].
fn run(name: &str) {
unsafe { run_with_preexec(name, || Ok(())) }
}

#[test]
fn alias() {
run("alias-p.sh")
Expand Down Expand Up @@ -237,6 +259,11 @@ fn until_loop() {
run("until-p.sh")
}

#[test]
fn wait_builtin() {
run_with_pty("wait-p.sh")
}

#[test]
fn while_loop() {
run("while-p.sh")
Expand Down
122 changes: 122 additions & 0 deletions yash/tests/scripted_test/wait-p.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# wait-p.sh: test of the wait built-in for any POSIX-compliant shell

posix="true"

test_oE 'waiting for all jobs (+m)'
echo a > a& echo b > b& echo c > c& exit 1&
wait
cat a b c
__IN__
a
b
c
__OUT__

test_OE -e 11 'waiting for specific single job (+m)'
exit 11&
wait $!
__IN__

test_OE -e 1 'waiting for specific many jobs (+m)'
exit 1& p1=$!
exit 2& p2=$!
exit 3& p3=$!
wait $p3 $p2 $p1
__IN__

test_OE -e 127 'waiting for unknown job (+m)'
exit 1&
wait $! $(($!+1))
__IN__

test_OE -e 127 'jobs are not inherited to subshells (+m, -s)'
exit 1&
p=$!
(wait $p)
__IN__

test_OE -e 127 'jobs are not inherited to subshells (+m, -c)' \
-c 'exit 1& p=$!; (wait $p)'
__IN__

test_OE -e 1 'jobs are not propagated from subshells (+m)'
exit 1&
(exit 2&)
wait $!
__IN__

test_oE 'waiting for all jobs (-m)' -m
echo a > a& echo b > b& echo c > c& exit 1&
wait
cat a b c
__IN__
a
b
c
__OUT__

test_OE -e 11 'waiting for specific single job (-m)' -m
exit 11&
wait $!
__IN__

test_OE -e 1 'waiting for specific many jobs (-m)' -m
exit 1& p1=$!
exit 2& p2=$!
exit 3& p3=$!
wait $p3 $p2 $p1
__IN__

test_OE -e 127 'waiting for unknown job (-m)' -m
exit 1&
wait $! $(($!+1))
__IN__

test_oE -e 11 'specifying job ID' -m
cat /dev/null&
echo 1&
exit 11&
wait %echo %exit
__IN__
1
__OUT__

test_OE -e 127 'jobs are not inherited to subshells (-m, -s)' -m
exit 1&
p=$!
(wait $p)
__IN__

test_OE -e 127 'jobs are not inherited to subshells (+m, -c)' \
-cm 'exit 1& p=$!; (wait $p)'
__IN__

test_OE -e 1 'jobs are not propagated from subshells (-m)' -m
exit 1&
(exit 2&)
wait $!
__IN__

: TODO Needs the kill built-in <<\__OUT__
test_oE 'trap interrupts wait' -m
interrupted=false
trap 'interrupted=true' USR1
(
set +m
trap 'echo received USR2; exit' USR2
while kill -s USR1 $$; do sleep 1; done # loop until signaled
)&
wait $!
status=$?
echo interrupted=$interrupted $((status > 128))
kill -l $status
# Now, the background job should be still running.
kill -s USR2 %
wait $!
echo waited $?
__IN__
interrupted=true 1
USR1
received USR2
waited 0
__OUT__

0 comments on commit e7d321e

Please sign in to comment.