From 749f748ae6ded6e229214d2dddf3c45482bffbd3 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Fri, 27 Jan 2017 02:32:55 +0300 Subject: Add support for test command pipe, expression and command-if --- build2/test/script/parser.cxx | 21 +- build2/test/script/runner.cxx | 480 ++++++++++++++++-------- build2/test/script/script | 2 +- tests/test/script/runner/buildfile | 2 +- tests/test/script/runner/expr.test | 522 +++++++++++++++++++++++++++ tests/test/script/runner/if.test | 25 ++ tests/test/script/runner/pipe.test | 35 ++ tests/test/script/runner/redirect.test | 52 +-- unit-tests/test/script/parser/pipe-expr.test | 44 +++ 9 files changed, 973 insertions(+), 210 deletions(-) create mode 100644 tests/test/script/runner/expr.test create mode 100644 tests/test/script/runner/if.test create mode 100644 tests/test/script/runner/pipe.test 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: 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 exit; builtin* b (builtins.find (c.program.string ())); + bool success; + if (b != nullptr) { // Execute the builtin. @@ -1065,12 +1143,14 @@ namespace build2 future 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 (c.exit.status); + else if (!success) + { + if (diag) + d << pr << " exit status " << ec << (eq ? " != " : " == ") + << static_cast (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 <'if' +if cat <'foo' >'foo' + echo 'if' >| +else + echo 'else' >| +end +EOI + +: else-branch +: +$c <'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' && $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 <=buildfile; + test{testscript}: $target + EOI + $0 --jobs 1 --quiet test 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 -'; - $b - - : out-str - : - $c <'echo "foo" >foo'; - $b - - : err-str - : - $c <'echo "foo" 2>foo 1>&2'; - $b - - : inout-str - : - $c <'cat foo'; - $b - - : inerr-str - : - $c <'cat 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 + : + $* <>EOO + cmd1 >EOE != 0 + cmd1 | cmd2 >EOO + cmd1 | cmd2 >foo + EOI + cmd1 | cmd2 >foo + EOO + + : non-last + : + $* <>EOE != 0 + cmd1 >foo | cmd2 + EOI + testscript:1:11: error: stdout is both redirected and piped + EOE + } +} -- cgit v1.1