/*
- * Copyright 2012 Facebook, Inc.
+ * Copyright 2012-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* limitations under the License.
*/
-#ifndef FOLLY_TESTUTIL_H_
-#define FOLLY_TESTUTIL_H_
+#pragma once
+#include <map>
#include <string>
+#include <folly/Range.h>
+#include <folly/ScopeGuard.h>
+#include <folly/experimental/io/FsUtil.h>
+
namespace folly {
namespace test {
*
* By default, the file is created in a system-specific location (the value
* of the TMPDIR environment variable, or /tmp), but you can override that
- * by making "prefix" be a path (containing a '/'; use a prefix starting with
- * './' to create a file in the current directory).
+ * with a different (non-empty) directory passed to the constructor.
*
* By default, the file is closed and deleted when the TemporaryFile object
* is destroyed, but both these behaviors can be overridden with arguments
UNLINK_IMMEDIATELY,
UNLINK_ON_DESTRUCTION
};
- explicit TemporaryFile(const char* prefix=nullptr,
- Scope scope=Scope::UNLINK_ON_DESTRUCTION,
- bool closeOnDestruction=true);
+ explicit TemporaryFile(StringPiece namePrefix = StringPiece(),
+ fs::path dir = fs::path(),
+ Scope scope = Scope::UNLINK_ON_DESTRUCTION,
+ bool closeOnDestruction = true);
~TemporaryFile();
+ // Movable, but not copyable
+ TemporaryFile(TemporaryFile&& other) noexcept {
+ assign(other);
+ }
+
+ TemporaryFile& operator=(TemporaryFile&& other) {
+ if (this != &other) {
+ reset();
+ assign(other);
+ }
+ return *this;
+ }
+
+ void close();
int fd() const { return fd_; }
- const std::string& path() const;
+ const fs::path& path() const;
+ void reset();
private:
Scope scope_;
bool closeOnDestruction_;
int fd_;
- std::string path_;
+ fs::path path_;
+
+ void assign(TemporaryFile& other) {
+ scope_ = other.scope_;
+ closeOnDestruction_ = other.closeOnDestruction_;
+ fd_ = std::exchange(other.fd_, -1);
+ path_ = other.path_;
+ }
+};
+
+/**
+ * Temporary directory.
+ *
+ * By default, the temporary directory is created in a system-specific
+ * location (the value of the TMPDIR environment variable, or /tmp), but you
+ * can override that with a non-empty directory passed to the constructor.
+ *
+ * By default, the directory is recursively deleted when the TemporaryDirectory
+ * object is destroyed, but that can be overridden with an argument
+ * to the constructor.
+ */
+
+class TemporaryDirectory {
+ public:
+ enum class Scope {
+ PERMANENT,
+ DELETE_ON_DESTRUCTION
+ };
+ explicit TemporaryDirectory(StringPiece namePrefix = StringPiece(),
+ fs::path dir = fs::path(),
+ Scope scope = Scope::DELETE_ON_DESTRUCTION);
+ ~TemporaryDirectory();
+
+ // Movable, but not copiable
+ TemporaryDirectory(TemporaryDirectory&&) = default;
+ TemporaryDirectory& operator=(TemporaryDirectory&&) = default;
+
+ const fs::path& path() const {
+ return *path_;
+ }
+
+ private:
+ Scope scope_;
+ std::unique_ptr<fs::path> path_;
+};
+
+/**
+ * Changes into a temporary directory, and deletes it with all its contents
+ * upon destruction, also changing back to the original working directory.
+ */
+class ChangeToTempDir {
+ public:
+ ChangeToTempDir();
+ ~ChangeToTempDir();
+
+ // Movable, but not copiable
+ ChangeToTempDir(ChangeToTempDir&&) = default;
+ ChangeToTempDir& operator=(ChangeToTempDir&&) = default;
+
+ const fs::path& path() const { return dir_.path(); }
+
+ private:
+ TemporaryDirectory dir_;
+ fs::path orig_;
+};
+
+namespace detail {
+struct SavedState {
+ void* previousThreadLocalHandler;
+ int previousCrtReportMode;
};
+SavedState disableInvalidParameters();
+void enableInvalidParameters(SavedState state);
+} // namespace detail
+
+// Ok, so fun fact: The CRT on windows will actually abort
+// on certain failed parameter validation checks in debug
+// mode rather than simply returning -1 as it does in release
+// mode. We can however, ensure consistent behavior by
+// registering our own thread-local invalid parameter handler
+// for the duration of the call, and just have that handler
+// immediately return. We also have to disable CRT asertion
+// alerts for the duration of the call, otherwise we get
+// the abort-retry-ignore window.
+template <typename Func>
+auto msvcSuppressAbortOnInvalidParams(Func func) -> decltype(func()) {
+ auto savedState = detail::disableInvalidParameters();
+ SCOPE_EXIT {
+ detail::enableInvalidParameters(savedState);
+ };
+ return func();
+}
+
+/**
+ * Easy PCRE regex matching. Note that pattern must match the ENTIRE target,
+ * so use .* at the start and end of the pattern, as appropriate. See
+ * http://regex101.com/ for a PCRE simulator.
+ */
+#define EXPECT_PCRE_MATCH(pattern_stringpiece, target_stringpiece) \
+ EXPECT_PRED2( \
+ ::folly::test::detail::hasPCREPatternMatch, \
+ pattern_stringpiece, \
+ target_stringpiece \
+ )
+#define EXPECT_NO_PCRE_MATCH(pattern_stringpiece, target_stringpiece) \
+ EXPECT_PRED2( \
+ ::folly::test::detail::hasNoPCREPatternMatch, \
+ pattern_stringpiece, \
+ target_stringpiece \
+ )
+
+namespace detail {
+bool hasPCREPatternMatch(StringPiece pattern, StringPiece target);
+bool hasNoPCREPatternMatch(StringPiece pattern, StringPiece target);
+} // namespace detail
+
+/**
+ * Use these patterns together with CaptureFD and EXPECT_PCRE_MATCH() to
+ * test for the presence (or absence) of log lines at a particular level:
+ *
+ * CaptureFD stderr(2);
+ * LOG(INFO) << "All is well";
+ * EXPECT_NO_PCRE_MATCH(glogErrOrWarnPattern(), stderr.readIncremental());
+ * LOG(ERROR) << "Uh-oh";
+ * EXPECT_PCRE_MATCH(glogErrorPattern(), stderr.readIncremental());
+ */
+inline std::string glogErrorPattern() { return ".*(^|\n)E[0-9].*"; }
+inline std::string glogWarningPattern() { return ".*(^|\n)W[0-9].*"; }
+// Error OR warning
+inline std::string glogErrOrWarnPattern() { return ".*(^|\n)[EW][0-9].*"; }
-} // namespace test
-} // namespace folly
+/**
+ * Temporarily capture a file descriptor by redirecting it into a file.
+ * You can consume its entire output thus far via read(), incrementally
+ * via readIncremental(), or via callback using chunk_cob.
+ * Great for testing logging (see also glog*Pattern()).
+ */
+class CaptureFD {
+ private:
+ struct NoOpChunkCob { void operator()(StringPiece) {} };
+
+ public:
+ using ChunkCob = std::function<void(folly::StringPiece)>;
+
+ /**
+ * chunk_cob is is guaranteed to consume all the captured output. It is
+ * invoked on each readIncremental(), and also on FD release to capture
+ * as-yet unread lines. Chunks can be empty.
+ */
+ explicit CaptureFD(int fd, ChunkCob chunk_cob = NoOpChunkCob());
+ ~CaptureFD();
+
+ /**
+ * Restore the captured FD to its original state. It can be useful to do
+ * this before the destructor so that you can read() the captured data and
+ * log about it to the formerly captured stderr or stdout.
+ */
+ void release();
-#endif /* FOLLY_TESTUTIL_H_ */
+ /**
+ * Reads the whole file into a string, but does not remove the redirect.
+ */
+ std::string read() const;
+
+ /**
+ * Read any bytes that were appended to the file since the last
+ * readIncremental. Great for testing line-by-line output.
+ */
+ std::string readIncremental();
+
+ private:
+ ChunkCob chunkCob_;
+ TemporaryFile file_;
+
+ int fd_;
+ int oldFDCopy_; // equal to fd_ after restore()
+
+ off_t readOffset_; // for incremental reading
+};
+} // namespace test
+} // namespace folly