From 5f85bd7d156b141508b7dd7ec0e340e73c4970a5 Mon Sep 17 00:00:00 2001
From: Sam Yates <yates@cscs.ch>
Date: Fri, 21 Apr 2017 13:58:15 +0200
Subject: [PATCH] Try more places for validation data; workaround CMake
 FindCUDA bug. (#233)

Fixes #232.

* Try `NMC_DATADIR` environment variable for validation data path, or else if the #defined `NMC_DATADIR` does not point to a directory, try `./validation/data` and `../validation/data`.
* Don't define `NMC_DATADIR` if CMake version 3.7 or 3.8.
* Extend C++17 filesystem emulation with (POSIX) implementation of `is_directory` and supporting classes, functions and enums `filesystem_error`, `file_status`, `status(..)`, `file_type` and `perms`.
---
 src/util/path.cpp                    |  88 +++-
 src/util/path.hpp                    | 633 +++++++++++++++++----------
 tests/unit/CMakeLists.txt            |   4 +-
 tests/unit/test_path.cpp             | 185 ++++++--
 tests/validation/CMakeLists.txt      |   6 +-
 tests/validation/validate.cpp        |   8 +-
 tests/validation/validation_data.cpp |  33 ++
 tests/validation/validation_data.hpp |  21 +-
 8 files changed, 685 insertions(+), 293 deletions(-)

diff --git a/src/util/path.cpp b/src/util/path.cpp
index 53cc710b..88c74b56 100644
--- a/src/util/path.cpp
+++ b/src/util/path.cpp
@@ -1,25 +1,35 @@
+// POSIX headers
+extern "C" {
+#define _POSIX_C_SOURCE 2
+#include <glob.h>
+#include <sys/stat.h>
+}
+
+// GLOB_TILDE and GLOB_BRACE are non-standard but convenient and common
+// flags for glob().
+
+#ifndef GLOB_TILDE
+#define GLOB_TILDE 0
+#endif
+#ifndef GLOB_BRACE
+#define GLOB_BRACE 0
+#endif
+
+#include <cerrno>
+
 #include <util/scope_exit.hpp>
 #include <util/path.hpp>
 
-// POSIX header
-#include <glob.h>
-
 namespace nest {
 namespace mc {
 namespace util {
+namespace posix {
 
 std::vector<path> glob(const std::string& pattern) {
     std::vector<path> paths;
-
-    int flags = GLOB_MARK | GLOB_NOCHECK;
-#if defined(GLOB_TILDE)
-    flags |= GLOB_TILDE;
-#endif
-#if defined(GLOB_TILDE)
-    flags |= GLOB_BRACE;;
-#endif
-
     glob_t matches;
+
+    int flags = GLOB_MARK | GLOB_NOCHECK | GLOB_TILDE | GLOB_BRACE;
     auto r = ::glob(pattern.c_str(), flags, nullptr, &matches);
     auto glob_guard = on_scope_exit([&]() { ::globfree(&matches); });
 
@@ -37,6 +47,60 @@ std::vector<path> glob(const std::string& pattern) {
     return paths;
 }
 
+namespace impl {
+    file_status status(const char* p, int r, struct stat& st, std::error_code& ec) {
+        if (!r) {
+            // Success:
+            ec.clear();
+            perms p = static_cast<perms>(st.st_mode&07777);
+            switch (st.st_mode&S_IFMT) {
+            case S_IFSOCK:
+                return file_status{file_type::socket, p};
+            case S_IFLNK:
+                return file_status{file_type::symlink, p};
+            case S_IFREG:
+                return file_status{file_type::regular, p};
+            case S_IFBLK:
+                return file_status{file_type::block, p};
+            case S_IFDIR:
+                return file_status{file_type::directory, p};
+            case S_IFCHR:
+                return file_status{file_type::character, p};
+            case S_IFIFO:
+                return file_status{file_type::fifo, p};
+            default:
+                return file_status{file_type::unknown, p};
+            }
+        }
+
+        // Handle error cases especially.
+
+        if ((errno==ENOENT || errno==ENOTDIR) && p && *p) {
+            // If a non-empty path, return `not_found`.
+            ec.clear();
+            return file_status{file_type::not_found};
+        }
+        else {
+            ec = std::error_code(errno, std::generic_category());
+            return file_status{file_type::none};
+        }
+    }
+} // namespace impl
+
+
+file_status status(const path& p, std::error_code& ec) {
+    struct stat st;
+    int r = stat(p.c_str(), &st);
+    return impl::status(p.c_str(), r, st, ec);
+}
+
+file_status symlink_status(const path& p, std::error_code& ec) {
+    struct stat st;
+    int r = lstat(p.c_str(), &st);
+    return impl::status(p.c_str(), r, st, ec);
+}
+
+} // namespace posix
 } // namespace util
 } // namespace mc
 } // namespace nest
diff --git a/src/util/path.hpp b/src/util/path.hpp
index d89873ad..e3114518 100644
--- a/src/util/path.hpp
+++ b/src/util/path.hpp
@@ -3,7 +3,7 @@
 /*
  * Small subset of C++17 filesystem::path functionality
  *
- * Missing functionality:
+ * Missing path functionality:
  *   * Wide character methods
  *   * Locale-aware conversions
  *   * Path decomposition
@@ -12,11 +12,13 @@
  *   * Path element iteration
  * 
  * If we do want to support non-POSIX paths properly, consider
- * making posix::path into a generic `basic_path` parameterized
+ * making posix_path into a generic `basic_path` parameterized
  * on `value_type`, `preferred_separator`, and using CRTP to
  * handle system-specific conversions.
  */
 
+#include <cstddef>
+#include <exception>
 #include <string>
 #include <iostream>
 #include <utility>
@@ -28,251 +30,400 @@ namespace nest {
 namespace mc {
 namespace util {
 
-namespace posix {
-    class path {
-    public:
-        using value_type = char;
-        using string_type = std::basic_string<value_type>;
-        static constexpr value_type preferred_separator = '/';
-
-        path() = default;
-        path(const path&) = default;
-        path(path&&) = default;
-
-        path& operator=(const path&) = default;
-        path& operator=(path&&) = default;
-
-        // Swap internals
-
-        void swap(path& other) {
-            p_.swap(other.p_);
-        }
-
-        // Construct or assign from value_type string or sequence.
-
-        template <typename Source>
-        path(Source&& source) { assign(std::forward<Source>(source)); }
-
-        template <typename Iter>
-        path(Iter b, Iter e) { assign(b, e); }
-
-        template <typename Source>
-        path& operator=(const Source& source) { return assign(source); }
-
-        path& assign(const path& other) {
-            p_ = other.p_;
-            return *this;
-        }
-
-        path& assign(const string_type& source) {
-            p_ = source;
-            return *this;
-        }
-
-        path& assign(const value_type* source) {
-            p_ = source;
-            return *this;
-        }
-
-        template <typename Seq, typename = enable_if_sequence_t<Seq>>
-        path& assign(const Seq& seq) {
-            util::assign(p_, seq);
-            return *this;
-        }
-
-        template <typename Iter>
-        path& assign(Iter b, Iter e) {
-            p_.assign(b, e);
-            return *this;
-        }
-
-        // Empty?
-        bool empty() const { return p_.empty(); }
-
-        // Make empty
-        void clear() { p_.clear(); }
-
-        // Append path components
-
-        template <typename Source>
-        path& append(const Source& source) {
-            return append(path(source));
-        }
-
-        template <typename Iter>
-        path& append(Iter b, Iter e) {
-            return append(path(b, e));
-        }
-
-        template <typename Source>
-        path& operator/=(const Source& source) {
-            return append(path(source));
-        }
-
-        path& append(const path& tail) {
-            if (!p_.empty() &&
-                !is_separator(p_.back()) &&
-                !tail.p_.empty() &&
-                !is_separator(tail.p_.front()))
-            {
-                p_ += preferred_separator;
-            }
-            if (!p_.empty() &&
-                !tail.empty() &&
-                is_separator(p_.back()) &&
-                is_separator(tail.p_.front()))
-            {
-                p_ += tail.p_.substr(1);
-            }
-            else {
-                p_ += tail.p_;
-            }
-            return *this;
-        }
-
-        // Concat to path
-
-        template <typename Source>
-        path& concat(const Source& source) {
-            return concat(path(source));
-        }
-
-        template <typename Iter>
-        path& concat(Iter b, Iter e) {
-            return concat(path(b, e));
-        }
-
-        template <typename Source>
-        path& operator+=(const Source& source) {
-           return  concat(path(source));
-        }
-
-        path& concat(const path& tail) {
+class posix_path {
+public:
+    using value_type = char;
+    using string_type = std::basic_string<value_type>;
+    static constexpr value_type preferred_separator = '/';
+
+    posix_path() = default;
+    posix_path(const posix_path&) = default;
+    posix_path(posix_path&&) = default;
+
+    posix_path& operator=(const posix_path&) = default;
+    posix_path& operator=(posix_path&&) = default;
+
+    // Swap internals
+
+    void swap(posix_path& other) {
+        p_.swap(other.p_);
+    }
+
+    // Construct or assign from value_type string or sequence.
+
+    template <typename Source>
+    posix_path(Source&& source) { assign(std::forward<Source>(source)); }
+
+    template <typename Iter>
+    posix_path(Iter b, Iter e) { assign(b, e); }
+
+    template <typename Source>
+    posix_path& operator=(const Source& source) { return assign(source); }
+
+    posix_path& assign(const posix_path& other) {
+        p_ = other.p_;
+        return *this;
+    }
+
+    posix_path& assign(const string_type& source) {
+        p_ = source;
+        return *this;
+    }
+
+    posix_path& assign(const value_type* source) {
+        p_ = source;
+        return *this;
+    }
+
+    template <typename Seq, typename = enable_if_sequence_t<Seq>>
+    posix_path& assign(const Seq& seq) {
+        util::assign(p_, seq);
+        return *this;
+    }
+
+    template <typename Iter>
+    posix_path& assign(Iter b, Iter e) {
+        p_.assign(b, e);
+        return *this;
+    }
+
+    bool empty() const { return p_.empty(); }
+
+    void clear() { p_.clear(); }
+
+    // Append posix_path components
+    template <typename Source>
+    posix_path& append(const Source& source) {
+        return append(posix_path(source));
+    }
+
+    template <typename Iter>
+    posix_path& append(Iter b, Iter e) {
+        return append(posix_path(b, e));
+    }
+
+    template <typename Source>
+    posix_path& operator/=(const Source& source) {
+        return append(posix_path(source));
+    }
+
+    posix_path& append(const posix_path& tail) {
+        if (!p_.empty() &&
+            !is_separator(p_.back()) &&
+            !tail.p_.empty() &&
+            !is_separator(tail.p_.front()))
+        {
+            p_ += preferred_separator;
+        }
+        if (!p_.empty() &&
+            !tail.empty() &&
+            is_separator(p_.back()) &&
+            is_separator(tail.p_.front()))
+        {
+            p_ += tail.p_.substr(1);
+        }
+        else {
             p_ += tail.p_;
-            return *this;
-        }
-
-        // Native path string
-
-        const value_type* c_str() const { return p_.c_str(); }
-        const string_type& native() const { return p_; }
-        operator string_type() const { return p_; }
-
-        // Generic path string (same for POSIX)
-
-        std::string generic_string() const { return p_; }
-
-        // Queries
-
-        bool is_absolute() const {
-            return !p_.empty() && p_.front()==preferred_separator;
-        }
-
-        bool is_relative() const {
-            return !is_absolute();
-        }
-
-        // Compare
-
-        int compare(const string_type& s) const {
-            return compare(path(s));
-        }
-
-        int compare(const value_type* s) const {
-            return compare(path(s));
-        }
-
-        int compare(const path& other) const {
-            // TODO: replace with cleaner implementation if/when iterators
-            // are implemented.
-
-            return canonical().compare(other.canonical());
-        }
-
-        // Non-member functions
-
-        friend path operator/(const path& a, const path& b) {
-            path p(a);
-            return p/=b;
-        }
-
-        friend std::size_t hash_value(const path& p) {
-            std::hash<path::string_type> hash;
-            return hash(p.p_);
-        }
-
-        friend std::ostream& operator<<(std::ostream& o, const path& p) {
-            return o << p.native();
-        }
-
-        friend std::istream& operator>>(std::istream& i, path& p) {
-            path::string_type s;
-            i >> s;
-            p = s;
-            return i;
         }
-
-        friend void swap(path& lhs, path& rhs) {
-            lhs.swap(rhs);
-        }
-
-        friend bool operator<(const path& p, const path& q) {
-            return p.compare(q)<0;
-        }
-
-        friend bool operator>(const path& p, const path& q) {
-            return p.compare(q)>0;
-        }
-
-        friend bool operator==(const path& p, const path& q) {
-            return p.compare(q)==0;
-        }
-
-        friend bool operator!=(const path& p, const path& q) {
-            return p.compare(q)!=0;
-        }
-
-        friend bool operator<=(const path& p, const path& q) {
-            return p.compare(q)<=0;
-        }
-
-        friend bool operator>=(const path& p, const path& q) {
-            return p.compare(q)>=0;
-        }
-
-    protected:
-        static bool is_separator(value_type c) {
-            return c=='/' || c==preferred_separator;
-        }
-
-        std::string canonical() const {
-            std::string n;
-            value_type prev = 0;
-            for (value_type c: p_) {
-                if (is_separator(c)) {
-                    if (!is_separator(prev)) {
-                        n += '/';
-                    }
-                }
-                else {
-                    n += c;
+        return *this;
+    }
+
+    // Concat to posix_path
+    template <typename Source>
+    posix_path& concat(const Source& source) {
+        return concat(posix_path(source));
+    }
+
+    template <typename Iter>
+    posix_path& concat(Iter b, Iter e) {
+        return concat(posix_path(b, e));
+    }
+
+    template <typename Source>
+    posix_path& operator+=(const Source& source) {
+       return  concat(posix_path(source));
+    }
+
+    posix_path& concat(const posix_path& tail) {
+        p_ += tail.p_;
+        return *this;
+    }
+
+    // Native posix_path string
+    const value_type* c_str() const { return p_.c_str(); }
+    const string_type& native() const { return p_; }
+    operator string_type() const { return p_; }
+
+    // Generic posix_path string (same for POSIX)
+    std::string generic_string() const { return p_; }
+
+    // Queries
+    bool is_absolute() const {
+        return !p_.empty() && p_.front()==preferred_separator;
+    }
+
+    bool is_relative() const {
+        return !is_absolute();
+    }
+
+    int compare(const string_type& s) const {
+        return compare(posix_path(s));
+    }
+
+    int compare(const value_type* s) const {
+        return compare(posix_path(s));
+    }
+
+    int compare(const posix_path& other) const {
+        // TODO: replace with cleaner implementation if/when iterators
+        // are implemented.
+
+        return canonical().compare(other.canonical());
+    }
+
+    // Non-member functions
+
+    friend posix_path operator/(const posix_path& a, const posix_path& b) {
+        posix_path p(a);
+        return p/=b;
+    }
+
+    friend std::size_t hash_value(const posix_path& p) {
+        std::hash<posix_path::string_type> hash;
+        return hash(p.p_);
+    }
+
+    friend std::ostream& operator<<(std::ostream& o, const posix_path& p) {
+        return o << p.native();
+    }
+
+    friend std::istream& operator>>(std::istream& i, posix_path& p) {
+        posix_path::string_type s;
+        i >> s;
+        p = s;
+        return i;
+    }
+
+    friend void swap(posix_path& lhs, posix_path& rhs) {
+        lhs.swap(rhs);
+    }
+
+    friend bool operator<(const posix_path& p, const posix_path& q) {
+        return p.compare(q)<0;
+    }
+
+    friend bool operator>(const posix_path& p, const posix_path& q) {
+        return p.compare(q)>0;
+    }
+
+    friend bool operator==(const posix_path& p, const posix_path& q) {
+        return p.compare(q)==0;
+    }
+
+    friend bool operator!=(const posix_path& p, const posix_path& q) {
+        return p.compare(q)!=0;
+    }
+
+    friend bool operator<=(const posix_path& p, const posix_path& q) {
+        return p.compare(q)<=0;
+    }
+
+    friend bool operator>=(const posix_path& p, const posix_path& q) {
+        return p.compare(q)>=0;
+    }
+
+protected:
+    static bool is_separator(value_type c) {
+        return c=='/' || c==preferred_separator;
+    }
+
+    std::string canonical() const {
+        std::string n;
+        value_type prev = 0;
+        for (value_type c: p_) {
+            if (is_separator(c)) {
+                if (!is_separator(prev)) {
+                    n += '/';
                 }
-                prev = c;
             }
-            if (!n.empty() && n.back()=='/') {
-                n += '.';
+            else {
+                n += c;
             }
-            return n;
-        }
-
-        string_type p_;
-    };
-} // namespace posix
+            prev = c;
+        }
+        if (!n.empty() && n.back()=='/') {
+            n += '.';
+        }
+        return n;
+    }
+
+    string_type p_;
+};
+
+// Paths for now are just POSIX paths.
+using path = posix_path;
+
+// Following C++17 std::filesystem::perms:
+enum class perms: unsigned {
+    none = 0,
+    owner_read = 0400,
+    owner_write = 0200,
+    owner_exec = 0100,
+    owner_all = 0700,
+    group_read = 040,
+    group_write = 020,
+    group_exec = 010,
+    group_all = 070,
+    others_read = 04,
+    others_write = 02,
+    others_exec = 01,
+    others_all = 07,
+    all = 0777,
+    set_uid = 04000,
+    set_gid = 02000,
+    sticky_bit = 01000,
+    mask = 07777,
+    unknown = 0xffff,
+    add_perms = 0x10000,
+    remove_perms = 0x20000,
+    resolve_symlinks = 0x40000,
+};
+
+inline perms operator&(perms x, perms y) { return static_cast<perms>(static_cast<unsigned>(x)&static_cast<unsigned>(y)); }
+inline perms operator|(perms x, perms y) { return static_cast<perms>(static_cast<unsigned>(x)|static_cast<unsigned>(y)); }
+inline perms operator^(perms x, perms y) { return static_cast<perms>(static_cast<unsigned>(x)^static_cast<unsigned>(y)); }
+inline perms operator~(perms x) { return static_cast<perms>(~static_cast<unsigned>(x)); }
+inline perms& operator&=(perms& x, perms y) { return x = x&y; }
+inline perms& operator|=(perms& x, perms y) { return x = x|y; }
+inline perms& operator^=(perms& x, perms y) { return x = x^y; }
+
+enum class file_type {
+    none, not_found, regular, directory, symlink, block,
+    character, fifo, socket, unknown
+};
+
+class file_status {
+public:
+    explicit file_status(file_type type, perms permissions = perms::unknown):
+        type_(type), perm_(permissions)
+    {}
+
+    file_status(): file_status(file_type::none) {}
+
+    file_status(const file_status&) = default;
+    file_status(file_status&&) = default;
+    file_status& operator=(const file_status&) = default;
+    file_status& operator=(file_status&&) = default;
+
+    file_type type() const { return type_; }
+    void type(file_type type) { type_ = type; }
+
+    perms permissions() const { return perm_; }
+    void permissions(perms perm) { perm_ = perm; }
+
+private:
+    file_type type_;
+    perms perm_;
+};
+
+class filesystem_error: public std::system_error {
+public:
+    filesystem_error(const std::string& what_arg, std::error_code ec):
+        std::system_error(ec, what_arg) {}
+
+    filesystem_error(const std::string& what_arg, const path& p1, std::error_code ec):
+        std::system_error(ec, what_arg), p1_(p1) {}
+
+    filesystem_error(const std::string& what_arg, const path& p1, const path& p2, std::error_code ec):
+        std::system_error(ec, what_arg), p1_(p1), p2_(p2) {}
+
+    const path& path1() const { return p1_; }
+    const path& path2() const { return p2_; }
+
+private:
+    path p1_, p2_;
+};
+
+// POSIX implementations of path queries (see path.cpp for implementations).
 
-using path = posix::path;
-
-// POSIX glob (3) wrapper.
-std::vector<path> glob(const std::string& pattern);
+namespace posix {
+    file_status status(const path&, std::error_code&);
+    file_status symlink_status(const path&, std::error_code&);
+
+    // POSIX glob (3) wrapper (not part of std::filesystem!).
+    std::vector<path> glob(const std::string& pattern);
+}
+
+inline file_status status(const path& p, std::error_code& ec) {
+    return posix::status(p, ec);
+}
+
+inline file_status symlink_status(const path& p, std::error_code& ec) {
+    return posix::symlink_status(p, ec);
+}
+
+inline std::vector<path> glob(const std::string& pattern) {
+    return posix::glob(pattern);
+}
+
+// Wrappers for `status()`, again following std::filesystem.
+
+inline file_status status(const path& p) {
+    std::error_code ec;
+    auto r = ::nest::mc::util::posix::status(p, ec);
+    if (ec) {
+        throw filesystem_error("status()", p, ec);
+    }
+    return r;
+}
+
+inline bool is_directory(file_status s) {
+    return s.type()==file_type::directory;
+}
+
+inline bool is_directory(const path& p) {
+    return is_directory(status(p));
+}
+
+inline bool is_directory(const path& p, std::error_code& ec) {
+    return is_directory(status(p, ec));
+}
+
+inline bool is_regular_file(file_status s) {
+    return s.type()==file_type::regular;
+}
+
+inline bool is_regular_file(const path& p) {
+    return is_regular_file(status(p));
+}
+
+inline bool is_regular_file(const path& p, std::error_code& ec) {
+    return is_regular_file(status(p, ec));
+}
+
+inline bool is_character_file(file_status s) {
+    return s.type()==file_type::character;
+}
+
+inline bool is_character_file(const path& p) {
+    return is_character_file(status(p));
+}
+
+inline bool is_character_file(const path& p, std::error_code& ec) {
+    return is_character_file(status(p, ec));
+}
+
+inline bool exists(file_status s) {
+    return s.type()!=file_type::not_found;
+}
+
+inline bool exists(const path& p) {
+    return exists(status(p));
+}
+
+inline bool exists(const path& p, std::error_code& ec) {
+    return exists(status(p, ec));
+}
 
 } // namespace util
 } // namespace mc
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 2aa34ffb..3ac4c1ff 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -72,11 +72,11 @@ set(TEST_SOURCES
     test.cpp
 )
 
-add_definitions("-DDATADIR=\"${PROJECT_SOURCE_DIR}/data\"")
 
 set(TARGETS test.exe)
 
 add_executable(test.exe ${TEST_SOURCES} ${HEADERS})
+target_compile_definitions(test.exe PUBLIC "-DDATADIR=\"${PROJECT_SOURCE_DIR}/data\"")
 
 if (NMC_AUTO_RUN_MODCC_ON_CHANGES)
   add_dependencies(test.exe build_test_mods)
@@ -85,6 +85,8 @@ endif()
 target_include_directories(test.exe PRIVATE "${mech_proto_dir}/..")
 
 if(NMC_WITH_CUDA)
+    # Omit -DDATADIR for cuda target because of CMake 3.7/3.8 FindCUDA quoting bug.
+
     set(TARGETS ${TARGETS} test_cuda.exe)
     cuda_add_executable(test_cuda.exe ${TEST_CUDA_SOURCES} ${HEADERS})
     target_link_libraries(test_cuda.exe LINK_PUBLIC gpu)
diff --git a/tests/unit/test_path.cpp b/tests/unit/test_path.cpp
index 54530f73..e48e4d20 100644
--- a/tests/unit/test_path.cpp
+++ b/tests/unit/test_path.cpp
@@ -11,7 +11,7 @@ using namespace nest::mc::util;
 
 TEST(path, posix_ctor) {
     // test constructor ans assignment overloads with sample character sequences.
-    posix::path p1;
+    posix_path p1;
     EXPECT_EQ("", p1.native());
     EXPECT_TRUE(p1.empty());
 
@@ -19,10 +19,10 @@ TEST(path, posix_ctor) {
     std::string str_cs(cs);
     std::vector<char> vec_cs(str_cs.begin(), str_cs.end());
 
-    posix::path p2(cs);
-    posix::path p3(str_cs);
-    posix::path p4(str_cs.begin(), str_cs.end());
-    posix::path p5(vec_cs.begin(), vec_cs.end());
+    posix_path p2(cs);
+    posix_path p3(str_cs);
+    posix_path p4(str_cs.begin(), str_cs.end());
+    posix_path p5(vec_cs.begin(), vec_cs.end());
 
     EXPECT_FALSE(p2.empty());
     EXPECT_EQ(str_cs, p2.native());
@@ -30,14 +30,14 @@ TEST(path, posix_ctor) {
     EXPECT_EQ(str_cs, p4.native());
     EXPECT_EQ(str_cs, p5.native());
 
-    posix::path p6(p2);
+    posix_path p6(p2);
     EXPECT_EQ(str_cs, p6.native());
 
-    posix::path p7(std::move(p6));
+    posix_path p7(std::move(p6));
     EXPECT_EQ(str_cs, p7.native());
 
     // test operator= overloads (and ref return values)
-    posix::path p;
+    posix_path p;
     EXPECT_EQ(str_cs, (p=p2).native());
     EXPECT_EQ(str_cs, (p=cs).native());
     EXPECT_EQ(str_cs, (p=str_cs).native());
@@ -57,18 +57,18 @@ TEST(path, posix_native) {
     std::string ps = "/abs/path";
     std::string qs = "rel/path.ext";
 
-    EXPECT_EQ(ps, posix::path{ps}.native());
-    EXPECT_EQ(qs, posix::path{qs}.native());
+    EXPECT_EQ(ps, posix_path{ps}.native());
+    EXPECT_EQ(qs, posix_path{qs}.native());
 
     // default string conversion
-    std::string ps_bis = posix::path{ps};
-    std::string qs_bis = posix::path{qs};
+    std::string ps_bis = posix_path{ps};
+    std::string qs_bis = posix_path{qs};
 
     EXPECT_EQ(ps, ps_bis);
     EXPECT_EQ(qs, qs_bis);
 
     // cstr
-    const char *c = posix::path{ps}.c_str();
+    const char *c = posix_path{ps}.c_str();
     EXPECT_TRUE(!std::strcmp(c, ps.c_str()));
 }
 
@@ -81,9 +81,9 @@ TEST(path, posix_generic) {
 }
 
 TEST(path, posix_append) {
-    posix::path p1{""}, q1{"rel"};
+    posix_path p1{""}, q1{"rel"};
 
-    posix::path p(p1);
+    posix_path p(p1);
     p.append(q1);
     EXPECT_EQ(p1/q1, p);
 
@@ -92,7 +92,7 @@ TEST(path, posix_append) {
     EXPECT_EQ(p1/q1, p);
     EXPECT_EQ("rel", p.native());
 
-    posix::path p2{"ab"}, q2{"rel"};
+    posix_path p2{"ab"}, q2{"rel"};
 
     p = p2;
     p.append(q2);
@@ -103,14 +103,14 @@ TEST(path, posix_append) {
     EXPECT_EQ(p2/q2, p);
     EXPECT_EQ("ab/rel", p.native());
 
-    EXPECT_EQ("foo/bar", (posix::path("foo/")/posix::path("/bar")).native());
-    EXPECT_EQ("foo/bar", (posix::path("foo")/posix::path("/bar")).native());
-    EXPECT_EQ("foo/bar", (posix::path("foo/")/posix::path("bar")).native());
-    EXPECT_EQ("/foo/bar/", (posix::path("/foo/")/posix::path("/bar/")).native());
+    EXPECT_EQ("foo/bar", (posix_path("foo/")/posix_path("/bar")).native());
+    EXPECT_EQ("foo/bar", (posix_path("foo")/posix_path("/bar")).native());
+    EXPECT_EQ("foo/bar", (posix_path("foo/")/posix_path("bar")).native());
+    EXPECT_EQ("/foo/bar/", (posix_path("/foo/")/posix_path("/bar/")).native());
 }
 
 TEST(path, compare) {
-    posix::path p1("/a/b"), p2("/a//b"), p3("/a/b/c/."), p4("/a/b/c/"), p5("a/bb/c"), p6("a/b/c/");
+    posix_path p1("/a/b"), p2("/a//b"), p3("/a/b/c/."), p4("/a/b/c/"), p5("a/bb/c"), p6("a/b/c/");
 
     EXPECT_EQ(p1, p2);
     EXPECT_LE(p1, p2);
@@ -135,9 +135,9 @@ TEST(path, compare) {
 }
 
 TEST(path, posix_concat) {
-    posix::path p1{""}, q1{"tail"};
+    posix_path p1{""}, q1{"tail"};
 
-    posix::path p(p1);
+    posix_path p(p1);
     p.concat(q1);
     EXPECT_EQ("tail", p.native());
 
@@ -145,7 +145,7 @@ TEST(path, posix_concat) {
     p += q1;
     EXPECT_EQ("tail", p.native());
 
-    posix::path p2{"ab"}, q2{"cd"};
+    posix_path p2{"ab"}, q2{"cd"};
 
     p = p2;
     p.concat(q2);
@@ -157,29 +157,29 @@ TEST(path, posix_concat) {
 }
 
 TEST(path, posix_absrel_query) {
-    posix::path p1("/abc/def");
+    posix_path p1("/abc/def");
     EXPECT_FALSE(p1.is_relative());
     EXPECT_TRUE(p1.is_absolute());
 
-    posix::path p2("abc/def");
+    posix_path p2("abc/def");
     EXPECT_TRUE(p2.is_relative());
     EXPECT_FALSE(p2.is_absolute());
 
-    posix::path p3("");
+    posix_path p3("");
     EXPECT_TRUE(p3.is_relative());
     EXPECT_FALSE(p3.is_absolute());
 
-    posix::path p4("/");
+    posix_path p4("/");
     EXPECT_FALSE(p4.is_relative());
     EXPECT_TRUE(p4.is_absolute());
 
-    posix::path p5("..");
+    posix_path p5("..");
     EXPECT_TRUE(p3.is_relative());
     EXPECT_FALSE(p3.is_absolute());
 }
 
 TEST(path, posix_swap) {
-    posix::path p1("foo"), p2("/bar");
+    posix_path p1("foo"), p2("/bar");
     p1.swap(p2);
 
     EXPECT_EQ("foo", p2.native());
@@ -193,7 +193,7 @@ TEST(path, posix_swap) {
 
 TEST(path, posix_iostream) {
     std::istringstream ss("/quux/xyzzy");
-    posix::path p;
+    posix_path p;
     ss >> p;
     EXPECT_EQ("/quux/xyzzy", p.native());
 
@@ -202,3 +202,126 @@ TEST(path, posix_iostream) {
     EXPECT_EQ("/quux/xyzzy", uu.str());
 }
 
+TEST(path, filesystem_error) {
+    auto io_error = std::make_error_code(std::errc::io_error);
+
+    filesystem_error err0("err0", io_error);
+    filesystem_error err1("err1", "/one", io_error);
+    filesystem_error err2("err2", "/one", "/two", io_error);
+
+    EXPECT_TRUE(dynamic_cast<std::system_error*>(&err0));
+    EXPECT_NE(std::string::npos, std::string(err0.what()).find("err0"));
+    EXPECT_EQ(path{}, err0.path1());
+    EXPECT_EQ(path{}, err0.path2());
+    EXPECT_EQ(io_error, err0.code());
+
+    EXPECT_NE(std::string::npos, std::string(err1.what()).find("err1"));
+    EXPECT_EQ(path("/one"), err1.path1());
+    EXPECT_EQ(path{}, err1.path2());
+    EXPECT_EQ(io_error, err1.code());
+
+    EXPECT_NE(std::string::npos, std::string(err2.what()).find("err2"));
+    EXPECT_EQ(path("/one"), err2.path1());
+    EXPECT_EQ(path("/two"), err2.path2());
+    EXPECT_EQ(io_error, err2.code());
+}
+
+TEST(path, posix_status) {
+    // Expect (POSIX) filesystem to have:
+    //      /            directory
+    //      .            directory
+    //      /dev/null    character special file
+
+    try {
+        file_status root = status("/");
+        file_status dot = status(".");
+        file_status dev_null = status("/dev/null");
+        // file and /none/ should not exist, but we don't expect an error
+        file_status nonesuch = status("/none/such");
+
+        EXPECT_EQ(file_type::directory, root.type());
+        EXPECT_EQ(file_type::directory, dot.type());
+        EXPECT_EQ(file_type::character, dev_null.type());
+        EXPECT_EQ(file_type::not_found, nonesuch.type());
+    }
+    catch (filesystem_error& e) {
+        FAIL() << "unexpected error with status(): " << e.what();
+    }
+
+    try {
+        file_status empty = status("");
+        (void)empty;
+        // should throw error wrapping ENOENT
+        FAIL() << "status() did not throw on empty string";
+    }
+    catch (filesystem_error& e) {
+        EXPECT_TRUE(e.code().default_error_condition()==std::errc::no_such_file_or_directory);
+    }
+
+    try {
+        std::string way_too_long(1<<20,'z');
+        file_status oops = status(way_too_long);
+        (void)oops;
+        // should throw error wrapping ENAMETOOLONG
+        FAIL() << "status() did not throw on stupendously long file name";
+    }
+    catch (filesystem_error& e) {
+        EXPECT_EQ(e.code().default_error_condition(), std::errc::filename_too_long);
+    }
+    try {
+        std::error_code ec;
+        file_status empty = status("", ec);
+        EXPECT_EQ(file_type::none, empty.type());
+        EXPECT_EQ(ec.default_error_condition(), std::errc::no_such_file_or_directory);
+    }
+    catch (filesystem_error& e) {
+        FAIL() << "status(path, ec) should not throw";
+    }
+}
+
+TEST(path, is_wrappers) {
+    EXPECT_TRUE(exists("/"));
+    EXPECT_TRUE(is_directory("/"));
+    EXPECT_FALSE(is_regular_file("/"));
+
+    EXPECT_TRUE(exists("/dev/null"));
+    EXPECT_TRUE(is_character_file("/dev/null"));
+    EXPECT_FALSE(is_regular_file("/"));
+
+    EXPECT_FALSE(exists("/none/such"));
+    EXPECT_FALSE(is_regular_file("/"));
+    EXPECT_TRUE(is_directory("/"));
+
+    EXPECT_THROW(exists(""), filesystem_error);
+}
+
+TEST(path, permissions) {
+    perms p = perms::owner_read | perms::owner_write | perms::owner_exec;
+    EXPECT_EQ(perms::owner_all, p);
+    EXPECT_EQ(perms::none, p & perms::group_all);
+    EXPECT_EQ(perms::none, p & perms::others_all);
+
+    p = perms::group_read | perms::group_write | perms::group_exec;
+    EXPECT_EQ(perms::group_all, p);
+    EXPECT_EQ(perms::none, p & perms::owner_all);
+    EXPECT_EQ(perms::none, p & perms::others_all);
+
+    p = perms::others_read | perms::others_write | perms::others_exec;
+    EXPECT_EQ(perms::others_all, p);
+    EXPECT_EQ(perms::none, p & perms::owner_all);
+    EXPECT_EQ(perms::none, p & perms::group_all);
+}
+
+TEST(path, posix_status_perms) {
+    // Expect /dev/null permissions to be 0666
+    perms null_perm = status("/dev/null").permissions();
+    perms expected = perms::owner_read|perms::owner_write|perms::group_read|perms::group_write|perms::others_read|perms::others_write;
+    EXPECT_EQ(expected, null_perm);
+
+    // Expect / to be have exec flag set for everyonr
+    perms root_perm = status("/").permissions();
+    EXPECT_NE(perms::none, root_perm&perms::owner_exec);
+    EXPECT_NE(perms::none, root_perm&perms::group_exec);
+    EXPECT_NE(perms::none, root_perm&perms::others_exec);
+}
+
diff --git a/tests/validation/CMakeLists.txt b/tests/validation/CMakeLists.txt
index cea12373..5523c3d2 100644
--- a/tests/validation/CMakeLists.txt
+++ b/tests/validation/CMakeLists.txt
@@ -30,7 +30,11 @@ set(VALIDATION_CUDA_SOURCES
 )
 
 if(NMC_VALIDATION_DATA_DIR)
-    add_definitions("-DDATADIR=\"${NMC_VALIDATION_DATA_DIR}\"")
+    if ("${CMAKE_VERSION}" MATCHES "^3.[78].")
+        message(WARNING "CMake ${CMAKE_VERSION} has broken FindCUDA; omitting NMC_DATADIR define.")
+    else()
+        add_definitions("-DNMC_DATADIR=\"${NMC_VALIDATION_DATA_DIR}\"")
+    endif()
 endif()
 
 add_executable(validate.exe ${VALIDATION_SOURCES})
diff --git a/tests/validation/validate.cpp b/tests/validation/validate.cpp
index e120ecc6..e186a64f 100644
--- a/tests/validation/validate.cpp
+++ b/tests/validation/validate.cpp
@@ -23,7 +23,13 @@ const char* usage_str =
 "                      compartments per segment\n"
 "  -d, --min-dt=DT     Run convergence tests with or with a minimumf\n"
 "                      timestep DT\n"
-"  -h, --help          Display usage information and exit\n";
+"  -h, --help          Display usage information and exit\n"
+"\n"
+"Validation data is searched by default in the directory specified by\n"
+"NMC_DATADIR at compile time. If NMC_DATADIR does not correspond to a\n"
+"directory, the tests will try the paths './validation/data' and\n"
+"'../validation/data'. This default path can be overridden with the\n"
+"NMC_DATADIR environment variable, or with the -p command-line option.\n";
 
 int main(int argc, char **argv) {
     using to::parse_opt;
diff --git a/tests/validation/validation_data.cpp b/tests/validation/validation_data.cpp
index 942ce8a7..7b44713b 100644
--- a/tests/validation/validation_data.cpp
+++ b/tests/validation/validation_data.cpp
@@ -1,4 +1,5 @@
 #include <algorithm>
+#include <cstdlib>
 #include <fstream>
 #include <stdexcept>
 #include <string>
@@ -15,6 +16,38 @@ namespace mc {
 
 trace_io g_trace_io;
 
+#ifndef NMC_DATADIR
+#define NMC_DATADIR ""
+#endif
+
+util::path trace_io::find_datadir() {
+    // If environment variable is set, use that in preference.
+
+    if (const char* env_path = std::getenv("NMC_DATADIR")) {
+        return util::path(env_path);
+    }
+
+    // Otherwise try compile-time path NMC_DATADIR and hard-coded
+    // relative paths below in turn, returning the first that
+    // corresponds to an existing directory.
+
+    const char* paths[] = {
+        NMC_DATADIR,
+        "./validation/data",
+        "../validation/data"
+    };
+
+    std::error_code ec;
+    for (auto p: paths) {
+        if (util::is_directory(p, ec)) {
+            return util::path(p);
+        }
+    }
+
+    // Otherwise set to empty path, and rely on command-line option.
+    return util::path();
+}
+
 void trace_io::save_trace(const std::string& label, const trace_data& data, const nlohmann::json& meta) {
     save_trace("time", label, data, meta);
 }
diff --git a/tests/validation/validation_data.hpp b/tests/validation/validation_data.hpp
index f6a4b821..a470ebf4 100644
--- a/tests/validation/validation_data.hpp
+++ b/tests/validation/validation_data.hpp
@@ -10,10 +10,6 @@
 #include <simple_sampler.hpp>
 #include <util/path.hpp>
 
-#ifndef DATADIR
-#define DATADIR "../data"
-#endif
-
 namespace nest {
 namespace mc {
 
@@ -29,6 +25,12 @@ namespace mc {
 
 class trace_io {
 public:
+    // Try to find the data directory on construction.
+
+    trace_io() {
+        datadir_ = find_datadir();
+    }
+
     void clear_traces() {
         jtraces_ = nlohmann::json::array();
     }
@@ -64,7 +66,7 @@ public:
         }
     }
 
-    // write traces on exit
+    // Write traces on exit.
 
     ~trace_io() {
         if (out_) {
@@ -73,12 +75,19 @@ public:
     }
 
 private:
-    util::path datadir_ = DATADIR;
+    util::path datadir_;
     std::ofstream out_;
     nlohmann::json jtraces_ = nlohmann::json::array();
     bool verbose_flag_ = false;
     int max_ncomp_ = 100;
     float min_dt_ = 0.001f;
+
+    // Returns value of NMC_DATADIR environment variable if set,
+    // otherwise make a 'best-effort' search for the data directory,
+    // starting with NMC_DATADIR preprocessor define if defined and
+    // if the directory exists, or else try './validation/data'
+    // and '../validation/data'.
+    static util::path find_datadir();
 };
 
 extern trace_io g_trace_io;
-- 
GitLab