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/runner.cxx | 640 ++++++++++++++++++++++++------------------ 1 file changed, 368 insertions(+), 272 deletions(-) (limited to 'build2/test/script/runner.cxx') 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"); } } } -- cgit v1.1