gtest: Add a testcase wrapper for handling tmpdirs

This makes it trivial to have a pristine temp directory on every test
case execution. Just derive from the provided class.

Change-Id: I5355914cdedc482eddd0749a9ccc10fc93de6571
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/include-gtest/meson.build b/include-gtest/meson.build
new file mode 100644
index 0000000..dfba348
--- /dev/null
+++ b/include-gtest/meson.build
@@ -0,0 +1,5 @@
+stdplus_gtest_headers = include_directories('.')
+
+install_headers(
+  'stdplus/gtest/tmp.hpp',
+  subdir: 'stdplus')
diff --git a/include-gtest/stdplus/gtest/tmp.hpp b/include-gtest/stdplus/gtest/tmp.hpp
new file mode 100644
index 0000000..9f88084
--- /dev/null
+++ b/include-gtest/stdplus/gtest/tmp.hpp
@@ -0,0 +1,35 @@
+#include <string>
+
+#include <gtest/gtest.h>
+
+namespace stdplus
+{
+namespace gtest
+{
+
+/**
+ * @brief Provides googletest users with automatic temp directory
+ *        handling per test case. You will still want to wrap your
+ *        unit test execution in a `TMPDIR` RAII script in case the
+ *        unit test binary ends up crashing.
+ */
+class TestWithTmp : public ::testing::Test
+{
+  protected:
+    TestWithTmp();
+    ~TestWithTmp();
+    static void SetUpTestSuite();
+    static void TearDownTestSuite();
+
+    static std::string SuiteTmpDir();
+    inline const std::string& CaseTmpDir() const
+    {
+        return casedir;
+    }
+
+  private:
+    std::string casedir;
+};
+
+} // namespace gtest
+} // namespace stdplus
diff --git a/meson.build b/meson.build
index 8659147..d7dad2c 100644
--- a/meson.build
+++ b/meson.build
@@ -38,6 +38,43 @@
   error('io_uring support is required')
 endif
 
+build_tests = get_option('tests')
+has_gtest = false
+if not build_tests.disabled() or not get_option('gtest').disabled()
+  gtest_dep = dependency('gtest', required: false)
+  gtest_main_dep = dependency('gtest', main: true, required: false)
+  gmock_dep = dependency('gmock', required: false)
+  if not gtest_dep.found() or not gmock_dep.found()
+    gtest_opts = import('cmake').subproject_options()
+    gtest_opts.add_cmake_defines({
+      'BUILD_SHARED_LIBS': 'ON',
+      'CMAKE_CXX_FLAGS': '-Wno-pedantic',
+    })
+    gtest_proj = import('cmake').subproject(
+      'googletest',
+      options: gtest_opts,
+      required: false)
+    if gtest_proj.found()
+      gtest_dep = declare_dependency(
+        dependencies: [
+          dependency('threads'),
+          gtest_proj.dependency('gtest'),
+        ])
+      gtest_main_dep = declare_dependency(
+        dependencies: [
+          gtest_dep,
+          gtest_proj.dependency('gtest_main'),
+        ])
+      gmock_dep = gtest_proj.dependency('gmock')
+    else
+      assert(not build_tests.enabled() and not get_option('gtest').enabled(), 'Googletest is required')
+    endif
+  endif
+  if not get_option('gtest').disabled() and gtest_dep.found()
+    has_gtest = true
+  endif
+endif
+
 subdir('include')
 if has_dl
   subdir('include-dl')
@@ -48,10 +85,12 @@
 if has_io_uring
   subdir('include-uring')
 endif
+if has_gtest
+  subdir('include-gtest')
+endif
 
 subdir('src')
 
-build_tests = get_option('tests')
 build_examples = get_option('examples')
 
 if build_examples
diff --git a/meson_options.txt b/meson_options.txt
index 37c428a..d5ac248 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,5 +1,6 @@
 option('dl', type: 'feature', description: 'libdl wrapper support')
 option('fd', type: 'feature', description: 'Managed file descriptor support')
 option('io_uring', type: 'feature', description: 'io_uring wrapper support')
+option('gtest', type: 'feature', description: 'Build googletest support library')
 option('tests', type: 'feature', description: 'Build tests')
 option('examples', type: 'boolean', value: true, description: 'Build examples')
diff --git a/src/gtest/tmp.cpp b/src/gtest/tmp.cpp
new file mode 100644
index 0000000..07dfb6d
--- /dev/null
+++ b/src/gtest/tmp.cpp
@@ -0,0 +1,48 @@
+#include <fmt/format.h>
+
+#include <filesystem>
+#include <stdplus/gtest/tmp.hpp>
+
+namespace stdplus
+{
+namespace gtest
+{
+
+TestWithTmp::TestWithTmp() :
+    casedir(fmt::format(
+        "{}/{}", SuiteTmpDir(),
+        ::testing::UnitTest::GetInstance()->current_test_info()->name()))
+{
+    std::filesystem::create_directory(CaseTmpDir());
+}
+
+TestWithTmp::~TestWithTmp()
+{
+    std::filesystem::remove_all(CaseTmpDir());
+}
+
+void TestWithTmp::SetUpTestSuite()
+{
+    std::filesystem::create_directory(SuiteTmpDir());
+}
+
+void TestWithTmp::TearDownTestSuite()
+{
+    std::filesystem::remove_all(SuiteTmpDir());
+}
+
+std::string TestWithTmp::SuiteTmpDir()
+{
+    const char* dir = getenv("TMPDIR");
+    if (dir == nullptr)
+    {
+        dir = "/tmp";
+    }
+    return fmt::format(
+        "{}/{}-{}", dir,
+        ::testing::UnitTest::GetInstance()->current_test_suite()->name(),
+        getpid());
+}
+
+} // namespace gtest
+} // namespace stdplus
diff --git a/src/meson.build b/src/meson.build
index 3f1021e..6ff400d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -112,3 +112,39 @@
     version: meson.project_version(),
     requires: stdplus_reqs)
 endif
+
+if has_gtest
+  stdplus_gtest_deps = [
+    fmt_dep,
+    gtest_dep,
+  ]
+
+  stdplus_gtest_pre = declare_dependency(
+    include_directories: stdplus_gtest_headers,
+    dependencies: stdplus_gtest_deps)
+
+  stdplus_gtest_lib = library(
+    'stdplus-gtest',
+    'gtest/tmp.cpp',
+    dependencies: stdplus_gtest_pre,
+    implicit_include_directories: false,
+    version: meson.project_version(),
+    install: true)
+
+  stdplus_gtest_dep = declare_dependency(
+    dependencies: stdplus_gtest_pre,
+    link_with: stdplus_gtest_lib)
+
+  stdplus_gtest_reqs = []
+  foreach dep : stdplus_gtest_deps
+    if dep.type_name() == 'pkgconfig'
+      stdplus_gtest_reqs += dep
+    endif
+  endforeach
+
+  import('pkgconfig').generate(
+    stdplus_gtest_lib,
+    description: 'C++ helper utilities',
+    version: meson.project_version(),
+    requires: stdplus_gtest_reqs)
+endif
diff --git a/test/gtest/tmp.cpp b/test/gtest/tmp.cpp
new file mode 100644
index 0000000..f473175
--- /dev/null
+++ b/test/gtest/tmp.cpp
@@ -0,0 +1,43 @@
+#include <filesystem>
+#include <gtest/gtest.h>
+#include <stdplus/gtest/tmp.hpp>
+
+namespace stdplus
+{
+namespace gtest
+{
+
+class TestWithTmpTest : public TestWithTmp
+{
+};
+
+TEST_F(TestWithTmpTest, One)
+{
+    EXPECT_TRUE(std::filesystem::create_directory(
+        std::filesystem::path(CaseTmpDir()) / "a"));
+    EXPECT_TRUE(std::filesystem::create_directory(
+        std::filesystem::path(SuiteTmpDir()) / "a"));
+}
+
+TEST_F(TestWithTmpTest, Two)
+{
+    EXPECT_TRUE(std::filesystem::create_directory(
+        std::filesystem::path(CaseTmpDir()) / "a"));
+    EXPECT_FALSE(std::filesystem::create_directory(
+        std::filesystem::path(SuiteTmpDir()) / "a"));
+}
+
+class TestWithTmpTest2 : public TestWithTmp
+{
+};
+
+TEST_F(TestWithTmpTest2, One)
+{
+    EXPECT_TRUE(std::filesystem::create_directory(
+        std::filesystem::path(SuiteTmpDir()) / "a"));
+    EXPECT_TRUE(std::filesystem::create_directory(
+        std::filesystem::path(CaseTmpDir()) / "a"));
+}
+
+} // namespace gtest
+} // namespace stdplus
diff --git a/test/meson.build b/test/meson.build
index 0317121..1f284a5 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -1,25 +1,3 @@
-gtest = dependency('gtest', main: true, disabler: true, required: false)
-gmock = dependency('gmock', disabler: true, required: false)
-if not gtest.found() or not gmock.found()
-  gtest_opts = import('cmake').subproject_options()
-  gtest_opts.add_cmake_defines({'CMAKE_CXX_FLAGS': '-Wno-pedantic'})
-  gtest_proj = import('cmake').subproject(
-    'googletest',
-    options: gtest_opts,
-    required: false)
-  if gtest_proj.found()
-    gtest = declare_dependency(
-      dependencies: [
-        dependency('threads'),
-        gtest_proj.dependency('gtest'),
-        gtest_proj.dependency('gtest_main'),
-      ])
-    gmock = gtest_proj.dependency('gmock')
-  else
-    assert(not build_tests.enabled(), 'Googletest is required')
-  endif
-endif
-
 gtests = [
   'cancel',
   'exception',
@@ -31,8 +9,8 @@
 
 gtest_deps = [
   stdplus_dep,
-  gtest,
-  gmock,
+  gtest_main_dep,
+  gmock_dep,
 ]
 
 if has_dl
@@ -83,12 +61,31 @@
   warning('Not testing io_uring feature')
 endif
 
-if gtest.found() and gmock.found()
+if has_gtest
+  gtests += [
+    'gtest/tmp',
+  ]
+
+  gtest_deps += [
+    stdplus_gtest_dep,
+  ]
+elif build_tests.enabled()
+  error('Not testing gtest lib feature')
+else
+  warning('Not testing gtest lib feature')
+endif
+
+if gtest_dep.found() and gmock_dep.found()
   foreach t : gtests
-    test(t, executable(t.underscorify(), t + '.cpp',
-                       build_by_default: false,
-                       implicit_include_directories: false,
-                       dependencies: gtest_deps))
+    test(
+      t,
+      files('run_with_tmp.sh'),
+      env: {'TMPTMPL': 'stdplus-test.XXXXXXXXXX'},
+      args: executable(
+        t.underscorify(), t + '.cpp',
+        build_by_default: false,
+        implicit_include_directories: false,
+        dependencies: gtest_deps))
   endforeach
 endif
 
diff --git a/test/run_with_tmp.sh b/test/run_with_tmp.sh
new file mode 100755
index 0000000..485d6a3
--- /dev/null
+++ b/test/run_with_tmp.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+TMPDIR="$(mktemp -d --tmpdir "${TMPTMPL-tmp.XXXXXXXXXX}")" || exit
+trap 'rm -rf -- "$TMPDIR"' EXIT
+export TMPDIR
+echo "Exec $* with TMPDIR=$TMPDIR" >&2
+"$@"