From 1a2d7be3f5b0c2a8d1d316083c296014dd07b40e Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Sat, 3 May 2014 09:48:54 -0700 Subject: [PATCH] Implement EXPECT_STDOUT and EXPECT_STDERR using pipes. --- CMakeLists.txt | 16 +- format.cc | 28 +++- format.h | 17 ++- test/assert-test.cc | 130 ---------------- test/format-test.cc | 52 +------ test/gtest-extra-test.cc | 313 +++++++++++++++++++++++++++++++++++++++ test/gtest-extra.cc | 152 +++++++++++++++++++ test/gtest-extra.h | 190 ++++++++++++++++++++++++ 8 files changed, 709 insertions(+), 189 deletions(-) delete mode 100644 test/assert-test.cc create mode 100644 test/gtest-extra-test.cc create mode 100644 test/gtest-extra.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index d995c31b..79f0884b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/format.cc b/format.cc index ce03ab5e..1776456f 100644 --- a/format.cc +++ b/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 int fmt::internal::CharTraits::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()( diff --git a/format.h b/format.h index 07066392..1dec4935 100644 --- a/format.h +++ b/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 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 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: diff --git a/test/assert-test.cc b/test/assert-test.cc deleted file mode 100644 index 87bc82e7..00000000 --- a/test/assert-test.cc +++ /dev/null @@ -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 -#include - -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 diff --git a/test/format-test.cc b/test/format-test.cc index f9f9ff7c..d41435c8 100644 --- a/test/format-test.cc +++ b/test/format-test.cc @@ -40,37 +40,6 @@ # include #endif -#if FMT_USE_DUP - -# include -# include -# include - -# ifdef _WIN32 - -# include - -# 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 -# 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 diff --git a/test/gtest-extra-test.cc b/test/gtest-extra-test.cc new file mode 100644 index 00000000..713d5fba --- /dev/null +++ b/test/gtest-extra-test.cc @@ -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 +#include +#include + +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(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 diff --git a/test/gtest-extra.cc b/test/gtest-extra.cc new file mode 100644 index 00000000..7e20f216 --- /dev/null +++ b/test/gtest-extra.cc @@ -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 +#include +#include + +#ifndef _WIN32 +# include +#else +# include + +# 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(BUFFER_SIZE - 1, result)] = '\0'; + return buffer; +} + +// TODO: test EXPECT_STDOUT and EXPECT_STDERR + +#endif // FMT_USE_FILE_DESCRIPTORS diff --git a/test/gtest-extra.h b/test/gtest-extra.h index 36852982..417bf520 100644 --- a/test/gtest-extra.h +++ b/test/gtest-extra.h @@ -28,8 +28,14 @@ #ifndef FMT_GTEST_EXTRA_H #define FMT_GTEST_EXTRA_H +#if FMT_USE_FILE_DESCRIPTORS +# include +#endif + #include +#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