aboutsummaryrefslogtreecommitdiff
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
parent6e1f59670b7aaabb27830a345aad9532af111016 (diff)
Add mkdir and touch builtins
-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
-rw-r--r--buildfile2
-rw-r--r--tests/test/script/integration/testscript20
-rw-r--r--tests/test/script/runner/buildfile5
-rw-r--r--tests/test/script/runner/cleanup.test122
-rw-r--r--tests/test/script/runner/mkdir.test50
-rw-r--r--tests/test/script/runner/redirect.test107
-rw-r--r--tests/test/script/runner/status.test45
-rw-r--r--tests/test/script/runner/touch.test44
14 files changed, 989 insertions, 386 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 ())
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 <<EOI >>>build/boostrap.build
project = test
amalgamation =
@@ -39,14 +39,12 @@ test.arguments = test
$* <<EOI
./: test{../foo ../bar}
EOI
-
- -rm -f testscript foo.test bar.test
}
: wd-is-file
:
-touch foo.test &foo.test; #@@ TMP
-touch test &test; #@@ TMP
+touch foo.test;
+touch test;
$* <<EOI 2>>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/;
$* <<EOI 2>>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;
$* <<EOI 2>>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 <<EOI >>>foo.test;
-touch ../../dummy &!../../dummy
+touch ../../dummy
EOI
$* <<EOI 2>>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 <<EOI >>>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 <<EOI;
+touch a &!a;
+$* -o foo >>>&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 <<EOO;
$* &!a;
diff --git a/tests/test/script/runner/mkdir.test b/tests/test/script/runner/mkdir.test
new file mode 100644
index 0000000..6b7b5c9
--- /dev/null
+++ b/tests/test/script/runner/mkdir.test
@@ -0,0 +1,50 @@
+# file : tests/test/script/runner/mkdir.test
+# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+: dirs
+:
+mkdir a b;
+touch a/a b/b
+
+: parent
+:
+mkdir -p a/b;
+touch a/a a/b/b
+
+: exists
+:
+mkdir -p a a a/b a/b
+
+: double-dash
+:
+: Make sure '-p' directory is created.
+:
+mkdir -p -- -p;
+touch -p/a
+
+: no-args
+:
+: Test passing no arguments.
+:
+mkdir 2>"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 # in-str
-$* -o foo >foo # out-str
-$* -e foo 2>foo # err-str
-$* -i 1 <foo >foo # inout-str
-$* -i 2 <foo 2>foo # inerr-str
-$* -i 1 -e bar <foo 1>foo 2>bar # inout-err-str
-
-$* -i 0 <<EOO # in-doc
+$* -o foo >- : out-null
+$* -e foo 2>- : err-null
+$* -i 0 <foo : in-str
+$* -o foo >foo : out-str
+$* -e foo 2>foo : err-str
+$* -i 1 <foo >foo : inout-str
+$* -i 2 <foo 2>foo : inerr-str
+$* -i 1 -e bar <foo 1>foo 2>bar : inout-err-str
+
+: in-doc
+:
+$* -i 0 <<EOO
foo
bar
EOO
-$* -o foo -o bar >>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 <<EOI >>EOO # inout-doc
+: inout-doc
+:
+$* -i 1 <<EOI >>EOO
foo
bar
EOI
@@ -34,7 +42,9 @@ foo
bar
EOO
-$* -i 2 <<EOI 2>>EOE # inerr-doc
+: inerr-doc
+:
+$* -i 2 <<EOI 2>>EOE
foo
bar
EOI
@@ -42,22 +52,15 @@ foo
bar
EOE
-$* -i 1 -e bar -e baz -s 2 <<EOI 1>>EOO 2>>EOE == 2 # inout-err-doc-status
-foo
-bar
+: empty-str-doc
+:
+$* -i 1 -e "" <<EOI >>EOO 2>""
EOI
-foo
-bar
EOO
-bar
-baz
-EOE
-$* -i 1 -e "" <<EOI >>EOO 2>"" # empty-str-doc
-EOI
-EOO
-
-$* -i 1 <<EOI >>EOO # nl-containing-doc
+: nl-containing-doc
+:
+$* -i 1 <<EOI >>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 <<<out >>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 <<EOI -e baz >>EOO 2>&1 # merge-doc
+: merge-doc
+:
+$* -i 1 <<EOI -e baz >>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 <<<out >>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 <foo 1>- : in-str
+ echo "foo" >foo : out-str
+ echo "foo" 2>foo 1>&2 : err-str
+ cat <foo >foo : inout-str # @@ cat is not a builtin yet.
+ cat <foo 2>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 <<EOI >>>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