// 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 .
//! Our scripted tests are performed by the `run-test.sh` script that runs the
//! test subject with its standard input redirected to a prepared file and then
//! 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_yash3");
const TMPDIR: &str = env!("CARGO_TARGET_TMPDIR");
/// 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(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 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);
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
// is a failed test case. Check the log file to see if there is one.
let log = std::fs::read_to_string(&log_file).unwrap();
let failures = failures(&log);
assert!(failures.is_empty(), "{failures}");
}
/// 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(())) }
}
/// Extracts the failed test cases from the log file.
fn failures(log: &str) -> String {
let mut lines = log.lines();
let mut test_case = Vec::new();
let mut result = String::new();
// Each test case in the log file is enclosed by the "%%% START: " and
// "%%% PASSED: " or "%%% FAILED: " lines. We extract lines between these
// markers and append them to the result string.
while let Some(start) = lines.find(|line| line.starts_with("%%% START: ")) {
test_case.clear();
test_case.push(start);
for line in lines.by_ref() {
if line.starts_with("%%% PASSED: ") {
// Discard this test case
break;
} else if line.starts_with("%%% FAILED: ") {
test_case.push(line);
// Add this test case to the result
for line in test_case.drain(..) {
result.push_str(line);
result.push('\n');
}
result.push('\n');
break;
} else {
test_case.push(line);
}
}
}
result
}
#[test]
fn alias() {
run("alias-p.sh")
}
#[test]
fn and_or_list() {
run("andor-p.sh")
}
#[test]
fn arithmetic_expansion() {
run("arith-p.sh")
}
#[test]
fn asynchronous_list() {
run("async-p.sh")
}
#[test]
fn bg_builtin() {
run_with_pty("bg-p.sh")
}
#[test]
fn break_builtin() {
run("break-p.sh")
}
#[test]
fn builtins() {
run("builtins-p.sh")
}
#[test]
fn case_command() {
run("case-p.sh")
}
#[test]
fn cd_builtin() {
run("cd-p.sh")
}
#[test]
fn command_builtin() {
run("command-p.sh")
}
#[test]
fn command_substitution() {
run("cmdsub-p.sh")
}
#[test]
fn comment() {
run("comment-p.sh")
}
#[test]
fn continue_builtin() {
run("continue-p.sh")
}
#[test]
fn errexit_option() {
run("errexit-p.sh")
}
#[test]
fn error_consequences() {
run("error-p.sh")
}
#[test]
fn error_consequences_ex() {
run("error-y.sh")
}
#[test]
fn eval_builtin() {
run("eval-p.sh")
}
#[test]
fn exec_builtin() {
run("exec-p.sh")
}
#[test]
fn exit_builtin() {
run("exit-p.sh")
}
#[test]
fn export_builtin() {
run("export-p.sh")
}
#[test]
fn false_builtin() {
run("false-p.sh")
}
#[test]
fn fg_builtin() {
run_with_pty("fg-p.sh")
}
#[test]
fn fnmatch() {
run("fnmatch-p.sh")
}
#[test]
fn field_splitting() {
run("fsplit-p.sh")
}
#[test]
fn for_loop() {
run("for-p.sh")
}
#[test]
fn function() {
run("function-p.sh")
}
#[test]
fn getopts_builtin() {
run("getopts-p.sh")
}
#[test]
fn grouping() {
run("grouping-p.sh")
}
#[test]
fn if_command() {
run("if-p.sh")
}
#[test]
fn input() {
run("input-p.sh")
}
#[test]
fn job_control() {
run_with_pty("job-p.sh")
}
#[test]
fn job_control_ex() {
run_with_pty("job-y.sh")
}
#[test]
fn kill_builtin_1() {
run("kill1-p.sh")
}
#[test]
fn kill_builtin_2() {
run("kill2-p.sh")
}
#[test]
fn kill_builtin_3() {
run("kill3-p.sh")
}
#[test]
fn kill_builtin_4() {
run_with_pty("kill4-p.sh")
}
#[test]
fn lineno() {
run("lineno-p.sh")
}
#[test]
fn nop_builtins() {
run("nop-p.sh")
}
#[test]
fn options() {
run("option-p.sh")
}
#[test]
fn options_ex() {
run("option-y.sh")
}
#[test]
fn parameter_expansion() {
run("param-p.sh")
}
// a.k.a. globbing
#[test]
fn pathname_expansion() {
run("path-p.sh")
}
#[test]
fn pipeline() {
run("pipeline-p.sh")
}
#[test]
fn ppid_variable() {
run("ppid-p.sh")
}
#[test]
fn quotation() {
run("quote-p.sh")
}
#[test]
fn read_builtin() {
run("read-p.sh")
}
#[test]
fn readonly_builtin() {
run("readonly-p.sh")
}
#[test]
fn redirection() {
run("redir-p.sh")
}
#[test]
fn return_builtin() {
run("return-p.sh")
}
#[test]
fn set_builtin() {
run("set-p.sh")
}
#[test]
fn shift_builtin() {
run("shift-p.sh")
}
#[test]
fn simple_command() {
run("simple-p.sh")
}
#[test]
fn source_builtin() {
run("source-p.sh")
}
#[test]
fn startup() {
run("startup-p.sh")
}
#[test]
fn startup_ex() {
run("startup-y.sh")
}
#[test]
fn tilde_expansion() {
run("tilde-p.sh")
}
// This test case also covers the behavior of the trap execution.
#[test]
fn trap_builtin() {
run("trap-p.sh")
}
#[test]
fn trap_ex_2() {
run_with_pty("trap2-y.sh")
}
#[test]
fn true_builtin() {
run("true-p.sh")
}
#[test]
fn typeset_builtin() {
run("typeset-y.sh")
}
#[test]
fn ulimit_builtin() {
run("ulimit-y.sh")
}
#[test]
fn umask_builtin() {
run("umask-p.sh")
}
#[test]
fn unset_builtin() {
run("unset-p.sh")
}
#[test]
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")
}