From 062e03325cf9bb7fecfb9ea254ceb5c0cf427a7a Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Fri, 16 Jun 2017 23:43:24 +0300 Subject: Add support for exit testscript builtin --- build2/test/script/parser.cxx | 584 ++++++++++++++++++++----------------- build2/test/script/parser.hxx | 7 +- build2/test/script/runner.cxx | 122 ++++++-- build2/test/script/runner.hxx | 13 + doc/testscript.cli | 21 ++ tests/test/script/runner/buildfile | 2 +- tests/test/script/runner/exit.test | 400 +++++++++++++++++++++++++ 7 files changed, 858 insertions(+), 291 deletions(-) create mode 100644 tests/test/script/runner/exit.test diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx index 6058ed2..b02c009 100644 --- a/build2/test/script/parser.cxx +++ b/build2/test/script/parser.cxx @@ -2895,147 +2895,171 @@ namespace build2 } else if (group* g = dynamic_cast (scope_)) { - exec_lines ( - g->setup_.begin (), g->setup_.end (), li, command_type::setup); - - atomic_count task_count (0); - wait_guard wg (task_count); + bool exec_scope ( + exec_lines ( + g->setup_.begin (), g->setup_.end (), li, command_type::setup)); - // Start asynchronous execution of inner scopes keeping track of how - // many we have handled. - // - for (unique_ptr& chain: g->scopes) + if (exec_scope) { - // Check if this scope is ignored (e.g., via config.test). - // - if (!runner_->test (*chain)) - { - chain = nullptr; - continue; - } + atomic_count task_count (0); + wait_guard wg (task_count); - // Pick a scope from the if-else chain. - // - // In fact, we are going to drop all but the selected (if any) - // scope. This way we can re-examine the scope states later. It - // will also free some memory. + // Start asynchronous execution of inner scopes keeping track of + // how many we have handled. // - unique_ptr* ps; - for (ps = &chain; *ps != nullptr; ps = &ps->get ()->if_chain) + for (unique_ptr& chain: g->scopes) { - scope& s (**ps); - - if (!s.if_cond_) // Unconditional. + // Check if this scope is ignored (e.g., via config.test). + // + if (!runner_->test (*chain) || !exec_scope) { - assert (s.if_chain == nullptr); - break; + chain = nullptr; + continue; } - line l (move (*s.if_cond_)); - line_type lt (l.type); + // Pick a scope from the if-else chain. + // + // In fact, we are going to drop all but the selected (if any) + // scope. This way we can re-examine the scope states later. It + // will also free some memory. + // + unique_ptr* ps; + for (ps = &chain; *ps != nullptr; ps = &ps->get ()->if_chain) + { + scope& s (**ps); - replay_data (move (l.tokens)); + if (!s.if_cond_) // Unconditional. + { + assert (s.if_chain == nullptr); + break; + } - token t; - type tt; + line l (move (*s.if_cond_)); + line_type lt (l.type); - next (t, tt); - const location ll (get_location (t)); - next (t, tt); // Skip to start of command. + replay_data (move (l.tokens)); - bool take; - if (lt != line_type::cmd_else) - { - // Note: the line index count continues from setup. - // - command_expr ce (parse_command_line (t, tt)); - take = runner_->run_if (*scope_, ce, ++li, ll); + token t; + type tt; - if (lt == line_type::cmd_ifn || lt == line_type::cmd_elifn) - take = !take; - } - else - { - assert (tt == type::newline); - take = true; + next (t, tt); + const location ll (get_location (t)); + next (t, tt); // Skip to start of command. + + bool take; + if (lt != line_type::cmd_else) + { + // Note: the line index count continues from setup. + // + command_expr ce (parse_command_line (t, tt)); + + try + { + take = runner_->run_if (*scope_, ce, ++li, ll); + } + catch (const exit_scope& e) + { + // Bail out if the scope is exited with the failure status. + // Otherwise leave the scope normally. + // + if (!e.status) + throw failed (); + + // Stop iterating through if conditions, and stop executing + // inner scopes. + // + exec_scope = false; + replay_stop (); + break; + } + + if (lt == line_type::cmd_ifn || lt == line_type::cmd_elifn) + take = !take; + } + else + { + assert (tt == type::newline); + take = true; + } + + replay_stop (); + + if (take) + { + // Count the remaining conditions for the line index. + // + for (scope* r (s.if_chain.get ()); + r != nullptr && + r->if_cond_->type != line_type::cmd_else; + r = r->if_chain.get ()) + ++li; + + s.if_chain.reset (); // Drop remaining scopes. + break; + } } - replay_stop (); + chain.reset (*ps == nullptr || (*ps)->empty () || !exec_scope + ? nullptr + : ps->release ()); - if (take) + if (chain != nullptr) { - // Count the remaining conditions for the line index. + // Hand it off to a sub-parser potentially in another thread. + // But we could also have handled it serially in this parser: // - for (scope* r (s.if_chain.get ()); - r != nullptr && r->if_cond_->type != line_type::cmd_else; - r = r->if_chain.get ()) - ++li; + // scope* os (scope_); + // scope_ = chain.get (); + // exec_scope_body (); + // scope_ = os; - s.if_chain.reset (); // Drop remaining scopes. - break; + // Pass our diagnostics stack (this is safe since we are going + // to wait for completion before unwinding the diag stack). + // + // If the scope was executed synchronously, check the status + // and bail out if we weren't asked to keep going. + // + if (!sched.async (task_count, + [] (scope& s, + script& scr, + runner& r, + const diag_frame* ds) + { + diag_frame df (ds); + execute_impl (s, scr, r); + }, + ref (*chain), + ref (*script_), + ref (*runner_), + diag_frame::stack)) + { + // Bail out if the scope has failed and we weren't instructed + // to keep going. + // + if (chain->state == scope_state::failed && !keep_going) + throw failed (); + } } } - chain.reset (*ps == nullptr || (*ps)->empty () - ? nullptr - : ps->release ()); + wg.wait (); - if (chain != nullptr) + // Re-examine the scopes we have executed collecting their state. + // + for (const unique_ptr& chain: g->scopes) { - // Hand it off to a sub-parser potentially in another thread. - // But we could also have handled it serially in this parser: - // - // scope* os (scope_); - // scope_ = chain.get (); - // exec_scope_body (); - // scope_ = os; + if (chain == nullptr) + continue; - // Pass our diagnostics stack (this is safe since we are going - // to wait for completion before unwinding the diag stack). - // - // If the scope was executed synchronously, check the status and - // bail out if we weren't asked to keep going. - // - if (!sched.async (task_count, - [] (scope& s, - script& scr, - runner& r, - const diag_frame* ds) - { - diag_frame df (ds); - execute_impl (s, scr, r); - }, - ref (*chain), - ref (*script_), - ref (*runner_), - diag_frame::stack)) + switch (chain->state) { - // Bail out if the scope has failed and we weren't instructed - // to keep going. - // - if (chain->state == scope_state::failed && !keep_going) - throw failed (); + case scope_state::passed: break; + case scope_state::failed: throw failed (); + default: assert (false); } } } - wg.wait (); - - // Re-examine the scopes we have executed collecting their state. - // - for (const unique_ptr& chain: g->scopes) - { - if (chain == nullptr) - continue; - - switch (chain->state) - { - case scope_state::passed: break; - case scope_state::failed: throw failed (); - default: assert (false); - } - } - exec_lines ( g->tdown_.begin (), g->tdown_.end (), li, command_type::teardown); } @@ -3047,205 +3071,231 @@ namespace build2 scope_->state = scope_state::passed; } - void parser:: + bool parser:: exec_lines (lines::iterator i, lines::iterator e, size_t& li, command_type ct) { - token t; - type tt; - - for (; i != e; ++i) + try { - line& ln (*i); - line_type lt (ln.type); - - assert (path_ == nullptr); - replay_data (move (ln.tokens)); // Set the tokens and start playing. - - // We don't really need to change the mode since we already know - // the line type. - // - next (t, tt); - const location ll (get_location (t)); + token t; + type tt; - switch (lt) + for (; i != e; ++i) { - case line_type::var: - { - // Parse. - // - string name (move (t.value)); + line& ln (*i); + line_type lt (ln.type); - next (t, tt); - type kind (tt); // Assignment kind. + assert (path_ == nullptr); - value rhs (parse_variable_line (t, tt)); + // Set the tokens and start playing. + // + replay_data (move (ln.tokens)); - if (tt == type::semi) - next (t, tt); + // We don't really need to change the mode since we already know + // the line type. + // + next (t, tt); + const location ll (get_location (t)); - assert (tt == type::newline); + switch (lt) + { + case line_type::var: + { + // Parse. + // + string name (move (t.value)); - // Assign. - // - const variable& var (*ln.var); + next (t, tt); + type kind (tt); // Assignment kind. - value& lhs (kind == type::assign - ? scope_->assign (var) - : scope_->append (var)); + value rhs (parse_variable_line (t, tt)); - build2::parser::apply_value_attributes ( - &var, lhs, move (rhs), kind); + if (tt == type::semi) + next (t, tt); - // If we changes any of the test.* values, then reset the $*, - // $N special aliases. - // - if (var.name == script_->test_var.name || - var.name == script_->options_var.name || - var.name == script_->arguments_var.name || - var.name == script_->redirects_var.name || - var.name == script_->cleanups_var.name) - { - scope_->reset_special (); - } + assert (tt == type::newline); - replay_stop (); - break; - } - case line_type::cmd: - { - // We use the 0 index to signal that this is the only command. - // Note that we only do this for test commands. - // - if (ct == command_type::test && li == 0) - { - lines::iterator j (i); - for (++j; j != e && j->type == line_type::var; ++j) ; + // Assign. + // + const variable& var (*ln.var); - if (j != e) // We have another command. - ++li; - } - else - ++li; + value& lhs (kind == type::assign + ? scope_->assign (var) + : scope_->append (var)); - command_expr ce (parse_command_line (t, tt)); - runner_->run (*scope_, ce, ct, li, ll); + build2::parser::apply_value_attributes ( + &var, lhs, move (rhs), kind); - replay_stop (); - break; - } - case line_type::cmd_if: - case line_type::cmd_ifn: - case line_type::cmd_elif: - case line_type::cmd_elifn: - case line_type::cmd_else: - { - next (t, tt); // Skip to start of command. + // If we changes any of the test.* values, then reset the $*, + // $N special aliases. + // + if (var.name == script_->test_var.name || + var.name == script_->options_var.name || + var.name == script_->arguments_var.name || + var.name == script_->redirects_var.name || + var.name == script_->cleanups_var.name) + { + scope_->reset_special (); + } - bool take; - if (lt != line_type::cmd_else) + replay_stop (); + break; + } + case line_type::cmd: { - // Assume if-else always involves multiple commands. + // We use the 0 index to signal that this is the only command. + // Note that we only do this for test commands. // + if (ct == command_type::test && li == 0) + { + lines::iterator j (i); + for (++j; j != e && j->type == line_type::var; ++j) ; + + if (j != e) // We have another command. + ++li; + } + else + ++li; + command_expr ce (parse_command_line (t, tt)); - take = runner_->run_if (*scope_, ce, ++li, ll); + runner_->run (*scope_, ce, ct, li, ll); - if (lt == line_type::cmd_ifn || lt == line_type::cmd_elifn) - take = !take; + replay_stop (); + break; } - else + case line_type::cmd_if: + case line_type::cmd_ifn: + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_else: { - assert (tt == type::newline); - take = true; - } + next (t, tt); // Skip to start of command. - replay_stop (); - - // If end is true, then find the 'end' line. Otherwise, find the - // next if-else line. If skip is true then increment the command - // line index. - // - auto next = [e, &li] - (lines::iterator j, bool end, bool skip) -> lines::iterator - { - // We need to be aware of nested if-else chains. - // - size_t n (0); + bool take; + if (lt != line_type::cmd_else) + { + // Assume if-else always involves multiple commands. + // + command_expr ce (parse_command_line (t, tt)); + take = runner_->run_if (*scope_, ce, ++li, ll); - for (++j; j != e; ++j) + if (lt == line_type::cmd_ifn || lt == line_type::cmd_elifn) + take = !take; + } + else { - line_type lt (j->type); + assert (tt == type::newline); + take = true; + } - if (lt == line_type::cmd_if || - lt == line_type::cmd_ifn) - ++n; + replay_stop (); - // If we are nested then we just wait until we get back to - // the surface. - // - if (n == 0) + // If end is true, then find the 'end' line. Otherwise, find + // the next if-else line. If skip is true then increment the + // command line index. + // + auto next = [e, &li] + (lines::iterator j, bool end, bool skip) -> lines::iterator { - switch (lt) + // We need to be aware of nested if-else chains. + // + size_t n (0); + + for (++j; j != e; ++j) { - case line_type::cmd_elif: - case line_type::cmd_elifn: - case line_type::cmd_else: if (end) break; // Fall through. - case line_type::cmd_end: return j; - default: break; - } - } + line_type lt (j->type); - if (lt == line_type::cmd_end) - --n; + if (lt == line_type::cmd_if || + lt == line_type::cmd_ifn) + ++n; - if (skip) - { - // Note that we don't count else and end as commands. - // - switch (lt) - { - case line_type::cmd: - case line_type::cmd_if: - case line_type::cmd_ifn: - case line_type::cmd_elif: - case line_type::cmd_elifn: ++li; break; - default: break; + // If we are nested then we just wait until we get back + // to the surface. + // + if (n == 0) + { + switch (lt) + { + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_else: + { + if (end) break; + + // Fall through. + } + case line_type::cmd_end: return j; + default: break; + } + } + + if (lt == line_type::cmd_end) + --n; + + if (skip) + { + // Note that we don't count else and end as commands. + // + switch (lt) + { + case line_type::cmd: + case line_type::cmd_if: + case line_type::cmd_ifn: + case line_type::cmd_elif: + case line_type::cmd_elifn: ++li; break; + default: break; + } + } } - } - } - assert (false); // Missing end. - return e; - }; + assert (false); // Missing end. + return e; + }; - // If we are taking this branch then we need to parse all the - // lines until the next if-else line and then skip all the lines - // until the end (unless next is already end). - // - // Otherwise, we need to skip all the lines until the next - // if-else line and then continue parsing. - // - if (take) - { - lines::iterator j (next (i, false, false)); // Next if-else. - exec_lines (i + 1, j, li, ct); - i = j->type == line_type::cmd_end ? j : next (j, true, true); + // If we are taking this branch then we need to parse all the + // lines until the next if-else line and then skip all the + // lines until the end (unless next is already end). + // + // Otherwise, we need to skip all the lines until the next + // if-else line and then continue parsing. + // + if (take) + { + lines::iterator j (next (i, false, false)); // Next if-else. + if (!exec_lines (i + 1, j, li, ct)) + return false; + + i = j->type == line_type::cmd_end ? j : next (j, true, true); + } + else + { + i = next (i, false, true); + if (i->type != line_type::cmd_end) + --i; // Continue with this line (e.g., elif or else). + } + + break; } - else + case line_type::cmd_end: { - i = next (i, false, true); - if (i->type != line_type::cmd_end) - --i; // Continue with this line (e.g., elif or else). + assert (false); } - - break; - } - case line_type::cmd_end: - { - assert (false); } } + + return true; + } + catch (const exit_scope& e) + { + // Bail out if the scope is exited with the failure status. Otherwise + // leave the scope normally. + // + if (!e.status) + throw failed (); + + replay_stop (); + return false; } } diff --git a/build2/test/script/parser.hxx b/build2/test/script/parser.hxx index 21ea61a..4c666f5 100644 --- a/build2/test/script/parser.hxx +++ b/build2/test/script/parser.hxx @@ -183,7 +183,12 @@ namespace build2 void exec_scope_body (); - void + // Return false if the execution of the scope should be terminated + // with the success status (e.g., as a result of encountering the exit + // builtin). For unsuccessful termination the failed exception should + // be thrown. + // + bool exec_lines (lines::iterator, lines::iterator, size_t&, command_type); // Customization hooks. diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx index 82c288b..8269f05 100644 --- a/build2/test/script/runner.cxx +++ b/build2/test/script/runner.cxx @@ -914,6 +914,35 @@ namespace build2 : sp.wd_path.directory ()); } + // The exit pseudo-builtin: exit the current scope successfully, or + // print the diagnostics and exit the current scope and all the outer + // scopes unsuccessfully. Always throw exit_scope exception. + // + // exit [] + // + [[noreturn]] static void + exit_builtin (const strings& args, const location& ll) + { + auto i (args.begin ()); + auto e (args.end ()); + + // Process arguments. + // + // If no argument is specified, then exit successfully. Otherwise, + // print the diagnostics and exit unsuccessfully. + // + if (i == e) + throw exit_scope (true); + + const string& s (*i++); + + if (i != e) + fail (ll) << "unexpected argument"; + + error (ll) << s; + throw exit_scope (false); + } + // The set pseudo-builtin: set variable from the stdin input. // // set [-e|--exact] [(-n|--newline)|(-w|--whitespace)] [] @@ -1141,6 +1170,63 @@ namespace build2 sp.clean ({cl.type, move (np)}, false); } + const redirect& in (c.in.effective ()); + const redirect& out (c.out.effective ()); + const redirect& err (c.err.effective ()); + bool eq (c.exit.comparison == exit_comparison::eq); + + // If stdin file descriptor is not open then this is the first pipeline + // command. + // + bool first (ifd.get () == -1); + + command_pipe::const_iterator nc (bc + 1); + bool last (nc == ec); + + // Prior to opening file descriptors for command input/output + // redirects let's check if the command is the exit builtin. Being a + // builtin syntactically it differs from the regular ones in a number + // of ways. It doesn't communicate with standard streams, so + // redirecting them is meaningless. It may appear only as a single + // command in a pipeline. It doesn't return any value and stops the + // scope execution, so checking its exit status is meaningless as + // well. That all means we can short-circuit here calling the builtin + // and bailing out right after that. Checking that the user didn't + // specify any redirects or exit code check sounds like a right thing + // to do. + // + if (c.program.string () == "exit") + { + // In case the builtin is erroneously pipelined from the other + // command, we will close stdin gracefully (reading out the stream + // content), to make sure that the command doesn't print any + // unwanted diagnostics about IO operation failure. + // + // Note that dtor will ignore any errors (which is what we want). + // + ifdstream is (move (ifd), fdstream_mode::skip); + + if (!first || !last) + fail (ll) << "exit builtin must be the only pipe command"; + + if (in.type != redirect_type::none) + fail (ll) << "exit builtin stdin cannot be redirected"; + + if (out.type != redirect_type::none) + fail (ll) << "exit builtin stdout cannot be redirected"; + + if (err.type != redirect_type::none) + fail (ll) << "exit builtin stderr cannot be redirected"; + + // We can't make sure that there is not exit code check. Let's, at + // least, check that non-zero code is not expected. + // + if (eq != (c.exit.code == 0)) + fail (ll) << "exit builtin exit code cannot be non-zero"; + + exit_builtin (c.arguments, ll); // Throws exit_scope exception. + } + // Create a unique path for a command standard stream cache file. // auto std_path = [&sp, &ci, &li, &ll] (const char* n) -> path @@ -1166,16 +1252,12 @@ namespace build2 return normalize (move (p), sp, ll); }; - const redirect& in (c.in.effective ()); - const redirect& out (c.out.effective ()); - const redirect& err (c.err.effective ()); - - // If stdin file descriptor is not open then this is the first pipeline - // command. Open stdin descriptor according to the redirect specified. + // If this is the first pipeline command, then open stdin descriptor + // according to the redirect specified. // path isp; - if (ifd.get () != -1) + if (!first) assert (in.type == redirect_type::none); // No redirect expected. else { @@ -1277,17 +1359,14 @@ namespace build2 assert (ifd.get () != -1); - command_pipe::const_iterator nc (bc + 1); - bool last (nc == ec); - - // Prior to follow up with opening file descriptors for command - // outputs redirects let's check if the command is the set builtin. - // Being a builtin syntactically it differs from the regular ones in a - // number of ways. It either succeeds or terminates abnormally, so - // redirecting stderr is meaningless. It also never produces any output - // and may appear only as a terminal command in a pipeline. That means - // we can short-circuit here calling the builtin and returning right - // after that. Checking that the user didn't specify any meaningless + // Prior to opening file descriptors for command outputs redirects + // let's check if the command is the set builtin. Being a builtin + // syntactically it differs from the regular ones in a number of ways. + // It either succeeds or terminates abnormally, so redirecting stderr + // is meaningless. It also never produces any output and may appear + // only as a terminal command in a pipeline. That means we can + // short-circuit here calling the builtin and returning right after + // that. Checking that the user didn't specify any meaningless // redirects or exit code check sounds as a right thing to do. // if (c.program.string () == "set") @@ -1301,7 +1380,7 @@ namespace build2 if (err.type != redirect_type::none) fail (ll) << "set builtin stderr cannot be redirected"; - if ((c.exit.comparison == exit_comparison::eq) != (c.exit.code == 0)) + if (eq != (c.exit.code == 0)) fail (ll) << "set builtin exit code cannot be non-zero"; set_builtin (sp, c.arguments, move (ifd), ll); @@ -1588,7 +1667,6 @@ namespace build2 valid = exit->code () < 256; #endif - bool eq (c.exit.comparison == exit_comparison::eq); success = valid && eq == (exit->code () == c.exit.code); if (!valid || (!success && diag)) @@ -1654,8 +1732,6 @@ namespace build2 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. // @@ -1677,7 +1753,9 @@ namespace build2 trailing_ands = i.base (); } + bool r (false); bool print (false); + for (auto b (expr.cbegin ()), i (b), e (expr.cend ()); i != e; ++i) { if (diag && i + 1 == trailing_ands) diff --git a/build2/test/script/runner.hxx b/build2/test/script/runner.hxx index 7833692..84487d0 100644 --- a/build2/test/script/runner.hxx +++ b/build2/test/script/runner.hxx @@ -20,6 +20,19 @@ namespace build2 namespace script { + // An exception that can be thrown by a runner to exit the scope (for + // example, as a result of executing the exit builtin). The status + // indicates whether the scope should be considered to have succeeded + // or failed. + // + struct exit_scope + { + bool status; + + explicit + exit_scope (bool s): status (s) {} + }; + class runner { public: diff --git a/doc/testscript.cli b/doc/testscript.cli index c039ec0..7b3d472 100644 --- a/doc/testscript.cli +++ b/doc/testscript.cli @@ -2334,6 +2334,27 @@ echo ... Write strings to \c{stdout} separating them with a single space and ending with a newline. + +\h#builtins-exit|\c{exit}| + +\ +exit [] +\ + +Exit the current group or test scope skipping any remaining commands. + +Note that \c{exit} is a \i{pseudo-builtin}. In particular, it must be the only +command in the pipe expression and its standard streams cannot be redirected. + +Without any arguments \c{exit} exits the current scope successfully. In this +case, if exiting a group scope, teardown commands and cleanups are executed +normally. + +If an argument is specified, then \c{exit} exits the current scope and all +the outer scopes unsuccessfully, as if the \c{exit} command failed. In this +case the argument must be the diagnostics string describing the error. + + \h#builtins-false|\c{false}| \ diff --git a/tests/test/script/runner/buildfile b/tests/test/script/runner/buildfile index a990405..3970cfd 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 expr if output pipe redirect regex set status} exe{driver} $b +./: test{*} exe{driver} $b test{*}: target = exe{driver} diff --git a/tests/test/script/runner/exit.test b/tests/test/script/runner/exit.test new file mode 100644 index 0000000..ae8f21a --- /dev/null +++ b/tests/test/script/runner/exit.test @@ -0,0 +1,400 @@ +# file : tests/test/script/runner/exit.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +.include ../common.test + +: special +: +{ + : pipelining + : + { + : to + : + $c <'exit | cat' && $b 2>>EOE != 0 + testscript:1:1: error: exit builtin must be the only pipe command + EOE + + : from + : + $c <'echo "foo" | exit' && $b 2>>EOE != 0 + testscript:1:1: error: exit builtin must be the only pipe command + EOE + } + + : redirecting + : + { + : stdin + : + $c <'exit >EOE != 0 + testscript:1:1: error: exit builtin stdin cannot be redirected + EOE + + : stdout + : + $c <'exit >foo' && $b 2>>EOE != 0 + testscript:1:1: error: exit builtin stdout cannot be redirected + EOE + + : stderr + : + $c <'exit 2>foo' && $b 2>>EOE != 0 + testscript:1:1: error: exit builtin stderr cannot be redirected + EOE + } + + : exit-code + : + $c <'exit != 0' && $b 2>>EOE != 0 + testscript:1:1: error: exit builtin exit code cannot be non-zero + EOE +} + +: arguments +: +{ + : none + : + $c <'exit' && $b + + : diagnostics + : + $c <'exit "foo"' && $b 2>>EOE != 0 + testscript:1:1: error: foo + EOE + + : unexpected + : + $c <'exit "foo" "bar"' && $b 2>>EOE != 0 + testscript:1:1: error: unexpected argument + EOE +} + +: execution +: +: Test that only expected commands are executed. Note that we rely on the fact +: that their execution is performed serially (see ../common.test for details). +: +{ + : test-scope + : + { + : success + : + : Note that we also test that cleanups are executed. + : + $c <>EOO + touch -f a; + echo foo >| && exit && echo bar >|; + echo baz >| + echo box >| + EOI + foo + box + EOO + + : failure + : + : Note that we also register fake cleanup, and test that cleanups are + : not executed. If they were, there would be a diagnostics printed to + : stderr regarding non-existent file. + : + $c <>EOO 2>'testscript:1:1: error: message' != 0 + echo foo >| &b && exit 'message' && echo bar >| + echo baz >|; + echo boz >| + EOI + foo + EOO + } + + : command-if + : + { + : if-clause + : + { + : success + : + $c <| + else + echo bar >| + end; + echo baz >| + EOI + + : failure + : + $c <'testscript:2:3: error: message' != 0 + if true + exit 'message' + echo foo >| + else + echo bar >| + end + echo baz >| + EOI + } + + : else-clause + : + { + : success + : + $c <| + else + exit + echo bar >| + end; + echo baz >| + EOI + + : failure + : + $c <'testscript:4:3: error: message' != 0 + if false + echo foo >| + else + exit 'message' + echo bar >| + end + echo baz >| + EOI + } + } + + : command-if-condition + : + { + : if + : + { + : success + : + $c <| + else + echo bar >| + end; + echo baz >| + EOI + + : failure + : + $c <'testscript:1:1: error: message' != 0 + if exit 'message' + echo foo >| + else + echo bar >| + end; + echo baz >| + EOI + } + + : elif + : + { + : success + : + $c <| + else + echo bar >| + end; + echo baz >| + EOI + + : failure + : + $c <'testscript:2:1: error: message' != 0 + if false + elif exit 'message' + echo foo >| + else + echo bar >| + end; + echo baz >| + EOI + } + } + + : scope-if-condition + : + { + : if + : + { + : success + : + $c <| + } + else + { + echo bar >| + } + EOI + + : failure + : + $c <'testscript:1:1: error: message' != 0 + if exit 'message' + { + echo foo >| + } + else + { + echo bar >| + } + EOI + } + + : elif + : + { + : success + : + $c <| + } + else + { + echo bar >| + } + EOI + + : failure + : + $c <'testscript:4:1: error: message' != 0 + if false + { + } + elif exit 'message' + { + echo foo >| + } + else + { + echo bar >| + } + EOI + } + } + + : group-scope + : + { + : setup + : + { + : success + : + : Test that teardown commands are executed (the 'a' file is removed), and + : cleanups are executed as well (the 'b' file is removed). + : + $c <| + + -rm a + EOI + + : failure + : + : Test that teardown commands are not executed (the touch would fail), + : and cleanups are also not executed (they would fail due to non-existent + : file 'a'). + : + $c <'testscript:2:2: error: message' != 0 + +true &a + +exit 'message' + + echo foo >| + + -touch b/c + EOI + } + + : inner-scope + : + { + : success + : + : Test that teardown commands and cleanups are executed (see above), and + : also that the independent inner scope is still executed. + : + $c <>EOO + +touch --no-cleanup a + +touch b + + exit + + echo foo >| + + -rm a + EOI + foo + EOO + + : failure + : + : Test that teardown commands and cleanups are not executed (see above), + : as well as the independent inner scope (remember the sequential + : execution). + : + $c <'testscript:3:1: error: message' != 0 + +true &a + + exit 'message' + + echo foo >| + + -touch b/c + EOI + } + + : teardown + : + { + : success + : + : Test that cleanups are executed. + : + $c <| + EOI + + : failure + : + : Test that cleanups are not executed. + : + $c <'testscript:2:2: error: message' != 0 + -true &a + -exit 'message' + -echo foo >| + EOI + } + } +} -- cgit v1.1