aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/test/rule.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2/test/rule.cxx')
-rw-r--r--libbuild2/test/rule.cxx514
1 files changed, 416 insertions, 98 deletions
diff --git a/libbuild2/test/rule.cxx b/libbuild2/test/rule.cxx
index 06fb12f..28eb35b 100644
--- a/libbuild2/test/rule.cxx
+++ b/libbuild2/test/rule.cxx
@@ -30,7 +30,7 @@ namespace build2
namespace test
{
bool rule::
- match (action, target&, const string&) const
+ match (action, target&) const
{
// We always match, even if this target is not testable (so that we can
// ignore it; see apply()).
@@ -66,11 +66,11 @@ namespace build2
// Resolve group members.
//
- if (!see_through || t.type ().see_through)
+ if (!see_through_only || t.type ().see_through ())
{
// Remember that we are called twice: first during update for test
// (pre-operation) and then during test. During the former, we rely on
- // the normall update rule to resolve the group members. During the
+ // the normal update rule to resolve the group members. During the
// latter, there will be no rule to do this but the group will already
// have been resolved by the pre-operation.
//
@@ -540,11 +540,19 @@ namespace build2
if (verb)
{
- diag_record dr (text);
- dr << "test " << ts;
-
- if (!t.is_a<alias> ())
- dr << ' ' << t;
+ // If the target is an alias, then testscript itself is the
+ // target.
+ //
+ if (t.is_a<alias> ())
+ print_diag ("test", ts);
+ else
+ {
+ // In this case the test is really a combination of the target
+ // and testscript and using "->" feels off. Also, let's list the
+ // testscript after the target even though its a source.
+ //
+ print_diag ("test", t, ts, "+");
+ }
}
res.push_back (ctx.dry_run
@@ -555,22 +563,22 @@ namespace build2
{
scope_state& r (res.back ());
- if (!ctx.sched.async (ctx.count_busy (),
- t[a].task_count,
- [this] (const diag_frame* ds,
- scope_state& r,
- const target& t,
- const testscript& ts,
- const dir_path& wd)
- {
- diag_frame::stack_guard dsg (ds);
- r = perform_script_impl (t, ts, wd, *this);
- },
- diag_frame::stack (),
- ref (r),
- cref (t),
- cref (ts),
- cref (wd)))
+ if (!ctx.sched->async (ctx.count_busy (),
+ t[a].task_count,
+ [this] (const diag_frame* ds,
+ scope_state& r,
+ const target& t,
+ const testscript& ts,
+ const dir_path& wd)
+ {
+ diag_frame::stack_guard dsg (ds);
+ r = perform_script_impl (t, ts, wd, *this);
+ },
+ diag_frame::stack (),
+ ref (r),
+ cref (t),
+ cref (ts),
+ cref (wd)))
{
// Executed synchronously. If failed and we were not asked to
// keep going, bail out.
@@ -641,25 +649,50 @@ namespace build2
// Stack-allocated linked list of information about the running pipeline
// processes.
//
+ // Note: constructed incrementally.
+ //
struct pipe_process
{
- process& proc;
- const char* prog; // Only for diagnostics.
+ // Initially NULL. Set to the address of the process object when it is
+ // created. Reset back to NULL when the process is executed and its exit
+ // status is collected (see complete_pipe() for details).
+ //
+ process* proc = nullptr;
+
+ char const** args; // Only for diagnostics.
+
+ diag_buffer dbuf;
+ bool force_dbuf;
// True if this process has been terminated.
//
bool terminated = false;
- pipe_process* prev; // NULL for the left-most program.
+ // True if this process has been terminated but we failed to read out
+ // its stderr stream in the reasonable timeframe (2 seconds) after the
+ // termination.
+ //
+ // Note that this may happen if there is a still running child process
+ // of the terminated process which has inherited the parent's stderr
+ // file descriptor.
+ //
+ bool unread_stderr = false;
- pipe_process (process& p, const char* g, pipe_process* r)
- : proc (p), prog (g), prev (r) {}
+ pipe_process* prev; // NULL for the left-most program.
+ pipe_process* next; // Left-most program for the right-most program.
+
+ pipe_process (context& x,
+ char const** as,
+ bool fb,
+ pipe_process* p,
+ pipe_process* f)
+ : args (as), dbuf (x), force_dbuf (fb), prev (p), next (f) {}
};
- static bool
+ static void
run_test (const target& t,
- diag_record& dr,
char const** args,
+ int ofd,
const optional<timestamp>& deadline,
pipe_process* prev = nullptr)
{
@@ -669,14 +702,28 @@ namespace build2
for (next++; *next != nullptr; next++) ;
next++;
+ bool last (*next == nullptr);
+
// Redirect stdout to a pipe unless we are last.
//
- int out (*next != nullptr ? -1 : 1);
- bool pr;
+ int out (last ? ofd : -1);
- // Absent if the process misses the deadline.
+ // Propagate the pointer to the left-most program.
//
- optional<process_exit> pe;
+ // Also force diag buffering for the trailing diff process, so it's
+ // stderr is never printed if the test program fails (see
+ // complete_pipe() for details).
+ //
+ pipe_process pp (t.ctx,
+ args,
+ last && ofd == 2,
+ prev,
+ prev != nullptr ? prev->next : nullptr);
+
+ if (prev != nullptr)
+ prev->next = &pp;
+ else
+ pp.next = &pp; // Points to itself.
try
{
@@ -707,11 +754,11 @@ namespace build2
{
try
{
- p->proc.term ();
+ p->proc->term ();
}
catch (const process_error& e)
{
- dr << fail << "unable to terminate " << p->prog << ": " << e;
+ dr << fail << "unable to terminate " << p->args[0] << ": " << e;
}
p->terminated = true;
@@ -724,7 +771,7 @@ namespace build2
for (pipe_process* p (pp); p != nullptr; p = p->prev)
{
- process& pr (p->proc);
+ process& pr (*p->proc);
try
{
@@ -736,26 +783,310 @@ namespace build2
}
catch (const process_error& e)
{
- dr << fail << "unable to wait/kill " << p->prog << ": " << e;
+ dr << fail << "unable to wait/kill " << p->args[0] << ": " << e;
+ }
+ }
+ };
+
+ // Read out all the pipeline's buffered strerr streams watching for
+ // the deadline, if specified. If the deadline is reached, then
+ // terminate the whole pipeline, move the deadline by another 2
+ // seconds, and continue reading.
+ //
+ // Note that we assume that this timeout increment is normally
+ // sufficient to read out the buffered data written by the already
+ // terminated processes. If, however, that's not the case (see
+ // pipe_process for the possible reasons), then we just set
+ // unread_stderr flag to true for such processes and bail out.
+ //
+ // Also note that this implementation is inspired by the
+ // script::run_pipe::read_pipe() lambda.
+ //
+ auto read_pipe = [&pp, &deadline, &term_pipe] ()
+ {
+ fdselect_set fds;
+ for (pipe_process* p (&pp); p != nullptr; p = p->prev)
+ {
+ diag_buffer& b (p->dbuf);
+
+ if (b.is.is_open ())
+ fds.emplace_back (b.is.fd (), p);
+ }
+
+ optional<timestamp> dl (deadline);
+ bool terminated (false);
+
+ for (size_t unread (fds.size ()); unread != 0;)
+ {
+ try
+ {
+ // If a deadline is specified, then pass the timeout to
+ // fdselect().
+ //
+ if (dl)
+ {
+ timestamp now (system_clock::now ());
+
+ if (*dl <= now || ifdselect (fds, *dl - now) == 0)
+ {
+ if (!terminated)
+ {
+ term_pipe (&pp);
+ terminated = true;
+
+ dl = system_clock::now () + chrono::seconds (2);
+ continue;
+ }
+ else
+ {
+ for (fdselect_state& s: fds)
+ {
+ if (s.fd != nullfd)
+ {
+ pipe_process* p (static_cast<pipe_process*> (s.data));
+
+ p->unread_stderr = true;
+
+ // Let's also close the stderr stream not to confuse
+ // diag_buffer::close() (see script::read() for
+ // details).
+ //
+ try
+ {
+ p->dbuf.is.close ();
+ }
+ catch (const io_error&) {}
+ }
+ }
+
+ break;
+ }
+ }
+ }
+ else
+ ifdselect (fds);
+
+ for (fdselect_state& s: fds)
+ {
+ if (s.ready)
+ {
+ pipe_process* p (static_cast<pipe_process*> (s.data));
+
+ if (!p->dbuf.read (p->force_dbuf))
+ {
+ s.fd = nullfd;
+ --unread;
+ }
+ }
+ }
+ }
+ catch (const io_error& e)
+ {
+ fail << "io error reading pipeline streams: " << e;
+ }
+ }
+ };
+
+ // Wait for the pipeline processes to complete, watching for the
+ // deadline, if specified. If the deadline is reached, then terminate
+ // the whole pipeline.
+ //
+ // Note: must be called after read_pipe().
+ //
+ auto wait_pipe = [&pp, &deadline, &timed_wait, &term_pipe] ()
+ {
+ for (pipe_process* p (&pp); p != nullptr; p = p->prev)
+ {
+ try
+ {
+ if (!deadline)
+ p->proc->wait ();
+ else if (!timed_wait (*p->proc, *deadline))
+ term_pipe (p);
+ }
+ catch (const process_error& e)
+ {
+ fail << "unable to wait " << p->args[0] << ": " << e;
+ }
+ }
+ };
+
+ // Iterate over the pipeline processes left to right, printing their
+ // stderr if buffered and issuing the diagnostics if the exit code is
+ // not available (terminated abnormally or due to a deadline), is
+ // non-zero, or stderr was not fully read. Afterwards, fail if any of
+ // such a faulty processes were encountered.
+ //
+ // Note that we only issue diagnostics for the first failure.
+ //
+ // Note: must be called after wait_pipe() and only once.
+ //
+ auto complete_pipe = [&pp, &t] ()
+ {
+ pipe_process* b (pp.next); // Left-most program.
+ assert (b != nullptr); // The lambda can only be called once.
+ pp.next = nullptr;
+
+ bool fail (false);
+ for (pipe_process* p (b); p != nullptr; p = p->next)
+ {
+ assert (p->proc != nullptr); // The lambda can only be called once.
+
+ // Collect the exit status, if present.
+ //
+ // Absent if the process misses the deadline.
+ //
+ optional<process_exit> pe;
+
+ const process& pr (*p->proc);
+
+#ifndef _WIN32
+ if (!(p->terminated &&
+ !pr.exit->normal () &&
+ pr.exit->signal () == SIGTERM))
+#else
+ if (!(p->terminated &&
+ !pr.exit->normal () &&
+ pr.exit->status == DBG_TERMINATE_PROCESS))
+#endif
+ pe = pr.exit;
+
+ p->proc = nullptr;
+
+ // Verify the exit status and issue the diagnostics on failure.
+ //
+ // Note that we only issue diagnostics for the first failure but
+ // continue iterating to reset process pointers to NULL. Also note
+ // that if the test program fails, then the potential diff's
+ // diagnostics is suppressed since it is always buffered.
+ //
+ if (!fail)
+ {
+ diag_record dr;
+
+ // Note that there can be a race, so that the process we have
+ // terminated due to reaching the deadline has in fact exited
+ // normally. Thus, the 'unread stderr' situation can also happen
+ // to a successfully terminated process. If that's the case, we
+ // report this problem as the main error and the secondary error
+ // otherwise.
+ //
+ if (!pe ||
+ !pe->normal () ||
+ pe->code () != 0 ||
+ p->unread_stderr)
+ {
+ fail = true;
+
+ dr << error << "test " << t << " failed" // Multi test: test 1.
+ << error << "process " << p->args[0] << ' ';
+
+ if (!pe)
+ {
+ dr << "terminated: execution timeout expired";
+
+ if (p->unread_stderr)
+ dr << error << "stderr not closed after exit";
+ }
+ else if (!pe->normal () || pe->code () != 0)
+ {
+ dr << *pe;
+
+ if (p->unread_stderr)
+ dr << error << "stderr not closed after exit";
+ }
+ else
+ {
+ assert (p->unread_stderr);
+
+ dr << "stderr not closed after exit";
+ }
+
+ if (verb == 1)
+ {
+ dr << info << "test command line: ";
+
+ for (pipe_process* p (b); p != nullptr; p = p->next)
+ {
+ if (p != b)
+ dr << " | ";
+
+ print_process (dr, p->args);
+ }
+ }
+ }
+
+ // Now print the buffered stderr, if present, and/or flush the
+ // diagnostics, if issued.
+ //
+ if (p->dbuf.is_open ())
+ p->dbuf.close (move (dr));
}
}
+
+ if (fail)
+ throw failed ();
};
- process p (prev == nullptr
- ? process (args, 0, out) // First process.
- : process (args, prev->proc, out)); // Next process.
+ process p;
+ {
+ process::pipe ep;
+ {
+ fdpipe p;
+ if (diag_buffer::pipe (t.ctx, pp.force_dbuf) == -1) // Buffering?
+ {
+ try
+ {
+ p = fdopen_pipe ();
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to redirect stderr: " << e;
+ }
+
+ // Note that we must return non-owning fd to our end of the pipe
+ // (see the process class for details).
+ //
+ ep = process::pipe (p.in.get (), move (p.out));
+ }
+ else
+ ep = process::pipe (-1, 2);
+
+ // Note that we must open the diag buffer regardless of the
+ // diag_buffer::pipe() result.
+ //
+ pp.dbuf.open (args[0], move (p.in), fdstream_mode::non_blocking);
+ }
+
+ p = (prev == nullptr
+ ? process (args, 0, out, move (ep)) // First process.
+ : process (args, *prev->proc, out, move (ep))); // Next process.
+ }
- pipe_process pp (p, args[0], prev);
+ pp.proc = &p;
- // If the deadline is specified, then make sure we don't miss it
- // waiting indefinitely in the process destructor on the right-hand
- // part of the pipe failure.
+ // If the right-hand part of the pipe fails, then make sure we don't
+ // wait indefinitely in the process destructor if the deadline is
+ // specified or just because a process is blocked on stderr.
//
- auto g (make_exception_guard ([&deadline, &pp, &term_pipe] ()
+ auto g (make_exception_guard ([&pp, &term_pipe] ()
{
- if (deadline)
+ if (pp.proc != nullptr)
try
{
+ // Close all buffered pipeline stderr streams ignoring io_error
+ // exceptions.
+ //
+ for (pipe_process* p (&pp); p != nullptr; p = p->prev)
+ {
+ if (p->dbuf.is.is_open ())
+ try
+ {
+ p->dbuf.is.close();
+ }
+ catch (const io_error&) {}
+ }
+
term_pipe (&pp);
}
catch (const failed&)
@@ -764,25 +1095,17 @@ namespace build2
}
}));
- pr = *next == nullptr || run_test (t, dr, next, deadline, &pp);
-
- if (!deadline)
- p.wait ();
- else if (!timed_wait (p, *deadline))
- term_pipe (&pp);
+ if (!last)
+ run_test (t, next, ofd, deadline, &pp);
- assert (p.exit);
-
-#ifndef _WIN32
- if (!(pp.terminated &&
- !p.exit->normal () &&
- p.exit->signal () == SIGTERM))
-#else
- if (!(pp.terminated &&
- !p.exit->normal () &&
- p.exit->status == DBG_TERMINATE_PROCESS))
-#endif
- pe = *p.exit;
+ // Complete the pipeline execution, if not done yet.
+ //
+ if (pp.proc != nullptr)
+ {
+ read_pipe ();
+ wait_pipe ();
+ complete_pipe ();
+ }
}
catch (const process_error& e)
{
@@ -793,24 +1116,6 @@ namespace build2
throw failed ();
}
-
- bool wr (pe && pe->normal () && pe->code () == 0);
-
- if (!wr)
- {
- if (pr) // First failure?
- dr << fail << "test " << t << " failed"; // Multi test: test 1.
-
- dr << error;
- print_process (dr, args);
-
- if (pe)
- dr << " " << *pe;
- else
- dr << " terminated: execution timeout expired";
- }
-
- return pr && wr;
}
target_state rule::
@@ -856,7 +1161,7 @@ namespace build2
fail << "invalid test executable override: '" << *n << "'";
else
{
- // Must be a target name.
+ // Must be a target name. Could be from src (e.g., a script).
//
// @@ OUT: what if this is a @-qualified pair of names?
//
@@ -986,10 +1291,19 @@ namespace build2
// Do we have stdout?
//
+ // If we do, then match it using diff. Also redirect the diff's stdout
+ // to stderr, similar to how we do that for the script (see
+ // script::check_output() for the reasoning). That will also prevent the
+ // diff's output from interleaving with any other output.
+ //
path dp ("diff");
process_path dpp;
+ int ofd (1);
+
if (pass_n != pts_n && pts[pass_n + 1] != nullptr)
{
+ ofd = 2;
+
const file& ot (pts[pass_n + 1]->as<file> ());
const path& op (ot.path ());
assert (!op.empty ()); // Should have been assigned by update.
@@ -1035,25 +1349,29 @@ namespace build2
args.push_back (nullptr); // Second.
if (verb >= 2)
- print_process (args);
+ print_process (args); // Note: prints the whole pipeline.
else if (verb)
- text << "test " << tt;
+ print_diag ("test", tt);
if (!ctx.dry_run)
{
- diag_record dr;
- pipe_process pp (cat, "cat", nullptr);
-
- if (!run_test (tt,
- dr,
- args.data () + (sin ? 3 : 0), // Skip cat.
- test_deadline (tt),
- sin ? &pp : nullptr))
+ pipe_process pp (tt.ctx,
+ args.data (), // Note: only cat's args are considered.
+ false /* force_dbuf */,
+ nullptr /* prev */,
+ nullptr /* next */);
+
+ if (sin)
{
- dr << info << "test command line: ";
- print_process (dr, args);
- dr << endf; // return
+ pp.next = &pp; // Points to itself.
+ pp.proc = &cat;
}
+
+ run_test (tt,
+ args.data () + (sin ? 3 : 0), // Skip cat.
+ ofd,
+ test_deadline (tt),
+ sin ? &pp : nullptr);
}
return target_state::changed;