aboutsummaryrefslogtreecommitdiff
path: root/build2
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2016-11-04 10:51:57 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2016-11-08 16:12:59 +0300
commit278140ebf2bc97eb72a1e8adb04a40a0a5807d8f (patch)
treebf6ea4e55c98b0543bb51c6eb46f48e29b58aecb /build2
parent6e1f59670b7aaabb27830a345aad9532af111016 (diff)
Add mkdir and touch builtins
Diffstat (limited to 'build2')
-rw-r--r--build2/test/script/builtin5
-rw-r--r--build2/test/script/builtin.cxx313
-rw-r--r--build2/test/script/parser.cxx8
-rw-r--r--build2/test/script/runner.cxx640
-rw-r--r--build2/test/script/script3
-rw-r--r--build2/test/script/script.cxx11
6 files changed, 672 insertions, 308 deletions
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<string, builtin>
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 <build2/test/script/builtin>
-#include <butl/fdstream>
+#ifndef _WIN32
+# include <utime.h>
+#else
+# include <sys/utime.h>
+#endif
+
+#include <butl/path-io> // use default operator<< implementation
+#include <butl/fdstream> // fdopen_mode
+#include <butl/filesystem> // mkdir_status
+
+#include <build2/test/script/script>
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 <string>...
+ //
+ 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] <dir>...
+ //
+ // -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<dir_path> (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 <path>...
+ //
+ // 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<strings> (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 <set>
#include <iostream> // cerr
+#include <butl/fdstream> // fdopen_mode, fdnull(), fddup()
+
#include <build2/filesystem>
+#include <build2/test/script/builtin>
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<dir_path> (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<dir_path> (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<process::status_type> 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<process::status_type> 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<uint16_t> (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<uint16_t> (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 ())