aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build2/test/script/parser.cxx584
-rw-r--r--build2/test/script/parser.hxx7
-rw-r--r--build2/test/script/runner.cxx122
-rw-r--r--build2/test/script/runner.hxx13
-rw-r--r--doc/testscript.cli21
-rw-r--r--tests/test/script/runner/buildfile2
-rw-r--r--tests/test/script/runner/exit.test400
7 files changed, 858 insertions, 291 deletions
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<group*> (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<scope>& 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<scope>* ps;
- for (ps = &chain; *ps != nullptr; ps = &ps->get ()->if_chain)
+ for (unique_ptr<scope>& 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<scope>* 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<scope>& 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<scope>& 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 [<diagnostics>]
+ //
+ [[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)] [<attr>] <var>
@@ -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 <string>...
Write strings to \c{stdout} separating them with a single space and ending
with a newline.
+
+\h#builtins-exit|\c{exit}|
+
+\
+exit [<diagnostics>]
+\
+
+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 <foo' && $b 2>>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 <<EOI && $b >>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 <<EOI && $b >>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 <<EOI && $b
+ if true
+ exit
+ echo foo >|
+ else
+ echo bar >|
+ end;
+ echo baz >|
+ EOI
+
+ : failure
+ :
+ $c <<EOI && $b 2>'testscript:2:3: error: message' != 0
+ if true
+ exit 'message'
+ echo foo >|
+ else
+ echo bar >|
+ end
+ echo baz >|
+ EOI
+ }
+
+ : else-clause
+ :
+ {
+ : success
+ :
+ $c <<EOI && $b
+ if false
+ echo foo >|
+ else
+ exit
+ echo bar >|
+ end;
+ echo baz >|
+ EOI
+
+ : failure
+ :
+ $c <<EOI && $b 2>'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 <<EOI && $b
+ if exit
+ echo foo >|
+ else
+ echo bar >|
+ end;
+ echo baz >|
+ EOI
+
+ : failure
+ :
+ $c <<EOI && $b 2>'testscript:1:1: error: message' != 0
+ if exit 'message'
+ echo foo >|
+ else
+ echo bar >|
+ end;
+ echo baz >|
+ EOI
+ }
+
+ : elif
+ :
+ {
+ : success
+ :
+ $c <<EOI && $b
+ if false
+ elif exit
+ echo foo >|
+ else
+ echo bar >|
+ end;
+ echo baz >|
+ EOI
+
+ : failure
+ :
+ $c <<EOI && $b 2>'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 <<EOI && $b
+ if exit
+ {
+ echo foo >|
+ }
+ else
+ {
+ echo bar >|
+ }
+ EOI
+
+ : failure
+ :
+ $c <<EOI && $b 2>'testscript:1:1: error: message' != 0
+ if exit 'message'
+ {
+ echo foo >|
+ }
+ else
+ {
+ echo bar >|
+ }
+ EOI
+ }
+
+ : elif
+ :
+ {
+ : success
+ :
+ $c <<EOI && $b
+ if false
+ {
+ }
+ elif exit
+ {
+ echo foo >|
+ }
+ else
+ {
+ echo bar >|
+ }
+ EOI
+
+ : failure
+ :
+ $c <<EOI && $b 2>'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 <<EOI && $b
+ +touch --no-cleanup a
+ +touch b
+ +exit
+
+ echo foo >|
+
+ -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 <<EOI && $b 2>'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 <<EOI && $b >>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 <<EOI && $b 2>'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 && $b
+ -touch a
+ -exit
+ -echo foo >|
+ EOI
+
+ : failure
+ :
+ : Test that cleanups are not executed.
+ :
+ $c <<EOI && $b 2>'testscript:2:2: error: message' != 0
+ -true &a
+ -exit 'message'
+ -echo foo >|
+ EOI
+ }
+ }
+}