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 +- buildfile | 2 +- tests/test/script/integration/testscript | 20 +- tests/test/script/runner/buildfile | 5 +- tests/test/script/runner/cleanup.test | 122 ++++-- tests/test/script/runner/mkdir.test | 50 +++ tests/test/script/runner/redirect.test | 107 ++++-- tests/test/script/runner/status.test | 45 ++- tests/test/script/runner/touch.test | 44 +++ 14 files changed, 989 insertions(+), 386 deletions(-) create mode 100644 tests/test/script/runner/mkdir.test create mode 100644 tests/test/script/runner/touch.test 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 ()) diff --git a/buildfile b/buildfile index 45bcdcc..be107a3 100644 --- a/buildfile +++ b/buildfile @@ -2,7 +2,7 @@ # copyright : Copyright (c) 2014-2016 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -d = build2/ tests/ doc/ +d = build2/ tests/ unit-tests/ doc/ ./: $d \ doc{INSTALL LICENSE NEWS README version} \ diff --git a/tests/test/script/integration/testscript b/tests/test/script/integration/testscript index f9e89e2..8663e6f 100644 --- a/tests/test/script/integration/testscript +++ b/tests/test/script/integration/testscript @@ -2,7 +2,7 @@ # copyright : Copyright (c) 2014-2016 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -+mkdir build &build/ # @@ TMP ++mkdir build +cat <>>build/boostrap.build project = test amalgamation = @@ -39,14 +39,12 @@ test.arguments = test $* <>EOE != 0 ./: test{foo} EOI @@ -55,7 +53,7 @@ EOE : wd-exists-before : -touch foo.test &foo.test; #@@ TMP +touch foo.test; mkdir test &!test/; $* <>EOE ./: test{foo} @@ -65,7 +63,7 @@ EOE : wd-not-empty-before : -touch foo.test &foo.test; #@@ TMP +touch foo.test; mkdir test &!test/; touch test/dummy &!test/dummy; $* <>EOE @@ -78,11 +76,13 @@ EOE : wd-not-empty-after : : The idea here is to run a nested testscript that creates (but does not -: clean up) a file in our working directory. Note that we still have to -: remove everything after detecting the failure. +: clean up) a file in our working directory. Note that an implicit cleanup +: cancelation is not required as the file is out of the test root directory +: scope. Also note that we still have to remove everything after detecting the +: failure. : cat <>>foo.test; -touch ../../dummy &!../../dummy +touch ../../dummy EOI $* <>EOE &test/*** != 0 ./: test{foo} diff --git a/tests/test/script/runner/buildfile b/tests/test/script/runner/buildfile index e5f2761..b9c0e69 100644 --- a/tests/test/script/runner/buildfile +++ b/tests/test/script/runner/buildfile @@ -4,6 +4,9 @@ import libs = libbutl%lib{butl} -exe{driver}: cxx{driver} $libs test{cleanup redirect status} +exe{driver}: cxx{driver} $libs test{cleanup mkdir redirect status touch} + +if ($cxx.target.class == "windows") # @@ TMP + test{*}: ext = ".exe" include ../../../../../build2/ diff --git a/tests/test/script/runner/cleanup.test b/tests/test/script/runner/cleanup.test index bb65c27..4cd8650 100644 --- a/tests/test/script/runner/cleanup.test +++ b/tests/test/script/runner/cleanup.test @@ -2,7 +2,7 @@ # copyright : Copyright (c) 2014-2016 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -+mkdir build &build/ # @@ Should not be necessary once builtin. ++mkdir build +cat <>>build/boostrap.build project = test amalgamation = @@ -13,6 +13,7 @@ EOI b = $build.driver -q --no-column --buildfile - <"./: test{testscript}" \ &?test/*** test c = cat >>>testscript +test = \'$test\' # Valid cleanups. # @@ -33,6 +34,28 @@ $b $c <"$* &!a"; $b +: file-implicit +: +: Test that a file created out of the script working directory is not +: implicitly registered for cleanup. If it were, the test would fail due to +: 'rm' failure. +: +$c <"touch ../../a"; +$b; +rm a + +: file-append +: +: Test that file append redirect doesn't not register cleanup. If it did, that +: cleanup would fail as the file would be already deleted by 'rm'. +: +$c <>>&a; +rm a +EOI +$b + : dir-always : $c <"$* -d a &a/"; @@ -43,6 +66,16 @@ $b $c <"$* &?a/"; $b +: dir-implicit +: +: Test that a directory created out of the script working directory is not +: implicitly registered for cleanup. If it were, the test would fail due to +: 'rm' failure. +: +$c <"mkdir ../../a"; +$b; +rm -r a + : wildcard-always : $c <"$* -d a/b -f a/b/c &a/***"; @@ -54,7 +87,8 @@ $c <"$* &?a/***"; $b : order -: Test that cleanup is performed in registration reversed order +: +: Test that cleanup is performed in registration reversed order. : $c <"$* -d a/b &a/ &a/b/"; $b @@ -62,7 +96,8 @@ $b # Invalid cleanups. # : file-not-exists -: Test cleanup of non-existing file +: +: Test cleanup of non-existing file. : $c <"$* &a"; $b 2>>EOE != 0 @@ -70,23 +105,34 @@ testscript:1: error: registered for cleanup file test/1/a does not exist EOE : file-out-wd -: Test cleanup of file out of working directory +: +: Test explicit cleanup of a file out of the script working directory. +: +$c <"$* &../../a"; +$b 2>>EOE != 0 +testscript:1: error: file cleanup ../../a is out of working directory test/ +EOE + +: file-in-wd +: +: Test cleanup explicit registration of a file being outside the scope working +: directory but inside the script working directory. : $c <"$* &../a"; $b 2>>EOE != 0 -testscript:1: error: registered for cleanup file test/a is out of working directory test/1/ +testscript:1: error: registered for cleanup file test/a does not exist EOE : not-file -: Test cleanup of directory as a file +: +: Test cleanup of a directory as a file. : $c <"$* -d a &a"; -$b 2>>EOE != 0 -error: unable to remove file test/1/a: Is a directory -EOE +$b 2>- != 0 # @@ REGEX : dir-not-exists -: Test cleanup of non-existing directory +: +: Test cleanup of non-existing directory. : $c <"$* &a/"; $b 2>>EOE != 0 @@ -94,15 +140,27 @@ testscript:1: error: registered for cleanup directory test/1/a/ does not exist EOE : dir-out-wd -: Test cleanup of directory out of working directory +: +: Test cleanup of a directory out of the script working directory. +: +$c <"$* &../../a/"; +$b 2>>EOE != 0 +testscript:1: error: directory cleanup ../../a/ is out of working directory test/ +EOE + +: dir-in-wd +: +: Test cleanup explicit registration of a directory being outside the scope +: working directory but inside the script working directory. : $c <"$* &../a/"; $b 2>>EOE != 0 -testscript:1: error: registered for cleanup directory test/a/ is out of working directory test/1/ +testscript:1: error: registered for cleanup directory test/a/ does not exist EOE : dir-not-empty -: Test cleanup of non-empty directory +: +: Test cleanup of a non-empty directory. : $c <"$* -d a -f a/b &a/"; $b 2>>EOE != 0 @@ -110,15 +168,15 @@ testscript:1: error: registered for cleanup directory test/1/a/ is not empty EOE : not-dir -: Test cleanup of file as a directory +: +: Test cleanup of a file as a directory. : $c <"$* -f a &a/"; -$b 2>>EOE != 0 -error: unable to remove directory test/1/a/: Not a directory -EOE +$b 2>- != 0 # @@ REGEX : wildcard-not-exists -: Test cleanup of wildcard not matching any directory +: +: Test cleanup of a wildcard not matching any directory. : $c <"$* &a/***"; $b 2>>EOE != 0 @@ -126,23 +184,34 @@ testscript:1: error: registered for cleanup wildcard test/1/a/*** doesn't match EOE : wildcard-out-wd -: Test cleanup of wildcard out of working directory +: +: Test cleanup of a wildcard out of the script working directory. +: +$c <"$* &../../a/***"; +$b 2>>EOE != 0 +testscript:1: error: wildcard cleanup ../../a/*** is out of working directory test/ +EOE + +: wildcard-in-wd +: +: Test cleanup registration of a wildcard matching the directory that being +: outside the scope working directory is inside the script working directory. : $c <"$* &../a/***"; $b 2>>EOE != 0 -testscript:1: error: registered for cleanup wildcard test/a/*** is out of working directory test/1/ +testscript:1: error: registered for cleanup wildcard test/a/*** doesn't match a directory EOE : wildcard-not-dir -: Test cleanup of file as a wildcard +: +: Test cleanup of a file as a wildcard. : $c <"$* -f a &a/***"; -$b 2>>EOE != 0 -error: unable to remove directory test/1/a/: Not a directory -EOE +$b 2>- != 0 # @@ REGEX : implicit-overwrite -: Test implicit cleanup being overwritten with an explicit one +: +: Test an implicit cleanup being overwritten with the explicit one, : $c <"$* -o foo >>>a &!a"; $b 2>>EOE != 0 @@ -150,7 +219,8 @@ testscript:1: error: registered for cleanup directory test/1/ is not empty EOE : explicit-overwrite -: Test explicit cleanup not being overwritten with an implicit one +: +: Test an explicit cleanup not being overwritten with the implicit one. : $c <"mkdir: missing directory" == 1 + +: empty-path +: +: Test creation of empty directory path. +: +mkdir '' 2>"mkdir: invalid path ''" == 1 + +: already-exists +: +: Test creation of an existing directory. Note that error message is +: platform-dependent so is not checked. +: +mkdir a 2>- a == 1 # @@ REGEX + +: not-exists +: +: Test creation of a directory with non-existent parent. Note that error +: message is platform-dependent so is not checked. +: +mkdir a/b 2>- == 1 # @@ REGEX diff --git a/tests/test/script/runner/redirect.test b/tests/test/script/runner/redirect.test index a49cd3c..68cc9aa 100644 --- a/tests/test/script/runner/redirect.test +++ b/tests/test/script/runner/redirect.test @@ -2,31 +2,39 @@ # copyright : Copyright (c) 2014-2016 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -$* -o foo >- # out-null -$* -e foo 2>- # err-null -$* -i 0 foo # out-str -$* -e foo 2>foo # err-str -$* -i 1 foo # inout-str -$* -i 2 foo # inerr-str -$* -i 1 -e bar foo 2>bar # inout-err-str - -$* -i 0 <- : out-null +$* -e foo 2>- : err-null +$* -i 0 foo : out-str +$* -e foo 2>foo : err-str +$* -i 1 foo : inout-str +$* -i 2 foo : inerr-str +$* -i 1 -e bar foo 2>bar : inout-err-str + +: in-doc +: +$* -i 0 <>EOO # out-doc +: out-doc +: +$* -o foo -o bar >>EOO foo bar EOO -$* -e foo -e bar 2>>EOO # err-doc +: err-doc +: +$* -e foo -e bar 2>>EOO foo bar EOO -$* -i 1 <>EOO # inout-doc +: inout-doc +: +$* -i 1 <>EOO foo bar EOI @@ -34,7 +42,9 @@ foo bar EOO -$* -i 2 <>EOE # inerr-doc +: inerr-doc +: +$* -i 2 <>EOE foo bar EOI @@ -42,22 +52,15 @@ foo bar EOE -$* -i 1 -e bar -e baz -s 2 <>EOO 2>>EOE == 2 # inout-err-doc-status -foo -bar +: empty-str-doc +: +$* -i 1 -e "" <>EOO 2>"" EOI -foo -bar EOO -bar -baz -EOE -$* -i 1 -e "" <>EOO 2>"" # empty-str-doc -EOI -EOO - -$* -i 1 <>EOO # nl-containing-doc +: nl-containing-doc +: +$* -i 1 <>EOO EOI @@ -67,13 +70,18 @@ EOO # # @@ TMP Need does not compare test. # -$* -i 1 <:"foo" >:"foo" # no-newline-str +: no-newline-str +: +$* -i 1 <:"foo" >:"foo" + #\ $* -i 1 <:"foo" >!"foo" # no-newline-str-fail1 $* -i 1 <"foo" >:!"foo" # no-newline-str-fail2 #\ -$* -i 1 <<:EOI >>:EOO # no-newline-doc +: no-newline-doc +: +$* -i 1 <<:EOI >>:EOO foo EOI foo @@ -93,29 +101,39 @@ foo EOO #\ -$* -i 1 <<:EOI >>:EOO 2>:"" # no-newline-empty-str-doc +: no-newline-empty-str-doc +: +$* -i 1 <<:EOI >>:EOO 2>:"" EOI EOO -$* -i 1 <<:EOI >>:EOO # no-newline-nl-cont-doc +: no-newline-nl-cont-doc +: +$* -i 1 <<:EOI >>:EOO EOI EOO -$* -o foo >>>out; # file +: file +: +$* -o foo >>>out; $* -e bar 2>>>&out; $* -i 1 <<>EOO foo bar EOO -$* -o foo -e bar 2>>EOE 1>&2 # merge-str +: merge-str +: +$* -o foo -e bar 2>>EOE 1>&2 foo bar EOE -$* -i 1 <>EOO 2>&1 # merge-doc +: merge-doc +: +$* -i 1 <>EOO 2>&1 foo bar EOI @@ -124,7 +142,9 @@ bar baz EOO -$* -o foo -e bar 2>&1 >>>out; # merge-file +: merge-file +: +$* -o foo -e bar 2>&1 >>>out; $* -e baz -o biz 1>&2 2>>>&out; $* -i 1 <<>EOO foo @@ -132,3 +152,20 @@ bar baz biz EOO + +# Builtins redirects. +# +# @@ That will probably become redundant when builtins and process obtain file +# descriptors uniformly. +# +: builtins +: +{ + echo "abc" >- : out-null + echo "abc" 1>&2 2>- : err-null + echo - : in-str + echo "foo" >foo : out-str + echo "foo" 2>foo 1>&2 : err-str + cat foo : inout-str # @@ cat is not a builtin yet. + cat foo 1>&2 : inerr-str +} diff --git a/tests/test/script/runner/status.test b/tests/test/script/runner/status.test index 0ef1ee9..f1ad5bf 100644 --- a/tests/test/script/runner/status.test +++ b/tests/test/script/runner/status.test @@ -2,8 +2,43 @@ # copyright : Copyright (c) 2014-2016 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -$* # status-def @@ status- is already in file name -$* == 0 # status-eq-0 -$* -s 1 != 0 # status-ne-0 -$* -s 1 == 1 # status-eq-1 -$* != 1 # status-ne-1 ++mkdir build ++cat <>>build/boostrap.build +project = test +amalgamation = + +using test +EOI + +b = $build.driver -q --no-column --buildfile - <"./: test{testscript}" \ + &?test/*** test +c = cat >>>testscript +test = \'$test\' + +# Successfull tests. +# +: eq-true +: +$c <"$* == 0"; +$b + +: ne-true +: +$c <"$* -s 1 != 0"; +$b + +# Faulty tests. +# +: eq-false +: +$c <"$* -s 1 == 0"; +$b 2>>EOE != 0 +testscript:1: error: ../../../driver$ext exit status 1 != 0 +EOE + +: ne-false +: +$c <"$* -s 1 != 1"; +$b 2>>EOE != 0 +testscript:1: error: ../../../driver$ext exit status 1 == 1 +EOE diff --git a/tests/test/script/runner/touch.test b/tests/test/script/runner/touch.test new file mode 100644 index 0000000..e9d9f68 --- /dev/null +++ b/tests/test/script/runner/touch.test @@ -0,0 +1,44 @@ +# file : tests/test/script/runner/touch.test +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +: file +: +touch a + +: file-create +: +: Test that file is created. If it didn't then 'rm' would fail. +: +touch a &!a; +rm a + +: file-update +: +: Test that existing file touch doesn't register cleanup. If it did then it +: would be left dangling after 'rm' call and so test would fail. +: +$* -f a; +touch a; +rm a + +: no-args +: +: Test passing no arguments. +: +touch 2>"touch: missing file" == 1 + +: empty-path +: +: Test touching an empty path. +: +touch '' 2>"touch: invalid path ''" == 1 + +: dir-update +: +: Test touching an existing directory. +: +a = [path] $~; +a += "a"; +mkdir a; +touch 2>"touch: '$a' exists and is not a file" a == 1 -- cgit v1.1