diff options
Diffstat (limited to 'libbuild2/test/script/runner.cxx')
-rw-r--r-- | libbuild2/test/script/runner.cxx | 2027 |
1 files changed, 52 insertions, 1975 deletions
diff --git a/libbuild2/test/script/runner.cxx b/libbuild2/test/script/runner.cxx index b40dea8..6e722de 100644 --- a/libbuild2/test/script/runner.cxx +++ b/libbuild2/test/script/runner.cxx @@ -3,697 +3,16 @@ #include <libbuild2/test/script/runner.hxx> -#include <ios> // streamsize - -#include <libbutl/regex.mxx> -#include <libbutl/builtin.mxx> -#include <libbutl/fdstream.mxx> // fdopen_mode, fddup() -#include <libbutl/filesystem.mxx> // path_search() -#include <libbutl/path-pattern.mxx> - -#include <libbuild2/variable.hxx> -#include <libbuild2/filesystem.hxx> -#include <libbuild2/diagnostics.hxx> +#include <libbuild2/script/run.hxx> #include <libbuild2/test/common.hxx> -#include <libbuild2/test/script/regex.hxx> -#include <libbuild2/test/script/parser.hxx> -#include <libbuild2/test/script/builtin-options.hxx> - -using namespace std; -using namespace butl; - namespace build2 { namespace test { namespace script { - // Normalize a path. Also make the relative path absolute using the - // scope's working directory unless it is already absolute. - // - static path - normalize (path p, const scope& sp, const location& l) - { - path r (p.absolute () ? move (p) : sp.wd_path / move (p)); - - try - { - r.normalize (); - } - catch (const invalid_path& e) - { - fail (l) << "invalid file path " << e.path; - } - - return r; - } - - // Check if a path is not empty, the referenced file exists and is not - // empty. - // - static bool - non_empty (const path& p, const location& ll) - { - if (p.empty () || !exists (p)) - return false; - - try - { - ifdstream is (p); - return is.peek () != ifdstream::traits_type::eof (); - } - catch (const io_error& e) - { - // While there can be no fault of the test command being currently - // executed let's add the location anyway to ease the - // troubleshooting. And let's stick to that principle down the road. - // - fail (ll) << "unable to read " << p << ": " << e << endf; - } - } - - // If the file exists, not empty and not larger than 4KB print it to the - // diag record. The file content goes from the new line and is not - // indented. - // - static void - print_file (diag_record& d, const path& p, const location& ll) - { - if (exists (p)) - { - try - { - ifdstream is (p, ifdstream::badbit); - - if (is.peek () != ifdstream::traits_type::eof ()) - { - char buf[4096 + 1]; // Extra byte is for terminating '\0'. - - // Note that the string is always '\0'-terminated with a maximum - // sizeof (buf) - 1 bytes read. - // - is.getline (buf, sizeof (buf), '\0'); - - // Print if the file fits 4KB-size buffer. Note that if it - // doesn't the failbit is set. - // - if (is.eof ()) - { - // Suppress the trailing newline character as the diag record - // adds it's own one when flush. - // - streamsize n (is.gcount ()); - assert (n > 0); - - // Note that if the file contains '\0' it will also be counted - // by gcount(). But even in the worst case we will stay in the - // buffer boundaries (and so not crash). - // - if (buf[n - 1] == '\n') - buf[n - 1] = '\0'; - - d << '\n' << buf; - } - } - } - catch (const io_error& e) - { - fail (ll) << "unable to read " << p << ": " << e; - } - } - } - - // Print first 10 directory sub-entries to the diag record. The directory - // must exist. - // - static void - print_dir (diag_record& d, const dir_path& p, const location& ll) - { - try - { - size_t n (0); - for (const dir_entry& de: dir_iterator (p, - false /* ignore_dangling */)) - { - if (n++ < 10) - d << '\n' << (de.ltype () == entry_type::directory - ? path_cast<dir_path> (de.path ()) - : de.path ()); - } - - if (n > 10) - d << "\nand " << n - 10 << " more file(s)"; - } - catch (const system_error& e) - { - fail (ll) << "unable to iterate over " << p << ": " << e; - } - } - - // Save a string to the file. Fail if exception is thrown by underlying - // operations. - // - static void - save (const path& p, const string& s, const location& ll) - { - try - { - ofdstream os (p); - os << s; - os.close (); - } - catch (const io_error& e) - { - fail (ll) << "unable to write to " << p << ": " << e; - } - } - - // Return the value of the test.target variable. - // - static inline const target_triplet& - test_target (const script& s) - { - // @@ Would be nice to use cached value from test::common_data. - // - if (auto r = cast_null<target_triplet> (s.test_target["test.target"])) - return *r; - - // We set it to default value in init() so it can only be NULL if the - // user resets it. - // - fail << "invalid test.target value" << endf; - } - - // Transform string according to here-* redirect modifiers from the {/} - // set. - // - static string - transform (const string& s, - bool regex, - const string& modifiers, - const script& scr) - { - if (modifiers.find ('/') == string::npos) - return s; - - // For targets other than Windows leave the string intact. - // - if (test_target (scr).class_ != "windows") - return s; - - // Convert forward slashes to Windows path separators (escape for - // regex). - // - string r; - for (size_t p (0);;) - { - size_t sp (s.find ('/', p)); - - if (sp != string::npos) - { - r.append (s, p, sp - p); - r.append (regex ? "\\\\" : "\\"); - p = sp + 1; - } - else - { - r.append (s, p, sp); - break; - } - } - - return r; - } - - // Check if the test command output matches the expected result (redirect - // value). Noop for redirect types other than none, here_*. - // - 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) - { - if (non_empty (ip, ll)) - d << info << "stdin: " << ip; - }; - - auto output_info = [&what, &ll] (diag_record& d, - const path& p, - const char* prefix = "", - const char* suffix = "") - { - if (non_empty (p, ll)) - d << info << prefix << what << suffix << ": " << p; - else - d << info << prefix << what << suffix << " is empty"; - }; - - if (rd.type == redirect_type::none) - { - // Check that there is no output produced. - // - assert (!op.empty ()); - - if (!non_empty (op, ll)) - return true; - - if (diag) - { - diag_record d (error (ll)); - d << pr << " unexpectedly writes to " << what << - info << what << ": " << op; - - input_info (d); - - // Print cached output. - // - print_file (d, op, ll); - } - - // 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)) - { - // 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) - eop = normalize (rd.file.path, sp, ll); - else - { - eop = path (op + ".orig"); - save (eop, transform (rd.str, false, rd.modifiers, sp.root), ll); - sp.clean_special (eop); - } - - // Use the diff utility for comparison. - // - path dp ("diff"); - process_path pp (run_search (dp, true)); - - cstrings args {pp.recall_string (), "-u"}; - - // Ignore Windows newline fluff if that's what we are running on. - // - if (test_target (sp.root).class_ == "windows") - args.push_back ("--strip-trailing-cr"); - - args.push_back (eop.string ().c_str ()); - args.push_back (op.string ().c_str ()); - args.push_back (nullptr); - - if (verb >= 2) - print_process (args); - - try - { - // Save diff's stdout to a file for troubleshooting and for the - // optional (if not too large) printing (at the end of - // diagnostics). - // - path ep (op + ".diff"); - auto_fd efd; - - try - { - efd = fdopen (ep, fdopen_mode::out | fdopen_mode::create); - sp.clean_special (ep); - } - catch (const io_error& e) - { - fail (ll) << "unable to write to " << ep << ": " << e; - } - - // Diff utility prints the differences to stdout. But for the - // user it is a part of the test failure diagnostics so let's - // redirect stdout to stderr. - // - process p (pp, args.data (), 0, 2, efd.get ()); - efd.reset (); - - if (p.wait ()) - return true; - - assert (p.exit); - const process_exit& pe (*p.exit); - - // Note that both POSIX and GNU diff report error by exiting with - // the code > 1. - // - if (!pe.normal () || pe.code () > 1) - { - diag_record d (fail (ll)); - print_process (d, args); - d << " " << pe; - } - - // Output doesn't match the expected result. - // - if (diag) - { - diag_record d (error (ll)); - d << pr << " " << what << " doesn't match expected"; - - output_info (d, op); - output_info (d, eop, "expected "); - output_info (d, ep, "", " diff"); - input_info (d); - - print_file (d, ep, ll); - } - - // Fall through (to return false). - // - } - catch (const process_error& e) - { - error (ll) << "unable to execute " << pp << ": " << e; - - if (e.child) - exit (1); - - throw failed (); - } - } - else if (rd.type == redirect_type::here_str_regex || - rd.type == redirect_type::here_doc_regex) - { - // The overall plan is: - // - // 1. Create regex line string. While creating it's line characters - // transform regex lines according to the redirect modifiers. - // - // 2. Create line regex using the line string. If creation fails - // then save the (transformed) regex redirect to a file for - // troubleshooting. - // - // 3. Parse the output into the literal line string. - // - // 4. Match the output line string with the line regex. - // - // 5. If match fails save the (transformed) regex redirect to a file - // for troubleshooting. - // - using namespace regex; - - assert (!op.empty ()); - - // Create regex line string. - // - line_pool pool; - line_string rls; - const regex_lines rl (rd.regex); - - // Parse regex flags. - // - // When add support for new flags don't forget to update - // parse_regex(). - // - auto parse_flags = [] (const string& f) -> char_flags - { - char_flags r (char_flags::none); - - for (char c: f) - { - switch (c) - { - case 'd': r |= char_flags::idot; break; - case 'i': r |= char_flags::icase; break; - default: assert (false); // Error so should have been checked. - } - } - - return r; - }; - - // Return original regex line with the transformation applied. - // - auto line = [&rl, &rd, &sp] (const regex_line& l) -> string - { - string r; - if (l.regex) // Regex (possibly empty), - { - r += rl.intro; - r += transform (l.value, true, rd.modifiers, sp.root); - r += rl.intro; - r += l.flags; - } - else if (!l.special.empty ()) // Special literal. - r += rl.intro; - else // Textual literal. - r += transform (l.value, false, rd.modifiers, sp.root); - - r += l.special; - return r; - }; - - // Return regex line location. - // - // Note that we rely on the fact that the command and regex lines - // are always belong to the same testscript file. - // - auto loc = [&ll] (uint64_t line, uint64_t column) -> location - { - location r (ll); - r.line = line; - r.column = column; - return r; - }; - - // Save the regex to file for troubleshooting, return the file path - // it have been saved to. - // - // Note that we save the regex on line regex creation failure or if - // the program output doesn't match. - // - auto save_regex = [&op, &rl, &rd, &ll, &line] () -> path - { - path rp (op + ".regex"); - - // Encode here-document regex global flags if present as a file - // name suffix. For example if icase and idot flags are specified - // the name will look like: - // - // test/1/stdout.regex-di - // - if (rd.type == redirect_type::here_doc_regex && !rl.flags.empty ()) - rp += '-' + rl.flags; - - // Note that if would be more efficient to directly write chunks - // to file rather than to compose a string first. Hower we don't - // bother (about performance) for the sake of the code as we - // already failed. - // - string s; - for (auto b (rl.lines.cbegin ()), i (b), e (rl.lines.cend ()); - i != e; ++i) - { - if (i != b) s += '\n'; - s += line (*i); - } - - save (rp, s, ll); - return rp; - }; - - // Finally create regex line string. - // - // Note that diagnostics doesn't refer to the program path as it is - // irrelevant to failures at this stage. - // - char_flags gf (parse_flags (rl.flags)); // Regex global flags. - - for (const auto& l: rl.lines) - { - if (l.regex) // Regex (with optional special characters). - { - line_char c; - - // Empty regex is a special case repesenting the blank line. - // - if (l.value.empty ()) - c = line_char ("", pool); - else - { - try - { - string s (transform (l.value, true, rd.modifiers, sp.root)); - - c = line_char ( - char_regex (s, gf | parse_flags (l.flags)), pool); - } - catch (const regex_error& e) - { - // Print regex_error description if meaningful. - // - diag_record d (fail (loc (l.line, l.column))); - - if (rd.type == redirect_type::here_str_regex) - d << "invalid " << what << " regex redirect" << e << - info << "regex: '" << line (l) << "'"; - else - d << "invalid char-regex in " << what << " regex redirect" - << e << - info << "regex line: '" << line (l) << "'"; - - d << endf; - } - } - - rls += c; // Append blank literal or regex line char. - } - else if (!l.special.empty ()) // Special literal. - { - // Literal can not be followed by special characters in the same - // line. - // - assert (l.value.empty ()); - } - else // Textual literal. - { - // Append literal line char. - // - rls += line_char ( - transform (l.value, false, rd.modifiers, sp.root), pool); - } - - for (char c: l.special) - { - if (line_char::syntax (c)) - rls += line_char (c); // Append special line char. - else - fail (loc (l.line, l.column)) - << "invalid syntax character '" << c << "' in " << what - << " regex redirect" << - info << "regex line: '" << line (l) << "'"; - } - } - - // Create line regex. - // - line_regex regex; - - try - { - regex = line_regex (move (rls), move (pool)); - } - catch (const regex_error& e) - { - // Note that line regex creation can not fail for here-string - // redirect as it doesn't have syntax line chars. That in - // particular means that end_line and end_column are meaningful. - // - assert (rd.type == redirect_type::here_doc_regex); - - diag_record d (fail (loc (rd.end_line, rd.end_column))); - - // Print regex_error description if meaningful. - // - d << "invalid " << what << " regex redirect" << e; - - output_info (d, save_regex (), "", " regex"); - } - - // Parse the output into the literal line string. - // - line_string ls; - - try - { - // Do not throw when eofbit is set (end of stream reached), and - // when failbit is set (getline() failed to extract any character). - // - // Note that newlines are treated as line-chars separators. That - // in particular means that the trailing newline produces a blank - // line-char (empty literal). Empty output produces the zero-length - // line-string. - // - // Also note that we strip the trailing CR characters (otherwise - // can mismatch when cross-test). - // - ifdstream is (op, ifdstream::badbit); - is.peek (); // Sets eofbit for an empty stream. - - while (!is.eof ()) - { - string s; - getline (is, s); - - // It is safer to strip CRs in cycle, as msvcrt unexplainably - // adds too much trailing junk to the system_error descriptions, - // and so it can appear in programs output. For example: - // - // ...: Invalid data.\r\r\n - // - // Note that our custom operator<<(ostream&, const exception&) - // removes this junk. - // - while (!s.empty () && s.back () == '\r') - s.pop_back (); - - ls += line_char (move (s), regex.pool); - } - } - catch (const io_error& e) - { - fail (ll) << "unable to read " << op << ": " << e; - } - - // Match the output with the regex. - // - if (regex_match (ls, regex)) // Doesn't throw. - return true; - - // Output doesn't match the regex. We save the regex to file for - // troubleshooting regardless of whether we print the diagnostics or - // not. We, however, register it for cleanup in the later case (the - // expression may still succeed, we can be evaluating the if - // condition, etc). - // - path rp (save_regex ()); - - if (diag) - { - diag_record d (error (ll)); - d << pr << " " << what << " doesn't match regex"; - - output_info (d, op); - output_info (d, rp, "", " regex"); - input_info (d); - - // Print cached output. - // - print_file (d, op, ll); - } - else - sp.clean_special (rp); - - // Fall through (to return false). - // - } - else // Noop. - return true; - - return false; - } - bool default_runner:: test (scope& s) const { @@ -703,7 +22,7 @@ namespace build2 void default_runner:: enter (scope& sp, const location&) { - context& ctx (sp.root.target_scope.ctx); + context& ctx (sp.context); auto df = make_diag_frame ( [&sp](const diag_record& dr) @@ -730,29 +49,25 @@ namespace build2 sp.parent == nullptr ? mkdir_buildignore ( ctx, - sp.wd_path, + sp.work_dir, sp.root.target_scope.root_scope ()->root_extra->buildignore_file, 2) - : mkdir (sp.wd_path, 2)); + : mkdir (sp.work_dir, 2)); if (r == mkdir_status::already_exists) - fail << "working directory " << sp.wd_path << " already exists" << + fail << "working directory " << sp.work_dir << " already exists" << info << "are tests stomping on each other's feet?"; // We don't change the current directory here but indicate that the // scope test commands will be executed in that directory. // if (verb >= 2) - text << "cd " << sp.wd_path; - - sp.clean ({cleanup_type::always, sp.wd_path}, true); + text << "cd " << sp.work_dir; } void default_runner:: leave (scope& sp, const location& ll) { - context& ctx (sp.root.target_scope.ctx); - auto df = make_diag_frame ( [&sp](const diag_record& dr) { @@ -766,200 +81,29 @@ namespace build2 // if (common_.after == output_after::clean) { - // Note that we operate with normalized paths here. - // - // Remove special files. The order is not important as we don't - // expect directories here. - // - for (const auto& p: sp.special_cleanups) - { - // Remove the file if exists. Fail otherwise. - // - if (rmfile (ctx, p, 3) == rmfile_status::not_exist) - fail (ll) << "registered for cleanup special file " << p - << " does not exist"; - } - - // Remove files and directories in the order opposite to the order of - // cleanup registration. - // - for (const auto& c: reverse_iterate (sp.cleanups)) - { - cleanup_type t (c.type); - - // Skip whenever the path exists or not. - // - if (t == cleanup_type::never) - continue; - - const path& cp (c.path); + clean (sp, ll); - // Wildcard with the last component being '***' (without trailing - // separator) matches all files and sub-directories recursively as - // well as the start directories itself. So we will recursively - // remove the directories that match the parent (for the original - // path) directory wildcard. - // - bool recursive (cp.leaf ().representation () == "***"); - const path& p (!recursive ? cp : cp.directory ()); + context& ctx (sp.context); - // Remove files or directories using wildcard. - // - if (path_pattern (p)) - { - bool removed (false); + rmdir_status r ( + sp.parent == nullptr + ? rmdir_buildignore (ctx, + sp.work_dir, + sp.root.target_scope.root_scope ()-> + root_extra->buildignore_file, + 2) + : rmdir (ctx, sp.work_dir, 2)); - auto rm = [&cp, recursive, &removed, &sp, &ll, &ctx] - (path&& pe, const string&, bool interm) - { - if (!interm) - { - // While removing the entry we can get not_exist due to - // racing conditions, but that's ok if somebody did our job. - // Note that we still set the removed flag to true in this - // case. - // - removed = true; // Will be meaningless on failure. - - if (pe.to_directory ()) - { - dir_path d (path_cast<dir_path> (pe)); - - if (!recursive) - { - rmdir_status r (rmdir (ctx, d, 3)); - - if (r != rmdir_status::not_empty) - return true; - - diag_record dr (fail (ll)); - dr << "registered for cleanup directory " << d - << " is not empty"; - - print_dir (dr, d, ll); - dr << info << "wildcard: '" << cp << "'"; - } - else - { - // Don't remove the working directory (it will be removed - // by the dedicated cleanup). - // - // Cast to uint16_t to avoid ambiguity with - // libbutl::rmdir_r(). - // - rmdir_status r (rmdir_r (ctx, d, d != sp.wd_path, 3)); - - if (r != rmdir_status::not_empty) - return true; - - // The directory is unlikely to be current but let's keep - // for completeness. - // - fail (ll) << "registered for cleanup wildcard " << cp - << " matches the current directory"; - } - } - else - rmfile (ctx, pe, 3); - } - - return true; - }; - - // Note that here we rely on the fact that recursive iterating - // goes depth-first (which make sense for the cleanup). - // - try - { - // Doesn't follow symlinks. - // - path_search (p, - rm, - dir_path () /* start */, - path_match_flags::none); - } - catch (const system_error& e) - { - fail (ll) << "unable to cleanup wildcard " << cp << ": " << e; - } - - // Removal of no filesystem entries is not an error for 'maybe' - // cleanup type. - // - if (removed || t == cleanup_type::maybe) - continue; - - fail (ll) << "registered for cleanup wildcard " << cp - << " doesn't match any " - << (recursive - ? "path" - : p.to_directory () - ? "directory" - : "file"); - } - - // Remove the directory if exists and empty. Fail otherwise. - // Removal of non-existing directory is not an error for 'maybe' - // cleanup type. - // - if (p.to_directory ()) - { - dir_path d (path_cast<dir_path> (p)); - bool wd (d == sp.wd_path); - - // Trace the scope working directory removal with the verbosity - // level 2 (that was used for its creation). For other - // directories use level 3 (as for other cleanups). - // - int v (wd ? 2 : 3); - - // Don't remove the working directory for the recursive cleanup - // (it will be removed by the dedicated one). - // - // Note that the root working directory contains the - // .buildignore file (see above). - // - // @@ If 'd' is a file then will fail with a diagnostics having - // no location info. Probably need to add an optional location - // parameter to rmdir() function. The same problem exists for - // a file cleanup when try to rmfile() directory instead of - // file. - // - rmdir_status r ( - recursive - ? rmdir_r (ctx, d, !wd, static_cast <uint16_t> (v)) - : (wd && sp.parent == nullptr - ? rmdir_buildignore ( - ctx, - d, - sp.root.target_scope.root_scope ()->root_extra-> - buildignore_file, - v) - : rmdir (ctx, d, v))); - - if (r == rmdir_status::success || - (r == rmdir_status::not_exist && t == cleanup_type::maybe)) - continue; - - diag_record dr (fail (ll)); - dr << "registered for cleanup directory " << d - << (r == rmdir_status::not_exist - ? " does not exist" - : !recursive - ? " is not empty" - : " is current"); - - if (r == rmdir_status::not_empty) - print_dir (dr, d, ll); - } + if (r != rmdir_status::success) + { + diag_record dr (fail (ll)); + dr << "working directory " << sp.work_dir + << (r == rmdir_status::not_exist + ? " does not exist" + : " is not empty"); - // Remove the file if exists. Fail otherwise. Removal of - // non-existing file is not an error for 'maybe' cleanup type. - // - if (rmfile (ctx, p, 3) == rmfile_status::not_exist && - t == cleanup_type::always) - fail (ll) << "registered for cleanup file " << p - << " does not exist"; + if (r == rmdir_status::not_empty) + build2::script::print_dir (dr, sp.work_dir, ll); } } @@ -968,1102 +112,14 @@ namespace build2 // if (verb >= 2) text << "cd " << (sp.parent != nullptr - ? sp.parent->wd_path - : 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 '" << *i << "'"; - - 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> - // - static void - set_builtin (scope& sp, - const strings& args, - auto_fd in, - const location& ll) - { - try - { - // Do not throw when eofbit is set (end of stream reached), and - // when failbit is set (read operation failed to extract any - // character). - // - ifdstream cin (move (in), ifdstream::badbit); - - // Parse arguments. - // - cli::vector_scanner scan (args); - set_options ops (scan); - - if (ops.whitespace () && ops.newline ()) - fail (ll) << "both -n|--newline and -w|--whitespace specified"; - - if (!scan.more ()) - fail (ll) << "missing variable name"; - - string a (scan.next ()); // Either attributes or variable name. - const string* ats (!scan.more () ? nullptr : &a); - const string& vname (!scan.more () ? a : scan.next ()); - - if (scan.more ()) - fail (ll) << "unexpected argument '" << scan.next () << "'"; - - if (ats != nullptr && ats->empty ()) - fail (ll) << "empty variable attributes"; - - if (vname.empty ()) - fail (ll) << "empty variable name"; - - // Read the input. - // - cin.peek (); // Sets eofbit for an empty stream. - - names ns; - while (!cin.eof ()) - { - // Read next element that depends on the whitespace mode being - // enabled or not. For the later case it also make sense to strip - // the trailing CRs that can appear while cross-testing Windows - // target or as a part of msvcrt junk production (see above). - // - string s; - if (ops.whitespace ()) - cin >> s; - else - { - getline (cin, s); - - while (!s.empty () && s.back () == '\r') - s.pop_back (); - } - - // If failbit is set then we read nothing into the string as eof is - // reached. That in particular means that the stream has trailing - // whitespaces (possibly including newlines) if the whitespace mode - // is enabled, or the trailing newline otherwise. If so then - // we append the "blank" to the variable value in the exact mode - // prior to bailing out. - // - if (cin.fail ()) - { - if (ops.exact ()) - { - if (ops.whitespace () || ops.newline ()) - ns.emplace_back (move (s)); // Reuse empty string. - else if (ns.empty ()) - ns.emplace_back ("\n"); - else - ns[0].value += '\n'; - } - - break; - } - - if (ops.whitespace () || ops.newline () || ns.empty ()) - ns.emplace_back (move (s)); - else - { - ns[0].value += '\n'; - ns[0].value += s; - } - } - - cin.close (); - - // Set the variable value and attributes. Note that we need to aquire - // unique lock before potentially changing the script's variable - // pool. The obtained variable reference can safelly be used with no - // locking as the variable pool is an associative container - // (underneath) and we are only adding new variables into it. - // - ulock ul (sp.root.var_pool_mutex); - const variable& var (sp.root.var_pool.insert (move (vname))); - ul.unlock (); - - value& lhs (sp.assign (var)); - - // If there are no attributes specified then the variable assignment - // is straightforward. Otherwise we will use the build2 parser helper - // function. - // - if (ats == nullptr) - lhs.assign (move (ns), &var); - else - { - // If there is an error in the attributes string, our diagnostics - // will look like this: - // - // <attributes>:1:1 error: unknown value attribute x - // testscript:10:1 info: while parsing attributes '[x]' - // - auto df = make_diag_frame ( - [ats, &ll](const diag_record& dr) - { - dr << info (ll) << "while parsing attributes '" << *ats << "'"; - }); - - parser p (sp.root.test_target.ctx); - p.apply_value_attributes (&var, - lhs, - value (move (ns)), - *ats, - token_type::assign, - path_name ("<attributes>")); - } - } - catch (const io_error& e) - { - fail (ll) << "set: " << e; - } - catch (const cli::exception& e) - { - fail (ll) << "set: " << e; - } - } - - // Sorted array of builtins that support filesystem entries cleanup. - // - static const char* cleanup_builtins[] = { - "cp", "ln", "mkdir", "mv", "touch"}; - - static inline bool - cleanup_builtin (const string& name) - { - return binary_search ( - cleanup_builtins, - cleanup_builtins + - sizeof (cleanup_builtins) / sizeof (*cleanup_builtins), - name); - } - - 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) - { - 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); - - // Register the command explicit cleanups. Verify that the path being - // cleaned up is a sub-path of the testscript working directory. Fail - // if this is not the case. - // - for (const auto& cl: c.cleanups) - { - const path& p (cl.path); - path np (normalize (p, sp, ll)); - - const string& ls (np.leaf ().string ()); - bool wc (ls == "*" || ls == "**" || ls == "***"); - const path& cp (wc ? np.directory () : np); - const dir_path& wd (sp.root.wd_path); - - if (!cp.sub (wd)) - fail (ll) << (wc - ? "wildcard" - : p.to_directory () - ? "directory" - : "file") - << " cleanup " << p << " is out of working directory " - << wd; - - 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); - - const string& program (c.program.string ()); - - // 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 (program == "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 no 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 - { - path p (n); - - // 0 if belongs to a single-line test scope, otherwise is the - // command line number (start from one) in the test scope. - // - 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); - }; - - // If this is the first pipeline command, then open stdin descriptor - // according to the redirect specified. - // - path isp; - - if (!first) - assert (in.type == redirect_type::none); // No redirect expected. - else - { - // Open a file for passing to the command stdin. - // - auto open_stdin = [&isp, &ifd, &ll] () - { - assert (!isp.empty ()); - - try - { - ifd = fdopen (isp, fdopen_mode::in); - } - catch (const io_error& e) - { - fail (ll) << "unable to read " << isp << ": " << e; - } - }; - - switch (in.type) - { - case redirect_type::pass: - { - try - { - ifd = fddup (0); - } - catch (const io_error& e) - { - fail (ll) << "unable to duplicate stdin: " << 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: - { - ifd = open_null (); - break; - } - - case redirect_type::file: - { - isp = normalize (in.file.path, sp, ll); - - 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"); - - save ( - isp, transform (in.str, false, in.modifiers, sp.root), ll); - - sp.clean_special (isp); - - open_stdin (); - break; - } - case redirect_type::trace: - 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; - } - } - - assert (ifd.get () != -1); - - // 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 (program == "set") - { - if (!last) - fail (ll) << "set builtin must be the last pipe command"; - - if (out.type != redirect_type::none) - fail (ll) << "set builtin stdout cannot be redirected"; - - if (err.type != redirect_type::none) - fail (ll) << "set builtin stderr cannot be redirected"; - - if (eq != (c.exit.code == 0)) - fail (ll) << "set builtin exit code cannot be non-zero"; - - set_builtin (sp, c.arguments, move (ifd), ll); - return true; - } - - // Open a file for command output redirect if requested explicitly - // (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. Interpret trace - // redirect according to the verbosity level (as null if below 2, as - // pass otherwise). 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 - { - assert (dfd == 1 || dfd == 2); - const char* what (dfd == 1 ? "stdout" : "stderr"); - - fdopen_mode m (fdopen_mode::out | fdopen_mode::create); - - redirect_type rt (r.type != redirect_type::trace - ? r.type - : verb < 2 - ? redirect_type::null - : redirect_type::pass); - switch (rt) - { - case redirect_type::pass: - { - try - { - return fddup (dfd); - } - catch (const io_error& e) - { - fail (ll) << "unable to duplicate " << what << ": " << e; - } - } - - case redirect_type::null: return open_null (); - - // Duplicate the paired file descriptor later. - // - case redirect_type::merge: return nullfd; - - case redirect_type::file: - { - // For the cmp mode the user-provided path refers a content to - // match against, rather than a content to be produced (as for - // overwrite and append modes). And so for cmp mode we redirect - // the process output to a temporary file. - // - p = r.file.mode == redirect_fmode::compare - ? std_path (what) - : normalize (r.file.path, sp, ll); - - m |= r.file.mode == redirect_fmode::append - ? fdopen_mode::at_end - : fdopen_mode::truncate; - - break; - } - - case redirect_type::none: - case redirect_type::here_str_literal: - case redirect_type::here_doc_literal: - case redirect_type::here_str_regex: - case redirect_type::here_doc_regex: - { - p = std_path (what); - m |= fdopen_mode::truncate; - break; - } - - case redirect_type::trace: - case redirect_type::here_doc_ref: assert (false); break; - } - - auto_fd fd; - - try - { - fd = fdopen (p, m); - - if ((m & fdopen_mode::at_end) != fdopen_mode::at_end) - { - if (rt == redirect_type::file) - sp.clean ({cleanup_type::always, p}, true); - else - sp.clean_special (p); - } - } - catch (const io_error& e) - { - fail (ll) << "unable to write to " << p << ": " << e; - } - - return fd; - }; - - path osp; - fdpipe 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". - // - if (last) - ofd.out = open (out, 1, osp); - else - { - assert (out.type == redirect_type::none); // No redirect expected. - ofd = open_pipe (); - } - - path esp; - auto_fd efd (open (err, 2, esp)); - - // Merge standard streams. - // - bool mo (out.type == redirect_type::merge); - if (mo || err.type == redirect_type::merge) - { - auto_fd& self (mo ? ofd.out : efd); - auto_fd& other (mo ? efd : ofd.out); - - try - { - assert (self.get () == -1 && other.get () != -1); - self = fddup (other.get ()); - } - catch (const io_error& e) - { - fail (ll) << "unable to duplicate " << (mo ? "stderr" : "stdout") - << ": " << e; - } - } - - // All descriptors should be open to the date. - // - assert (ofd.out.get () != -1 && efd.get () != -1); - - optional<process_exit> exit; - builtin_function* bf (builtins.find (program)); - - bool success; - - auto process_args = [&c] () -> cstrings - { - cstrings args {c.program.string ().c_str ()}; - - for (const auto& a: c.arguments) - args.push_back (a.c_str ()); - - args.push_back (nullptr); - return args; - }; - - if (bf != nullptr) - { - // Execute the builtin. - // - if (verb >= 2) - print_process (process_args ()); - - // Some of the testscript builtins (cp, mkdir, etc) extend libbutl - // builtins (via callbacks) registering/moving cleanups for the - // filesystem entries they create/move, unless explicitly requested - // not to do so via the --no-cleanup option. - // - // Let's "wrap up" the cleanup-related flags into the single object - // to rely on "small function object" optimization. - // - struct cleanup - { - // Whether the cleanups are enabled for the builtin. Can be set to - // false by the parse_option callback if --no-cleanup is - // encountered. - // - bool enabled = true; - - // Whether to register cleanup for a filesystem entry being - // created/updated depending on its existence. Calculated by the - // create pre-hook and used by the subsequent post-hook. - // - bool add; - - // Whether to move existing cleanups for the filesystem entry - // being moved, rather than to erase them. Calculated by the move - // pre-hook and used by the subsequent post-hook. - // - bool move; - }; - - // nullopt if the builtin doesn't support cleanups. - // - optional<cleanup> cln; - - if (cleanup_builtin (program)) - cln = cleanup (); - - builtin_callbacks bcs { - - // create - // - // Unless cleanups are suppressed, test that the filesystem entry - // doesn't exist (pre-hook) and, if that's the case, register the - // cleanup for the newly created filesystem entry (post-hook). - // - [&sp, &cln] (const path& p, bool pre) - { - // Cleanups must be supported by a filesystem entry-creating - // builtin. - // - assert (cln); - - if (cln->enabled) - { - if (pre) - cln->add = !butl::entry_exists (p); - else if (cln->add) - sp.clean ({cleanup_type::always, p}, true /* implicit */); - } - }, - - // move - // - // Validate the source and destination paths (pre-hook) and, - // unless suppressed, adjust the cleanups that are sub-paths of - // the source path (post-hook). - // - [&sp, &cln] - (const path& from, const path& to, bool force, bool pre) - { - // Cleanups must be supported by a filesystem entry-moving - // builtin. - // - assert (cln); - - if (pre) - { - const dir_path& wd (sp.wd_path); - const dir_path& rwd (sp.root.wd_path); - - auto fail = [] (const string& d) {throw runtime_error (d);}; - - if (!from.sub (rwd) && !force) - fail ("'" + from.representation () + - "' is out of working directory '" + rwd.string () + - "'"); - - auto check_wd = [&wd, fail] (const path& p) - { - if (wd.sub (path_cast<dir_path> (p))) - fail ("'" + p.string () + - "' contains test working directory '" + - wd.string () + "'"); - }; - - check_wd (from); - check_wd (to); - - // Unless cleanups are disabled, "move" the matching cleanups - // if the destination path doesn't exist and it is a sub-path - // of the working directory and just remove them otherwise. - // - if (cln->enabled) - cln->move = !butl::entry_exists (to) && to.sub (rwd); - } - else if (cln->enabled) - { - // Move or remove the matching cleanups (see above). - // - // Note that it's not enough to just change the cleanup paths. - // We also need to make sure that these cleanups happen before - // the destination directory (or any of its parents) cleanup, - // that is potentially registered. To achieve that we can just - // relocate these cleanup entries to the end of the list, - // preserving their mutual order. Remember that cleanups in - // the list are executed in the reversed order. - // - cleanups cs; - - // Remove the source path sub-path cleanups from the list, - // adjusting/caching them if required (see above). - // - for (auto i (sp.cleanups.begin ()); i != sp.cleanups.end (); ) - { - build2::test::script::cleanup& c (*i); - path& p (c.path); - - if (p.sub (from)) - { - if (cln->move) - { - // Note that we need to preserve the cleanup path - // trailing separator which indicates the removal - // method. Also note that leaf(), in particular, does - // that. - // - p = p != from - ? to / p.leaf (path_cast<dir_path> (from)) - : p.to_directory () - ? path_cast<dir_path> (to) - : to; - - cs.push_back (move (c)); - } - - i = sp.cleanups.erase (i); - } - else - ++i; - } - - // Re-insert the adjusted cleanups at the end of the list. - // - sp.cleanups.insert (sp.cleanups.end (), - make_move_iterator (cs.begin ()), - make_move_iterator (cs.end ())); - - } - }, - - // remove - // - // Validate the filesystem entry path (pre-hook). - // - [&sp] (const path& p, bool force, bool pre) - { - if (pre) - { - const dir_path& wd (sp.wd_path); - const dir_path& rwd (sp.root.wd_path); - - auto fail = [] (const string& d) {throw runtime_error (d);}; - - if (!p.sub (rwd) && !force) - fail ("'" + p.representation () + - "' is out of working directory '" + rwd.string () + - "'"); - - if (wd.sub (path_cast<dir_path> (p))) - fail ("'" + p.string () + - "' contains test working directory '" + wd.string () + - "'"); - } - }, - - // parse_option - // - [&cln] (const strings& args, size_t i) - { - // Parse --no-cleanup, if it is supported by the builtin. - // - if (cln && args[i] == "--no-cleanup") - { - cln->enabled = false; - return 1; - } - - return 0; - }, - - // sleep - // - // Deactivate the thread before going to sleep. - // - [&sp] (const duration& d) - { - // If/when required we could probably support the precise sleep - // mode (e.g., via an option). - // - sp.root.test_target.ctx.sched.sleep (d); - } - }; - - try - { - uint8_t r; // Storage. - builtin b (bf (r, - c.arguments, - move (ifd), move (ofd.out), move (efd), - sp.wd_path, - bcs)); - - success = run_pipe (sp, - nc, - ec, - move (ofd.in), - ci + 1, li, ll, diag); - - exit = process_exit (b.wait ()); - } - catch (const system_error& e) - { - fail (ll) << "unable to execute " << c.program << " builtin: " - << e << endf; - } - } - else - { - // Execute the process. - // - cstrings args (process_args ()); - - // Resolve the relative not simple program path against the scope's - // working directory. The simple one will be left for the process - // path search machinery. Also strip the potential leading `^`, - // indicating that this is an external program rather than a - // builtin. - // - path p; - - try - { - p = path (args[0]); - - if (p.relative ()) - { - auto program = [&p, &args] (path pp) - { - p = move (pp); - args[0] = p.string ().c_str (); - }; - - if (p.simple ()) - { - const string& s (p.string ()); - - // Don't end up with an empty path. - // - if (s.size () > 1 && s[0] == '^') - program (path (s, 1, s.size () - 1)); - } - else - program (sp.wd_path / p); - } - } - catch (const invalid_path& e) - { - fail (ll) << "invalid program path " << e.path; - } - - try - { - process_path pp (process::path_search (args[0])); - - // Note: the builtin-escaping character '^' is not printed. - // - if (verb >= 2) - print_process (args); - - process pr ( - pp, - args.data (), - {ifd.get (), -1}, process::pipe (ofd), {-1, efd.get ()}, - sp.wd_path.string ().c_str ()); - - ifd.reset (); - ofd.out.reset (); - efd.reset (); - - success = run_pipe (sp, - nc, - ec, - move (ofd.in), - ci + 1, li, ll, diag); - - pr.wait (); - - exit = move (pr.exit); - } - catch (const process_error& e) - { - error (ll) << "unable to execute " << args[0] << ": " << e; - - if (e.child) - std::exit (1); - - throw failed (); - } - } - - assert (exit); - - // 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 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 ()); - - // On Windows the exit code can be out of the valid codes range being - // defined as uint16_t. - // -#ifdef _WIN32 - if (valid) - valid = exit->code () < 256; -#endif - - success = valid && eq == (exit->code () == c.exit.code); - - if (!valid || (!success && diag)) - { - // In the presense of a valid exit code we print the diagnostics and - // return false rather than throw. - // - diag_record d (valid ? error (ll) : fail (ll)); - - if (!exit->normal ()) - d << pr << " " << *exit; - else - { - uint16_t ec (exit->code ()); // Make sure is printed as integer. - - if (!valid) - d << pr << " exit code " << ec << " out of 0-255 range"; - else if (!success) - { - if (diag) - d << pr << " exit code " << ec << (eq ? " != " : " == ") - << static_cast<uint16_t> (c.exit.code); - } - else - assert (false); - } - - if (non_empty (esp, ll)) - d << info << "stderr: " << esp; - - if (non_empty (osp, ll)) - d << info << "stdout: " << osp; - - if (non_empty (isp, ll)) - d << info << "stdin: " << isp; - - // Print cached stderr. - // - print_file (d, esp, ll); - } - - // 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. - // - // The thinking behind matching stderr first is that if it mismatches, - // then the program probably misbehaves (executes wrong functionality, - // etc) in which case its stdout doesn't really matter. - // - if (success) - success = - check_output (pr, esp, isp, err, ll, sp, diag, "stderr") && - (!last || - check_output (pr, osp, isp, out, ll, sp, diag, "stdout")); - - return success; - } - - static bool - run_expr (scope& sp, - const command_expr& expr, - size_t li, const location& ll, - bool diag) - { - // Print test id once per test expression. - // - auto df = make_diag_frame ( - [&sp](const diag_record& dr) - { - // Let's not depend on how the path representation can be improved - // for readability on printing. - // - dr << info << "test id: " << sp.id_path.posix_string (); - }); - - // 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 r (false); - 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; + ? sp.parent->work_dir + : sp.work_dir.directory ()); } void default_runner:: run (scope& sp, const command_expr& expr, command_type ct, - size_t li, - const location& ll) + size_t li, const location& ll) { // Noop for teardown commands if keeping tests output is requested. // @@ -2085,8 +141,18 @@ namespace build2 text << ": " << c << expr; } - if (!run_expr (sp, expr, li, ll, true)) - throw failed (); // Assume diagnostics is already printed. + // Print test id once per test expression. + // + auto df = make_diag_frame ( + [&sp](const diag_record& dr) + { + // Let's not depend on how the path representation can be improved + // for readability on printing. + // + dr << info << "test id: " << sp.id_path.posix_string (); + }); + + build2::script::run (sp, expr, li, ll); } bool default_runner:: @@ -2097,7 +163,18 @@ namespace build2 if (verb >= 3) text << ": ?" << expr; - return run_expr (sp, expr, li, ll, false); + // Print test id once per test expression. + // + auto df = make_diag_frame ( + [&sp](const diag_record& dr) + { + // Let's not depend on how the path representation can be improved + // for readability on printing. + // + dr << info << "test id: " << sp.id_path.posix_string (); + }); + + return build2::script::run_if (sp, expr, li, ll); } } } |