aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-01-27 02:32:55 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-01-31 15:54:33 +0300
commit749f748ae6ded6e229214d2dddf3c45482bffbd3 (patch)
treec504c8b225db01c8c152b3772467d16c036a42fc
parente61a287832532124a1a90a8bb9cc0f61f3a4db92 (diff)
Add support for test command pipe, expression and command-if
-rw-r--r--build2/test/script/parser.cxx21
-rw-r--r--build2/test/script/runner.cxx480
-rw-r--r--build2/test/script/script2
-rw-r--r--tests/test/script/runner/buildfile2
-rw-r--r--tests/test/script/runner/expr.test522
-rw-r--r--tests/test/script/runner/if.test25
-rw-r--r--tests/test/script/runner/pipe.test35
-rw-r--r--tests/test/script/runner/redirect.test52
-rw-r--r--unit-tests/test/script/parser/pipe-expr.test44
9 files changed, 973 insertions, 210 deletions
diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx
index 0b00861..5822d9e 100644
--- a/build2/test/script/parser.cxx
+++ b/build2/test/script/parser.cxx
@@ -1367,17 +1367,23 @@ namespace build2
// leave: <newline>
command_expr expr;
- expr.emplace_back (expr_term ());
+
+ // OR-ed to an implied false for the first term.
+ //
+ expr.push_back ({expr_operator::log_or, command_pipe ()});
command c; // Command being assembled.
// Make sure the command makes sense.
//
- auto check_command = [&c, this] (const location& l)
+ auto check_command = [&c, this] (const location& l, bool last)
{
if (c.out.type == redirect_type::merge &&
c.err.type == redirect_type::merge)
fail (l) << "stdout and stderr redirected to each other";
+
+ if (!last && c.out.type != redirect_type::none)
+ fail (l) << "stdout is both redirected and piped";
};
// Check that the introducer character differs from '/' if the
@@ -1629,7 +1635,7 @@ namespace build2
// Parse the redirect operator.
//
auto parse_redirect =
- [&c, &p, &mod, this] (token& t, const location& l)
+ [&c, &expr, &p, &mod, this] (token& t, const location& l)
{
// Our semantics is the last redirect seen takes effect.
//
@@ -1676,6 +1682,9 @@ namespace build2
if ((fd = fd == 3 ? 0 : fd) != 0)
fail (l) << "invalid in redirect file descriptor " << fd;
+ if (!expr.back ().pipe.empty ())
+ fail (l) << "stdin is both piped and redirected";
+
break;
}
case type::out_pass:
@@ -2030,7 +2039,7 @@ namespace build2
{
// Check that the previous command makes sense.
//
- check_command (l);
+ check_command (l, tt != type::pipe);
expr.back ().pipe.push_back (move (c));
c = command ();
@@ -2302,7 +2311,7 @@ namespace build2
{
// Check that the previous command makes sense.
//
- check_command (l);
+ check_command (l, tt != type::pipe);
expr.back ().pipe.push_back (move (c));
c = command ();
@@ -2373,7 +2382,7 @@ namespace build2
// command makes sense.
//
check_pending (l);
- check_command (l);
+ check_command (l, true);
expr.back ().pipe.push_back (move (c));
}
diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx
index 2eefdb7..dcfaec9 100644
--- a/build2/test/script/runner.cxx
+++ b/build2/test/script/runner.cxx
@@ -218,13 +218,14 @@ namespace build2
// Check if the test command output matches the expected result (redirect
// value). Noop for redirect types other than none, here_*.
//
- static void
+ static bool
check_output (const path& pr,
const path& op,
const path& ip,
const redirect& rd,
const location& ll,
scope& sp,
+ bool diag,
const char* what)
{
auto input_info = [&ip, &ll] (diag_record& d)
@@ -246,29 +247,35 @@ namespace build2
if (rd.type == redirect_type::none)
{
- assert (!op.empty ());
-
// Check that there is no output produced.
//
- if (non_empty (op, ll))
+ assert (!op.empty ());
+
+ if (!non_empty (op, ll))
+ return true;
+
+ if (diag)
{
- diag_record d (fail (ll));
+ diag_record d (error (ll));
d << pr << " unexpectedly writes to " << what <<
info << what << ": " << op;
input_info (d);
}
+
+ // Fall through (to return false).
+ //
}
else if (rd.type == redirect_type::here_str_literal ||
rd.type == redirect_type::here_doc_literal ||
(rd.type == redirect_type::file &&
rd.file.mode == redirect_fmode::compare))
{
- assert (!op.empty ());
-
// The expected output is provided as a file or as a string. Save the
// string to a file in the later case.
//
+ assert (!op.empty ());
+
path eop;
if (rd.type == redirect_type::file)
@@ -327,21 +334,24 @@ namespace build2
efd.reset ();
if (p.wait ())
- return;
+ return true;
// Output doesn't match the expected result.
//
- diag_record d (error (ll));
- d << pr << " " << what << " doesn't match the expected output";
+ if (diag)
+ {
+ diag_record d (error (ll));
+ d << pr << " " << what << " doesn't match the expected output";
- output_info (d, op);
- output_info (d, eop, "expected ");
- output_info (d, ep, "", " diff");
- input_info (d);
+ output_info (d, op);
+ output_info (d, eop, "expected ");
+ output_info (d, ep, "", " diff");
+ input_info (d);
- print_file (d, ep, ll);
+ print_file (d, ep, ll);
+ }
- // Fall through.
+ // Fall through (to return false).
//
}
catch (const process_error& e)
@@ -350,15 +360,13 @@ namespace build2
if (e.child ())
exit (1);
- }
- throw failed ();
+ throw failed ();
+ }
}
else if (rd.type == redirect_type::here_str_regex ||
rd.type == redirect_type::here_doc_regex)
{
- assert (!op.empty ());
-
// The overall plan is:
//
// 1. Create regex line string. While creating it's line characters
@@ -377,6 +385,8 @@ namespace build2
//
using namespace regex;
+ assert (!op.empty ());
+
// Create regex line string.
//
line_pool pool;
@@ -621,17 +631,31 @@ namespace build2
// Match the output with the regex.
//
if (regex_match (ls, regex)) // Doesn't throw.
- return;
+ return true;
- // Output doesn't match the regex.
+ // Output doesn't match the regex. We save the regex to file for
+ // troubleshooting regardless of whether we print the diagnostics or
+ // not.
//
- diag_record d (fail (ll));
- d << pr << " " << what << " doesn't match the regex";
+ path rp (save_regex ());
- output_info (d, op);
- output_info (d, save_regex (), "", " regex");
- input_info (d);
+ if (diag)
+ {
+ diag_record d (error (ll));
+ d << pr << " " << what << " doesn't match the regex";
+
+ output_info (d, op);
+ output_info (d, rp, "", " regex");
+ input_info (d);
+ }
+
+ // Fall through (to return false).
+ //
}
+ else // Noop.
+ return true;
+
+ return false;
}
bool default_runner::
@@ -750,10 +774,26 @@ namespace build2
: sp.wd_path.directory ());
}
- void default_runner::
- run (scope& sp, const command_expr& expr, size_t li, const location& ll)
+ static bool
+ run_pipe (scope& sp,
+ command_pipe::const_iterator bc,
+ command_pipe::const_iterator ec,
+ auto_fd ifd,
+ size_t ci, size_t li, const location& ll,
+ bool diag)
{
- const command& c (expr.back ().pipe.back ()); // @@ TMP
+ if (bc == ec) // End of the pipeline.
+ return true;
+
+ // The overall plan is to run the first command in the pipe, reading
+ // its input from the file descriptor passed (or, for the first
+ // command, according to stdin redirect specification) and redirecting
+ // its output to the right-hand part of the pipe recursively. Fail if
+ // the right-hand part fails. Otherwise check the process exit code,
+ // match stderr (and stdout for the last command in the pipe) according
+ // to redirect specification(s) and fail if any of the above fails.
+ //
+ const command& c (*bc);
if (verb >= 3)
text << c;
@@ -785,7 +825,7 @@ namespace build2
// Create a unique path for a command standard stream cache file.
//
- auto std_path = [&li, &sp, &ll] (const char* n) -> path
+ auto std_path = [&sp, &ci, &li, &ll] (const char* n) -> path
{
path p (n);
@@ -795,146 +835,145 @@ namespace build2
if (li > 0)
p += "-" + to_string (li);
+ // 0 if belongs to a single-command expression, otherwise is the
+ // command number (start from one) in the expression.
+ //
+ // Note that the name like stdin-N can relate to N-th command of a
+ // single-line test or to N-th single-command line of multi-line
+ // test. These cases are mutually exclusive and so are unambiguous.
+ //
+ if (ci > 0)
+ p += "-" + to_string (ci);
+
return normalize (move (p), sp, ll);
};
- // Assign file descriptors to pass as a builtin or a process standard
- // streams. Eventually the raw descriptors should gone when the process
- // is fully moved to auto_fd usage.
+ // If stdin file descriptor is not open then this is the first pipeline
+ // command. Open stdin descriptor according to the redirect specified.
//
path isp;
- auto_fd ifd;
- int id (0); // @@ TMP
const redirect& in (c.in.effective ());
- // Open a file for passing to the command stdin.
- //
- auto open_stdin = [&isp, &ifd, &id, &ll] ()
+ if (ifd.get () != -1)
+ assert (in.type == redirect_type::none); // No redirect expected.
+ else
{
- assert (!isp.empty ());
-
- try
- {
- ifd = fdopen (isp, fdopen_mode::in);
- id = ifd.get ();
- }
- catch (const io_error& e)
+ // Open a file for passing to the command stdin.
+ //
+ auto open_stdin = [&isp, &ifd, &ll] ()
{
- fail (ll) << "unable to read " << isp << ": " << e;
- }
- };
+ assert (!isp.empty ());
- switch (in.type)
- {
- case redirect_type::pass:
- {
try
{
- ifd = fddup (id);
- id = 0;
+ ifd = fdopen (isp, fdopen_mode::in);
}
catch (const io_error& e)
{
- fail (ll) << "unable to duplicate stdin: " << e;
+ fail (ll) << "unable to read " << isp << ": " << e;
}
+ };
- break;
- }
-
- case redirect_type::none:
- // Somehow need to make sure that the child process doesn't read from
- // stdin. That is tricky to do in a portable way. Here we suppose
- // that the program which (erroneously) tries to read some data from
- // stdin being redirected to /dev/null fails not being able to read
- // the expected data, and so the test doesn't pass through.
- //
- // @@ Obviously doesn't cover the case when the process reads
- // whatever available.
- // @@ Another approach could be not to redirect stdin and let the
- // process to hang which can be interpreted as a test failure.
- // @@ Both ways are quite ugly. Is there some better way to do this?
- //
- // Fall through.
- //
- case redirect_type::null:
+ switch (in.type)
{
- try
+ case redirect_type::pass:
{
- ifd.reset (fdnull ()); // @@ Eventually will be throwing.
-
- if (ifd.get () == -1) // @@ TMP
- throw io_error (
- error_code (errno, system_category ()).message ());
+ try
+ {
+ ifd = fddup (0);
+ }
+ catch (const io_error& e)
+ {
+ fail (ll) << "unable to duplicate stdin: " << e;
+ }
- id = -2;
+ break;
}
- catch (const io_error& e)
+
+ case redirect_type::none:
+ // Somehow need to make sure that the child process doesn't read
+ // from stdin. That is tricky to do in a portable way. Here we
+ // suppose that the program which (erroneously) tries to read some
+ // data from stdin being redirected to /dev/null fails not being
+ // able to read the expected data, and so the test doesn't pass
+ // through.
+ //
+ // @@ Obviously doesn't cover the case when the process reads
+ // whatever available.
+ // @@ Another approach could be not to redirect stdin and let the
+ // process to hang which can be interpreted as a test failure.
+ // @@ Both ways are quite ugly. Is there some better way to do
+ // this?
+ //
+ // Fall through.
+ //
+ case redirect_type::null:
{
- fail (ll) << "unable to write to null device: " << e;
+ try
+ {
+ ifd.reset (fdnull ()); // @@ Eventually will be throwing.
+
+ if (ifd.get () == -1) // @@ TMP
+ throw io_error (
+ error_code (errno, system_category ()).message ());
+ }
+ catch (const io_error& e)
+ {
+ fail (ll) << "unable to write to null device: " << e;
+ }
+
+ break;
}
- break;
- }
+ case redirect_type::file:
+ {
+ isp = normalize (in.file.path, sp, ll);
- case redirect_type::file:
- {
- isp = normalize (in.file.path, sp, ll);
+ open_stdin ();
+ break;
+ }
- open_stdin ();
- break;
- }
+ case redirect_type::here_str_literal:
+ case redirect_type::here_doc_literal:
+ {
+ // We could write to the command stdin directly but instead will
+ // cache the data for potential troubleshooting.
+ //
+ isp = std_path ("stdin");
- case redirect_type::here_str_literal:
- case redirect_type::here_doc_literal:
- {
- // We could write to the command stdin directly but instead will
- // cache the data for potential troubleshooting.
- //
- isp = std_path ("stdin");
+ save (
+ isp, transform (in.str, false, in.modifiers, *sp.root), ll);
- save (isp, transform (in.str, false, in.modifiers, *sp.root), ll);
- sp.clean ({cleanup_type::always, isp}, true);
+ sp.clean ({cleanup_type::always, isp}, true);
- open_stdin ();
- break;
- }
+ open_stdin ();
+ break;
+ }
- case redirect_type::merge:
- case redirect_type::here_str_regex:
- case redirect_type::here_doc_regex:
- case redirect_type::here_doc_ref: assert (false); break;
+ case redirect_type::merge:
+ case redirect_type::here_str_regex:
+ case redirect_type::here_doc_regex:
+ case redirect_type::here_doc_ref: assert (false); break;
+ }
}
- // Dealing with stdout and stderr redirect types other than 'null'
- // using pipes is tricky in the general case. Going this path we would
- // need to read both streams in non-blocking manner which we can't
- // (easily) do in a portable way. Using diff utility to get a
- // nice-looking actual/expected outputs difference would complicate
- // things further.
- //
- // So the approach is the following. Child standard streams are
- // redirected to files. When the child exits and the exit status is
- // validated we just sequentially compare each file content with the
- // expected output. The positive side-effect of this approach is that
- // the output of a faulty command can be provided for troubleshooting.
- //
-
// Open a file for command output redirect if requested explicitly
- // (file redirect) or for the purpose of the output validation (none,
- // here_*), register the file for cleanup, return the file descriptor.
- // Return the specified, default or -2 file descriptors for merge, pass
- // or null redirects respectively not opening a file.
+ // (file overwrite/append redirects) or for the purpose of the output
+ // validation (none, here_*, file comparison redirects), register the
+ // file for cleanup, return the file descriptor. Return nullfd,
+ // standard stream descriptor duplicate or null-device descriptor for
+ // merge, pass or null redirects respectively (not opening any file).
//
auto open = [&sp, &ll, &std_path] (const redirect& r,
int dfd,
- path& p,
- auto_fd& fd) -> int
- {
+ path& p) -> auto_fd
+ {
assert (dfd == 1 || dfd == 2);
const char* what (dfd == 1 ? "stdout" : "stderr");
fdopen_mode m (fdopen_mode::out | fdopen_mode::create);
+ auto_fd fd;
switch (r.type)
{
case redirect_type::pass:
@@ -948,7 +987,7 @@ namespace build2
fail (ll) << "unable to duplicate " << what << ": " << e;
}
- return dfd;
+ return fd;
}
case redirect_type::null:
@@ -966,14 +1005,14 @@ namespace build2
fail (ll) << "unable to write to null device: " << e;
}
- return -2;
+ return fd;
}
case redirect_type::merge:
{
// Duplicate the paired file descriptor later.
//
- return r.fd;
+ return fd; // nullfd
}
case redirect_type::file:
@@ -1020,18 +1059,51 @@ namespace build2
fail (ll) << "unable to write " << p << ": " << e;
}
- return fd.get ();
+ return fd;
};
path osp;
- auto_fd ofd;
const redirect& out (c.out.effective ());
- int od (open (out, 1, osp, ofd));
+ auto_fd ofd;
+
+ // If this is the last command in the pipeline than redirect the
+ // command process stdout to a file. Otherwise create a pipe and
+ // redirect the stdout to the write-end of the pipe. The read-end will
+ // be passed as stdin for the next command in the pipeline.
+ //
+ // @@ Shouldn't we allow the here-* and file output redirects for a
+ // command with pipelined output? Say if such redirect is present
+ // then the process output is redirected to a file first (as it is
+ // when no output pipelined), and only after the process exit code
+ // and the output are validated the next command in the pipeline is
+ // executed taking the file as an input. This could be usefull for
+ // test failures investigation and for tests "tightening".
+ //
+ fdpipe p;
+ command_pipe::const_iterator nc (bc + 1);
+ bool last (nc == ec);
+
+ if (last)
+ ofd = open (out, 1, osp);
+ else
+ {
+ assert (out.type == redirect_type::none); // No redirect expected.
+
+ try
+ {
+ p = fdopen_pipe ();
+ }
+ catch (const io_error& e)
+ {
+ fail (ll) << "unable to open pipe: " << e;
+ }
+
+ ofd = move (p.out);
+ }
path esp;
- auto_fd efd;
const redirect& err (c.err.effective ());
- int ed (open (err, 2, esp, efd));
+ auto_fd efd (open (err, 2, esp));
// Merge standard streams.
//
@@ -1053,9 +1125,15 @@ namespace build2
}
}
+ // All descriptors should be open to the date.
+ //
+ assert (ifd.get () != -1 && ofd.get () != -1 && efd.get () != -1);
+
optional<process_exit> exit;
builtin* b (builtins.find (c.program.string ()));
+ bool success;
+
if (b != nullptr)
{
// Execute the builtin.
@@ -1065,12 +1143,14 @@ namespace build2
future<uint8_t> f (
(*b) (sp, c.arguments, move (ifd), move (ofd), move (efd)));
+ success = run_pipe (sp, nc, ec, move (p.in), ci + 1, li, ll, diag);
+
exit = process_exit (f.get ());
}
catch (const system_error& e)
{
fail (ll) << "unable to execute " << c.program << " builtin: "
- << e;
+ << e << endf;
}
}
else
@@ -1094,13 +1174,16 @@ namespace build2
process pr (sp.wd_path.string ().c_str (),
pp,
args.data (),
- id, od, ed);
+ ifd.get (), ofd.get (), efd.get ());
ifd.reset ();
ofd.reset ();
efd.reset ();
+ success = run_pipe (sp, nc, ec, move (p.in), ci + 1, li, ll, diag);
+
pr.wait ();
+
exit = move (pr.exit);
}
catch (const process_error& e)
@@ -1116,11 +1199,19 @@ namespace build2
assert (exit);
- const path& p (c.program);
+ // If the righ-hand side pipeline failed than the whole pipeline fails,
+ // and no further checks are required.
+ //
+ if (!success)
+ return false;
+
+ const path& pr (c.program);
- // If there is no correct exit code by whatever reason then print the
- // proper diagnostics, dump stderr (if cached and not too large) and
- // fail.
+ // If there is no valid exit code available by whatever reason then we
+ // print the proper diagnostics, dump stderr (if cached and not too
+ // large) and fail the whole test. Otherwise if the exit code is not
+ // correct then we print diagnostics if requested and fail the
+ // pipeline.
//
bool valid (exit->normal ());
@@ -1133,17 +1224,18 @@ namespace build2
#endif
bool eq (c.exit.comparison == exit_comparison::eq);
- bool correct (valid && eq == (exit->code () == c.exit.status));
+ success = valid && eq == (exit->code () == c.exit.status);
- if (!correct)
+ if (!valid || (!success && diag))
{
- // Fail with a proper diagnostics.
+ // In the presense of a valid exit code we print the diagnostics and
+ // return false rather than throw.
//
- diag_record d (fail (ll));
+ diag_record d (valid ? error (ll) : fail (ll));
if (!exit->normal ())
{
- d << p << " terminated abnormally" <<
+ d << pr << " terminated abnormally" <<
info << exit->description ();
#ifndef _WIN32
@@ -1156,11 +1248,14 @@ namespace build2
uint16_t ec (exit->code ()); // Make sure is printed as integer.
if (!valid)
- d << p << " exit status " << ec << " is invalid" <<
+ d << pr << " exit status " << ec << " is invalid" <<
info << "must be an unsigned integer < 256";
- else if (!correct)
- d << p << " exit status " << ec << (eq ? " != " : " == ")
- << static_cast<uint16_t> (c.exit.status);
+ else if (!success)
+ {
+ if (diag)
+ d << pr << " exit status " << ec << (eq ? " != " : " == ")
+ << static_cast<uint16_t> (c.exit.status);
+ }
else
assert (false);
}
@@ -1179,18 +1274,83 @@ namespace build2
print_file (d, esp, ll);
}
- // Exit code is correct. Check if the standard outputs match the
- // expectations.
+ // If exit code is correct then check if the standard outputs match the
+ // expectations. Note that stdout is only redirected to file for the
+ // last command in the pipeline.
//
- check_output (p, osp, isp, out, ll, sp, "stdout");
- check_output (p, esp, isp, err, ll, sp, "stderr");
+ if (success)
+ success =
+ (!last ||
+ check_output (pr, osp, isp, out, ll, sp, diag, "stdout")) &&
+ check_output (pr, esp, isp, err, ll, sp, diag, "stderr");
+
+ return success;
+ }
+
+ static bool
+ run_expr (scope& sp,
+ const command_expr& expr,
+ size_t li, const location& ll,
+ bool diag)
+ {
+ bool r (false);
+
+ // Commands are numbered sequentially throughout the expression
+ // starting with 1. Number 0 means the command is a single one.
+ //
+ size_t ci (expr.size () == 1 && expr.back ().pipe.size () == 1
+ ? 0
+ : 1);
+
+ // If there is no ORs to the right of a pipe then the pipe failure is
+ // fatal for the whole expression. In particular, the pipe must print
+ // the diagnostics on failure (if generally allowed). So we find the
+ // pipe that "switches on" the diagnostics potential printing.
+ //
+ command_expr::const_iterator trailing_ands; // Undefined if diag is
+ // disallowed.
+ if (diag)
+ {
+ auto i (expr.crbegin ());
+ for (; i != expr.crend () && i->op == expr_operator::log_and; ++i) ;
+ trailing_ands = i.base ();
+ }
+
+ bool print (false);
+ for (auto b (expr.cbegin ()), i (b), e (expr.cend ()); i != e; ++i)
+ {
+ if (diag && i + 1 == trailing_ands)
+ print = true;
+
+ const command_pipe& p (i->pipe);
+ bool or_op (i->op == expr_operator::log_or);
+
+ // Short-circuit if the pipe result must be OR-ed with true or AND-ed
+ // with false.
+ //
+ if (!((or_op && r) || (!or_op && !r)))
+ r = run_pipe (
+ sp, p.begin (), p.end (), auto_fd (), ci, li, ll, print);
+
+ ci += p.size ();
+ }
+
+ return r;
+ }
+
+ void default_runner::
+ run (scope& sp, const command_expr& expr, size_t li, const location& ll)
+ {
+ if (!run_expr (sp, expr, li, ll, true))
+ throw failed (); // Assume diagnostics is already printed.
}
bool default_runner::
- run_if (scope&, const command_expr& expr, size_t, const location&)
+ run_if (scope& sp,
+ const command_expr& expr,
+ size_t li, const location& ll)
{
- const command& c (expr.back ().pipe.back ()); // @@ TMP
- return c.program.string () == "true"; // @@ TMP
+ return run_expr (sp, expr, li, ll, false);
}
}
}
diff --git a/build2/test/script/script b/build2/test/script/script
index 90b71bf..e528cdd 100644
--- a/build2/test/script/script
+++ b/build2/test/script/script
@@ -301,7 +301,7 @@ namespace build2
struct expr_term
{
- expr_operator op; // Ignored for the first term.
+ expr_operator op; // OR-ed to an implied false for the first term.
command_pipe pipe;
};
diff --git a/tests/test/script/runner/buildfile b/tests/test/script/runner/buildfile
index 86c18e2..df37c6d 100644
--- a/tests/test/script/runner/buildfile
+++ b/tests/test/script/runner/buildfile
@@ -2,7 +2,7 @@
# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-./: test{cleanup redirect regex status} exe{driver} $b
+./: test{cleanup expr if pipe redirect regex status} exe{driver} $b
test{*}: target = exe{driver}
diff --git a/tests/test/script/runner/expr.test b/tests/test/script/runner/expr.test
new file mode 100644
index 0000000..454da1e
--- /dev/null
+++ b/tests/test/script/runner/expr.test
@@ -0,0 +1,522 @@
+# file : tests/test/script/runner/expr.test
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.test
+
+: short-circuit
+:
+: Test expression result calculation and short-circuiting. We verify the
+: correctness of the above for all possible expressions of a length up to 3
+: operands. While some of tests may look redundant the full expression tree is
+: easier to maintain than the one with gaps (also much less chances that we
+: have missed something useful). Each pipe-operand has a label which is printed
+: to stdout when the pipe is executed. Pipes stdouts are pass-redirected, so we
+: just check that build2 output matches expectations.
+:
+: Note that expression evaluation goes left-to-right with AND and OR having the
+: same precedence.
+:
+{
+ true = '$* >| -o'
+ false = '$* -s 1 >| -o'
+
+ bf = $b 2>/~'%.+/driver(\.exe)? exit status 1 != 0%'
+
+ : true
+ :
+ {
+ : TERM
+ :
+ $c <"$true 1" && $b >>EOO
+ 1
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ : TERM
+ :
+ $c <"$true 1 || $true 2" && $b >>EOO
+ 1
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 || $true 2 || $true 3" && $b >>EOO
+ 1
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 || $true 2 || $false 3" && $b >>EOO
+ 1
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 || $true 2 && $true 3" && $b >>EOO
+ 1
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 || $true 2 && $false 3" && $bf >>EOO != 0
+ 1
+ 3
+ EOO
+ }
+ }
+ }
+
+ : false
+ :
+ {
+ : TERM
+ :
+ $c <"$true 1 || $false 2" && $b >>EOO
+ 1
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 || $false 2 || $true 3" && $b >>EOO
+ 1
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 || $false 2 || $false 3" && $b >>EOO
+ 1
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 || $false 2 && $true 3" && $b >>EOO
+ 1
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 || $false 2 && $false 3" && $bf >>EOO != 0
+ 1
+ 3
+ EOO
+ }
+ }
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ : TERM
+ :
+ $c <"$true 1 && $true 2" && $b >>EOO
+ 1
+ 2
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 && $true 2 || $true 3" && $b >>EOO
+ 1
+ 2
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 && $true 2 || $false 3" && $b >>EOO
+ 1
+ 2
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 && $true 2 && $true 3" && $b >>EOO
+ 1
+ 2
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 && $true 2 && $false 3" && $bf >>EOO != 0
+ 1
+ 2
+ 3
+ EOO
+ }
+ }
+ }
+
+ : false
+ :
+ {
+ : TERM
+ :
+ $c <"$true 1 && $false 2" && $bf >>EOO != 0
+ 1
+ 2
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 && $false 2 || $true 3" && $b >>EOO
+ 1
+ 2
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 && $false 2 || $false 3" && $bf >>EOO != 0
+ 1
+ 2
+ 3
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$true 1 && $false 2 && $true 3" && $bf >>EOO != 0
+ 1
+ 2
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$true 1 && $false 2 && $false 3" && $bf >>EOO != 0
+ 1
+ 2
+ EOO
+ }
+ }
+ }
+ }
+ }
+
+ : false
+ :
+ {
+ : TERM
+ :
+ $c <"$false 1" && $bf >>EOO != 0
+ 1
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ : TERM
+ :
+ $c <"$false 1 || $true 2" && $b >>EOO
+ 1
+ 2
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 || $true 2 || $true 3" && $b >>EOO
+ 1
+ 2
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 || $true 2 || $false 3" && $b >>EOO
+ 1
+ 2
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 || $true 2 && $true 3" && $b >>EOO
+ 1
+ 2
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 || $true 2 && $false 3" && $bf >>EOO != 0
+ 1
+ 2
+ 3
+ EOO
+ }
+ }
+ }
+
+ : false
+ :
+ {
+ : TERM
+ :
+ $c <"$false 1 || $false 2" && $bf >>EOO != 0
+ 1
+ 2
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 || $false 2 || $true 3" && $b >>EOO
+ 1
+ 2
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 || $false 2 || $false 3" && $bf >>EOO != 0
+ 1
+ 2
+ 3
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 || $false 2 && $true 3" && $bf >>EOO != 0
+ 1
+ 2
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 || $false 2 && $false 3" && $bf >>EOO != 0
+ 1
+ 2
+ EOO
+ }
+ }
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ : TERM
+ :
+ $c <"$false 1 && $true 2" && $bf >>EOO != 0
+ 1
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 && $true 2 || $true 3" && $b >>EOO
+ 1
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 && $true 2 || $false 3" && $bf >>EOO != 0
+ 1
+ 3
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 && $true 2 && $true 3" && $bf >>EOO != 0
+ 1
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 && $true 2 && $false 3" && $bf >>EOO != 0
+ 1
+ EOO
+ }
+ }
+ }
+
+ : false
+ :
+ {
+ : TERM
+ :
+ $c <"$false 1 && $false 2" && $bf >>EOO != 0
+ 1
+ EOO
+
+ : OR
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 && $false 2 || $true 3" && $b >>EOO
+ 1
+ 3
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 && $false 2 || $false 3" && $bf >>EOO != 0
+ 1
+ 3
+ EOO
+ }
+ }
+
+ : AND
+ :
+ {
+ : true
+ :
+ {
+ $c <"$false 1 && $false 2 && $true 3" && $bf >>EOO != 0
+ 1
+ EOO
+ }
+
+ : false
+ :
+ {
+ $c <"$false 1 && $false 2 && $false 3" && $bf >>EOO != 0
+ 1
+ EOO
+ }
+ }
+ }
+ }
+ }
+}
+
+: diagnostics
+:
+: Check that the diagnostics is printed for only the last faulty pipe.
+:
+{
+ true = '$*'
+ false = '$* -s 1 2>'X' -e' # Compares stderr to value that never matches.
+
+ : trailing
+ :
+ $c <"$false 1 != 0 || $true && $false 2 != 0" && $b 2>>~/EOE/ != 0
+ /.{7}
+ -X
+ +2
+ EOE
+
+ : non-trailing
+ :
+ $c <"$false 1 != 0 || $true && $false 2 != 0 && $true" && $b 2>>~/EOE/ != 0
+ /.{7}
+ -X
+ +2
+ EOE
+}
diff --git a/tests/test/script/runner/if.test b/tests/test/script/runner/if.test
new file mode 100644
index 0000000..d9c3601
--- /dev/null
+++ b/tests/test/script/runner/if.test
@@ -0,0 +1,25 @@
+# file : tests/test/script/runner/if.test
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.test
+
+: if-branch
+:
+$c <<EOI && $b >'if'
+if cat <'foo' >'foo'
+ echo 'if' >|
+else
+ echo 'else' >|
+end
+EOI
+
+: else-branch
+:
+$c <<EOI && $b >'else'
+if cat <'foo' >'bar'
+ echo 'if' >|
+else
+ echo 'else' >|
+end
+EOI
diff --git a/tests/test/script/runner/pipe.test b/tests/test/script/runner/pipe.test
new file mode 100644
index 0000000..bfbe8cb
--- /dev/null
+++ b/tests/test/script/runner/pipe.test
@@ -0,0 +1,35 @@
+# file : tests/test/script/runner/pipe.test
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.test
+
+$c <'cat <foo | $* -i 1 >foo' && $b : builtin-to-process
+$c <'$* -o foo | cat >foo' && $b : process-to-builtin
+
+
+: failure
+:
+: Note that while both commands for the pipe are faulty the diagnostics for
+: only the last one is printed.
+:
+{
+ : exit-code
+ :
+ $c <'$* -o foo -s 1 | $* -i 1 >foo -s 2' && $b 2>>/~%EOE% != 0
+ %testscript:1:1: error: .+ exit status 2 != 0%
+ info: stdout: test/1/stdout-2
+ EOE
+
+ : stderr
+ :
+ $c <'$* -o foo -e foo 2>bar | $* -i 2 2>baz' && $b 2>>/~%EOE% != 0
+ %testscript:1:1: error: .+ stderr doesn't match the expected output%
+ info: stderr: test/1/stderr-2
+ info: expected stderr: test/1/stderr-2.orig
+ info: stderr diff: test/1/stderr-2.diff
+ %.{3}
+ -baz
+ +foo
+ EOE
+}
diff --git a/tests/test/script/runner/redirect.test b/tests/test/script/runner/redirect.test
index 7cb6316..cfc12c5 100644
--- a/tests/test/script/runner/redirect.test
+++ b/tests/test/script/runner/redirect.test
@@ -9,6 +9,16 @@ b += --no-column
ps = ($cxx.target.class != 'windows' ? '/' : '\') # Path separator.
psr = ($cxx.target.class != 'windows' ? '/' : '\\') # Path separator in regex.
+: pass
+:
+{
+ $c <'$* -i 1 -e bar <| >| 2>|';
+ cat <<EOI >=buildfile;
+ test{testscript}: $target
+ EOI
+ $0 --jobs 1 --quiet test <foo >foo 2>bar
+}
+
: null
:
{
@@ -540,45 +550,3 @@ psr = ($cxx.target.class != 'windows' ? '/' : '\\') # Path separator in regex.
EOI
$b
}
-
-# @@ That will probably become redundant when builtins and process obtain file
-# descriptors uniformly.
-#
-: builtins
-:
-{
- : out-null
- :
- $c <'echo "abc" >-';
- $b
-
- : err-null
- :
- $c <'echo "abc" 1>&2 2>-';
- $b
-
- : in-str
- :
- $c <'echo <foo 1>-';
- $b
-
- : out-str
- :
- $c <'echo "foo" >foo';
- $b
-
- : err-str
- :
- $c <'echo "foo" 2>foo 1>&2';
- $b
-
- : inout-str
- :
- $c <'cat <foo >foo';
- $b
-
- : inerr-str
- :
- $c <'cat <foo 2>foo 1>&2';
- $b
-}
diff --git a/unit-tests/test/script/parser/pipe-expr.test b/unit-tests/test/script/parser/pipe-expr.test
index cc0bd7e..d789f56 100644
--- a/unit-tests/test/script/parser/pipe-expr.test
+++ b/unit-tests/test/script/parser/pipe-expr.test
@@ -87,3 +87,47 @@ cmd &&
EOI
testscript:1:7: error: missing program
EOE
+
+: redirected
+:
+{
+ : input
+ :
+ {
+ : first
+ :
+ $* <<EOI >>EOO
+ cmd1 <foo | cmd2
+ EOI
+ cmd1 <foo | cmd2
+ EOO
+
+ : non-first
+ :
+ $* <<EOI 2>>EOE != 0
+ cmd1 | cmd2 <foo
+ EOI
+ testscript:1:13: error: stdin is both piped and redirected
+ EOE
+ }
+
+ : output
+ :
+ {
+ : last
+ :
+ $* <<EOI >>EOO
+ cmd1 | cmd2 >foo
+ EOI
+ cmd1 | cmd2 >foo
+ EOO
+
+ : non-last
+ :
+ $* <<EOI 2>>EOE != 0
+ cmd1 >foo | cmd2
+ EOI
+ testscript:1:11: error: stdout is both redirected and piped
+ EOE
+ }
+}