Implement EXPECT_STDOUT and EXPECT_STDERR using pipes.
This commit is contained in:
parent
dd0120c189
commit
1a2d7be3f5
@ -69,22 +69,24 @@ endif ()
|
||||
|
||||
include(CheckSymbolExists)
|
||||
if (WIN32)
|
||||
check_symbol_exists(dup io.h HAVE_DUP)
|
||||
check_symbol_exists(open io.h HAVE_OPEN)
|
||||
else ()
|
||||
check_symbol_exists(dup unistd.h HAVE_DUP)
|
||||
check_symbol_exists(open fcntl.h HAVE_OPEN)
|
||||
endif ()
|
||||
if (HAVE_DUP)
|
||||
add_definitions(-DFMT_USE_DUP=1)
|
||||
if (HAVE_OPEN)
|
||||
add_definitions(-DFMT_USE_FILE_DESCRIPTORS=1)
|
||||
endif ()
|
||||
|
||||
enable_testing()
|
||||
|
||||
include_directories(.)
|
||||
|
||||
add_library(test-main test/test-main.cc)
|
||||
add_library(test-main
|
||||
test/test-main.cc test/gtest-extra.cc test/gtest-extra.h)
|
||||
target_link_libraries(test-main gtest format)
|
||||
|
||||
cxx_test(assert-test "gtest;test-main")
|
||||
cxx_test(format-test "format;gtest;test-main")
|
||||
cxx_test(gtest-extra-test test-main)
|
||||
cxx_test(format-test test-main)
|
||||
if (CMAKE_COMPILER_IS_GNUCXX)
|
||||
set_target_properties(format-test PROPERTIES COMPILE_FLAGS
|
||||
"-Wall -Wextra -pedantic -Wno-long-long -Wno-variadic-macros")
|
||||
|
28
format.cc
28
format.cc
@ -1,5 +1,5 @@
|
||||
/*
|
||||
String formatting library for C++
|
||||
Formatting library for C++
|
||||
|
||||
Copyright (c) 2012, Victor Zverovich
|
||||
All rights reserved.
|
||||
@ -101,7 +101,19 @@ inline int FMT_SNPRINTF(char *buffer, size_t size, const char *format, ...) {
|
||||
#endif // _MSC_VER
|
||||
|
||||
const char RESET_COLOR[] = "\x1b[0m";
|
||||
|
||||
typedef void (*FormatFunc)(fmt::Writer &, int , fmt::StringRef);
|
||||
|
||||
void ReportError(FormatFunc func,
|
||||
int error_code, fmt::StringRef message) FMT_NOEXCEPT(true) {
|
||||
try {
|
||||
fmt::Writer full_message;
|
||||
func(full_message, error_code, message); // TODO: this may throw?
|
||||
std::fwrite(full_message.c_str(), full_message.size(), 1, stderr);
|
||||
std::fputc('\n', stderr);
|
||||
} catch (...) {}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
template <typename T>
|
||||
int fmt::internal::CharTraits<char>::FormatFloat(
|
||||
@ -206,7 +218,7 @@ int fmt::internal::UTF16ToUTF8::Convert(fmt::WStringRef s) {
|
||||
#endif
|
||||
|
||||
int fmt::internal::StrError(
|
||||
int error_code, char *&buffer, std::size_t buffer_size) {
|
||||
int error_code, char *&buffer, std::size_t buffer_size) FMT_NOEXCEPT(true) {
|
||||
assert(buffer != 0 && buffer_size != 0);
|
||||
int result = 0;
|
||||
#ifdef _GNU_SOURCE
|
||||
@ -809,12 +821,24 @@ void fmt::SystemErrorSink::operator()(const fmt::Writer &w) const {
|
||||
throw SystemError(message.c_str(), error_code_);
|
||||
}
|
||||
|
||||
void fmt::ReportSystemError(
|
||||
int error_code, fmt::StringRef message) FMT_NOEXCEPT(true) {
|
||||
// FIXME: FormatSystemErrorMessage may throw
|
||||
ReportError(internal::FormatSystemErrorMessage, error_code, message);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
void fmt::WinErrorSink::operator()(const Writer &w) const {
|
||||
Writer message;
|
||||
internal::FormatWinErrorMessage(message, error_code_, w.c_str());
|
||||
throw SystemError(message.c_str(), error_code_);
|
||||
}
|
||||
|
||||
void fmt::ReportWinError(
|
||||
int error_code, fmt::StringRef message) FMT_NOEXCEPT(true) {
|
||||
// FIXME: FormatWinErrorMessage may throw
|
||||
ReportError(internal::FormatWinErrorMessage, error_code, message);
|
||||
}
|
||||
#endif
|
||||
|
||||
void fmt::ANSITerminalSink::operator()(
|
||||
|
17
format.h
17
format.h
@ -1,5 +1,5 @@
|
||||
/*
|
||||
String formatting library for C++
|
||||
Formatting library for C++
|
||||
|
||||
Copyright (c) 2012, Victor Zverovich
|
||||
All rights reserved.
|
||||
@ -498,7 +498,8 @@ class UTF16ToUTF8 {
|
||||
// ERANGE - buffer is not large enough to store the error message
|
||||
// other - failure
|
||||
// Buffer should be at least of size 1.
|
||||
int StrError(int error_code, char *&buffer, std::size_t buffer_size);
|
||||
int StrError(int error_code,
|
||||
char *&buffer, std::size_t buffer_size) FMT_NOEXCEPT(true);
|
||||
|
||||
void FormatSystemErrorMessage(
|
||||
fmt::Writer &out, int error_code, fmt::StringRef message);
|
||||
@ -1563,6 +1564,12 @@ inline Formatter<SystemErrorSink> ThrowSystemError(
|
||||
return f;
|
||||
}
|
||||
|
||||
// Reports a system error without throwing an exception.
|
||||
// Can be used to report errors from destructors.
|
||||
void ReportSystemError(int error_code, StringRef message) FMT_NOEXCEPT(true);
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
/**
|
||||
A sink that gets the error message corresponding to a Windows error code
|
||||
as given by GetLastError and throws SystemError.
|
||||
@ -1588,6 +1595,12 @@ inline Formatter<WinErrorSink> ThrowWinError(int error_code, StringRef format) {
|
||||
return f;
|
||||
}
|
||||
|
||||
// Reports a Windows error without throwing an exception.
|
||||
// Can be used to report errors from destructors.
|
||||
void ReportWinError(int error_code, StringRef message) FMT_NOEXCEPT(true);
|
||||
|
||||
#endif
|
||||
|
||||
/** A sink that writes output to a file. */
|
||||
class FileSink {
|
||||
private:
|
||||
|
@ -1,130 +0,0 @@
|
||||
/*
|
||||
Tests of custom Google Test assertions.
|
||||
|
||||
Copyright (c) 2012-2014, Victor Zverovich
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "gtest-extra.h"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <gtest/gtest-spi.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Tests that assertion macros evaluate their arguments exactly once.
|
||||
class SingleEvaluationTest : public ::testing::Test {
|
||||
protected:
|
||||
SingleEvaluationTest() {
|
||||
a_ = 0;
|
||||
}
|
||||
|
||||
static int a_;
|
||||
};
|
||||
|
||||
int SingleEvaluationTest::a_;
|
||||
|
||||
void ThrowNothing() {}
|
||||
|
||||
void ThrowException() {
|
||||
throw std::runtime_error("test");
|
||||
}
|
||||
|
||||
// Tests that assertion arguments are evaluated exactly once.
|
||||
TEST_F(SingleEvaluationTest, ExceptionTests) {
|
||||
// successful EXPECT_THROW_MSG
|
||||
EXPECT_THROW_MSG({ // NOLINT
|
||||
a_++;
|
||||
ThrowException();
|
||||
}, std::exception, "test");
|
||||
EXPECT_EQ(1, a_);
|
||||
|
||||
// failed EXPECT_THROW_MSG, throws different type
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG({ // NOLINT
|
||||
a_++;
|
||||
ThrowException();
|
||||
}, std::logic_error, "test"), "throws a different type");
|
||||
EXPECT_EQ(2, a_);
|
||||
|
||||
// failed EXPECT_THROW_MSG, throws an exception with different message
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG({ // NOLINT
|
||||
a_++;
|
||||
ThrowException();
|
||||
}, std::exception, "other"), "throws an exception with a different message");
|
||||
EXPECT_EQ(3, a_);
|
||||
|
||||
// failed EXPECT_THROW_MSG, throws nothing
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(a_++, std::exception, "test"), "throws nothing");
|
||||
EXPECT_EQ(4, a_);
|
||||
}
|
||||
|
||||
// Tests that the compiler will not complain about unreachable code in the
|
||||
// EXPECT_THROW_MSG macro.
|
||||
TEST(ExpectThrowTest, DoesNotGenerateUnreachableCodeWarning) {
|
||||
int n = 0;
|
||||
using std::runtime_error;
|
||||
EXPECT_THROW_MSG(throw runtime_error(""), runtime_error, "");
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG(n++, runtime_error, ""), "");
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG(throw 1, runtime_error, ""), "");
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG(
|
||||
throw runtime_error("a"), runtime_error, "b"), "");
|
||||
}
|
||||
|
||||
TEST(AssertionSyntaxTest, ExceptionAssertionsBehavesLikeSingleStatement) {
|
||||
if (::testing::internal::AlwaysFalse())
|
||||
EXPECT_THROW_MSG(ThrowNothing(), std::exception, "");
|
||||
|
||||
if (::testing::internal::AlwaysTrue())
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "test");
|
||||
else
|
||||
; // NOLINT
|
||||
}
|
||||
|
||||
// Tests EXPECT_THROW_MSG.
|
||||
TEST(ExpectTest, EXPECT_THROW_MSG) {
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "test");
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowException(), std::logic_error, "test"),
|
||||
"Expected: ThrowException() throws an exception of "
|
||||
"type std::logic_error.\n Actual: it throws a different type.");
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowNothing(), std::exception, "test"),
|
||||
"Expected: ThrowNothing() throws an exception of type std::exception.\n"
|
||||
" Actual: it throws nothing.");
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "other"),
|
||||
"ThrowException() throws an exception with a different message.\n"
|
||||
"Expected: other\n"
|
||||
" Actual: test");
|
||||
}
|
||||
|
||||
TEST(StreamingAssertionsTest, ThrowMsg) {
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "test")
|
||||
<< "unexpected failure";
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "other")
|
||||
<< "expected failure", "expected failure");
|
||||
}
|
||||
|
||||
} // namespace
|
@ -40,37 +40,6 @@
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
#if FMT_USE_DUP
|
||||
|
||||
# include <sys/types.h>
|
||||
# include <sys/stat.h>
|
||||
# include <fcntl.h>
|
||||
|
||||
# ifdef _WIN32
|
||||
|
||||
# include <io.h>
|
||||
|
||||
# define O_WRONLY _O_WRONLY
|
||||
# define O_CREAT _O_CREAT
|
||||
# define O_TRUNC _O_TRUNC
|
||||
# define S_IRUSR _S_IREAD
|
||||
# define S_IWUSR _S_IWRITE
|
||||
# define close _close
|
||||
# define dup _dup
|
||||
# define dup2 _dup2
|
||||
|
||||
namespace {
|
||||
int open(const char *path, int oflag, int pmode) {
|
||||
int fd = -1;
|
||||
_sopen_s(&fd, path, oflag, _SH_DENYNO, pmode);
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
# else
|
||||
# include <unistd.h>
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#include "format.h"
|
||||
#include "gtest-extra.h"
|
||||
|
||||
@ -1830,25 +1799,12 @@ TEST(FormatIntTest, FormatDec) {
|
||||
EXPECT_EQ("42", FormatDec(42ull));
|
||||
}
|
||||
|
||||
#ifdef FMT_USE_DUP
|
||||
|
||||
// TODO: implement EXPECT_PRINT
|
||||
#ifdef FMT_USE_FILE_DESCRIPTORS
|
||||
|
||||
TEST(FormatTest, PrintColored) {
|
||||
// Temporarily redirect stdout to a file and check if PrintColored adds
|
||||
// necessary ANSI escape sequences.
|
||||
std::fflush(stdout);
|
||||
int saved_stdio = dup(1);
|
||||
EXPECT_NE(-1, saved_stdio);
|
||||
int out = open("out", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
|
||||
EXPECT_NE(-1, out);
|
||||
EXPECT_NE(-1, dup2(out, 1));
|
||||
close(out);
|
||||
fmt::PrintColored(fmt::RED, "Hello, {}!\n") << "world";
|
||||
std::fflush(stdout);
|
||||
EXPECT_NE(-1, dup2(saved_stdio, 1));
|
||||
close(saved_stdio);
|
||||
EXPECT_EQ("\x1b[31mHello, world!\n\x1b[0m", ReadFile("out"));
|
||||
EXPECT_STDOUT(
|
||||
fmt::PrintColored(fmt::RED, "Hello, {}!\n") << "world",
|
||||
"\x1b[31mHello, world!\n\x1b[0m");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
313
test/gtest-extra-test.cc
Normal file
313
test/gtest-extra-test.cc
Normal file
@ -0,0 +1,313 @@
|
||||
/*
|
||||
Tests of custom Google Test assertions.
|
||||
|
||||
Copyright (c) 2012-2014, Victor Zverovich
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "gtest-extra.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <gtest/gtest-spi.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Tests that assertion macros evaluate their arguments exactly once.
|
||||
class SingleEvaluationTest : public ::testing::Test {
|
||||
protected:
|
||||
SingleEvaluationTest() {
|
||||
a_ = 0;
|
||||
}
|
||||
|
||||
static int a_;
|
||||
};
|
||||
|
||||
int SingleEvaluationTest::a_;
|
||||
|
||||
void ThrowNothing() {}
|
||||
|
||||
void ThrowException() {
|
||||
throw std::runtime_error("test");
|
||||
}
|
||||
|
||||
// Tests that assertion arguments are evaluated exactly once.
|
||||
TEST_F(SingleEvaluationTest, ExceptionTests) {
|
||||
// successful EXPECT_THROW_MSG
|
||||
EXPECT_THROW_MSG({ // NOLINT
|
||||
a_++;
|
||||
ThrowException();
|
||||
}, std::exception, "test");
|
||||
EXPECT_EQ(1, a_);
|
||||
|
||||
// failed EXPECT_THROW_MSG, throws different type
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG({ // NOLINT
|
||||
a_++;
|
||||
ThrowException();
|
||||
}, std::logic_error, "test"), "throws a different type");
|
||||
EXPECT_EQ(2, a_);
|
||||
|
||||
// failed EXPECT_THROW_MSG, throws an exception with different message
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG({ // NOLINT
|
||||
a_++;
|
||||
ThrowException();
|
||||
}, std::exception, "other"), "throws an exception with a different message");
|
||||
EXPECT_EQ(3, a_);
|
||||
|
||||
// failed EXPECT_THROW_MSG, throws nothing
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(a_++, std::exception, "test"), "throws nothing");
|
||||
EXPECT_EQ(4, a_);
|
||||
}
|
||||
|
||||
// Tests that the compiler will not complain about unreachable code in the
|
||||
// EXPECT_THROW_MSG macro.
|
||||
TEST(ExpectThrowTest, DoesNotGenerateUnreachableCodeWarning) {
|
||||
int n = 0;
|
||||
using std::runtime_error;
|
||||
EXPECT_THROW_MSG(throw runtime_error(""), runtime_error, "");
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG(n++, runtime_error, ""), "");
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG(throw 1, runtime_error, ""), "");
|
||||
EXPECT_NONFATAL_FAILURE(EXPECT_THROW_MSG(
|
||||
throw runtime_error("a"), runtime_error, "b"), "");
|
||||
}
|
||||
|
||||
TEST(AssertionSyntaxTest, ExceptionAssertionsBehavesLikeSingleStatement) {
|
||||
if (::testing::internal::AlwaysFalse())
|
||||
EXPECT_THROW_MSG(ThrowNothing(), std::exception, "");
|
||||
|
||||
if (::testing::internal::AlwaysTrue())
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "test");
|
||||
else
|
||||
; // NOLINT
|
||||
}
|
||||
|
||||
// Tests EXPECT_THROW_MSG.
|
||||
TEST(ExpectTest, EXPECT_THROW_MSG) {
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "test");
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowException(), std::logic_error, "test"),
|
||||
"Expected: ThrowException() throws an exception of "
|
||||
"type std::logic_error.\n Actual: it throws a different type.");
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowNothing(), std::exception, "test"),
|
||||
"Expected: ThrowNothing() throws an exception of type std::exception.\n"
|
||||
" Actual: it throws nothing.");
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "other"),
|
||||
"ThrowException() throws an exception with a different message.\n"
|
||||
"Expected: other\n"
|
||||
" Actual: test");
|
||||
}
|
||||
|
||||
TEST(StreamingAssertionsTest, ThrowMsg) {
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "test")
|
||||
<< "unexpected failure";
|
||||
EXPECT_NONFATAL_FAILURE(
|
||||
EXPECT_THROW_MSG(ThrowException(), std::exception, "other")
|
||||
<< "expected failure", "expected failure");
|
||||
}
|
||||
|
||||
#if FMT_USE_FILE_DESCRIPTORS
|
||||
|
||||
TEST(ErrorCodeTest, Ctor) {
|
||||
EXPECT_EQ(0, ErrorCode().get());
|
||||
EXPECT_EQ(42, ErrorCode(42).get());
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, DefaultCtor) {
|
||||
FileDescriptor fd;
|
||||
EXPECT_EQ(-1, fd.get());
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, OpenFileInCtor) {
|
||||
FILE *f = 0;
|
||||
{
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
f = fdopen(fd.get(), "r");
|
||||
ASSERT_TRUE(f != 0);
|
||||
}
|
||||
// Make sure fclose is called after the file descriptor is destroyed.
|
||||
// Otherwise the destructor will report an error because fclose has
|
||||
// already closed the file.
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, OpenFileError) {
|
||||
fmt::Writer message;
|
||||
fmt::internal::FormatSystemErrorMessage(
|
||||
message, ENOENT, "cannot open file nonexistent");
|
||||
EXPECT_THROW_MSG(FileDescriptor("nonexistent", FileDescriptor::RDONLY),
|
||||
fmt::SystemError, str(message));
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, MoveCtor) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
int fd_value = fd.get();
|
||||
EXPECT_NE(-1, fd_value);
|
||||
FileDescriptor fd2(std::move(fd));
|
||||
EXPECT_EQ(fd_value, fd2.get());
|
||||
EXPECT_EQ(-1, fd.get());
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, MoveAssignment) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
int fd_value = fd.get();
|
||||
EXPECT_NE(-1, fd_value);
|
||||
FileDescriptor fd2;
|
||||
fd2 = std::move(fd);
|
||||
EXPECT_EQ(fd_value, fd2.get());
|
||||
EXPECT_EQ(-1, fd.get());
|
||||
}
|
||||
|
||||
bool IsClosed(int fd) {
|
||||
char buffer[1];
|
||||
ssize_t result = read(fd, buffer, sizeof(buffer));
|
||||
return result == -1 && errno == EBADF;
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, MoveAssignmentClosesFile) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
FileDescriptor fd2("CMakeLists.txt", FileDescriptor::RDONLY);
|
||||
int old_fd = fd2.get();
|
||||
fd2 = std::move(fd);
|
||||
EXPECT_TRUE(IsClosed(old_fd));
|
||||
}
|
||||
|
||||
FileDescriptor OpenFile(int &fd_value) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
fd_value = fd.get();
|
||||
return std::move(fd);
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, MoveFromTemporaryInCtor) {
|
||||
int fd_value = 0xdeadbeef;
|
||||
FileDescriptor fd(OpenFile(fd_value));
|
||||
EXPECT_EQ(fd_value, fd.get());
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, MoveFromTemporaryInAssignment) {
|
||||
int fd_value = 0xdeadbeef;
|
||||
FileDescriptor fd;
|
||||
fd = OpenFile(fd_value);
|
||||
EXPECT_EQ(fd_value, fd.get());
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, MoveFromTemporaryInAssignmentClosesFile) {
|
||||
int fd_value = 0xdeadbeef;
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
int old_fd = fd.get();
|
||||
fd = OpenFile(fd_value);
|
||||
EXPECT_TRUE(IsClosed(old_fd));
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, CloseFileInDtor) {
|
||||
int fd_value = 0;
|
||||
{
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
fd_value = fd.get();
|
||||
}
|
||||
FILE *f = fdopen(fd_value, "r");
|
||||
int error_code = errno;
|
||||
if (f)
|
||||
fclose(f);
|
||||
EXPECT_TRUE(f == 0);
|
||||
EXPECT_EQ(EBADF, error_code);
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, CloseError) {
|
||||
FileDescriptor *fd =
|
||||
new FileDescriptor(".travis.yml", FileDescriptor::RDONLY);
|
||||
fmt::Writer message;
|
||||
fmt::internal::FormatSystemErrorMessage(message, EBADF, "cannot close file");
|
||||
EXPECT_STDERR(close(fd->get()); delete fd, str(message) + "\n");
|
||||
}
|
||||
|
||||
std::string ReadLine(FileDescriptor &fd) {
|
||||
enum { BUFFER_SIZE = 100 };
|
||||
char buffer[BUFFER_SIZE];
|
||||
ssize_t result = read(fd.get(), buffer, BUFFER_SIZE);
|
||||
if (result == -1)
|
||||
fmt::ThrowSystemError(errno, "cannot read file");
|
||||
buffer[std::min<ssize_t>(BUFFER_SIZE - 1, result)] = '\0';
|
||||
if (char *end = strchr(buffer, '\n'))
|
||||
*end = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, Dup) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
FileDescriptor dup = FileDescriptor::dup(fd.get());
|
||||
EXPECT_NE(fd.get(), dup.get());
|
||||
EXPECT_EQ("language: cpp", ReadLine(dup));
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, DupError) {
|
||||
fmt::Writer message;
|
||||
fmt::internal::FormatSystemErrorMessage(
|
||||
message, EBADF, "cannot duplicate file descriptor -1");
|
||||
EXPECT_THROW_MSG(FileDescriptor::dup(-1), fmt::SystemError, str(message));
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, Dup2) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
FileDescriptor dup("CMakeLists.txt", FileDescriptor::RDONLY);
|
||||
fd.dup2(dup.get());
|
||||
EXPECT_NE(fd.get(), dup.get());
|
||||
EXPECT_EQ("language: cpp", ReadLine(dup));
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, Dup2Error) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
fmt::Writer message;
|
||||
fmt::internal::FormatSystemErrorMessage(message, EBADF,
|
||||
fmt::Format("cannot duplicate file descriptor {} to -1") << fd.get());
|
||||
EXPECT_THROW_MSG(fd.dup2(-1), fmt::SystemError, str(message));
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, Dup2NoExcept) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
FileDescriptor dup("CMakeLists.txt", FileDescriptor::RDONLY);
|
||||
ErrorCode ec;
|
||||
fd.dup2(dup.get(), ec);
|
||||
EXPECT_EQ(0, ec.get());
|
||||
EXPECT_NE(fd.get(), dup.get());
|
||||
EXPECT_EQ("language: cpp", ReadLine(dup));
|
||||
}
|
||||
|
||||
TEST(FileDescriptorTest, Dup2NoExceptError) {
|
||||
FileDescriptor fd(".travis.yml", FileDescriptor::RDONLY);
|
||||
ErrorCode ec;
|
||||
fd.dup2(-1, ec);
|
||||
EXPECT_EQ(EBADF, ec.get());
|
||||
}
|
||||
|
||||
// TODO: test pipe
|
||||
|
||||
// TODO: compile both with C++11 & C++98 mode
|
||||
|
||||
#endif
|
||||
|
||||
// TODO: test OutputRedirector
|
||||
|
||||
} // namespace
|
152
test/gtest-extra.cc
Normal file
152
test/gtest-extra.cc
Normal file
@ -0,0 +1,152 @@
|
||||
/*
|
||||
Custom Google Test assertions.
|
||||
|
||||
Copyright (c) 2012-2014, Victor Zverovich
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "gtest-extra.h"
|
||||
|
||||
#if FMT_USE_FILE_DESCRIPTORS
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
# include <unistd.h>
|
||||
#else
|
||||
# include <io.h>
|
||||
|
||||
# define O_CREAT _O_CREAT
|
||||
# define O_TRUNC _O_TRUNC
|
||||
# define S_IRUSR _S_IREAD
|
||||
# define S_IWUSR _S_IWRITE
|
||||
# define close _close
|
||||
# define dup _dup
|
||||
# define dup2 _dup2
|
||||
#endif // _WIN32
|
||||
|
||||
// Retries the expression while it evaluates to -1 and error equals to EINTR.
|
||||
#define FMT_RETRY(result, expression) \
|
||||
do { \
|
||||
result = (expression); \
|
||||
} while (result == -1 && errno == EINTR)
|
||||
|
||||
FileDescriptor::FileDescriptor(const char *path, int oflag) {
|
||||
int mode = S_IRUSR | S_IWUSR;
|
||||
#ifdef _WIN32
|
||||
fd_ = -1;
|
||||
_sopen_s(&fd, path, oflag, _SH_DENYNO, mode);
|
||||
#else
|
||||
FMT_RETRY(fd_, open(path, oflag, mode));
|
||||
#endif
|
||||
if (fd_ == -1)
|
||||
fmt::ThrowSystemError(errno, "cannot open file {}") << path;
|
||||
}
|
||||
|
||||
void FileDescriptor::close() {
|
||||
if (fd_ == -1)
|
||||
return;
|
||||
// Don't need to retry close in case of EINTR.
|
||||
// See http://linux.derkeiler.com/Mailing-Lists/Kernel/2005-09/3000.html
|
||||
if (::close(fd_) != 0)
|
||||
fmt::ReportSystemError(errno, "cannot close file");
|
||||
}
|
||||
|
||||
FileDescriptor FileDescriptor::dup(int fd) {
|
||||
int new_fd = 0;
|
||||
FMT_RETRY(new_fd, ::dup(fd));
|
||||
if (new_fd == -1)
|
||||
fmt::ThrowSystemError(errno, "cannot duplicate file descriptor {}") << fd;
|
||||
return FileDescriptor(new_fd);
|
||||
}
|
||||
|
||||
void FileDescriptor::dup2(int fd) {
|
||||
int result = 0;
|
||||
FMT_RETRY(result, ::dup2(fd_, fd));
|
||||
if (result == -1) {
|
||||
fmt::ThrowSystemError(errno,
|
||||
"cannot duplicate file descriptor {} to {}") << fd_ << fd;
|
||||
}
|
||||
}
|
||||
|
||||
void FileDescriptor::dup2(int fd, ErrorCode &ec) FMT_NOEXCEPT(true) {
|
||||
int result = 0;
|
||||
FMT_RETRY(result, ::dup2(fd_, fd));
|
||||
if (result == -1)
|
||||
ec = ErrorCode(errno);
|
||||
}
|
||||
|
||||
void FileDescriptor::pipe(FileDescriptor &read_fd, FileDescriptor &write_fd) {
|
||||
// Close the descriptors first to make sure that assignments don't throw
|
||||
// and there are no leaks.
|
||||
read_fd.close();
|
||||
write_fd.close();
|
||||
int fds[2] = {};
|
||||
if (::pipe(fds) != 0)
|
||||
fmt::ThrowSystemError(errno, "cannot create pipe");
|
||||
// The following assignments don't throw because read_fd and write_fd
|
||||
// are closed.
|
||||
read_fd = FileDescriptor(fds[0]);
|
||||
write_fd = FileDescriptor(fds[1]);
|
||||
}
|
||||
|
||||
OutputRedirector::OutputRedirector(FILE *file) : file_(file) {
|
||||
if (std::fflush(file) != 0)
|
||||
fmt::ThrowSystemError(errno, "cannot flush stream");
|
||||
int fd = fileno(file);
|
||||
saved_fd_ = FileDescriptor::dup(fd);
|
||||
FileDescriptor write_fd;
|
||||
FileDescriptor::pipe(read_fd_, write_fd);
|
||||
write_fd.dup2(fd);
|
||||
}
|
||||
|
||||
OutputRedirector::~OutputRedirector() {
|
||||
if (std::fflush(file_) != 0)
|
||||
fmt::ReportSystemError(errno, "cannot flush stream");
|
||||
ErrorCode ec;
|
||||
saved_fd_.dup2(fileno(file_), ec);
|
||||
if (ec.get())
|
||||
fmt::ReportSystemError(errno, "cannot restore output");
|
||||
}
|
||||
|
||||
std::string OutputRedirector::Read() {
|
||||
// Restore output.
|
||||
if (std::fflush(file_) != 0)
|
||||
fmt::ThrowSystemError(errno, "cannot flush stream");
|
||||
saved_fd_.dup2(fileno(file_));
|
||||
|
||||
// TODO: move to FileDescriptor
|
||||
enum { BUFFER_SIZE = 100 };
|
||||
char buffer[BUFFER_SIZE];
|
||||
ssize_t result = read(read_fd_.get(), buffer, BUFFER_SIZE);
|
||||
if (result == -1)
|
||||
fmt::ThrowSystemError(errno, "cannot read file");
|
||||
buffer[std::min<ssize_t>(BUFFER_SIZE - 1, result)] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// TODO: test EXPECT_STDOUT and EXPECT_STDERR
|
||||
|
||||
#endif // FMT_USE_FILE_DESCRIPTORS
|
@ -28,8 +28,14 @@
|
||||
#ifndef FMT_GTEST_EXTRA_H
|
||||
#define FMT_GTEST_EXTRA_H
|
||||
|
||||
#if FMT_USE_FILE_DESCRIPTORS
|
||||
# include <fcntl.h>
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "format.h"
|
||||
|
||||
#define FMT_TEST_THROW_(statement, expected_exception, expected_message, fail) \
|
||||
GTEST_AMBIGUOUS_ELSE_BLOCKER_ \
|
||||
if (::testing::AssertionResult gtest_ar = ::testing::AssertionSuccess()) { \
|
||||
@ -69,4 +75,188 @@
|
||||
FMT_TEST_THROW_(statement, expected_exception, \
|
||||
expected_message, GTEST_NONFATAL_FAILURE_)
|
||||
|
||||
#ifndef FMT_USE_FILE_DESCRIPTORS
|
||||
# define FMT_USE_FILE_DESCRIPTORS 0
|
||||
#endif
|
||||
|
||||
#if FMT_USE_FILE_DESCRIPTORS
|
||||
|
||||
#ifdef _WIN32
|
||||
// Fix warnings about deprecated symbols.
|
||||
# define FMT_POSIX(name) _##name
|
||||
#else
|
||||
# define FMT_POSIX(name) name
|
||||
#endif
|
||||
|
||||
// An error code.
|
||||
class ErrorCode {
|
||||
private:
|
||||
int value_;
|
||||
|
||||
public:
|
||||
explicit ErrorCode(int value = 0) FMT_NOEXCEPT(true) : value_(value) {}
|
||||
|
||||
int get() const FMT_NOEXCEPT(true) { return value_; }
|
||||
};
|
||||
|
||||
// A RAII class for file descriptors.
|
||||
class FileDescriptor {
|
||||
private:
|
||||
int fd_;
|
||||
|
||||
// Closes the file if its descriptor is not -1.
|
||||
void close();
|
||||
|
||||
// Constructs a FileDescriptor object with a given descriptor.
|
||||
explicit FileDescriptor(int fd) : fd_(fd) {}
|
||||
|
||||
public:
|
||||
// Possible values for the oflag argument to the constructor.
|
||||
enum {
|
||||
RDONLY = FMT_POSIX(O_RDONLY), // Open for reading only.
|
||||
WRONLY = FMT_POSIX(O_WRONLY), // Open for writing only.
|
||||
RDWR = FMT_POSIX(O_RDWR) // Open for reading and writing.
|
||||
};
|
||||
|
||||
// Constructs a FileDescriptor object with a descriptor of -1 which
|
||||
// is ignored by the destructor.
|
||||
FileDescriptor() FMT_NOEXCEPT(true) : fd_(-1) {}
|
||||
|
||||
// Opens a file and constructs a FileDescriptor object with the descriptor
|
||||
// of the opened file. Throws fmt::SystemError on error.
|
||||
FileDescriptor(const char *path, int oflag);
|
||||
|
||||
#if !FMT_USE_RVALUE_REFERENCES
|
||||
// Emulate a move constructor and a move assignment operator if rvalue
|
||||
// references are not supported.
|
||||
|
||||
private:
|
||||
// A proxy object to emulate a move constructor.
|
||||
// It is private to make it impossible call operator Proxy directly.
|
||||
struct Proxy {
|
||||
int fd;
|
||||
};
|
||||
|
||||
public:
|
||||
// A "move" constructor for moving from a temporary.
|
||||
FileDescriptor(Proxy p) FMT_NOEXCEPT(true) : fd_(p.fd) {}
|
||||
|
||||
// A "move" constructor for for moving from an lvalue.
|
||||
FileDescriptor(FileDescriptor &other) FMT_NOEXCEPT(true) : fd_(other.fd_) {
|
||||
other.fd_ = -1;
|
||||
}
|
||||
|
||||
// A "move" assignment operator for moving from a temporary.
|
||||
FileDescriptor &operator=(Proxy p) {
|
||||
close();
|
||||
fd_ = p.fd;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// A "move" assignment operator for moving from an lvalue.
|
||||
FileDescriptor &operator=(FileDescriptor &other) {
|
||||
close();
|
||||
fd_ = other.fd_;
|
||||
other.fd_ = -1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Returns a proxy object for moving from a temporary:
|
||||
// FileDescriptor fd = FileDescriptor(...);
|
||||
operator Proxy() FMT_NOEXCEPT(true) {
|
||||
Proxy p = {fd_};
|
||||
fd_ = -1;
|
||||
return p;
|
||||
}
|
||||
#else
|
||||
private:
|
||||
GTEST_DISALLOW_COPY_AND_ASSIGN_(FileDescriptor);
|
||||
|
||||
public:
|
||||
FileDescriptor(FileDescriptor &&other) FMT_NOEXCEPT(true) : fd_(other.fd_) {
|
||||
other.fd_ = -1;
|
||||
}
|
||||
|
||||
FileDescriptor& operator=(FileDescriptor &&other) FMT_NOEXCEPT(true) {
|
||||
fd_ = other.fd_;
|
||||
other.fd_ = -1;
|
||||
return *this;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Closes the file if its descriptor is not -1 and destroys the object.
|
||||
~FileDescriptor() { close(); }
|
||||
|
||||
// Returns the file descriptor.
|
||||
int get() const FMT_NOEXCEPT(true) { return fd_; }
|
||||
|
||||
// Duplicates a file descriptor with the dup function and returns
|
||||
// the duplicate. Throws fmt::SystemError on error.
|
||||
static FileDescriptor dup(int fd);
|
||||
|
||||
// Makes fd be the copy of this file descriptor, closing fd first if
|
||||
// necessary. Throws fmt::SystemError on error.
|
||||
void dup2(int fd);
|
||||
|
||||
// Makes fd be the copy of this file descriptor, closing fd first if
|
||||
// necessary.
|
||||
void dup2(int fd, ErrorCode &ec) FMT_NOEXCEPT(true);
|
||||
|
||||
static void pipe(FileDescriptor &read_fd, FileDescriptor &write_fd);
|
||||
};
|
||||
|
||||
#if !FMT_USE_RVALUE_REFERENCES
|
||||
namespace std {
|
||||
// For compatibility with C++98.
|
||||
inline FileDescriptor &move(FileDescriptor &fd) { return fd; }
|
||||
}
|
||||
#endif
|
||||
|
||||
// Redirect file output to a pipe.
|
||||
class OutputRedirector {
|
||||
private:
|
||||
FILE *file_;
|
||||
FileDescriptor saved_fd_; // Saved file descriptor created with dup.
|
||||
FileDescriptor read_fd_; // Read end of the pipe where the output is
|
||||
// redirected.
|
||||
|
||||
GTEST_DISALLOW_COPY_AND_ASSIGN_(OutputRedirector);
|
||||
|
||||
public:
|
||||
explicit OutputRedirector(FILE *file);
|
||||
~OutputRedirector();
|
||||
|
||||
std::string Read();
|
||||
};
|
||||
|
||||
#define FMT_TEST_PRINT_(statement, expected_output, file, fail) \
|
||||
GTEST_AMBIGUOUS_ELSE_BLOCKER_ \
|
||||
if (::testing::AssertionResult gtest_ar = ::testing::AssertionSuccess()) { \
|
||||
std::string output; \
|
||||
{ \
|
||||
OutputRedirector redir(file); \
|
||||
GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement); \
|
||||
output = redir.Read(); \
|
||||
} \
|
||||
if (output != expected_output) { \
|
||||
gtest_ar \
|
||||
<< #statement " produces different output.\n" \
|
||||
<< "Expected: " << expected_output << "\n" \
|
||||
<< " Actual: " << output; \
|
||||
goto GTEST_CONCAT_TOKEN_(gtest_label_testthrow_, __LINE__); \
|
||||
} \
|
||||
} else \
|
||||
GTEST_CONCAT_TOKEN_(gtest_label_testthrow_, __LINE__): \
|
||||
fail(gtest_ar.failure_message())
|
||||
|
||||
// Tests that the statement prints the expected output to stdout.
|
||||
#define EXPECT_STDOUT(statement, expected_output) \
|
||||
FMT_TEST_PRINT_(statement, expected_output, stdout, GTEST_NONFATAL_FAILURE_)
|
||||
|
||||
// Tests that the statement prints the expected output to stderr.
|
||||
#define EXPECT_STDERR(statement, expected_output) \
|
||||
FMT_TEST_PRINT_(statement, expected_output, stderr, GTEST_NONFATAL_FAILURE_)
|
||||
|
||||
#endif // FMT_USE_FILE_DESCRIPTORS
|
||||
|
||||
#endif // FMT_GTEST_EXTRA_H
|
||||
|
Loading…
Reference in New Issue
Block a user