From 278140ebf2bc97eb72a1e8adb04a40a0a5807d8f Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Fri, 4 Nov 2016 10:51:57 +0300 Subject: Add mkdir and touch builtins --- build2/test/script/builtin | 5 +- build2/test/script/builtin.cxx | 313 ++++++++++++++++++-- build2/test/script/parser.cxx | 8 +- build2/test/script/runner.cxx | 640 +++++++++++++++++++++++------------------ build2/test/script/script | 3 +- build2/test/script/script.cxx | 11 +- 6 files changed, 672 insertions(+), 308 deletions(-) (limited to 'build2/test/script') diff --git a/build2/test/script/builtin b/build2/test/script/builtin index 3e49f98..e3c16b3 100644 --- a/build2/test/script/builtin +++ b/build2/test/script/builtin @@ -16,9 +16,12 @@ namespace build2 { namespace script { + class scope; + // Note that unlike argc/argv, our args don't include the program name. // - using builtin = int (*) (const strings& args, + using builtin = int (*) (scope&, + const strings& args, auto_fd in, auto_fd out, auto_fd err); class builtin_map: public std::map diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx index a83bb6d..9241447 100644 --- a/build2/test/script/builtin.cxx +++ b/build2/test/script/builtin.cxx @@ -4,52 +4,303 @@ #include -#include +#ifndef _WIN32 +# include +#else +# include +#endif + +#include // use default operator<< implementation +#include // fdopen_mode +#include // mkdir_status + +#include using namespace std; using namespace butl; namespace build2 { - static int - echo (const strings& args, auto_fd, auto_fd out, auto_fd err) - try + namespace test { - int r (0); - ofdstream cerr (move (err)); - - try + namespace script { - ofdstream cout (move (out)); + // Operation failed, diagnostics has already been issued. + // + struct failed {}; - for (auto b (args.begin ()), i (b), e (args.end ()); i != e; ++i) - cout << (i != b ? " " : "") << *i; + // Parse and normalize a path. Also, unless it is already absolute, make + // the path absolute using the specified directory. Throw invalid_path + // if the path is empty, and on parsing and normalization failures. + // + static path + parse_path (string s, const dir_path& d) + { + path p (move (s)); - cout << endl; + if (p.empty ()) + throw invalid_path (""); - cout.close (); - } - catch (const std::exception& e) - { - cerr << "echo: " << e.what (); - r = 1; - } + if (p.relative ()) + p = d / move (p); - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } + p.normalize (); + return p; + } + + // echo ... + // + static int + echo (scope&, const strings& args, auto_fd in, auto_fd out, auto_fd err) + try + { + int r (1); + ofdstream cerr (move (err)); + + try + { + in.close (); + ofdstream cout (move (out)); + + for (auto b (args.begin ()), i (b), e (args.end ()); i != e; ++i) + cout << (i != b ? " " : "") << *i; + + cout << endl; + + cout.close (); + r = 0; + } + catch (const std::exception& e) + { + cerr << "echo: " << e.what () << endl; + } + + cerr.close (); + return r; + } + catch (const std::exception&) + { + return 1; + } + + // Create a directory if not exist and its parent directories if + // necessary. Throw system_error on failure. Register created + // directories for cleanup. The directory path must be absolute. + // + static void + mkdir_p (scope& sp, const dir_path& p) + { + if (!dir_exists (p)) + { + if (!p.root ()) + mkdir_p (sp, p.directory ()); + + try_mkdir (p); // Returns success or throws. + sp.clean ({cleanup_type::always, p}, true); + } + } + + // mkdir [-p] ... + // + // -p + // Create any missing intermediate pathname components. Each argument + // that names an existing directory must be ignored without error. + // + static int + mkdir (scope& sp, + const strings& args, + auto_fd in, auto_fd out, auto_fd err) + try + { + // @@ Should we set a proper verbosity so paths get printed as + // relative? Can be inconvenient for end-user when build2 runs from + // a testscript. + // + // No, don't think so. If this were an external program, there + // won't be such functionality. + // + int r (1); + ofdstream cerr (move (err)); + + try + { + in.close (); + out.close (); + + auto i (args.begin ()); + + // Process options. + // + bool parent (false); + for (; i != args.end (); ++i) + { + if (*i == "-p") + parent = true; + else + { + if (*i == "--") + ++i; + + break; + } + } + + // Create directories. + // + if (i == args.end ()) + { + cerr << "mkdir: missing directory" << endl; + throw failed (); + } + + for (; i != args.end (); ++i) + { + dir_path p (path_cast (parse_path (*i, sp.wd_path))); + + try + { + if (parent) + mkdir_p (sp, p); + else if (try_mkdir (p) == mkdir_status::success) + sp.clean ({cleanup_type::always, p}, true); + else // == mkdir_status::already_exists + throw system_error (EEXIST, system_category ()); + } + catch (const system_error& e) + { + cerr << "mkdir: unable to create directory '" << p << "': " + << e.what () << endl; + + throw failed (); + } + } + + r = 0; + } + catch (const invalid_path& e) + { + cerr << "mkdir: invalid path '" << e.path << "'" << endl; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + + cerr.close (); + return r; + } + catch (const std::exception&) + { + return 1; + } + + // touch ... + // + // Change file access and modification times to the current time. Create + // a file if doesn't exist. Fail if a file system entry other than file + // exists for the name specified. + // + // Note that POSIX doesn't specify the behavior for touching an entry + // other than file. + // + static int + touch (scope& sp, + const strings& args, + auto_fd in, auto_fd out, auto_fd err) + try + { + int r (1); + ofdstream cerr (move (err)); + + try + { + in.close (); + out.close (); + + if (args.empty ()) + { + cerr << "touch: missing file" << endl; + throw failed (); + } + + // Create files. + // + for (auto i (args.begin ()); i != args.end (); ++i) + { + path p (parse_path (*i, sp.wd_path)); + + try + { + if (file_exists (p)) + { + // Set the file access and modification times to the current + // time. Note that we don't register (implicit) cleanup for an + // existing path. + // +#ifndef _WIN32 + if (utime (p.string ().c_str (), nullptr) == -1) +#else + if (_utime (p.string ().c_str (), nullptr) == -1) +#endif + throw system_error (errno, system_category ()); + } + else if (!entry_exists (p)) + { + // Create the file. Assume the file access and modification + // times are set to the current time automatically. + // + try + { + fdopen (p, fdopen_mode::out | fdopen_mode::create); + } + catch (const io_error& e) + { + cerr << "touch: cannot create file '" << p << "': " + << e.what () << endl; + throw failed (); + } + + sp.clean ({cleanup_type::always, p}, true); + } + else + { + cerr << "touch: '" << p << "' exists and is not a file" + << endl; + throw failed (); + } + } + catch (const system_error& e) + { + cerr << "touch: cannot create/update '" << p << "': " + << e.what () << endl; + throw failed (); + } + } + + r = 0; + } + catch (const invalid_path& e) + { + cerr << "touch: invalid path '" << e.path << "'" << endl; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + + cerr.close (); + return r; + } + catch (const std::exception&) + { + return 1; + } - namespace test - { - namespace script - { const builtin_map builtins { - {"echo", &echo} + {"echo", &echo}, + {"mkdir", &mkdir}, + {"touch", &touch} }; } } diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx index 5902008..3cae314 100644 --- a/build2/test/script/parser.cxx +++ b/build2/test/script/parser.cxx @@ -1139,7 +1139,10 @@ namespace build2 path p (move (w)); if (!p.empty ()) + { + p.normalize (); return p; + } error (l) << "empty " << what; } @@ -1302,7 +1305,7 @@ namespace build2 } } - redirect_type rt; + redirect_type rt (redirect_type::none); switch (tt) { case type::in_pass: @@ -2119,7 +2122,8 @@ namespace build2 } catch (const exception&) { - fail (loc) << "invalid $* index " << var.name; + error (loc) << "invalid $* index " << var.name; + throw failed (); } const strings& s (cast (v)); diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx index b071cf3..8db31d9 100644 --- a/build2/test/script/runner.cxx +++ b/build2/test/script/runner.cxx @@ -7,9 +7,13 @@ #include #include // cerr +#include // fdopen_mode, fdnull(), fddup() + #include +#include using namespace std; +using namespace butl; namespace build2 { @@ -47,7 +51,7 @@ namespace build2 // here_document. // static void - check_output (const process_path& pr, + check_output (const path& pr, const path& op, const path& ip, const redirect& rd, @@ -201,18 +205,6 @@ namespace build2 // // Note that we operate with normalized paths here. // - - // Verify that the path being cleaned up is a sub-path of the scope - // working directory. - // - auto verify = - [&sp, &ll] (const path& p, const path& orig, const char* what) - { - if (!p.sub (sp.wd_path)) - fail (ll) << "registered for cleanup " << what << " " << orig - << " is out of working directory " << sp.wd_path; - }; - for (const auto& c: reverse_iterate (sp.cleanups)) { cleanup_type t (c.type); @@ -224,40 +216,12 @@ namespace build2 const path& p (c.path); - // Remove directory if exists and empty. Fail otherwise. Removal of - // non-existing directory is not an error for 'maybe' cleanup type. - // - if (p.to_directory ()) - { - verify (p, p, "directory"); - - dir_path d (path_cast (p)); - - // @@ 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 (rmdir (d, 2)); - - if (r == rmdir_status::success || - (r == rmdir_status::not_exist && t == cleanup_type::maybe)) - continue; - - fail (ll) << "registered for cleanup directory " << d - << (r == rmdir_status::not_empty - ? " is not empty" - : " does not exist"); - } - - // Remove directory recursively if not current. Fail otherwise. + // Remove the directory recursively if not current. Fail otherwise. // Recursive removal of non-existing directory is not an error for // 'maybe' cleanup type. // if (p.leaf ().string () == "***") { - verify (p.directory (), p, "wildcard"); - // Cast to uint16_t to avoid ambiguity with libbutl::rmdir_r(). // rmdir_status r ( @@ -276,11 +240,34 @@ namespace build2 : " doesn't match a directory"); } - // Remove file if exists. Fail otherwise. Removal of non-existing - // file is not an error for 'maybe' cleanup type. + // Remove the directory if exists and empty. Fail otherwise. Removal + // of non-existing directory is not an error for 'maybe' cleanup + // type. // - verify (p, p, "file"); + if (p.to_directory ()) + { + dir_path d (path_cast (p)); + + // @@ 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 (rmdir (d, 2)); + + if (r == rmdir_status::success || + (r == rmdir_status::not_exist && t == cleanup_type::maybe)) + continue; + + fail (ll) << "registered for cleanup directory " << d + << (r == rmdir_status::not_empty + ? " is not empty" + : " does not exist"); + } + // Remove the file if exists. Fail otherwise. Removal of non-existing + // file is not an error for 'maybe' cleanup type. + // if (rmfile (p, 2) == rmfile_status::not_exist && t == cleanup_type::always) fail (ll) << "registered for cleanup file " << p @@ -296,26 +283,113 @@ namespace build2 if (verb >= 3) text << c; - // Pre-search the program path so it is reflected in the failure - // diagnostics. The user can see the original path running the test - // operation with the verbosity level > 2. + // Normalize a path. Also make the relative path absolute using the + // scope's working directory unless it is already absolute. + // + auto normalize = [&sp, &ll] (path p) -> path + { + path r (p.absolute () ? move (p) : sp.wd_path / move (p)); + + try + { + r.normalize (); + } + catch (const invalid_path& e) + { + fail (ll) << "invalid file path " << e.path; + } + + return r; + }; + + // Register the command explicit cleanups. Verify that the path being + // cleaned up is a sub-path of the root test scope working directory. + // Fail if this is not the case. // - process_path pp (run_search (c.program, true)); - cstrings args {pp.recall_string ()}; + for (const auto& cl: c.cleanups) + { + const path& p (cl.path); + path np (normalize (p)); + + bool wc (np.leaf ().string () == "***"); + const path& cp (wc ? np.directory () : np); + const 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); + } - for (const auto& a: c.arguments) - args.push_back (a.c_str ()); + // Create a unique path for a command standard stream cache file. + // + auto std_path = [&li, &normalize] (const char* n) -> path + { + path p (n); - args.push_back (nullptr); + // 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); - try + return normalize (move (p)); + }; + + // Assign file descriptors to pass as a builtin or a process standard + // streams. Eventually the raw descriptors should gone when the process + // is fully moved to auto_fd usage. + // + path isp; + auto_fd ifd; + int in (0); // @@ TMP + + // Open a file for passing to the command stdin. + // + auto open_stdin = [&isp, &ifd, &in, &ll] () + { + assert (!isp.empty ()); + + try + { + ifd = fdopen (isp, fdopen_mode::in); + in = ifd.get (); + } + catch (const io_error& e) + { + fail (ll) << "unable to read " << isp << ": " << e.what (); + } + }; + + switch (c.in.type) { - // For stdin 'none' redirect type we 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. + case redirect_type::pass: + { + try + { + ifd = fddup (in); + in = 0; + } + catch (const io_error& e) + { + fail (ll) << "unable to duplicate stdin: " << e.what (); + } + + 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. @@ -323,288 +397,310 @@ namespace build2 // process to hang which can be interpreted as a test failure. // @@ Both ways are quite ugly. Is there some better way to do this? // - - // Normalize a path. Also make relative path absolute using the - // scope's working directory unless it is already absolute. + // Fall through. // - auto normalize = [&sp, &ll] (path p) -> path + case redirect_type::null: { - path r (p.absolute () ? move (p) : sp.wd_path / move (p)); - try { - r.normalize (); + ifd.reset (fdnull ()); // @@ Eventually will be throwing. + + if (ifd.get () == -1) // @@ TMP + throw io_error ("", error_code (errno, system_category ())); + + in = -2; } - catch (const invalid_path& e) + catch (const io_error& e) { - fail (ll) << "invalid file path " << e.path; + fail (ll) << "unable to write to null device: " << e.what (); } - return r; - }; + break; + } - // Create unique path for a test command standard stream cache file. - // - auto std_path = [&li, &normalize] (const char* n) -> path + case redirect_type::file: { - path p (n); + isp = normalize (c.in.file.path); - // 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); - - return normalize (move (p)); - }; - - path sin; - ifdstream si; - int in; + open_stdin (); + break; + } - // Open a file for passing to the test command stdin. - // - auto open_stdin = [&sin, &si, &in, &ll] () + case redirect_type::here_string: + case redirect_type::here_document: { - assert (!sin.empty ()); + // We could write to the command stdin directly but instead will + // cache the data for potential troubleshooting. + // + isp = std_path ("stdin"); try { - si.open (sin); + ofdstream os (isp); + sp.clean ({cleanup_type::always, isp}, true); + + os << (c.in.type == redirect_type::here_string + ? c.in.str + : c.in.doc.doc); + + os.close (); } catch (const io_error& e) { - fail (ll) << "unable to read " << sin << ": " << e.what (); + fail (ll) << "unable to write " << isp << ": " << e.what (); } - in = si.fd (); - }; + open_stdin (); + break; + } - switch (c.in.type) - { - case redirect_type::pass: in = 0; break; - case redirect_type::null: - case redirect_type::none: in = -2; break; + case redirect_type::merge: assert (false); break; + } - case redirect_type::file: - { - sin = normalize (c.in.file.path); - open_stdin (); - break; - } + // Dealing with stdout and stderr redirect types other than 'null' + // using pipes is tricky in the general case. Going this path we would + // need to read both streams in non-blocking manner which we can't + // (easily) do in a portable way. Using diff utility to get a + // nice-looking actual/expected outputs difference would complicate + // things further. + // + // So the approach is the following. Child standard streams are + // redirected to files. When the child exits and the exit status is + // validated we just sequentially compare each file content with the + // expected output. The positive side-effect of this approach is that + // the output of a faulty command can be provided for troubleshooting. + // - case redirect_type::here_string: - case redirect_type::here_document: - { - // We could write to process stdin directly but instead will - // cache the data for potential troubleshooting. - // - sin = std_path ("stdin"); + // Open a file for command output redirect if requested explicitly + // (file redirect) or for the purpose of the output validation (none, + // here_string, here_document), register the file for cleanup, return + // the file descriptor. Return the specified, default or -2 file + // descriptors for merge, pass or null redirects respectively not + // opening a file. + // + auto open = [&sp, &ll, &std_path, &normalize] (const redirect& r, + int dfd, + path& p, + auto_fd& fd) -> int + { + assert (dfd == 1 || dfd == 2); + const char* what (dfd == 1 ? "stdout" : "stderr"); + + fdopen_mode m (fdopen_mode::out | fdopen_mode::create); + switch (r.type) + { + case redirect_type::pass: + { try { - ofdstream os (sin); - os << (c.in.type == redirect_type::here_string - ? c.in.str - : c.in.doc.doc); - os.close (); + fd = fddup (dfd); } catch (const io_error& e) { - fail (ll) << "unable to write " << sin << ": " << e.what (); + fail (ll) << "unable to duplicate " << what << ": " + << e.what (); } - open_stdin (); - sp.clean ({cleanup_type::always, sin}, true); - break; + return dfd; } - case redirect_type::merge: assert (false); break; - } - - // Dealing with stdout and stderr redirect types other than 'null' - // using pipes is tricky in the general case. Going this path we - // would need to read both streams in non-blocking manner which we - // can't (easily) do in a portable way. Using diff utility to get a - // nice-looking actual/expected outputs difference would complicate - // things further. - // - // So the approach is the following. Child standard streams are - // redirected to files. When the child exits and the exit status is - // validated we just sequentially compare each file content with the - // expected output. The positive side-effect of this approach is that - // the output of a faulty test command can be provided for - // troubleshooting. - // - - // Open a file for command output redirect if requested explicitly - // (file redirect) or for the purpose of the output validation (none, - // here_string, here_document), register the file for cleanup, return - // the file descriptor. Return the specified, default or -2 file - // descriptors for merge, pass or null redirects respectively not - // opening a file. - // - auto open = [&sp, &ll, &std_path, &normalize] (const redirect& r, - int dfd, - path& p, - ofdstream& os) -> int - { - assert (dfd == 1 || dfd == 2); - - ofdstream::openmode m (ofdstream::out); - - switch (r.type) + case redirect_type::null: { - case redirect_type::pass: return dfd; - case redirect_type::null: return -2; - case redirect_type::merge: return r.fd; - - case redirect_type::file: + try { - p = normalize (r.file.path); - - if (r.file.append) - m |= ofdstream::app; + fd.reset (fdnull ()); // @@ Eventully will be throwing. - break; + if (fd.get () == -1) // @@ TMP + throw io_error ("", error_code (errno, system_category ())); } - - case redirect_type::none: - case redirect_type::here_string: - case redirect_type::here_document: + catch (const io_error& e) { - p = std_path (dfd == 1 ? "stdout" : "stderr"); - break; + fail (ll) << "unable to write to null device: " << e.what (); } + + return -2; } - try + case redirect_type::merge: + { + // Duplicate the paired file descriptor later. + // + return r.fd; + } + + case redirect_type::file: { - os.open (p, m); + p = normalize (r.file.path); + m |= r.file.append ? fdopen_mode::at_end : fdopen_mode::truncate; + break; } - catch (const io_error& e) + + case redirect_type::none: + case redirect_type::here_string: + case redirect_type::here_document: { - fail (ll) << "unable to write " << p << ": " << e.what (); + p = std_path (what); + m |= fdopen_mode::truncate; + break; } + } - sp.clean ({cleanup_type::always, p}, true); - return os.fd (); - }; + try + { + fd = fdopen (p, m); - path sout; - ofdstream so; - int out (open (c.out, 1, sout, so)); + if ((m & fdopen_mode::at_end) != fdopen_mode::at_end) + sp.clean ({cleanup_type::always, p}, true); + } + catch (const io_error& e) + { + fail (ll) << "unable to write " << p << ": " << e.what (); + } - path serr; - ofdstream se; - int err (open (c.err, 2, serr, se)); + return fd.get (); + }; - if (verb >= 2) - print_process (args); + path osp; + auto_fd ofd; + int out (open (c.out, 1, osp, ofd)); - process pr (sp.wd_path.string ().c_str (), - pp, - args.data (), - in, out, err); + path esp; + auto_fd efd; + int err (open (c.err, 2, esp, efd)); + + // Merge standard streams. + // + bool mo (c.out.type == redirect_type::merge); + if (mo || c.err.type == redirect_type::merge) + { + auto_fd& self (mo ? ofd : efd); + auto_fd& other (mo ? efd : ofd); try { - si.close (); - so.close (); - se.close (); + 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.what (); + } + } - // Just wait. The program failure can mean the test success. - // - pr.wait (); + optional status; + builtin b (builtins.find (c.program.string ())); - // Register command-created paths for cleanup. - // - for (const auto& p: c.cleanups) - sp.clean ({p.type, normalize (p.path)}, false); + if (b != nullptr) + { + // Execute the builtin. + // + status = (*b) (sp, c.arguments, move (ifd), move (ofd), move (efd)); + } + else + { + // Execute the process. + // + // Pre-search the program path so it is reflected in the failure + // diagnostics. The user can see the original path running the test + // operation with the verbosity level > 2. + // + process_path pp (run_search (c.program, true)); + cstrings args {pp.recall_string ()}; - // If there is no correct exit status by whatever reason then dump - // stderr (if cached), print the proper diagnostics and fail. - // - optional status (move (pr.status)); + for (const auto& a: c.arguments) + args.push_back (a.c_str ()); - // Comparison *status >= 0 causes "always true" warning on Windows - // where process::status_type is defined as uint32_t. - // - bool valid_status (status && *status + 1 > 0 && *status < 256); - bool eq (c.exit.comparison == exit_comparison::eq); + args.push_back (nullptr); - bool correct_status (valid_status && - (*status == c.exit.status) == eq); + try + { + if (verb >= 2) + print_process (args); - if (!correct_status) - { - // Dump cached stderr. - // - if (exists (serr)) - { - try - { - ifdstream is (serr); - if (is.peek () != ifdstream::traits_type::eof ()) - cerr << is.rdbuf (); - } - catch (const io_error& e) - { - fail (ll) << "unable to read " << serr << ": " - << e.what (); - } - } + process pr (sp.wd_path.string ().c_str (), + pp, + args.data (), + in, out, err); - // Fail with a proper diagnostics. - // - diag_record d (fail (ll)); - - if (!status) - d << pp << " terminated abnormally"; - else if (!valid_status) - d << pp << " exit status " << *status << " is invalid" << - info << "must be an unsigned integer < 256"; - else if (!correct_status) - d << pp << " exit status " << *status - << (eq ? " != " : " == ") - << static_cast (c.exit.status); - else - assert (false); - - if (non_empty (serr, ll)) - d << info << "stderr: " << serr; - - if (non_empty (sout, ll)) - d << info << "stdout: " << sout; - - if (non_empty (sin, ll)) - d << info << "stdin: " << sin; - } + ifd.reset (); + ofd.reset (); + efd.reset (); - // Check if the standard outputs match expectations. - // - check_output (pp, sout, sin, c.out, ll, sp, "stdout"); - check_output (pp, serr, sin, c.err, ll, sp, "stderr"); + pr.wait (); + status = move (pr.status); } - catch (const io_error& e) + catch (const process_error& e) { - // Child exit status doesn't matter. Just wait for the process - // completion. - // - pr.wait (); // Check throw. + error (ll) << "unable to execute " << pp << ": " << e.what (); + + if (e.child ()) + exit (1); - fail (ll) << "IO operation failed for " << pp << ": " << e.what (); + throw failed (); } } - catch (const process_error& e) - { - error (ll) << "unable to execute " << pp << ": " << e.what (); - if (e.child ()) - exit (1); + const path& p (c.program); - throw failed (); + // If there is no correct exit status by whatever reason then dump + // stderr (if cached), print the proper diagnostics and fail. + // + // Comparison *status >= 0 causes "always true" warning on Windows + // where process::status_type is defined as uint32_t. + // + bool valid_status (status && *status < 256 && *status + 1 > 0); + bool eq (c.exit.comparison == exit_comparison::eq); + bool correct_status (valid_status && eq == (*status == c.exit.status)); + + if (!correct_status) + { + // Dump cached stderr. + // + if (exists (esp)) + { + try + { + ifdstream is (esp); + if (is.peek () != ifdstream::traits_type::eof ()) + cerr << is.rdbuf (); + } + catch (const io_error& e) + { + fail (ll) << "unable to read " << esp << ": " << e.what (); + } + } + + // Fail with a proper diagnostics. + // + diag_record d (fail (ll)); + + if (!status) + d << p << " terminated abnormally"; + else if (!valid_status) + d << p << " exit status " << *status << " is invalid" << + info << "must be an unsigned integer < 256"; + else if (!correct_status) + d << p << " exit status " << *status << (eq ? " != " : " == ") + << static_cast (c.exit.status); + 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; } + + // Check if the standard outputs match expectations. + // + check_output (p, osp, isp, c.out, ll, sp, "stdout"); + check_output (p, esp, isp, c.err, ll, sp, "stderr"); } } } diff --git a/build2/test/script/script b/build2/test/script/script index 0964af1..a2e4c41 100644 --- a/build2/test/script/script +++ b/build2/test/script/script @@ -281,7 +281,8 @@ namespace build2 // public: // Register a cleanup. If the cleanup is explicit, then override the - // cleanup type if this path is already registered. + // cleanup type if this path is already registered. Ignore implicit + // registration of a path outside script working directory. // void clean (cleanup, bool implicit); diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx index cfc1d91..7d1d8cf 100644 --- a/build2/test/script/script.cxx +++ b/build2/test/script/script.cxx @@ -347,7 +347,16 @@ namespace build2 assert (!implicit || c.type == cleanup_type::always); - auto pr = [&c] (const cleanup& v) -> bool {return v.path == c.path;}; + const path& p (c.path); + if (!p.sub (root->wd_path)) + { + if (implicit) + return; + else + assert (false); // Error so should have been checked. + } + + auto pr = [&p] (const cleanup& v) -> bool {return v.path == p;}; auto i (find_if (cleanups.begin (), cleanups.end (), pr)); if (i == cleanups.end ()) -- cgit v1.1