diff options
Diffstat (limited to 'libbuild2/test/rule.cxx')
-rw-r--r-- | libbuild2/test/rule.cxx | 514 |
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; |