diff options
Diffstat (limited to 'utils')
252 files changed, 45147 insertions, 0 deletions
diff --git a/utils/.gitignore b/utils/.gitignore new file mode 100644 index 000000000000..b33d720f27a4 --- /dev/null +++ b/utils/.gitignore @@ -0,0 +1,2 @@ +defs.hpp +stacktrace_helper diff --git a/utils/Kyuafile b/utils/Kyuafile new file mode 100644 index 000000000000..042ad77a3fe4 --- /dev/null +++ b/utils/Kyuafile @@ -0,0 +1,24 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="auto_array_test"} +atf_test_program{name="datetime_test"} +atf_test_program{name="env_test"} +atf_test_program{name="memory_test"} +atf_test_program{name="optional_test"} +atf_test_program{name="passwd_test"} +atf_test_program{name="sanity_test"} +atf_test_program{name="stacktrace_test"} +atf_test_program{name="stream_test"} +atf_test_program{name="units_test"} + +include("cmdline/Kyuafile") +include("config/Kyuafile") +include("format/Kyuafile") +include("fs/Kyuafile") +include("logging/Kyuafile") +include("process/Kyuafile") +include("signals/Kyuafile") +include("sqlite/Kyuafile") +include("text/Kyuafile") diff --git a/utils/Makefile.am.inc b/utils/Makefile.am.inc new file mode 100644 index 000000000000..d6690bdbecde --- /dev/null +++ b/utils/Makefile.am.inc @@ -0,0 +1,133 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +UTILS_CFLAGS = +UTILS_LIBS = libutils.a + +noinst_LIBRARIES += libutils.a +libutils_a_CPPFLAGS = -DGDB=\"$(GDB)\" +libutils_a_SOURCES = utils/auto_array.hpp +libutils_a_SOURCES += utils/auto_array.ipp +libutils_a_SOURCES += utils/auto_array_fwd.hpp +libutils_a_SOURCES += utils/datetime.cpp +libutils_a_SOURCES += utils/datetime.hpp +libutils_a_SOURCES += utils/datetime_fwd.hpp +libutils_a_SOURCES += utils/env.hpp +libutils_a_SOURCES += utils/env.cpp +libutils_a_SOURCES += utils/memory.hpp +libutils_a_SOURCES += utils/memory.cpp +libutils_a_SOURCES += utils/noncopyable.hpp +libutils_a_SOURCES += utils/optional.hpp +libutils_a_SOURCES += utils/optional_fwd.hpp +libutils_a_SOURCES += utils/optional.ipp +libutils_a_SOURCES += utils/passwd.cpp +libutils_a_SOURCES += utils/passwd.hpp +libutils_a_SOURCES += utils/passwd_fwd.hpp +libutils_a_SOURCES += utils/sanity.cpp +libutils_a_SOURCES += utils/sanity.hpp +libutils_a_SOURCES += utils/sanity_fwd.hpp +libutils_a_SOURCES += utils/stacktrace.cpp +libutils_a_SOURCES += utils/stacktrace.hpp +libutils_a_SOURCES += utils/stream.cpp +libutils_a_SOURCES += utils/stream.hpp +libutils_a_SOURCES += utils/units.cpp +libutils_a_SOURCES += utils/units.hpp +libutils_a_SOURCES += utils/units_fwd.hpp +nodist_libutils_a_SOURCES = utils/defs.hpp + +EXTRA_DIST += utils/test_utils.ipp + +if WITH_ATF +tests_utilsdir = $(pkgtestsdir)/utils + +tests_utils_DATA = utils/Kyuafile +EXTRA_DIST += $(tests_utils_DATA) + +tests_utils_PROGRAMS = utils/auto_array_test +utils_auto_array_test_SOURCES = utils/auto_array_test.cpp +utils_auto_array_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_auto_array_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/datetime_test +utils_datetime_test_SOURCES = utils/datetime_test.cpp +utils_datetime_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_datetime_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/env_test +utils_env_test_SOURCES = utils/env_test.cpp +utils_env_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_env_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/memory_test +utils_memory_test_SOURCES = utils/memory_test.cpp +utils_memory_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_memory_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/optional_test +utils_optional_test_SOURCES = utils/optional_test.cpp +utils_optional_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_optional_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/passwd_test +utils_passwd_test_SOURCES = utils/passwd_test.cpp +utils_passwd_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_passwd_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/sanity_test +utils_sanity_test_SOURCES = utils/sanity_test.cpp +utils_sanity_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sanity_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/stacktrace_helper +utils_stacktrace_helper_SOURCES = utils/stacktrace_helper.cpp + +tests_utils_PROGRAMS += utils/stacktrace_test +utils_stacktrace_test_SOURCES = utils/stacktrace_test.cpp +utils_stacktrace_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_stacktrace_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/stream_test +utils_stream_test_SOURCES = utils/stream_test.cpp +utils_stream_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_stream_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/units_test +utils_units_test_SOURCES = utils/units_test.cpp +utils_units_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_units_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif + +include utils/cmdline/Makefile.am.inc +include utils/config/Makefile.am.inc +include utils/format/Makefile.am.inc +include utils/fs/Makefile.am.inc +include utils/logging/Makefile.am.inc +include utils/process/Makefile.am.inc +include utils/signals/Makefile.am.inc +include utils/sqlite/Makefile.am.inc +include utils/text/Makefile.am.inc diff --git a/utils/auto_array.hpp b/utils/auto_array.hpp new file mode 100644 index 000000000000..0cc3d0e0afd5 --- /dev/null +++ b/utils/auto_array.hpp @@ -0,0 +1,102 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/auto_array.hpp +/// Provides the utils::auto_array class. +/// +/// The class is provided as a separate module on its own to minimize +/// header-inclusion side-effects. + +#if !defined(UTILS_AUTO_ARRAY_HPP) +#define UTILS_AUTO_ARRAY_HPP + +#include "utils/auto_array_fwd.hpp" + +#include <cstddef> + +namespace utils { + + +namespace detail { + + +/// Wrapper class to provide reference semantics for utils::auto_array. +/// +/// This class is internally used, for example, to allow returning a +/// utils::auto_array from a function. +template< class T > +class auto_array_ref { + /// Internal pointer to the dynamically-allocated array. + T* _ptr; + + template< class > friend class utils::auto_array; + +public: + explicit auto_array_ref(T*); +}; + + +} // namespace detail + + +/// A simple smart pointer for arrays providing strict ownership semantics. +/// +/// This class is the counterpart of std::auto_ptr for arrays. The semantics of +/// the API of this class are the same as those of std::auto_ptr. +/// +/// The wrapped pointer must be NULL or must have been allocated using operator +/// new[]. +template< class T > +class auto_array { + /// Internal pointer to the dynamically-allocated array. + T* _ptr; + +public: + auto_array(T* = NULL) throw(); + auto_array(auto_array< T >&) throw(); + auto_array(detail::auto_array_ref< T >) throw(); + ~auto_array(void) throw(); + + T* get(void) throw(); + const T* get(void) const throw(); + + T* release(void) throw(); + void reset(T* = NULL) throw(); + + auto_array< T >& operator=(auto_array< T >&) throw(); + auto_array< T >& operator=(detail::auto_array_ref< T >) throw(); + T& operator[](int) throw(); + const T& operator[](int) const throw(); + operator detail::auto_array_ref< T >(void) throw(); +}; + + +} // namespace utils + + +#endif // !defined(UTILS_AUTO_ARRAY_HPP) diff --git a/utils/auto_array.ipp b/utils/auto_array.ipp new file mode 100644 index 000000000000..fd29311def8c --- /dev/null +++ b/utils/auto_array.ipp @@ -0,0 +1,227 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_AUTO_ARRAY_IPP) +#define UTILS_AUTO_ARRAY_IPP + +#include "utils/auto_array.hpp" + +namespace utils { + + +namespace detail { + + +/// Constructs a new auto_array_ref from a pointer. +/// +/// \param ptr The pointer to wrap. +template< class T > inline +auto_array_ref< T >::auto_array_ref(T* ptr) : + _ptr(ptr) +{ +} + + +} // namespace detail + + +/// Constructs a new auto_array from a given pointer. +/// +/// This grabs ownership of the pointer unless it is NULL. +/// +/// \param ptr The pointer to wrap. If not NULL, the memory pointed to must +/// have been allocated with operator new[]. +template< class T > inline +auto_array< T >::auto_array(T* ptr) throw() : + _ptr(ptr) +{ +} + + +/// Constructs a copy of an auto_array. +/// +/// \param ptr The pointer to copy from. This pointer is invalidated and the +/// new copy grabs ownership of the object pointed to. +template< class T > inline +auto_array< T >::auto_array(auto_array< T >& ptr) throw() : + _ptr(ptr.release()) +{ +} + + +/// Constructs a new auto_array form a reference. +/// +/// Internal function used to construct a new auto_array from an object +/// returned, for example, from a function. +/// +/// \param ref The reference. +template< class T > inline +auto_array< T >::auto_array(detail::auto_array_ref< T > ref) throw() : + _ptr(ref._ptr) +{ +} + + +/// Destructor for auto_array objects. +template< class T > inline +auto_array< T >::~auto_array(void) throw() +{ + if (_ptr != NULL) + delete [] _ptr; +} + + +/// Gets the value of the wrapped pointer without releasing ownership. +/// +/// \return The raw mutable pointer. +template< class T > inline +T* +auto_array< T >::get(void) throw() +{ + return _ptr; +} + + +/// Gets the value of the wrapped pointer without releasing ownership. +/// +/// \return The raw immutable pointer. +template< class T > inline +const T* +auto_array< T >::get(void) const throw() +{ + return _ptr; +} + + +/// Gets the value of the wrapped pointer and releases ownership. +/// +/// \return The raw mutable pointer. +template< class T > inline +T* +auto_array< T >::release(void) throw() +{ + T* ptr = _ptr; + _ptr = NULL; + return ptr; +} + + +/// Changes the value of the wrapped pointer. +/// +/// If the auto_array was pointing to an array, such array is released and the +/// wrapped pointer is replaced with the new pointer provided. +/// +/// \param ptr The pointer to use as a replacement; may be NULL. +template< class T > inline +void +auto_array< T >::reset(T* ptr) throw() +{ + if (_ptr != NULL) + delete [] _ptr; + _ptr = ptr; +} + + +/// Assignment operator. +/// +/// \param ptr The object to copy from. This is invalidated after the copy. +/// \return A reference to the auto_array object itself. +template< class T > inline +auto_array< T >& +auto_array< T >::operator=(auto_array< T >& ptr) throw() +{ + reset(ptr.release()); + return *this; +} + + +/// Internal assignment operator for function returns. +/// +/// \param ref The reference object to copy from. +/// \return A reference to the auto_array object itself. +template< class T > inline +auto_array< T >& +auto_array< T >::operator=(detail::auto_array_ref< T > ref) throw() +{ + if (_ptr != ref._ptr) { + delete [] _ptr; + _ptr = ref._ptr; + } + return *this; +} + + +/// Subscript operator to access the array by position. +/// +/// This does not perform any bounds checking, in particular because auto_array +/// does not know the size of the arrays pointed to by it. +/// +/// \param pos The position to access, indexed from zero. +/// +/// \return A mutable reference to the element at the specified position. +template< class T > inline +T& +auto_array< T >::operator[](int pos) throw() +{ + return _ptr[pos]; +} + + +/// Subscript operator to access the array by position. +/// +/// This does not perform any bounds checking, in particular because auto_array +/// does not know the size of the arrays pointed to by it. +/// +/// \param pos The position to access, indexed from zero. +/// +/// \return An immutable reference to the element at the specified position. +template< class T > inline +const T& +auto_array< T >::operator[](int pos) const throw() +{ + return _ptr[pos]; +} + + +/// Internal conversion to a reference wrapper. +/// +/// This is used internally to support returning auto_array objects from +/// functions. The auto_array is invalidated when used. +/// +/// \return A new detail::auto_array_ref object holding the pointer. +template< class T > inline +auto_array< T >::operator detail::auto_array_ref< T >(void) throw() +{ + return detail::auto_array_ref< T >(release()); +} + + +} // namespace utils + + +#endif // !defined(UTILS_AUTO_ARRAY_IPP) diff --git a/utils/auto_array_fwd.hpp b/utils/auto_array_fwd.hpp new file mode 100644 index 000000000000..e1522a25bf7d --- /dev/null +++ b/utils/auto_array_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/auto_array_fwd.hpp +/// Forward declarations for utils/auto_array.hpp + +#if !defined(UTILS_AUTO_ARRAY_FWD_HPP) +#define UTILS_AUTO_ARRAY_FWD_HPP + +namespace utils { + + +template< class > class auto_array; + + +} // namespace utils + +#endif // !defined(UTILS_AUTO_ARRAY_FWD_HPP) diff --git a/utils/auto_array_test.cpp b/utils/auto_array_test.cpp new file mode 100644 index 000000000000..041eb65863ba --- /dev/null +++ b/utils/auto_array_test.cpp @@ -0,0 +1,312 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/auto_array.ipp" + +extern "C" { +#include <sys/types.h> +} + +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/defs.hpp" + +using utils::auto_array; + + +namespace { + + +/// Mock class to capture calls to the new and delete operators. +class test_array { +public: + /// User-settable cookie to disambiguate instances of this class. + int m_value; + + /// The current balance of existing test_array instances. + static ssize_t m_nblocks; + + /// Captures invalid calls to new on an array. + /// + /// \return Nothing; this always fails the test case. + void* + operator new(const size_t /* size */) + { + ATF_FAIL("New called but should have been new[]"); + return new int(5); + } + + /// Obtains memory for a new instance and increments m_nblocks. + /// + /// \param size The amount of memory to allocate, in bytes. + /// + /// \return A pointer to the allocated memory. + /// + /// \throw std::bad_alloc If the memory cannot be allocated. + void* + operator new[](const size_t size) + { + void* mem = ::operator new(size); + m_nblocks++; + std::cout << "Allocated 'test_array' object " << mem << "\n"; + return mem; + } + + /// Captures invalid calls to delete on an array. + /// + /// \return Nothing; this always fails the test case. + void + operator delete(void* /* mem */) + { + ATF_FAIL("Delete called but should have been delete[]"); + } + + /// Deletes a previously allocated array and decrements m_nblocks. + /// + /// \param mem The pointer to the memory to be deleted. + void + operator delete[](void* mem) + { + std::cout << "Releasing 'test_array' object " << mem << "\n"; + if (m_nblocks == 0) + ATF_FAIL("Unbalanced delete[]"); + m_nblocks--; + ::operator delete(mem); + } +}; + + +ssize_t test_array::m_nblocks = 0; + + +} // anonymous namespace + + +ATF_TEST_CASE(scope); +ATF_TEST_CASE_HEAD(scope) +{ + set_md_var("descr", "Tests the automatic scope handling in the " + "auto_array smart pointer class"); +} +ATF_TEST_CASE_BODY(scope) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(copy); +ATF_TEST_CASE_HEAD(copy) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' copy " + "constructor"); +} +ATF_TEST_CASE_BODY(copy) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2(t1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(copy_ref); +ATF_TEST_CASE_HEAD(copy_ref) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' copy " + "constructor through the auxiliary ref object"); +} +ATF_TEST_CASE_BODY(copy_ref) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2 = t1; + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(get); +ATF_TEST_CASE_HEAD(get) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' get " + "method"); +} +ATF_TEST_CASE_BODY(get) +{ + test_array* ta = new test_array[10]; + auto_array< test_array > t(ta); + ATF_REQUIRE_EQ(t.get(), ta); +} + + +ATF_TEST_CASE(release); +ATF_TEST_CASE_HEAD(release) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' release " + "method"); +} +ATF_TEST_CASE_BODY(release) +{ + test_array* ta1 = new test_array[10]; + { + auto_array< test_array > t(ta1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + test_array* ta2 = t.release(); + ATF_REQUIRE_EQ(ta2, ta1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + delete [] ta1; +} + + +ATF_TEST_CASE(reset); +ATF_TEST_CASE_HEAD(reset) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' reset " + "method"); +} +ATF_TEST_CASE_BODY(reset) +{ + test_array* ta1 = new test_array[10]; + test_array* ta2 = new test_array[10]; + ATF_REQUIRE_EQ(test_array::m_nblocks, 2); + + { + auto_array< test_array > t(ta1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 2); + t.reset(ta2); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + t.reset(); + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(assign); +ATF_TEST_CASE_HEAD(assign) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' " + "assignment operator"); +} +ATF_TEST_CASE_BODY(assign) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2; + t2 = t1; + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(assign_ref); +ATF_TEST_CASE_HEAD(assign_ref) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' " + "assignment operator through the auxiliary ref " + "object"); +} +ATF_TEST_CASE_BODY(assign_ref) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2; + t2 = t1; + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(access); +ATF_TEST_CASE_HEAD(access) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' access " + "operator"); +} +ATF_TEST_CASE_BODY(access) +{ + auto_array< test_array > t(new test_array[10]); + + for (int i = 0; i < 10; i++) + t[i].m_value = i * 2; + + for (int i = 0; i < 10; i++) + ATF_REQUIRE_EQ(t[i].m_value, i * 2); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, scope); + ATF_ADD_TEST_CASE(tcs, copy); + ATF_ADD_TEST_CASE(tcs, copy_ref); + ATF_ADD_TEST_CASE(tcs, get); + ATF_ADD_TEST_CASE(tcs, release); + ATF_ADD_TEST_CASE(tcs, reset); + ATF_ADD_TEST_CASE(tcs, assign); + ATF_ADD_TEST_CASE(tcs, assign_ref); + ATF_ADD_TEST_CASE(tcs, access); +} diff --git a/utils/cmdline/Kyuafile b/utils/cmdline/Kyuafile new file mode 100644 index 000000000000..d5e6f7122b07 --- /dev/null +++ b/utils/cmdline/Kyuafile @@ -0,0 +1,11 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="base_command_test"} +atf_test_program{name="commands_map_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="globals_test"} +atf_test_program{name="options_test"} +atf_test_program{name="parser_test"} +atf_test_program{name="ui_test"} diff --git a/utils/cmdline/Makefile.am.inc b/utils/cmdline/Makefile.am.inc new file mode 100644 index 000000000000..65081cbeafee --- /dev/null +++ b/utils/cmdline/Makefile.am.inc @@ -0,0 +1,96 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +libutils_a_SOURCES += utils/cmdline/base_command.cpp +libutils_a_SOURCES += utils/cmdline/base_command.hpp +libutils_a_SOURCES += utils/cmdline/base_command_fwd.hpp +libutils_a_SOURCES += utils/cmdline/base_command.ipp +libutils_a_SOURCES += utils/cmdline/commands_map.hpp +libutils_a_SOURCES += utils/cmdline/commands_map_fwd.hpp +libutils_a_SOURCES += utils/cmdline/commands_map.ipp +libutils_a_SOURCES += utils/cmdline/exceptions.cpp +libutils_a_SOURCES += utils/cmdline/exceptions.hpp +libutils_a_SOURCES += utils/cmdline/globals.cpp +libutils_a_SOURCES += utils/cmdline/globals.hpp +libutils_a_SOURCES += utils/cmdline/options.cpp +libutils_a_SOURCES += utils/cmdline/options.hpp +libutils_a_SOURCES += utils/cmdline/options_fwd.hpp +libutils_a_SOURCES += utils/cmdline/parser.cpp +libutils_a_SOURCES += utils/cmdline/parser.hpp +libutils_a_SOURCES += utils/cmdline/parser_fwd.hpp +libutils_a_SOURCES += utils/cmdline/parser.ipp +libutils_a_SOURCES += utils/cmdline/ui.cpp +libutils_a_SOURCES += utils/cmdline/ui.hpp +libutils_a_SOURCES += utils/cmdline/ui_fwd.hpp +# The following two files are only supposed to be used from test code. They +# should not be bundled into libutils.a, but doing so simplifies the build +# significantly. +libutils_a_SOURCES += utils/cmdline/ui_mock.hpp +libutils_a_SOURCES += utils/cmdline/ui_mock.cpp + +if WITH_ATF +tests_utils_cmdlinedir = $(pkgtestsdir)/utils/cmdline + +tests_utils_cmdline_DATA = utils/cmdline/Kyuafile +EXTRA_DIST += $(tests_utils_cmdline_DATA) + +tests_utils_cmdline_PROGRAMS = utils/cmdline/base_command_test +utils_cmdline_base_command_test_SOURCES = utils/cmdline/base_command_test.cpp +utils_cmdline_base_command_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_base_command_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/commands_map_test +utils_cmdline_commands_map_test_SOURCES = utils/cmdline/commands_map_test.cpp +utils_cmdline_commands_map_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_commands_map_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/exceptions_test +utils_cmdline_exceptions_test_SOURCES = utils/cmdline/exceptions_test.cpp +utils_cmdline_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/globals_test +utils_cmdline_globals_test_SOURCES = utils/cmdline/globals_test.cpp +utils_cmdline_globals_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_globals_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/options_test +utils_cmdline_options_test_SOURCES = utils/cmdline/options_test.cpp +utils_cmdline_options_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_options_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/parser_test +utils_cmdline_parser_test_SOURCES = utils/cmdline/parser_test.cpp +utils_cmdline_parser_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_parser_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/ui_test +utils_cmdline_ui_test_SOURCES = utils/cmdline/ui_test.cpp +utils_cmdline_ui_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_ui_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/cmdline/base_command.cpp b/utils/cmdline/base_command.cpp new file mode 100644 index 000000000000..837ded9cffab --- /dev/null +++ b/utils/cmdline/base_command.cpp @@ -0,0 +1,201 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/base_command.hpp" + +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + + +/// Creates a new command. +/// +/// \param name_ The name of the command. Must be unique within the context of +/// a program and have no spaces. +/// \param arg_list_ A textual description of the arguments received by the +/// command. May be empty. +/// \param min_args_ The minimum number of arguments required by the command. +/// \param max_args_ The maximum number of arguments required by the command. +/// -1 means infinity. +/// \param short_description_ A description of the purpose of the command. +cmdline::command_proto::command_proto(const std::string& name_, + const std::string& arg_list_, + const int min_args_, + const int max_args_, + const std::string& short_description_) : + _name(name_), + _arg_list(arg_list_), + _min_args(min_args_), + _max_args(max_args_), + _short_description(short_description_) +{ + PRE(name_.find(' ') == std::string::npos); + PRE(max_args_ == -1 || min_args_ <= max_args_); +} + + +/// Destructor for a command. +cmdline::command_proto::~command_proto(void) +{ + for (options_vector::const_iterator iter = _options.begin(); + iter != _options.end(); iter++) + delete *iter; +} + + +/// Internal method to register a dynamically-allocated option. +/// +/// Always use add_option() from subclasses to add options. +/// +/// \param option_ The option to add. Must have been dynamically allocated. +/// This grabs ownership of the pointer, which is released when the command +/// is destroyed. +void +cmdline::command_proto::add_option_ptr(const cmdline::base_option* option_) +{ + try { + _options.push_back(option_); + } catch (...) { + delete option_; + throw; + } +} + + +/// Processes the command line based on the command description. +/// +/// \param args The raw command line to be processed. +/// +/// \return An object containing the list of options and free arguments found in +/// args. +/// +/// \throw cmdline::usage_error If there is a problem processing the command +/// line. This error is caused by invalid input from the user. +cmdline::parsed_cmdline +cmdline::command_proto::parse_cmdline(const cmdline::args_vector& args) const +{ + PRE(name() == args[0]); + const parsed_cmdline cmdline = cmdline::parse(args, options()); + + const int argc = cmdline.arguments().size(); + if (argc < _min_args) + throw usage_error("Not enough arguments"); + if (_max_args != -1 && argc > _max_args) + throw usage_error("Too many arguments"); + + return cmdline; +} + + +/// Gets the name of the command. +/// +/// \return The command name. +const std::string& +cmdline::command_proto::name(void) const +{ + return _name; +} + + +/// Gets the textual representation of the arguments list. +/// +/// \return The description of the arguments list. +const std::string& +cmdline::command_proto::arg_list(void) const +{ + return _arg_list; +} + + +/// Gets the description of the purpose of the command. +/// +/// \return The description of the command. +const std::string& +cmdline::command_proto::short_description(void) const +{ + return _short_description; +} + + +/// Gets the definition of the options accepted by the command. +/// +/// \return The list of options. +const cmdline::options_vector& +cmdline::command_proto::options(void) const +{ + return _options; +} + + +/// Creates a new command. +/// +/// \param name_ The name of the command. Must be unique within the context of +/// a program and have no spaces. +/// \param arg_list_ A textual description of the arguments received by the +/// command. May be empty. +/// \param min_args_ The minimum number of arguments required by the command. +/// \param max_args_ The maximum number of arguments required by the command. +/// -1 means infinity. +/// \param short_description_ A description of the purpose of the command. +cmdline::base_command_no_data::base_command_no_data( + const std::string& name_, + const std::string& arg_list_, + const int min_args_, + const int max_args_, + const std::string& short_description_) : + command_proto(name_, arg_list_, min_args_, max_args_, short_description_) +{ +} + + +/// Entry point for the command. +/// +/// This delegates execution to the run() abstract function after the command +/// line provided in args has been parsed. +/// +/// If this function returns, the command is assumed to have been executed +/// successfully. Any error must be reported by means of exceptions. +/// +/// \param ui Object to interact with the I/O of the command. The command must +/// always use this object to write to stdout and stderr. +/// \param args The command line passed to the command broken by word, which +/// includes options and arguments. +/// +/// \return The exit code that the program has to return. 0 on success, some +/// other value on error. +/// \throw usage_error If args is invalid (i.e. if the options are mispecified +/// or if the arguments are invalid). +int +cmdline::base_command_no_data::main(cmdline::ui* ui, + const cmdline::args_vector& args) +{ + return run(ui, parse_cmdline(args)); +} diff --git a/utils/cmdline/base_command.hpp b/utils/cmdline/base_command.hpp new file mode 100644 index 000000000000..819dfe98dad3 --- /dev/null +++ b/utils/cmdline/base_command.hpp @@ -0,0 +1,162 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/base_command.hpp +/// Provides the utils::cmdline::base_command class. + +#if !defined(UTILS_CMDLINE_BASE_COMMAND_HPP) +#define UTILS_CMDLINE_BASE_COMMAND_HPP + +#include "utils/cmdline/base_command_fwd.hpp" + +#include <string> + +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace utils { +namespace cmdline { + + +/// Prototype class for the implementation of subcommands of a program. +/// +/// Use the subclasses of command_proto defined in this module instead of +/// command_proto itself as base classes for your application-specific +/// commands. +class command_proto : noncopyable { + /// The user-visible name of the command. + const std::string _name; + + /// Textual description of the command arguments. + const std::string _arg_list; + + /// The minimum number of required arguments. + const int _min_args; + + /// The maximum number of allowed arguments; -1 for infinity. + const int _max_args; + + /// A textual description of the command. + const std::string _short_description; + + /// Collection of command-specific options. + options_vector _options; + + void add_option_ptr(const base_option*); + +protected: + template< typename Option > void add_option(const Option&); + parsed_cmdline parse_cmdline(const args_vector&) const; + +public: + command_proto(const std::string&, const std::string&, const int, const int, + const std::string&); + virtual ~command_proto(void); + + const std::string& name(void) const; + const std::string& arg_list(void) const; + const std::string& short_description(void) const; + const options_vector& options(void) const; +}; + + +/// Unparametrized base subcommand for a program. +/// +/// Use this class to define subcommands for your program that do not need any +/// information passed in from the main command-line dispatcher other than the +/// command-line arguments. +class base_command_no_data : public command_proto { + /// Main code of the command. + /// + /// This is called from main() after the command line has been processed and + /// validated. + /// + /// \param ui Object to interact with the I/O of the command. The command + /// must always use this object to write to stdout and stderr. + /// \param cmdline The parsed command line, containing the values of any + /// given options and arguments. + /// + /// \return The exit code that the program has to return. 0 on success, + /// some other value on error. + /// + /// \throw std::runtime_error Any errors detected during the execution of + /// the command are reported by means of exceptions. + virtual int run(ui* ui, const parsed_cmdline& cmdline) = 0; + +public: + base_command_no_data(const std::string&, const std::string&, const int, + const int, const std::string&); + + int main(ui*, const args_vector&); +}; + + +/// Parametrized base subcommand for a program. +/// +/// Use this class to define subcommands for your program that need some kind of +/// runtime information passed in from the main command-line dispatcher. +/// +/// \param Data The type of the object passed to the subcommand at runtime. +/// This is useful, for example, to pass around the runtime configuration of the +/// program. +template< typename Data > +class base_command : public command_proto { + /// Main code of the command. + /// + /// This is called from main() after the command line has been processed and + /// validated. + /// + /// \param ui Object to interact with the I/O of the command. The command + /// must always use this object to write to stdout and stderr. + /// \param cmdline The parsed command line, containing the values of any + /// given options and arguments. + /// \param data An instance of the runtime data passed from main(). + /// + /// \return The exit code that the program has to return. 0 on success, + /// some other value on error. + /// + /// \throw std::runtime_error Any errors detected during the execution of + /// the command are reported by means of exceptions. + virtual int run(ui* ui, const parsed_cmdline& cmdline, + const Data& data) = 0; + +public: + base_command(const std::string&, const std::string&, const int, const int, + const std::string&); + + int main(ui*, const args_vector&, const Data&); +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_HPP) diff --git a/utils/cmdline/base_command.ipp b/utils/cmdline/base_command.ipp new file mode 100644 index 000000000000..5696637085d7 --- /dev/null +++ b/utils/cmdline/base_command.ipp @@ -0,0 +1,104 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_CMDLINE_BASE_COMMAND_IPP) +#define UTILS_CMDLINE_BASE_COMMAND_IPP + +#include "utils/cmdline/base_command.hpp" + + +namespace utils { +namespace cmdline { + + +/// Adds an option to the command. +/// +/// This is to be called from the constructor of the subclass that implements +/// the command. +/// +/// \param option_ The option to add. +template< typename Option > +void +command_proto::add_option(const Option& option_) +{ + add_option_ptr(new Option(option_)); +} + + +/// Creates a new command. +/// +/// \param name_ The name of the command. Must be unique within the context of +/// a program and have no spaces. +/// \param arg_list_ A textual description of the arguments received by the +/// command. May be empty. +/// \param min_args_ The minimum number of arguments required by the command. +/// \param max_args_ The maximum number of arguments required by the command. +/// -1 means infinity. +/// \param short_description_ A description of the purpose of the command. +template< typename Data > +base_command< Data >::base_command(const std::string& name_, + const std::string& arg_list_, + const int min_args_, + const int max_args_, + const std::string& short_description_) : + command_proto(name_, arg_list_, min_args_, max_args_, short_description_) +{ +} + + +/// Entry point for the command. +/// +/// This delegates execution to the run() abstract function after the command +/// line provided in args has been parsed. +/// +/// If this function returns, the command is assumed to have been executed +/// successfully. Any error must be reported by means of exceptions. +/// +/// \param ui Object to interact with the I/O of the command. The command must +/// always use this object to write to stdout and stderr. +/// \param args The command line passed to the command broken by word, which +/// includes options and arguments. +/// \param data An opaque data structure to pass to the run method. +/// +/// \return The exit code that the program has to return. 0 on success, some +/// other value on error. +/// \throw usage_error If args is invalid (i.e. if the options are mispecified +/// or if the arguments are invalid). +template< typename Data > +int +base_command< Data >::main(ui* ui, const args_vector& args, const Data& data) +{ + return run(ui, parse_cmdline(args), data); +} + + +} // namespace cli +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_IPP) diff --git a/utils/cmdline/base_command_fwd.hpp b/utils/cmdline/base_command_fwd.hpp new file mode 100644 index 000000000000..c94db1ae2d05 --- /dev/null +++ b/utils/cmdline/base_command_fwd.hpp @@ -0,0 +1,47 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/base_command_fwd.hpp +/// Forward declarations for utils/cmdline/base_command.hpp + +#if !defined(UTILS_CMDLINE_BASE_COMMAND_FWD_HPP) +#define UTILS_CMDLINE_BASE_COMMAND_FWD_HPP + +namespace utils { +namespace cmdline { + + +class command_proto; +class base_command_no_data; +template< typename > class base_command; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_FWD_HPP) diff --git a/utils/cmdline/base_command_test.cpp b/utils/cmdline/base_command_test.cpp new file mode 100644 index 000000000000..20df8ea49512 --- /dev/null +++ b/utils/cmdline/base_command_test.cpp @@ -0,0 +1,295 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/base_command.ipp" + +#include <atf-c++.hpp> + +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/defs.hpp" + +namespace cmdline = utils::cmdline; + + +namespace { + + +/// Mock command to test the cmdline::base_command base class. +/// +/// \param Data The type of the opaque data object passed to main(). +/// \param ExpectedData The value run() will expect to find in the Data object +/// passed to main(). +template< typename Data, Data ExpectedData > +class mock_cmd : public cmdline::base_command< Data > { +public: + /// Indicates if run() has been called already and executed correctly. + bool executed; + + /// Contains the argument of --the_string after run() is executed. + std::string optvalue; + + /// Constructs a new mock command. + mock_cmd(void) : + cmdline::base_command< Data >("mock", "arg1 [arg2 [arg3]]", 1, 3, + "Command for testing."), + executed(false) + { + this->add_option(cmdline::string_option("the_string", "Test option", + "arg")); + } + + /// Executes the command. + /// + /// \param cmdline Representation of the command line to the subcommand. + /// \param data Arbitrary data cookie passed to the command. + /// + /// \return A hardcoded number for testing purposes. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& cmdline, const Data& data) + { + if (cmdline.has_option("the_string")) + optvalue = cmdline.get_option< cmdline::string_option >( + "the_string"); + ATF_REQUIRE_EQ(ExpectedData, data); + executed = true; + return 1234; + } +}; + + +/// Mock command to test the cmdline::base_command_no_data base class. +class mock_cmd_no_data : public cmdline::base_command_no_data { +public: + /// Indicates if run() has been called already and executed correctly. + bool executed; + + /// Contains the argument of --the_string after run() is executed. + std::string optvalue; + + /// Constructs a new mock command. + mock_cmd_no_data(void) : + cmdline::base_command_no_data("mock", "arg1 [arg2 [arg3]]", 1, 3, + "Command for testing."), + executed(false) + { + add_option(cmdline::string_option("the_string", "Test option", "arg")); + } + + /// Executes the command. + /// + /// \param cmdline Representation of the command line to the subcommand. + /// + /// \return A hardcoded number for testing purposes. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& cmdline) + { + if (cmdline.has_option("the_string")) + optvalue = cmdline.get_option< cmdline::string_option >( + "the_string"); + executed = true; + return 1234; + } +}; + + +/// Implementation of a command to get access to parse_cmdline(). +class parse_cmdline_portal : public cmdline::command_proto { +public: + /// Constructs a new mock command. + parse_cmdline_portal(void) : + cmdline::command_proto("portal", "arg1 [arg2 [arg3]]", 1, 3, + "Command for testing.") + { + this->add_option(cmdline::string_option("the_string", "Test option", + "arg")); + } + + /// Delegator for the internal parse_cmdline() method. + /// + /// \param args The input arguments to be parsed. + /// + /// \return The parsed command line, split in options and arguments. + cmdline::parsed_cmdline + operator()(const cmdline::args_vector& args) const + { + return parse_cmdline(args); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(command_proto__parse_cmdline__ok); +ATF_TEST_CASE_BODY(command_proto__parse_cmdline__ok) +{ + cmdline::args_vector args; + args.push_back("portal"); + args.push_back("--the_string=foo bar"); + args.push_back("one arg"); + args.push_back("another arg"); + (void)parse_cmdline_portal()(args); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(command_proto__parse_cmdline__parse_fail); +ATF_TEST_CASE_BODY(command_proto__parse_cmdline__parse_fail) +{ + cmdline::args_vector args; + args.push_back("portal"); + args.push_back("--foo-bar"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Unknown.*foo-bar", + (void)parse_cmdline_portal()(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(command_proto__parse_cmdline__args_invalid); +ATF_TEST_CASE_BODY(command_proto__parse_cmdline__args_invalid) +{ + cmdline::args_vector args; + args.push_back("portal"); + + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Not enough arguments", + (void)parse_cmdline_portal()(args)); + + args.push_back("1"); + args.push_back("2"); + args.push_back("3"); + args.push_back("4"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + (void)parse_cmdline_portal()(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command__getters); +ATF_TEST_CASE_BODY(base_command__getters) +{ + mock_cmd< int, 584 > cmd; + ATF_REQUIRE_EQ("mock", cmd.name()); + ATF_REQUIRE_EQ("arg1 [arg2 [arg3]]", cmd.arg_list()); + ATF_REQUIRE_EQ("Command for testing.", cmd.short_description()); + ATF_REQUIRE_EQ(1, cmd.options().size()); + ATF_REQUIRE_EQ("the_string", cmd.options()[0]->long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command__main__ok) +ATF_TEST_CASE_BODY(base_command__main__ok) +{ + mock_cmd< int, 584 > cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--the_string=foo bar"); + args.push_back("one arg"); + args.push_back("another arg"); + ATF_REQUIRE_EQ(1234, cmd.main(&ui, args, 584)); + ATF_REQUIRE(cmd.executed); + ATF_REQUIRE_EQ("foo bar", cmd.optvalue); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command__main__parse_cmdline_fail) +ATF_TEST_CASE_BODY(base_command__main__parse_cmdline_fail) +{ + mock_cmd< int, 584 > cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--foo-bar"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Unknown.*foo-bar", + cmd.main(&ui, args, 584)); + ATF_REQUIRE(!cmd.executed); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command_no_data__getters); +ATF_TEST_CASE_BODY(base_command_no_data__getters) +{ + mock_cmd_no_data cmd; + ATF_REQUIRE_EQ("mock", cmd.name()); + ATF_REQUIRE_EQ("arg1 [arg2 [arg3]]", cmd.arg_list()); + ATF_REQUIRE_EQ("Command for testing.", cmd.short_description()); + ATF_REQUIRE_EQ(1, cmd.options().size()); + ATF_REQUIRE_EQ("the_string", cmd.options()[0]->long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command_no_data__main__ok) +ATF_TEST_CASE_BODY(base_command_no_data__main__ok) +{ + mock_cmd_no_data cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--the_string=foo bar"); + args.push_back("one arg"); + args.push_back("another arg"); + ATF_REQUIRE_EQ(1234, cmd.main(&ui, args)); + ATF_REQUIRE(cmd.executed); + ATF_REQUIRE_EQ("foo bar", cmd.optvalue); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command_no_data__main__parse_cmdline_fail) +ATF_TEST_CASE_BODY(base_command_no_data__main__parse_cmdline_fail) +{ + mock_cmd_no_data cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--foo-bar"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Unknown.*foo-bar", + cmd.main(&ui, args)); + ATF_REQUIRE(!cmd.executed); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, command_proto__parse_cmdline__ok); + ATF_ADD_TEST_CASE(tcs, command_proto__parse_cmdline__parse_fail); + ATF_ADD_TEST_CASE(tcs, command_proto__parse_cmdline__args_invalid); + + ATF_ADD_TEST_CASE(tcs, base_command__getters); + ATF_ADD_TEST_CASE(tcs, base_command__main__ok); + ATF_ADD_TEST_CASE(tcs, base_command__main__parse_cmdline_fail); + + ATF_ADD_TEST_CASE(tcs, base_command_no_data__getters); + ATF_ADD_TEST_CASE(tcs, base_command_no_data__main__ok); + ATF_ADD_TEST_CASE(tcs, base_command_no_data__main__parse_cmdline_fail); +} diff --git a/utils/cmdline/commands_map.hpp b/utils/cmdline/commands_map.hpp new file mode 100644 index 000000000000..5378a6f2c471 --- /dev/null +++ b/utils/cmdline/commands_map.hpp @@ -0,0 +1,96 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/commands_map.hpp +/// Maintains a collection of dynamically-instantiated commands. +/// +/// Commands need to be dynamically-instantiated because they are often +/// complex data structures. Instantiating them as static variables causes +/// problems with the order of construction of globals. The commands_map class +/// provided by this module provides a mechanism to maintain these instantiated +/// objects. + +#if !defined(UTILS_CMDLINE_COMMANDS_MAP_HPP) +#define UTILS_CMDLINE_COMMANDS_MAP_HPP + +#include "utils/cmdline/commands_map_fwd.hpp" + +#include <map> +#include <memory> +#include <set> +#include <string> + +#include "utils/noncopyable.hpp" + + +namespace utils { +namespace cmdline { + + +/// Collection of dynamically-instantiated commands. +template< typename BaseCommand > +class commands_map : noncopyable { + /// Map of command names to their implementations. + typedef std::map< std::string, BaseCommand* > impl_map; + + /// Map of category names to the command names they contain. + typedef std::map< std::string, std::set< std::string > > categories_map; + + /// Collection of all available commands. + impl_map _commands; + + /// Collection of defined categories and their commands. + categories_map _categories; + +public: + commands_map(void); + ~commands_map(void); + + /// Scoped, strictly-owned pointer to a command from this map. + typedef typename std::auto_ptr< BaseCommand > command_ptr; + void insert(command_ptr, const std::string& = ""); + void insert(BaseCommand*, const std::string& = ""); + + /// Type for a constant iterator. + typedef typename categories_map::const_iterator const_iterator; + + bool empty(void) const; + + const_iterator begin(void) const; + const_iterator end(void) const; + + BaseCommand* find(const std::string&); + const BaseCommand* find(const std::string&) const; +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_HPP) diff --git a/utils/cmdline/commands_map.ipp b/utils/cmdline/commands_map.ipp new file mode 100644 index 000000000000..8be87ab3b5cc --- /dev/null +++ b/utils/cmdline/commands_map.ipp @@ -0,0 +1,161 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/commands_map.hpp" +#include "utils/sanity.hpp" + + +namespace utils { + + +/// Constructs an empty set of commands. +template< typename BaseCommand > +cmdline::commands_map< BaseCommand >::commands_map(void) +{ +} + + +/// Destroys a set of commands. +/// +/// This releases the dynamically-instantiated objects. +template< typename BaseCommand > +cmdline::commands_map< BaseCommand >::~commands_map(void) +{ + for (typename impl_map::iterator iter = _commands.begin(); + iter != _commands.end(); iter++) + delete (*iter).second; +} + + +/// Inserts a new command into the map. +/// +/// \param command The command to insert. This must have been dynamically +/// allocated with new. The call grabs ownership of the command, or the +/// command is freed if the call fails. +/// \param category The category this command belongs to. Defaults to the empty +/// string, which indicates that the command has not be categorized. +template< typename BaseCommand > +void +cmdline::commands_map< BaseCommand >::insert(command_ptr command, + const std::string& category) +{ + INV(_commands.find(command->name()) == _commands.end()); + BaseCommand* ptr = command.release(); + INV(ptr != NULL); + _commands[ptr->name()] = ptr; + _categories[category].insert(ptr->name()); +} + + +/// Inserts a new command into the map. +/// +/// This grabs ownership of the pointer, so it is ONLY safe to use with the +/// following idiom: insert(new foo()). +/// +/// \param command The command to insert. This must have been dynamically +/// allocated with new. The call grabs ownership of the command, or the +/// command is freed if the call fails. +/// \param category The category this command belongs to. Defaults to the empty +/// string, which indicates that the command has not be categorized. +template< typename BaseCommand > +void +cmdline::commands_map< BaseCommand >::insert(BaseCommand* command, + const std::string& category) +{ + insert(command_ptr(command), category); +} + + +/// Checks whether the list of commands is empty. +/// +/// \return True if there are no commands in this map. +template< typename BaseCommand > +bool +cmdline::commands_map< BaseCommand >::empty(void) const +{ + return _commands.empty(); +} + + +/// Returns a constant iterator to the beginning of the categories mapping. +/// +/// \return A map (string -> BaseCommand*) iterator. +template< typename BaseCommand > +typename cmdline::commands_map< BaseCommand >::const_iterator +cmdline::commands_map< BaseCommand >::begin(void) const +{ + return _categories.begin(); +} + + +/// Returns a constant iterator to the end of the categories mapping. +/// +/// \return A map (string -> BaseCommand*) iterator. +template< typename BaseCommand > +typename cmdline::commands_map< BaseCommand >::const_iterator +cmdline::commands_map< BaseCommand >::end(void) const +{ + return _categories.end(); +} + + +/// Finds a command by name; mutable version. +/// +/// \param name The name of the command to locate. +/// +/// \return The command itself or NULL if it does not exist. +template< typename BaseCommand > +BaseCommand* +cmdline::commands_map< BaseCommand >::find(const std::string& name) +{ + typename impl_map::iterator iter = _commands.find(name); + if (iter == _commands.end()) + return NULL; + else + return (*iter).second; +} + + +/// Finds a command by name; constant version. +/// +/// \param name The name of the command to locate. +/// +/// \return The command itself or NULL if it does not exist. +template< typename BaseCommand > +const BaseCommand* +cmdline::commands_map< BaseCommand >::find(const std::string& name) const +{ + typename impl_map::const_iterator iter = _commands.find(name); + if (iter == _commands.end()) + return NULL; + else + return (*iter).second; +} + + +} // namespace utils diff --git a/utils/cmdline/commands_map_fwd.hpp b/utils/cmdline/commands_map_fwd.hpp new file mode 100644 index 000000000000..a81a852790da --- /dev/null +++ b/utils/cmdline/commands_map_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/commands_map_fwd.hpp +/// Forward declarations for utils/cmdline/commands_map.hpp + +#if !defined(UTILS_CMDLINE_COMMANDS_MAP_FWD_HPP) +#define UTILS_CMDLINE_COMMANDS_MAP_FWD_HPP + +namespace utils { +namespace cmdline { + + +template< typename > class commands_map; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_COMMANDS_MAP_FWD_HPP) diff --git a/utils/cmdline/commands_map_test.cpp b/utils/cmdline/commands_map_test.cpp new file mode 100644 index 000000000000..47a7404f64fb --- /dev/null +++ b/utils/cmdline/commands_map_test.cpp @@ -0,0 +1,140 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/commands_map.ipp" + +#include <atf-c++.hpp> + +#include "utils/cmdline/base_command.hpp" +#include "utils/defs.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + + +namespace { + + +/// Fake command to validate the behavior of commands_map. +/// +/// Note that this command does not do anything. It is only intended to provide +/// a specific class that can be inserted into commands_map instances and check +/// that it can be located properly. +class mock_cmd : public cmdline::base_command_no_data { +public: + /// Constructor for the mock command. + /// + /// \param mock_name The name of the command. All other settings are set to + /// irrelevant values. + mock_cmd(const char* mock_name) : + cmdline::base_command_no_data(mock_name, "", 0, 0, + "Command for testing.") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */) + { + UNREACHABLE; + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(empty); +ATF_TEST_CASE_BODY(empty) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + ATF_REQUIRE(commands.empty()); + ATF_REQUIRE(commands.begin() == commands.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some); +ATF_TEST_CASE_BODY(some) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + cmdline::base_command_no_data* cmd1 = new mock_cmd("cmd1"); + commands.insert(cmd1); + cmdline::base_command_no_data* cmd2 = new mock_cmd("cmd2"); + commands.insert(cmd2, "foo"); + + ATF_REQUIRE(!commands.empty()); + + cmdline::commands_map< cmdline::base_command_no_data >::const_iterator + iter = commands.begin(); + ATF_REQUIRE_EQ("", (*iter).first); + ATF_REQUIRE_EQ(1, (*iter).second.size()); + ATF_REQUIRE_EQ("cmd1", *(*iter).second.begin()); + + ++iter; + ATF_REQUIRE_EQ("foo", (*iter).first); + ATF_REQUIRE_EQ(1, (*iter).second.size()); + ATF_REQUIRE_EQ("cmd2", *(*iter).second.begin()); + + ATF_REQUIRE(++iter == commands.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__match); +ATF_TEST_CASE_BODY(find__match) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + cmdline::base_command_no_data* cmd1 = new mock_cmd("cmd1"); + commands.insert(cmd1); + cmdline::base_command_no_data* cmd2 = new mock_cmd("cmd2"); + commands.insert(cmd2); + + ATF_REQUIRE(cmd1 == commands.find("cmd1")); + ATF_REQUIRE(cmd2 == commands.find("cmd2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__nomatch); +ATF_TEST_CASE_BODY(find__nomatch) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + commands.insert(new mock_cmd("cmd1")); + + ATF_REQUIRE(NULL == commands.find("cmd2")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, empty); + ATF_ADD_TEST_CASE(tcs, some); + ATF_ADD_TEST_CASE(tcs, find__match); + ATF_ADD_TEST_CASE(tcs, find__nomatch); +} diff --git a/utils/cmdline/exceptions.cpp b/utils/cmdline/exceptions.cpp new file mode 100644 index 000000000000..fa9ba2218a7f --- /dev/null +++ b/utils/cmdline/exceptions.cpp @@ -0,0 +1,175 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/exceptions.hpp" + +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + + +#define VALIDATE_OPTION_NAME(option) PRE_MSG( \ + (option.length() == 2 && (option[0] == '-' && option[1] != '-')) || \ + (option.length() > 2 && (option[0] == '-' && option[1] == '-')), \ + F("The option name %s must be fully specified") % option); + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +cmdline::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +cmdline::error::~error(void) throw() +{ +} + + +/// Constructs a new usage_error. +/// +/// \param message The reason behind the usage error. +cmdline::usage_error::usage_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +cmdline::usage_error::~usage_error(void) throw() +{ +} + + +/// Constructs a new missing_option_argument_error. +/// +/// \param option_ The option for which no argument was provided. The option +/// name must be fully specified (with - or -- in front). +cmdline::missing_option_argument_error::missing_option_argument_error( + const std::string& option_) : + usage_error(F("Missing required argument for option %s") % option_), + _option(option_) +{ + VALIDATE_OPTION_NAME(option_); +} + + +/// Destructor for the error. +cmdline::missing_option_argument_error::~missing_option_argument_error(void) + throw() +{ +} + + +/// Returns the option name for which no argument was provided. +/// +/// \return The option name. +const std::string& +cmdline::missing_option_argument_error::option(void) const +{ + return _option; +} + + +/// Constructs a new option_argument_value_error. +/// +/// \param option_ The option to which an invalid argument was passed. The +/// option name must be fully specified (with - or -- in front). +/// \param argument_ The invalid argument. +/// \param reason_ The reason describing why the argument is invalid. +cmdline::option_argument_value_error::option_argument_value_error( + const std::string& option_, const std::string& argument_, + const std::string& reason_) : + usage_error(F("Invalid argument '%s' for option %s: %s") % argument_ % + option_ % reason_), + _option(option_), + _argument(argument_), + _reason(reason_) +{ + VALIDATE_OPTION_NAME(option_); +} + + +/// Destructor for the error. +cmdline::option_argument_value_error::~option_argument_value_error(void) + throw() +{ +} + + +/// Returns the option to which the invalid argument was passed. +/// +/// \return The option name. +const std::string& +cmdline::option_argument_value_error::option(void) const +{ + return _option; +} + + +/// Returns the invalid argument value. +/// +/// \return The invalid argument. +const std::string& +cmdline::option_argument_value_error::argument(void) const +{ + return _argument; +} + + +/// Constructs a new unknown_option_error. +/// +/// \param option_ The unknown option. The option name must be fully specified +/// (with - or -- in front). +cmdline::unknown_option_error::unknown_option_error( + const std::string& option_) : + usage_error(F("Unknown option %s") % option_), + _option(option_) +{ + VALIDATE_OPTION_NAME(option_); +} + + +/// Destructor for the error. +cmdline::unknown_option_error::~unknown_option_error(void) throw() +{ +} + + +/// Returns the unknown option name. +/// +/// \return The unknown option. +const std::string& +cmdline::unknown_option_error::option(void) const +{ + return _option; +} diff --git a/utils/cmdline/exceptions.hpp b/utils/cmdline/exceptions.hpp new file mode 100644 index 000000000000..59f99e835ce1 --- /dev/null +++ b/utils/cmdline/exceptions.hpp @@ -0,0 +1,109 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/exceptions.hpp +/// Exception types raised by the cmdline module. + +#if !defined(UTILS_CMDLINE_EXCEPTIONS_HPP) +#define UTILS_CMDLINE_EXCEPTIONS_HPP + +#include <stdexcept> +#include <string> + +namespace utils { +namespace cmdline { + + +/// Base exception for cmdline errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Generic error to describe problems caused by the user. +class usage_error : public error { +public: + explicit usage_error(const std::string&); + ~usage_error(void) throw(); +}; + + +/// Error denoting that no argument was provided to an option that required one. +class missing_option_argument_error : public usage_error { + /// Name of the option for which no required argument was specified. + std::string _option; + +public: + explicit missing_option_argument_error(const std::string&); + ~missing_option_argument_error(void) throw(); + + const std::string& option(void) const; +}; + + +/// Error denoting that the argument provided to an option is invalid. +class option_argument_value_error : public usage_error { + /// Name of the option for which the argument was invalid. + std::string _option; + + /// Raw value of the invalid user-provided argument. + std::string _argument; + + /// Reason describing why the argument is invalid. + std::string _reason; + +public: + explicit option_argument_value_error(const std::string&, const std::string&, + const std::string&); + ~option_argument_value_error(void) throw(); + + const std::string& option(void) const; + const std::string& argument(void) const; +}; + + +/// Error denoting that the user specified an unknown option. +class unknown_option_error : public usage_error { + /// Name of the option that was not known. + std::string _option; + +public: + explicit unknown_option_error(const std::string&); + ~unknown_option_error(void) throw(); + + const std::string& option(void) const; +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_EXCEPTIONS_HPP) diff --git a/utils/cmdline/exceptions_test.cpp b/utils/cmdline/exceptions_test.cpp new file mode 100644 index 000000000000..b541e08f6995 --- /dev/null +++ b/utils/cmdline/exceptions_test.cpp @@ -0,0 +1,83 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/exceptions.hpp" + +#include <cstring> + +#include <atf-c++.hpp> + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const cmdline::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error); +ATF_TEST_CASE_BODY(missing_option_argument_error) +{ + const cmdline::missing_option_argument_error e("-o"); + ATF_REQUIRE(std::strcmp("Missing required argument for option -o", + e.what()) == 0); + ATF_REQUIRE_EQ("-o", e.option()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(option_argument_value_error); +ATF_TEST_CASE_BODY(option_argument_value_error) +{ + const cmdline::option_argument_value_error e("--the_option", "the value", + "the reason"); + ATF_REQUIRE(std::strcmp("Invalid argument 'the value' for option " + "--the_option: the reason", e.what()) == 0); + ATF_REQUIRE_EQ("--the_option", e.option()); + ATF_REQUIRE_EQ("the value", e.argument()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error); +ATF_TEST_CASE_BODY(unknown_option_error) +{ + const cmdline::unknown_option_error e("--foo"); + ATF_REQUIRE(std::strcmp("Unknown option --foo", e.what()) == 0); + ATF_REQUIRE_EQ("--foo", e.option()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error); + ATF_ADD_TEST_CASE(tcs, option_argument_value_error); + ATF_ADD_TEST_CASE(tcs, unknown_option_error); +} diff --git a/utils/cmdline/globals.cpp b/utils/cmdline/globals.cpp new file mode 100644 index 000000000000..76e0231fa36b --- /dev/null +++ b/utils/cmdline/globals.cpp @@ -0,0 +1,78 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/globals.hpp" + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + +namespace { + + +/// The name of the binary used to execute the program. +static std::string Progname; + + +} // anonymous namespace + + +/// Initializes the global state of the CLI. +/// +/// This function can only be called once during the execution of a program, +/// unless override_for_testing is set to true. +/// +/// \param argv0 The value of argv[0]; i.e. the program name. +/// \param override_for_testing Should always be set to false unless for tests +/// of this functionality, which may set this to true to redefine internal +/// state. +void +cmdline::init(const char* argv0, const bool override_for_testing) +{ + if (!override_for_testing) + PRE_MSG(Progname.empty(), "cmdline::init called more than once"); + Progname = utils::fs::path(argv0).leaf_name(); + LD(F("Program name: %s") % Progname); + POST(!Progname.empty()); +} + + +/// Gets the program name. +/// +/// \pre init() must have been called in advance. +/// +/// \return The program name. +const std::string& +cmdline::progname(void) +{ + PRE_MSG(!Progname.empty(), "cmdline::init not called yet"); + return Progname; +} diff --git a/utils/cmdline/globals.hpp b/utils/cmdline/globals.hpp new file mode 100644 index 000000000000..ab7904d69520 --- /dev/null +++ b/utils/cmdline/globals.hpp @@ -0,0 +1,48 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/globals.hpp +/// Representation of global, immutable state for a CLI. + +#if !defined(UTILS_CMDLINE_GLOBALS_HPP) +#define UTILS_CMDLINE_GLOBALS_HPP + +#include <string> + +namespace utils { +namespace cmdline { + + +void init(const char*, const bool = false); +const std::string& progname(void); + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_GLOBALS_HPP) diff --git a/utils/cmdline/globals_test.cpp b/utils/cmdline/globals_test.cpp new file mode 100644 index 000000000000..5c2ac7cc2d6c --- /dev/null +++ b/utils/cmdline/globals_test.cpp @@ -0,0 +1,77 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/globals.hpp" + +#include <atf-c++.hpp> + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__absolute); +ATF_TEST_CASE_BODY(progname__absolute) +{ + cmdline::init("/path/to/foobar"); + ATF_REQUIRE_EQ("foobar", cmdline::progname()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__relative); +ATF_TEST_CASE_BODY(progname__relative) +{ + cmdline::init("to/barbaz"); + ATF_REQUIRE_EQ("barbaz", cmdline::progname()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__plain); +ATF_TEST_CASE_BODY(progname__plain) +{ + cmdline::init("program"); + ATF_REQUIRE_EQ("program", cmdline::progname()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__override_for_testing); +ATF_TEST_CASE_BODY(progname__override_for_testing) +{ + cmdline::init("program"); + ATF_REQUIRE_EQ("program", cmdline::progname()); + + cmdline::init("foo", true); + ATF_REQUIRE_EQ("foo", cmdline::progname()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, progname__absolute); + ATF_ADD_TEST_CASE(tcs, progname__relative); + ATF_ADD_TEST_CASE(tcs, progname__plain); + ATF_ADD_TEST_CASE(tcs, progname__override_for_testing); +} diff --git a/utils/cmdline/options.cpp b/utils/cmdline/options.cpp new file mode 100644 index 000000000000..61736e31c11e --- /dev/null +++ b/utils/cmdline/options.cpp @@ -0,0 +1,605 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/options.hpp" + +#include <stdexcept> +#include <vector> + +#include "utils/cmdline/exceptions.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace cmdline = utils::cmdline; +namespace text = utils::text; + + +/// Constructs a generic option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ If not NULL, specifies that the option must receive an +/// argument and specifies the name of such argument for documentation +/// purposes. +/// \param default_value_ If not NULL, specifies that the option has a default +/// value for the mandatory argument. +cmdline::base_option::base_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + _short_name(short_name_), + _long_name(long_name_), + _description(description_), + _arg_name(arg_name_ == NULL ? "" : arg_name_), + _has_default_value(default_value_ != NULL), + _default_value(default_value_ == NULL ? "" : default_value_) +{ + INV(short_name_ != '\0'); +} + + +/// Constructs a generic option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ If not NULL, specifies that the option must receive an +/// argument and specifies the name of such argument for documentation +/// purposes. +/// \param default_value_ If not NULL, specifies that the option has a default +/// value for the mandatory argument. +cmdline::base_option::base_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + _short_name('\0'), + _long_name(long_name_), + _description(description_), + _arg_name(arg_name_ == NULL ? "" : arg_name_), + _has_default_value(default_value_ != NULL), + _default_value(default_value_ == NULL ? "" : default_value_) +{ +} + + +/// Destructor for the option. +cmdline::base_option::~base_option(void) +{ +} + + +/// Checks whether the option has a short name or not. +/// +/// \return True if the option has a short name, false otherwise. +bool +cmdline::base_option::has_short_name(void) const +{ + return _short_name != '\0'; +} + + +/// Returns the short name of the option. +/// +/// \pre has_short_name() must be true. +/// +/// \return The short name. +char +cmdline::base_option::short_name(void) const +{ + PRE(has_short_name()); + return _short_name; +} + + +/// Returns the long name of the option. +/// +/// \return The long name. +const std::string& +cmdline::base_option::long_name(void) const +{ + return _long_name; +} + + +/// Returns the description of the option. +/// +/// \return The description. +const std::string& +cmdline::base_option::description(void) const +{ + return _description; +} + + +/// Checks whether the option needs an argument or not. +/// +/// \return True if the option needs an argument, false otherwise. +bool +cmdline::base_option::needs_arg(void) const +{ + return !_arg_name.empty(); +} + + +/// Returns the argument name of the option for documentation purposes. +/// +/// \pre needs_arg() must be true. +/// +/// \return The argument name. +const std::string& +cmdline::base_option::arg_name(void) const +{ + INV(needs_arg()); + return _arg_name; +} + + +/// Checks whether the option has a default value for its argument. +/// +/// \pre needs_arg() must be true. +/// +/// \return True if the option has a default value, false otherwise. +bool +cmdline::base_option::has_default_value(void) const +{ + PRE(needs_arg()); + return _has_default_value; +} + + +/// Returns the default value for the argument to the option. +/// +/// \pre has_default_value() must be true. +/// +/// \return The default value. +const std::string& +cmdline::base_option::default_value(void) const +{ + INV(has_default_value()); + return _default_value;; +} + + +/// Formats the short name of the option for documentation purposes. +/// +/// \return A string describing the option's short name. +std::string +cmdline::base_option::format_short_name(void) const +{ + PRE(has_short_name()); + + if (needs_arg()) { + return F("-%s %s") % short_name() % arg_name(); + } else { + return F("-%s") % short_name(); + } +} + + +/// Formats the long name of the option for documentation purposes. +/// +/// \return A string describing the option's long name. +std::string +cmdline::base_option::format_long_name(void) const +{ + if (needs_arg()) { + return F("--%s=%s") % long_name() % arg_name(); + } else { + return F("--%s") % long_name(); + } +} + + + +/// Ensures that an argument passed to the option is valid. +/// +/// This must be reimplemented by subclasses that describe options with +/// arguments. +/// +/// \throw cmdline::option_argument_value_error Subclasses must raise this +/// exception to indicate the cases in which str is invalid. +void +cmdline::base_option::validate(const std::string& /* str */) const +{ + UNREACHABLE_MSG("Option does not support an argument"); +} + + +/// Constructs a boolean option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +cmdline::bool_option::bool_option(const char short_name_, + const char* long_name_, + const char* description_) : + base_option(short_name_, long_name_, description_) +{ +} + + +/// Constructs a boolean option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +cmdline::bool_option::bool_option(const char* long_name_, + const char* description_) : + base_option(long_name_, description_) +{ +} + + +/// Constructs an integer option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::int_option::int_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs an integer option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::int_option::int_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Ensures that an integer argument passed to the int_option is valid. +/// +/// \param raw_value The argument representing an integer as provided by the +/// user. +/// +/// \throw cmdline::option_argument_value_error If the integer provided in +/// raw_value is invalid. +void +cmdline::int_option::validate(const std::string& raw_value) const +{ + try { + (void)text::to_type< int >(raw_value); + } catch (const std::runtime_error& e) { + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, "Not a valid integer"); + } +} + + +/// Converts an integer argument to a native integer. +/// +/// \param raw_value The argument representing an integer as provided by the +/// user. +/// +/// \return The integer. +/// +/// \pre validate(raw_value) must be true. +int +cmdline::int_option::convert(const std::string& raw_value) +{ + try { + return text::to_type< int >(raw_value); + } catch (const std::runtime_error& e) { + PRE_MSG(false, F("Raw value '%s' for int option not properly " + "validated: %s") % raw_value % e.what()); + } +} + + +/// Constructs a list option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::list_option::list_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs a list option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::list_option::list_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Ensures that a lisstring argument passed to the list_option is valid. +void +cmdline::list_option::validate( + const std::string& /* raw_value */) const +{ + // Any list is potentially valid; the caller must check for semantics. +} + + +/// Converts a string argument to a vector. +/// +/// \param raw_value The argument representing a list as provided by the user. +/// +/// \return The list. +/// +/// \pre validate(raw_value) must be true. +cmdline::list_option::option_type +cmdline::list_option::convert(const std::string& raw_value) +{ + try { + return text::split(raw_value, ','); + } catch (const std::runtime_error& e) { + PRE_MSG(false, F("Raw value '%s' for list option not properly " + "validated: %s") % raw_value % e.what()); + } +} + + +/// Constructs a path option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::path_option::path_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs a path option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::path_option::path_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Ensures that a path argument passed to the path_option is valid. +/// +/// \param raw_value The argument representing a path as provided by the user. +/// +/// \throw cmdline::option_argument_value_error If the path provided in +/// raw_value is invalid. +void +cmdline::path_option::validate(const std::string& raw_value) const +{ + try { + (void)utils::fs::path(raw_value); + } catch (const utils::fs::error& e) { + throw cmdline::option_argument_value_error(F("--%s") % long_name(), + raw_value, e.what()); + } +} + + +/// Converts a path argument to a utils::fs::path. +/// +/// \param raw_value The argument representing a path as provided by the user. +/// +/// \return The path. +/// +/// \pre validate(raw_value) must be true. +utils::fs::path +cmdline::path_option::convert(const std::string& raw_value) +{ + try { + return utils::fs::path(raw_value); + } catch (const std::runtime_error& e) { + PRE_MSG(false, F("Raw value '%s' for path option not properly " + "validated: %s") % raw_value % e.what()); + } +} + + +/// Constructs a property option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. Must include the '=' delimiter. +cmdline::property_option::property_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_) : + base_option(short_name_, long_name_, description_, arg_name_) +{ + PRE(arg_name().find('=') != std::string::npos); +} + + +/// Constructs a property option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. Must include the '=' delimiter. +cmdline::property_option::property_option(const char* long_name_, + const char* description_, + const char* arg_name_) : + base_option(long_name_, description_, arg_name_) +{ + PRE(arg_name().find('=') != std::string::npos); +} + + +/// Validates the argument to a property option. +/// +/// \param raw_value The argument provided by the user. +void +cmdline::property_option::validate(const std::string& raw_value) const +{ + const std::string::size_type pos = raw_value.find('='); + if (pos == std::string::npos) + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, + F("Argument does not have the form '%s'") % arg_name()); + + const std::string key = raw_value.substr(0, pos); + if (key.empty()) + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, "Empty property name"); + + const std::string value = raw_value.substr(pos + 1); + if (value.empty()) + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, "Empty value"); +} + + +/// Returns the property option in a key/value pair form. +/// +/// \param raw_value The argument provided by the user. +/// +/// \return raw_value The key/value pair representation of the property. +/// +/// \pre validate(raw_value) must be true. +cmdline::property_option::option_type +cmdline::property_option::convert(const std::string& raw_value) +{ + const std::string::size_type pos = raw_value.find('='); + return std::make_pair(raw_value.substr(0, pos), raw_value.substr(pos + 1)); +} + + +/// Constructs a string option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::string_option::string_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs a string option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::string_option::string_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Does nothing; all string values are valid arguments to a string_option. +void +cmdline::string_option::validate( + const std::string& /* raw_value */) const +{ + // Nothing to do. +} + + +/// Returns the string unmodified. +/// +/// \param raw_value The argument provided by the user. +/// +/// \return raw_value +/// +/// \pre validate(raw_value) must be true. +std::string +cmdline::string_option::convert(const std::string& raw_value) +{ + return raw_value; +} diff --git a/utils/cmdline/options.hpp b/utils/cmdline/options.hpp new file mode 100644 index 000000000000..f3a83889e491 --- /dev/null +++ b/utils/cmdline/options.hpp @@ -0,0 +1,237 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/options.hpp +/// Definitions of command-line options. + +#if !defined(UTILS_CMDLINE_OPTIONS_HPP) +#define UTILS_CMDLINE_OPTIONS_HPP + +#include "utils/cmdline/options_fwd.hpp" + +#include <string> +#include <utility> +#include <vector> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace cmdline { + + +/// Type-less base option class. +/// +/// This abstract class provides the most generic representation of options. It +/// allows defining options with both short and long names, with and without +/// arguments and with and without optional values. These are all the possible +/// combinations supported by the getopt_long(3) function, on which this is +/// built. +/// +/// The internal values (e.g. the default value) of a generic option are all +/// represented as strings. However, from the caller's perspective, this is +/// suboptimal. Hence why this class must be specialized: the subclasses +/// provide type-specific accessors and provide automatic validation of the +/// types (e.g. a string '3foo' is not passed to an integer option). +/// +/// Given that subclasses are used through templatized code, they must provide: +/// +/// <ul> +/// <li>A public option_type typedef that defines the type of the +/// option.</li> +/// +/// <li>A convert() method that takes a string and converts it to +/// option_type. The string can be assumed to be convertible to the +/// destination type. Should not raise exceptions.</li> +/// +/// <li>A validate() method that matches the implementation of convert(). +/// This method can throw option_argument_value_error if the string cannot +/// be converted appropriately. If validate() does not throw, then +/// convert() must execute successfully.</li> +/// </ul> +/// +/// TODO(jmmv): Many methods in this class are split into two parts: has_foo() +/// and foo(), the former to query if the foo is available and the latter to get +/// the foo. It'd be very nice if we'd use something similar Boost.Optional to +/// simplify this interface altogether. +class base_option { + /// Short name of the option; 0 to indicate that none is available. + char _short_name; + + /// Long name of the option. + std::string _long_name; + + /// Textual description of the purpose of the option. + std::string _description; + + /// Descriptive name of the required argument; empty if not allowed. + std::string _arg_name; + + /// Whether the option has a default value or not. + /// + /// \todo We should probably be using the optional class here. + bool _has_default_value; + + /// If _has_default_value is true, the default value. + std::string _default_value; + +public: + base_option(const char, const char*, const char*, const char* = NULL, + const char* = NULL); + base_option(const char*, const char*, const char* = NULL, + const char* = NULL); + virtual ~base_option(void); + + bool has_short_name(void) const; + char short_name(void) const; + const std::string& long_name(void) const; + const std::string& description(void) const; + + bool needs_arg(void) const; + const std::string& arg_name(void) const; + + bool has_default_value(void) const; + const std::string& default_value(void) const; + + std::string format_short_name(void) const; + std::string format_long_name(void) const; + + virtual void validate(const std::string&) const; +}; + + +/// Definition of a boolean option. +/// +/// A boolean option can be specified once in the command line, at which point +/// is set to true. Such an option cannot carry optional arguments. +class bool_option : public base_option { +public: + bool_option(const char, const char*, const char*); + bool_option(const char*, const char*); + virtual ~bool_option(void) {} + + /// The data type of this option. + typedef bool option_type; +}; + + +/// Definition of an integer option. +class int_option : public base_option { +public: + int_option(const char, const char*, const char*, const char*, + const char* = NULL); + int_option(const char*, const char*, const char*, const char* = NULL); + virtual ~int_option(void) {} + + /// The data type of this option. + typedef int option_type; + + virtual void validate(const std::string& str) const; + static int convert(const std::string& str); +}; + + +/// Definition of a comma-separated list of strings. +class list_option : public base_option { +public: + list_option(const char, const char*, const char*, const char*, + const char* = NULL); + list_option(const char*, const char*, const char*, const char* = NULL); + virtual ~list_option(void) {} + + /// The data type of this option. + typedef std::vector< std::string > option_type; + + virtual void validate(const std::string&) const; + static option_type convert(const std::string&); +}; + + +/// Definition of an option representing a path. +/// +/// The path pointed to by the option may not exist, but it must be +/// syntactically valid. +class path_option : public base_option { +public: + path_option(const char, const char*, const char*, const char*, + const char* = NULL); + path_option(const char*, const char*, const char*, const char* = NULL); + virtual ~path_option(void) {} + + /// The data type of this option. + typedef utils::fs::path option_type; + + virtual void validate(const std::string&) const; + static utils::fs::path convert(const std::string&); +}; + + +/// Definition of a property option. +/// +/// A property option is an option whose required arguments are of the form +/// 'name=value'. Both components of the property are treated as free-form +/// non-empty strings; any other validation must happen on the caller side. +/// +/// \todo Would be nice if the delimiter was parametrizable. With the current +/// parser interface (convert() being a static method), the only way to do +/// this would be to templatize this class. +class property_option : public base_option { +public: + property_option(const char, const char*, const char*, const char*); + property_option(const char*, const char*, const char*); + virtual ~property_option(void) {} + + /// The data type of this option. + typedef std::pair< std::string, std::string > option_type; + + virtual void validate(const std::string& str) const; + static option_type convert(const std::string& str); +}; + + +/// Definition of a free-form string option. +/// +/// This class provides no restrictions on the argument passed to the option. +class string_option : public base_option { +public: + string_option(const char, const char*, const char*, const char*, + const char* = NULL); + string_option(const char*, const char*, const char*, const char* = NULL); + virtual ~string_option(void) {} + + /// The data type of this option. + typedef std::string option_type; + + virtual void validate(const std::string& str) const; + static std::string convert(const std::string& str); +}; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_OPTIONS_HPP) diff --git a/utils/cmdline/options_fwd.hpp b/utils/cmdline/options_fwd.hpp new file mode 100644 index 000000000000..8b45797e3920 --- /dev/null +++ b/utils/cmdline/options_fwd.hpp @@ -0,0 +1,51 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/options_fwd.hpp +/// Forward declarations for utils/cmdline/options.hpp + +#if !defined(UTILS_CMDLINE_OPTIONS_FWD_HPP) +#define UTILS_CMDLINE_OPTIONS_FWD_HPP + +namespace utils { +namespace cmdline { + + +class base_option; +class bool_option; +class int_option; +class list_option; +class path_option; +class property_option; +class string_option; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_OPTIONS_FWD_HPP) diff --git a/utils/cmdline/options_test.cpp b/utils/cmdline/options_test.cpp new file mode 100644 index 000000000000..82fd706a191a --- /dev/null +++ b/utils/cmdline/options_test.cpp @@ -0,0 +1,526 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/options.hpp" + +#include <atf-c++.hpp> + +#include "utils/cmdline/exceptions.hpp" +#include "utils/defs.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; + +namespace { + + +/// Simple string-based option type for testing purposes. +class mock_option : public cmdline::base_option { +public: + /// Constructs a mock option with a short name and a long name. + /// + /// + /// \param short_name_ The short name for the option. + /// \param long_name_ The long name for the option. + /// \param description_ A user-friendly description for the option. + /// \param arg_name_ If not NULL, specifies that the option must receive an + /// argument and specifies the name of such argument for documentation + /// purposes. + /// \param default_value_ If not NULL, specifies that the option has a + /// default value for the mandatory argument. + mock_option(const char short_name_, const char* long_name_, + const char* description_, const char* arg_name_ = NULL, + const char* default_value_ = NULL) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) {} + + /// Constructs a mock option with a long name only. + /// + /// \param long_name_ The long name for the option. + /// \param description_ A user-friendly description for the option. + /// \param arg_name_ If not NULL, specifies that the option must receive an + /// argument and specifies the name of such argument for documentation + /// purposes. + /// \param default_value_ If not NULL, specifies that the option has a + /// default value for the mandatory argument. + mock_option(const char* long_name_, + const char* description_, const char* arg_name_ = NULL, + const char* default_value_ = NULL) : + base_option(long_name_, description_, arg_name_, default_value_) {} + + /// The data type of this option. + typedef std::string option_type; + + /// Ensures that the argument passed to the option is valid. + /// + /// In this particular mock option, this does not perform any validation. + void + validate(const std::string& /* str */) const + { + // Do nothing. + } + + /// Returns the input parameter without any conversion. + /// + /// \param str The user-provided argument to the option. + /// + /// \return The same value as provided by the user without conversion. + static std::string + convert(const std::string& str) + { + return str; + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__short_name__no_arg); +ATF_TEST_CASE_BODY(base_option__short_name__no_arg) +{ + const mock_option o('f', "force", "Force execution"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('f', o.short_name()); + ATF_REQUIRE_EQ("force", o.long_name()); + ATF_REQUIRE_EQ("Force execution", o.description()); + ATF_REQUIRE(!o.needs_arg()); + ATF_REQUIRE_EQ("-f", o.format_short_name()); + ATF_REQUIRE_EQ("--force", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__short_name__with_arg__no_default); +ATF_TEST_CASE_BODY(base_option__short_name__with_arg__no_default) +{ + const mock_option o('c', "conf_file", "Configuration file", "path"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('c', o.short_name()); + ATF_REQUIRE_EQ("conf_file", o.long_name()); + ATF_REQUIRE_EQ("Configuration file", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("path", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); + ATF_REQUIRE_EQ("-c path", o.format_short_name()); + ATF_REQUIRE_EQ("--conf_file=path", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__short_name__with_arg__with_default); +ATF_TEST_CASE_BODY(base_option__short_name__with_arg__with_default) +{ + const mock_option o('c', "conf_file", "Configuration file", "path", + "defpath"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('c', o.short_name()); + ATF_REQUIRE_EQ("conf_file", o.long_name()); + ATF_REQUIRE_EQ("Configuration file", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("path", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("defpath", o.default_value()); + ATF_REQUIRE_EQ("-c path", o.format_short_name()); + ATF_REQUIRE_EQ("--conf_file=path", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__long_name__no_arg); +ATF_TEST_CASE_BODY(base_option__long_name__no_arg) +{ + const mock_option o("dryrun", "Dry run mode"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("dryrun", o.long_name()); + ATF_REQUIRE_EQ("Dry run mode", o.description()); + ATF_REQUIRE(!o.needs_arg()); + ATF_REQUIRE_EQ("--dryrun", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__long_name__with_arg__no_default); +ATF_TEST_CASE_BODY(base_option__long_name__with_arg__no_default) +{ + const mock_option o("helper", "Path to helper", "path"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("helper", o.long_name()); + ATF_REQUIRE_EQ("Path to helper", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("path", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); + ATF_REQUIRE_EQ("--helper=path", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__long_name__with_arg__with_default); +ATF_TEST_CASE_BODY(base_option__long_name__with_arg__with_default) +{ + const mock_option o("executable", "Executable name", "file", "foo"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("executable", o.long_name()); + ATF_REQUIRE_EQ("Executable name", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("file", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("foo", o.default_value()); + ATF_REQUIRE_EQ("--executable=file", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_option__short_name); +ATF_TEST_CASE_BODY(bool_option__short_name) +{ + const cmdline::bool_option o('f', "force", "Force execution"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('f', o.short_name()); + ATF_REQUIRE_EQ("force", o.long_name()); + ATF_REQUIRE_EQ("Force execution", o.description()); + ATF_REQUIRE(!o.needs_arg()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_option__long_name); +ATF_TEST_CASE_BODY(bool_option__long_name) +{ + const cmdline::bool_option o("force", "Force execution"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("force", o.long_name()); + ATF_REQUIRE_EQ("Force execution", o.description()); + ATF_REQUIRE(!o.needs_arg()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_option__short_name); +ATF_TEST_CASE_BODY(int_option__short_name) +{ + const cmdline::int_option o('p', "int", "The int", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("int", o.long_name()); + ATF_REQUIRE_EQ("The int", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_option__long_name); +ATF_TEST_CASE_BODY(int_option__long_name) +{ + const cmdline::int_option o("int", "The int", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("int", o.long_name()); + ATF_REQUIRE_EQ("The int", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_option__type); +ATF_TEST_CASE_BODY(int_option__type) +{ + const cmdline::int_option o("int", "The int", "arg"); + + o.validate("123"); + ATF_REQUIRE_EQ(123, cmdline::int_option::convert("123")); + + o.validate("-567"); + ATF_REQUIRE_EQ(-567, cmdline::int_option::convert("-567")); + + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("5a")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("a5")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("5 a")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("5.0")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_option__short_name); +ATF_TEST_CASE_BODY(list_option__short_name) +{ + const cmdline::list_option o('p', "list", "The list", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("list", o.long_name()); + ATF_REQUIRE_EQ("The list", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_option__long_name); +ATF_TEST_CASE_BODY(list_option__long_name) +{ + const cmdline::list_option o("list", "The list", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("list", o.long_name()); + ATF_REQUIRE_EQ("The list", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_option__type); +ATF_TEST_CASE_BODY(list_option__type) +{ + const cmdline::list_option o("list", "The list", "arg"); + + o.validate(""); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert(""); + ATF_REQUIRE(words.empty()); + } + + o.validate("foo"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo"); + ATF_REQUIRE_EQ(1, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + } + + o.validate("foo,bar,baz"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo,bar,baz"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + ATF_REQUIRE_EQ("bar", words[1]); + ATF_REQUIRE_EQ("baz", words[2]); + } + + o.validate("foo,bar,"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo,bar,"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + ATF_REQUIRE_EQ("bar", words[1]); + ATF_REQUIRE_EQ("", words[2]); + } + + o.validate(",foo,bar"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert(",foo,bar"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("", words[0]); + ATF_REQUIRE_EQ("foo", words[1]); + ATF_REQUIRE_EQ("bar", words[2]); + } + + o.validate("foo,,bar"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo,,bar"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + ATF_REQUIRE_EQ("", words[1]); + ATF_REQUIRE_EQ("bar", words[2]); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(path_option__short_name); +ATF_TEST_CASE_BODY(path_option__short_name) +{ + const cmdline::path_option o('p', "path", "The path", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("path", o.long_name()); + ATF_REQUIRE_EQ("The path", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(path_option__long_name); +ATF_TEST_CASE_BODY(path_option__long_name) +{ + const cmdline::path_option o("path", "The path", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("path", o.long_name()); + ATF_REQUIRE_EQ("The path", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(path_option__type); +ATF_TEST_CASE_BODY(path_option__type) +{ + const cmdline::path_option o("path", "The path", "arg"); + + o.validate("/some/path"); + + try { + o.validate(""); + fail("option_argument_value_error not raised"); + } catch (const cmdline::option_argument_value_error& e) { + // Expected; ignore. + } + + const cmdline::path_option::option_type path = + cmdline::path_option::convert("/foo/bar"); + ATF_REQUIRE_EQ("bar", path.leaf_name()); // Ensure valid type. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(property_option__short_name); +ATF_TEST_CASE_BODY(property_option__short_name) +{ + const cmdline::property_option o('p', "property", "The property", "a=b"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("property", o.long_name()); + ATF_REQUIRE_EQ("The property", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("a=b", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(property_option__long_name); +ATF_TEST_CASE_BODY(property_option__long_name) +{ + const cmdline::property_option o("property", "The property", "a=b"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("property", o.long_name()); + ATF_REQUIRE_EQ("The property", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("a=b", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(property_option__type); +ATF_TEST_CASE_BODY(property_option__type) +{ + typedef std::pair< std::string, std::string > string_pair; + const cmdline::property_option o("property", "The property", "a=b"); + + o.validate("foo=bar"); + ATF_REQUIRE(string_pair("foo", "bar") == + cmdline::property_option::convert("foo=bar")); + + o.validate(" foo = bar baz"); + ATF_REQUIRE(string_pair(" foo ", " bar baz") == + cmdline::property_option::convert(" foo = bar baz")); + + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("=")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("a=")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("=b")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_option__short_name); +ATF_TEST_CASE_BODY(string_option__short_name) +{ + const cmdline::string_option o('p', "string", "The string", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("string", o.long_name()); + ATF_REQUIRE_EQ("The string", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_option__long_name); +ATF_TEST_CASE_BODY(string_option__long_name) +{ + const cmdline::string_option o("string", "The string", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("string", o.long_name()); + ATF_REQUIRE_EQ("The string", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_option__type); +ATF_TEST_CASE_BODY(string_option__type) +{ + const cmdline::string_option o("string", "The string", "foo"); + + o.validate(""); + o.validate("some string"); + + const cmdline::string_option::option_type string = + cmdline::string_option::convert("foo"); + ATF_REQUIRE_EQ(3, string.length()); // Ensure valid type. +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, base_option__short_name__no_arg); + ATF_ADD_TEST_CASE(tcs, base_option__short_name__with_arg__no_default); + ATF_ADD_TEST_CASE(tcs, base_option__short_name__with_arg__with_default); + ATF_ADD_TEST_CASE(tcs, base_option__long_name__no_arg); + ATF_ADD_TEST_CASE(tcs, base_option__long_name__with_arg__no_default); + ATF_ADD_TEST_CASE(tcs, base_option__long_name__with_arg__with_default); + + ATF_ADD_TEST_CASE(tcs, bool_option__short_name); + ATF_ADD_TEST_CASE(tcs, bool_option__long_name); + + ATF_ADD_TEST_CASE(tcs, int_option__short_name); + ATF_ADD_TEST_CASE(tcs, int_option__long_name); + ATF_ADD_TEST_CASE(tcs, int_option__type); + + ATF_ADD_TEST_CASE(tcs, list_option__short_name); + ATF_ADD_TEST_CASE(tcs, list_option__long_name); + ATF_ADD_TEST_CASE(tcs, list_option__type); + + ATF_ADD_TEST_CASE(tcs, path_option__short_name); + ATF_ADD_TEST_CASE(tcs, path_option__long_name); + ATF_ADD_TEST_CASE(tcs, path_option__type); + + ATF_ADD_TEST_CASE(tcs, property_option__short_name); + ATF_ADD_TEST_CASE(tcs, property_option__long_name); + ATF_ADD_TEST_CASE(tcs, property_option__type); + + ATF_ADD_TEST_CASE(tcs, string_option__short_name); + ATF_ADD_TEST_CASE(tcs, string_option__long_name); + ATF_ADD_TEST_CASE(tcs, string_option__type); +} diff --git a/utils/cmdline/parser.cpp b/utils/cmdline/parser.cpp new file mode 100644 index 000000000000..5c83f6d69cc4 --- /dev/null +++ b/utils/cmdline/parser.cpp @@ -0,0 +1,385 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/parser.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include <getopt.h> +} + +#include <cstdlib> +#include <cstring> +#include <limits> + +#include "utils/auto_array.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + +namespace { + + +/// Auxiliary data to call getopt_long(3). +struct getopt_data : utils::noncopyable { + /// Plain-text representation of the short options. + /// + /// This string follows the syntax expected by getopt_long(3) in the + /// argument to describe the short options. + std::string short_options; + + /// Representation of the long options as expected by getopt_long(3). + utils::auto_array< ::option > long_options; + + /// Auto-generated identifiers to be able to parse long options. + std::map< int, const cmdline::base_option* > ids; +}; + + +/// Converts a cmdline::options_vector to a getopt_data. +/// +/// \param options The high-level definition of the options. +/// \param [out] data An object containing the necessary data to call +/// getopt_long(3) and interpret its results. +static void +options_to_getopt_data(const cmdline::options_vector& options, + getopt_data& data) +{ + data.short_options.clear(); + data.long_options.reset(new ::option[options.size() + 1]); + + int cur_id = 512; + + for (cmdline::options_vector::size_type i = 0; i < options.size(); i++) { + const cmdline::base_option* option = options[i]; + ::option& long_option = data.long_options[i]; + + long_option.name = option->long_name().c_str(); + if (option->needs_arg()) + long_option.has_arg = required_argument; + else + long_option.has_arg = no_argument; + + int id = -1; + if (option->has_short_name()) { + data.short_options += option->short_name(); + if (option->needs_arg()) + data.short_options += ':'; + id = option->short_name(); + } else { + id = cur_id++; + } + long_option.flag = NULL; + long_option.val = id; + data.ids[id] = option; + } + + ::option& last_long_option = data.long_options[options.size()]; + last_long_option.name = NULL; + last_long_option.has_arg = 0; + last_long_option.flag = NULL; + last_long_option.val = 0; +} + + +/// Converts an argc/argv pair to an args_vector. +/// +/// \param argc The value of argc as passed to main(). +/// \param argv The value of argv as passed to main(). +/// +/// \return An args_vector with the same contents of argc/argv. +static cmdline::args_vector +argv_to_vector(int argc, const char* const argv[]) +{ + PRE(argv[argc] == NULL); + cmdline::args_vector args; + for (int i = 0; i < argc; i++) + args.push_back(argv[i]); + return args; +} + + +/// Creates a mutable version of argv. +/// +/// \param argc The value of argc as passed to main(). +/// \param argv The value of argv as passed to main(). +/// +/// \return A new argv, with mutable buffers. The returned array must be +/// released using the free_mutable_argv() function. +static char** +make_mutable_argv(const int argc, const char* const* argv) +{ + char** mutable_argv = new char*[argc + 1]; + for (int i = 0; i < argc; i++) + mutable_argv[i] = ::strdup(argv[i]); + mutable_argv[argc] = NULL; + return mutable_argv; +} + + +/// Releases the object returned by make_mutable_argv(). +/// +/// \param argv A dynamically-allocated argv as returned by make_mutable_argv(). +static void +free_mutable_argv(char** argv) +{ + char** ptr = argv; + while (*ptr != NULL) { + ::free(*ptr); + ptr++; + } + delete [] argv; +} + + +/// Finds the name of the offending option after a getopt_long error. +/// +/// \param data Our internal getopt data used for the call to getopt_long. +/// \param getopt_optopt The value of getopt(3)'s optopt after the error. +/// \param argv The argv passed to getopt_long. +/// \param getopt_optind The value of getopt(3)'s optind after the error. +/// +/// \return A fully-specified option name (i.e. an option name prefixed by +/// either '-' or '--'). +static std::string +find_option_name(const getopt_data& data, const int getopt_optopt, + char** argv, const int getopt_optind) +{ + PRE(getopt_optopt >= 0); + + if (getopt_optopt == 0) { + return argv[getopt_optind - 1]; + } else if (getopt_optopt < std::numeric_limits< char >::max()) { + INV(getopt_optopt > 0); + const char ch = static_cast< char >(getopt_optopt); + return F("-%s") % ch; + } else { + for (const ::option* opt = &data.long_options[0]; opt->name != NULL; + opt++) { + if (opt->val == getopt_optopt) + return F("--%s") % opt->name; + } + UNREACHABLE; + } +} + + +} // anonymous namespace + + +/// Constructs a new parsed_cmdline. +/// +/// Use the cmdline::parse() free functions to construct. +/// +/// \param option_values_ A mapping of long option names to values. This +/// contains a representation of the options provided by the user. Note +/// that each value is actually a collection values: a user may specify a +/// flag multiple times, and depending on the case we want to honor one or +/// the other. For those options that support no argument, the argument +/// value is the empty string. +/// \param arguments_ The list of non-option arguments in the command line. +cmdline::parsed_cmdline::parsed_cmdline( + const std::map< std::string, std::vector< std::string > >& option_values_, + const cmdline::args_vector& arguments_) : + _option_values(option_values_), + _arguments(arguments_) +{ +} + + +/// Checks if the given option has been given in the command line. +/// +/// \param name The long option name to check for presence. +/// +/// \return True if the option has been given; false otherwise. +bool +cmdline::parsed_cmdline::has_option(const std::string& name) const +{ + return _option_values.find(name) != _option_values.end(); +} + + +/// Gets the raw value of an option. +/// +/// The raw value of an option is a collection of strings that represent all the +/// values passed to the option on the command line. It is up to the consumer +/// if he wants to honor only the last value or all of them. +/// +/// The caller has to use get_option() instead; this function is internal. +/// +/// \pre has_option(name) must be true. +/// +/// \param name The option to query. +/// +/// \return The value of the option as a plain string. +const std::vector< std::string >& +cmdline::parsed_cmdline::get_option_raw(const std::string& name) const +{ + std::map< std::string, std::vector< std::string > >::const_iterator iter = + _option_values.find(name); + INV_MSG(iter != _option_values.end(), F("Undefined option --%s") % name); + return (*iter).second; +} + + +/// Returns the non-option arguments found in the command line. +/// +/// \return The arguments, if any. +const cmdline::args_vector& +cmdline::parsed_cmdline::arguments(void) const +{ + return _arguments; +} + + +/// Parses a command line. +/// +/// \param args The command line to parse, broken down by words. +/// \param options The description of the supported options. +/// +/// \return The parsed command line. +/// +/// \pre args[0] must be the program or command name. +/// +/// \throw cmdline::error See the description of parse(argc, argv, options) for +/// more details on the raised errors. +cmdline::parsed_cmdline +cmdline::parse(const cmdline::args_vector& args, + const cmdline::options_vector& options) +{ + PRE_MSG(args.size() >= 1, "No progname or command name found"); + + utils::auto_array< const char* > argv(new const char*[args.size() + 1]); + for (args_vector::size_type i = 0; i < args.size(); i++) + argv[i] = args[i].c_str(); + argv[args.size()] = NULL; + return parse(static_cast< int >(args.size()), argv.get(), options); +} + + +/// Parses a command line. +/// +/// \param argc The number of arguments in argv, without counting the +/// terminating NULL. +/// \param argv The arguments to parse. The array is NULL-terminated. +/// \param options The description of the supported options. +/// +/// \return The parsed command line. +/// +/// \pre args[0] must be the program or command name. +/// +/// \throw cmdline::missing_option_argument_error If the user specified an +/// option that requires an argument, but no argument was provided. +/// \throw cmdline::unknown_option_error If the user specified an unknown +/// option (i.e. an option not defined in options). +/// \throw cmdline::option_argument_value_error If the user passed an invalid +/// argument to a supported option. +cmdline::parsed_cmdline +cmdline::parse(const int argc, const char* const* argv, + const cmdline::options_vector& options) +{ + PRE_MSG(argc >= 1, "No progname or command name found"); + + getopt_data data; + options_to_getopt_data(options, data); + + std::map< std::string, std::vector< std::string > > option_values; + + for (cmdline::options_vector::const_iterator iter = options.begin(); + iter != options.end(); iter++) { + const cmdline::base_option* option = *iter; + if (option->needs_arg() && option->has_default_value()) + option_values[option->long_name()].push_back( + option->default_value()); + } + + args_vector args; + + int mutable_argc = argc; + char** mutable_argv = make_mutable_argv(argc, argv); + const int old_opterr = ::opterr; + try { + int ch; + + ::opterr = 0; + + while ((ch = ::getopt_long(mutable_argc, mutable_argv, + ("+:" + data.short_options).c_str(), + data.long_options.get(), NULL)) != -1) { + if (ch == ':' ) { + const std::string name = find_option_name( + data, ::optopt, mutable_argv, ::optind); + throw cmdline::missing_option_argument_error(name); + } else if (ch == '?') { + const std::string name = find_option_name( + data, ::optopt, mutable_argv, ::optind); + throw cmdline::unknown_option_error(name); + } + + const std::map< int, const cmdline::base_option* >::const_iterator + id = data.ids.find(ch); + INV(id != data.ids.end()); + const cmdline::base_option* option = (*id).second; + + if (option->needs_arg()) { + if (::optarg != NULL) { + option->validate(::optarg); + option_values[option->long_name()].push_back(::optarg); + } else + INV(option->has_default_value()); + } else { + option_values[option->long_name()].push_back(""); + } + } + args = argv_to_vector(mutable_argc - optind, mutable_argv + optind); + + ::opterr = old_opterr; + ::optind = GETOPT_OPTIND_RESET_VALUE; +#if defined(HAVE_GETOPT_WITH_OPTRESET) + ::optreset = 1; +#endif + } catch (...) { + free_mutable_argv(mutable_argv); + ::opterr = old_opterr; + ::optind = GETOPT_OPTIND_RESET_VALUE; +#if defined(HAVE_GETOPT_WITH_OPTRESET) + ::optreset = 1; +#endif + throw; + } + free_mutable_argv(mutable_argv); + + return parsed_cmdline(option_values, args); +} diff --git a/utils/cmdline/parser.hpp b/utils/cmdline/parser.hpp new file mode 100644 index 000000000000..657fd1f01dd3 --- /dev/null +++ b/utils/cmdline/parser.hpp @@ -0,0 +1,85 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/parser.hpp +/// Routines and data types to parse command line options and arguments. + +#if !defined(UTILS_CMDLINE_PARSER_HPP) +#define UTILS_CMDLINE_PARSER_HPP + +#include "utils/cmdline/parser_fwd.hpp" + +#include <map> +#include <string> +#include <vector> + +namespace utils { +namespace cmdline { + + +/// Representation of a parsed command line. +/// +/// This class is returned by the command line parsing algorithm and provides +/// methods to query the values of the options and the value of the arguments. +/// All the values fed into this class can considered to be sane (i.e. the +/// arguments to the options and the arguments to the command are valid), as all +/// validation happens during parsing (before this class is instantiated). +class parsed_cmdline { + /// Mapping of option names to all the values provided. + std::map< std::string, std::vector< std::string > > _option_values; + + /// Collection of arguments with all options removed. + args_vector _arguments; + + const std::vector< std::string >& get_option_raw(const std::string&) const; + +public: + parsed_cmdline(const std::map< std::string, std::vector< std::string > >&, + const args_vector&); + + bool has_option(const std::string&) const; + + template< typename Option > + typename Option::option_type get_option(const std::string&) const; + + template< typename Option > + std::vector< typename Option::option_type > get_multi_option( + const std::string&) const; + + const args_vector& arguments(void) const; +}; + + +parsed_cmdline parse(const args_vector&, const options_vector&); +parsed_cmdline parse(const int, const char* const*, const options_vector&); + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_PARSER_HPP) diff --git a/utils/cmdline/parser.ipp b/utils/cmdline/parser.ipp new file mode 100644 index 000000000000..820826a15bfe --- /dev/null +++ b/utils/cmdline/parser.ipp @@ -0,0 +1,83 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_CMDLINE_PARSER_IPP) +#define UTILS_CMDLINE_PARSER_IPP + +#include "utils/cmdline/parser.hpp" + + +/// Gets the value of an option. +/// +/// If the option has been specified multiple times on the command line, this +/// only returns the last value. This is the traditional behavior. +/// +/// The option must support arguments. Otherwise, a call to this function will +/// not compile because the option type will lack the definition of some fields +/// and/or methods. +/// +/// \param name The option to query. +/// +/// \return The value of the option converted to the appropriate type. +/// +/// \pre has_option(name) must be true. +template< typename Option > typename Option::option_type +utils::cmdline::parsed_cmdline::get_option(const std::string& name) const +{ + const std::vector< std::string >& raw_values = get_option_raw(name); + return Option::convert(raw_values[raw_values.size() - 1]); +} + + +/// Gets the values of an option that supports repetition. +/// +/// The option must support arguments. Otherwise, a call to this function will +/// not compile because the option type will lack the definition of some fields +/// and/or methods. +/// +/// \param name The option to query. +/// +/// \return The values of the option converted to the appropriate type. +/// +/// \pre has_option(name) must be true. +template< typename Option > std::vector< typename Option::option_type > +utils::cmdline::parsed_cmdline::get_multi_option(const std::string& name) const +{ + std::vector< typename Option::option_type > values; + + const std::vector< std::string >& raw_values = get_option_raw(name); + for (std::vector< std::string >::const_iterator iter = raw_values.begin(); + iter != raw_values.end(); iter++) { + values.push_back(Option::convert(*iter)); + } + + return values; +} + + +#endif // !defined(UTILS_CMDLINE_PARSER_IPP) diff --git a/utils/cmdline/parser_fwd.hpp b/utils/cmdline/parser_fwd.hpp new file mode 100644 index 000000000000..a136e99a47ac --- /dev/null +++ b/utils/cmdline/parser_fwd.hpp @@ -0,0 +1,58 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/parser_fwd.hpp +/// Forward declarations for utils/cmdline/parser.hpp + +#if !defined(UTILS_CMDLINE_PARSER_FWD_HPP) +#define UTILS_CMDLINE_PARSER_FWD_HPP + +#include <string> +#include <vector> + +#include "utils/cmdline/options_fwd.hpp" + +namespace utils { +namespace cmdline { + + +/// Replacement for argc and argv to represent a command line. +typedef std::vector< std::string > args_vector; + + +/// Collection of options to be used during parsing. +typedef std::vector< const base_option* > options_vector; + + +class parsed_cmdline; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_PARSER_FWD_HPP) diff --git a/utils/cmdline/parser_test.cpp b/utils/cmdline/parser_test.cpp new file mode 100644 index 000000000000..96370d279d2e --- /dev/null +++ b/utils/cmdline/parser_test.cpp @@ -0,0 +1,688 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/parser.ipp" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +extern "C" { +#include <fcntl.h> +#include <getopt.h> +#include <unistd.h> +} + +#include <cstdlib> +#include <cstring> +#include <fstream> +#include <iostream> +#include <string> +#include <utility> + +#include <atf-c++.hpp> + +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + +using cmdline::base_option; +using cmdline::bool_option; +using cmdline::int_option; +using cmdline::parse; +using cmdline::parsed_cmdline; +using cmdline::string_option; + + +namespace { + + +/// Mock option type to check the validate and convert methods sequence. +/// +/// Instances of this option accept a string argument that must be either "zero" +/// or "one". These are validated and converted to integers. +class mock_option : public base_option { +public: + /// Constructs the new option. + /// + /// \param long_name_ The long name for the option. All other option + /// properties are irrelevant for the tests using this, so they are set + /// to arbitrary values. + mock_option(const char* long_name_) : + base_option(long_name_, "Irrelevant description", "arg") + { + } + + /// The type of the argument of this option. + typedef int option_type; + + /// Checks that the user-provided option is valid. + /// + /// \param str The user argument; must be "zero" or "one". + /// + /// \throw cmdline::option_argument_value_error If str is not valid. + void + validate(const std::string& str) const + { + if (str != "zero" && str != "one") + throw cmdline::option_argument_value_error(F("--%s") % long_name(), + str, "Unknown value"); + } + + /// Converts the user-provided argument to our native integer type. + /// + /// \param str The user argument; must be "zero" or "one". + /// + /// \return 0 if the input is "zero", or 1 if the input is "one". + /// + /// \throw std::runtime_error If str is not valid. In real life, this + /// should be a precondition because validate() has already ensured that + /// the values passed to convert() are correct. However, we raise an + /// exception here because we are actually validating that this code + /// sequence holds true. + static int + convert(const std::string& str) + { + if (str == "zero") + return 0; + else if (str == "one") + return 1; + else { + // This would generally be an assertion but, given that this is + // test code, we want to catch any errors regardless of how the + // binary is built. + throw std::runtime_error("Value not validated properly."); + } + } +}; + + +/// Redirects stdout and stderr to a file. +/// +/// This fails the test case in case of any error. +/// +/// \param file The name of the file to redirect stdout and stderr to. +/// +/// \return A copy of the old stdout and stderr file descriptors. +static std::pair< int, int > +mock_stdfds(const char* file) +{ + std::cout.flush(); + std::cerr.flush(); + + const int oldout = ::dup(STDOUT_FILENO); + ATF_REQUIRE(oldout != -1); + const int olderr = ::dup(STDERR_FILENO); + ATF_REQUIRE(olderr != -1); + + const int fd = ::open(file, O_WRONLY | O_CREAT | O_TRUNC, 0644); + ATF_REQUIRE(fd != -1); + ATF_REQUIRE(::dup2(fd, STDOUT_FILENO) != -1); + ATF_REQUIRE(::dup2(fd, STDERR_FILENO) != -1); + ::close(fd); + + return std::make_pair(oldout, olderr); +} + + +/// Restores stdout and stderr after a call to mock_stdfds. +/// +/// \param oldfds The copy of the previous stdout and stderr as returned by the +/// call to mock_fds(). +static void +restore_stdfds(const std::pair< int, int >& oldfds) +{ + ATF_REQUIRE(::dup2(oldfds.first, STDOUT_FILENO) != -1); + ::close(oldfds.first); + ATF_REQUIRE(::dup2(oldfds.second, STDERR_FILENO) != -1); + ::close(oldfds.second); +} + + +/// Checks whether a '+:' prefix to the short options of getopt_long works. +/// +/// It turns out that the getopt_long(3) implementation of Ubuntu 10.04.1 (and +/// very likely other distributions) does not properly report a missing argument +/// to a second long option as such. Instead of returning ':' when the second +/// long option provided on the command line does not carry a required argument, +/// it will mistakenly return '?' which translates to "unknown option". +/// +/// As a result of this bug, we cannot properly detect that 'flag2' requires an +/// argument in a command line like: 'progname --flag1=foo --flag2'. +/// +/// I am not sure if we could fully workaround the issue in the implementation +/// of our library. For the time being I am just using this bug detection in +/// the test cases to prevent failures that are not really our fault. +/// +/// \return bool True if getopt_long is broken and does not interpret '+:' +/// correctly; False otherwise. +static bool +is_getopt_long_pluscolon_broken(void) +{ + struct ::option long_options[] = { + { "flag1", 1, NULL, '1' }, + { "flag2", 1, NULL, '2' }, + { NULL, 0, NULL, 0 } + }; + + const int argc = 3; + char* argv[4]; + argv[0] = ::strdup("progname"); + argv[1] = ::strdup("--flag1=a"); + argv[2] = ::strdup("--flag2"); + argv[3] = NULL; + + const int old_opterr = ::opterr; + ::opterr = 0; + + bool got_colon = false; + + int opt; + while ((opt = ::getopt_long(argc, argv, "+:", long_options, NULL)) != -1) { + switch (opt) { + case '1': break; + case '2': break; + case ':': got_colon = true; break; + case '?': break; + default: UNREACHABLE; break; + } + } + + ::opterr = old_opterr; + ::optind = 1; +#if defined(HAVE_GETOPT_WITH_OPTRESET) + ::optreset = 1; +#endif + + for (char** arg = &argv[0]; *arg != NULL; arg++) + std::free(*arg); + + return !got_colon; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__no_options); +ATF_TEST_CASE_BODY(progname__no_options) +{ + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + std::vector< const base_option* > options; + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(cmdline.arguments().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__some_options); +ATF_TEST_CASE_BODY(progname__some_options) +{ + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + const string_option a('a', "a_option", "Foo", NULL); + const string_option b('b', "b_option", "Bar", "arg", "foo"); + const string_option c("c_option", "Baz", NULL); + const string_option d("d_option", "Wohoo", "arg", "bar"); + std::vector< const base_option* > options; + options.push_back(&a); + options.push_back(&b); + options.push_back(&c); + options.push_back(&d); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE_EQ("foo", cmdline.get_option< string_option >("b_option")); + ATF_REQUIRE_EQ("bar", cmdline.get_option< string_option >("d_option")); + ATF_REQUIRE(cmdline.arguments().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_args__no_options); +ATF_TEST_CASE_BODY(some_args__no_options) +{ + const int argc = 5; + const char* const argv[] = {"progname", "foo", "-c", "--opt", "bar", NULL}; + std::vector< const base_option* > options; + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(!cmdline.has_option("c")); + ATF_REQUIRE(!cmdline.has_option("opt")); + ATF_REQUIRE_EQ(4, cmdline.arguments().size()); + ATF_REQUIRE_EQ("foo", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("-c", cmdline.arguments()[1]); + ATF_REQUIRE_EQ("--opt", cmdline.arguments()[2]); + ATF_REQUIRE_EQ("bar", cmdline.arguments()[3]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_args__some_options); +ATF_TEST_CASE_BODY(some_args__some_options) +{ + const int argc = 5; + const char* const argv[] = {"progname", "foo", "-c", "--opt", "bar", NULL}; + const string_option c('c', "opt", "Description", NULL); + std::vector< const base_option* > options; + options.push_back(&c); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(!cmdline.has_option("c")); + ATF_REQUIRE(!cmdline.has_option("opt")); + ATF_REQUIRE_EQ(4, cmdline.arguments().size()); + ATF_REQUIRE_EQ("foo", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("-c", cmdline.arguments()[1]); + ATF_REQUIRE_EQ("--opt", cmdline.arguments()[2]); + ATF_REQUIRE_EQ("bar", cmdline.arguments()[3]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_options__all_known); +ATF_TEST_CASE_BODY(some_options__all_known) +{ + const int argc = 14; + const char* const argv[] = { + "progname", + "-a", + "-bvalue_b", + "-c", "value_c", + //"-d", // Options with default optional values are unsupported. + "-evalue_e", // Has default; overriden. + "--f_long", + "--g_long=value_g", + "--h_long", "value_h", + //"--i_long", // Options with default optional values are unsupported. + "--j_long", "value_j", // Has default; overriden as separate argument. + "arg1", "arg2", NULL, + }; + const bool_option a('a', "a_long", ""); + const string_option b('b', "b_long", "Description", "arg"); + const string_option c('c', "c_long", "ABCD", "foo"); + const string_option d('d', "d_long", "Description", "bar", "default_d"); + const string_option e('e', "e_long", "Description", "baz", "default_e"); + const bool_option f("f_long", "Description"); + const string_option g("g_long", "Description", "arg"); + const string_option h("h_long", "Description", "foo"); + const string_option i("i_long", "EFGH", "bar", "default_i"); + const string_option j("j_long", "Description", "baz", "default_j"); + std::vector< const base_option* > options; + options.push_back(&a); + options.push_back(&b); + options.push_back(&c); + options.push_back(&d); + options.push_back(&e); + options.push_back(&f); + options.push_back(&g); + options.push_back(&h); + options.push_back(&i); + options.push_back(&j); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(cmdline.has_option("a_long")); + ATF_REQUIRE_EQ("value_b", cmdline.get_option< string_option >("b_long")); + ATF_REQUIRE_EQ("value_c", cmdline.get_option< string_option >("c_long")); + ATF_REQUIRE_EQ("default_d", cmdline.get_option< string_option >("d_long")); + ATF_REQUIRE_EQ("value_e", cmdline.get_option< string_option >("e_long")); + ATF_REQUIRE(cmdline.has_option("f_long")); + ATF_REQUIRE_EQ("value_g", cmdline.get_option< string_option >("g_long")); + ATF_REQUIRE_EQ("value_h", cmdline.get_option< string_option >("h_long")); + ATF_REQUIRE_EQ("default_i", cmdline.get_option< string_option >("i_long")); + ATF_REQUIRE_EQ("value_j", cmdline.get_option< string_option >("j_long")); + ATF_REQUIRE_EQ(2, cmdline.arguments().size()); + ATF_REQUIRE_EQ("arg1", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("arg2", cmdline.arguments()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_options__multi); +ATF_TEST_CASE_BODY(some_options__multi) +{ + const int argc = 9; + const char* const argv[] = { + "progname", + "-a1", + "-bvalue1", + "-a2", + "--a_long=3", + "-bvalue2", + "--b_long=value3", + "arg1", "arg2", NULL, + }; + const int_option a('a', "a_long", "Description", "arg"); + const string_option b('b', "b_long", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&a); + options.push_back(&b); + const parsed_cmdline cmdline = parse(argc, argv, options); + + { + ATF_REQUIRE_EQ(3, cmdline.get_option< int_option >("a_long")); + const std::vector< int > multi = + cmdline.get_multi_option< int_option >("a_long"); + ATF_REQUIRE_EQ(3, multi.size()); + ATF_REQUIRE_EQ(1, multi[0]); + ATF_REQUIRE_EQ(2, multi[1]); + ATF_REQUIRE_EQ(3, multi[2]); + } + + { + ATF_REQUIRE_EQ("value3", cmdline.get_option< string_option >("b_long")); + const std::vector< std::string > multi = + cmdline.get_multi_option< string_option >("b_long"); + ATF_REQUIRE_EQ(3, multi.size()); + ATF_REQUIRE_EQ("value1", multi[0]); + ATF_REQUIRE_EQ("value2", multi[1]); + ATF_REQUIRE_EQ("value3", multi[2]); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommands); +ATF_TEST_CASE_BODY(subcommands) +{ + const int argc = 5; + const char* const argv[] = {"progname", "--flag1", "subcommand", + "--flag2", "arg", NULL}; + const bool_option flag1("flag1", ""); + std::vector< const base_option* > options; + options.push_back(&flag1); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE( cmdline.has_option("flag1")); + ATF_REQUIRE(!cmdline.has_option("flag2")); + ATF_REQUIRE_EQ(3, cmdline.arguments().size()); + ATF_REQUIRE_EQ("subcommand", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("--flag2", cmdline.arguments()[1]); + ATF_REQUIRE_EQ("arg", cmdline.arguments()[2]); + + const bool_option flag2("flag2", ""); + std::vector< const base_option* > options2; + options2.push_back(&flag2); + const parsed_cmdline cmdline2 = parse(cmdline.arguments(), options2); + + ATF_REQUIRE(!cmdline2.has_option("flag1")); + ATF_REQUIRE( cmdline2.has_option("flag2")); + ATF_REQUIRE_EQ(1, cmdline2.arguments().size()); + ATF_REQUIRE_EQ("arg", cmdline2.arguments()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error__short); +ATF_TEST_CASE_BODY(missing_option_argument_error__short) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-a3", "-b", NULL}; + const string_option flag1('a', "flag1", "Description", "arg"); + const string_option flag2('b', "flag2", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + try { + parse(argc, argv, options); + fail("missing_option_argument_error not raised"); + } catch (const cmdline::missing_option_argument_error& e) { + ATF_REQUIRE_EQ("-b", e.option()); + } catch (const cmdline::unknown_option_error& e) { + if (is_getopt_long_pluscolon_broken()) + expect_fail("Your getopt_long is broken"); + fail("Got unknown_option_error instead of " + "missing_option_argument_error"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error__shortblock); +ATF_TEST_CASE_BODY(missing_option_argument_error__shortblock) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-ab3", "-ac", NULL}; + const bool_option flag1('a', "flag1", "Description"); + const string_option flag2('b', "flag2", "Description", "arg"); + const string_option flag3('c', "flag2", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + options.push_back(&flag3); + + try { + parse(argc, argv, options); + fail("missing_option_argument_error not raised"); + } catch (const cmdline::missing_option_argument_error& e) { + ATF_REQUIRE_EQ("-c", e.option()); + } catch (const cmdline::unknown_option_error& e) { + if (is_getopt_long_pluscolon_broken()) + expect_fail("Your getopt_long is broken"); + fail("Got unknown_option_error instead of " + "missing_option_argument_error"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error__long); +ATF_TEST_CASE_BODY(missing_option_argument_error__long) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=a", "--flag2", NULL}; + const string_option flag1("flag1", "Description", "arg"); + const string_option flag2("flag2", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + try { + parse(argc, argv, options); + fail("missing_option_argument_error not raised"); + } catch (const cmdline::missing_option_argument_error& e) { + ATF_REQUIRE_EQ("--flag2", e.option()); + } catch (const cmdline::unknown_option_error& e) { + if (is_getopt_long_pluscolon_broken()) + expect_fail("Your getopt_long is broken"); + fail("Got unknown_option_error instead of " + "missing_option_argument_error"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error__short); +ATF_TEST_CASE_BODY(unknown_option_error__short) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-a", "-b", NULL}; + const bool_option flag1('a', "flag1", "Description"); + std::vector< const base_option* > options; + options.push_back(&flag1); + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-b", e.option()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error__shortblock); +ATF_TEST_CASE_BODY(unknown_option_error__shortblock) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-a", "-bdc", NULL}; + const bool_option flag1('a', "flag1", "Description"); + const bool_option flag2('b', "flag2", "Description"); + const bool_option flag3('c', "flag3", "Description"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + options.push_back(&flag3); + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-d", e.option()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error__long); +ATF_TEST_CASE_BODY(unknown_option_error__long) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=a", "--flag2", NULL}; + const string_option flag1("flag1", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("--flag2", e.option()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_plus_option_error); +ATF_TEST_CASE_BODY(unknown_plus_option_error) +{ + const int argc = 2; + const char* const argv[] = {"progname", "-+", NULL}; + const cmdline::options_vector options; + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-+", e.option()); + } catch (const cmdline::missing_option_argument_error& e) { + fail("Looks like getopt_long thinks a + option is defined and it " + "even requires an argument"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(option_types); +ATF_TEST_CASE_BODY(option_types) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=a", "--flag2=one", NULL}; + const string_option flag1("flag1", "The flag1", "arg"); + const mock_option flag2("flag2"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(cmdline.has_option("flag1")); + ATF_REQUIRE(cmdline.has_option("flag2")); + ATF_REQUIRE_EQ("a", cmdline.get_option< string_option >("flag1")); + ATF_REQUIRE_EQ(1, cmdline.get_option< mock_option >("flag2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(option_validation_error); +ATF_TEST_CASE_BODY(option_validation_error) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=zero", "--flag2=foo", + NULL}; + const mock_option flag1("flag1"); + const mock_option flag2("flag2"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + try { + parse(argc, argv, options); + fail("option_argument_value_error not raised"); + } catch (const cmdline::option_argument_value_error& e) { + ATF_REQUIRE_EQ("--flag2", e.option()); + ATF_REQUIRE_EQ("foo", e.argument()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(silent_errors); +ATF_TEST_CASE_BODY(silent_errors) +{ + const int argc = 2; + const char* const argv[] = {"progname", "-h", NULL}; + cmdline::options_vector options; + + try { + std::pair< int, int > oldfds = mock_stdfds("output.txt"); + try { + parse(argc, argv, options); + } catch (...) { + restore_stdfds(oldfds); + throw; + } + restore_stdfds(oldfds); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-h", e.option()); + } + + std::ifstream input("output.txt"); + ATF_REQUIRE(input); + + bool has_output = false; + std::string line; + while (std::getline(input, line).good()) { + std::cout << line << '\n'; + has_output = true; + } + + if (has_output) + fail("getopt_long printed messages on stdout/stderr by itself"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, progname__no_options); + ATF_ADD_TEST_CASE(tcs, progname__some_options); + ATF_ADD_TEST_CASE(tcs, some_args__no_options); + ATF_ADD_TEST_CASE(tcs, some_args__some_options); + ATF_ADD_TEST_CASE(tcs, some_options__all_known); + ATF_ADD_TEST_CASE(tcs, some_options__multi); + ATF_ADD_TEST_CASE(tcs, subcommands); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error__short); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error__shortblock); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error__long); + ATF_ADD_TEST_CASE(tcs, unknown_option_error__short); + ATF_ADD_TEST_CASE(tcs, unknown_option_error__shortblock); + ATF_ADD_TEST_CASE(tcs, unknown_option_error__long); + ATF_ADD_TEST_CASE(tcs, unknown_plus_option_error); + ATF_ADD_TEST_CASE(tcs, option_types); + ATF_ADD_TEST_CASE(tcs, option_validation_error); + ATF_ADD_TEST_CASE(tcs, silent_errors); +} diff --git a/utils/cmdline/ui.cpp b/utils/cmdline/ui.cpp new file mode 100644 index 000000000000..a682360a4259 --- /dev/null +++ b/utils/cmdline/ui.cpp @@ -0,0 +1,276 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/ui.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include <sys/param.h> +#include <sys/ioctl.h> + +#if defined(HAVE_TERMIOS_H) +# include <termios.h> +#endif +#include <unistd.h> +} + +#include <iostream> + +#include "utils/cmdline/globals.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/text/operations.ipp" +#include "utils/text/table.hpp" + +namespace cmdline = utils::cmdline; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Destructor for the class. +cmdline::ui::~ui(void) +{ +} + + +/// Writes a single line to stderr. +/// +/// The written line is printed as is, without being wrapped to fit within the +/// screen width. If the caller wants to print more than one line, it shall +/// invoke this function once per line. +/// +/// \param message The line to print. Should not include a trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +cmdline::ui::err(const std::string& message, const bool newline) +{ + LI(F("stderr: %s") % message); + if (newline) + std::cerr << message << "\n"; + else { + std::cerr << message; + std::cerr.flush(); + } +} + + +/// Writes a single line to stdout. +/// +/// The written line is printed as is, without being wrapped to fit within the +/// screen width. If the caller wants to print more than one line, it shall +/// invoke this function once per line. +/// +/// \param message The line to print. Should not include a trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +cmdline::ui::out(const std::string& message, const bool newline) +{ + LI(F("stdout: %s") % message); + if (newline) + std::cout << message << "\n"; + else { + std::cout << message; + std::cout.flush(); + } +} + + +/// Queries the width of the screen. +/// +/// This information comes first from the COLUMNS environment variable. If not +/// present or invalid, and if the stdout of the current process is connected to +/// a terminal the width is deduced from the terminal itself. Ultimately, if +/// all fails, none is returned. This function shall not raise any errors. +/// +/// Be aware that the results of this query are cached during execution. +/// Subsequent calls to this function will always return the same value even if +/// the terminal size has actually changed. +/// +/// \todo Install a signal handler for SIGWINCH so that we can readjust our +/// knowledge of the terminal width when the user resizes the window. +/// +/// \return The width of the screen if it was possible to determine it, or none +/// otherwise. +optional< std::size_t > +cmdline::ui::screen_width(void) const +{ + static bool done = false; + static optional< std::size_t > width = none; + + if (!done) { + const optional< std::string > columns = utils::getenv("COLUMNS"); + if (columns) { + if (columns.get().length() > 0) { + try { + width = utils::make_optional( + utils::text::to_type< std::size_t >(columns.get())); + } catch (const utils::text::value_error& e) { + LD(F("Ignoring invalid value in COLUMNS variable: %s") % + e.what()); + } + } + } + if (!width) { + struct ::winsize ws; + if (::ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1) + width = optional< std::size_t >(ws.ws_col); + } + + if (width && width.get() >= 80) + width.get() -= 5; + + done = true; + } + + return width; +} + + +/// Writes a line to stdout. +/// +/// The line is wrapped to fit on screen. +/// +/// \param message The line to print, without the trailing newline character. +void +cmdline::ui::out_wrap(const std::string& message) +{ + const optional< std::size_t > max_width = screen_width(); + if (max_width) { + const std::vector< std::string > lines = text::refill( + message, max_width.get()); + for (std::vector< std::string >::const_iterator iter = lines.begin(); + iter != lines.end(); iter++) + out(*iter); + } else + out(message); +} + + +/// Writes a line to stdout with a leading tag. +/// +/// If the line does not fit on the current screen width, the line is broken +/// into pieces and the tag is repeated on every line. +/// +/// \param tag The leading line tag. +/// \param message The message to be printed, without the trailing newline +/// character. +/// \param repeat If true, print the tag on every line; otherwise, indent the +/// text of all lines to match the width of the tag on the first line. +void +cmdline::ui::out_tag_wrap(const std::string& tag, const std::string& message, + const bool repeat) +{ + const optional< std::size_t > max_width = screen_width(); + if (max_width && max_width.get() > tag.length()) { + const std::vector< std::string > lines = text::refill( + message, max_width.get() - tag.length()); + for (std::vector< std::string >::const_iterator iter = lines.begin(); + iter != lines.end(); iter++) { + if (repeat || iter == lines.begin()) + out(F("%s%s") % tag % *iter); + else + out(F("%s%s") % std::string(tag.length(), ' ') % *iter); + } + } else { + out(F("%s%s") % tag % message); + } +} + + +/// Writes a table to stdout. +/// +/// \param table The table to write. +/// \param formatter The table formatter to use to convert the table to a +/// console representation. +/// \param prefix Text to prepend to all the lines of the output table. +void +cmdline::ui::out_table(const text::table& table, + text::table_formatter formatter, + const std::string& prefix) +{ + if (table.empty()) + return; + + const optional< std::size_t > max_width = screen_width(); + if (max_width) + formatter.set_table_width(max_width.get() - prefix.length()); + + const std::vector< std::string > lines = formatter.format(table); + for (std::vector< std::string >::const_iterator iter = lines.begin(); + iter != lines.end(); ++iter) + out(prefix + *iter); +} + + +/// Formats and prints an error message. +/// +/// \param ui_ The user interface object used to print the message. +/// \param message The message to print. Should not end with a newline +/// character. +void +cmdline::print_error(ui* ui_, const std::string& message) +{ + LE(message); + ui_->err(F("%s: E: %s") % cmdline::progname() % message); +} + + +/// Formats and prints an informational message. +/// +/// \param ui_ The user interface object used to print the message. +/// \param message The message to print. Should not end with a newline +/// character. +void +cmdline::print_info(ui* ui_, const std::string& message) +{ + LI(message); + ui_->err(F("%s: I: %s") % cmdline::progname() % message); +} + + +/// Formats and prints a warning message. +/// +/// \param ui_ The user interface object used to print the message. +/// \param message The message to print. Should not end with a newline +/// character. +void +cmdline::print_warning(ui* ui_, const std::string& message) +{ + LW(message); + ui_->err(F("%s: W: %s") % cmdline::progname() % message); +} diff --git a/utils/cmdline/ui.hpp b/utils/cmdline/ui.hpp new file mode 100644 index 000000000000..433bbe903b03 --- /dev/null +++ b/utils/cmdline/ui.hpp @@ -0,0 +1,79 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/ui.hpp +/// Abstractions and utilities to write formatted messages to the console. + +#if !defined(UTILS_CMDLINE_UI_HPP) +#define UTILS_CMDLINE_UI_HPP + +#include "utils/cmdline/ui_fwd.hpp" + +#include <cstddef> +#include <string> + +#include "utils/optional_fwd.hpp" +#include "utils/text/table_fwd.hpp" + +namespace utils { +namespace cmdline { + + +/// Interface to interact with the CLI. +/// +/// The main purpose of this class is to substitute direct usages of stdout and +/// stderr. An instance of this class is passed to every command of a CLI, +/// which allows unit testing and validation of the interaction with the user. +/// +/// This class writes directly to stdout and stderr. For testing purposes, see +/// the utils::cmdline::ui_mock class. +class ui { +public: + virtual ~ui(void); + + virtual void err(const std::string&, const bool = true); + virtual void out(const std::string&, const bool = true); + virtual optional< std::size_t > screen_width(void) const; + + void out_wrap(const std::string&); + void out_tag_wrap(const std::string&, const std::string&, + const bool = true); + void out_table(const utils::text::table&, utils::text::table_formatter, + const std::string&); +}; + + +void print_error(ui*, const std::string&); +void print_info(ui*, const std::string&); +void print_warning(ui*, const std::string&); + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_UI_HPP) diff --git a/utils/cmdline/ui_fwd.hpp b/utils/cmdline/ui_fwd.hpp new file mode 100644 index 000000000000..4417beb1a8e8 --- /dev/null +++ b/utils/cmdline/ui_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/ui_fwd.hpp +/// Forward declarations for utils/cmdline/ui.hpp + +#if !defined(UTILS_CMDLINE_UI_FWD_HPP) +#define UTILS_CMDLINE_UI_FWD_HPP + +namespace utils { +namespace cmdline { + + +class ui; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_UI_FWD_HPP) diff --git a/utils/cmdline/ui_mock.cpp b/utils/cmdline/ui_mock.cpp new file mode 100644 index 000000000000..b77943cf147b --- /dev/null +++ b/utils/cmdline/ui_mock.cpp @@ -0,0 +1,114 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/ui_mock.hpp" + +#include <iostream> + +#include "utils/optional.ipp" + +using utils::cmdline::ui_mock; +using utils::none; +using utils::optional; + + +/// Constructs a new mock UI. +/// +/// \param screen_width_ The width of the screen to use for testing purposes. +/// Defaults to 0 to prevent uncontrolled wrapping on our tests. +ui_mock::ui_mock(const std::size_t screen_width_) : + _screen_width(screen_width_) +{ +} + + +/// Writes a line to stderr and records it for further inspection. +/// +/// \param message The line to print and record, without the trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +ui_mock::err(const std::string& message, const bool newline) +{ + if (newline) + std::cerr << message << "\n"; + else { + std::cerr << message << "\n"; + std::cerr.flush(); + } + _err_log.push_back(message); +} + + +/// Writes a line to stdout and records it for further inspection. +/// +/// \param message The line to print and record, without the trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +ui_mock::out(const std::string& message, const bool newline) +{ + if (newline) + std::cout << message << "\n"; + else { + std::cout << message << "\n"; + std::cout.flush(); + } + _out_log.push_back(message); +} + + +/// Queries the width of the screen. +/// +/// \return Always none, as we do not want to depend on line wrapping in our +/// tests. +optional< std::size_t > +ui_mock::screen_width(void) const +{ + return _screen_width > 0 ? optional< std::size_t >(_screen_width) : none; +} + + +/// Gets all the lines written to stderr. +/// +/// \return The printed lines. +const std::vector< std::string >& +ui_mock::err_log(void) const +{ + return _err_log; +} + + +/// Gets all the lines written to stdout. +/// +/// \return The printed lines. +const std::vector< std::string >& +ui_mock::out_log(void) const +{ + return _out_log; +} diff --git a/utils/cmdline/ui_mock.hpp b/utils/cmdline/ui_mock.hpp new file mode 100644 index 000000000000..2c37683af7f3 --- /dev/null +++ b/utils/cmdline/ui_mock.hpp @@ -0,0 +1,78 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/cmdline/ui_mock.hpp +/// Provides the utils::cmdline::ui_mock class. +/// +/// This file is only supposed to be included from test program, never from +/// production code. + +#if !defined(UTILS_CMDLINE_UI_MOCK_HPP) +#define UTILS_CMDLINE_UI_MOCK_HPP + +#include <cstddef> +#include <string> +#include <vector> + +#include "utils/cmdline/ui.hpp" + +namespace utils { +namespace cmdline { + + +/// Testable interface to interact with the CLI. +/// +/// This class records all writes to stdout and stderr to allow further +/// inspection for testing purposes. +class ui_mock : public ui { + /// Fake width of the screen; if 0, represents none. + std::size_t _screen_width; + + /// Messages sent to stderr. + std::vector< std::string > _err_log; + + /// Messages sent to stdout. + std::vector< std::string > _out_log; + +public: + ui_mock(const std::size_t = 0); + + void err(const std::string&, const bool = true); + void out(const std::string&, const bool = true); + optional< std::size_t > screen_width(void) const; + + const std::vector< std::string >& err_log(void) const; + const std::vector< std::string >& out_log(void) const; +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_UI_MOCK_HPP) diff --git a/utils/cmdline/ui_test.cpp b/utils/cmdline/ui_test.cpp new file mode 100644 index 000000000000..92c64baf95a3 --- /dev/null +++ b/utils/cmdline/ui_test.cpp @@ -0,0 +1,424 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/cmdline/ui.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include <sys/param.h> +#include <sys/ioctl.h> + +#include <fcntl.h> +#if defined(HAVE_TERMIOS_H) +# include <termios.h> +#endif +#include <unistd.h> +} + +#include <cerrno> +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/text/table.hpp" + +namespace cmdline = utils::cmdline; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Reopens stdout as a tty and returns its width. +/// +/// \return The width of the tty in columns. If the width is wider than 80, the +/// result is 5 columns narrower to match the screen_width() algorithm. +static std::size_t +reopen_stdout(void) +{ + const int fd = ::open("/dev/tty", O_WRONLY); + if (fd == -1) + ATF_SKIP(F("Cannot open tty for test: %s") % ::strerror(errno)); + struct ::winsize ws; + if (::ioctl(fd, TIOCGWINSZ, &ws) == -1) + ATF_SKIP(F("Cannot determine size of tty: %s") % ::strerror(errno)); + + if (fd != STDOUT_FILENO) { + if (::dup2(fd, STDOUT_FILENO) == -1) + ATF_SKIP(F("Failed to redirect stdout: %s") % ::strerror(errno)); + ::close(fd); + } + + return ws.ws_col >= 80 ? ws.ws_col - 5 : ws.ws_col; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_set__no_tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_set__no_tty) +{ + utils::setenv("COLUMNS", "4321"); + ::close(STDOUT_FILENO); + + cmdline::ui ui; + ATF_REQUIRE_EQ(4321 - 5, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_set__tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_set__tty) +{ + utils::setenv("COLUMNS", "4321"); + (void)reopen_stdout(); + + cmdline::ui ui; + ATF_REQUIRE_EQ(4321 - 5, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_empty__no_tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_empty__no_tty) +{ + utils::setenv("COLUMNS", ""); + ::close(STDOUT_FILENO); + + cmdline::ui ui; + ATF_REQUIRE(!ui.screen_width()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_empty__tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_empty__tty) +{ + utils::setenv("COLUMNS", ""); + const std::size_t columns = reopen_stdout(); + + cmdline::ui ui; + ATF_REQUIRE_EQ(columns, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_invalid__no_tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_invalid__no_tty) +{ + utils::setenv("COLUMNS", "foo bar"); + ::close(STDOUT_FILENO); + + cmdline::ui ui; + ATF_REQUIRE(!ui.screen_width()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_invalid__tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_invalid__tty) +{ + utils::setenv("COLUMNS", "foo bar"); + const std::size_t columns = reopen_stdout(); + + cmdline::ui ui; + ATF_REQUIRE_EQ(columns, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__tty_is_file); +ATF_TEST_CASE_BODY(ui__screen_width__tty_is_file) +{ + utils::unsetenv("COLUMNS"); + const int fd = ::open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0755); + ATF_REQUIRE(fd != -1); + if (fd != STDOUT_FILENO) { + ATF_REQUIRE(::dup2(fd, STDOUT_FILENO) != -1); + ::close(fd); + } + + cmdline::ui ui; + ATF_REQUIRE(!ui.screen_width()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__cached); +ATF_TEST_CASE_BODY(ui__screen_width__cached) +{ + cmdline::ui ui; + + utils::setenv("COLUMNS", "100"); + ATF_REQUIRE_EQ(100 - 5, ui.screen_width().get()); + + utils::setenv("COLUMNS", "80"); + ATF_REQUIRE_EQ(100 - 5, ui.screen_width().get()); + + utils::unsetenv("COLUMNS"); + ATF_REQUIRE_EQ(100 - 5, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__err); +ATF_TEST_CASE_BODY(ui__err) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.err("This is a short message"); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("This is a short message", ui.err_log()[0]); + ATF_REQUIRE(ui.out_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__err__tolerates_newline); +ATF_TEST_CASE_BODY(ui__err__tolerates_newline) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.err("This is a short message\n"); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("This is a short message\n", ui.err_log()[0]); + ATF_REQUIRE(ui.out_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out); +ATF_TEST_CASE_BODY(ui__out) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.out("This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out__tolerates_newline); +ATF_TEST_CASE_BODY(ui__out__tolerates_newline) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.out("This is a short message\n"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short message\n", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_wrap__no_refill); +ATF_TEST_CASE_BODY(ui__out_wrap__no_refill) +{ + cmdline::ui_mock ui(100); + ui.out_wrap("This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_wrap__refill); +ATF_TEST_CASE_BODY(ui__out_wrap__refill) +{ + cmdline::ui_mock ui(16); + ui.out_wrap("This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short", ui.out_log()[0]); + ATF_REQUIRE_EQ("message", ui.out_log()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__no_refill); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__no_refill) +{ + cmdline::ui_mock ui(100); + ui.out_tag_wrap("Some long tag: ", "This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__refill__repeat); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__refill__repeat) +{ + cmdline::ui_mock ui(32); + ui.out_tag_wrap("Some long tag: ", "This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short", ui.out_log()[0]); + ATF_REQUIRE_EQ("Some long tag: message", ui.out_log()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__refill__no_repeat); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__refill__no_repeat) +{ + cmdline::ui_mock ui(32); + ui.out_tag_wrap("Some long tag: ", "This is a short message", false); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short", ui.out_log()[0]); + ATF_REQUIRE_EQ(" message", ui.out_log()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__tag_too_long); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__tag_too_long) +{ + cmdline::ui_mock ui(5); + ui.out_tag_wrap("Some long tag: ", "This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_table__empty); +ATF_TEST_CASE_BODY(ui__out_table__empty) +{ + const text::table table(3); + + text::table_formatter formatter; + formatter.set_separator(" | "); + formatter.set_column_width(0, 23); + formatter.set_column_width(1, text::table_formatter::width_refill); + + cmdline::ui_mock ui(52); + ui.out_table(table, formatter, " "); + ATF_REQUIRE(ui.out_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_table__not_empty); +ATF_TEST_CASE_BODY(ui__out_table__not_empty) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + text::table_formatter formatter; + formatter.set_separator(" | "); + formatter.set_column_width(0, 23); + formatter.set_column_width(1, text::table_formatter::width_refill); + + cmdline::ui_mock ui(52); + ui.out_table(table, formatter, " "); + ATF_REQUIRE_EQ(4, ui.out_log().size()); + ATF_REQUIRE_EQ(" First | Second | Third", + ui.out_log()[0]); + ATF_REQUIRE_EQ(" Fourth with some text | Fifth with | Sixth foo", + ui.out_log()[1]); + ATF_REQUIRE_EQ(" | some more | ", + ui.out_log()[2]); + ATF_REQUIRE_EQ(" | text | ", + ui.out_log()[3]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(print_error); +ATF_TEST_CASE_BODY(print_error) +{ + cmdline::init("error-program"); + cmdline::ui_mock ui; + cmdline::print_error(&ui, "The error."); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("error-program: E: The error.", ui.err_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(print_info); +ATF_TEST_CASE_BODY(print_info) +{ + cmdline::init("info-program"); + cmdline::ui_mock ui; + cmdline::print_info(&ui, "The info."); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("info-program: I: The info.", ui.err_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(print_warning); +ATF_TEST_CASE_BODY(print_warning) +{ + cmdline::init("warning-program"); + cmdline::ui_mock ui; + cmdline::print_warning(&ui, "The warning."); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("warning-program: W: The warning.", ui.err_log()[0]); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_set__no_tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_set__tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_empty__no_tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_empty__tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_invalid__no_tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_invalid__tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__tty_is_file); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__cached); + + ATF_ADD_TEST_CASE(tcs, ui__err); + ATF_ADD_TEST_CASE(tcs, ui__err__tolerates_newline); + ATF_ADD_TEST_CASE(tcs, ui__out); + ATF_ADD_TEST_CASE(tcs, ui__out__tolerates_newline); + + ATF_ADD_TEST_CASE(tcs, ui__out_wrap__no_refill); + ATF_ADD_TEST_CASE(tcs, ui__out_wrap__refill); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__no_refill); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__refill__repeat); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__refill__no_repeat); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__tag_too_long); + ATF_ADD_TEST_CASE(tcs, ui__out_table__empty); + ATF_ADD_TEST_CASE(tcs, ui__out_table__not_empty); + + ATF_ADD_TEST_CASE(tcs, print_error); + ATF_ADD_TEST_CASE(tcs, print_info); + ATF_ADD_TEST_CASE(tcs, print_warning); +} diff --git a/utils/config/Kyuafile b/utils/config/Kyuafile new file mode 100644 index 000000000000..c607a1757275 --- /dev/null +++ b/utils/config/Kyuafile @@ -0,0 +1,10 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="exceptions_test"} +atf_test_program{name="keys_test"} +atf_test_program{name="lua_module_test"} +atf_test_program{name="nodes_test"} +atf_test_program{name="parser_test"} +atf_test_program{name="tree_test"} diff --git a/utils/config/Makefile.am.inc b/utils/config/Makefile.am.inc new file mode 100644 index 000000000000..7c276ec4e798 --- /dev/null +++ b/utils/config/Makefile.am.inc @@ -0,0 +1,87 @@ +# Copyright 2012 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +UTILS_CFLAGS += $(LUTOK_CFLAGS) +UTILS_LIBS += $(LUTOK_LIBS) + +libutils_a_CPPFLAGS += $(LUTOK_CFLAGS) +libutils_a_SOURCES += utils/config/exceptions.cpp +libutils_a_SOURCES += utils/config/exceptions.hpp +libutils_a_SOURCES += utils/config/keys.cpp +libutils_a_SOURCES += utils/config/keys.hpp +libutils_a_SOURCES += utils/config/keys_fwd.hpp +libutils_a_SOURCES += utils/config/lua_module.cpp +libutils_a_SOURCES += utils/config/lua_module.hpp +libutils_a_SOURCES += utils/config/nodes.cpp +libutils_a_SOURCES += utils/config/nodes.hpp +libutils_a_SOURCES += utils/config/nodes.ipp +libutils_a_SOURCES += utils/config/nodes_fwd.hpp +libutils_a_SOURCES += utils/config/parser.cpp +libutils_a_SOURCES += utils/config/parser.hpp +libutils_a_SOURCES += utils/config/parser_fwd.hpp +libutils_a_SOURCES += utils/config/tree.cpp +libutils_a_SOURCES += utils/config/tree.hpp +libutils_a_SOURCES += utils/config/tree.ipp +libutils_a_SOURCES += utils/config/tree_fwd.hpp + +if WITH_ATF +tests_utils_configdir = $(pkgtestsdir)/utils/config + +tests_utils_config_DATA = utils/config/Kyuafile +EXTRA_DIST += $(tests_utils_config_DATA) + +tests_utils_config_PROGRAMS = utils/config/exceptions_test +utils_config_exceptions_test_SOURCES = utils/config/exceptions_test.cpp +utils_config_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/keys_test +utils_config_keys_test_SOURCES = utils/config/keys_test.cpp +utils_config_keys_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_keys_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/lua_module_test +utils_config_lua_module_test_SOURCES = utils/config/lua_module_test.cpp +utils_config_lua_module_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_lua_module_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/nodes_test +utils_config_nodes_test_SOURCES = utils/config/nodes_test.cpp +utils_config_nodes_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_nodes_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/parser_test +utils_config_parser_test_SOURCES = utils/config/parser_test.cpp +utils_config_parser_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_parser_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/tree_test +utils_config_tree_test_SOURCES = utils/config/tree_test.cpp +utils_config_tree_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_tree_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/config/exceptions.cpp b/utils/config/exceptions.cpp new file mode 100644 index 000000000000..e9afdf7ea6f7 --- /dev/null +++ b/utils/config/exceptions.cpp @@ -0,0 +1,149 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/exceptions.hpp" + +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +config::error::~error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param key The key that caused the combination conflict. +/// \param format The plain-text error message. +config::bad_combination_error::bad_combination_error( + const detail::tree_key& key, const std::string& format) : + error(F(format.empty() ? "Combination conflict in key '%s'" : format) % + detail::flatten_key(key)) +{ +} + + +/// Destructor for the error. +config::bad_combination_error::~bad_combination_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::invalid_key_error::invalid_key_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +config::invalid_key_error::~invalid_key_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param key The unknown key. +/// \param message The plain-text error message. +config::invalid_key_value::invalid_key_value(const detail::tree_key& key, + const std::string& message) : + error(F("Invalid value for property '%s': %s") + % detail::flatten_key(key) % message) +{ +} + + +/// Destructor for the error. +config::invalid_key_value::~invalid_key_value(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::syntax_error::syntax_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +config::syntax_error::~syntax_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param key The unknown key. +/// \param format The message for the error. Must include a single "%s" +/// placedholder, which will be replaced by the key itself. +config::unknown_key_error::unknown_key_error(const detail::tree_key& key, + const std::string& format) : + error(F(format.empty() ? "Unknown configuration property '%s'" : format) % + detail::flatten_key(key)) +{ +} + + +/// Destructor for the error. +config::unknown_key_error::~unknown_key_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::value_error::value_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +config::value_error::~value_error(void) throw() +{ +} diff --git a/utils/config/exceptions.hpp b/utils/config/exceptions.hpp new file mode 100644 index 000000000000..2096e67f43c8 --- /dev/null +++ b/utils/config/exceptions.hpp @@ -0,0 +1,106 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/exceptions.hpp +/// Exception types raised by the config module. + +#if !defined(UTILS_CONFIG_EXCEPTIONS_HPP) +#define UTILS_CONFIG_EXCEPTIONS_HPP + +#include <stdexcept> + +#include "utils/config/keys_fwd.hpp" +#include "utils/config/tree_fwd.hpp" + +namespace utils { +namespace config { + + +/// Base exceptions for config errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Exception denoting that two trees cannot be combined. +class bad_combination_error : public error { +public: + explicit bad_combination_error(const detail::tree_key&, + const std::string&); + ~bad_combination_error(void) throw(); +}; + + +/// Exception denoting that a key was not found within a tree. +class invalid_key_error : public error { +public: + explicit invalid_key_error(const std::string&); + ~invalid_key_error(void) throw(); +}; + + +/// Exception denoting that a key was given an invalid value. +class invalid_key_value : public error { +public: + explicit invalid_key_value(const detail::tree_key&, const std::string&); + ~invalid_key_value(void) throw(); +}; + + +/// Exception denoting that a configuration file is invalid. +class syntax_error : public error { +public: + explicit syntax_error(const std::string&); + ~syntax_error(void) throw(); +}; + + +/// Exception denoting that a key was not found within a tree. +class unknown_key_error : public error { +public: + explicit unknown_key_error(const detail::tree_key&, + const std::string& = ""); + ~unknown_key_error(void) throw(); +}; + + +/// Exception denoting that a value was invalid. +class value_error : public error { +public: + explicit value_error(const std::string&); + ~value_error(void) throw(); +}; + + +} // namespace config +} // namespace utils + + +#endif // !defined(UTILS_CONFIG_EXCEPTIONS_HPP) diff --git a/utils/config/exceptions_test.cpp b/utils/config/exceptions_test.cpp new file mode 100644 index 000000000000..a82fb9ea8f0c --- /dev/null +++ b/utils/config/exceptions_test.cpp @@ -0,0 +1,133 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/exceptions.hpp" + +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/config/tree.ipp" + +namespace config = utils::config; +namespace detail = utils::config::detail; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const config::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bad_combination_error); +ATF_TEST_CASE_BODY(bad_combination_error) +{ + detail::tree_key key; + key.push_back("first"); + key.push_back("second"); + + const config::bad_combination_error e(key, "Failed to combine '%s'"); + ATF_REQUIRE(std::strcmp("Failed to combine 'first.second'", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_key_error); +ATF_TEST_CASE_BODY(invalid_key_error) +{ + const config::invalid_key_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_key_value); +ATF_TEST_CASE_BODY(invalid_key_value) +{ + detail::tree_key key; + key.push_back("1"); + key.push_back("two"); + + const config::invalid_key_value e(key, "foo bar"); + ATF_REQUIRE(std::strcmp("Invalid value for property '1.two': foo bar", + e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_error); +ATF_TEST_CASE_BODY(syntax_error) +{ + const config::syntax_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_key_error__default_message); +ATF_TEST_CASE_BODY(unknown_key_error__default_message) +{ + detail::tree_key key; + key.push_back("1"); + key.push_back("two"); + + const config::unknown_key_error e(key); + ATF_REQUIRE(std::strcmp("Unknown configuration property '1.two'", + e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_key_error__custom_message); +ATF_TEST_CASE_BODY(unknown_key_error__custom_message) +{ + detail::tree_key key; + key.push_back("1"); + key.push_back("two"); + + const config::unknown_key_error e(key, "The test '%s' string"); + ATF_REQUIRE(std::strcmp("The test '1.two' string", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(value_error); +ATF_TEST_CASE_BODY(value_error) +{ + const config::value_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, bad_combination_error); + ATF_ADD_TEST_CASE(tcs, invalid_key_error); + ATF_ADD_TEST_CASE(tcs, invalid_key_value); + ATF_ADD_TEST_CASE(tcs, syntax_error); + ATF_ADD_TEST_CASE(tcs, unknown_key_error__default_message); + ATF_ADD_TEST_CASE(tcs, unknown_key_error__custom_message); + ATF_ADD_TEST_CASE(tcs, value_error); +} diff --git a/utils/config/keys.cpp b/utils/config/keys.cpp new file mode 100644 index 000000000000..574eee14dcd2 --- /dev/null +++ b/utils/config/keys.cpp @@ -0,0 +1,70 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/tree.ipp" + +#include "utils/config/exceptions.hpp" +#include "utils/format/macros.hpp" +#include "utils/text/operations.hpp" + +namespace config = utils::config; +namespace text = utils::text; + + +/// Converts a key to its textual representation. +/// +/// \param key The key to convert. +/// +/// \return a flattened representation of \p key, "."-joined. +std::string +utils::config::detail::flatten_key(const tree_key& key) +{ + PRE(!key.empty()); + return text::join(key, "."); +} + + +/// Parses and validates a textual key. +/// +/// \param str The key to process in dotted notation. +/// +/// \return The tokenized key if valid. +/// +/// \throw invalid_key_error If the input key is empty or invalid for any other +/// reason. Invalid does NOT mean unknown though. +utils::config::detail::tree_key +utils::config::detail::parse_key(const std::string& str) +{ + const tree_key key = text::split(str, '.'); + if (key.empty()) + throw invalid_key_error("Empty key"); + for (tree_key::const_iterator iter = key.begin(); iter != key.end(); iter++) + if ((*iter).empty()) + throw invalid_key_error(F("Empty component in key '%s'") % str); + return key; +} diff --git a/utils/config/keys.hpp b/utils/config/keys.hpp new file mode 100644 index 000000000000..ad258d69fc08 --- /dev/null +++ b/utils/config/keys.hpp @@ -0,0 +1,52 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/keys.hpp +/// Representation and manipulation of tree keys. + +#if !defined(UTILS_CONFIG_KEYS_HPP) +#define UTILS_CONFIG_KEYS_HPP + +#include "utils/config/keys_fwd.hpp" + +#include <string> + +namespace utils { +namespace config { +namespace detail { + + +std::string flatten_key(const tree_key&); +tree_key parse_key(const std::string&); + + +} // namespace detail +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_KEYS_HPP) diff --git a/utils/config/keys_fwd.hpp b/utils/config/keys_fwd.hpp new file mode 100644 index 000000000000..101272698b65 --- /dev/null +++ b/utils/config/keys_fwd.hpp @@ -0,0 +1,51 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/keys_fwd.hpp +/// Forward declarations for utils/config/keys.hpp + +#if !defined(UTILS_CONFIG_KEYS_FWD_HPP) +#define UTILS_CONFIG_KEYS_FWD_HPP + +#include <string> +#include <vector> + +namespace utils { +namespace config { +namespace detail { + + +/// Representation of a valid, tokenized key. +typedef std::vector< std::string > tree_key; + + +} // namespace detail +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_KEYS_FWD_HPP) diff --git a/utils/config/keys_test.cpp b/utils/config/keys_test.cpp new file mode 100644 index 000000000000..dc30f0fc8806 --- /dev/null +++ b/utils/config/keys_test.cpp @@ -0,0 +1,114 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/keys.hpp" + +#include <atf-c++.hpp> + +#include "utils/config/exceptions.hpp" + +namespace config = utils::config; + + +ATF_TEST_CASE_WITHOUT_HEAD(flatten_key__one); +ATF_TEST_CASE_BODY(flatten_key__one) +{ + config::detail::tree_key key; + key.push_back("foo"); + ATF_REQUIRE_EQ("foo", config::detail::flatten_key(key)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(flatten_key__many); +ATF_TEST_CASE_BODY(flatten_key__many) +{ + config::detail::tree_key key; + key.push_back("foo"); + key.push_back("1"); + key.push_back("bar"); + ATF_REQUIRE_EQ("foo.1.bar", config::detail::flatten_key(key)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__one); +ATF_TEST_CASE_BODY(parse_key__one) +{ + config::detail::tree_key exp_key; + exp_key.push_back("one"); + ATF_REQUIRE(exp_key == config::detail::parse_key("one")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__many); +ATF_TEST_CASE_BODY(parse_key__many) +{ + config::detail::tree_key exp_key; + exp_key.push_back("one"); + exp_key.push_back("2"); + exp_key.push_back("foo"); + ATF_REQUIRE(exp_key == config::detail::parse_key("one.2.foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__empty_key); +ATF_TEST_CASE_BODY(parse_key__empty_key) +{ + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty key", + config::detail::parse_key("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__empty_component); +ATF_TEST_CASE_BODY(parse_key__empty_component) +{ + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key '.'", + config::detail::parse_key(".")); + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key 'a.'", + config::detail::parse_key("a.")); + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key '.b'", + config::detail::parse_key(".b")); + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key 'a..b'", + config::detail::parse_key("a..b")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, flatten_key__one); + ATF_ADD_TEST_CASE(tcs, flatten_key__many); + + ATF_ADD_TEST_CASE(tcs, parse_key__one); + ATF_ADD_TEST_CASE(tcs, parse_key__many); + ATF_ADD_TEST_CASE(tcs, parse_key__empty_key); + ATF_ADD_TEST_CASE(tcs, parse_key__empty_component); +} diff --git a/utils/config/lua_module.cpp b/utils/config/lua_module.cpp new file mode 100644 index 000000000000..891f07302e0a --- /dev/null +++ b/utils/config/lua_module.cpp @@ -0,0 +1,282 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/lua_module.hpp" + +#include <lutok/stack_cleaner.hpp> +#include <lutok/state.ipp> + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/config/tree.ipp" + +namespace config = utils::config; +namespace detail = utils::config::detail; + + +namespace { + + +/// Gets the tree singleton stored in the Lua state. +/// +/// \param state The Lua state. The registry must contain a key named +/// "tree" with a pointer to the singleton. +/// +/// \return A reference to the tree associated with the Lua state. +/// +/// \throw syntax_error If the tree cannot be located. +config::tree& +get_global_tree(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + state.push_value(lutok::registry_index); + state.push_string("tree"); + state.get_table(-2); + if (state.is_nil(-1)) + throw config::syntax_error("Cannot find tree singleton; global state " + "corrupted?"); + config::tree& tree = **state.to_userdata< config::tree* >(-1); + state.pop(1); + return tree; +} + + +/// Gets a fully-qualified tree key from the state. +/// +/// \param state The Lua state. +/// \param table_index An index to the Lua stack pointing to the table being +/// accessed. If this table contains a tree_key metadata property, this is +/// considered to be the prefix of the tree key. +/// \param field_index An index to the Lua stack pointing to the entry +/// containing the name of the field being indexed. +/// +/// \return A dotted key. +/// +/// \throw invalid_key_error If the name of the key is invalid. +static std::string +get_tree_key(lutok::state& state, const int table_index, const int field_index) +{ + PRE(state.is_string(field_index)); + const std::string field = state.to_string(field_index); + if (!field.empty() && field[0] == '_') + throw config::invalid_key_error( + F("Configuration key cannot have an underscore as a prefix; " + "found %s") % field); + + std::string tree_key; + if (state.get_metafield(table_index, "tree_key")) { + tree_key = state.to_string(-1) + "." + state.to_string(field_index - 1); + state.pop(1); + } else + tree_key = state.to_string(field_index); + return tree_key; +} + + +static int redirect_newindex(lutok::state&); +static int redirect_index(lutok::state&); + + +/// Creates a table for a new configuration inner node. +/// +/// \post state(-1) Contains the new table. +/// +/// \param state The Lua state in which to push the table. +/// \param tree_key The key to which the new table corresponds. +static void +new_table_for_key(lutok::state& state, const std::string& tree_key) +{ + state.new_table(); + { + state.new_table(); + { + state.push_string("__index"); + state.push_cxx_function(redirect_index); + state.set_table(-3); + + state.push_string("__newindex"); + state.push_cxx_function(redirect_newindex); + state.set_table(-3); + + state.push_string("tree_key"); + state.push_string(tree_key); + state.set_table(-3); + } + state.set_metatable(-2); + } +} + + +/// Sets the value of an configuration node. +/// +/// \pre state(-3) The table to index. If this is not _G, then the table +/// metadata must contain a tree_key property describing the path to +/// current level. +/// \pre state(-2) The field to index into the table. Must be a string. +/// \pre state(-1) The value to set the indexed table field to. +/// +/// \param state The Lua state in which to operate. +/// +/// \return The number of result values on the Lua stack; always 0. +/// +/// \throw invalid_key_error If the provided key is invalid. +/// \throw unknown_key_error If the key cannot be located. +/// \throw value_error If the value has an unsupported type or cannot be +/// set on the key, or if the input table or index are invalid. +static int +redirect_newindex(lutok::state& state) +{ + if (!state.is_table(-3)) + throw config::value_error("Indexed object is not a table"); + if (!state.is_string(-2)) + throw config::value_error("Invalid field in configuration object " + "reference; must be a string"); + + const std::string dotted_key = get_tree_key(state, -3, -2); + try { + config::tree& tree = get_global_tree(state); + tree.set_lua(dotted_key, state, -1); + } catch (const config::value_error& e) { + throw config::invalid_key_value(detail::parse_key(dotted_key), + e.what()); + } + + // Now really set the key in the Lua table, but prevent direct accesses from + // the user by prefixing it. We do this to ensure that re-setting the same + // key of the tree results in a call to __newindex instead of __index. + state.push_string("_" + state.to_string(-2)); + state.push_value(-2); + state.raw_set(-5); + + return 0; +} + + +/// Indexes a configuration node. +/// +/// \pre state(-3) The table to index. If this is not _G, then the table +/// metadata must contain a tree_key property describing the path to +/// current level. If the field does not exist, a new table is created. +/// \pre state(-1) The field to index into the table. Must be a string. +/// +/// \param state The Lua state in which to operate. +/// +/// \return The number of result values on the Lua stack; always 1. +/// +/// \throw value_error If the input table or index are invalid. +static int +redirect_index(lutok::state& state) +{ + if (!state.is_table(-2)) + throw config::value_error("Indexed object is not a table"); + if (!state.is_string(-1)) + throw config::value_error("Invalid field in configuration object " + "reference; must be a string"); + + // Query if the key has already been set by a call to redirect_newindex. + state.push_string("_" + state.to_string(-1)); + state.raw_get(-3); + if (!state.is_nil(-1)) + return 1; + state.pop(1); + + state.push_value(-1); // Duplicate the field name. + state.raw_get(-3); // Get table[field] to see if it's defined. + if (state.is_nil(-1)) { + state.pop(1); + + // The stack is now the same as when we entered the function, but we + // know that the field is undefined and thus have to create a new + // configuration table. + INV(state.is_table(-2)); + INV(state.is_string(-1)); + + const config::tree& tree = get_global_tree(state); + const std::string tree_key = get_tree_key(state, -2, -1); + if (tree.is_set(tree_key)) { + // Publish the pre-recorded value in the tree to the Lua state, + // instead of considering this table key a new inner node. + tree.push_lua(tree_key, state); + } else { + state.push_string("_" + state.to_string(-1)); + state.insert(-2); + state.pop(1); + + new_table_for_key(state, tree_key); + + // Duplicate the newly created table and place it deep in the stack + // so that the raw_set below leaves us with the return value of this + // function at the top of the stack. + state.push_value(-1); + state.insert(-4); + + state.raw_set(-3); + state.pop(1); + } + } + return 1; +} + + +} // anonymous namespace + + +/// Install wrappers for globals to set values in the configuration tree. +/// +/// This function installs wrappers to capture all accesses to global variables. +/// Such wrappers redirect the reads and writes to the out_tree, which is the +/// entity that defines what configuration variables exist. +/// +/// \param state The Lua state into which to install the wrappers. +/// \param out_tree The tree with the layout definition and where the +/// configuration settings will be collected. +void +config::redirect(lutok::state& state, tree& out_tree) +{ + lutok::stack_cleaner cleaner(state); + + state.get_global_table(); + { + state.push_string("__index"); + state.push_cxx_function(redirect_index); + state.set_table(-3); + + state.push_string("__newindex"); + state.push_cxx_function(redirect_newindex); + state.set_table(-3); + } + state.set_metatable(-1); + + state.push_value(lutok::registry_index); + state.push_string("tree"); + config::tree** tree = state.new_userdata< config::tree* >(); + *tree = &out_tree; + state.set_table(-3); + state.pop(1); +} diff --git a/utils/config/lua_module.hpp b/utils/config/lua_module.hpp new file mode 100644 index 000000000000..7f0d5d0b4c5f --- /dev/null +++ b/utils/config/lua_module.hpp @@ -0,0 +1,50 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/lua_module.hpp +/// Bindings to expose a configuration tree to Lua. + +#if !defined(UTILS_CONFIG_LUA_MODULE_HPP) +#define UTILS_CONFIG_LUA_MODULE_HPP + +#include <string> + +#include "lutok/state.hpp" +#include "utils/config/tree_fwd.hpp" + +namespace utils { +namespace config { + + +void redirect(lutok::state&, tree&); + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_LUA_MODULE_HPP) diff --git a/utils/config/lua_module_test.cpp b/utils/config/lua_module_test.cpp new file mode 100644 index 000000000000..484d129c4021 --- /dev/null +++ b/utils/config/lua_module_test.cpp @@ -0,0 +1,474 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/lua_module.hpp" + +#include <atf-c++.hpp> + +#include <lutok/exceptions.hpp> +#include <lutok/operations.hpp> +#include <lutok/state.ipp> + +#include "utils/config/tree.ipp" +#include "utils/defs.hpp" + +namespace config = utils::config; + + +namespace { + + +/// Non-native type to use as a leaf node. +struct custom_type { + /// The value recorded in the object. + int value; + + /// Constructs a new object. + /// + /// \param value_ The value to store in the object. + explicit custom_type(const int value_) : + value(value_) + { + } +}; + + +/// Custom implementation of a node type for testing purposes. +class custom_node : public config::typed_leaf_node< custom_type > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< custom_node > new_node(new custom_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Pushes the node's value onto the Lua stack. + /// + /// \param state The Lua state onto which to push the value. + void + push_lua(lutok::state& state) const + { + state.push_integer(value().value * 5); + } + + /// Sets the value of the node from an entry in the Lua stack. + /// + /// \param state The Lua state from which to get the value. + /// \param value_index The stack index in which the value resides. + void + set_lua(lutok::state& state, const int value_index) + { + ATF_REQUIRE(state.is_number(value_index)); + set(custom_type(state.to_integer(value_index) * 2)); + } + + /// Sets the value of the node from a raw string representation. + /// + /// \post The test case is marked as failed, as this function is not + /// supposed to be invoked by the lua_module code. + void + set_string(const std::string& /* raw_value */) + { + ATF_FAIL("Should not be used"); + } + + /// Converts the contents of the node to a string. + /// + /// \post The test case is marked as failed, as this function is not + /// supposed to be invoked by the lua_module code. + /// + /// \return Nothing. + std::string + to_string(void) const + { + ATF_FAIL("Should not be used"); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(top__valid_types); +ATF_TEST_CASE_BODY(top__valid_types) +{ + config::tree tree; + tree.define< config::bool_node >("top_boolean"); + tree.define< config::int_node >("top_integer"); + tree.define< config::string_node >("top_string"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "top_boolean = true\n" + "top_integer = 12345\n" + "top_string = 'a foo'\n", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(true, tree.lookup< config::bool_node >("top_boolean")); + ATF_REQUIRE_EQ(12345, tree.lookup< config::int_node >("top_integer")); + ATF_REQUIRE_EQ("a foo", tree.lookup< config::string_node >("top_string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__invalid_types); +ATF_TEST_CASE_BODY(top__invalid_types) +{ + config::tree tree; + tree.define< config::bool_node >("top_boolean"); + tree.define< config::int_node >("top_integer"); + + { + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE( + lutok::error, + "Invalid value for property 'top_boolean': Not a boolean", + lutok::do_string(state, + "top_boolean = true\n" + "top_integer = 8\n" + "top_boolean = 'foo'\n", + 0, 0, 0)); + } + + ATF_REQUIRE_EQ(true, tree.lookup< config::bool_node >("top_boolean")); + ATF_REQUIRE_EQ(8, tree.lookup< config::int_node >("top_integer")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__reuse); +ATF_TEST_CASE_BODY(top__reuse) +{ + config::tree tree; + tree.define< config::int_node >("first"); + tree.define< config::int_node >("second"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "first = 100; second = first * 2", 0, 0, 0); + } + + ATF_REQUIRE_EQ(100, tree.lookup< config::int_node >("first")); + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("second")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__reset); +ATF_TEST_CASE_BODY(top__reset) +{ + config::tree tree; + tree.define< config::int_node >("first"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "first = 100; first = 200", 0, 0, 0); + } + + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__already_set_on_entry); +ATF_TEST_CASE_BODY(top__already_set_on_entry) +{ + config::tree tree; + tree.define< config::int_node >("first"); + tree.set< config::int_node >("first", 100); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "first = first * 15", 0, 0, 0); + } + + ATF_REQUIRE_EQ(1500, tree.lookup< config::int_node >("first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__valid_types); +ATF_TEST_CASE_BODY(subtree__valid_types) +{ + config::tree tree; + tree.define< config::bool_node >("root.boolean"); + tree.define< config::int_node >("root.a.integer"); + tree.define< config::string_node >("root.string"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "root.boolean = true\n" + "root.a.integer = 12345\n" + "root.string = 'a foo'\n", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(true, tree.lookup< config::bool_node >("root.boolean")); + ATF_REQUIRE_EQ(12345, tree.lookup< config::int_node >("root.a.integer")); + ATF_REQUIRE_EQ("a foo", tree.lookup< config::string_node >("root.string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__reuse); +ATF_TEST_CASE_BODY(subtree__reuse) +{ + config::tree tree; + tree.define< config::int_node >("a.first"); + tree.define< config::int_node >("a.second"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "a.first = 100; a.second = a.first * 2", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(100, tree.lookup< config::int_node >("a.first")); + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("a.second")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__reset); +ATF_TEST_CASE_BODY(subtree__reset) +{ + config::tree tree; + tree.define< config::int_node >("a.first"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "a.first = 100; a.first = 200", 0, 0, 0); + } + + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("a.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__already_set_on_entry); +ATF_TEST_CASE_BODY(subtree__already_set_on_entry) +{ + config::tree tree; + tree.define< config::int_node >("a.first"); + tree.set< config::int_node >("a.first", 100); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "a.first = a.first * 15", 0, 0, 0); + } + + ATF_REQUIRE_EQ(1500, tree.lookup< config::int_node >("a.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__override_inner); +ATF_TEST_CASE_BODY(subtree__override_inner) +{ + config::tree tree; + tree.define_dynamic("root"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "root.test = 'a'", 0, 0, 0); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid value for property 'root'", + lutok::do_string(state, "root = 'b'", 0, 0, 0)); + // Ensure that the previous assignment to 'root' did not cause any + // inconsistencies in the environment that would prevent a new + // assignment from working. + lutok::do_string(state, "root.test2 = 'c'", 0, 0, 0); + } + + ATF_REQUIRE_EQ("a", tree.lookup< config::string_node >("root.test")); + ATF_REQUIRE_EQ("c", tree.lookup< config::string_node >("root.test2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dynamic_subtree__strings); +ATF_TEST_CASE_BODY(dynamic_subtree__strings) +{ + config::tree tree; + tree.define_dynamic("root"); + + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "root.key1 = 1234\n" + "root.a.b.key2 = 'foo bar'\n", + 0, 0, 0); + + ATF_REQUIRE_EQ("1234", tree.lookup< config::string_node >("root.key1")); + ATF_REQUIRE_EQ("foo bar", + tree.lookup< config::string_node >("root.a.b.key2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dynamic_subtree__invalid_types); +ATF_TEST_CASE_BODY(dynamic_subtree__invalid_types) +{ + config::tree tree; + tree.define_dynamic("root"); + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'root.boolean': " + "Not a string", + lutok::do_string(state, "root.boolean = true", + 0, 0, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'root.table': " + "Not a string", + lutok::do_string(state, "root.table = {}", + 0, 0, 0)); + ATF_REQUIRE(!tree.is_set("root.boolean")); + ATF_REQUIRE(!tree.is_set("root.table")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(locals); +ATF_TEST_CASE_BODY(locals) +{ + config::tree tree; + tree.define< config::int_node >("the_key"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "local function generate()\n" + " return 15\n" + "end\n" + "local test_var = 20\n" + "the_key = generate() + test_var\n", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(35, tree.lookup< config::int_node >("the_key")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(custom_node); +ATF_TEST_CASE_BODY(custom_node) +{ + config::tree tree; + tree.define< custom_node >("key1"); + tree.define< custom_node >("key2"); + tree.set< custom_node >("key2", custom_type(10)); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "key1 = 512\n", 0, 0, 0); + lutok::do_string(state, "key2 = key2 * 2\n", 0, 0, 0); + } + + ATF_REQUIRE_EQ(1024, tree.lookup< custom_node >("key1").value); + ATF_REQUIRE_EQ(200, tree.lookup< custom_node >("key2").value); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_key); +ATF_TEST_CASE_BODY(invalid_key) +{ + config::tree tree; + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, "Empty component in key 'root.'", + lutok::do_string(state, "root['']['a'] = 12345\n", + 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_key); +ATF_TEST_CASE_BODY(unknown_key) +{ + config::tree tree; + tree.define< config::bool_node >("static.bool"); + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, + "Unknown configuration property 'static.int'", + lutok::do_string(state, + "static.int = 12345\n", + 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(value_error); +ATF_TEST_CASE_BODY(value_error) +{ + config::tree tree; + tree.define< config::bool_node >("a.b"); + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'a.b': Not a boolean", + lutok::do_string(state, "a.b = 12345\n", 0, 0, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'a': ", + lutok::do_string(state, "a = 1\n", 0, 0, 0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, top__valid_types); + ATF_ADD_TEST_CASE(tcs, top__invalid_types); + ATF_ADD_TEST_CASE(tcs, top__reuse); + ATF_ADD_TEST_CASE(tcs, top__reset); + ATF_ADD_TEST_CASE(tcs, top__already_set_on_entry); + + ATF_ADD_TEST_CASE(tcs, subtree__valid_types); + ATF_ADD_TEST_CASE(tcs, subtree__reuse); + ATF_ADD_TEST_CASE(tcs, subtree__reset); + ATF_ADD_TEST_CASE(tcs, subtree__already_set_on_entry); + ATF_ADD_TEST_CASE(tcs, subtree__override_inner); + + ATF_ADD_TEST_CASE(tcs, dynamic_subtree__strings); + ATF_ADD_TEST_CASE(tcs, dynamic_subtree__invalid_types); + + ATF_ADD_TEST_CASE(tcs, locals); + ATF_ADD_TEST_CASE(tcs, custom_node); + + ATF_ADD_TEST_CASE(tcs, invalid_key); + ATF_ADD_TEST_CASE(tcs, unknown_key); + ATF_ADD_TEST_CASE(tcs, value_error); +} diff --git a/utils/config/nodes.cpp b/utils/config/nodes.cpp new file mode 100644 index 000000000000..1c6e848daf07 --- /dev/null +++ b/utils/config/nodes.cpp @@ -0,0 +1,589 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/nodes.ipp" + +#include <memory> + +#include <lutok/state.ipp> + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; + + +/// Destructor. +config::detail::base_node::~base_node(void) +{ +} + + +/// Constructor. +/// +/// \param dynamic_ Whether the node is dynamic or not. +config::detail::inner_node::inner_node(const bool dynamic_) : + _dynamic(dynamic_) +{ +} + + +/// Destructor. +config::detail::inner_node::~inner_node(void) +{ + for (children_map::const_iterator iter = _children.begin(); + iter != _children.end(); ++iter) + delete (*iter).second; +} + + +/// Fills the given node with a copy of this node's data. +/// +/// \param node The node to fill. Should be the fresh return value of a +/// deep_copy() operation. +void +config::detail::inner_node::copy_into(inner_node* node) const +{ + node->_dynamic = _dynamic; + for (children_map::const_iterator iter = _children.begin(); + iter != _children.end(); ++iter) { + base_node* new_node = (*iter).second->deep_copy(); + try { + node->_children[(*iter).first] = new_node; + } catch (...) { + delete new_node; + throw; + } + } +} + + +/// Combines two children sets, preferring the keys in the first set only. +/// +/// This operation is not symmetrical on c1 and c2. The caller is responsible +/// for invoking this twice so that the two key sets are combined if they happen +/// to differ. +/// +/// \param key Key to this node. +/// \param c1 First children set. +/// \param c2 First children set. +/// \param [in,out] node The node to combine into. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +void +config::detail::inner_node::combine_children_into( + const tree_key& key, + const children_map& c1, const children_map& c2, + inner_node* node) const +{ + for (children_map::const_iterator iter1 = c1.begin(); + iter1 != c1.end(); ++iter1) { + const std::string& name = (*iter1).first; + + if (node->_children.find(name) != node->_children.end()) { + continue; + } + + std::auto_ptr< base_node > new_node; + + children_map::const_iterator iter2 = c2.find(name); + if (iter2 == c2.end()) { + new_node.reset((*iter1).second->deep_copy()); + } else { + tree_key child_key = key; + child_key.push_back(name); + new_node.reset((*iter1).second->combine(child_key, + (*iter2).second)); + } + + node->_children[name] = new_node.release(); + } +} + + +/// Combines this inner node with another inner node onto a new node. +/// +/// The "dynamic" property is inherited by the new node if either of the two +/// nodes are dynamic. +/// +/// \param key Key to this node. +/// \param other_base The node to combine with. +/// \param [in,out] node The node to combine into. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +void +config::detail::inner_node::combine_into(const tree_key& key, + const base_node* other_base, + inner_node* node) const +{ + try { + const inner_node& other = dynamic_cast< const inner_node& >( + *other_base); + + node->_dynamic = _dynamic || other._dynamic; + + combine_children_into(key, _children, other._children, node); + combine_children_into(key, other._children, _children, node); + } catch (const std::bad_cast& unused_e) { + throw config::bad_combination_error( + key, "'%s' is an inner node in the base tree but a leaf node in " + "the overrides treee"); + } +} + + +/// Finds a node without creating it if not found. +/// +/// This recursive algorithm traverses the tree searching for a particular key. +/// The returned node is constant, so this can only be used for querying +/// purposes. For this reason, this algorithm does not create intermediate +/// nodes if they don't exist (as would be necessary to set a new node). +/// +/// \param key The key to be queried. +/// \param key_pos The current level within the key to be examined. +/// +/// \return A reference to the located node, if successful. +/// +/// \throw unknown_key_error If the provided key is unknown. +const config::detail::base_node* +config::detail::inner_node::lookup_ro(const tree_key& key, + const tree_key::size_type key_pos) const +{ + PRE(key_pos < key.size()); + + const children_map::const_iterator child_iter = _children.find( + key[key_pos]); + if (child_iter == _children.end()) + throw unknown_key_error(key); + + if (key_pos == key.size() - 1) { + return (*child_iter).second; + } else { + PRE(key_pos < key.size() - 1); + try { + const inner_node& child = dynamic_cast< const inner_node& >( + *(*child_iter).second); + return child.lookup_ro(key, key_pos + 1); + } catch (const std::bad_cast& e) { + throw unknown_key_error( + key, "Cannot address incomplete configuration property '%s'"); + } + } +} + + +/// Finds a node and creates it if not found. +/// +/// This recursive algorithm traverses the tree searching for a particular key, +/// creating any intermediate nodes if they do not already exist (for the case +/// of dynamic inner nodes). The returned node is non-constant, so this can be +/// used by the algorithms that set key values. +/// +/// \param key The key to be queried. +/// \param key_pos The current level within the key to be examined. +/// \param new_node A function that returns a new leaf node of the desired +/// type. This is only called if the leaf cannot be found, but it has +/// already been defined. +/// +/// \return A reference to the located node, if successful. +/// +/// \throw invalid_key_value If the resulting node of the search would be an +/// inner node. +/// \throw unknown_key_error If the provided key is unknown. +config::leaf_node* +config::detail::inner_node::lookup_rw(const tree_key& key, + const tree_key::size_type key_pos, + new_node_hook new_node) +{ + PRE(key_pos < key.size()); + + children_map::const_iterator child_iter = _children.find(key[key_pos]); + if (child_iter == _children.end()) { + if (_dynamic) { + base_node* const child = (key_pos == key.size() - 1) ? + static_cast< base_node* >(new_node()) : + static_cast< base_node* >(new dynamic_inner_node()); + _children.insert(children_map::value_type(key[key_pos], child)); + child_iter = _children.find(key[key_pos]); + } else { + throw unknown_key_error(key); + } + } + + if (key_pos == key.size() - 1) { + try { + leaf_node& child = dynamic_cast< leaf_node& >( + *(*child_iter).second); + return &child; + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } + } else { + PRE(key_pos < key.size() - 1); + try { + inner_node& child = dynamic_cast< inner_node& >( + *(*child_iter).second); + return child.lookup_rw(key, key_pos + 1, new_node); + } catch (const std::bad_cast& e) { + throw unknown_key_error( + key, "Cannot address incomplete configuration property '%s'"); + } + } +} + + +/// Converts the subtree to a collection of key/value string pairs. +/// +/// \param [out] properties The accumulator for the generated properties. The +/// contents of the map are only extended. +/// \param key The path to the current node. +void +config::detail::inner_node::all_properties(properties_map& properties, + const tree_key& key) const +{ + for (children_map::const_iterator iter = _children.begin(); + iter != _children.end(); ++iter) { + tree_key child_key = key; + child_key.push_back((*iter).first); + try { + leaf_node& child = dynamic_cast< leaf_node& >(*(*iter).second); + if (child.is_set()) + properties[flatten_key(child_key)] = child.to_string(); + } catch (const std::bad_cast& unused_error) { + inner_node& child = dynamic_cast< inner_node& >(*(*iter).second); + child.all_properties(properties, child_key); + } + } +} + + +/// Constructor. +config::detail::static_inner_node::static_inner_node(void) : + inner_node(false) +{ +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::detail::static_inner_node::deep_copy(void) const +{ + std::auto_ptr< inner_node > new_node(new static_inner_node()); + copy_into(new_node.get()); + return new_node.release(); +} + + +/// Combines this node with another one. +/// +/// \param key Key to this node. +/// \param other The node to combine with. +/// +/// \return A new node representing the combination. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +config::detail::base_node* +config::detail::static_inner_node::combine(const tree_key& key, + const base_node* other) const +{ + std::auto_ptr< inner_node > new_node(new static_inner_node()); + combine_into(key, other, new_node.get()); + return new_node.release(); +} + + +/// Registers a key as valid and having a specific type. +/// +/// This method does not raise errors on invalid/unknown keys or other +/// tree-related issues. The reasons is that define() is a method that does not +/// depend on user input: it is intended to pre-populate the tree with a +/// specific structure, and that happens once at coding time. +/// +/// \param key The key to be registered. +/// \param key_pos The current level within the key to be examined. +/// \param new_node A function that returns a new leaf node of the desired +/// type. +void +config::detail::static_inner_node::define(const tree_key& key, + const tree_key::size_type key_pos, + new_node_hook new_node) +{ + PRE(key_pos < key.size()); + + if (key_pos == key.size() - 1) { + PRE_MSG(_children.find(key[key_pos]) == _children.end(), + "Key already defined"); + _children.insert(children_map::value_type(key[key_pos], new_node())); + } else { + PRE(key_pos < key.size() - 1); + const children_map::const_iterator child_iter = _children.find( + key[key_pos]); + + if (child_iter == _children.end()) { + static_inner_node* const child_ptr = new static_inner_node(); + _children.insert(children_map::value_type(key[key_pos], child_ptr)); + child_ptr->define(key, key_pos + 1, new_node); + } else { + try { + static_inner_node& child = dynamic_cast< static_inner_node& >( + *(*child_iter).second); + child.define(key, key_pos + 1, new_node); + } catch (const std::bad_cast& e) { + UNREACHABLE; + } + } + } +} + + +/// Constructor. +config::detail::dynamic_inner_node::dynamic_inner_node(void) : + inner_node(true) +{ +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::detail::dynamic_inner_node::deep_copy(void) const +{ + std::auto_ptr< inner_node > new_node(new dynamic_inner_node()); + copy_into(new_node.get()); + return new_node.release(); +} + + +/// Combines this node with another one. +/// +/// \param key Key to this node. +/// \param other The node to combine with. +/// +/// \return A new node representing the combination. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +config::detail::base_node* +config::detail::dynamic_inner_node::combine(const tree_key& key, + const base_node* other) const +{ + std::auto_ptr< inner_node > new_node(new dynamic_inner_node()); + combine_into(key, other, new_node.get()); + return new_node.release(); +} + + +/// Destructor. +config::leaf_node::~leaf_node(void) +{ +} + + +/// Combines this node with another one. +/// +/// \param key Key to this node. +/// \param other_base The node to combine with. +/// +/// \return A new node representing the combination. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +config::detail::base_node* +config::leaf_node::combine(const detail::tree_key& key, + const base_node* other_base) const +{ + try { + const leaf_node& other = dynamic_cast< const leaf_node& >(*other_base); + + if (other.is_set()) { + return other.deep_copy(); + } else { + return deep_copy(); + } + } catch (const std::bad_cast& unused_e) { + throw config::bad_combination_error( + key, "'%s' is a leaf node in the base tree but an inner node in " + "the overrides treee"); + } +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::bool_node::deep_copy(void) const +{ + std::auto_ptr< bool_node > new_node(new bool_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Pushes the node's value onto the Lua stack. +/// +/// \param state The Lua state onto which to push the value. +void +config::bool_node::push_lua(lutok::state& state) const +{ + state.push_boolean(value()); +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \param state The Lua state from which to get the value. +/// \param value_index The stack index in which the value resides. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +void +config::bool_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_boolean(value_index)) + set(state.to_boolean(value_index)); + else + throw value_error("Not a boolean"); +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::int_node::deep_copy(void) const +{ + std::auto_ptr< int_node > new_node(new int_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Pushes the node's value onto the Lua stack. +/// +/// \param state The Lua state onto which to push the value. +void +config::int_node::push_lua(lutok::state& state) const +{ + state.push_integer(value()); +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \param state The Lua state from which to get the value. +/// \param value_index The stack index in which the value resides. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +void +config::int_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_number(value_index)) + set(state.to_integer(value_index)); + else + throw value_error("Not an integer"); +} + + +/// Checks a given value for validity. +/// +/// \param new_value The value to validate. +/// +/// \throw value_error If the value is not valid. +void +config::positive_int_node::validate(const value_type& new_value) const +{ + if (new_value <= 0) + throw value_error("Must be a positive integer"); +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::string_node::deep_copy(void) const +{ + std::auto_ptr< string_node > new_node(new string_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Pushes the node's value onto the Lua stack. +/// +/// \param state The Lua state onto which to push the value. +void +config::string_node::push_lua(lutok::state& state) const +{ + state.push_string(value()); +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \param state The Lua state from which to get the value. +/// \param value_index The stack index in which the value resides. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +void +config::string_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_string(value_index)) + set(state.to_string(value_index)); + else + throw value_error("Not a string"); +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::strings_set_node::deep_copy(void) const +{ + std::auto_ptr< strings_set_node > new_node(new strings_set_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Converts a single word to the native type. +/// +/// \param raw_value The value to parse. +/// +/// \return The parsed value. +std::string +config::strings_set_node::parse_one(const std::string& raw_value) const +{ + return raw_value; +} diff --git a/utils/config/nodes.hpp b/utils/config/nodes.hpp new file mode 100644 index 000000000000..6b766ff5d8f7 --- /dev/null +++ b/utils/config/nodes.hpp @@ -0,0 +1,272 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/nodes.hpp +/// Representation of tree nodes. + +#if !defined(UTILS_CONFIG_NODES_HPP) +#define UTILS_CONFIG_NODES_HPP + +#include "utils/config/nodes_fwd.hpp" + +#include <set> +#include <string> + +#include <lutok/state.hpp> + +#include "utils/config/keys_fwd.hpp" +#include "utils/config/nodes_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.hpp" + +namespace utils { +namespace config { + + +namespace detail { + + +/// Base representation of a node. +/// +/// This abstract class provides the base type for every node in the tree. Due +/// to the dynamic nature of our trees (each leaf being able to hold arbitrary +/// data types), this base type is a necessity. +class base_node : noncopyable { +public: + virtual ~base_node(void) = 0; + + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* deep_copy(void) const = 0; + + /// Combines this node with another one. + /// + /// \param key Key to this node. + /// \param other The node to combine with. + /// + /// \return A new node representing the combination. + /// + /// \throw bad_combination_error If the two nodes cannot be combined. + virtual base_node* combine(const tree_key& key, const base_node* other) + const = 0; +}; + + +} // namespace detail + + +/// Abstract leaf node without any specified type. +/// +/// This base abstract type is necessary to have a common pointer type to which +/// to cast any leaf. We later provide templated derivates of this class, and +/// those cannot act in this manner. +/// +/// It is important to understand that a leaf can exist without actually holding +/// a value. Our trees are "strictly keyed": keys must have been pre-defined +/// before a value can be set on them. This is to ensure that the end user is +/// using valid key names and not making mistakes due to typos, for example. To +/// represent this condition, we define an "empty" key in the tree to denote +/// that the key is valid, yet it has not been set by the user. Only when an +/// explicit set is performed on the key, it gets a value. +class leaf_node : public detail::base_node { +public: + virtual ~leaf_node(void); + + virtual bool is_set(void) const = 0; + + base_node* combine(const detail::tree_key&, const base_node*) const; + + virtual void push_lua(lutok::state&) const = 0; + virtual void set_lua(lutok::state&, const int) = 0; + + virtual void set_string(const std::string&) = 0; + virtual std::string to_string(void) const = 0; +}; + + +/// Base leaf node for a single arbitrary type. +/// +/// This templated leaf node holds a single object of any type. The conversion +/// to/from string representations is undefined, as that depends on the +/// particular type being processed. You should reimplement this class for any +/// type that needs additional processing/validation during conversion. +template< typename ValueType > +class typed_leaf_node : public leaf_node { +public: + /// The type of the value held by this node. + typedef ValueType value_type; + + /// Constructs a new leaf node that contains no value. + typed_leaf_node(void); + + /// Checks whether the node has been set by the user. + bool is_set(void) const; + + /// Gets the value stored in the node. + const value_type& value(void) const; + + /// Gets the read-write value stored in the node. + value_type& value(void); + + /// Sets the value of the node. + void set(const value_type&); + +protected: + /// The value held by this node. + optional< value_type > _value; + +private: + virtual void validate(const value_type&) const; +}; + + +/// Leaf node holding a native type. +/// +/// This templated leaf node holds a native type. The conversion to/from string +/// representations of the value happens by means of iostreams. +template< typename ValueType > +class native_leaf_node : public typed_leaf_node< ValueType > { +public: + void set_string(const std::string&); + std::string to_string(void) const; +}; + + +/// A leaf node that holds a boolean value. +class bool_node : public native_leaf_node< bool > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); +}; + + +/// A leaf node that holds an integer value. +class int_node : public native_leaf_node< int > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); +}; + + +/// A leaf node that holds a positive non-zero integer value. +class positive_int_node : public int_node { + virtual void validate(const value_type&) const; +}; + + +/// A leaf node that holds a string value. +class string_node : public native_leaf_node< std::string > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); +}; + + +/// Base leaf node for a set of native types. +/// +/// This is a base abstract class because there is no generic way to parse a +/// single word in the textual representation of the set to the native value. +template< typename ValueType > +class base_set_node : public leaf_node { +public: + /// The type of the value held by this node. + typedef std::set< ValueType > value_type; + + base_set_node(void); + + /// Checks whether the node has been set by the user. + /// + /// \return True if a value has been set in the node. + bool is_set(void) const; + + /// Gets the value stored in the node. + /// + /// \pre The node must have a value. + /// + /// \return The value in the node. + const value_type& value(void) const; + + /// Gets the read-write value stored in the node. + /// + /// \pre The node must have a value. + /// + /// \return The value in the node. + value_type& value(void); + + /// Sets the value of the node. + void set(const value_type&); + + /// Sets the value of the node from a raw string representation. + void set_string(const std::string&); + + /// Converts the contents of the node to a string. + std::string to_string(void) const; + + /// Pushes the node's value onto the Lua stack. + void push_lua(lutok::state&) const; + + /// Sets the value of the node from an entry in the Lua stack. + void set_lua(lutok::state&, const int); + +protected: + /// The value held by this node. + optional< value_type > _value; + +private: + /// Converts a single word to the native type. + /// + /// \return The parsed value. + /// + /// \throw value_error If the value is invalid. + virtual ValueType parse_one(const std::string&) const = 0; + + virtual void validate(const value_type&) const; +}; + + +/// A leaf node that holds a set of strings. +class strings_set_node : public base_set_node< std::string > { +public: + virtual base_node* deep_copy(void) const; + +private: + std::string parse_one(const std::string&) const; +}; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_NODES_HPP) diff --git a/utils/config/nodes.ipp b/utils/config/nodes.ipp new file mode 100644 index 000000000000..9e0a1228cccd --- /dev/null +++ b/utils/config/nodes.ipp @@ -0,0 +1,408 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/nodes.hpp" + +#if !defined(UTILS_CONFIG_NODES_IPP) +#define UTILS_CONFIG_NODES_IPP + +#include <memory> +#include <typeinfo> + +#include "utils/config/exceptions.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" +#include "utils/sanity.hpp" + +namespace utils { + + +namespace config { +namespace detail { + + +/// Type of the new_node() family of functions. +typedef base_node* (*new_node_hook)(void); + + +/// Creates a new leaf node of a given type. +/// +/// \tparam NodeType The type of the leaf node to create. +/// +/// \return A pointer to the newly-created node. +template< class NodeType > +base_node* +new_node(void) +{ + return new NodeType(); +} + + +/// Internal node of the tree. +/// +/// This abstract base class provides the mechanism to implement both static and +/// dynamic nodes. Ideally, the implementation would be split in subclasses and +/// this class would not include the knowledge of whether the node is dynamic or +/// not. However, because the static/dynamic difference depends on the leaf +/// types, we need to declare template functions and these cannot be virtual. +class inner_node : public base_node { + /// Whether the node is dynamic or not. + bool _dynamic; + +protected: + /// Type to represent the collection of children of this node. + /// + /// Note that these are one-level keys. They cannot contain dots, and thus + /// is why we use a string rather than a tree_key. + typedef std::map< std::string, base_node* > children_map; + + /// Mapping of keys to values that are descendants of this node. + children_map _children; + + void copy_into(inner_node*) const; + void combine_into(const tree_key&, const base_node*, inner_node*) const; + +private: + void combine_children_into(const tree_key&, + const children_map&, const children_map&, + inner_node*) const; + +public: + inner_node(const bool); + virtual ~inner_node(void) = 0; + + const base_node* lookup_ro(const tree_key&, + const tree_key::size_type) const; + leaf_node* lookup_rw(const tree_key&, const tree_key::size_type, + new_node_hook); + + void all_properties(properties_map&, const tree_key&) const; +}; + + +/// Static internal node of the tree. +/// +/// The direct children of this node must be pre-defined by calls to define(). +/// Attempts to traverse this node and resolve a key that is not a pre-defined +/// children will result in an "unknown key" error. +class static_inner_node : public config::detail::inner_node { +public: + static_inner_node(void); + + virtual base_node* deep_copy(void) const; + virtual base_node* combine(const tree_key&, const base_node*) const; + + void define(const tree_key&, const tree_key::size_type, new_node_hook); +}; + + +/// Dynamic internal node of the tree. +/// +/// The children of this node need not be pre-defined. Attempts to traverse +/// this node and resolve a key will result in such key being created. Any +/// intermediate non-existent nodes of the traversal will be created as dynamic +/// inner nodes as well. +class dynamic_inner_node : public config::detail::inner_node { +public: + virtual base_node* deep_copy(void) const; + virtual base_node* combine(const tree_key&, const base_node*) const; + + dynamic_inner_node(void); +}; + + +} // namespace detail +} // namespace config + + +/// Constructor for a node with an undefined value. +/// +/// This should only be called by the tree's define() method as a way to +/// register a node as known but undefined. The node will then serve as a +/// placeholder for future values. +template< typename ValueType > +config::typed_leaf_node< ValueType >::typed_leaf_node(void) : + _value(none) +{ +} + + +/// Checks whether the node has been set by the user. +/// +/// Nodes of the tree are predefined by the caller to specify the valid +/// types of the leaves. Such predefinition results in the creation of +/// nodes within the tree, but these nodes have not yet been set. +/// Traversing these nodes is invalid and should result in an "unknown key" +/// error. +/// +/// \return True if a value has been set in the node. +template< typename ValueType > +bool +config::typed_leaf_node< ValueType >::is_set(void) const +{ + return static_cast< bool >(_value); +} + + +/// Gets the value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +const typename config::typed_leaf_node< ValueType >::value_type& +config::typed_leaf_node< ValueType >::value(void) const +{ + PRE(is_set()); + return _value.get(); +} + + +/// Gets the read-write value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +typename config::typed_leaf_node< ValueType >::value_type& +config::typed_leaf_node< ValueType >::value(void) +{ + PRE(is_set()); + return _value.get(); +} + + +/// Sets the value of the node. +/// +/// \param value_ The new value to set the node to. +/// +/// \throw value_error If the value is invalid, according to validate(). +template< typename ValueType > +void +config::typed_leaf_node< ValueType >::set(const value_type& value_) +{ + validate(value_); + _value = optional< value_type >(value_); +} + + +/// Checks a given value for validity. +/// +/// This is called internally by the node right before updating the recorded +/// value. This method can be redefined by subclasses. +/// +/// \throw value_error If the value is not valid. +template< typename ValueType > +void +config::typed_leaf_node< ValueType >::validate( + const value_type& /* new_value */) const +{ +} + + +/// Sets the value of the node from a raw string representation. +/// +/// \param raw_value The value to set the node to. +/// +/// \throw value_error If the value is invalid. +template< typename ValueType > +void +config::native_leaf_node< ValueType >::set_string(const std::string& raw_value) +{ + try { + typed_leaf_node< ValueType >::set(text::to_type< ValueType >( + raw_value)); + } catch (const text::value_error& e) { + throw config::value_error(F("Failed to convert string value '%s' to " + "the node's type") % raw_value); + } +} + + +/// Converts the contents of the node to a string. +/// +/// \pre The node must have a value. +/// +/// \return A string representation of the value held by the node. +template< typename ValueType > +std::string +config::native_leaf_node< ValueType >::to_string(void) const +{ + PRE(typed_leaf_node< ValueType >::is_set()); + return F("%s") % typed_leaf_node< ValueType >::value(); +} + + +/// Constructor for a node with an undefined value. +/// +/// This should only be called by the tree's define() method as a way to +/// register a node as known but undefined. The node will then serve as a +/// placeholder for future values. +template< typename ValueType > +config::base_set_node< ValueType >::base_set_node(void) : + _value(none) +{ +} + + +/// Checks whether the node has been set. +/// +/// Remember that a node can exist before holding a value (i.e. when the node +/// has been defined as "known" but not yet set by the user). This function +/// checks whether the node laready holds a value. +/// +/// \return True if a value has been set in the node. +template< typename ValueType > +bool +config::base_set_node< ValueType >::is_set(void) const +{ + return static_cast< bool >(_value); +} + + +/// Gets the value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +const typename config::base_set_node< ValueType >::value_type& +config::base_set_node< ValueType >::value(void) const +{ + PRE(is_set()); + return _value.get(); +} + + +/// Gets the read-write value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +typename config::base_set_node< ValueType >::value_type& +config::base_set_node< ValueType >::value(void) +{ + PRE(is_set()); + return _value.get(); +} + + +/// Sets the value of the node. +/// +/// \param value_ The new value to set the node to. +/// +/// \throw value_error If the value is invalid, according to validate(). +template< typename ValueType > +void +config::base_set_node< ValueType >::set(const value_type& value_) +{ + validate(value_); + _value = optional< value_type >(value_); +} + + +/// Sets the value of the node from a raw string representation. +/// +/// \param raw_value The value to set the node to. +/// +/// \throw value_error If the value is invalid. +template< typename ValueType > +void +config::base_set_node< ValueType >::set_string(const std::string& raw_value) +{ + std::set< ValueType > new_value; + + const std::vector< std::string > words = text::split(raw_value, ' '); + for (std::vector< std::string >::const_iterator iter = words.begin(); + iter != words.end(); ++iter) { + if (!(*iter).empty()) + new_value.insert(parse_one(*iter)); + } + + set(new_value); +} + + +/// Converts the contents of the node to a string. +/// +/// \pre The node must have a value. +/// +/// \return A string representation of the value held by the node. +template< typename ValueType > +std::string +config::base_set_node< ValueType >::to_string(void) const +{ + PRE(is_set()); + return text::join(_value.get(), " "); +} + + +/// Pushes the node's value onto the Lua stack. +template< typename ValueType > +void +config::base_set_node< ValueType >::push_lua(lutok::state& /* state */) const +{ + UNREACHABLE; +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +template< typename ValueType > +void +config::base_set_node< ValueType >::set_lua( + lutok::state& /* state */, + const int /* value_index */) +{ + UNREACHABLE; +} + + +/// Checks a given value for validity. +/// +/// This is called internally by the node right before updating the recorded +/// value. This method can be redefined by subclasses. +/// +/// \throw value_error If the value is not valid. +template< typename ValueType > +void +config::base_set_node< ValueType >::validate( + const value_type& /* new_value */) const +{ +} + + +} // namespace utils + +#endif // !defined(UTILS_CONFIG_NODES_IPP) diff --git a/utils/config/nodes_fwd.hpp b/utils/config/nodes_fwd.hpp new file mode 100644 index 000000000000..b03328e79e95 --- /dev/null +++ b/utils/config/nodes_fwd.hpp @@ -0,0 +1,70 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/nodes_fwd.hpp +/// Forward declarations for utils/config/nodes.hpp + +#if !defined(UTILS_CONFIG_NODES_FWD_HPP) +#define UTILS_CONFIG_NODES_FWD_HPP + +#include <map> +#include <string> + +namespace utils { +namespace config { + + +/// Flat representation of all properties as strings. +typedef std::map< std::string, std::string > properties_map; + + +namespace detail { + + +class base_node; +class static_inner_node; + + +} // namespace detail + + +class leaf_node; +template< typename > class typed_leaf_node; +template< typename > class native_leaf_node; +class bool_node; +class int_node; +class positive_int_node; +class string_node; +template< typename > class base_set_node; +class strings_set_node; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_NODES_FWD_HPP) diff --git a/utils/config/nodes_test.cpp b/utils/config/nodes_test.cpp new file mode 100644 index 000000000000..e762d3aac38c --- /dev/null +++ b/utils/config/nodes_test.cpp @@ -0,0 +1,695 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/nodes.ipp" + +#include <atf-c++.hpp> + +#include <lutok/state.ipp> + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/defs.hpp" + +namespace config = utils::config; + + +namespace { + + +/// Typed leaf node that specializes the validate() method. +class validation_node : public config::int_node { + /// Checks a given value for validity against a fake value. + /// + /// \param new_value The value to validate. + /// + /// \throw value_error If the value is not valid. + void + validate(const value_type& new_value) const + { + if (new_value == 12345) + throw config::value_error("Custom validate method"); + } +}; + + +/// Set node that specializes the validate() method. +class set_validation_node : public config::strings_set_node { + /// Checks a given value for validity against a fake value. + /// + /// \param new_value The value to validate. + /// + /// \throw value_error If the value is not valid. + void + validate(const value_type& new_value) const + { + for (value_type::const_iterator iter = new_value.begin(); + iter != new_value.end(); ++iter) + if (*iter == "throw") + throw config::value_error("Custom validate method"); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__deep_copy); +ATF_TEST_CASE_BODY(bool_node__deep_copy) +{ + config::bool_node node; + node.set(true); + config::detail::base_node* raw_copy = node.deep_copy(); + config::bool_node* copy = static_cast< config::bool_node* >(raw_copy); + ATF_REQUIRE(copy->value()); + copy->set(false); + ATF_REQUIRE(node.value()); + ATF_REQUIRE(!copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__is_set_and_set); +ATF_TEST_CASE_BODY(bool_node__is_set_and_set) +{ + config::bool_node node; + ATF_REQUIRE(!node.is_set()); + node.set(false); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__value_and_set); +ATF_TEST_CASE_BODY(bool_node__value_and_set) +{ + config::bool_node node; + node.set(false); + ATF_REQUIRE(!node.value()); + node.set(true); + ATF_REQUIRE( node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__push_lua); +ATF_TEST_CASE_BODY(bool_node__push_lua) +{ + lutok::state state; + + config::bool_node node; + node.set(true); + node.push_lua(state); + ATF_REQUIRE(state.is_boolean(-1)); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_lua__ok); +ATF_TEST_CASE_BODY(bool_node__set_lua__ok) +{ + lutok::state state; + + config::bool_node node; + state.push_boolean(false); + node.set_lua(state, -1); + state.pop(1); + ATF_REQUIRE(!node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(bool_node__set_lua__invalid_value) +{ + lutok::state state; + + config::bool_node node; + state.push_string("foo bar"); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_string__ok); +ATF_TEST_CASE_BODY(bool_node__set_string__ok) +{ + config::bool_node node; + node.set_string("false"); + ATF_REQUIRE(!node.value()); + node.set_string("true"); + ATF_REQUIRE( node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_string__invalid_value); +ATF_TEST_CASE_BODY(bool_node__set_string__invalid_value) +{ + config::bool_node node; + ATF_REQUIRE_THROW(config::value_error, node.set_string("12345")); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__to_string); +ATF_TEST_CASE_BODY(bool_node__to_string) +{ + config::bool_node node; + node.set(false); + ATF_REQUIRE_EQ("false", node.to_string()); + node.set(true); + ATF_REQUIRE_EQ("true", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__deep_copy); +ATF_TEST_CASE_BODY(int_node__deep_copy) +{ + config::int_node node; + node.set(5); + config::detail::base_node* raw_copy = node.deep_copy(); + config::int_node* copy = static_cast< config::int_node* >(raw_copy); + ATF_REQUIRE_EQ(5, copy->value()); + copy->set(10); + ATF_REQUIRE_EQ(5, node.value()); + ATF_REQUIRE_EQ(10, copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__is_set_and_set); +ATF_TEST_CASE_BODY(int_node__is_set_and_set) +{ + config::int_node node; + ATF_REQUIRE(!node.is_set()); + node.set(20); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__value_and_set); +ATF_TEST_CASE_BODY(int_node__value_and_set) +{ + config::int_node node; + node.set(20); + ATF_REQUIRE_EQ(20, node.value()); + node.set(0); + ATF_REQUIRE_EQ(0, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__push_lua); +ATF_TEST_CASE_BODY(int_node__push_lua) +{ + lutok::state state; + + config::int_node node; + node.set(754); + node.push_lua(state); + ATF_REQUIRE(state.is_number(-1)); + ATF_REQUIRE_EQ(754, state.to_integer(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_lua__ok); +ATF_TEST_CASE_BODY(int_node__set_lua__ok) +{ + lutok::state state; + + config::int_node node; + state.push_integer(123); + state.push_string("456"); + node.set_lua(state, -2); + ATF_REQUIRE_EQ(123, node.value()); + node.set_lua(state, -1); + ATF_REQUIRE_EQ(456, node.value()); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(int_node__set_lua__invalid_value) +{ + lutok::state state; + + config::int_node node; + state.push_boolean(true); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_string__ok); +ATF_TEST_CASE_BODY(int_node__set_string__ok) +{ + config::int_node node; + node.set_string("178"); + ATF_REQUIRE_EQ(178, node.value()); + node.set_string("-123"); + ATF_REQUIRE_EQ(-123, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_string__invalid_value); +ATF_TEST_CASE_BODY(int_node__set_string__invalid_value) +{ + config::int_node node; + ATF_REQUIRE_THROW(config::value_error, node.set_string(" 23")); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__to_string); +ATF_TEST_CASE_BODY(int_node__to_string) +{ + config::int_node node; + node.set(89); + ATF_REQUIRE_EQ("89", node.to_string()); + node.set(-57); + ATF_REQUIRE_EQ("-57", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__deep_copy); +ATF_TEST_CASE_BODY(positive_int_node__deep_copy) +{ + config::positive_int_node node; + node.set(5); + config::detail::base_node* raw_copy = node.deep_copy(); + config::positive_int_node* copy = static_cast< config::positive_int_node* >( + raw_copy); + ATF_REQUIRE_EQ(5, copy->value()); + copy->set(10); + ATF_REQUIRE_EQ(5, node.value()); + ATF_REQUIRE_EQ(10, copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__is_set_and_set); +ATF_TEST_CASE_BODY(positive_int_node__is_set_and_set) +{ + config::positive_int_node node; + ATF_REQUIRE(!node.is_set()); + node.set(20); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__value_and_set); +ATF_TEST_CASE_BODY(positive_int_node__value_and_set) +{ + config::positive_int_node node; + node.set(20); + ATF_REQUIRE_EQ(20, node.value()); + node.set(1); + ATF_REQUIRE_EQ(1, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__push_lua); +ATF_TEST_CASE_BODY(positive_int_node__push_lua) +{ + lutok::state state; + + config::positive_int_node node; + node.set(754); + node.push_lua(state); + ATF_REQUIRE(state.is_number(-1)); + ATF_REQUIRE_EQ(754, state.to_integer(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_lua__ok); +ATF_TEST_CASE_BODY(positive_int_node__set_lua__ok) +{ + lutok::state state; + + config::positive_int_node node; + state.push_integer(123); + state.push_string("456"); + node.set_lua(state, -2); + ATF_REQUIRE_EQ(123, node.value()); + node.set_lua(state, -1); + ATF_REQUIRE_EQ(456, node.value()); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(positive_int_node__set_lua__invalid_value) +{ + lutok::state state; + + config::positive_int_node node; + state.push_boolean(true); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); + state.push_integer(0); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_string__ok); +ATF_TEST_CASE_BODY(positive_int_node__set_string__ok) +{ + config::positive_int_node node; + node.set_string("1"); + ATF_REQUIRE_EQ(1, node.value()); + node.set_string("178"); + ATF_REQUIRE_EQ(178, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_string__invalid_value); +ATF_TEST_CASE_BODY(positive_int_node__set_string__invalid_value) +{ + config::positive_int_node node; + ATF_REQUIRE_THROW(config::value_error, node.set_string(" 23")); + ATF_REQUIRE(!node.is_set()); + ATF_REQUIRE_THROW(config::value_error, node.set_string("0")); + ATF_REQUIRE(!node.is_set()); + ATF_REQUIRE_THROW(config::value_error, node.set_string("-5")); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__to_string); +ATF_TEST_CASE_BODY(positive_int_node__to_string) +{ + config::positive_int_node node; + node.set(89); + ATF_REQUIRE_EQ("89", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__deep_copy); +ATF_TEST_CASE_BODY(string_node__deep_copy) +{ + config::string_node node; + node.set("first"); + config::detail::base_node* raw_copy = node.deep_copy(); + config::string_node* copy = static_cast< config::string_node* >(raw_copy); + ATF_REQUIRE_EQ("first", copy->value()); + copy->set("second"); + ATF_REQUIRE_EQ("first", node.value()); + ATF_REQUIRE_EQ("second", copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__is_set_and_set); +ATF_TEST_CASE_BODY(string_node__is_set_and_set) +{ + config::string_node node; + ATF_REQUIRE(!node.is_set()); + node.set("foo"); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__value_and_set); +ATF_TEST_CASE_BODY(string_node__value_and_set) +{ + config::string_node node; + node.set("foo"); + ATF_REQUIRE_EQ("foo", node.value()); + node.set(""); + ATF_REQUIRE_EQ("", node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__push_lua); +ATF_TEST_CASE_BODY(string_node__push_lua) +{ + lutok::state state; + + config::string_node node; + node.set("some message"); + node.push_lua(state); + ATF_REQUIRE(state.is_string(-1)); + ATF_REQUIRE_EQ("some message", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__set_lua__ok); +ATF_TEST_CASE_BODY(string_node__set_lua__ok) +{ + lutok::state state; + + config::string_node node; + state.push_string("text 1"); + state.push_integer(231); + node.set_lua(state, -2); + ATF_REQUIRE_EQ("text 1", node.value()); + node.set_lua(state, -1); + ATF_REQUIRE_EQ("231", node.value()); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(string_node__set_lua__invalid_value) +{ + lutok::state state; + + config::bool_node node; + state.new_table(); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__set_string); +ATF_TEST_CASE_BODY(string_node__set_string) +{ + config::string_node node; + node.set_string("abcd efgh"); + ATF_REQUIRE_EQ("abcd efgh", node.value()); + node.set_string(" 1234 "); + ATF_REQUIRE_EQ(" 1234 ", node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__to_string); +ATF_TEST_CASE_BODY(string_node__to_string) +{ + config::string_node node; + node.set(""); + ATF_REQUIRE_EQ("", node.to_string()); + node.set("aaa"); + ATF_REQUIRE_EQ("aaa", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__deep_copy); +ATF_TEST_CASE_BODY(strings_set_node__deep_copy) +{ + std::set< std::string > value; + config::strings_set_node node; + value.insert("foo"); + node.set(value); + config::detail::base_node* raw_copy = node.deep_copy(); + config::strings_set_node* copy = + static_cast< config::strings_set_node* >(raw_copy); + value.insert("bar"); + ATF_REQUIRE_EQ(1, copy->value().size()); + copy->set(value); + ATF_REQUIRE_EQ(1, node.value().size()); + ATF_REQUIRE_EQ(2, copy->value().size()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__is_set_and_set); +ATF_TEST_CASE_BODY(strings_set_node__is_set_and_set) +{ + std::set< std::string > value; + value.insert("foo"); + + config::strings_set_node node; + ATF_REQUIRE(!node.is_set()); + node.set(value); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__value_and_set); +ATF_TEST_CASE_BODY(strings_set_node__value_and_set) +{ + std::set< std::string > value; + value.insert("first"); + + config::strings_set_node node; + node.set(value); + ATF_REQUIRE(value == node.value()); + value.clear(); + node.set(value); + value.insert("second"); + ATF_REQUIRE(node.value().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__set_string); +ATF_TEST_CASE_BODY(strings_set_node__set_string) +{ + config::strings_set_node node; + { + std::set< std::string > expected; + expected.insert("abcd"); + expected.insert("efgh"); + + node.set_string("abcd efgh"); + ATF_REQUIRE(expected == node.value()); + } + { + std::set< std::string > expected; + expected.insert("1234"); + + node.set_string(" 1234 "); + ATF_REQUIRE(expected == node.value()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__to_string); +ATF_TEST_CASE_BODY(strings_set_node__to_string) +{ + std::set< std::string > value; + config::strings_set_node node; + value.insert("second"); + value.insert("first"); + node.set(value); + ATF_REQUIRE_EQ("first second", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(typed_leaf_node__validate_set); +ATF_TEST_CASE_BODY(typed_leaf_node__validate_set) +{ + validation_node node; + node.set(1234); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set(12345)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(typed_leaf_node__validate_set_string); +ATF_TEST_CASE_BODY(typed_leaf_node__validate_set_string) +{ + validation_node node; + node.set_string("1234"); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set_string("12345")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_set_node__validate_set); +ATF_TEST_CASE_BODY(base_set_node__validate_set) +{ + set_validation_node node; + set_validation_node::value_type values; + values.insert("foo"); + values.insert("bar"); + node.set(values); + values.insert("throw"); + values.insert("baz"); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set(values)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_set_node__validate_set_string); +ATF_TEST_CASE_BODY(base_set_node__validate_set_string) +{ + set_validation_node node; + node.set_string("foo bar"); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set_string("foo bar throw baz")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, bool_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, bool_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, bool_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, bool_node__push_lua); + ATF_ADD_TEST_CASE(tcs, bool_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, bool_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, bool_node__set_string__ok); + ATF_ADD_TEST_CASE(tcs, bool_node__set_string__invalid_value); + ATF_ADD_TEST_CASE(tcs, bool_node__to_string); + + ATF_ADD_TEST_CASE(tcs, int_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, int_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, int_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, int_node__push_lua); + ATF_ADD_TEST_CASE(tcs, int_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, int_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, int_node__set_string__ok); + ATF_ADD_TEST_CASE(tcs, int_node__set_string__invalid_value); + ATF_ADD_TEST_CASE(tcs, int_node__to_string); + + ATF_ADD_TEST_CASE(tcs, positive_int_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, positive_int_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, positive_int_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, positive_int_node__push_lua); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_string__ok); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_string__invalid_value); + ATF_ADD_TEST_CASE(tcs, positive_int_node__to_string); + + ATF_ADD_TEST_CASE(tcs, string_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, string_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, string_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, string_node__push_lua); + ATF_ADD_TEST_CASE(tcs, string_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, string_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, string_node__set_string); + ATF_ADD_TEST_CASE(tcs, string_node__to_string); + + ATF_ADD_TEST_CASE(tcs, strings_set_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, strings_set_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, strings_set_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, strings_set_node__set_string); + ATF_ADD_TEST_CASE(tcs, strings_set_node__to_string); + + ATF_ADD_TEST_CASE(tcs, typed_leaf_node__validate_set); + ATF_ADD_TEST_CASE(tcs, typed_leaf_node__validate_set_string); + ATF_ADD_TEST_CASE(tcs, base_set_node__validate_set); + ATF_ADD_TEST_CASE(tcs, base_set_node__validate_set_string); +} diff --git a/utils/config/parser.cpp b/utils/config/parser.cpp new file mode 100644 index 000000000000..7bfe5517fdd0 --- /dev/null +++ b/utils/config/parser.cpp @@ -0,0 +1,181 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/parser.hpp" + +#include <lutok/exceptions.hpp> +#include <lutok/operations.hpp> +#include <lutok/stack_cleaner.hpp> +#include <lutok/state.ipp> + +#include "utils/config/exceptions.hpp" +#include "utils/config/lua_module.hpp" +#include "utils/config/tree.ipp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" + +namespace config = utils::config; + + +// History of configuration file versions: +// +// 2 - Changed the syntax() call to take only a version number, instead of the +// word 'config' as the first argument and the version as the second one. +// Files now start with syntax(2) instead of syntax('config', 1). +// +// 1 - Initial version. + + +/// Internal implementation of the parser. +struct utils::config::parser::impl : utils::noncopyable { + /// Pointer to the parent parser. Needed for callbacks. + parser* _parent; + + /// The Lua state used by this parser to process the configuration file. + lutok::state _state; + + /// The tree to be filed in by the configuration parameters, as provided by + /// the caller. + config::tree& _tree; + + /// Whether syntax() has been called or not. + bool _syntax_called; + + /// Constructs a new implementation. + /// + /// \param parent_ Pointer to the class being constructed. + /// \param config_tree_ The configuration tree provided by the user. + impl(parser* const parent_, tree& config_tree_) : + _parent(parent_), _tree(config_tree_), _syntax_called(false) + { + } + + friend void lua_syntax(lutok::state&); + + /// Callback executed by the Lua syntax() function. + /// + /// \param syntax_version The syntax format version as provided by the + /// configuration file in the call to syntax(). + void + syntax_callback(const int syntax_version) + { + if (_syntax_called) + throw syntax_error("syntax() can only be called once"); + _syntax_called = true; + + // Allow the parser caller to populate the tree with its own schema + // depending on the format/version combination. + _parent->setup(_tree, syntax_version); + + // Export the config module to the Lua state so that all global variable + // accesses are redirected to the configuration tree. + config::redirect(_state, _tree); + } +}; + + +namespace { + + +static int +lua_syntax(lutok::state& state) +{ + if (!state.is_number(-1)) + throw config::value_error("Last argument to syntax must be a number"); + const int syntax_version = state.to_integer(-1); + + if (syntax_version == 1) { + if (state.get_top() != 2) + throw config::value_error("Version 1 files need two arguments to " + "syntax()"); + if (!state.is_string(-2) || state.to_string(-2) != "config") + throw config::value_error("First argument to syntax must be " + "'config' for version 1 files"); + } else { + if (state.get_top() != 1) + throw config::value_error("syntax() only takes one argument"); + } + + state.get_global("_config_parser"); + config::parser::impl* impl = + *state.to_userdata< config::parser::impl* >(-1); + state.pop(1); + + impl->syntax_callback(syntax_version); + + return 0; +} + + +} // anonymous namespace + + +/// Constructs a new parser. +/// +/// \param [in,out] config_tree The configuration tree into which the values set +/// in the configuration file will be stored. +config::parser::parser(tree& config_tree) : + _pimpl(new impl(this, config_tree)) +{ + lutok::stack_cleaner cleaner(_pimpl->_state); + + _pimpl->_state.push_cxx_function(lua_syntax); + _pimpl->_state.set_global("syntax"); + *_pimpl->_state.new_userdata< config::parser::impl* >() = _pimpl.get(); + _pimpl->_state.set_global("_config_parser"); +} + + +/// Destructor. +config::parser::~parser(void) +{ +} + + +/// Parses a configuration file. +/// +/// \post The tree registered during the construction of this class is updated +/// to contain the values read from the configuration file. If the processing +/// fails, the state of the output tree is undefined. +/// +/// \param file The path to the file to process. +/// +/// \throw syntax_error If there is any problem processing the file. +void +config::parser::parse(const fs::path& file) +{ + try { + lutok::do_file(_pimpl->_state, file.str(), 0, 0, 0); + } catch (const lutok::error& e) { + throw syntax_error(e.what()); + } + + if (!_pimpl->_syntax_called) + throw syntax_error("No syntax defined (no call to syntax() found)"); +} diff --git a/utils/config/parser.hpp b/utils/config/parser.hpp new file mode 100644 index 000000000000..cb69e756cbe8 --- /dev/null +++ b/utils/config/parser.hpp @@ -0,0 +1,95 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/parser.hpp +/// Utilities to read a configuration file into memory. + +#if !defined(UTILS_CONFIG_PARSER_HPP) +#define UTILS_CONFIG_PARSER_HPP + +#include "utils/config/parser_fwd.hpp" + +#include <memory> + +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace utils { +namespace config { + + +/// A configuration parser. +/// +/// This parser is a class rather than a function because we need to support +/// callbacks to perform the initialization of the config file schema. The +/// configuration files always start with a call to syntax(), which define the +/// particular version of the schema being used. Depending on such version, the +/// layout of the internal tree representation needs to be different. +/// +/// A parser implementation must provide a setup() method to set up the +/// configuration schema based on the particular combination of syntax format +/// and version specified on the file. +/// +/// Parser objects are not supposed to be reused, and specific trees are not +/// supposed to be passed to multiple parsers (even if sequentially). Doing so +/// will cause all kinds of inconsistencies in the managed tree itself or in the +/// Lua state. +class parser : noncopyable { +public: + struct impl; + +private: + /// Pointer to the internal implementation. + std::auto_ptr< impl > _pimpl; + + /// Hook to initialize the tree keys before reading the file. + /// + /// This hook gets called when the configuration file defines its specific + /// format by calling the syntax() function. We have to delay the tree + /// initialization until this point because, before we know what version of + /// a configuration file we are parsing, we cannot know what keys are valid. + /// + /// \param [in,out] config_tree The tree in which to define the key + /// structure. + /// \param syntax_version The version of the file format as specified in the + /// configuration file. + virtual void setup(tree& config_tree, const int syntax_version) = 0; + +public: + explicit parser(tree&); + virtual ~parser(void); + + void parse(const fs::path&); +}; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_PARSER_HPP) diff --git a/utils/config/parser_fwd.hpp b/utils/config/parser_fwd.hpp new file mode 100644 index 000000000000..6278b6c95c12 --- /dev/null +++ b/utils/config/parser_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/parser_fwd.hpp +/// Forward declarations for utils/config/parser.hpp + +#if !defined(UTILS_CONFIG_PARSER_FWD_HPP) +#define UTILS_CONFIG_PARSER_FWD_HPP + +namespace utils { +namespace config { + + +class parser; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_PARSER_FWD_HPP) diff --git a/utils/config/parser_test.cpp b/utils/config/parser_test.cpp new file mode 100644 index 000000000000..f5445f55c490 --- /dev/null +++ b/utils/config/parser_test.cpp @@ -0,0 +1,252 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/parser.hpp" + +#include <stdexcept> + +#include <atf-c++.hpp> + +#include "utils/config/exceptions.hpp" +#include "utils/config/parser.hpp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Implementation of a parser for testing purposes. +class mock_parser : public config::parser { + /// Initializes the tree keys before reading the file. + /// + /// \param [in,out] tree The tree in which to define the key structure. + /// \param syntax_version The version of the file format as specified in the + /// configuration file. + void + setup(config::tree& tree, const int syntax_version) + { + if (syntax_version == 1) { + // Do nothing on config_tree. + } else if (syntax_version == 2) { + tree.define< config::string_node >("top_string"); + tree.define< config::int_node >("inner.int"); + tree.define_dynamic("inner.dynamic"); + } else { + throw std::runtime_error(F("Unknown syntax version %s") % + syntax_version); + } + } + +public: + /// Initializes a parser. + /// + /// \param tree The mock config tree to parse. + mock_parser(config::tree& tree) : + config::parser(tree) + { + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(no_keys__ok); +ATF_TEST_CASE_BODY(no_keys__ok) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "local foo = 'value'\n"); + + config::tree tree; + mock_parser(tree).parse(fs::path("output.lua")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::string_node >("foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(no_keys__unknown_key); +ATF_TEST_CASE_BODY(no_keys__unknown_key) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "foo = 'value'\n"); + + config::tree tree; + ATF_REQUIRE_THROW_RE(config::syntax_error, "foo", + mock_parser(tree).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_keys__ok); +ATF_TEST_CASE_BODY(some_keys__ok) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "top_string = 'foo'\n" + "inner.int = 12345\n" + "inner.dynamic.foo = 78\n" + "inner.dynamic.bar = 'some text'\n"); + + config::tree tree; + mock_parser(tree).parse(fs::path("output.lua")); + ATF_REQUIRE_EQ("foo", tree.lookup< config::string_node >("top_string")); + ATF_REQUIRE_EQ(12345, tree.lookup< config::int_node >("inner.int")); + ATF_REQUIRE_EQ("78", + tree.lookup< config::string_node >("inner.dynamic.foo")); + ATF_REQUIRE_EQ("some text", + tree.lookup< config::string_node >("inner.dynamic.bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_keys__not_strict); +ATF_TEST_CASE_BODY(some_keys__not_strict) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "top_string = 'foo'\n" + "unknown_string = 'bar'\n" + "top_string = 'baz'\n"); + + config::tree tree(false); + mock_parser(tree).parse(fs::path("output.lua")); + ATF_REQUIRE_EQ("baz", tree.lookup< config::string_node >("top_string")); + ATF_REQUIRE(!tree.is_set("unknown_string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_keys__unknown_key); +ATF_TEST_CASE_BODY(some_keys__unknown_key) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "top_string2 = 'foo'\n"); + config::tree tree1; + ATF_REQUIRE_THROW_RE(config::syntax_error, + "Unknown configuration property 'top_string2'", + mock_parser(tree1).parse(fs::path("output.lua"))); + + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "inner.int2 = 12345\n"); + config::tree tree2; + ATF_REQUIRE_THROW_RE(config::syntax_error, + "Unknown configuration property 'inner.int2'", + mock_parser(tree2).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_syntax); +ATF_TEST_CASE_BODY(invalid_syntax) +{ + config::tree tree; + + atf::utils::create_file("output.lua", "syntax(56)\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, + "Unknown syntax version 56", + mock_parser(tree).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_deprecated_format); +ATF_TEST_CASE_BODY(syntax_deprecated_format) +{ + config::tree tree; + + atf::utils::create_file("output.lua", "syntax('config', 1)\n"); + (void)mock_parser(tree).parse(fs::path("output.lua")); + + atf::utils::create_file("output.lua", "syntax('foo', 1)\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, "must be 'config'", + mock_parser(tree).parse(fs::path("output.lua"))); + + atf::utils::create_file("output.lua", "syntax('config', 2)\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, "only takes one argument", + mock_parser(tree).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_not_called); +ATF_TEST_CASE_BODY(syntax_not_called) +{ + config::tree tree; + tree.define< config::int_node >("var"); + + atf::utils::create_file("output.lua", "var = 3\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, "No syntax defined", + mock_parser(tree).parse(fs::path("output.lua"))); + + ATF_REQUIRE(!tree.is_set("var")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_called_more_than_once); +ATF_TEST_CASE_BODY(syntax_called_more_than_once) +{ + config::tree tree; + tree.define< config::int_node >("var"); + + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "var = 3\n" + "syntax(2)\n" + "var = 5\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, + "syntax\\(\\) can only be called once", + mock_parser(tree).parse(fs::path("output.lua"))); + + ATF_REQUIRE_EQ(3, tree.lookup< config::int_node >("var")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, no_keys__ok); + ATF_ADD_TEST_CASE(tcs, no_keys__unknown_key); + + ATF_ADD_TEST_CASE(tcs, some_keys__ok); + ATF_ADD_TEST_CASE(tcs, some_keys__not_strict); + ATF_ADD_TEST_CASE(tcs, some_keys__unknown_key); + + ATF_ADD_TEST_CASE(tcs, invalid_syntax); + ATF_ADD_TEST_CASE(tcs, syntax_deprecated_format); + ATF_ADD_TEST_CASE(tcs, syntax_not_called); + ATF_ADD_TEST_CASE(tcs, syntax_called_more_than_once); +} diff --git a/utils/config/tree.cpp b/utils/config/tree.cpp new file mode 100644 index 000000000000..1aa2d85b89cd --- /dev/null +++ b/utils/config/tree.cpp @@ -0,0 +1,338 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/tree.ipp" + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/config/nodes.ipp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; + + +/// Constructor. +/// +/// \param strict Whether keys must be validated at "set" time. +config::tree::tree(const bool strict) : + _strict(strict), _root(new detail::static_inner_node()) +{ +} + + +/// Constructor with a non-empty root. +/// +/// \param strict Whether keys must be validated at "set" time. +/// \param root The root to the tree to be owned by this instance. +config::tree::tree(const bool strict, detail::static_inner_node* root) : + _strict(strict), _root(root) +{ +} + + +/// Destructor. +config::tree::~tree(void) +{ +} + + +/// Generates a deep copy of the input tree. +/// +/// \return A new tree that is an exact copy of this tree. +config::tree +config::tree::deep_copy(void) const +{ + detail::static_inner_node* new_root = + dynamic_cast< detail::static_inner_node* >(_root->deep_copy()); + return config::tree(_strict, new_root); +} + + +/// Combines two trees. +/// +/// By combination we understand a new tree that contains the full key space of +/// the two input trees and, for the keys that match, respects the value of the +/// right-hand side (aka "other") tree. +/// +/// Any nodes marked as dynamic "win" over non-dynamic nodes and the resulting +/// tree will have the dynamic property set on those. +/// +/// \param overrides The tree to use as value overrides. +/// +/// \return The combined tree. +/// +/// \throw bad_combination_error If the two trees cannot be combined; for +/// example, if a single key represents an inner node in one tree but a leaf +/// node in the other one. +config::tree +config::tree::combine(const tree& overrides) const +{ + const detail::static_inner_node* other_root = + dynamic_cast< const detail::static_inner_node * >( + overrides._root.get()); + + detail::static_inner_node* new_root = + dynamic_cast< detail::static_inner_node* >( + _root->combine(detail::tree_key(), other_root)); + return config::tree(_strict, new_root); +} + + +/// Registers a node as being dynamic. +/// +/// This operation creates the given key as an inner node. Further set +/// operations that trespass this node will automatically create any missing +/// keys. +/// +/// This method does not raise errors on invalid/unknown keys or other +/// tree-related issues. The reasons is that define() is a method that does not +/// depend on user input: it is intended to pre-populate the tree with a +/// specific structure, and that happens once at coding time. +/// +/// \param dotted_key The key to be registered in dotted representation. +void +config::tree::define_dynamic(const std::string& dotted_key) +{ + try { + const detail::tree_key key = detail::parse_key(dotted_key); + _root->define(key, 0, detail::new_node< detail::dynamic_inner_node >); + } catch (const error& e) { + UNREACHABLE_MSG("define() failing due to key errors is a programming " + "mistake: " + std::string(e.what())); + } +} + + +/// Checks if a given node is set. +/// +/// \param dotted_key The key to be checked. +/// +/// \return True if the key is set to a specific value (not just defined). +/// False if the key is not set or if the key does not exist. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +bool +config::tree::is_set(const std::string& dotted_key) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const leaf_node& child = dynamic_cast< const leaf_node& >( + *raw_node); + return child.is_set(); + } catch (const std::bad_cast& unused_error) { + return false; + } + } catch (const unknown_key_error& unused_error) { + return false; + } +} + + +/// Pushes a leaf node's value onto the Lua stack. +/// +/// \param dotted_key The key to be pushed. +/// \param state The Lua state into which to push the key's value. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +void +config::tree::push_lua(const std::string& dotted_key, lutok::state& state) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const leaf_node& child = dynamic_cast< const leaf_node& >(*raw_node); + child.push_lua(state); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Sets a leaf node's value from a value in the Lua stack. +/// +/// \param dotted_key The key to be set. +/// \param state The Lua state from which to retrieve the value. +/// \param value_index The position in the Lua stack holding the value. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw invalid_key_value If the value mismatches the node type. +/// \throw unknown_key_error If the provided key is unknown. +void +config::tree::set_lua(const std::string& dotted_key, lutok::state& state, + const int value_index) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + detail::base_node* raw_node = _root->lookup_rw( + key, 0, detail::new_node< string_node >); + leaf_node& child = dynamic_cast< leaf_node& >(*raw_node); + child.set_lua(state, value_index); + } catch (const unknown_key_error& e) { + if (_strict) + throw e; + } catch (const value_error& e) { + throw invalid_key_value(key, e.what()); + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } +} + + +/// Gets the value of a node as a plain string. +/// +/// \param dotted_key The key to be looked up. +/// +/// \return The value of the located node as a string. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +std::string +config::tree::lookup_string(const std::string& dotted_key) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const leaf_node& child = dynamic_cast< const leaf_node& >(*raw_node); + return child.to_string(); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Sets the value of a leaf addressed by its key from a string value. +/// +/// This respects the native types of all the nodes that have been predefined. +/// For new nodes under a dynamic subtree, this has no mechanism of determining +/// what type they need to have, so they are created as plain string nodes. +/// +/// \param dotted_key The key to be registered in dotted representation. +/// \param raw_value The string representation of the value to set the node to. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw invalid_key_value If the value mismatches the node type. +/// \throw unknown_key_error If the provided key is unknown. +void +config::tree::set_string(const std::string& dotted_key, + const std::string& raw_value) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + detail::base_node* raw_node = _root->lookup_rw( + key, 0, detail::new_node< string_node >); + leaf_node& child = dynamic_cast< leaf_node& >(*raw_node); + child.set_string(raw_value); + } catch (const unknown_key_error& e) { + if (_strict) + throw e; + } catch (const value_error& e) { + throw invalid_key_value(key, e.what()); + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } +} + + +/// Converts the tree to a collection of key/value string pairs. +/// +/// \param dotted_key Subtree from which to start the export. +/// \param strip_key If true, remove the dotted_key prefix from the resulting +/// properties. +/// +/// \return A map of keys to values in their textual representation. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +/// \throw value_error If the provided key points to a leaf. +config::properties_map +config::tree::all_properties(const std::string& dotted_key, + const bool strip_key) const +{ + PRE(!strip_key || !dotted_key.empty()); + + properties_map properties; + + detail::tree_key key; + const detail::base_node* raw_node; + if (dotted_key.empty()) { + raw_node = _root.get(); + } else { + key = detail::parse_key(dotted_key); + raw_node = _root->lookup_ro(key, 0); + } + try { + const detail::inner_node& child = + dynamic_cast< const detail::inner_node& >(*raw_node); + child.all_properties(properties, key); + } catch (const std::bad_cast& unused_error) { + INV(!dotted_key.empty()); + throw value_error(F("Cannot export properties from a leaf node; " + "'%s' given") % dotted_key); + } + + if (strip_key) { + properties_map stripped; + for (properties_map::const_iterator iter = properties.begin(); + iter != properties.end(); ++iter) { + stripped[(*iter).first.substr(dotted_key.length() + 1)] = + (*iter).second; + } + properties = stripped; + } + + return properties; +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +config::tree::operator==(const tree& other) const +{ + // TODO(jmmv): Would be nicer to perform the comparison directly on the + // nodes, instead of exporting the values to strings first. + return _root == other._root || all_properties() == other.all_properties(); +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +config::tree::operator!=(const tree& other) const +{ + return !(*this == other); +} diff --git a/utils/config/tree.hpp b/utils/config/tree.hpp new file mode 100644 index 000000000000..cad0a9b4fc0b --- /dev/null +++ b/utils/config/tree.hpp @@ -0,0 +1,128 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/tree.hpp +/// Data type to represent a tree of arbitrary values with string keys. + +#if !defined(UTILS_CONFIG_TREE_HPP) +#define UTILS_CONFIG_TREE_HPP + +#include "utils/config/tree_fwd.hpp" + +#include <memory> +#include <string> + +#include <lutok/state.hpp> + +#include "utils/config/keys_fwd.hpp" +#include "utils/config/nodes_fwd.hpp" + +namespace utils { +namespace config { + + +/// Representation of a tree. +/// +/// The string keys of the tree are in dotted notation and actually represent +/// path traversals through the nodes. +/// +/// Our trees are "strictly-keyed": keys must be defined as "existent" before +/// their values can be set. Defining a key is a separate action from setting +/// its value. The rationale is that we want to be able to control what keys +/// get defined: because trees are used to hold configuration, we want to catch +/// typos as early as possible. Also, users cannot set keys unless the types +/// are known in advance because our leaf nodes are strictly typed. +/// +/// However, there is an exception to the strict keys: the inner nodes of the +/// tree can be static or dynamic. Static inner nodes have a known subset of +/// children and attempting to set keys not previously defined will result in an +/// error. Dynamic inner nodes do not have a predefined set of keys and can be +/// used to accept arbitrary user input. +/// +/// For simplicity reasons, we force the root of the tree to be a static inner +/// node. In other words, the root can never contain a value by itself and this +/// is not a problem because the root is not addressable by the key space. +/// Additionally, the root is strict so all of its direct children must be +/// explicitly defined. +/// +/// This is, effectively, a simple wrapper around the node representing the +/// root. Having a separate class aids in clearly representing the concept of a +/// tree and all of its public methods. Also, the tree accepts dotted notations +/// for the keys while the internal structures do not. +/// +/// Note that trees are shallow-copied unless a deep copy is requested with +/// deep_copy(). +class tree { + /// Whether keys must be validated at "set" time. + bool _strict; + + /// The root of the tree. + std::shared_ptr< detail::static_inner_node > _root; + + tree(const bool, detail::static_inner_node*); + +public: + tree(const bool = true); + ~tree(void); + + tree deep_copy(void) const; + tree combine(const tree&) const; + + template< class LeafType > + void define(const std::string&); + + void define_dynamic(const std::string&); + + bool is_set(const std::string&) const; + + template< class LeafType > + const typename LeafType::value_type& lookup(const std::string&) const; + template< class LeafType > + typename LeafType::value_type& lookup_rw(const std::string&); + + template< class LeafType > + void set(const std::string&, const typename LeafType::value_type&); + + void push_lua(const std::string&, lutok::state&) const; + void set_lua(const std::string&, lutok::state&, const int); + + std::string lookup_string(const std::string&) const; + void set_string(const std::string&, const std::string&); + + properties_map all_properties(const std::string& = "", + const bool = false) const; + + bool operator==(const tree&) const; + bool operator!=(const tree&) const; +}; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_TREE_HPP) diff --git a/utils/config/tree.ipp b/utils/config/tree.ipp new file mode 100644 index 000000000000..a79acc3be184 --- /dev/null +++ b/utils/config/tree.ipp @@ -0,0 +1,156 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/tree.hpp" + +#if !defined(UTILS_CONFIG_TREE_IPP) +#define UTILS_CONFIG_TREE_IPP + +#include <typeinfo> + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/config/nodes.ipp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace utils { + + +/// Registers a key as valid and having a specific type. +/// +/// This method does not raise errors on invalid/unknown keys or other +/// tree-related issues. The reasons is that define() is a method that does not +/// depend on user input: it is intended to pre-populate the tree with a +/// specific structure, and that happens once at coding time. +/// +/// \tparam LeafType The node type of the leaf we are defining. +/// \param dotted_key The key to be registered in dotted representation. +template< class LeafType > +void +config::tree::define(const std::string& dotted_key) +{ + try { + const detail::tree_key key = detail::parse_key(dotted_key); + _root->define(key, 0, detail::new_node< LeafType >); + } catch (const error& e) { + UNREACHABLE_MSG(F("define() failing due to key errors is a programming " + "mistake: %s") % e.what()); + } +} + + +/// Gets a read-only reference to the value of a leaf addressed by its key. +/// +/// \tparam LeafType The node type of the leaf we are querying. +/// \param dotted_key The key to be registered in dotted representation. +/// +/// \return A reference to the value in the located leaf, if successful. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +template< class LeafType > +const typename LeafType::value_type& +config::tree::lookup(const std::string& dotted_key) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const LeafType& child = dynamic_cast< const LeafType& >(*raw_node); + if (child.is_set()) + return child.value(); + else + throw unknown_key_error(key); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Gets a read-write reference to the value of a leaf addressed by its key. +/// +/// \tparam LeafType The node type of the leaf we are querying. +/// \param dotted_key The key to be registered in dotted representation. +/// +/// \return A reference to the value in the located leaf, if successful. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +template< class LeafType > +typename LeafType::value_type& +config::tree::lookup_rw(const std::string& dotted_key) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + detail::base_node* raw_node = _root->lookup_rw( + key, 0, detail::new_node< LeafType >); + try { + LeafType& child = dynamic_cast< LeafType& >(*raw_node); + if (child.is_set()) + return child.value(); + else + throw unknown_key_error(key); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Sets the value of a leaf addressed by its key. +/// +/// \tparam LeafType The node type of the leaf we are setting. +/// \param dotted_key The key to be registered in dotted representation. +/// \param value The value to set into the node. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw invalid_key_value If the value mismatches the node type. +/// \throw unknown_key_error If the provided key is unknown. +template< class LeafType > +void +config::tree::set(const std::string& dotted_key, + const typename LeafType::value_type& value) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + leaf_node* raw_node = _root->lookup_rw(key, 0, + detail::new_node< LeafType >); + LeafType& child = dynamic_cast< LeafType& >(*raw_node); + child.set(value); + } catch (const unknown_key_error& e) { + if (_strict) + throw e; + } catch (const value_error& e) { + throw invalid_key_value(key, e.what()); + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } +} + + +} // namespace utils + +#endif // !defined(UTILS_CONFIG_TREE_IPP) diff --git a/utils/config/tree_fwd.hpp b/utils/config/tree_fwd.hpp new file mode 100644 index 000000000000..e494d8c0f4ee --- /dev/null +++ b/utils/config/tree_fwd.hpp @@ -0,0 +1,52 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/config/tree_fwd.hpp +/// Forward declarations for utils/config/tree.hpp + +#if !defined(UTILS_CONFIG_TREE_FWD_HPP) +#define UTILS_CONFIG_TREE_FWD_HPP + +#include <map> +#include <string> + +namespace utils { +namespace config { + + +/// Flat representation of all properties as strings. +typedef std::map< std::string, std::string > properties_map; + + +class tree; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_TREE_FWD_HPP) diff --git a/utils/config/tree_test.cpp b/utils/config/tree_test.cpp new file mode 100644 index 000000000000..b6efd64a84a6 --- /dev/null +++ b/utils/config/tree_test.cpp @@ -0,0 +1,1086 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/config/tree.ipp" + +#include <atf-c++.hpp> + +#include "utils/config/nodes.ipp" +#include "utils/format/macros.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace text = utils::text; + + +namespace { + + +/// Simple wrapper around an integer value without default constructors. +/// +/// The purpose of this type is to have a simple class without default +/// constructors to validate that we can use it as a leaf of a tree. +class int_wrapper { + /// The wrapped integer value. + int _value; + +public: + /// Constructs a new wrapped integer. + /// + /// \param value_ The value to store in the object. + explicit int_wrapper(int value_) : + _value(value_) + { + } + + /// \return The integer value stored by the object. + int + value(void) const + { + return _value; + } +}; + + +/// Custom tree leaf type for an object without defualt constructors. +class wrapped_int_node : public config::typed_leaf_node< int_wrapper > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< wrapped_int_node > new_node(new wrapped_int_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Pushes the node's value onto the Lua stack. + /// + /// \param state The Lua state onto which to push the value. + void + push_lua(lutok::state& state) const + { + state.push_integer( + config::typed_leaf_node< int_wrapper >::value().value()); + } + + /// Sets the value of the node from an entry in the Lua stack. + /// + /// \param state The Lua state from which to get the value. + /// \param value_index The stack index in which the value resides. + void + set_lua(lutok::state& state, const int value_index) + { + ATF_REQUIRE(state.is_number(value_index)); + int_wrapper new_value(state.to_integer(value_index)); + config::typed_leaf_node< int_wrapper >::set(new_value); + } + + /// Sets the value of the node from a raw string representation. + /// + /// \param raw_value The value to set the node to. + void + set_string(const std::string& raw_value) + { + int_wrapper new_value(text::to_type< int >(raw_value)); + config::typed_leaf_node< int_wrapper >::set(new_value); + } + + /// Converts the contents of the node to a string. + /// + /// \return A string representation of the value held by the node. + std::string + to_string(void) const + { + return F("%s") % + config::typed_leaf_node< int_wrapper >::value().value(); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(define_set_lookup__one_level); +ATF_TEST_CASE_BODY(define_set_lookup__one_level) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::string_node >("var2"); + tree.define< config::bool_node >("var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::string_node >("var2", "hello"); + tree.set< config::bool_node >("var3", false); + + ATF_REQUIRE_EQ(42, tree.lookup< config::int_node >("var1")); + ATF_REQUIRE_EQ("hello", tree.lookup< config::string_node >("var2")); + ATF_REQUIRE(!tree.lookup< config::bool_node >("var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(define_set_lookup__multiple_levels); +ATF_TEST_CASE_BODY(define_set_lookup__multiple_levels) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar.1"); + tree.define< config::string_node >("foo.bar.2"); + tree.define< config::bool_node >("foo.3"); + tree.define_dynamic("sub.tree"); + + tree.set< config::int_node >("foo.bar.1", 42); + tree.set< config::string_node >("foo.bar.2", "hello"); + tree.set< config::bool_node >("foo.3", true); + tree.set< config::string_node >("sub.tree.1", "bye"); + tree.set< config::int_node >("sub.tree.2", 4); + tree.set< config::int_node >("sub.tree.3.4", 123); + + ATF_REQUIRE_EQ(42, tree.lookup< config::int_node >("foo.bar.1")); + ATF_REQUIRE_EQ("hello", tree.lookup< config::string_node >("foo.bar.2")); + ATF_REQUIRE(tree.lookup< config::bool_node >("foo.3")); + ATF_REQUIRE_EQ(4, tree.lookup< config::int_node >("sub.tree.2")); + ATF_REQUIRE_EQ(123, tree.lookup< config::int_node >("sub.tree.3.4")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(deep_copy__empty); +ATF_TEST_CASE_BODY(deep_copy__empty) +{ + config::tree tree1; + config::tree tree2 = tree1.deep_copy(); + + tree1.define< config::bool_node >("var1"); + // This would crash if the copy shared the internal data. + tree2.define< config::int_node >("var1"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(deep_copy__some); +ATF_TEST_CASE_BODY(deep_copy__some) +{ + config::tree tree1; + tree1.define< config::bool_node >("this.is.a.var"); + tree1.set< config::bool_node >("this.is.a.var", true); + tree1.define< config::int_node >("this.is.another.var"); + tree1.set< config::int_node >("this.is.another.var", 34); + tree1.define< config::int_node >("and.another"); + tree1.set< config::int_node >("and.another", 123); + + config::tree tree2 = tree1.deep_copy(); + tree2.set< config::bool_node >("this.is.a.var", false); + tree2.set< config::int_node >("this.is.another.var", 43); + + ATF_REQUIRE( tree1.lookup< config::bool_node >("this.is.a.var")); + ATF_REQUIRE(!tree2.lookup< config::bool_node >("this.is.a.var")); + + ATF_REQUIRE_EQ(34, tree1.lookup< config::int_node >("this.is.another.var")); + ATF_REQUIRE_EQ(43, tree2.lookup< config::int_node >("this.is.another.var")); + + ATF_REQUIRE_EQ(123, tree1.lookup< config::int_node >("and.another")); + ATF_REQUIRE_EQ(123, tree2.lookup< config::int_node >("and.another")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__empty); +ATF_TEST_CASE_BODY(combine__empty) +{ + const config::tree t1, t2; + const config::tree combined = t1.combine(t2); + + const config::tree expected; + ATF_REQUIRE(expected == combined); +} + + +static void +init_tree_for_combine_test(config::tree& tree) +{ + tree.define< config::int_node >("int-node"); + tree.define< config::string_node >("string-node"); + tree.define< config::int_node >("unused.node"); + tree.define< config::int_node >("deeper.int.node"); + tree.define_dynamic("deeper.dynamic"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__same_layout__no_overrides); +ATF_TEST_CASE_BODY(combine__same_layout__no_overrides) +{ + config::tree t1, t2; + init_tree_for_combine_test(t1); + init_tree_for_combine_test(t2); + t1.set< config::int_node >("int-node", 3); + t1.set< config::string_node >("string-node", "foo"); + t1.set< config::int_node >("deeper.int.node", 15); + t1.set_string("deeper.dynamic.first", "value1"); + t1.set_string("deeper.dynamic.second", "value2"); + const config::tree combined = t1.combine(t2); + + ATF_REQUIRE(t1 == combined); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__same_layout__no_base); +ATF_TEST_CASE_BODY(combine__same_layout__no_base) +{ + config::tree t1, t2; + init_tree_for_combine_test(t1); + init_tree_for_combine_test(t2); + t2.set< config::int_node >("int-node", 3); + t2.set< config::string_node >("string-node", "foo"); + t2.set< config::int_node >("deeper.int.node", 15); + t2.set_string("deeper.dynamic.first", "value1"); + t2.set_string("deeper.dynamic.second", "value2"); + const config::tree combined = t1.combine(t2); + + ATF_REQUIRE(t2 == combined); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__same_layout__mix); +ATF_TEST_CASE_BODY(combine__same_layout__mix) +{ + config::tree t1, t2; + init_tree_for_combine_test(t1); + init_tree_for_combine_test(t2); + t1.set< config::int_node >("int-node", 3); + t2.set< config::int_node >("int-node", 5); + t1.set< config::string_node >("string-node", "foo"); + t2.set< config::int_node >("deeper.int.node", 15); + t1.set_string("deeper.dynamic.first", "value1"); + t1.set_string("deeper.dynamic.second", "value2.1"); + t2.set_string("deeper.dynamic.second", "value2.2"); + t2.set_string("deeper.dynamic.third", "value3"); + const config::tree combined = t1.combine(t2); + + config::tree expected; + init_tree_for_combine_test(expected); + expected.set< config::int_node >("int-node", 5); + expected.set< config::string_node >("string-node", "foo"); + expected.set< config::int_node >("deeper.int.node", 15); + expected.set_string("deeper.dynamic.first", "value1"); + expected.set_string("deeper.dynamic.second", "value2.2"); + expected.set_string("deeper.dynamic.third", "value3"); + ATF_REQUIRE(expected == combined); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__different_layout); +ATF_TEST_CASE_BODY(combine__different_layout) +{ + config::tree t1; + t1.define< config::int_node >("common.base1"); + t1.define< config::int_node >("common.base2"); + t1.define_dynamic("dynamic.base"); + t1.define< config::int_node >("unset.base"); + + config::tree t2; + t2.define< config::int_node >("common.base2"); + t2.define< config::int_node >("common.base3"); + t2.define_dynamic("dynamic.other"); + t2.define< config::int_node >("unset.other"); + + t1.set< config::int_node >("common.base1", 1); + t1.set< config::int_node >("common.base2", 2); + t1.set_string("dynamic.base.first", "foo"); + t1.set_string("dynamic.base.second", "bar"); + + t2.set< config::int_node >("common.base2", 4); + t2.set< config::int_node >("common.base3", 3); + t2.set_string("dynamic.other.first", "FOO"); + t2.set_string("dynamic.other.second", "BAR"); + + config::tree combined = t1.combine(t2); + + config::tree expected; + expected.define< config::int_node >("common.base1"); + expected.define< config::int_node >("common.base2"); + expected.define< config::int_node >("common.base3"); + expected.define_dynamic("dynamic.base"); + expected.define_dynamic("dynamic.other"); + expected.define< config::int_node >("unset.base"); + expected.define< config::int_node >("unset.other"); + + expected.set< config::int_node >("common.base1", 1); + expected.set< config::int_node >("common.base2", 4); + expected.set< config::int_node >("common.base3", 3); + expected.set_string("dynamic.base.first", "foo"); + expected.set_string("dynamic.base.second", "bar"); + expected.set_string("dynamic.other.first", "FOO"); + expected.set_string("dynamic.other.second", "BAR"); + + ATF_REQUIRE(expected == combined); + + // The combined tree should have respected existing but unset nodes. Check + // that these calls do not crash. + combined.set< config::int_node >("unset.base", 5); + combined.set< config::int_node >("unset.other", 5); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__dynamic_wins); +ATF_TEST_CASE_BODY(combine__dynamic_wins) +{ + config::tree t1; + t1.define< config::int_node >("inner.leaf1"); + t1.set< config::int_node >("inner.leaf1", 3); + + config::tree t2; + t2.define_dynamic("inner"); + t2.set_string("inner.leaf2", "4"); + + config::tree combined = t1.combine(t2); + + config::tree expected; + expected.define_dynamic("inner"); + expected.set_string("inner.leaf1", "3"); + expected.set_string("inner.leaf2", "4"); + + ATF_REQUIRE(expected == combined); + + // The combined inner node should have become dynamic so this call should + // not fail. + combined.set_string("inner.leaf3", "5"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__inner_leaf_mismatch); +ATF_TEST_CASE_BODY(combine__inner_leaf_mismatch) +{ + config::tree t1; + t1.define< config::int_node >("top.foo.bar"); + + config::tree t2; + t2.define< config::int_node >("top.foo"); + + ATF_REQUIRE_THROW_RE(config::bad_combination_error, + "'top.foo' is an inner node in the base tree but a " + "leaf node in the overrides tree", + t1.combine(t2)); + + ATF_REQUIRE_THROW_RE(config::bad_combination_error, + "'top.foo' is a leaf node in the base tree but an " + "inner node in the overrides tree", + t2.combine(t1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup__invalid_key); +ATF_TEST_CASE_BODY(lookup__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, + tree.lookup< config::int_node >(".")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup__unknown_key); +ATF_TEST_CASE_BODY(lookup__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set< config::int_node >("a.b.c", 123); + tree.set< config::int_node >("a.d.100", 0); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("abc")); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("foo")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("foo.bar")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("foo.bar.baz")); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.b")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.c")); + (void)tree.lookup< config::int_node >("a.b.c"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.b.c.d")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d")); + (void)tree.lookup< config::int_node >("a.d.100"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d.101")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d.100.3")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d.e")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_set__one_level); +ATF_TEST_CASE_BODY(is_set__one_level) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::string_node >("var2"); + tree.define< config::bool_node >("var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::bool_node >("var3", false); + + ATF_REQUIRE( tree.is_set("var1")); + ATF_REQUIRE(!tree.is_set("var2")); + ATF_REQUIRE( tree.is_set("var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_set__multiple_levels); +ATF_TEST_CASE_BODY(is_set__multiple_levels) +{ + config::tree tree; + + tree.define< config::int_node >("a.b.var1"); + tree.define< config::string_node >("a.b.var2"); + tree.define< config::bool_node >("e.var3"); + + tree.set< config::int_node >("a.b.var1", 42); + tree.set< config::bool_node >("e.var3", false); + + ATF_REQUIRE(!tree.is_set("a")); + ATF_REQUIRE(!tree.is_set("a.b")); + ATF_REQUIRE( tree.is_set("a.b.var1")); + ATF_REQUIRE(!tree.is_set("a.b.var1.trailing")); + + ATF_REQUIRE(!tree.is_set("a")); + ATF_REQUIRE(!tree.is_set("a.b")); + ATF_REQUIRE(!tree.is_set("a.b.var2")); + ATF_REQUIRE(!tree.is_set("a.b.var2.trailing")); + + ATF_REQUIRE(!tree.is_set("e")); + ATF_REQUIRE( tree.is_set("e.var3")); + ATF_REQUIRE(!tree.is_set("e.var3.trailing")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_set__invalid_key); +ATF_TEST_CASE_BODY(is_set__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.is_set(".abc")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__invalid_key); +ATF_TEST_CASE_BODY(set__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, + tree.set< config::int_node >("foo.", 54)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__invalid_key_value); +ATF_TEST_CASE_BODY(set__invalid_key_value) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define_dynamic("a.d"); + + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set< config::int_node >("foo", 3)); + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set< config::int_node >("a", -10)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__unknown_key); +ATF_TEST_CASE_BODY(set__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set< config::int_node >("a.b.c", 123); + tree.set< config::string_node >("a.d.3", "foo"); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("abc", 2)); + + tree.set< config::int_node >("foo.bar", 15); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("foo.bar.baz", 0)); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("a.c", 100)); + tree.set< config::int_node >("a.b.c", -3); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("a.b.c.d", 82)); + tree.set< config::string_node >("a.d.3", "bar"); + tree.set< config::string_node >("a.d.4", "bar"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("a.d.4.5", 82)); + tree.set< config::int_node >("a.d.5.6", 82); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__unknown_key_not_strict); +ATF_TEST_CASE_BODY(set__unknown_key_not_strict) +{ + config::tree tree(false); + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set< config::int_node >("a.b.c", 123); + tree.set< config::string_node >("a.d.3", "foo"); + + tree.set< config::int_node >("abc", 2); + ATF_REQUIRE(!tree.is_set("abc")); + + tree.set< config::int_node >("foo.bar", 15); + tree.set< config::int_node >("foo.bar.baz", 0); + ATF_REQUIRE(!tree.is_set("foo.bar.baz")); + + tree.set< config::int_node >("a.c", 100); + ATF_REQUIRE(!tree.is_set("a.c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(push_lua__ok); +ATF_TEST_CASE_BODY(push_lua__ok) +{ + config::tree tree; + + tree.define< config::int_node >("top.integer"); + tree.define< wrapped_int_node >("top.custom"); + tree.define_dynamic("dynamic"); + tree.set< config::int_node >("top.integer", 5); + tree.set< wrapped_int_node >("top.custom", int_wrapper(10)); + tree.set_string("dynamic.first", "foo"); + + lutok::state state; + tree.push_lua("top.integer", state); + tree.push_lua("top.custom", state); + tree.push_lua("dynamic.first", state); + ATF_REQUIRE(state.is_number(-3)); + ATF_REQUIRE_EQ(5, state.to_integer(-3)); + ATF_REQUIRE(state.is_number(-2)); + ATF_REQUIRE_EQ(10, state.to_integer(-2)); + ATF_REQUIRE(state.is_string(-1)); + ATF_REQUIRE_EQ("foo", state.to_string(-1)); + state.pop(3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_lua__ok); +ATF_TEST_CASE_BODY(set_lua__ok) +{ + config::tree tree; + + tree.define< config::int_node >("top.integer"); + tree.define< wrapped_int_node >("top.custom"); + tree.define_dynamic("dynamic"); + + { + lutok::state state; + state.push_integer(5); + state.push_integer(10); + state.push_string("foo"); + tree.set_lua("top.integer", state, -3); + tree.set_lua("top.custom", state, -2); + tree.set_lua("dynamic.first", state, -1); + state.pop(3); + } + + ATF_REQUIRE_EQ(5, tree.lookup< config::int_node >("top.integer")); + ATF_REQUIRE_EQ(10, tree.lookup< wrapped_int_node >("top.custom").value()); + ATF_REQUIRE_EQ("foo", tree.lookup< config::string_node >("dynamic.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_rw); +ATF_TEST_CASE_BODY(lookup_rw) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::bool_node >("var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::bool_node >("var3", false); + + tree.lookup_rw< config::int_node >("var1") += 10; + ATF_REQUIRE_EQ(52, tree.lookup< config::int_node >("var1")); + ATF_REQUIRE(!tree.lookup< config::bool_node >("var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_string__ok); +ATF_TEST_CASE_BODY(lookup_string__ok) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::string_node >("b.var2"); + tree.define< config::bool_node >("c.d.var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::string_node >("b.var2", "hello"); + tree.set< config::bool_node >("c.d.var3", false); + + ATF_REQUIRE_EQ("42", tree.lookup_string("var1")); + ATF_REQUIRE_EQ("hello", tree.lookup_string("b.var2")); + ATF_REQUIRE_EQ("false", tree.lookup_string("c.d.var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_string__invalid_key); +ATF_TEST_CASE_BODY(lookup_string__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.lookup_string("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_string__unknown_key); +ATF_TEST_CASE_BODY(lookup_string__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("a.b.c"); + + ATF_REQUIRE_THROW(config::unknown_key_error, tree.lookup_string("a.b")); + ATF_REQUIRE_THROW(config::unknown_key_error, tree.lookup_string("a.b.c.d")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__ok); +ATF_TEST_CASE_BODY(set_string__ok) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar.1"); + tree.define< config::string_node >("foo.bar.2"); + tree.define_dynamic("sub.tree"); + + tree.set_string("foo.bar.1", "42"); + tree.set_string("foo.bar.2", "hello"); + tree.set_string("sub.tree.2", "15"); + tree.set_string("sub.tree.3.4", "bye"); + + ATF_REQUIRE_EQ(42, tree.lookup< config::int_node >("foo.bar.1")); + ATF_REQUIRE_EQ("hello", tree.lookup< config::string_node >("foo.bar.2")); + ATF_REQUIRE_EQ("15", tree.lookup< config::string_node >("sub.tree.2")); + ATF_REQUIRE_EQ("bye", tree.lookup< config::string_node >("sub.tree.3.4")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__invalid_key); +ATF_TEST_CASE_BODY(set_string__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.set_string(".", "foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__invalid_key_value); +ATF_TEST_CASE_BODY(set_string__invalid_key_value) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set_string("foo", "abc")); + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set_string("foo.bar", " -3")); + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set_string("foo.bar", "3 ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__unknown_key); +ATF_TEST_CASE_BODY(set_string__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set_string("a.b.c", "123"); + tree.set_string("a.d.3", "foo"); + + ATF_REQUIRE_THROW(config::unknown_key_error, tree.set_string("abc", "2")); + + tree.set_string("foo.bar", "15"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("foo.bar.baz", "0")); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("a.c", "100")); + tree.set_string("a.b.c", "-3"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("a.b.c.d", "82")); + tree.set_string("a.d.3", "bar"); + tree.set_string("a.d.4", "bar"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("a.d.4.5", "82")); + tree.set_string("a.d.5.6", "82"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__unknown_key_not_strict); +ATF_TEST_CASE_BODY(set_string__unknown_key_not_strict) +{ + config::tree tree(false); + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set_string("a.b.c", "123"); + tree.set_string("a.d.3", "foo"); + + tree.set_string("abc", "2"); + ATF_REQUIRE(!tree.is_set("abc")); + + tree.set_string("foo.bar", "15"); + tree.set_string("foo.bar.baz", "0"); + ATF_REQUIRE(!tree.is_set("foo.bar.baz")); + + tree.set_string("a.c", "100"); + ATF_REQUIRE(!tree.is_set("a.c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__none); +ATF_TEST_CASE_BODY(all_properties__none) +{ + const config::tree tree; + ATF_REQUIRE(tree.all_properties().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__all_set); +ATF_TEST_CASE_BODY(all_properties__all_set) +{ + config::tree tree; + + tree.define< config::int_node >("plain"); + tree.set< config::int_node >("plain", 1234); + + tree.define< config::int_node >("static.first"); + tree.set< config::int_node >("static.first", -3); + tree.define< config::string_node >("static.second"); + tree.set< config::string_node >("static.second", "some text"); + + tree.define_dynamic("dynamic"); + tree.set< config::string_node >("dynamic.first", "hello"); + tree.set< config::string_node >("dynamic.second", "bye"); + + config::properties_map exp_properties; + exp_properties["plain"] = "1234"; + exp_properties["static.first"] = "-3"; + exp_properties["static.second"] = "some text"; + exp_properties["dynamic.first"] = "hello"; + exp_properties["dynamic.second"] = "bye"; + + const config::properties_map properties = tree.all_properties(); + ATF_REQUIRE(exp_properties == properties); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__some_unset); +ATF_TEST_CASE_BODY(all_properties__some_unset) +{ + config::tree tree; + + tree.define< config::int_node >("static.first"); + tree.set< config::int_node >("static.first", -3); + tree.define< config::string_node >("static.second"); + + tree.define_dynamic("dynamic"); + + config::properties_map exp_properties; + exp_properties["static.first"] = "-3"; + + const config::properties_map properties = tree.all_properties(); + ATF_REQUIRE(exp_properties == properties); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__inner); +ATF_TEST_CASE_BODY(all_properties__subtree__inner) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.define< config::int_node >("root.a.b.c.second"); + tree.define< config::int_node >("root.a.d.first"); + + tree.set< config::int_node >("root.a.b.c.first", 1); + tree.set< config::int_node >("root.a.b.c.second", 2); + tree.set< config::int_node >("root.a.d.first", 3); + + { + config::properties_map exp_properties; + exp_properties["root.a.b.c.first"] = "1"; + exp_properties["root.a.b.c.second"] = "2"; + exp_properties["root.a.d.first"] = "3"; + ATF_REQUIRE(exp_properties == tree.all_properties("root")); + ATF_REQUIRE(exp_properties == tree.all_properties("root.a")); + } + + { + config::properties_map exp_properties; + exp_properties["root.a.b.c.first"] = "1"; + exp_properties["root.a.b.c.second"] = "2"; + ATF_REQUIRE(exp_properties == tree.all_properties("root.a.b")); + ATF_REQUIRE(exp_properties == tree.all_properties("root.a.b.c")); + } + + { + config::properties_map exp_properties; + exp_properties["root.a.d.first"] = "3"; + ATF_REQUIRE(exp_properties == tree.all_properties("root.a.d")); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__leaf); +ATF_TEST_CASE_BODY(all_properties__subtree__leaf) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.set< config::int_node >("root.a.b.c.first", 1); + ATF_REQUIRE_THROW_RE(config::value_error, "Cannot export.*leaf", + tree.all_properties("root.a.b.c.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__strip_key); +ATF_TEST_CASE_BODY(all_properties__subtree__strip_key) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.define< config::int_node >("root.a.b.c.second"); + tree.define< config::int_node >("root.a.d.first"); + + tree.set< config::int_node >("root.a.b.c.first", 1); + tree.set< config::int_node >("root.a.b.c.second", 2); + tree.set< config::int_node >("root.a.d.first", 3); + + config::properties_map exp_properties; + exp_properties["b.c.first"] = "1"; + exp_properties["b.c.second"] = "2"; + exp_properties["d.first"] = "3"; + ATF_REQUIRE(exp_properties == tree.all_properties("root.a", true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__invalid_key); +ATF_TEST_CASE_BODY(all_properties__subtree__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.all_properties(".")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__unknown_key); +ATF_TEST_CASE_BODY(all_properties__subtree__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.set< config::int_node >("root.a.b.c.first", 1); + tree.define< config::int_node >("root.a.b.c.unset"); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.all_properties("root.a.b.c.first.foo")); + ATF_REQUIRE_THROW_RE(config::value_error, "Cannot export.*leaf", + tree.all_properties("root.a.b.c.unset")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__empty); +ATF_TEST_CASE_BODY(operators_eq_and_ne__empty) +{ + config::tree t1; + config::tree t2; + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__shallow_copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__shallow_copy) +{ + config::tree t1; + t1.define< config::int_node >("root.a.b.c.first"); + t1.set< config::int_node >("root.a.b.c.first", 1); + config::tree t2 = t1; + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__deep_copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__deep_copy) +{ + config::tree t1; + t1.define< config::int_node >("root.a.b.c.first"); + t1.set< config::int_node >("root.a.b.c.first", 1); + config::tree t2 = t1.deep_copy(); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__some_contents); +ATF_TEST_CASE_BODY(operators_eq_and_ne__some_contents) +{ + config::tree t1, t2; + + t1.define< config::int_node >("root.a.b.c.first"); + t1.set< config::int_node >("root.a.b.c.first", 1); + ATF_REQUIRE(!(t1 == t2)); + ATF_REQUIRE( t1 != t2); + + t2.define< config::int_node >("root.a.b.c.first"); + t2.set< config::int_node >("root.a.b.c.first", 1); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); + + t1.set< config::int_node >("root.a.b.c.first", 2); + ATF_REQUIRE(!(t1 == t2)); + ATF_REQUIRE( t1 != t2); + + t2.set< config::int_node >("root.a.b.c.first", 2); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); + + t1.define< config::string_node >("another.key"); + t1.set< config::string_node >("another.key", "some text"); + ATF_REQUIRE(!(t1 == t2)); + ATF_REQUIRE( t1 != t2); + + t2.define< config::string_node >("another.key"); + t2.set< config::string_node >("another.key", "some text"); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(custom_leaf__no_default_ctor); +ATF_TEST_CASE_BODY(custom_leaf__no_default_ctor) +{ + config::tree tree; + + tree.define< wrapped_int_node >("test1"); + tree.define< wrapped_int_node >("test2"); + tree.set< wrapped_int_node >("test1", int_wrapper(5)); + tree.set< wrapped_int_node >("test2", int_wrapper(10)); + const int_wrapper& test1 = tree.lookup< wrapped_int_node >("test1"); + ATF_REQUIRE_EQ(5, test1.value()); + const int_wrapper& test2 = tree.lookup< wrapped_int_node >("test2"); + ATF_REQUIRE_EQ(10, test2.value()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, define_set_lookup__one_level); + ATF_ADD_TEST_CASE(tcs, define_set_lookup__multiple_levels); + + ATF_ADD_TEST_CASE(tcs, deep_copy__empty); + ATF_ADD_TEST_CASE(tcs, deep_copy__some); + + ATF_ADD_TEST_CASE(tcs, combine__empty); + ATF_ADD_TEST_CASE(tcs, combine__same_layout__no_overrides); + ATF_ADD_TEST_CASE(tcs, combine__same_layout__no_base); + ATF_ADD_TEST_CASE(tcs, combine__same_layout__mix); + ATF_ADD_TEST_CASE(tcs, combine__different_layout); + ATF_ADD_TEST_CASE(tcs, combine__dynamic_wins); + ATF_ADD_TEST_CASE(tcs, combine__inner_leaf_mismatch); + + ATF_ADD_TEST_CASE(tcs, lookup__invalid_key); + ATF_ADD_TEST_CASE(tcs, lookup__unknown_key); + + ATF_ADD_TEST_CASE(tcs, is_set__one_level); + ATF_ADD_TEST_CASE(tcs, is_set__multiple_levels); + ATF_ADD_TEST_CASE(tcs, is_set__invalid_key); + + ATF_ADD_TEST_CASE(tcs, set__invalid_key); + ATF_ADD_TEST_CASE(tcs, set__invalid_key_value); + ATF_ADD_TEST_CASE(tcs, set__unknown_key); + ATF_ADD_TEST_CASE(tcs, set__unknown_key_not_strict); + + ATF_ADD_TEST_CASE(tcs, push_lua__ok); + ATF_ADD_TEST_CASE(tcs, set_lua__ok); + + ATF_ADD_TEST_CASE(tcs, lookup_rw); + + ATF_ADD_TEST_CASE(tcs, lookup_string__ok); + ATF_ADD_TEST_CASE(tcs, lookup_string__invalid_key); + ATF_ADD_TEST_CASE(tcs, lookup_string__unknown_key); + + ATF_ADD_TEST_CASE(tcs, set_string__ok); + ATF_ADD_TEST_CASE(tcs, set_string__invalid_key); + ATF_ADD_TEST_CASE(tcs, set_string__invalid_key_value); + ATF_ADD_TEST_CASE(tcs, set_string__unknown_key); + ATF_ADD_TEST_CASE(tcs, set_string__unknown_key_not_strict); + + ATF_ADD_TEST_CASE(tcs, all_properties__none); + ATF_ADD_TEST_CASE(tcs, all_properties__all_set); + ATF_ADD_TEST_CASE(tcs, all_properties__some_unset); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__inner); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__leaf); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__strip_key); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__invalid_key); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__unknown_key); + + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__empty); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__shallow_copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__deep_copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__some_contents); + + ATF_ADD_TEST_CASE(tcs, custom_leaf__no_default_ctor); +} diff --git a/utils/datetime.cpp b/utils/datetime.cpp new file mode 100644 index 000000000000..ae3fdb62fe55 --- /dev/null +++ b/utils/datetime.cpp @@ -0,0 +1,613 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/datetime.hpp" + +extern "C" { +#include <sys/time.h> + +#include <time.h> +} + +#include <stdexcept> + +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" + +namespace datetime = utils::datetime; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Fake value for the current time. +static optional< datetime::timestamp > mock_now = none; + + +} // anonymous namespace + + +/// Creates a zero time delta. +datetime::delta::delta(void) : + seconds(0), + useconds(0) +{ +} + + +/// Creates a time delta. +/// +/// \param seconds_ The seconds in the delta. +/// \param useconds_ The microseconds in the delta. +/// +/// \throw std::runtime_error If the input delta is negative. +datetime::delta::delta(const int64_t seconds_, + const unsigned long useconds_) : + seconds(seconds_), + useconds(useconds_) +{ + if (seconds_ < 0) { + throw std::runtime_error(F("Negative deltas are not supported by the " + "datetime::delta class; got: %s") % (*this)); + } +} + + +/// Converts a time expressed in microseconds to a delta. +/// +/// \param useconds The amount of microseconds representing the delta. +/// +/// \return A new delta object. +/// +/// \throw std::runtime_error If the input delta is negative. +datetime::delta +datetime::delta::from_microseconds(const int64_t useconds) +{ + if (useconds < 0) { + throw std::runtime_error(F("Negative deltas are not supported by the " + "datetime::delta class; got: %sus") % + useconds); + } + + return delta(useconds / 1000000, useconds % 1000000); +} + + +/// Convers the delta to a flat representation expressed in microseconds. +/// +/// \return The amount of microseconds that corresponds to this delta. +int64_t +datetime::delta::to_microseconds(void) const +{ + return seconds * 1000000 + useconds; +} + + +/// Checks if two time deltas are equal. +/// +/// \param other The object to compare to. +/// +/// \return True if the two time deltas are equals; false otherwise. +bool +datetime::delta::operator==(const datetime::delta& other) const +{ + return seconds == other.seconds && useconds == other.useconds; +} + + +/// Checks if two time deltas are different. +/// +/// \param other The object to compare to. +/// +/// \return True if the two time deltas are different; false otherwise. +bool +datetime::delta::operator!=(const datetime::delta& other) const +{ + return !(*this == other); +} + + +/// Checks if this time delta is shorter than another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is shorter than other; false otherwise. +bool +datetime::delta::operator<(const datetime::delta& other) const +{ + return seconds < other.seconds || + (seconds == other.seconds && useconds < other.useconds); +} + + +/// Checks if this time delta is shorter than or equal to another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is shorter than or equal to; false +/// otherwise. +bool +datetime::delta::operator<=(const datetime::delta& other) const +{ + return (*this) < other || (*this) == other; +} + + +/// Checks if this time delta is larger than another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is larger than other; false otherwise. +bool +datetime::delta::operator>(const datetime::delta& other) const +{ + return seconds > other.seconds || + (seconds == other.seconds && useconds > other.useconds); +} + + +/// Checks if this time delta is larger than or equal to another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is larger than or equal to; false +/// otherwise. +bool +datetime::delta::operator>=(const datetime::delta& other) const +{ + return (*this) > other || (*this) == other; +} + + +/// Adds a time delta to this one. +/// +/// \param other The time delta to add. +/// +/// \return The addition of this time delta with the other time delta. +datetime::delta +datetime::delta::operator+(const datetime::delta& other) const +{ + return delta::from_microseconds(to_microseconds() + + other.to_microseconds()); +} + + +/// Adds a time delta to this one and updates this with the result. +/// +/// \param other The time delta to add. +/// +/// \return The addition of this time delta with the other time delta. +datetime::delta& +datetime::delta::operator+=(const datetime::delta& other) +{ + *this = *this + other; + return *this; +} + + +/// Scales this delta by a positive integral factor. +/// +/// \param factor The scaling factor. +/// +/// \return The scaled delta. +datetime::delta +datetime::delta::operator*(const std::size_t factor) const +{ + return delta::from_microseconds(to_microseconds() * factor); +} + + +/// Scales this delta by and updates this delta with the result. +/// +/// \param factor The scaling factor. +/// +/// \return The scaled delta as a reference to the input object. +datetime::delta& +datetime::delta::operator*=(const std::size_t factor) +{ + *this = *this * factor; + return *this; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +datetime::operator<<(std::ostream& output, const delta& object) +{ + return (output << object.to_microseconds() << "us"); +} + + +namespace utils { +namespace datetime { + + +/// Internal representation for datetime::timestamp. +struct timestamp::impl : utils::noncopyable { + /// The raw timestamp as provided by libc. + ::timeval data; + + /// Constructs an impl object from initialized data. + /// + /// \param data_ The raw timestamp to use. + impl(const ::timeval& data_) : data(data_) + { + } +}; + + +} // namespace datetime +} // namespace utils + + +/// Constructs a new timestamp. +/// +/// \param pimpl_ An existing impl representation. +datetime::timestamp::timestamp(std::shared_ptr< impl > pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Constructs a timestamp from the amount of microseconds since the epoch. +/// +/// \param value Microseconds since the epoch in UTC. Must be positive. +/// +/// \return A new timestamp. +datetime::timestamp +datetime::timestamp::from_microseconds(const int64_t value) +{ + PRE(value >= 0); + ::timeval data; + data.tv_sec = static_cast< time_t >(value / 1000000); + data.tv_usec = static_cast< suseconds_t >(value % 1000000); + return timestamp(std::shared_ptr< impl >(new impl(data))); +} + + +/// Constructs a timestamp based on user-friendly values. +/// +/// \param year The year in the [1900,inf) range. +/// \param month The month in the [1,12] range. +/// \param day The day in the [1,30] range. +/// \param hour The hour in the [0,23] range. +/// \param minute The minute in the [0,59] range. +/// \param second The second in the [0,60] range. Yes, that is 60, which can be +/// the case on leap seconds. +/// \param microsecond The microsecond in the [0,999999] range. +/// +/// \return A new timestamp. +datetime::timestamp +datetime::timestamp::from_values(const int year, const int month, + const int day, const int hour, + const int minute, const int second, + const int microsecond) +{ + PRE(year >= 1900); + PRE(month >= 1 && month <= 12); + PRE(day >= 1 && day <= 30); + PRE(hour >= 0 && hour <= 23); + PRE(minute >= 0 && minute <= 59); + PRE(second >= 0 && second <= 60); + PRE(microsecond >= 0 && microsecond <= 999999); + + // The code below is quite convoluted. The problem is that we can't assume + // that some fields (like tm_zone) of ::tm exist, and thus we can't blindly + // set them from the code. Instead of detecting their presence in the + // configure script, we just query the current time to initialize such + // fields and then we override the ones we are interested in. (There might + // be some better way to do this, but I don't know it and the documentation + // does not shed much light into how to create your own fake date.) + + const time_t current_time = ::time(NULL); + + ::tm timedata; + if (::gmtime_r(¤t_time, &timedata) == NULL) + UNREACHABLE; + + timedata.tm_sec = second; + timedata.tm_min = minute; + timedata.tm_hour = hour; + timedata.tm_mday = day; + timedata.tm_mon = month - 1; + timedata.tm_year = year - 1900; + // Ignored: timedata.tm_wday + // Ignored: timedata.tm_yday + + ::timeval data; + data.tv_sec = ::mktime(&timedata); + data.tv_usec = static_cast< suseconds_t >(microsecond); + return timestamp(std::shared_ptr< impl >(new impl(data))); +} + + +/// Constructs a new timestamp representing the current time in UTC. +/// +/// \return A new timestamp. +datetime::timestamp +datetime::timestamp::now(void) +{ + if (mock_now) + return mock_now.get(); + + ::timeval data; + { + const int ret = ::gettimeofday(&data, NULL); + INV(ret != -1); + } + + return timestamp(std::shared_ptr< impl >(new impl(data))); +} + + +/// Formats a timestamp. +/// +/// \param format The format string to use as consumed by strftime(3). +/// +/// \return The formatted time. +std::string +datetime::timestamp::strftime(const std::string& format) const +{ + ::tm timedata; + // This conversion to time_t is necessary because tv_sec is not guaranteed + // to be a time_t. For example, it isn't in NetBSD 5.x + ::time_t epoch_seconds; + epoch_seconds = _pimpl->data.tv_sec; + if (::gmtime_r(&epoch_seconds, &timedata) == NULL) + UNREACHABLE_MSG("gmtime_r(3) did not accept the value returned by " + "gettimeofday(2)"); + + char buf[128]; + if (::strftime(buf, sizeof(buf), format.c_str(), &timedata) == 0) + UNREACHABLE_MSG("Arbitrary-long format strings are unimplemented"); + return buf; +} + + +/// Formats a timestamp with the ISO 8601 standard and in UTC. +/// +/// \return A string with the formatted timestamp. +std::string +datetime::timestamp::to_iso8601_in_utc(void) const +{ + return F("%s.%06sZ") % strftime("%Y-%m-%dT%H:%M:%S") % _pimpl->data.tv_usec; +} + + +/// Returns the number of microseconds since the epoch in UTC. +/// +/// \return A number of microseconds. +int64_t +datetime::timestamp::to_microseconds(void) const +{ + return static_cast< int64_t >(_pimpl->data.tv_sec) * 1000000 + + _pimpl->data.tv_usec; +} + + +/// Returns the number of seconds since the epoch in UTC. +/// +/// \return A number of seconds. +int64_t +datetime::timestamp::to_seconds(void) const +{ + return static_cast< int64_t >(_pimpl->data.tv_sec); +} + + +/// Sets the current time for testing purposes. +void +datetime::set_mock_now(const int year, const int month, + const int day, const int hour, + const int minute, const int second, + const int microsecond) +{ + mock_now = timestamp::from_values(year, month, day, hour, minute, second, + microsecond); +} + + +/// Sets the current time for testing purposes. +/// +/// \param mock_now_ The mock timestamp to set the time to. +void +datetime::set_mock_now(const timestamp& mock_now_) +{ + mock_now = mock_now_; +} + + +/// Checks if two timestamps are equal. +/// +/// \param other The object to compare to. +/// +/// \return True if the two timestamps are equals; false otherwise. +bool +datetime::timestamp::operator==(const datetime::timestamp& other) const +{ + return _pimpl->data.tv_sec == other._pimpl->data.tv_sec && + _pimpl->data.tv_usec == other._pimpl->data.tv_usec; +} + + +/// Checks if two timestamps are different. +/// +/// \param other The object to compare to. +/// +/// \return True if the two timestamps are different; false otherwise. +bool +datetime::timestamp::operator!=(const datetime::timestamp& other) const +{ + return !(*this == other); +} + + +/// Checks if a timestamp is before another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes before other; false otherwise. +bool +datetime::timestamp::operator<(const datetime::timestamp& other) const +{ + return to_microseconds() < other.to_microseconds(); +} + + +/// Checks if a timestamp is before or equal to another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes before other or is equal to it; +/// false otherwise. +bool +datetime::timestamp::operator<=(const datetime::timestamp& other) const +{ + return to_microseconds() <= other.to_microseconds(); +} + + +/// Checks if a timestamp is after another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes after other; false otherwise; +bool +datetime::timestamp::operator>(const datetime::timestamp& other) const +{ + return to_microseconds() > other.to_microseconds(); +} + + +/// Checks if a timestamp is after or equal to another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes after other or is equal to it; +/// false otherwise. +bool +datetime::timestamp::operator>=(const datetime::timestamp& other) const +{ + return to_microseconds() >= other.to_microseconds(); +} + + +/// Calculates the addition of a delta to a timestamp. +/// +/// \param other The delta to add. +/// +/// \return A new timestamp in the future. +datetime::timestamp +datetime::timestamp::operator+(const datetime::delta& other) const +{ + return datetime::timestamp::from_microseconds(to_microseconds() + + other.to_microseconds()); +} + + +/// Calculates the addition of a delta to this timestamp. +/// +/// \param other The delta to add. +/// +/// \return A reference to the modified timestamp. +datetime::timestamp& +datetime::timestamp::operator+=(const datetime::delta& other) +{ + *this = *this + other; + return *this; +} + + +/// Calculates the subtraction of a delta from a timestamp. +/// +/// \param other The delta to subtract. +/// +/// \return A new timestamp in the past. +datetime::timestamp +datetime::timestamp::operator-(const datetime::delta& other) const +{ + return datetime::timestamp::from_microseconds(to_microseconds() - + other.to_microseconds()); +} + + +/// Calculates the subtraction of a delta from this timestamp. +/// +/// \param other The delta to subtract. +/// +/// \return A reference to the modified timestamp. +datetime::timestamp& +datetime::timestamp::operator-=(const datetime::delta& other) +{ + *this = *this - other; + return *this; +} + + +/// Calculates the delta between two timestamps. +/// +/// \param other The subtrahend. +/// +/// \return The difference between this object and the other object. +/// +/// \throw std::runtime_error If the subtraction would result in a negative time +/// delta, which are currently not supported. +datetime::delta +datetime::timestamp::operator-(const datetime::timestamp& other) const +{ + if ((*this) < other) { + throw std::runtime_error( + F("Cannot subtract %s from %s as it would result in a negative " + "datetime::delta, which are not supported") % other % (*this)); + } + return datetime::delta::from_microseconds(to_microseconds() - + other.to_microseconds()); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +datetime::operator<<(std::ostream& output, const timestamp& object) +{ + return (output << object.to_microseconds() << "us"); +} diff --git a/utils/datetime.hpp b/utils/datetime.hpp new file mode 100644 index 000000000000..0c24f332f6d3 --- /dev/null +++ b/utils/datetime.hpp @@ -0,0 +1,140 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/datetime.hpp +/// Provides date and time-related classes and functions. + +#if !defined(UTILS_DATETIME_HPP) +#define UTILS_DATETIME_HPP + +#include "utils/datetime_fwd.hpp" + +extern "C" { +#include <stdint.h> +} + +#include <cstddef> +#include <memory> +#include <ostream> +#include <string> + + +namespace utils { +namespace datetime { + + +/// Represents a time delta to describe deadlines. +/// +/// Because we use this class to handle deadlines, we currently do not support +/// negative deltas. +class delta { +public: + /// The amount of seconds in the time delta. + int64_t seconds; + + /// The amount of microseconds in the time delta. + unsigned long useconds; + + delta(void); + delta(const int64_t, const unsigned long); + + static delta from_microseconds(const int64_t); + int64_t to_microseconds(void) const; + + bool operator==(const delta&) const; + bool operator!=(const delta&) const; + bool operator<(const delta&) const; + bool operator<=(const delta&) const; + bool operator>(const delta&) const; + bool operator>=(const delta&) const; + + delta operator+(const delta&) const; + delta& operator+=(const delta&); + // operator- and operator-= do not exist because we do not support negative + // deltas. See class docstring. + delta operator*(const std::size_t) const; + delta& operator*=(const std::size_t); +}; + + +std::ostream& operator<<(std::ostream&, const delta&); + + +/// Represents a fixed date/time. +/// +/// Timestamps are immutable objects and therefore we can simply use a shared +/// pointer to hide the implementation type of the date. By not using an auto +/// pointer, we don't have to worry about providing our own copy constructor and +/// assignment opertor. +class timestamp { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + timestamp(std::shared_ptr< impl >); + +public: + static timestamp from_microseconds(const int64_t); + static timestamp from_values(const int, const int, const int, + const int, const int, const int, + const int); + static timestamp now(void); + + std::string strftime(const std::string&) const; + std::string to_iso8601_in_utc(void) const; + int64_t to_microseconds(void) const; + int64_t to_seconds(void) const; + + bool operator==(const timestamp&) const; + bool operator!=(const timestamp&) const; + bool operator<(const timestamp&) const; + bool operator<=(const timestamp&) const; + bool operator>(const timestamp&) const; + bool operator>=(const timestamp&) const; + + timestamp operator+(const delta&) const; + timestamp& operator+=(const delta&); + timestamp operator-(const delta&) const; + timestamp& operator-=(const delta&); + delta operator-(const timestamp&) const; +}; + + +std::ostream& operator<<(std::ostream&, const timestamp&); + + +void set_mock_now(const int, const int, const int, const int, const int, + const int, const int); +void set_mock_now(const timestamp&); + + +} // namespace datetime +} // namespace utils + +#endif // !defined(UTILS_DATETIME_HPP) diff --git a/utils/datetime_fwd.hpp b/utils/datetime_fwd.hpp new file mode 100644 index 000000000000..1dd886070a34 --- /dev/null +++ b/utils/datetime_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/datetime_fwd.hpp +/// Forward declarations for utils/datetime.hpp + +#if !defined(UTILS_DATETIME_FWD_HPP) +#define UTILS_DATETIME_FWD_HPP + +namespace utils { +namespace datetime { + + +class delta; +class timestamp; + + +} // namespace datetime +} // namespace utils + +#endif // !defined(UTILS_DATETIME_FWD_HPP) diff --git a/utils/datetime_test.cpp b/utils/datetime_test.cpp new file mode 100644 index 000000000000..9f8ff50cd0f8 --- /dev/null +++ b/utils/datetime_test.cpp @@ -0,0 +1,593 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/datetime.hpp" + +extern "C" { +#include <time.h> +#include <unistd.h> +} + +#include <sstream> +#include <stdexcept> + +#include <atf-c++.hpp> + +namespace datetime = utils::datetime; + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__defaults); +ATF_TEST_CASE_BODY(delta__defaults) +{ + const datetime::delta delta; + ATF_REQUIRE_EQ(0, delta.seconds); + ATF_REQUIRE_EQ(0, delta.useconds); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__overrides); +ATF_TEST_CASE_BODY(delta__overrides) +{ + const datetime::delta delta(1, 2); + ATF_REQUIRE_EQ(1, delta.seconds); + ATF_REQUIRE_EQ(2, delta.useconds); + + ATF_REQUIRE_THROW_RE( + std::runtime_error, "Negative.*not supported.*-4999997us", + datetime::delta(-5, 3)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__from_microseconds); +ATF_TEST_CASE_BODY(delta__from_microseconds) +{ + { + const datetime::delta delta = datetime::delta::from_microseconds(0); + ATF_REQUIRE_EQ(0, delta.seconds); + ATF_REQUIRE_EQ(0, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 999999); + ATF_REQUIRE_EQ(0, delta.seconds); + ATF_REQUIRE_EQ(999999, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 1000000); + ATF_REQUIRE_EQ(1, delta.seconds); + ATF_REQUIRE_EQ(0, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 10576293); + ATF_REQUIRE_EQ(10, delta.seconds); + ATF_REQUIRE_EQ(576293, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 123456789123456LL); + ATF_REQUIRE_EQ(123456789, delta.seconds); + ATF_REQUIRE_EQ(123456, delta.useconds); + } + + ATF_REQUIRE_THROW_RE( + std::runtime_error, "Negative.*not supported.*-12345us", + datetime::delta::from_microseconds(-12345)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__to_microseconds); +ATF_TEST_CASE_BODY(delta__to_microseconds) +{ + ATF_REQUIRE_EQ(0, datetime::delta(0, 0).to_microseconds()); + ATF_REQUIRE_EQ(999999, datetime::delta(0, 999999).to_microseconds()); + ATF_REQUIRE_EQ(1000000, datetime::delta(1, 0).to_microseconds()); + ATF_REQUIRE_EQ(10576293, datetime::delta(10, 576293).to_microseconds()); + ATF_REQUIRE_EQ(11576293, datetime::delta(10, 1576293).to_microseconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__equals); +ATF_TEST_CASE_BODY(delta__equals) +{ + ATF_REQUIRE(datetime::delta() == datetime::delta()); + ATF_REQUIRE(datetime::delta() == datetime::delta(0, 0)); + ATF_REQUIRE(datetime::delta(1, 2) == datetime::delta(1, 2)); + + ATF_REQUIRE(!(datetime::delta() == datetime::delta(0, 1))); + ATF_REQUIRE(!(datetime::delta() == datetime::delta(1, 0))); + ATF_REQUIRE(!(datetime::delta(1, 2) == datetime::delta(2, 1))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__differs); +ATF_TEST_CASE_BODY(delta__differs) +{ + ATF_REQUIRE(!(datetime::delta() != datetime::delta())); + ATF_REQUIRE(!(datetime::delta() != datetime::delta(0, 0))); + ATF_REQUIRE(!(datetime::delta(1, 2) != datetime::delta(1, 2))); + + ATF_REQUIRE(datetime::delta() != datetime::delta(0, 1)); + ATF_REQUIRE(datetime::delta() != datetime::delta(1, 0)); + ATF_REQUIRE(datetime::delta(1, 2) != datetime::delta(2, 1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__sorting); +ATF_TEST_CASE_BODY(delta__sorting) +{ + ATF_REQUIRE(!(datetime::delta() < datetime::delta())); + ATF_REQUIRE( datetime::delta() <= datetime::delta()); + ATF_REQUIRE(!(datetime::delta() > datetime::delta())); + ATF_REQUIRE( datetime::delta() >= datetime::delta()); + + ATF_REQUIRE(!(datetime::delta(9, 8) < datetime::delta(9, 8))); + ATF_REQUIRE( datetime::delta(9, 8) <= datetime::delta(9, 8)); + ATF_REQUIRE(!(datetime::delta(9, 8) > datetime::delta(9, 8))); + ATF_REQUIRE( datetime::delta(9, 8) >= datetime::delta(9, 8)); + + ATF_REQUIRE( datetime::delta(2, 5) < datetime::delta(4, 8)); + ATF_REQUIRE( datetime::delta(2, 5) <= datetime::delta(4, 8)); + ATF_REQUIRE(!(datetime::delta(2, 5) > datetime::delta(4, 8))); + ATF_REQUIRE(!(datetime::delta(2, 5) >= datetime::delta(4, 8))); + + ATF_REQUIRE( datetime::delta(2, 5) < datetime::delta(2, 8)); + ATF_REQUIRE( datetime::delta(2, 5) <= datetime::delta(2, 8)); + ATF_REQUIRE(!(datetime::delta(2, 5) > datetime::delta(2, 8))); + ATF_REQUIRE(!(datetime::delta(2, 5) >= datetime::delta(2, 8))); + + ATF_REQUIRE(!(datetime::delta(4, 8) < datetime::delta(2, 5))); + ATF_REQUIRE(!(datetime::delta(4, 8) <= datetime::delta(2, 5))); + ATF_REQUIRE( datetime::delta(4, 8) > datetime::delta(2, 5)); + ATF_REQUIRE( datetime::delta(4, 8) >= datetime::delta(2, 5)); + + ATF_REQUIRE(!(datetime::delta(2, 8) < datetime::delta(2, 5))); + ATF_REQUIRE(!(datetime::delta(2, 8) <= datetime::delta(2, 5))); + ATF_REQUIRE( datetime::delta(2, 8) > datetime::delta(2, 5)); + ATF_REQUIRE( datetime::delta(2, 8) >= datetime::delta(2, 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__addition); +ATF_TEST_CASE_BODY(delta__addition) +{ + using datetime::delta; + + ATF_REQUIRE_EQ(delta(), delta() + delta()); + ATF_REQUIRE_EQ(delta(0, 10), delta() + delta(0, 10)); + ATF_REQUIRE_EQ(delta(10, 0), delta(10, 0) + delta()); + + ATF_REQUIRE_EQ(delta(1, 234567), delta(0, 1234567) + delta()); + ATF_REQUIRE_EQ(delta(12, 34), delta(10, 20) + delta(2, 14)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__addition_and_set); +ATF_TEST_CASE_BODY(delta__addition_and_set) +{ + using datetime::delta; + + { + delta d; + d += delta(3, 5); + ATF_REQUIRE_EQ(delta(3, 5), d); + } + { + delta d(1, 2); + d += delta(3, 5); + ATF_REQUIRE_EQ(delta(4, 7), d); + } + { + delta d(1, 2); + ATF_REQUIRE_EQ(delta(4, 7), (d += delta(3, 5))); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__scale); +ATF_TEST_CASE_BODY(delta__scale) +{ + using datetime::delta; + + ATF_REQUIRE_EQ(delta(), delta() * 0); + ATF_REQUIRE_EQ(delta(), delta() * 5); + + ATF_REQUIRE_EQ(delta(0, 30), delta(0, 10) * 3); + ATF_REQUIRE_EQ(delta(17, 500000), delta(3, 500000) * 5); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__scale_and_set); +ATF_TEST_CASE_BODY(delta__scale_and_set) +{ + using datetime::delta; + + { + delta d(3, 5); + d *= 2; + ATF_REQUIRE_EQ(delta(6, 10), d); + } + { + delta d(8, 0); + d *= 8; + ATF_REQUIRE_EQ(delta(64, 0), d); + } + { + delta d(3, 5); + ATF_REQUIRE_EQ(delta(9, 15), (d *= 3)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__output); +ATF_TEST_CASE_BODY(delta__output) +{ + { + std::ostringstream str; + str << datetime::delta(15, 8791); + ATF_REQUIRE_EQ("15008791us", str.str()); + } + { + std::ostringstream str; + str << datetime::delta(12345678, 0); + ATF_REQUIRE_EQ("12345678000000us", str.str()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__copy); +ATF_TEST_CASE_BODY(timestamp__copy) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2011, 2, 16, 19, 15, 30, 0); + { + const datetime::timestamp ts2 = ts1; + const datetime::timestamp ts3 = datetime::timestamp::from_values( + 2012, 2, 16, 19, 15, 30, 0); + ATF_REQUIRE_EQ("2011", ts1.strftime("%Y")); + ATF_REQUIRE_EQ("2011", ts2.strftime("%Y")); + ATF_REQUIRE_EQ("2012", ts3.strftime("%Y")); + } + ATF_REQUIRE_EQ("2011", ts1.strftime("%Y")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__from_microseconds); +ATF_TEST_CASE_BODY(timestamp__from_microseconds) +{ + const datetime::timestamp ts = datetime::timestamp::from_microseconds( + 1328829351987654LL); + ATF_REQUIRE_EQ("2012-02-09 23:15:51", ts.strftime("%Y-%m-%d %H:%M:%S")); + ATF_REQUIRE_EQ(1328829351987654LL, ts.to_microseconds()); + ATF_REQUIRE_EQ(1328829351, ts.to_seconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__mock); +ATF_TEST_CASE_BODY(timestamp__now__mock) +{ + datetime::set_mock_now(2011, 2, 21, 18, 5, 10, 0); + ATF_REQUIRE_EQ("2011-02-21 18:05:10", + datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); + + datetime::set_mock_now(datetime::timestamp::from_values( + 2012, 3, 22, 19, 6, 11, 54321)); + ATF_REQUIRE_EQ("2012-03-22 19:06:11", + datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); + ATF_REQUIRE_EQ("2012-03-22 19:06:11", + datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__real); +ATF_TEST_CASE_BODY(timestamp__now__real) +{ + // This test is might fail if we happen to run at the crossing of one + // day to the other and the two measures we pick of the current time + // differ. This is so unlikely that I haven't bothered to do this in any + // other way. + + const time_t just_before = ::time(NULL); + const datetime::timestamp now = datetime::timestamp::now(); + + ::tm data; + char buf[1024]; + ATF_REQUIRE(::gmtime_r(&just_before, &data) != 0); + ATF_REQUIRE(::strftime(buf, sizeof(buf), "%Y-%m-%d", &data) != 0); + ATF_REQUIRE_EQ(buf, now.strftime("%Y-%m-%d")); + + ATF_REQUIRE(now.strftime("%Z") == "GMT" || now.strftime("%Z") == "UTC"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__granularity); +ATF_TEST_CASE_BODY(timestamp__now__granularity) +{ + const datetime::timestamp first = datetime::timestamp::now(); + ::usleep(1); + const datetime::timestamp second = datetime::timestamp::now(); + ATF_REQUIRE(first.to_microseconds() != second.to_microseconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__strftime); +ATF_TEST_CASE_BODY(timestamp__strftime) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 0); + ATF_REQUIRE_EQ("2010-12-10", ts1.strftime("%Y-%m-%d")); + ATF_REQUIRE_EQ("08:45:50", ts1.strftime("%H:%M:%S")); + + const datetime::timestamp ts2 = datetime::timestamp::from_values( + 2011, 2, 16, 19, 15, 30, 0); + ATF_REQUIRE_EQ("2011-02-16T19:15:30", ts2.strftime("%Y-%m-%dT%H:%M:%S")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_iso8601_in_utc); +ATF_TEST_CASE_BODY(timestamp__to_iso8601_in_utc) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 0); + ATF_REQUIRE_EQ("2010-12-10T08:45:50.000000Z", ts1.to_iso8601_in_utc()); + + const datetime::timestamp ts2= datetime::timestamp::from_values( + 2016, 7, 11, 17, 51, 28, 123456); + ATF_REQUIRE_EQ("2016-07-11T17:51:28.123456Z", ts2.to_iso8601_in_utc()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_microseconds); +ATF_TEST_CASE_BODY(timestamp__to_microseconds) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 123456); + ATF_REQUIRE_EQ(1291970750123456LL, ts1.to_microseconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_seconds); +ATF_TEST_CASE_BODY(timestamp__to_seconds) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 123456); + ATF_REQUIRE_EQ(1291970750, ts1.to_seconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__leap_second); +ATF_TEST_CASE_BODY(timestamp__leap_second) +{ + // This is actually a test for from_values(), which is the function that + // includes assertions to validate the input parameters. + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2012, 6, 30, 23, 59, 60, 543); + ATF_REQUIRE_EQ(1341100800, ts1.to_seconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__equals); +ATF_TEST_CASE_BODY(timestamp__equals) +{ + ATF_REQUIRE(datetime::timestamp::from_microseconds(1291970750123456LL) == + datetime::timestamp::from_microseconds(1291970750123456LL)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__differs); +ATF_TEST_CASE_BODY(timestamp__differs) +{ + ATF_REQUIRE(datetime::timestamp::from_microseconds(1291970750123456LL) != + datetime::timestamp::from_microseconds(1291970750123455LL)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__sorting); +ATF_TEST_CASE_BODY(timestamp__sorting) +{ + { + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + + ATF_REQUIRE(!(ts1 < ts2)); + ATF_REQUIRE( ts1 <= ts2); + ATF_REQUIRE(!(ts1 > ts2)); + ATF_REQUIRE( ts1 >= ts2); + } + { + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970759123455LL); + + ATF_REQUIRE( ts1 < ts2); + ATF_REQUIRE( ts1 <= ts2); + ATF_REQUIRE(!(ts1 > ts2)); + ATF_REQUIRE(!(ts1 >= ts2)); + } + { + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970759123455LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + + ATF_REQUIRE(!(ts1 < ts2)); + ATF_REQUIRE(!(ts1 <= ts2)); + ATF_REQUIRE( ts1 > ts2); + ATF_REQUIRE( ts1 >= ts2); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__add_delta); +ATF_TEST_CASE_BODY(timestamp__add_delta) +{ + using datetime::delta; + using datetime::timestamp; + + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 30, 1234), + timestamp::from_values(2014, 12, 11, 21, 43, 0, 0) + + delta(30, 1234)); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 22, 43, 7, 100), + timestamp::from_values(2014, 12, 11, 21, 43, 0, 0) + + delta(3602, 5000100)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__add_delta_and_set); +ATF_TEST_CASE_BODY(timestamp__add_delta_and_set) +{ + using datetime::delta; + using datetime::timestamp; + + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 0, 0); + ts += delta(30, 1234); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 30, 1234), + ts); + } + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 0, 0); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 22, 43, 7, 100), + ts += delta(3602, 5000100)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtract_delta); +ATF_TEST_CASE_BODY(timestamp__subtract_delta) +{ + using datetime::delta; + using datetime::timestamp; + + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 10, 4321), + timestamp::from_values(2014, 12, 11, 21, 43, 40, 5555) - + delta(30, 1234)); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 20, 43, 1, 300), + timestamp::from_values(2014, 12, 11, 21, 43, 8, 400) - + delta(3602, 5000100)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtract_delta_and_set); +ATF_TEST_CASE_BODY(timestamp__subtract_delta_and_set) +{ + using datetime::delta; + using datetime::timestamp; + + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 40, 5555); + ts -= delta(30, 1234); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 10, 4321), + ts); + } + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 8, 400); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 20, 43, 1, 300), + ts -= delta(3602, 5000100)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtraction); +ATF_TEST_CASE_BODY(timestamp__subtraction) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970750123456LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970750123468LL); + const datetime::timestamp ts3 = datetime::timestamp::from_microseconds( + 1291970850123456LL); + + ATF_REQUIRE_EQ(datetime::delta(0, 0), ts1 - ts1); + ATF_REQUIRE_EQ(datetime::delta(0, 12), ts2 - ts1); + ATF_REQUIRE_EQ(datetime::delta(100, 0), ts3 - ts1); + ATF_REQUIRE_EQ(datetime::delta(99, 999988), ts3 - ts2); + + ATF_REQUIRE_THROW_RE( + std::runtime_error, + "Cannot subtract 1291970850123456us from 1291970750123468us " + ".*negative datetime::delta.*not supported", + ts2 - ts3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__output); +ATF_TEST_CASE_BODY(timestamp__output) +{ + { + std::ostringstream str; + str << datetime::timestamp::from_microseconds(1291970750123456LL); + ATF_REQUIRE_EQ("1291970750123456us", str.str()); + } + { + std::ostringstream str; + str << datetime::timestamp::from_microseconds(1028309798759812LL); + ATF_REQUIRE_EQ("1028309798759812us", str.str()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, delta__defaults); + ATF_ADD_TEST_CASE(tcs, delta__overrides); + ATF_ADD_TEST_CASE(tcs, delta__from_microseconds); + ATF_ADD_TEST_CASE(tcs, delta__to_microseconds); + ATF_ADD_TEST_CASE(tcs, delta__equals); + ATF_ADD_TEST_CASE(tcs, delta__differs); + ATF_ADD_TEST_CASE(tcs, delta__sorting); + ATF_ADD_TEST_CASE(tcs, delta__addition); + ATF_ADD_TEST_CASE(tcs, delta__addition_and_set); + ATF_ADD_TEST_CASE(tcs, delta__scale); + ATF_ADD_TEST_CASE(tcs, delta__scale_and_set); + ATF_ADD_TEST_CASE(tcs, delta__output); + + ATF_ADD_TEST_CASE(tcs, timestamp__copy); + ATF_ADD_TEST_CASE(tcs, timestamp__from_microseconds); + ATF_ADD_TEST_CASE(tcs, timestamp__now__mock); + ATF_ADD_TEST_CASE(tcs, timestamp__now__real); + ATF_ADD_TEST_CASE(tcs, timestamp__now__granularity); + ATF_ADD_TEST_CASE(tcs, timestamp__strftime); + ATF_ADD_TEST_CASE(tcs, timestamp__to_iso8601_in_utc); + ATF_ADD_TEST_CASE(tcs, timestamp__to_microseconds); + ATF_ADD_TEST_CASE(tcs, timestamp__to_seconds); + ATF_ADD_TEST_CASE(tcs, timestamp__leap_second); + ATF_ADD_TEST_CASE(tcs, timestamp__equals); + ATF_ADD_TEST_CASE(tcs, timestamp__differs); + ATF_ADD_TEST_CASE(tcs, timestamp__sorting); + ATF_ADD_TEST_CASE(tcs, timestamp__add_delta); + ATF_ADD_TEST_CASE(tcs, timestamp__add_delta_and_set); + ATF_ADD_TEST_CASE(tcs, timestamp__subtract_delta); + ATF_ADD_TEST_CASE(tcs, timestamp__subtract_delta_and_set); + ATF_ADD_TEST_CASE(tcs, timestamp__subtraction); + ATF_ADD_TEST_CASE(tcs, timestamp__output); +} diff --git a/utils/defs.hpp.in b/utils/defs.hpp.in new file mode 100644 index 000000000000..62fc50d0e525 --- /dev/null +++ b/utils/defs.hpp.in @@ -0,0 +1,57 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/defs.hpp +/// +/// Definitions for compiler and system features autodetected at configuration +/// time. + +#if !defined(UTILS_DEFS_HPP) +#define UTILS_DEFS_HPP + + +/// Attribute to mark a function as non-returning. +#define UTILS_NORETURN @ATTRIBUTE_NORETURN@ + + +/// Attribute to mark a function as pure. +#define UTILS_PURE @ATTRIBUTE_PURE@ + + +/// Attribute to mark an entity as unused. +#define UTILS_UNUSED @ATTRIBUTE_UNUSED@ + + +/// Unconstifies a pointer. +/// +/// \param type The target type of the conversion. +/// \param ptr The pointer to be unconstified. +#define UTILS_UNCONST(type, ptr) ((type*)(unsigned long)(const void*)(ptr)) + + +#endif // !defined(UTILS_DEFS_HPP) diff --git a/utils/env.cpp b/utils/env.cpp new file mode 100644 index 000000000000..b0d995c0ff31 --- /dev/null +++ b/utils/env.cpp @@ -0,0 +1,200 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/env.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <stdexcept> + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" + +namespace fs = utils::fs; + +using utils::none; +using utils::optional; + + +extern "C" { + extern char** environ; +} + + +/// Gets all environment variables. +/// +/// \return A mapping of (name, value) pairs describing the environment +/// variables. +std::map< std::string, std::string > +utils::getallenv(void) +{ + std::map< std::string, std::string > allenv; + for (char** envp = environ; *envp != NULL; envp++) { + const std::string oneenv = *envp; + const std::string::size_type pos = oneenv.find('='); + const std::string name = oneenv.substr(0, pos); + const std::string value = oneenv.substr(pos + 1); + + PRE(allenv.find(name) == allenv.end()); + allenv[name] = value; + } + return allenv; +} + + +/// Gets the value of an environment variable. +/// +/// \param name The name of the environment variable to query. +/// +/// \return The value of the environment variable if it is defined, or none +/// otherwise. +optional< std::string > +utils::getenv(const std::string& name) +{ + const char* value = std::getenv(name.c_str()); + if (value == NULL) { + LD(F("Environment variable '%s' is not defined") % name); + return none; + } else { + LD(F("Environment variable '%s' is '%s'") % name % value); + return utils::make_optional(std::string(value)); + } +} + + +/// Gets the value of an environment variable with a default fallback. +/// +/// \param name The name of the environment variable to query. +/// \param default_value The value to return if the variable is not defined. +/// +/// \return The value of the environment variable. +std::string +utils::getenv_with_default(const std::string& name, + const std::string& default_value) +{ + const char* value = std::getenv(name.c_str()); + if (value == NULL) { + LD(F("Environment variable '%s' is not defined; using default '%s'") % + name % default_value); + return default_value; + } else { + LD(F("Environment variable '%s' is '%s'") % name % value); + return value; + } +} + + +/// Gets the value of the HOME environment variable with path validation. +/// +/// \return The value of the HOME environment variable if it is a valid path; +/// none if it is not defined or if it contains an invalid path. +optional< fs::path > +utils::get_home(void) +{ + const optional< std::string > home = utils::getenv("HOME"); + if (home) { + try { + return utils::make_optional(fs::path(home.get())); + } catch (const fs::error& e) { + LW(F("Invalid value '%s' in HOME environment variable: %s") % + home.get() % e.what()); + return none; + } + } else { + return none; + } +} + + +/// Sets the value of an environment variable. +/// +/// \param name The name of the environment variable to set. +/// \param val The value to set the environment variable to. May be empty. +/// +/// \throw std::runtime_error If there is an error setting the environment +/// variable. +void +utils::setenv(const std::string& name, const std::string& val) +{ + LD(F("Setting environment variable '%s' to '%s'") % name % val); +#if defined(HAVE_SETENV) + if (::setenv(name.c_str(), val.c_str(), 1) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to set environment variable '%s' to " + "'%s': %s") % + name % val % std::strerror(original_errno)); + } +#elif defined(HAVE_PUTENV) + if (::putenv((F("%s=%s") % name % val).c_str()) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to set environment variable '%s' to " + "'%s': %s") % + name % val % std::strerror(original_errno)); + } +#else +# error "Don't know how to set an environment variable." +#endif +} + + +/// Unsets an environment variable. +/// +/// \param name The name of the environment variable to unset. +/// +/// \throw std::runtime_error If there is an error unsetting the environment +/// variable. +void +utils::unsetenv(const std::string& name) +{ + LD(F("Unsetting environment variable '%s'") % name); +#if defined(HAVE_UNSETENV) + if (::unsetenv(name.c_str()) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to unset environment variable " + "'%s'") % + name % std::strerror(original_errno)); + } +#elif defined(HAVE_PUTENV) + if (::putenv((F("%s=") % name).c_str()) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to unset environment variable " + "'%s'") % + name % std::strerror(original_errno)); + } +#else +# error "Don't know how to unset an environment variable." +#endif +} diff --git a/utils/env.hpp b/utils/env.hpp new file mode 100644 index 000000000000..2370ee490dc1 --- /dev/null +++ b/utils/env.hpp @@ -0,0 +1,58 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/env.hpp +/// Querying and manipulation of environment variables. +/// +/// These utility functions wrap the system functions to manipulate the +/// environment in a portable way and expose their arguments and return values +/// in a C++-friendly manner. + +#if !defined(UTILS_ENV_HPP) +#define UTILS_ENV_HPP + +#include <map> +#include <string> + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace utils { + + +std::map< std::string, std::string > getallenv(void); +optional< std::string > getenv(const std::string&); +std::string getenv_with_default(const std::string&, const std::string&); +optional< utils::fs::path > get_home(void); +void setenv(const std::string&, const std::string&); +void unsetenv(const std::string&); + + +} // namespace utils + +#endif // !defined(UTILS_ENV_HPP) diff --git a/utils/env_test.cpp b/utils/env_test.cpp new file mode 100644 index 000000000000..1b16266443af --- /dev/null +++ b/utils/env_test.cpp @@ -0,0 +1,167 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/env.hpp" + +#include <atf-c++.hpp> + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace fs = utils::fs; + +using utils::optional; + + +ATF_TEST_CASE_WITHOUT_HEAD(getallenv); +ATF_TEST_CASE_BODY(getallenv) +{ + utils::unsetenv("test-missing"); + utils::setenv("test-empty", ""); + utils::setenv("test-text", "some-value"); + + const std::map< std::string, std::string > allenv = utils::getallenv(); + + { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("test-missing"); + ATF_REQUIRE(iter == allenv.end()); + } + + { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("test-empty"); + ATF_REQUIRE(iter != allenv.end()); + ATF_REQUIRE((*iter).second.empty()); + } + + { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("test-text"); + ATF_REQUIRE(iter != allenv.end()); + ATF_REQUIRE_EQ("some-value", (*iter).second); + } + + if (utils::getenv("PATH")) { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("PATH"); + ATF_REQUIRE(iter != allenv.end()); + ATF_REQUIRE_EQ(utils::getenv("PATH").get(), (*iter).second); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(getenv); +ATF_TEST_CASE_BODY(getenv) +{ + const optional< std::string > path = utils::getenv("PATH"); + ATF_REQUIRE(path); + ATF_REQUIRE(!path.get().empty()); + + ATF_REQUIRE(!utils::getenv("__UNDEFINED_VARIABLE__")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(getenv_with_default); +ATF_TEST_CASE_BODY(getenv_with_default) +{ + ATF_REQUIRE("don't use" != + utils::getenv_with_default("PATH", "don't use")); + + ATF_REQUIRE_EQ("foo", + utils::getenv_with_default("__UNDEFINED_VARIABLE__", "foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_home__ok); +ATF_TEST_CASE_BODY(get_home__ok) +{ + const fs::path home("/foo/bar"); + utils::setenv("HOME", home.str()); + const optional< fs::path > computed = utils::get_home(); + ATF_REQUIRE(computed); + ATF_REQUIRE_EQ(home, computed.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_home__missing); +ATF_TEST_CASE_BODY(get_home__missing) +{ + utils::unsetenv("HOME"); + ATF_REQUIRE(!utils::get_home()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_home__invalid); +ATF_TEST_CASE_BODY(get_home__invalid) +{ + utils::setenv("HOME", ""); + ATF_REQUIRE(!utils::get_home()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(setenv); +ATF_TEST_CASE_BODY(setenv) +{ + ATF_REQUIRE(utils::getenv("PATH")); + const std::string oldval = utils::getenv("PATH").get(); + utils::setenv("PATH", "foo-bar"); + ATF_REQUIRE(utils::getenv("PATH").get() != oldval); + ATF_REQUIRE_EQ("foo-bar", utils::getenv("PATH").get()); + + ATF_REQUIRE(!utils::getenv("__UNDEFINED_VARIABLE__")); + utils::setenv("__UNDEFINED_VARIABLE__", "foo2-bar2"); + ATF_REQUIRE_EQ("foo2-bar2", utils::getenv("__UNDEFINED_VARIABLE__").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unsetenv); +ATF_TEST_CASE_BODY(unsetenv) +{ + ATF_REQUIRE(utils::getenv("PATH")); + utils::unsetenv("PATH"); + ATF_REQUIRE(!utils::getenv("PATH")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, getallenv); + + ATF_ADD_TEST_CASE(tcs, getenv); + + ATF_ADD_TEST_CASE(tcs, getenv_with_default); + + ATF_ADD_TEST_CASE(tcs, get_home__ok); + ATF_ADD_TEST_CASE(tcs, get_home__missing); + ATF_ADD_TEST_CASE(tcs, get_home__invalid); + + ATF_ADD_TEST_CASE(tcs, setenv); + + ATF_ADD_TEST_CASE(tcs, unsetenv); +} diff --git a/utils/format/Kyuafile b/utils/format/Kyuafile new file mode 100644 index 000000000000..344ae455422c --- /dev/null +++ b/utils/format/Kyuafile @@ -0,0 +1,7 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="containers_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="formatter_test"} diff --git a/utils/format/Makefile.am.inc b/utils/format/Makefile.am.inc new file mode 100644 index 000000000000..a37fc4057079 --- /dev/null +++ b/utils/format/Makefile.am.inc @@ -0,0 +1,59 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +libutils_a_SOURCES += utils/format/containers.hpp +libutils_a_SOURCES += utils/format/containers.ipp +libutils_a_SOURCES += utils/format/exceptions.cpp +libutils_a_SOURCES += utils/format/exceptions.hpp +libutils_a_SOURCES += utils/format/formatter.cpp +libutils_a_SOURCES += utils/format/formatter.hpp +libutils_a_SOURCES += utils/format/formatter_fwd.hpp +libutils_a_SOURCES += utils/format/formatter.ipp +libutils_a_SOURCES += utils/format/macros.hpp + +if WITH_ATF +tests_utils_formatdir = $(pkgtestsdir)/utils/format + +tests_utils_format_DATA = utils/format/Kyuafile +EXTRA_DIST += $(tests_utils_format_DATA) + +tests_utils_format_PROGRAMS = utils/format/containers_test +utils_format_containers_test_SOURCES = utils/format/containers_test.cpp +utils_format_containers_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_format_containers_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_format_PROGRAMS += utils/format/exceptions_test +utils_format_exceptions_test_SOURCES = utils/format/exceptions_test.cpp +utils_format_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_format_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_format_PROGRAMS += utils/format/formatter_test +utils_format_formatter_test_SOURCES = utils/format/formatter_test.cpp +utils_format_formatter_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_format_formatter_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/format/containers.hpp b/utils/format/containers.hpp new file mode 100644 index 000000000000..7334c250de4e --- /dev/null +++ b/utils/format/containers.hpp @@ -0,0 +1,66 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/format/containers.hpp +/// Overloads to support formatting various base container types. + +#if !defined(UTILS_FORMAT_CONTAINERS_HPP) +#define UTILS_FORMAT_CONTAINERS_HPP + +#include <map> +#include <memory> +#include <ostream> +#include <set> +#include <utility> +#include <vector> + + +// This is ugly but necessary for C++ name resolution. Unsure if we'd do it +// differently... +namespace std { + + +template< typename K, typename V > +std::ostream& operator<<(std::ostream&, const std::map< K, V >&); + +template< typename T1, typename T2 > +std::ostream& operator<<(std::ostream&, const std::pair< T1, T2 >&); + +template< typename T > +std::ostream& operator<<(std::ostream&, const std::shared_ptr< T >); + +template< typename T > +std::ostream& operator<<(std::ostream&, const std::set< T >&); + +template< typename T > +std::ostream& operator<<(std::ostream&, const std::vector< T >&); + + +} // namespace std + +#endif // !defined(UTILS_FORMAT_CONTAINERS_HPP) diff --git a/utils/format/containers.ipp b/utils/format/containers.ipp new file mode 100644 index 000000000000..11d8e2914149 --- /dev/null +++ b/utils/format/containers.ipp @@ -0,0 +1,138 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_FORMAT_CONTAINERS_IPP) +#define UTILS_FORMAT_CONTAINERS_IPP + +#include "utils/format/containers.hpp" + +#include <ostream> + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename K, typename V > +std::ostream& +std::operator<<(std::ostream& output, const std::map< K, V >& object) +{ + output << "map("; + typename std::map< K, V >::size_type counter = 0; + for (typename std::map< K, V >::const_iterator iter = object.begin(); + iter != object.end(); ++iter, ++counter) { + if (counter != 0) + output << ", "; + output << (*iter).first << "=" << (*iter).second; + } + output << ")"; + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T1, typename T2 > +std::ostream& +std::operator<<(std::ostream& output, const std::pair< T1, T2 >& object) +{ + output << "pair(" << object.first << ", " << object.second << ")"; + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T > +std::ostream& +std::operator<<(std::ostream& output, const std::shared_ptr< T > object) +{ + if (object.get() == NULL) { + output << "<NULL>"; + } else { + output << *object; + } + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T > +std::ostream& +std::operator<<(std::ostream& output, const std::set< T >& object) +{ + output << "set("; + typename std::set< T >::size_type counter = 0; + for (typename std::set< T >::const_iterator iter = object.begin(); + iter != object.end(); ++iter, ++counter) { + if (counter != 0) + output << ", "; + output << (*iter); + } + output << ")"; + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T > +std::ostream& +std::operator<<(std::ostream& output, const std::vector< T >& object) +{ + output << "["; + for (typename std::vector< T >::size_type i = 0; i < object.size(); ++i) { + if (i != 0) + output << ", "; + output << object[i]; + } + output << "]"; + return output; +} + + +#endif // !defined(UTILS_FORMAT_CONTAINERS_IPP) diff --git a/utils/format/containers_test.cpp b/utils/format/containers_test.cpp new file mode 100644 index 000000000000..e1c452da2df6 --- /dev/null +++ b/utils/format/containers_test.cpp @@ -0,0 +1,190 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/format/containers.ipp" + +#include <memory> +#include <ostream> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include <atf-c++.hpp> + + + +namespace { + + +/// Formats a value and compares it to an expected string. +/// +/// \tparam T The type of the value to format. +/// \param expected Expected formatted text. +/// \param actual The value to format. +/// +/// \post Fails the test case if the formatted actual value does not match +/// the provided expected string. +template< typename T > +static void +do_check(const char* expected, const T& actual) +{ + std::ostringstream str; + str << actual; + ATF_REQUIRE_EQ(expected, str.str()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(std_map__empty); +ATF_TEST_CASE_BODY(std_map__empty) +{ + do_check("map()", std::map< char, char >()); + do_check("map()", std::map< int, long >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_map__some); +ATF_TEST_CASE_BODY(std_map__some) +{ + { + std::map< char, int > v; + v['b'] = 123; + v['z'] = 321; + do_check("map(b=123, z=321)", v); + } + + { + std::map< int, std::string > v; + v[5] = "first"; + v[2] = "second"; + v[8] = "third"; + do_check("map(2=second, 5=first, 8=third)", v); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_pair); +ATF_TEST_CASE_BODY(std_pair) +{ + do_check("pair(5, b)", std::pair< int, char >(5, 'b')); + do_check("pair(foo bar, baz)", + std::pair< std::string, std::string >("foo bar", "baz")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_shared_ptr__null); +ATF_TEST_CASE_BODY(std_shared_ptr__null) +{ + do_check("<NULL>", std::shared_ptr< char >()); + do_check("<NULL>", std::shared_ptr< int >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_shared_ptr__not_null); +ATF_TEST_CASE_BODY(std_shared_ptr__not_null) +{ + do_check("f", std::shared_ptr< char >(new char('f'))); + do_check("8", std::shared_ptr< int >(new int(8))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_set__empty); +ATF_TEST_CASE_BODY(std_set__empty) +{ + do_check("set()", std::set< char >()); + do_check("set()", std::set< int >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_set__some); +ATF_TEST_CASE_BODY(std_set__some) +{ + { + std::set< char > v; + v.insert('b'); + v.insert('z'); + do_check("set(b, z)", v); + } + + { + std::set< int > v; + v.insert(5); + v.insert(2); + v.insert(8); + do_check("set(2, 5, 8)", v); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_vector__empty); +ATF_TEST_CASE_BODY(std_vector__empty) +{ + do_check("[]", std::vector< char >()); + do_check("[]", std::vector< int >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_vector__some); +ATF_TEST_CASE_BODY(std_vector__some) +{ + { + std::vector< char > v; + v.push_back('b'); + v.push_back('z'); + do_check("[b, z]", v); + } + + { + std::vector< int > v; + v.push_back(5); + v.push_back(2); + v.push_back(8); + do_check("[5, 2, 8]", v); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, std_map__empty); + ATF_ADD_TEST_CASE(tcs, std_map__some); + + ATF_ADD_TEST_CASE(tcs, std_pair); + + ATF_ADD_TEST_CASE(tcs, std_shared_ptr__null); + ATF_ADD_TEST_CASE(tcs, std_shared_ptr__not_null); + + ATF_ADD_TEST_CASE(tcs, std_set__empty); + ATF_ADD_TEST_CASE(tcs, std_set__some); + + ATF_ADD_TEST_CASE(tcs, std_vector__empty); + ATF_ADD_TEST_CASE(tcs, std_vector__some); +} diff --git a/utils/format/exceptions.cpp b/utils/format/exceptions.cpp new file mode 100644 index 000000000000..299b1d23cd8d --- /dev/null +++ b/utils/format/exceptions.cpp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/format/exceptions.hpp" + +using utils::format::bad_format_error; +using utils::format::error; +using utils::format::extra_args_error; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +error::~error(void) throw() +{ +} + + +/// Constructs a new bad_format_error. +/// +/// \param format_ The invalid format string. +/// \param message Description of the error in the format string. +bad_format_error::bad_format_error(const std::string& format_, + const std::string& message) : + error("Invalid formatting string '" + format_ + "': " + message), + _format(format_) +{ +} + + +/// Destructor for the error. +bad_format_error::~bad_format_error(void) throw() +{ +} + + +/// \return The format string that caused the error. +const std::string& +bad_format_error::format(void) const +{ + return _format; +} + + +/// Constructs a new extra_args_error. +/// +/// \param format_ The format string. +/// \param arg_ The first extra argument passed to the format string. +extra_args_error::extra_args_error(const std::string& format_, + const std::string& arg_) : + error("Not enough fields in formatting string '" + format_ + "' to place " + "argument '" + arg_ + "'"), + _format(format_), + _arg(arg_) +{ +} + + +/// Destructor for the error. +extra_args_error::~extra_args_error(void) throw() +{ +} + + +/// \return The format string that was passed too many arguments. +const std::string& +extra_args_error::format(void) const +{ + return _format; +} + + +/// \return The first argument that caused the error. +const std::string& +extra_args_error::arg(void) const +{ + return _arg; +} diff --git a/utils/format/exceptions.hpp b/utils/format/exceptions.hpp new file mode 100644 index 000000000000..a28376df9c08 --- /dev/null +++ b/utils/format/exceptions.hpp @@ -0,0 +1,84 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/format/exceptions.hpp +/// Exception types raised by the format module. + +#if !defined(UTILS_FORMAT_EXCEPTIONS_HPP) +#define UTILS_FORMAT_EXCEPTIONS_HPP + +#include <stdexcept> +#include <string> + +namespace utils { +namespace format { + + +/// Base exception for format errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error denoting a bad format string. +class bad_format_error : public error { + /// The format string that caused the error. + std::string _format; + +public: + explicit bad_format_error(const std::string&, const std::string&); + virtual ~bad_format_error(void) throw(); + + const std::string& format(void) const; +}; + + +/// Error denoting too many arguments for the format string. +class extra_args_error : public error { + /// The format string that was passed too many arguments. + std::string _format; + + /// The first argument that caused the error. + std::string _arg; + +public: + explicit extra_args_error(const std::string&, const std::string&); + virtual ~extra_args_error(void) throw(); + + const std::string& format(void) const; + const std::string& arg(void) const; +}; + + +} // namespace format +} // namespace utils + + +#endif // !defined(UTILS_FORMAT_EXCEPTIONS_HPP) diff --git a/utils/format/exceptions_test.cpp b/utils/format/exceptions_test.cpp new file mode 100644 index 000000000000..28d401e57dad --- /dev/null +++ b/utils/format/exceptions_test.cpp @@ -0,0 +1,74 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/format/exceptions.hpp" + +#include <cstring> + +#include <atf-c++.hpp> + +using utils::format::bad_format_error; +using utils::format::error; +using utils::format::extra_args_error; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bad_format_error); +ATF_TEST_CASE_BODY(bad_format_error) +{ + const bad_format_error e("format-string", "the-error"); + ATF_REQUIRE(std::strcmp("Invalid formatting string 'format-string': " + "the-error", e.what()) == 0); + ATF_REQUIRE_EQ("format-string", e.format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(extra_args_error); +ATF_TEST_CASE_BODY(extra_args_error) +{ + const extra_args_error e("fmt", "extra"); + ATF_REQUIRE(std::strcmp("Not enough fields in formatting string 'fmt' to " + "place argument 'extra'", e.what()) == 0); + ATF_REQUIRE_EQ("fmt", e.format()); + ATF_REQUIRE_EQ("extra", e.arg()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, bad_format_error); + ATF_ADD_TEST_CASE(tcs, extra_args_error); +} diff --git a/utils/format/formatter.cpp b/utils/format/formatter.cpp new file mode 100644 index 000000000000..99cfd40f03ab --- /dev/null +++ b/utils/format/formatter.cpp @@ -0,0 +1,293 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/format/formatter.hpp" + +#include <memory> +#include <string> +#include <utility> + +#include "utils/format/exceptions.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace format = utils::format; +namespace text = utils::text; + + +namespace { + + +/// Finds the next placeholder in a string. +/// +/// \param format The original format string provided by the user; needed for +/// error reporting purposes only. +/// \param expansion The string containing the placeholder to look for. Any +/// '%%' in the string will be skipped, and they must be stripped later by +/// strip_double_percent(). +/// \param begin The position from which to start looking for the next +/// placeholder. +/// +/// \return The position in the string in which the placeholder is located and +/// the placeholder itself. If there are no placeholders left, this returns +/// the length of the string and an empty string. +/// +/// \throw bad_format_error If the input string contains a trailing formatting +/// character. We cannot detect any other kind of invalid formatter because +/// we do not implement a full parser for them. +static std::pair< std::string::size_type, std::string > +find_next_placeholder(const std::string& format, + const std::string& expansion, + std::string::size_type begin) +{ + begin = expansion.find('%', begin); + while (begin != std::string::npos && expansion[begin + 1] == '%') + begin = expansion.find('%', begin + 2); + if (begin == std::string::npos) + return std::make_pair(expansion.length(), ""); + if (begin == expansion.length() - 1) + throw format::bad_format_error(format, "Trailing %"); + + std::string::size_type end = begin + 1; + while (end < expansion.length() && expansion[end] != 's') + end++; + const std::string placeholder = expansion.substr(begin, end - begin + 1); + if (end == expansion.length() || + placeholder.find('%', 1) != std::string::npos) + throw format::bad_format_error(format, "Unterminated placeholder '" + + placeholder + "'"); + return std::make_pair(begin, placeholder); +} + + +/// Converts a string to an integer. +/// +/// \param format The format string; for error reporting purposes only. +/// \param str The string to conver. +/// \param what The name of the field this integer belongs to; for error +/// reporting purposes only. +/// +/// \return An integer representing the input string. +inline int +to_int(const std::string& format, const std::string& str, const char* what) +{ + try { + return text::to_type< int >(str); + } catch (const text::value_error& e) { + throw format::bad_format_error(format, "Invalid " + std::string(what) + + "specifier"); + } +} + + +/// Constructs an std::ostringstream based on a formatting placeholder. +/// +/// \param format The format placeholder; may be empty. +/// +/// \return A new std::ostringstream that is prepared to format a single +/// object in the manner specified by the format placeholder. +/// +/// \throw bad_format_error If the format string is bad. We do minimal +/// validation on this string though. +static std::ostringstream* +new_ostringstream(const std::string& format) +{ + std::auto_ptr< std::ostringstream > output(new std::ostringstream()); + + if (format.length() <= 2) { + // If the format is empty, we create a new stream so that we don't have + // to check for NULLs later on. We rarely should hit this condition + // (and when we do it's a bug in the caller), so this is not a big deal. + // + // Otherwise, if the format is a regular '%s', then we don't have to do + // any processing for additional formatters. So this is just a "fast + // path". + } else { + std::string partial = format.substr(1, format.length() - 2); + if (partial[0] == '0') { + output->fill('0'); + partial.erase(0, 1); + } + if (!partial.empty()) { + const std::string::size_type dot = partial.find('.'); + if (dot != 0) + output->width(to_int(format, partial.substr(0, dot), "width")); + if (dot != std::string::npos) { + output->setf(std::ios::fixed, std::ios::floatfield); + output->precision(to_int(format, partial.substr(dot + 1), + "precision")); + } + } + } + + return output.release(); +} + + +/// Replaces '%%' by '%' in a given string range. +/// +/// \param in The input string to be rewritten. +/// \param begin The position at which to start the replacement. +/// \param end The position at which to end the replacement. +/// +/// \return The modified string and the amount of characters removed. +static std::pair< std::string, int > +strip_double_percent(const std::string& in, const std::string::size_type begin, + std::string::size_type end) +{ + std::string part = in.substr(begin, end - begin); + + int removed = 0; + std::string::size_type pos = part.find("%%"); + while (pos != std::string::npos) { + part.erase(pos, 1); + ++removed; + pos = part.find("%%", pos + 1); + } + + return std::make_pair(in.substr(0, begin) + part + in.substr(end), removed); +} + + +} // anonymous namespace + + +/// Performs internal initialization of the formatter. +/// +/// This is separate from the constructor just because it is shared by different +/// overloaded constructors. +void +format::formatter::init(void) +{ + const std::pair< std::string::size_type, std::string > placeholder = + find_next_placeholder(_format, _expansion, _last_pos); + const std::pair< std::string, int > no_percents = + strip_double_percent(_expansion, _last_pos, placeholder.first); + + _oss = new_ostringstream(placeholder.second); + + _expansion = no_percents.first; + _placeholder_pos = placeholder.first - no_percents.second; + _placeholder = placeholder.second; +} + + +/// Constructs a new formatter object (internal). +/// +/// \param format The format string. +/// \param expansion The format string with any replacements performed so far. +/// \param last_pos The position from which to start looking for formatting +/// placeholders. This must be maintained in case one of the replacements +/// introduced a new placeholder, which must be ignored. Think, for +/// example, replacing a "%s" string with "foo %s". +format::formatter::formatter(const std::string& format, + const std::string& expansion, + const std::string::size_type last_pos) : + _format(format), + _expansion(expansion), + _last_pos(last_pos), + _oss(NULL) +{ + init(); +} + + +/// Constructs a new formatter object. +/// +/// \param format The format string. The formatters in the string are not +/// validated during construction, but will cause errors when used later if +/// they are invalid. +format::formatter::formatter(const std::string& format) : + _format(format), + _expansion(format), + _last_pos(0), + _oss(NULL) +{ + init(); +} + + +format::formatter::~formatter(void) +{ + delete _oss; +} + + +/// Returns the formatted string. +/// +/// \return A string representation of the formatted string. +const std::string& +format::formatter::str(void) const +{ + return _expansion; +} + + +/// Automatic conversion of formatter objects to strings. +/// +/// This is provided to allow painless injection of formatter objects into +/// streams, without having to manually call the str() method. +format::formatter::operator const std::string&(void) const +{ + return _expansion; +} + + +/// Specialization of operator% for booleans. +/// +/// \param value The boolean to inject into the format string. +/// +/// \return A new formatter that has one less format placeholder. +format::formatter +format::formatter::operator%(const bool& value) const +{ + (*_oss) << (value ? "true" : "false"); + return replace(_oss->str()); +} + + +/// Replaces the first formatting placeholder with a value. +/// +/// \param arg The replacement string. +/// +/// \return A new formatter in which the first formatting placeholder has been +/// replaced by arg and is ready to replace the next item. +/// +/// \throw utils::format::extra_args_error If there are no more formatting +/// placeholders in the input string, or if the placeholder is invalid. +format::formatter +format::formatter::replace(const std::string& arg) const +{ + if (_placeholder_pos == _expansion.length()) + throw format::extra_args_error(_format, arg); + + const std::string expansion = _expansion.substr(0, _placeholder_pos) + + arg + _expansion.substr(_placeholder_pos + _placeholder.length()); + return formatter(_format, expansion, _placeholder_pos + arg.length()); +} diff --git a/utils/format/formatter.hpp b/utils/format/formatter.hpp new file mode 100644 index 000000000000..8c6188745a2e --- /dev/null +++ b/utils/format/formatter.hpp @@ -0,0 +1,123 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/format/formatter.hpp +/// Provides the definition of the utils::format::formatter class. +/// +/// The utils::format::formatter class is a poor man's replacement for the +/// Boost.Format library, as it is much simpler and has less dependencies. +/// +/// Be aware that the formatting supported by this module is NOT compatible +/// with printf(3) nor with Boost.Format. The general syntax for a +/// placeholder in a formatting string is: +/// +/// %[0][width][.precision]s +/// +/// In particular, note that the only valid formatting specifier is %s: the +/// library deduces what to print based on the type of the variable passed +/// in, not based on what the format string says. Also, note that the only +/// valid padding character is 0. + +#if !defined(UTILS_FORMAT_FORMATTER_HPP) +#define UTILS_FORMAT_FORMATTER_HPP + +#include "utils/format/formatter_fwd.hpp" + +#include <sstream> +#include <string> + +namespace utils { +namespace format { + + +/// Mechanism to format strings similar to printf. +/// +/// A formatter always maintains the original format string but also holds a +/// partial expansion. The partial expansion is immutable in the context of a +/// formatter instance, but calls to operator% return new formatter objects with +/// one less formatting placeholder. +/// +/// In general, one can format a string in the following manner: +/// +/// \code +/// const std::string s = (formatter("%s %s") % "foo" % 5).str(); +/// \endcode +/// +/// which, following the explanation above, would correspond to: +/// +/// \code +/// const formatter f1("%s %s"); +/// const formatter f2 = f1 % "foo"; +/// const formatter f3 = f2 % 5; +/// const std::string s = f3.str(); +/// \endcode +class formatter { + /// The original format string provided by the user. + std::string _format; + + /// The current "expansion" of the format string. + /// + /// This field gets updated on every call to operator%() to have one less + /// formatting placeholder. + std::string _expansion; + + /// The position of _expansion from which to scan for placeholders. + std::string::size_type _last_pos; + + /// The position of the first placeholder in the current expansion. + std::string::size_type _placeholder_pos; + + /// The first placeholder in the current expansion. + std::string _placeholder; + + /// Stream used to format any possible argument supplied by operator%(). + std::ostringstream* _oss; + + formatter replace(const std::string&) const; + + void init(void); + formatter(const std::string&, const std::string&, + const std::string::size_type); + +public: + explicit formatter(const std::string&); + ~formatter(void); + + const std::string& str(void) const; + operator const std::string&(void) const; + + template< typename Type > formatter operator%(const Type&) const; + formatter operator%(const bool&) const; +}; + + +} // namespace format +} // namespace utils + + +#endif // !defined(UTILS_FORMAT_FORMATTER_HPP) diff --git a/utils/format/formatter.ipp b/utils/format/formatter.ipp new file mode 100644 index 000000000000..6fad024b704f --- /dev/null +++ b/utils/format/formatter.ipp @@ -0,0 +1,76 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_FORMAT_FORMATTER_IPP) +#define UTILS_FORMAT_FORMATTER_IPP + +#include <ostream> + +#include "utils/format/formatter.hpp" + +namespace utils { +namespace format { + + +/// Replaces the first format placeholder in a formatter. +/// +/// Constructs a new formatter object that has one less formatting placeholder, +/// as this has been replaced by the provided argument. Calling this operator +/// N times, where N is the number of formatting placeholders, effectively +/// formats the string. +/// +/// \param arg The argument to use as replacement for the format placeholder. +/// +/// \return A new formatter that has one less format placeholder. +template< typename Type > +inline formatter +formatter::operator%(const Type& arg) const +{ + (*_oss) << arg; + return replace(_oss->str()); +} + + +/// Inserts a formatter string into a stream. +/// +/// \param os The output stream. +/// \param f The formatter to process and inject into the stream. +/// +/// \return The output stream os. +inline std::ostream& +operator<<(std::ostream& os, const formatter& f) +{ + return (os << f.str()); +} + + +} // namespace format +} // namespace utils + + +#endif // !defined(UTILS_FORMAT_FORMATTER_IPP) diff --git a/utils/format/formatter_fwd.hpp b/utils/format/formatter_fwd.hpp new file mode 100644 index 000000000000..72c9e5ebf196 --- /dev/null +++ b/utils/format/formatter_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/format/formatter_fwd.hpp +/// Forward declarations for utils/format/formatter.hpp + +#if !defined(UTILS_FORMAT_FORMATTER_FWD_HPP) +#define UTILS_FORMAT_FORMATTER_FWD_HPP + +namespace utils { +namespace format { + + +class formatter; + + +} // namespace format +} // namespace utils + +#endif // !defined(UTILS_FORMAT_FORMATTER_FWD_HPP) diff --git a/utils/format/formatter_test.cpp b/utils/format/formatter_test.cpp new file mode 100644 index 000000000000..fdae785b1db7 --- /dev/null +++ b/utils/format/formatter_test.cpp @@ -0,0 +1,265 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/format/formatter.hpp" + +#include <ostream> + +#include <atf-c++.hpp> + +#include "utils/format/exceptions.hpp" +#include "utils/format/macros.hpp" + +namespace format = utils::format; + + +namespace { + + +/// Wraps an integer in a C++ class. +/// +/// This custom type exists to ensure that we can feed arbitrary objects that +/// support operator<< to the formatter; +class int_wrapper { + /// The wrapped integer. + int _value; + +public: + /// Constructs a new wrapper. + /// + /// \param value_ The value to wrap. + int_wrapper(const int value_) : _value(value_) + { + } + + /// Returns the wrapped value. + /// + /// \return An integer. + int + value(void) const + { + return _value; + } +}; + + +/// Writes a wrapped integer into an output stream. +/// +/// \param output The output stream into which to place the integer. +/// \param wrapper The wrapped integer. +/// +/// \return The output stream. +std::ostream& +operator<<(std::ostream& output, const int_wrapper& wrapper) +{ + return (output << wrapper.value()); +} + + +} // anonymous namespace + + +/// Calls ATF_REQUIRE_EQ on an expected string and a formatter. +/// +/// This is pure syntactic sugar to avoid calling the str() method on all the +/// individual tests below, which results in very long lines that require +/// wrapping and clutter readability. +/// +/// \param expected The expected string generated by the formatter. +/// \param formatter The formatter to test. +#define EQ(expected, formatter) ATF_REQUIRE_EQ(expected, (formatter).str()) + + +ATF_TEST_CASE_WITHOUT_HEAD(no_fields); +ATF_TEST_CASE_BODY(no_fields) +{ + EQ("Plain string", F("Plain string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(one_field); +ATF_TEST_CASE_BODY(one_field) +{ + EQ("foo", F("%sfoo") % ""); + EQ(" foo", F("%sfoo") % " "); + EQ("foo ", F("foo %s") % ""); + EQ("foo bar", F("foo %s") % "bar"); + EQ("foo bar baz", F("foo %s baz") % "bar"); + EQ("foo %s %s", F("foo %s %s") % "%s" % "%s"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(many_fields); +ATF_TEST_CASE_BODY(many_fields) +{ + EQ("", F("%s%s") % "" % ""); + EQ("foo", F("%s%s%s") % "" % "foo" % ""); + EQ("some 5 text", F("%s %s %s") % "some" % 5 % "text"); + EQ("f%s 5 text", F("%s %s %s") % "f%s" % 5 % "text"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(escape); +ATF_TEST_CASE_BODY(escape) +{ + EQ("%", F("%%")); + EQ("% %", F("%% %%")); + EQ("%% %%", F("%%%% %%%%")); + + EQ("foo %", F("foo %%")); + EQ("foo bar %", F("foo %s %%") % "bar"); + EQ("foo % bar", F("foo %% %s") % "bar"); + + EQ("foo %%", F("foo %s") % "%%"); + EQ("foo a%%b", F("foo a%sb") % "%%"); + EQ("foo a%%b", F("foo %s") % "a%%b"); + + EQ("foo % bar %%", F("foo %% %s %%%%") % "bar"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(extra_args_error); +ATF_TEST_CASE_BODY(extra_args_error) +{ + using format::extra_args_error; + + ATF_REQUIRE_THROW(extra_args_error, F("foo") % "bar"); + ATF_REQUIRE_THROW(extra_args_error, F("foo %%") % "bar"); + ATF_REQUIRE_THROW(extra_args_error, F("foo %s") % "bar" % "baz"); + ATF_REQUIRE_THROW(extra_args_error, F("foo %s") % "%s" % "bar"); + ATF_REQUIRE_THROW(extra_args_error, F("%s foo %s") % "bar" % "baz" % "foo"); + + try { + F("foo %s %s") % "bar" % "baz" % "something extra"; + fail("extra_args_error not raised"); + } catch (const extra_args_error& e) { + ATF_REQUIRE_EQ("foo %s %s", e.format()); + ATF_REQUIRE_EQ("something extra", e.arg()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__class); +ATF_TEST_CASE_BODY(format__class) +{ + EQ("foo bar", F("%s") % std::string("foo bar")); + EQ("3", F("%s") % int_wrapper(3)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__pointer); +ATF_TEST_CASE_BODY(format__pointer) +{ + EQ("0xcafebabe", F("%s") % reinterpret_cast< void* >(0xcafebabe)); + EQ("foo bar", F("%s") % "foo bar"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__bool); +ATF_TEST_CASE_BODY(format__bool) +{ + EQ("true", F("%s") % true); + EQ("false", F("%s") % false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__char); +ATF_TEST_CASE_BODY(format__char) +{ + EQ("Z", F("%s") % 'Z'); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__float); +ATF_TEST_CASE_BODY(format__float) +{ + EQ("3", F("%s") % 3.0); + EQ("3.0", F("%.1s") % 3.0); + EQ("3.0", F("%0.1s") % 3.0); + EQ(" 15.600", F("%8.3s") % 15.6); + EQ("01.52", F("%05.2s") % 1.52); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__int); +ATF_TEST_CASE_BODY(format__int) +{ + EQ("3", F("%s") % 3); + EQ("3", F("%0s") % 3); + EQ(" -123", F("%5s") % -123); + EQ("00078", F("%05s") % 78); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__error); +ATF_TEST_CASE_BODY(format__error) +{ + using format::bad_format_error; + + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("%")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("f%")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("f%%%")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("ab %s cd%") % "cd"); + + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid width", F("%1bs")); + + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%.s")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%0.s")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%123.s")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%.12bs")); + + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%c") % 'Z'); + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%d") % 5); + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%.1f") % 3); + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%d%s") % 3 % "a"); + + try { + F("foo %s%") % "bar"; + fail("bad_format_error not raised"); + } catch (const bad_format_error& e) { + ATF_REQUIRE_EQ("foo %s%", e.format()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, no_fields); + ATF_ADD_TEST_CASE(tcs, one_field); + ATF_ADD_TEST_CASE(tcs, many_fields); + ATF_ADD_TEST_CASE(tcs, escape); + ATF_ADD_TEST_CASE(tcs, extra_args_error); + + ATF_ADD_TEST_CASE(tcs, format__class); + ATF_ADD_TEST_CASE(tcs, format__pointer); + ATF_ADD_TEST_CASE(tcs, format__bool); + ATF_ADD_TEST_CASE(tcs, format__char); + ATF_ADD_TEST_CASE(tcs, format__float); + ATF_ADD_TEST_CASE(tcs, format__int); + ATF_ADD_TEST_CASE(tcs, format__error); +} diff --git a/utils/format/macros.hpp b/utils/format/macros.hpp new file mode 100644 index 000000000000..09ef14ea485e --- /dev/null +++ b/utils/format/macros.hpp @@ -0,0 +1,58 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/format/macros.hpp +/// Convenience macros to simplify usage of the format library. +/// +/// This file <em>must not be included from other header files</em>. + +#if !defined(UTILS_FORMAT_MACROS_HPP) +#define UTILS_FORMAT_MACROS_HPP + +// We include the .ipp file instead of .hpp because, after all, macros.hpp +// is provided purely for convenience and must not be included from other +// header files. Henceforth, we make things easier to the callers. +#include "utils/format/formatter.ipp" + + +/// Constructs a utils::format::formatter object with the given format string. +/// +/// This macro is just a wrapper to make the construction of +/// utils::format::formatter objects shorter, and thus to allow inlining these +/// calls right in where formatted strings are required. A typical usage would +/// look like: +/// +/// \code +/// std::cout << F("%s %d\n") % my_str % my_int; +/// \endcode +/// +/// \param fmt The format string. +#define F(fmt) utils::format::formatter(fmt) + + +#endif // !defined(UTILS_FORMAT_MACROS_HPP) diff --git a/utils/fs/Kyuafile b/utils/fs/Kyuafile new file mode 100644 index 000000000000..66cb918fca92 --- /dev/null +++ b/utils/fs/Kyuafile @@ -0,0 +1,10 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="auto_cleaners_test"} +atf_test_program{name="directory_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="lua_module_test"} +atf_test_program{name="operations_test"} +atf_test_program{name="path_test"} diff --git a/utils/fs/Makefile.am.inc b/utils/fs/Makefile.am.inc new file mode 100644 index 000000000000..2acdadafa79b --- /dev/null +++ b/utils/fs/Makefile.am.inc @@ -0,0 +1,84 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +UTILS_CFLAGS += $(LUTOK_CFLAGS) +UTILS_LIBS += $(LUTOK_LIBS) + +libutils_a_CPPFLAGS += $(LUTOK_CFLAGS) +libutils_a_SOURCES += utils/fs/auto_cleaners.cpp +libutils_a_SOURCES += utils/fs/auto_cleaners.hpp +libutils_a_SOURCES += utils/fs/auto_cleaners_fwd.hpp +libutils_a_SOURCES += utils/fs/directory.cpp +libutils_a_SOURCES += utils/fs/directory.hpp +libutils_a_SOURCES += utils/fs/directory_fwd.hpp +libutils_a_SOURCES += utils/fs/exceptions.cpp +libutils_a_SOURCES += utils/fs/exceptions.hpp +libutils_a_SOURCES += utils/fs/lua_module.cpp +libutils_a_SOURCES += utils/fs/lua_module.hpp +libutils_a_SOURCES += utils/fs/operations.cpp +libutils_a_SOURCES += utils/fs/operations.hpp +libutils_a_SOURCES += utils/fs/path.cpp +libutils_a_SOURCES += utils/fs/path.hpp +libutils_a_SOURCES += utils/fs/path_fwd.hpp + +if WITH_ATF +tests_utils_fsdir = $(pkgtestsdir)/utils/fs + +tests_utils_fs_DATA = utils/fs/Kyuafile +EXTRA_DIST += $(tests_utils_fs_DATA) + +tests_utils_fs_PROGRAMS = utils/fs/auto_cleaners_test +utils_fs_auto_cleaners_test_SOURCES = utils/fs/auto_cleaners_test.cpp +utils_fs_auto_cleaners_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_auto_cleaners_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/directory_test +utils_fs_directory_test_SOURCES = utils/fs/directory_test.cpp +utils_fs_directory_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_directory_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/exceptions_test +utils_fs_exceptions_test_SOURCES = utils/fs/exceptions_test.cpp +utils_fs_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/lua_module_test +utils_fs_lua_module_test_SOURCES = utils/fs/lua_module_test.cpp +utils_fs_lua_module_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_lua_module_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/operations_test +utils_fs_operations_test_SOURCES = utils/fs/operations_test.cpp +utils_fs_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/path_test +utils_fs_path_test_SOURCES = utils/fs/path_test.cpp +utils_fs_path_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_path_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/fs/auto_cleaners.cpp b/utils/fs/auto_cleaners.cpp new file mode 100644 index 000000000000..94ef94465e57 --- /dev/null +++ b/utils/fs/auto_cleaners.cpp @@ -0,0 +1,261 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/auto_cleaners.hpp" + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" + +namespace fs = utils::fs; +namespace signals = utils::signals; + + +/// Shared implementation of the auto_directory. +struct utils::fs::auto_directory::impl : utils::noncopyable { + /// The path to the directory being managed. + fs::path _directory; + + /// Whether cleanup() has been already executed or not. + bool _cleaned; + + /// Constructor. + /// + /// \param directory_ The directory to grab the ownership of. + impl(const path& directory_) : + _directory(directory_), + _cleaned(false) + { + } + + /// Destructor. + ~impl(void) + { + try { + this->cleanup(); + } catch (const fs::error& e) { + LW(F("Failed to auto-cleanup directory '%s': %s") % _directory % + e.what()); + } + } + + /// Removes the directory. + /// + /// See the cleanup() method of the auto_directory class for details. + void + cleanup(void) + { + if (!_cleaned) { + // Mark this as cleaned first so that, in case of failure, we don't + // reraise the error from the destructor. + _cleaned = true; + + fs::rmdir(_directory); + } + } +}; + + +/// Constructs a new auto_directory and grabs ownership of a directory. +/// +/// \param directory_ The directory to grab the ownership of. +fs::auto_directory::auto_directory(const path& directory_) : + _pimpl(new impl(directory_)) +{ +} + + +/// Deletes the managed directory; must be empty. +/// +/// This should not be relied on because it cannot provide proper error +/// reporting. Instead, the caller should use the cleanup() method. +fs::auto_directory::~auto_directory(void) +{ +} + + +/// Creates a self-destructing temporary directory. +/// +/// See the notes for fs::mkdtemp_public() for details on the permissions +/// given to the temporary directory, which are looser than what the standard +/// mkdtemp would grant. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The self-destructing directory. +/// +/// \throw fs::error If the creation fails. +fs::auto_directory +fs::auto_directory::mkdtemp_public(const std::string& path_template) +{ + signals::interrupts_inhibiter inhibiter; + const fs::path directory_ = fs::mkdtemp_public(path_template); + try { + return auto_directory(directory_); + } catch (...) { + fs::rmdir(directory_); + throw; + } +} + + +/// Gets the directory managed by this auto_directory. +/// +/// \return The path to the managed directory. +const fs::path& +fs::auto_directory::directory(void) const +{ + return _pimpl->_directory; +} + + +/// Deletes the managed directory; must be empty. +/// +/// This operation is idempotent. +/// +/// \throw fs::error If there is a problem removing any directory or file. +void +fs::auto_directory::cleanup(void) +{ + _pimpl->cleanup(); +} + + +/// Shared implementation of the auto_file. +struct utils::fs::auto_file::impl : utils::noncopyable { + /// The path to the file being managed. + fs::path _file; + + /// Whether removed() has been already executed or not. + bool _removed; + + /// Constructor. + /// + /// \param file_ The file to grab the ownership of. + impl(const path& file_) : + _file(file_), + _removed(false) + { + } + + /// Destructor. + ~impl(void) + { + try { + this->remove(); + } catch (const fs::error& e) { + LW(F("Failed to auto-cleanup file '%s': %s") % _file % + e.what()); + } + } + + /// Removes the file. + /// + /// See the remove() method of the auto_file class for details. + void + remove(void) + { + if (!_removed) { + // Mark this as cleaned first so that, in case of failure, we don't + // reraise the error from the destructor. + _removed = true; + + fs::unlink(_file); + } + } +}; + + +/// Constructs a new auto_file and grabs ownership of a file. +/// +/// \param file_ The file to grab the ownership of. +fs::auto_file::auto_file(const path& file_) : + _pimpl(new impl(file_)) +{ +} + + +/// Deletes the managed file. +/// +/// This should not be relied on because it cannot provide proper error +/// reporting. Instead, the caller should use the remove() method. +fs::auto_file::~auto_file(void) +{ +} + + +/// Creates a self-destructing temporary file. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The self-destructing file. +/// +/// \throw fs::error If the creation fails. +fs::auto_file +fs::auto_file::mkstemp(const std::string& path_template) +{ + signals::interrupts_inhibiter inhibiter; + const fs::path file_ = fs::mkstemp(path_template); + try { + return auto_file(file_); + } catch (...) { + fs::unlink(file_); + throw; + } +} + + +/// Gets the file managed by this auto_file. +/// +/// \return The path to the managed file. +const fs::path& +fs::auto_file::file(void) const +{ + return _pimpl->_file; +} + + +/// Deletes the managed file. +/// +/// This operation is idempotent. +/// +/// \throw fs::error If there is a problem removing the file. +void +fs::auto_file::remove(void) +{ + _pimpl->remove(); +} diff --git a/utils/fs/auto_cleaners.hpp b/utils/fs/auto_cleaners.hpp new file mode 100644 index 000000000000..f3e6937e3cea --- /dev/null +++ b/utils/fs/auto_cleaners.hpp @@ -0,0 +1,89 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/auto_cleaners.hpp +/// RAII wrappers to automatically remove file system entries. + +#if !defined(UTILS_FS_AUTO_CLEANERS_HPP) +#define UTILS_FS_AUTO_CLEANERS_HPP + +#include "utils/fs/auto_cleaners_fwd.hpp" + +#include <memory> +#include <string> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace fs { + + +/// Grabs ownership of a directory and removes it upon destruction. +/// +/// This class is reference-counted and therefore only the destruction of the +/// last instance will cause the removal of the directory. +class auto_directory { + struct impl; + /// Reference-counted, shared implementation. + std::shared_ptr< impl > _pimpl; + +public: + explicit auto_directory(const path&); + ~auto_directory(void); + + static auto_directory mkdtemp_public(const std::string&); + + const path& directory(void) const; + void cleanup(void); +}; + + +/// Grabs ownership of a file and removes it upon destruction. +/// +/// This class is reference-counted and therefore only the destruction of the +/// last instance will cause the removal of the file. +class auto_file { + struct impl; + /// Reference-counted, shared implementation. + std::shared_ptr< impl > _pimpl; + +public: + explicit auto_file(const path&); + ~auto_file(void); + + static auto_file mkstemp(const std::string&); + + const path& file(void) const; + void remove(void); +}; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_AUTO_CLEANERS_HPP) diff --git a/utils/fs/auto_cleaners_fwd.hpp b/utils/fs/auto_cleaners_fwd.hpp new file mode 100644 index 000000000000..c0cfa6333a1a --- /dev/null +++ b/utils/fs/auto_cleaners_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/auto_cleaners_fwd.hpp +/// Forward declarations for utils/fs/auto_cleaners.hpp + +#if !defined(UTILS_FS_AUTO_CLEANERS_FWD_HPP) +#define UTILS_FS_AUTO_CLEANERS_FWD_HPP + +namespace utils { +namespace fs { + + +class auto_directory; +class auto_file; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_AUTO_CLEANERS_FWD_HPP) diff --git a/utils/fs/auto_cleaners_test.cpp b/utils/fs/auto_cleaners_test.cpp new file mode 100644 index 000000000000..da4bbeb2da68 --- /dev/null +++ b/utils/fs/auto_cleaners_test.cpp @@ -0,0 +1,167 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/auto_cleaners.hpp" + +extern "C" { +#include <unistd.h> +} + +#include <atf-c++.hpp> + +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__automatic); +ATF_TEST_CASE_BODY(auto_directory__automatic) +{ + const fs::path root("root"); + fs::mkdir(root, 0755); + + { + fs::auto_directory dir(root); + ATF_REQUIRE_EQ(root, dir.directory()); + + ATF_REQUIRE(::access("root", X_OK) == 0); + + { + fs::auto_directory dir_copy(dir); + } + // Should still exist after a copy is destructed. + ATF_REQUIRE(::access("root", X_OK) == 0); + } + ATF_REQUIRE(::access("root", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__explicit); +ATF_TEST_CASE_BODY(auto_directory__explicit) +{ + const fs::path root("root"); + fs::mkdir(root, 0755); + + fs::auto_directory dir(root); + ATF_REQUIRE_EQ(root, dir.directory()); + + ATF_REQUIRE(::access("root", X_OK) == 0); + dir.cleanup(); + dir.cleanup(); + ATF_REQUIRE(::access("root", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__mkdtemp_public); +ATF_TEST_CASE_BODY(auto_directory__mkdtemp_public) +{ + utils::setenv("TMPDIR", (fs::current_path() / "tmp").str()); + fs::mkdir(fs::path("tmp"), 0755); + + const std::string path_template("test.XXXXXX"); + { + fs::auto_directory auto_directory = fs::auto_directory::mkdtemp_public( + path_template); + ATF_REQUIRE(::access((fs::path("tmp") / path_template).c_str(), + X_OK) == -1); + ATF_REQUIRE(::rmdir("tmp") == -1); + + ATF_REQUIRE(::access(auto_directory.directory().c_str(), X_OK) == 0); + } + ATF_REQUIRE(::rmdir("tmp") != -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__automatic); +ATF_TEST_CASE_BODY(auto_file__automatic) +{ + const fs::path file("foo"); + atf::utils::create_file(file.str(), ""); + { + fs::auto_file auto_file(file); + ATF_REQUIRE_EQ(file, auto_file.file()); + + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + + { + fs::auto_file auto_file_copy(auto_file); + } + // Should still exist after a copy is destructed. + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + } + ATF_REQUIRE(::access(file.c_str(), R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__explicit); +ATF_TEST_CASE_BODY(auto_file__explicit) +{ + const fs::path file("bar"); + atf::utils::create_file(file.str(), ""); + + fs::auto_file auto_file(file); + ATF_REQUIRE_EQ(file, auto_file.file()); + + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + auto_file.remove(); + auto_file.remove(); + ATF_REQUIRE(::access(file.c_str(), R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__mkstemp); +ATF_TEST_CASE_BODY(auto_file__mkstemp) +{ + utils::setenv("TMPDIR", (fs::current_path() / "tmp").str()); + fs::mkdir(fs::path("tmp"), 0755); + + const std::string path_template("test.XXXXXX"); + { + fs::auto_file auto_file = fs::auto_file::mkstemp(path_template); + ATF_REQUIRE(::access((fs::path("tmp") / path_template).c_str(), + X_OK) == -1); + ATF_REQUIRE(::rmdir("tmp") == -1); + + ATF_REQUIRE(::access(auto_file.file().c_str(), R_OK) == 0); + } + ATF_REQUIRE(::rmdir("tmp") != -1); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, auto_directory__automatic); + ATF_ADD_TEST_CASE(tcs, auto_directory__explicit); + ATF_ADD_TEST_CASE(tcs, auto_directory__mkdtemp_public); + + ATF_ADD_TEST_CASE(tcs, auto_file__automatic); + ATF_ADD_TEST_CASE(tcs, auto_file__explicit); + ATF_ADD_TEST_CASE(tcs, auto_file__mkstemp); +} diff --git a/utils/fs/directory.cpp b/utils/fs/directory.cpp new file mode 100644 index 000000000000..ff7ad5e34357 --- /dev/null +++ b/utils/fs/directory.cpp @@ -0,0 +1,360 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/directory.hpp" + +extern "C" { +#include <sys/types.h> + +#include <dirent.h> +} + +#include <cerrno> +#include <memory> + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace detail = utils::fs::detail; +namespace fs = utils::fs; +namespace text = utils::text; + + +/// Constructs a new directory entry. +/// +/// \param name_ Name of the directory entry. +fs::directory_entry::directory_entry(const std::string& name_) : name(name_) +{ +} + + +/// Checks if two directory entries are equal. +/// +/// \param other The entry to compare to. +/// +/// \return True if the two entries are equal; false otherwise. +bool +fs::directory_entry::operator==(const directory_entry& other) const +{ + return name == other.name; +} + + +/// Checks if two directory entries are different. +/// +/// \param other The entry to compare to. +/// +/// \return True if the two entries are different; false otherwise. +bool +fs::directory_entry::operator!=(const directory_entry& other) const +{ + return !(*this == other); +} + + +/// Checks if this entry sorts before another entry. +/// +/// \param other The entry to compare to. +/// +/// \return True if this entry sorts before the other entry; false otherwise. +bool +fs::directory_entry::operator<(const directory_entry& other) const +{ + return name < other.name; +} + + +/// Formats a directory entry. +/// +/// \param output Stream into which to inject the formatted entry. +/// \param entry The entry to format. +/// +/// \return A reference to output. +std::ostream& +fs::operator<<(std::ostream& output, const directory_entry& entry) +{ + output << F("directory_entry{name=%s}") % text::quote(entry.name, '\''); + return output; +} + + +/// Internal implementation details for the directory_iterator. +/// +/// In order to support multiple concurrent iterators over the same directory +/// object, this class is the one that performs all directory-level accesses. +/// In particular, even if it may seem surprising, this is the class that +/// handles the DIR object for the directory. +/// +/// Note that iterators implemented by this class do not rely on the container +/// directory class at all. This should not be relied on for object lifecycle +/// purposes. +struct utils::fs::detail::directory_iterator::impl : utils::noncopyable { + /// Path of the directory accessed by this iterator. + const fs::path _path; + + /// Raw pointer to the system representation of the directory. + /// + /// We also use this to determine if the iterator is valid (at the end) or + /// not. A null pointer means an invalid iterator. + ::DIR* _dirp; + + /// Raw representation of the system directory entry. + /// + /// We need to keep this at the class level so that we can use the + /// readdir_r(3) function. + ::dirent _dirent; + + /// Custom representation of the directory entry. + /// + /// This is separate from _dirent because this is the type we return to the + /// user. We must keep this as a pointer so that we can support the common + /// operators (* and ->) over iterators. + std::auto_ptr< directory_entry > _entry; + + /// Constructs an iterator pointing to the "end" of the directory. + impl(void) : _path("invalid-directory-entry"), _dirp(NULL) + { + } + + /// Constructs a new iterator to start scanning a directory. + /// + /// \param path The directory that will be scanned. + /// + /// \throw system_error If there is a problem opening the directory. + explicit impl(const path& path) : _path(path) + { + DIR* dirp = ::opendir(_path.c_str()); + if (dirp == NULL) { + const int original_errno = errno; + throw fs::system_error(F("opendir(%s) failed") % _path, + original_errno); + } + _dirp = dirp; + + // Initialize our first directory entry. Note that this may actually + // close the directory we just opened if the directory happens to be + // empty -- but directories are never empty because they at least have + // '.' and '..' entries. + next(); + } + + /// Destructor. + /// + /// This closes the directory if still open. + ~impl(void) + { + if (_dirp != NULL) + close(); + } + + /// Closes the directory and invalidates the iterator. + void + close(void) + { + PRE(_dirp != NULL); + if (::closedir(_dirp) == -1) { + UNREACHABLE_MSG("Invalid dirp provided to closedir(3)"); + } + _dirp = NULL; + } + + /// Advances the directory entry to the next one. + /// + /// It is possible to use this function on a new directory_entry object to + /// initialize the first entry. + /// + /// \throw system_error If the call to readdir_r fails. + void + next(void) + { + ::dirent* result; + + if (::readdir_r(_dirp, &_dirent, &result) == -1) { + const int original_errno = errno; + throw fs::system_error(F("readdir_r(%s) failed") % _path, + original_errno); + } + if (result == NULL) { + _entry.reset(NULL); + close(); + } else { + _entry.reset(new directory_entry(_dirent.d_name)); + } + } +}; + + +/// Constructs a new directory iterator. +/// +/// \param pimpl The constructed internal implementation structure to use. +detail::directory_iterator::directory_iterator(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +detail::directory_iterator::~directory_iterator(void) +{ +} + + +/// Creates a new directory iterator for a directory. +/// +/// \return The directory iterator. Note that the result may be invalid. +/// +/// \throw system_error If opening the directory or reading its first entry +/// fails. +detail::directory_iterator +detail::directory_iterator::new_begin(const path& path) +{ + return directory_iterator(std::shared_ptr< impl >(new impl(path))); +} + + +/// Creates a new invalid directory iterator. +/// +/// \return The invalid directory iterator. +detail::directory_iterator +detail::directory_iterator::new_end(void) +{ + return directory_iterator(std::shared_ptr< impl >(new impl())); +} + + +/// Checks if two iterators are equal. +/// +/// We consider two iterators to be equal if both of them are invalid or, +/// otherwise, if they have the exact same internal representation (as given by +/// equality of the pimpl pointers). +/// +/// \param other The object to compare to. +/// +/// \return True if the two iterators are equal; false otherwise. +bool +detail::directory_iterator::operator==(const directory_iterator& other) const +{ + return (_pimpl->_dirp == NULL && other._pimpl->_dirp == NULL) || + _pimpl == other._pimpl; +} + + +/// Checks if two iterators are different. +/// +/// \param other The object to compare to. +/// +/// \return True if the two iterators are different; false otherwise. +bool +detail::directory_iterator::operator!=(const directory_iterator& other) const +{ + return !(*this == other); +} + + +/// Moves the iterator one element forward. +/// +/// \return A reference to the iterator. +/// +/// \throw system_error If advancing the iterator fails. +detail::directory_iterator& +detail::directory_iterator::operator++(void) +{ + _pimpl->next(); + return *this; +} + + +/// Dereferences the iterator to its contents. +/// +/// \return A reference to the directory entry pointed to by the iterator. +const fs::directory_entry& +detail::directory_iterator::operator*(void) const +{ + PRE(_pimpl->_entry.get() != NULL); + return *_pimpl->_entry; +} + + +/// Dereferences the iterator to its contents. +/// +/// \return A pointer to the directory entry pointed to by the iterator. +const fs::directory_entry* +detail::directory_iterator::operator->(void) const +{ + PRE(_pimpl->_entry.get() != NULL); + return _pimpl->_entry.get(); +} + + +/// Internal implementation details for the directory. +struct utils::fs::directory::impl : utils::noncopyable { + /// Path to the directory to scan. + fs::path _path; + + /// Constructs a new directory. + /// + /// \param path_ Path to the directory to scan. + impl(const fs::path& path_) : _path(path_) + { + } +}; + + +/// Constructs a new directory. +/// +/// \param path_ Path to the directory to scan. +fs::directory::directory(const path& path_) : _pimpl(new impl(path_)) +{ +} + + +/// Returns an iterator to start scanning the directory. +/// +/// \return An iterator on the directory. +/// +/// \throw system_error If the directory cannot be opened to obtain its first +/// entry. +fs::directory::const_iterator +fs::directory::begin(void) const +{ + return const_iterator::new_begin(_pimpl->_path); +} + + +/// Returns an invalid iterator to check for the end of an scan. +/// +/// \return An invalid iterator. +fs::directory::const_iterator +fs::directory::end(void) const +{ + return const_iterator::new_end(); +} diff --git a/utils/fs/directory.hpp b/utils/fs/directory.hpp new file mode 100644 index 000000000000..53c37ec86450 --- /dev/null +++ b/utils/fs/directory.hpp @@ -0,0 +1,120 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/directory.hpp +/// Provides the utils::fs::directory class. + +#if !defined(UTILS_FS_DIRECTORY_HPP) +#define UTILS_FS_DIRECTORY_HPP + +#include "utils/fs/directory_fwd.hpp" + +#include <memory> +#include <ostream> +#include <string> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace fs { + + +/// Representation of a single directory entry. +struct directory_entry { + /// Name of the directory entry. + std::string name; + + explicit directory_entry(const std::string&); + + bool operator==(const directory_entry&) const; + bool operator!=(const directory_entry&) const; + bool operator<(const directory_entry&) const; +}; + + +std::ostream& operator<<(std::ostream&, const directory_entry&); + + +namespace detail { + + +/// Forward directory iterator. +class directory_iterator { + struct impl; + + /// Internal implementation details. + std::shared_ptr< impl > _pimpl; + + directory_iterator(std::shared_ptr< impl >); + + friend class fs::directory; + static directory_iterator new_begin(const path&); + static directory_iterator new_end(void); + +public: + ~directory_iterator(); + + bool operator==(const directory_iterator&) const; + bool operator!=(const directory_iterator&) const; + directory_iterator& operator++(void); + + const directory_entry& operator*(void) const; + const directory_entry* operator->(void) const; +}; + + +} // namespace detail + + +/// Representation of a local filesystem directory. +/// +/// This class is pretty much stateless. All the directory manipulation +/// operations happen within the iterator. +class directory { +public: + /// Public type for a constant forward directory iterator. + typedef detail::directory_iterator const_iterator; + +private: + struct impl; + + /// Internal implementation details. + std::shared_ptr< impl > _pimpl; + +public: + explicit directory(const path&); + + const_iterator begin(void) const; + const_iterator end(void) const; +}; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_DIRECTORY_HPP) diff --git a/utils/fs/directory_fwd.hpp b/utils/fs/directory_fwd.hpp new file mode 100644 index 000000000000..50886551ca88 --- /dev/null +++ b/utils/fs/directory_fwd.hpp @@ -0,0 +1,55 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/directory_fwd.hpp +/// Forward declarations for utils/fs/directory.hpp + +#if !defined(UTILS_FS_DIRECTORY_FWD_HPP) +#define UTILS_FS_DIRECTORY_FWD_HPP + +namespace utils { +namespace fs { + + +namespace detail { + + +class directory_iterator; + + +} // namespace detail + + +struct directory_entry; +class directory; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_DIRECTORY_FWD_HPP) diff --git a/utils/fs/directory_test.cpp b/utils/fs/directory_test.cpp new file mode 100644 index 000000000000..4c1aa2d010f4 --- /dev/null +++ b/utils/fs/directory_test.cpp @@ -0,0 +1,190 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/directory.hpp" + +#include <sstream> + +#include <atf-c++.hpp> + +#include "utils/format/containers.ipp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__public_fields); +ATF_TEST_CASE_BODY(directory_entry__public_fields) +{ + const fs::directory_entry entry("name"); + ATF_REQUIRE_EQ("name", entry.name); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__equality); +ATF_TEST_CASE_BODY(directory_entry__equality) +{ + const fs::directory_entry entry1("name"); + const fs::directory_entry entry2("other-name"); + + ATF_REQUIRE( entry1 == entry1); + ATF_REQUIRE(!(entry1 != entry1)); + + ATF_REQUIRE(!(entry1 == entry2)); + ATF_REQUIRE( entry1 != entry2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__sorting); +ATF_TEST_CASE_BODY(directory_entry__sorting) +{ + const fs::directory_entry entry1("name"); + const fs::directory_entry entry2("other-name"); + + ATF_REQUIRE(!(entry1 < entry1)); + ATF_REQUIRE(!(entry2 < entry2)); + ATF_REQUIRE( entry1 < entry2); + ATF_REQUIRE(!(entry2 < entry1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__format); +ATF_TEST_CASE_BODY(directory_entry__format) +{ + const fs::directory_entry entry("this is the name"); + std::ostringstream output; + output << entry; + ATF_REQUIRE_EQ("directory_entry{name='this is the name'}", output.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__empty); +ATF_TEST_CASE_BODY(integration__empty) +{ + fs::mkdir(fs::path("empty"), 0755); + + std::set< fs::directory_entry > contents; + const fs::directory dir(fs::path("empty")); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + // While we are here, make sure both * and -> represent the same. + ATF_REQUIRE((*iter).name == iter->name); + } + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__some_contents); +ATF_TEST_CASE_BODY(integration__some_contents) +{ + fs::mkdir(fs::path("full"), 0755); + atf::utils::create_file("full/a file", ""); + atf::utils::create_file("full/something-else", ""); + atf::utils::create_file("full/.hidden", ""); + fs::mkdir(fs::path("full/subdir"), 0755); + atf::utils::create_file("full/subdir/not-listed", ""); + + std::set< fs::directory_entry > contents; + const fs::directory dir(fs::path("full")); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + // While we are here, make sure both * and -> represent the same. + ATF_REQUIRE((*iter).name == iter->name); + } + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + exp_contents.insert(fs::directory_entry(".hidden")); + exp_contents.insert(fs::directory_entry("a file")); + exp_contents.insert(fs::directory_entry("something-else")); + exp_contents.insert(fs::directory_entry("subdir")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__open_failure); +ATF_TEST_CASE_BODY(integration__open_failure) +{ + const fs::directory directory(fs::path("non-existent")); + ATF_REQUIRE_THROW_RE(fs::system_error, "opendir(.*non-existent.*) failed", + directory.begin()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__iterators_equality); +ATF_TEST_CASE_BODY(integration__iterators_equality) +{ + const fs::directory directory(fs::path(".")); + + fs::directory::const_iterator iter_ok1 = directory.begin(); + fs::directory::const_iterator iter_ok2 = directory.begin(); + fs::directory::const_iterator iter_end = directory.end(); + + ATF_REQUIRE( iter_ok1 == iter_ok1); + ATF_REQUIRE(!(iter_ok1 != iter_ok1)); + + ATF_REQUIRE( iter_ok2 == iter_ok2); + ATF_REQUIRE(!(iter_ok2 != iter_ok2)); + + ATF_REQUIRE(!(iter_ok1 == iter_ok2)); + ATF_REQUIRE( iter_ok1 != iter_ok2); + + ATF_REQUIRE(!(iter_ok1 == iter_end)); + ATF_REQUIRE( iter_ok1 != iter_end); + + ATF_REQUIRE(!(iter_ok2 == iter_end)); + ATF_REQUIRE( iter_ok2 != iter_end); + + ATF_REQUIRE( iter_end == iter_end); + ATF_REQUIRE(!(iter_end != iter_end)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, directory_entry__public_fields); + ATF_ADD_TEST_CASE(tcs, directory_entry__equality); + ATF_ADD_TEST_CASE(tcs, directory_entry__sorting); + ATF_ADD_TEST_CASE(tcs, directory_entry__format); + + ATF_ADD_TEST_CASE(tcs, integration__empty); + ATF_ADD_TEST_CASE(tcs, integration__some_contents); + ATF_ADD_TEST_CASE(tcs, integration__open_failure); + ATF_ADD_TEST_CASE(tcs, integration__iterators_equality); +} diff --git a/utils/fs/exceptions.cpp b/utils/fs/exceptions.cpp new file mode 100644 index 000000000000..102e9069ee3c --- /dev/null +++ b/utils/fs/exceptions.cpp @@ -0,0 +1,162 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/exceptions.hpp" + +#include <cstring> + +#include "utils/format/macros.hpp" + +namespace fs = utils::fs; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +fs::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +fs::error::~error(void) throw() +{ +} + + +/// Constructs a new invalid_path_error. +/// +/// \param textual_path Textual representation of the invalid path. +/// \param reason Description of the error in the path. +fs::invalid_path_error::invalid_path_error(const std::string& textual_path, + const std::string& reason) : + error(F("Invalid path '%s': %s") % textual_path % reason), + _textual_path(textual_path) +{ +} + + +/// Destructor for the error. +fs::invalid_path_error::~invalid_path_error(void) throw() +{ +} + + +/// Returns the invalid path related to the exception. +/// +/// \return The textual representation of the invalid path. +const std::string& +fs::invalid_path_error::invalid_path(void) const +{ + return _textual_path; +} + + +/// Constructs a new join_error. +/// +/// \param textual_path1_ Textual representation of the first path. +/// \param textual_path2_ Textual representation of the second path. +/// \param reason Description of the error in the join operation. +fs::join_error::join_error(const std::string& textual_path1_, + const std::string& textual_path2_, + const std::string& reason) : + error(F("Cannot join paths '%s' and '%s': %s") % textual_path1_ % + textual_path2_ % reason), + _textual_path1(textual_path1_), + _textual_path2(textual_path2_) +{ +} + + +/// Destructor for the error. +fs::join_error::~join_error(void) throw() +{ +} + + +/// Gets the first path that caused the error in a join operation. +/// +/// \return The textual representation of the path. +const std::string& +fs::join_error::textual_path1(void) const +{ + return _textual_path1; +} + + +/// Gets the second path that caused the error in a join operation. +/// +/// \return The textual representation of the path. +const std::string& +fs::join_error::textual_path2(void) const +{ + return _textual_path2; +} + + +/// Constructs a new error based on an errno code. +/// +/// \param message_ The message describing what caused the error. +/// \param errno_ The error code. +fs::system_error::system_error(const std::string& message_, const int errno_) : + error(F("%s: %s") % message_ % std::strerror(errno_)), + _original_errno(errno_) +{ +} + + +/// Destructor for the error. +fs::system_error::~system_error(void) throw() +{ +} + + + +/// \return The original errno code. +int +fs::system_error::original_errno(void) const throw() +{ + return _original_errno; +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +fs::unsupported_operation_error::unsupported_operation_error( + const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +fs::unsupported_operation_error::~unsupported_operation_error(void) throw() +{ +} diff --git a/utils/fs/exceptions.hpp b/utils/fs/exceptions.hpp new file mode 100644 index 000000000000..32b4af2ce463 --- /dev/null +++ b/utils/fs/exceptions.hpp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/exceptions.hpp +/// Exception types raised by the fs module. + +#if !defined(UTILS_FS_EXCEPTIONS_HPP) +#define UTILS_FS_EXCEPTIONS_HPP + +#include <stdexcept> +#include <string> + +namespace utils { +namespace fs { + + +/// Base exception for fs errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error denoting an invalid path while constructing a fs::path object. +class invalid_path_error : public error { + /// Raw value of the invalid path. + std::string _textual_path; + +public: + explicit invalid_path_error(const std::string&, const std::string&); + virtual ~invalid_path_error(void) throw(); + + const std::string& invalid_path(void) const; +}; + + +/// Paths cannot be joined. +class join_error : public error { + /// Raw value of the first path in the join operation. + std::string _textual_path1; + + /// Raw value of the second path in the join operation. + std::string _textual_path2; + +public: + explicit join_error(const std::string&, const std::string&, + const std::string&); + virtual ~join_error(void) throw(); + + const std::string& textual_path1(void) const; + const std::string& textual_path2(void) const; +}; + + +/// Exceptions for errno-based errors. +/// +/// TODO(jmmv): This code is duplicated in, at least, utils::process. Figure +/// out a way to reuse this exception while maintaining the correct inheritance +/// (i.e. be able to keep it as a child of fs::error). +class system_error : public error { + /// Error number describing this libc error condition. + int _original_errno; + +public: + explicit system_error(const std::string&, const int); + ~system_error(void) throw(); + + int original_errno(void) const throw(); +}; + + +/// Exception to denote an unsupported operation. +class unsupported_operation_error : public error { +public: + explicit unsupported_operation_error(const std::string&); + virtual ~unsupported_operation_error(void) throw(); +}; + + +} // namespace fs +} // namespace utils + + +#endif // !defined(UTILS_FS_EXCEPTIONS_HPP) diff --git a/utils/fs/exceptions_test.cpp b/utils/fs/exceptions_test.cpp new file mode 100644 index 000000000000..e67a846506cc --- /dev/null +++ b/utils/fs/exceptions_test.cpp @@ -0,0 +1,95 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/exceptions.hpp" + +#include <cerrno> +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const fs::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_path_error); +ATF_TEST_CASE_BODY(invalid_path_error) +{ + const fs::invalid_path_error e("some/invalid/path", "The reason"); + ATF_REQUIRE(std::strcmp("Invalid path 'some/invalid/path': The reason", + e.what()) == 0); + ATF_REQUIRE_EQ("some/invalid/path", e.invalid_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join_error); +ATF_TEST_CASE_BODY(join_error) +{ + const fs::join_error e("dir1/file1", "/dir2/file2", "The reason"); + ATF_REQUIRE(std::strcmp("Cannot join paths 'dir1/file1' and '/dir2/file2': " + "The reason", e.what()) == 0); + ATF_REQUIRE_EQ("dir1/file1", e.textual_path1()); + ATF_REQUIRE_EQ("/dir2/file2", e.textual_path2()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(system_error); +ATF_TEST_CASE_BODY(system_error) +{ + const fs::system_error e("Call failed", ENOENT); + const std::string expected = F("Call failed: %s") % std::strerror(ENOENT); + ATF_REQUIRE_EQ(expected, e.what()); + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unsupported_operation_error); +ATF_TEST_CASE_BODY(unsupported_operation_error) +{ + const fs::unsupported_operation_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, invalid_path_error); + ATF_ADD_TEST_CASE(tcs, join_error); + ATF_ADD_TEST_CASE(tcs, system_error); + ATF_ADD_TEST_CASE(tcs, unsupported_operation_error); +} diff --git a/utils/fs/lua_module.cpp b/utils/fs/lua_module.cpp new file mode 100644 index 000000000000..dec410927e1a --- /dev/null +++ b/utils/fs/lua_module.cpp @@ -0,0 +1,340 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/lua_module.hpp" + +extern "C" { +#include <dirent.h> +} + +#include <cerrno> +#include <cstring> +#include <stdexcept> +#include <string> + +#include <lutok/operations.hpp> +#include <lutok/stack_cleaner.hpp> +#include <lutok/state.ipp> + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Given a path, qualifies it with the module's start directory if necessary. +/// +/// \param state The Lua state. +/// \param path The path to qualify. +/// +/// \return The original path if it was absolute; otherwise the original path +/// appended to the module's start directory. +/// +/// \throw std::runtime_error If the module's state has been corrupted. +static fs::path +qualify_path(lutok::state& state, const fs::path& path) +{ + lutok::stack_cleaner cleaner(state); + + if (path.is_absolute()) { + return path; + } else { + state.get_global("_fs_start_dir"); + if (!state.is_string(-1)) + throw std::runtime_error("Missing _fs_start_dir global variable; " + "state corrupted?"); + return fs::path(state.to_string(-1)) / path; + } +} + + +/// Safely gets a path from the Lua state. +/// +/// \param state The Lua state. +/// \param index The position in the Lua stack that contains the path to query. +/// +/// \return The queried path. +/// +/// \throw fs::error If the value is not a valid path. +/// \throw std::runtime_error If the value on the Lua stack is not convertible +/// to a path. +static fs::path +to_path(lutok::state& state, const int index) +{ + if (!state.is_string(index)) + throw std::runtime_error("Need a string parameter"); + return fs::path(state.to_string(index)); +} + + +/// Lua binding for fs::path::basename. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The basename of the input path. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_basename(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + state.push_string(path.leaf_name().c_str()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::dirname. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The directory part of the input path. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_dirname(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + state.push_string(path.branch_path().c_str()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::exists. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) Whether the input path exists or not. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_exists(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = qualify_path(state, to_path(state, -1)); + state.push_boolean(fs::exists(path)); + cleaner.forget(); + return 1; +} + + +/// Lua binding for the files iterator. +/// +/// This function takes an open directory from the closure of the iterator and +/// returns the next entry. See lua_fs_files() for the iterator generator +/// function. +/// +/// \pre upvalue(1) The userdata containing an open DIR* object. +/// +/// \param state The lua state. +/// +/// \return The number of result values, i.e. 0 if there are no more entries or +/// 1 if an entry has been read. +static int +files_iterator(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + DIR** dirp = state.to_userdata< DIR* >(state.upvalue_index(1)); + const struct dirent* entry = ::readdir(*dirp); + if (entry == NULL) + return 0; + else { + state.push_string(entry->d_name); + cleaner.forget(); + return 1; + } +} + + +/// Lua binding for the destruction of the files iterator. +/// +/// This function takes an open directory and closes it. See lua_fs_files() for +/// the iterator generator function. +/// +/// \pre stack(-1) The userdata containing an open DIR* object. +/// \post The DIR* object is closed. +/// +/// \param state The lua state. +/// +/// \return The number of result values, i.e. 0. +static int +files_gc(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + PRE(state.is_userdata(-1)); + + DIR** dirp = state.to_userdata< DIR* >(-1); + // For some reason, this may be called more than once. I don't know why + // this happens, but we must protect against it. + if (*dirp != NULL) { + ::closedir(*dirp); + *dirp = NULL; + } + + return 0; +} + + +/// Lua binding to create an iterator to scan the contents of a directory. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The iterator function. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_files(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = qualify_path(state, to_path(state, -1)); + + DIR** dirp = state.new_userdata< DIR* >(); + + state.new_table(); + state.push_string("__gc"); + state.push_cxx_function(files_gc); + state.set_table(-3); + + state.set_metatable(-2); + + *dirp = ::opendir(path.c_str()); + if (*dirp == NULL) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to open directory: %s") % + std::strerror(original_errno)); + } + + state.push_cxx_closure(files_iterator, 1); + + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::is_absolute. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) Whether the input path is absolute or not. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_is_absolute(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + + state.push_boolean(path.is_absolute()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::operator/. +/// +/// \pre stack(-2) The first input path. +/// \pre stack(-1) The second input path. +/// \post stack(-1) The concatenation of the two paths. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_join(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path1 = to_path(state, -2); + const fs::path path2 = to_path(state, -1); + state.push_string((path1 / path2).c_str()); + cleaner.forget(); + return 1; +} + + +} // anonymous namespace + + +/// Creates a Lua 'fs' module with a default start directory of ".". +/// +/// \post The global 'fs' symbol is set to a table that contains functions to a +/// variety of utilites from the fs C++ module. +/// +/// \param s The Lua state. +void +fs::open_fs(lutok::state& s) +{ + open_fs(s, fs::current_path()); +} + + +/// Creates a Lua 'fs' module with an explicit start directory. +/// +/// \post The global 'fs' symbol is set to a table that contains functions to a +/// variety of utilites from the fs C++ module. +/// +/// \param s The Lua state. +/// \param start_dir The start directory to use in all operations that reference +/// the underlying file sytem. +void +fs::open_fs(lutok::state& s, const fs::path& start_dir) +{ + lutok::stack_cleaner cleaner(s); + + s.push_string(start_dir.str()); + s.set_global("_fs_start_dir"); + + std::map< std::string, lutok::cxx_function > members; + members["basename"] = lua_fs_basename; + members["dirname"] = lua_fs_dirname; + members["exists"] = lua_fs_exists; + members["files"] = lua_fs_files; + members["is_absolute"] = lua_fs_is_absolute; + members["join"] = lua_fs_join; + lutok::create_module(s, "fs", members); +} diff --git a/utils/fs/lua_module.hpp b/utils/fs/lua_module.hpp new file mode 100644 index 000000000000..c9c303b15eb7 --- /dev/null +++ b/utils/fs/lua_module.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/lua_module.hpp +/// Lua bindings for the utils::fs module. +/// +/// When the fs module is bound to Lua, the module has the concept of a "start +/// directory". The start directory is the directory used to qualify all +/// relative paths, and is provided at module binding time. + +#if !defined(UTILS_FS_LUA_MODULE_HPP) +#define UTILS_FS_LUA_MODULE_HPP + +#include <lutok/state.hpp> + +#include "utils/fs/path.hpp" + +namespace utils { +namespace fs { + + +void open_fs(lutok::state&); +void open_fs(lutok::state&, const fs::path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_LUA_MODULE_HPP) diff --git a/utils/fs/lua_module_test.cpp b/utils/fs/lua_module_test.cpp new file mode 100644 index 000000000000..263632ded13f --- /dev/null +++ b/utils/fs/lua_module_test.cpp @@ -0,0 +1,376 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/lua_module.hpp" + +#include <atf-c++.hpp> +#include <lutok/operations.hpp> +#include <lutok/state.hpp> +#include <lutok/test_utils.hpp> + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(open_fs); +ATF_TEST_CASE_BODY(open_fs) +{ + lutok::state state; + stack_balance_checker checker(state); + fs::open_fs(state); + lutok::do_string(state, "return fs.basename", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + lutok::do_string(state, "return fs.dirname", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + lutok::do_string(state, "return fs.join", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + state.pop(3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(basename__ok); +ATF_TEST_CASE_BODY(basename__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.basename('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE_EQ("file_foobar", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(basename__fail); +ATF_TEST_CASE_BODY(basename__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.basename({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.basename('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dirname__ok); +ATF_TEST_CASE_BODY(dirname__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.dirname('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE_EQ("/my/test", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dirname__fail); +ATF_TEST_CASE_BODY(dirname__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.dirname({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.dirname('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__ok); +ATF_TEST_CASE_BODY(exists__ok) +{ + lutok::state state; + fs::open_fs(state); + + atf::utils::create_file("foo", ""); + + lutok::do_string(state, "return fs.exists('foo')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('bar')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, + F("return fs.exists('%s')") % fs::current_path(), 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__fail); +ATF_TEST_CASE_BODY(exists__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.exists({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.exists('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__custom_start_dir); +ATF_TEST_CASE_BODY(exists__custom_start_dir) +{ + lutok::state state; + fs::open_fs(state, fs::path("subdir")); + + fs::mkdir(fs::path("subdir"), 0755); + atf::utils::create_file("subdir/foo", ""); + atf::utils::create_file("bar", ""); + + lutok::do_string(state, "return fs.exists('foo')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('subdir/foo')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('bar')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('../bar')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, + F("return fs.exists('%s')") % (fs::current_path() / "bar"), + 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__none); +ATF_TEST_CASE_BODY(files__none) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state); + + fs::mkdir(fs::path("root"), 0755); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('root') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". ..", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__some); +ATF_TEST_CASE_BODY(files__some) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/file1", ""); + atf::utils::create_file("root/file2", ""); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('root') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". .. file1 file2", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__some_with_custom_start_dir); +ATF_TEST_CASE_BODY(files__some_with_custom_start_dir) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state, fs::current_path() / "root"); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/file1", ""); + atf::utils::create_file("root/file2", ""); + atf::utils::create_file("file3", ""); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('.') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". .. file1 file2", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__fail_arg); +ATF_TEST_CASE_BODY(files__fail_arg) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string parameter", + lutok::do_string(state, "fs.files({})", 0, 0, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "fs.files('')", 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__fail_opendir); +ATF_TEST_CASE_BODY(files__fail_opendir) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Failed to open directory", + lutok::do_string(state, "fs.files('root')", 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute__ok); +ATF_TEST_CASE_BODY(is_absolute__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.is_absolute('my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + lutok::do_string(state, "return fs.is_absolute('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute__fail); +ATF_TEST_CASE_BODY(is_absolute__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.is_absolute({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.is_absolute('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__ok); +ATF_TEST_CASE_BODY(join__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.join('/a/b///', 'c/d')", 0, 1, 0); + ATF_REQUIRE_EQ("/a/b/c/d", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__fail); +ATF_TEST_CASE_BODY(join__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.join({}, 'a')", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.join('a', {})", + 0, 1, 0)); + + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.join('', 'a')", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.join('a', '')", + 0, 1, 0)); + + ATF_REQUIRE_THROW_RE(lutok::error, "Cannot join.*'a/b'.*'/c'", + lutok::do_string(state, "fs.join('a/b', '/c')", + 0, 0, 0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, open_fs); + + ATF_ADD_TEST_CASE(tcs, basename__ok); + ATF_ADD_TEST_CASE(tcs, basename__fail); + + ATF_ADD_TEST_CASE(tcs, dirname__ok); + ATF_ADD_TEST_CASE(tcs, dirname__fail); + + ATF_ADD_TEST_CASE(tcs, exists__ok); + ATF_ADD_TEST_CASE(tcs, exists__fail); + ATF_ADD_TEST_CASE(tcs, exists__custom_start_dir); + + ATF_ADD_TEST_CASE(tcs, files__none); + ATF_ADD_TEST_CASE(tcs, files__some); + ATF_ADD_TEST_CASE(tcs, files__some_with_custom_start_dir); + ATF_ADD_TEST_CASE(tcs, files__fail_arg); + ATF_ADD_TEST_CASE(tcs, files__fail_opendir); + + ATF_ADD_TEST_CASE(tcs, is_absolute__ok); + ATF_ADD_TEST_CASE(tcs, is_absolute__fail); + + ATF_ADD_TEST_CASE(tcs, join__ok); + ATF_ADD_TEST_CASE(tcs, join__fail); +} diff --git a/utils/fs/operations.cpp b/utils/fs/operations.cpp new file mode 100644 index 000000000000..7a96d0b2058a --- /dev/null +++ b/utils/fs/operations.cpp @@ -0,0 +1,803 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/operations.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include <sys/param.h> +#if defined(HAVE_SYS_MOUNT_H) +# include <sys/mount.h> +#endif +#include <sys/stat.h> +#if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS) +# include <sys/statvfs.h> +#endif +#if defined(HAVE_SYS_VFS_H) +# include <sys/vfs.h> +#endif +#include <sys/wait.h> + +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <fstream> +#include <iostream> +#include <sstream> +#include <string> + +#include "utils/auto_array.ipp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/units.hpp" + +namespace fs = utils::fs; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Operating systems recognized by the code below. +enum os_type { + os_unsupported = 0, + os_freebsd, + os_linux, + os_netbsd, + os_sunos, +}; + + +/// The current operating system. +static enum os_type current_os = +#if defined(__FreeBSD__) + os_freebsd +#elif defined(__linux__) + os_linux +#elif defined(__NetBSD__) + os_netbsd +#elif defined(__SunOS__) + os_sunos +#else + os_unsupported +#endif + ; + + +/// Specifies if a real unmount(2) is available. +/// +/// We use this as a constant instead of a macro so that we can compile both +/// versions of the unmount code unconditionally. This is a way to prevent +/// compilation bugs going unnoticed for long. +static const bool have_unmount2 = +#if defined(HAVE_UNMOUNT) + true; +#else + false; +#endif + + +#if !defined(UMOUNT) +/// Fake replacement value to the path to umount(8). +# define UMOUNT "do-not-use-this-value" +#else +# if defined(HAVE_UNMOUNT) +# error "umount(8) detected when unmount(2) is also available" +# endif +#endif + + +#if !defined(HAVE_UNMOUNT) +/// Fake unmount(2) function for systems without it. +/// +/// This is only provided to allow our code to compile in all platforms +/// regardless of whether they actually have an unmount(2) or not. +/// +/// \return -1 to indicate error, although this should never happen. +static int +unmount(const char* /* path */, + const int /* flags */) +{ + PRE(false); + return -1; +} +#endif + + +/// Error code returned by subprocess to indicate a controlled failure. +const int exit_known_error = 123; + + +static void run_mount_tmpfs(const fs::path&, const uint64_t) UTILS_NORETURN; + + +/// Executes 'mount -t tmpfs' (or a similar variant). +/// +/// This function must be called from a subprocess as it never returns. +/// +/// \param mount_point Location on which to mount a tmpfs. +/// \param size The size of the tmpfs to mount. If 0, use unlimited. +static void +run_mount_tmpfs(const fs::path& mount_point, const uint64_t size) +{ + const char* mount_args[16]; + std::string size_arg; + + std::size_t last = 0; + switch (current_os) { + case os_freebsd: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-osize=%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_linux: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-osize=%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_netbsd: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-o-s%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_sunos: + mount_args[last++] = "mount"; + mount_args[last++] = "-Ftmpfs"; + if (size > 0) { + size_arg = F("-o-s%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + default: + std::cerr << "Don't know how to mount a temporary file system in this " + "host operating system\n"; + std::exit(exit_known_error); + } + mount_args[last] = NULL; + + const char** arg; + std::cout << "Mounting tmpfs onto " << mount_point << " with:"; + for (arg = &mount_args[0]; *arg != NULL; arg++) + std::cout << " " << *arg; + std::cout << "\n"; + + const int ret = ::execvp(mount_args[0], + UTILS_UNCONST(char* const, mount_args)); + INV(ret == -1); + std::cerr << "Failed to exec " << mount_args[0] << "\n"; + std::exit(EXIT_FAILURE); +} + + +/// Unmounts a file system using unmount(2). +/// +/// \pre unmount(2) must be available; i.e. have_unmount2 must be true. +/// +/// \param mount_point The file system to unmount. +/// +/// \throw fs::system_error If the call to unmount(2) fails. +static void +unmount_with_unmount2(const fs::path& mount_point) +{ + PRE(have_unmount2); + + if (::unmount(mount_point.c_str(), 0) == -1) { + const int original_errno = errno; + throw fs::system_error(F("unmount(%s) failed") % mount_point, + original_errno); + } +} + + +/// Unmounts a file system using umount(8). +/// +/// \pre umount(2) must not be available; i.e. have_unmount2 must be false. +/// +/// \param mount_point The file system to unmount. +/// +/// \throw fs::error If the execution of umount(8) fails. +static void +unmount_with_umount8(const fs::path& mount_point) +{ + PRE(!have_unmount2); + + const pid_t pid = ::fork(); + if (pid == -1) { + const int original_errno = errno; + throw fs::system_error("Cannot fork to execute unmount tool", + original_errno); + } else if (pid == 0) { + const int ret = ::execlp(UMOUNT, "umount", mount_point.c_str(), NULL); + INV(ret == -1); + std::cerr << "Failed to exec " UMOUNT "\n"; + std::exit(EXIT_FAILURE); + } + + int status; +retry: + if (::waitpid(pid, &status, 0) == -1) { + const int original_errno = errno; + if (errno == EINTR) + goto retry; + throw fs::system_error("Failed to wait for unmount subprocess", + original_errno); + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == EXIT_SUCCESS) + return; + else + throw fs::error(F("Failed to unmount %s; returned exit code %s") + % mount_point % WEXITSTATUS(status)); + } else + throw fs::error(F("Failed to unmount %s; unmount tool received signal") + % mount_point); +} + + +/// Stats a file, without following links. +/// +/// \param path The file to stat. +/// +/// \return The stat structure on success. +/// +/// \throw system_error An error on failure. +static struct ::stat +safe_stat(const fs::path& path) +{ + struct ::stat sb; + if (::lstat(path.c_str(), &sb) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Cannot get information about %s") % path, + original_errno); + } + return sb; +} + + +} // anonymous namespace + + +/// Copies a file. +/// +/// \param source The file to copy. +/// \param target The destination of the new copy; must be a file name, not a +/// directory. +/// +/// \throw error If there is a problem copying the file. +void +fs::copy(const fs::path& source, const fs::path& target) +{ + std::ifstream input(source.c_str()); + if (!input) + throw error(F("Cannot open copy source %s") % source); + + std::ofstream output(target.c_str()); + if (!output) + throw error(F("Cannot create copy target %s") % target); + + char buffer[1024]; + while (input.good()) { + input.read(buffer, sizeof(buffer)); + if (input.good() || input.eof()) + output.write(buffer, input.gcount()); + } + if (!input.good() && !input.eof()) + throw error(F("Error while reading input file %s") % source); +} + + +/// Queries the path to the current directory. +/// +/// \return The path to the current directory. +/// +/// \throw fs::error If there is a problem querying the current directory. +fs::path +fs::current_path(void) +{ + char* cwd; +#if defined(HAVE_GETCWD_DYN) + cwd = ::getcwd(NULL, 0); +#else + cwd = ::getcwd(NULL, MAXPATHLEN); +#endif + if (cwd == NULL) { + const int original_errno = errno; + throw fs::system_error(F("Failed to get current working directory"), + original_errno); + } + + try { + const fs::path result(cwd); + std::free(cwd); + return result; + } catch (...) { + std::free(cwd); + throw; + } +} + + +/// Checks if a file exists. +/// +/// Be aware that this is racy in the same way as access(2) is. +/// +/// \param path The file to check the existance of. +/// +/// \return True if the file exists; false otherwise. +bool +fs::exists(const fs::path& path) +{ + return ::access(path.c_str(), F_OK) == 0; +} + + +/// Locates a file in the PATH. +/// +/// \param name The file to locate. +/// +/// \return The path to the located file or none if it was not found. The +/// returned path is always absolute. +optional< fs::path > +fs::find_in_path(const char* name) +{ + const optional< std::string > current_path = utils::getenv("PATH"); + if (!current_path || current_path.get().empty()) + return none; + + std::istringstream path_input(current_path.get() + ":"); + std::string path_component; + while (std::getline(path_input, path_component, ':').good()) { + const fs::path candidate = path_component.empty() ? + fs::path(name) : (fs::path(path_component) / name); + if (exists(candidate)) { + if (candidate.is_absolute()) + return utils::make_optional(candidate); + else + return utils::make_optional(candidate.to_absolute()); + } + } + return none; +} + + +/// Calculates the free space in a given file system. +/// +/// \param path Path to a file in the file system for which to check the free +/// disk space. +/// +/// \return The amount of free space usable by a non-root user. +/// +/// \throw system_error If the call to statfs(2) fails. +utils::units::bytes +fs::free_disk_space(const fs::path& path) +{ +#if defined(HAVE_STATVFS) + struct ::statvfs buf; + if (::statvfs(path.c_str(), &buf) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to stat file system for %s") % path, + original_errno); + } + return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); +#elif defined(HAVE_STATFS) + struct ::statfs buf; + if (::statfs(path.c_str(), &buf) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to stat file system for %s") % path, + original_errno); + } + return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); +#else +# error "Don't know how to query free disk space" +#endif +} + + +/// Checks if the given path is a directory or not. +/// +/// \return True if the path is a directory; false otherwise. +bool +fs::is_directory(const fs::path& path) +{ + const struct ::stat sb = safe_stat(path); + return S_ISDIR(sb.st_mode); +} + + +/// Creates a directory. +/// +/// \param dir The path to the directory to create. +/// \param mode The permissions for the new directory. +/// +/// \throw system_error If the call to mkdir(2) fails. +void +fs::mkdir(const fs::path& dir, const int mode) +{ + if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to create directory %s") % dir, + original_errno); + } +} + + +/// Creates a directory and any missing parents. +/// +/// This is separate from the fs::mkdir function to clearly differentiate the +/// libc wrapper from the more complex algorithm implemented here. +/// +/// \param dir The path to the directory to create. +/// \param mode The permissions for the new directories. +/// +/// \throw system_error If any call to mkdir(2) fails. +void +fs::mkdir_p(const fs::path& dir, const int mode) +{ + try { + fs::mkdir(dir, mode); + } catch (const fs::system_error& e) { + if (e.original_errno() == ENOENT) { + fs::mkdir_p(dir.branch_path(), mode); + fs::mkdir(dir, mode); + } else if (e.original_errno() != EEXIST) + throw e; + } +} + + +/// Creates a temporary directory that is world readable/accessible. +/// +/// The temporary directory is created using mkdtemp(3) using the provided +/// template. This should be most likely used in conjunction with +/// fs::auto_directory. +/// +/// The temporary directory is given read and execute permissions to everyone +/// and thus should not be used to protect data that may be subject to snooping. +/// This goes together with the assumption that this function is used to create +/// temporary directories for test cases, and that those test cases may +/// sometimes be executed as an unprivileged user. In those cases, we need to +/// support two different things: +/// +/// - Allow the unprivileged code to write to files in the work directory by +/// name (e.g. to write the results file, whose name is provided by the +/// monitor code running as root). This requires us to grant search +/// permissions. +/// +/// - Allow the test cases themselves to call getcwd(3) at any point. At least +/// on NetBSD 7.x, getcwd(3) requires both read and search permissions on all +/// path components leading to the current directory. This requires us to +/// grant both read and search permissions. +/// +/// TODO(jmmv): A cleaner way to support this would be for the test executor to +/// create two work directory hierarchies directly rooted under TMPDIR: one for +/// root and one for the unprivileged user. However, that requires more +/// bookkeeping for no real gain, because we are not really trying to protect +/// the data within our temporary directories against attacks. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The generated path for the temporary directory. +/// +/// \throw fs::system_error If the call to mkdtemp(3) fails. +fs::path +fs::mkdtemp_public(const std::string& path_template) +{ + PRE(path_template.find("XXXXXX") != std::string::npos); + + const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); + const fs::path full_template = tmpdir / path_template; + + utils::auto_array< char > buf(new char[full_template.str().length() + 1]); + std::strcpy(buf.get(), full_template.c_str()); + if (::mkdtemp(buf.get()) == NULL) { + const int original_errno = errno; + throw fs::system_error(F("Cannot create temporary directory using " + "template %s") % full_template, + original_errno); + } + const fs::path path(buf.get()); + + if (::chmod(path.c_str(), 0755) == -1) { + const int original_errno = errno; + + try { + rmdir(path); + } catch (const fs::system_error& e) { + // This really should not fail. We just created the directory and + // have not written anything to it so there is no reason for this to + // fail. But better handle the failure just in case. + LW(F("Failed to delete just-created temporary directory %s") + % path); + } + + throw fs::system_error(F("Failed to grant search permissions on " + "temporary directory %s") % path, + original_errno); + } + + return path; +} + + +/// Creates a temporary file. +/// +/// The temporary file is created using mkstemp(3) using the provided template. +/// This should be most likely used in conjunction with fs::auto_file. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The generated path for the temporary directory. +/// +/// \throw fs::system_error If the call to mkstemp(3) fails. +fs::path +fs::mkstemp(const std::string& path_template) +{ + PRE(path_template.find("XXXXXX") != std::string::npos); + + const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); + const fs::path full_template = tmpdir / path_template; + + utils::auto_array< char > buf(new char[full_template.str().length() + 1]); + std::strcpy(buf.get(), full_template.c_str()); + if (::mkstemp(buf.get()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Cannot create temporary file using template " + "%s") % full_template, original_errno); + } + return fs::path(buf.get()); +} + + +/// Mounts a temporary file system with unlimited size. +/// +/// \param in_mount_point The path on which the file system will be mounted. +/// +/// \throw fs::system_error If the attempt to mount process fails. +/// \throw fs::unsupported_operation_error If the code does not know how to +/// mount a temporary file system in the current operating system. +void +fs::mount_tmpfs(const fs::path& in_mount_point) +{ + mount_tmpfs(in_mount_point, units::bytes()); +} + + +/// Mounts a temporary file system. +/// +/// \param in_mount_point The path on which the file system will be mounted. +/// \param size The size of the tmpfs to mount. If 0, use unlimited. +/// +/// \throw fs::system_error If the attempt to mount process fails. +/// \throw fs::unsupported_operation_error If the code does not know how to +/// mount a temporary file system in the current operating system. +void +fs::mount_tmpfs(const fs::path& in_mount_point, const units::bytes& size) +{ + // SunOS's mount(8) requires paths to be absolute. To err on the side of + // caution, let's make the mount point absolute in all cases. + const fs::path mount_point = in_mount_point.is_absolute() ? + in_mount_point : in_mount_point.to_absolute(); + + const pid_t pid = ::fork(); + if (pid == -1) { + const int original_errno = errno; + throw fs::system_error("Cannot fork to execute mount tool", + original_errno); + } + if (pid == 0) + run_mount_tmpfs(mount_point, size); + + int status; +retry: + if (::waitpid(pid, &status, 0) == -1) { + const int original_errno = errno; + if (errno == EINTR) + goto retry; + throw fs::system_error("Failed to wait for mount subprocess", + original_errno); + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == exit_known_error) + throw fs::unsupported_operation_error( + "Don't know how to mount a tmpfs on this operating system"); + else if (WEXITSTATUS(status) == EXIT_SUCCESS) + return; + else + throw fs::error(F("Failed to mount tmpfs on %s; returned exit " + "code %s") % mount_point % WEXITSTATUS(status)); + } else { + throw fs::error(F("Failed to mount tmpfs on %s; mount tool " + "received signal") % mount_point); + } +} + + +/// Recursively removes a directory. +/// +/// This operation simulates a "rm -r". No effort is made to forcibly delete +/// files and no attention is paid to mount points. +/// +/// \param directory The directory to remove. +/// +/// \throw fs::error If there is a problem removing any directory or file. +void +fs::rm_r(const fs::path& directory) +{ + const fs::directory dir(directory); + + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + if (iter->name == "." || iter->name == "..") + continue; + + const fs::path entry = directory / iter->name; + + if (fs::is_directory(entry)) { + LD(F("Descending into %s") % entry); + fs::rm_r(entry); + } else { + LD(F("Removing file %s") % entry); + fs::unlink(entry); + } + } + + LD(F("Removing empty directory %s") % directory); + fs::rmdir(directory); +} + + +/// Removes an empty directory. +/// +/// \param file The directory to remove. +/// +/// \throw fs::system_error If the call to rmdir(2) fails. +void +fs::rmdir(const path& file) +{ + if (::rmdir(file.c_str()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Removal of %s failed") % file, + original_errno); + } +} + + +/// Obtains all the entries in a directory. +/// +/// \param path The directory to scan. +/// +/// \return The set of all directory entries in the given directory. +/// +/// \throw fs::system_error If reading the directory fails for any reason. +std::set< fs::directory_entry > +fs::scan_directory(const fs::path& path) +{ + std::set< fs::directory_entry > contents; + + fs::directory dir(path); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + } + + return contents; +} + + +/// Removes a file. +/// +/// \param file The file to remove. +/// +/// \throw fs::system_error If the call to unlink(2) fails. +void +fs::unlink(const path& file) +{ + if (::unlink(file.c_str()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Removal of %s failed") % file, + original_errno); + } +} + + +/// Unmounts a file system. +/// +/// \param in_mount_point The file system to unmount. +/// +/// \throw fs::error If the unmount fails. +void +fs::unmount(const fs::path& in_mount_point) +{ + // FreeBSD's unmount(2) requires paths to be absolute. To err on the side + // of caution, let's make it absolute in all cases. + const fs::path mount_point = in_mount_point.is_absolute() ? + in_mount_point : in_mount_point.to_absolute(); + + static const int unmount_retries = 3; + static const int unmount_retry_delay_seconds = 1; + + int retries = unmount_retries; +retry: + try { + if (have_unmount2) { + unmount_with_unmount2(mount_point); + } else { + unmount_with_umount8(mount_point); + } + } catch (const fs::system_error& error) { + if (error.original_errno() == EBUSY && retries > 0) { + LW(F("%s busy; unmount retries left %s") % mount_point % retries); + retries--; + ::sleep(unmount_retry_delay_seconds); + goto retry; + } + throw; + } +} diff --git a/utils/fs/operations.hpp b/utils/fs/operations.hpp new file mode 100644 index 000000000000..bd7560ffc048 --- /dev/null +++ b/utils/fs/operations.hpp @@ -0,0 +1,72 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/operations.hpp +/// File system algorithms and access functions. +/// +/// The functions in this module are exception-based, type-improved wrappers +/// over the functions provided by libc. + +#if !defined(UTILS_FS_OPERATIONS_HPP) +#define UTILS_FS_OPERATIONS_HPP + +#include <set> +#include <string> + +#include "utils/fs/directory_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/units_fwd.hpp" + +namespace utils { +namespace fs { + + +void copy(const fs::path&, const fs::path&); +path current_path(void); +bool exists(const fs::path&); +utils::optional< path > find_in_path(const char*); +utils::units::bytes free_disk_space(const fs::path&); +bool is_directory(const fs::path&); +void mkdir(const path&, const int); +void mkdir_p(const path&, const int); +fs::path mkdtemp_public(const std::string&); +fs::path mkstemp(const std::string&); +void mount_tmpfs(const path&); +void mount_tmpfs(const path&, const units::bytes&); +void rm_r(const path&); +void rmdir(const path&); +std::set< directory_entry > scan_directory(const path&); +void unlink(const path&); +void unmount(const path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_OPERATIONS_HPP) diff --git a/utils/fs/operations_test.cpp b/utils/fs/operations_test.cpp new file mode 100644 index 000000000000..f1349351166e --- /dev/null +++ b/utils/fs/operations_test.cpp @@ -0,0 +1,826 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/operations.hpp" + +extern "C" { +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include <dirent.h> +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <stdexcept> +#include <string> +#include <vector> + +#include <atf-c++.hpp> + +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/stream.hpp" +#include "utils/units.hpp" + +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Checks if a directory entry exists and matches a specific type. +/// +/// \param dir The directory in which to look for the entry. +/// \param name The name of the entry to look up. +/// \param expected_type The expected type of the file as given by dir(5). +/// +/// \return True if the entry exists and matches the given type; false +/// otherwise. +static bool +lookup(const char* dir, const char* name, const unsigned int expected_type) +{ + DIR* dirp = ::opendir(dir); + ATF_REQUIRE(dirp != NULL); + + bool found = false; + struct dirent* dp; + while (!found && (dp = readdir(dirp)) != NULL) { + if (std::strcmp(dp->d_name, name) == 0) { + struct ::stat s; + const fs::path lookup_path = fs::path(dir) / name; + ATF_REQUIRE(::stat(lookup_path.c_str(), &s) != -1); + if ((s.st_mode & S_IFMT) == expected_type) { + found = true; + } + } + } + ::closedir(dirp); + return found; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(copy__ok); +ATF_TEST_CASE_BODY(copy__ok) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + atf::utils::create_file(source.str(), "This is the input"); + fs::copy(source, target); + ATF_REQUIRE(atf::utils::compare_file(target.str(), "This is the input")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(copy__fail_open); +ATF_TEST_CASE_BODY(copy__fail_open) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + ATF_REQUIRE_THROW_RE(fs::error, "Cannot open copy source f1.txt", + fs::copy(source, target)); +} + + +ATF_TEST_CASE(copy__fail_create); +ATF_TEST_CASE_HEAD(copy__fail_create) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(copy__fail_create) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + atf::utils::create_file(target.str(), "Do not override"); + ATF_REQUIRE(::chmod(target.c_str(), 0444) != -1); + + atf::utils::create_file(source.str(), "This is the input"); + ATF_REQUIRE_THROW_RE(fs::error, "Cannot create copy target f2.txt", + fs::copy(source, target)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_path__ok); +ATF_TEST_CASE_BODY(current_path__ok) +{ + const fs::path previous = fs::current_path(); + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(::chdir("root") != -1); + const fs::path cwd = fs::current_path(); + ATF_REQUIRE_EQ(cwd.str().length() - 5, cwd.str().find("/root")); + ATF_REQUIRE_EQ(previous / "root", cwd); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_path__enoent); +ATF_TEST_CASE_BODY(current_path__enoent) +{ + const fs::path previous = fs::current_path(); + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(::chdir("root") != -1); + ATF_REQUIRE(::rmdir("../root") != -1); + try { + (void)fs::current_path(); + fail("system_errpr not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists); +ATF_TEST_CASE_BODY(exists) +{ + const fs::path dir("dir"); + ATF_REQUIRE(!fs::exists(dir)); + fs::mkdir(dir, 0755); + ATF_REQUIRE(fs::exists(dir)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__no_path); +ATF_TEST_CASE_BODY(find_in_path__no_path) +{ + utils::unsetenv("PATH"); + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file("ls", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__empty_path); +ATF_TEST_CASE_BODY(find_in_path__empty_path) +{ + utils::setenv("PATH", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file("ls", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__one_component); +ATF_TEST_CASE_BODY(find_in_path__one_component) +{ + const fs::path dir = fs::current_path() / "bin"; + fs::mkdir(dir, 0755); + utils::setenv("PATH", dir.str()); + + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file((dir / "ls").str(), ""); + ATF_REQUIRE_EQ(dir / "ls", fs::find_in_path("ls").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__many_components); +ATF_TEST_CASE_BODY(find_in_path__many_components) +{ + const fs::path dir1 = fs::current_path() / "dir1"; + const fs::path dir2 = fs::current_path() / "dir2"; + fs::mkdir(dir1, 0755); + fs::mkdir(dir2, 0755); + utils::setenv("PATH", dir1.str() + ":" + dir2.str()); + + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file((dir2 / "ls").str(), ""); + ATF_REQUIRE_EQ(dir2 / "ls", fs::find_in_path("ls").get()); + atf::utils::create_file((dir1 / "ls").str(), ""); + ATF_REQUIRE_EQ(dir1 / "ls", fs::find_in_path("ls").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__current_directory); +ATF_TEST_CASE_BODY(find_in_path__current_directory) +{ + utils::setenv("PATH", "bin:"); + + ATF_REQUIRE(!fs::find_in_path("foo-bar")); + atf::utils::create_file("foo-bar", ""); + ATF_REQUIRE_EQ(fs::path("foo-bar").to_absolute(), + fs::find_in_path("foo-bar").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__always_absolute); +ATF_TEST_CASE_BODY(find_in_path__always_absolute) +{ + fs::mkdir(fs::path("my-bin"), 0755); + utils::setenv("PATH", "my-bin"); + + ATF_REQUIRE(!fs::find_in_path("abcd")); + atf::utils::create_file("my-bin/abcd", ""); + ATF_REQUIRE_EQ(fs::path("my-bin/abcd").to_absolute(), + fs::find_in_path("abcd").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(free_disk_space__ok__smoke); +ATF_TEST_CASE_BODY(free_disk_space__ok__smoke) +{ + const units::bytes space = fs::free_disk_space(fs::path(".")); + ATF_REQUIRE(space > units::MB); // Simple test that should always pass. +} + + +/// Unmounts a directory without raising errors. +/// +/// \param cookie Name of a file that exists while the mount point is still +/// mounted. Used to prevent a double-unmount, which would print a +/// misleading error message. +/// \param mount_point Path to the mount point to unmount. +static void +cleanup_mount_point(const fs::path& cookie, const fs::path& mount_point) +{ + try { + if (fs::exists(cookie)) { + fs::unmount(mount_point); + } + } catch (const std::runtime_error& e) { + std::cerr << "Failed trying to unmount " + mount_point.str() + + " during cleanup: " << e.what() << '\n'; + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(free_disk_space__ok__real); +ATF_TEST_CASE_HEAD(free_disk_space__ok__real) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(free_disk_space__ok__real) +{ + try { + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + fs::mount_tmpfs(mount_point, units::bytes(32 * units::MB)); + atf::utils::create_file("mounted", ""); + const units::bytes space = fs::free_disk_space(fs::path(mount_point)); + fs::unmount(mount_point); + fs::unlink(fs::path("mounted")); + ATF_REQUIRE(space < 35 * units::MB); + ATF_REQUIRE(space > 28 * units::MB); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } +} +ATF_TEST_CASE_CLEANUP(free_disk_space__ok__real) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(free_disk_space__fail); +ATF_TEST_CASE_BODY(free_disk_space__fail) +{ + ATF_REQUIRE_THROW_RE(fs::error, "Failed to stat file system for missing", + fs::free_disk_space(fs::path("missing"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_directory__ok); +ATF_TEST_CASE_BODY(is_directory__ok) +{ + const fs::path file("file"); + atf::utils::create_file(file.str(), ""); + ATF_REQUIRE(!fs::is_directory(file)); + + const fs::path dir("dir"); + fs::mkdir(dir, 0755); + ATF_REQUIRE(fs::is_directory(dir)); +} + + +ATF_TEST_CASE_WITH_CLEANUP(is_directory__fail); +ATF_TEST_CASE_HEAD(is_directory__fail) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(is_directory__fail) +{ + fs::mkdir(fs::path("dir"), 0000); + ATF_REQUIRE_THROW(fs::error, fs::is_directory(fs::path("dir/foo"))); +} +ATF_TEST_CASE_CLEANUP(is_directory__fail) +{ + if (::chmod("dir", 0755) == -1) { + // If we cannot restore the original permissions, we cannot do much + // more. However, leaving an unwritable directory behind will cause the + // runtime engine to report us as broken. + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir__ok); +ATF_TEST_CASE_BODY(mkdir__ok) +{ + fs::mkdir(fs::path("dir"), 0755); + ATF_REQUIRE(lookup(".", "dir", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir__enoent); +ATF_TEST_CASE_BODY(mkdir__enoent) +{ + try { + fs::mkdir(fs::path("dir1/dir2"), 0755); + fail("system_error not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); + } + ATF_REQUIRE(!lookup(".", "dir1", S_IFDIR)); + ATF_REQUIRE(!lookup(".", "dir2", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__one_component); +ATF_TEST_CASE_BODY(mkdir_p__one_component) +{ + ATF_REQUIRE(!lookup(".", "new-dir", S_IFDIR)); + fs::mkdir_p(fs::path("new-dir"), 0755); + ATF_REQUIRE(lookup(".", "new-dir", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__many_components); +ATF_TEST_CASE_BODY(mkdir_p__many_components) +{ + ATF_REQUIRE(!lookup(".", "a", S_IFDIR)); + fs::mkdir_p(fs::path("a/b/c"), 0755); + ATF_REQUIRE(lookup(".", "a", S_IFDIR)); + ATF_REQUIRE(lookup("a", "b", S_IFDIR)); + ATF_REQUIRE(lookup("a/b", "c", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__already_exists); +ATF_TEST_CASE_BODY(mkdir_p__already_exists) +{ + fs::mkdir(fs::path("a"), 0755); + fs::mkdir(fs::path("a/b"), 0755); + fs::mkdir_p(fs::path("a/b"), 0755); +} + + +ATF_TEST_CASE(mkdir_p__eacces) +ATF_TEST_CASE_HEAD(mkdir_p__eacces) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(mkdir_p__eacces) +{ + fs::mkdir(fs::path("a"), 0755); + fs::mkdir(fs::path("a/b"), 0755); + ATF_REQUIRE(::chmod("a/b", 0555) != -1); + try { + fs::mkdir_p(fs::path("a/b/c/d"), 0755); + fail("system_error not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(EACCES, e.original_errno()); + } + ATF_REQUIRE(lookup(".", "a", S_IFDIR)); + ATF_REQUIRE(lookup("a", "b", S_IFDIR)); + ATF_REQUIRE(!lookup(".", "c", S_IFDIR)); + ATF_REQUIRE(!lookup("a", "c", S_IFDIR)); + ATF_REQUIRE(!lookup("a/b", "c", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdtemp_public) +ATF_TEST_CASE_BODY(mkdtemp_public) +{ + const fs::path tmpdir = fs::current_path() / "tmp"; + utils::setenv("TMPDIR", tmpdir.str()); + fs::mkdir(tmpdir, 0755); + + const std::string dir_template("tempdir.XXXXXX"); + const fs::path tempdir = fs::mkdtemp_public(dir_template); + ATF_REQUIRE(!lookup("tmp", dir_template.c_str(), S_IFDIR)); + ATF_REQUIRE(lookup("tmp", tempdir.leaf_name().c_str(), S_IFDIR)); +} + + +ATF_TEST_CASE(mkdtemp_public__getcwd_as_non_root) +ATF_TEST_CASE_HEAD(mkdtemp_public__getcwd_as_non_root) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mkdtemp_public__getcwd_as_non_root) +{ + const std::string dir_template("dir.XXXXXX"); + const fs::path dir = fs::mkdtemp_public(dir_template); + const fs::path subdir = dir / "subdir"; + fs::mkdir(subdir, 0755); + + const uid_t old_euid = ::geteuid(); + const gid_t old_egid = ::getegid(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + ATF_REQUIRE(::setegid(unprivileged_user.gid) != -1); + ATF_REQUIRE(::seteuid(unprivileged_user.uid) != -1); + + // The next code block runs as non-root. We cannot use any ATF macros nor + // functions in it because a failure would cause the test to attempt to + // write to the ATF result file which may not be writable as non-root. + bool failed = false; + { + try { + if (::chdir(subdir.c_str()) == -1) { + std::cerr << "Cannot enter directory\n"; + failed |= true; + } else { + fs::current_path(); + } + } catch (const fs::error& e) { + failed |= true; + std::cerr << "Failed to query current path in: " << subdir << '\n'; + } + + if (::seteuid(old_euid) == -1) { + std::cerr << "Failed to restore euid; cannot continue\n"; + std::abort(); + } + if (::setegid(old_egid) == -1) { + std::cerr << "Failed to restore egid; cannot continue\n"; + std::abort(); + } + } + + if (failed) + fail("Test failed; see stdout for details"); +} + + +ATF_TEST_CASE(mkdtemp_public__search_permissions_as_non_root) +ATF_TEST_CASE_HEAD(mkdtemp_public__search_permissions_as_non_root) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mkdtemp_public__search_permissions_as_non_root) +{ + const std::string dir_template("dir.XXXXXX"); + const fs::path dir = fs::mkdtemp_public(dir_template); + const fs::path cookie = dir / "not-secret"; + atf::utils::create_file(cookie.str(), "this is readable"); + + // We are running as root so there is no reason to assume that our current + // work directory is accessible by non-root. Weaken the permissions so that + // our code below works. + ATF_REQUIRE(::chmod(".", 0755) != -1); + + const uid_t old_euid = ::geteuid(); + const gid_t old_egid = ::getegid(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + ATF_REQUIRE(::setegid(unprivileged_user.gid) != -1); + ATF_REQUIRE(::seteuid(unprivileged_user.uid) != -1); + + // The next code block runs as non-root. We cannot use any ATF macros nor + // functions in it because a failure would cause the test to attempt to + // write to the ATF result file which may not be writable as non-root. + bool failed = false; + { + try { + const std::string contents = utils::read_file(cookie); + std::cerr << "Read contents: " << contents << '\n'; + failed |= (contents != "this is readable"); + } catch (const std::runtime_error& e) { + failed |= true; + std::cerr << "Failed to read " << cookie << '\n'; + } + + if (::seteuid(old_euid) == -1) { + std::cerr << "Failed to restore euid; cannot continue\n"; + std::abort(); + } + if (::setegid(old_egid) == -1) { + std::cerr << "Failed to restore egid; cannot continue\n"; + std::abort(); + } + } + + if (failed) + fail("Test failed; see stdout for details"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkstemp) +ATF_TEST_CASE_BODY(mkstemp) +{ + const fs::path tmpdir = fs::current_path() / "tmp"; + utils::setenv("TMPDIR", tmpdir.str()); + fs::mkdir(tmpdir, 0755); + + const std::string file_template("tempfile.XXXXXX"); + const fs::path tempfile = fs::mkstemp(file_template); + ATF_REQUIRE(!lookup("tmp", file_template.c_str(), S_IFREG)); + ATF_REQUIRE(lookup("tmp", tempfile.leaf_name().c_str(), S_IFREG)); +} + + +static void +test_mount_tmpfs_ok(const units::bytes& size) +{ + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + + try { + atf::utils::create_file("outside", ""); + fs::mount_tmpfs(mount_point, size); + atf::utils::create_file("mounted", ""); + atf::utils::create_file((mount_point / "inside").str(), ""); + + struct ::stat outside, inside; + ATF_REQUIRE(::stat("outside", &outside) != -1); + ATF_REQUIRE(::stat((mount_point / "inside").c_str(), &inside) != -1); + ATF_REQUIRE(outside.st_dev != inside.st_dev); + fs::unmount(mount_point); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(mount_tmpfs__ok__default_size) +ATF_TEST_CASE_HEAD(mount_tmpfs__ok__default_size) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__ok__default_size) +{ + test_mount_tmpfs_ok(units::bytes()); +} +ATF_TEST_CASE_CLEANUP(mount_tmpfs__ok__default_size) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE_WITH_CLEANUP(mount_tmpfs__ok__explicit_size) +ATF_TEST_CASE_HEAD(mount_tmpfs__ok__explicit_size) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__ok__explicit_size) +{ + test_mount_tmpfs_ok(units::bytes(10 * units::MB)); +} +ATF_TEST_CASE_CLEANUP(mount_tmpfs__ok__explicit_size) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE(mount_tmpfs__fail) +ATF_TEST_CASE_HEAD(mount_tmpfs__fail) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__fail) +{ + try { + fs::mount_tmpfs(fs::path("non-existent")); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } catch (const fs::error& e) { + // Expected. + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rm_r__empty); +ATF_TEST_CASE_BODY(rm_r__empty) +{ + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(lookup(".", "root", S_IFDIR)); + fs::rm_r(fs::path("root")); + ATF_REQUIRE(!lookup(".", "root", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rm_r__files_and_directories); +ATF_TEST_CASE_BODY(rm_r__files_and_directories) +{ + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/.hidden_file", ""); + fs::mkdir(fs::path("root/.hidden_dir"), 0755); + atf::utils::create_file("root/.hidden_dir/a", ""); + atf::utils::create_file("root/file", ""); + atf::utils::create_file("root/with spaces", ""); + fs::mkdir(fs::path("root/dir1"), 0755); + fs::mkdir(fs::path("root/dir1/dir2"), 0755); + atf::utils::create_file("root/dir1/dir2/file", ""); + fs::mkdir(fs::path("root/dir1/dir3"), 0755); + ATF_REQUIRE(lookup(".", "root", S_IFDIR)); + fs::rm_r(fs::path("root")); + ATF_REQUIRE(!lookup(".", "root", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rmdir__ok) +ATF_TEST_CASE_BODY(rmdir__ok) +{ + ATF_REQUIRE(::mkdir("foo", 0755) != -1); + ATF_REQUIRE(::access("foo", X_OK) == 0); + fs::rmdir(fs::path("foo")); + ATF_REQUIRE(::access("foo", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rmdir__fail) +ATF_TEST_CASE_BODY(rmdir__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "Removal of foo failed", + fs::rmdir(fs::path("foo"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scan_directory__ok) +ATF_TEST_CASE_BODY(scan_directory__ok) +{ + fs::mkdir(fs::path("dir"), 0755); + atf::utils::create_file("dir/foo", ""); + atf::utils::create_file("dir/.hidden", ""); + + const std::set< fs::directory_entry > contents = fs::scan_directory( + fs::path("dir")); + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + exp_contents.insert(fs::directory_entry(".hidden")); + exp_contents.insert(fs::directory_entry("foo")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scan_directory__fail) +ATF_TEST_CASE_BODY(scan_directory__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "opendir(.*missing.*) failed", + fs::scan_directory(fs::path("missing"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlink__ok) +ATF_TEST_CASE_BODY(unlink__ok) +{ + atf::utils::create_file("foo", ""); + ATF_REQUIRE(::access("foo", R_OK) == 0); + fs::unlink(fs::path("foo")); + ATF_REQUIRE(::access("foo", R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlink__fail) +ATF_TEST_CASE_BODY(unlink__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "Removal of foo failed", + fs::unlink(fs::path("foo"))); +} + + +ATF_TEST_CASE(unmount__ok) +ATF_TEST_CASE_HEAD(unmount__ok) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(unmount__ok) +{ + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + + atf::utils::create_file((mount_point / "test1").str(), ""); + try { + fs::mount_tmpfs(mount_point); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } + + atf::utils::create_file((mount_point / "test2").str(), ""); + + ATF_REQUIRE(!fs::exists(mount_point / "test1")); + ATF_REQUIRE( fs::exists(mount_point / "test2")); + fs::unmount(mount_point); + ATF_REQUIRE( fs::exists(mount_point / "test1")); + ATF_REQUIRE(!fs::exists(mount_point / "test2")); +} + + +ATF_TEST_CASE(unmount__fail) +ATF_TEST_CASE_HEAD(unmount__fail) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(unmount__fail) +{ + ATF_REQUIRE_THROW(fs::error, fs::unmount(fs::path("non-existent"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, copy__ok); + ATF_ADD_TEST_CASE(tcs, copy__fail_open); + ATF_ADD_TEST_CASE(tcs, copy__fail_create); + + ATF_ADD_TEST_CASE(tcs, current_path__ok); + ATF_ADD_TEST_CASE(tcs, current_path__enoent); + + ATF_ADD_TEST_CASE(tcs, exists); + + ATF_ADD_TEST_CASE(tcs, find_in_path__no_path); + ATF_ADD_TEST_CASE(tcs, find_in_path__empty_path); + ATF_ADD_TEST_CASE(tcs, find_in_path__one_component); + ATF_ADD_TEST_CASE(tcs, find_in_path__many_components); + ATF_ADD_TEST_CASE(tcs, find_in_path__current_directory); + ATF_ADD_TEST_CASE(tcs, find_in_path__always_absolute); + + ATF_ADD_TEST_CASE(tcs, free_disk_space__ok__smoke); + ATF_ADD_TEST_CASE(tcs, free_disk_space__ok__real); + ATF_ADD_TEST_CASE(tcs, free_disk_space__fail); + + ATF_ADD_TEST_CASE(tcs, is_directory__ok); + ATF_ADD_TEST_CASE(tcs, is_directory__fail); + + ATF_ADD_TEST_CASE(tcs, mkdir__ok); + ATF_ADD_TEST_CASE(tcs, mkdir__enoent); + + ATF_ADD_TEST_CASE(tcs, mkdir_p__one_component); + ATF_ADD_TEST_CASE(tcs, mkdir_p__many_components); + ATF_ADD_TEST_CASE(tcs, mkdir_p__already_exists); + ATF_ADD_TEST_CASE(tcs, mkdir_p__eacces); + + ATF_ADD_TEST_CASE(tcs, mkdtemp_public); + ATF_ADD_TEST_CASE(tcs, mkdtemp_public__getcwd_as_non_root); + ATF_ADD_TEST_CASE(tcs, mkdtemp_public__search_permissions_as_non_root); + + ATF_ADD_TEST_CASE(tcs, mkstemp); + + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__ok__default_size); + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__ok__explicit_size); + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__fail); + + ATF_ADD_TEST_CASE(tcs, rm_r__empty); + ATF_ADD_TEST_CASE(tcs, rm_r__files_and_directories); + + ATF_ADD_TEST_CASE(tcs, rmdir__ok); + ATF_ADD_TEST_CASE(tcs, rmdir__fail); + + ATF_ADD_TEST_CASE(tcs, scan_directory__ok); + ATF_ADD_TEST_CASE(tcs, scan_directory__fail); + + ATF_ADD_TEST_CASE(tcs, unlink__ok); + ATF_ADD_TEST_CASE(tcs, unlink__fail); + + ATF_ADD_TEST_CASE(tcs, unmount__ok); + ATF_ADD_TEST_CASE(tcs, unmount__fail); +} diff --git a/utils/fs/path.cpp b/utils/fs/path.cpp new file mode 100644 index 000000000000..465ed49c4c2a --- /dev/null +++ b/utils/fs/path.cpp @@ -0,0 +1,303 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/path.hpp" + +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Normalizes an input string to a valid path. +/// +/// A normalized path cannot have empty components; i.e. there can be at most +/// one consecutive separator (/). +/// +/// \param in The string to normalize. +/// +/// \return The normalized string, representing a path. +/// +/// \throw utils::fs::invalid_path_error If the path is empty. +static std::string +normalize(const std::string& in) +{ + if (in.empty()) + throw fs::invalid_path_error(in, "Cannot be empty"); + + std::string out; + + std::string::size_type pos = 0; + do { + const std::string::size_type next_pos = in.find('/', pos); + + const std::string component = in.substr(pos, next_pos - pos); + if (!component.empty()) { + if (pos == 0) + out += component; + else if (component != ".") + out += "/" + component; + } + + if (next_pos == std::string::npos) + pos = next_pos; + else + pos = next_pos + 1; + } while (pos != std::string::npos); + + return out.empty() ? "/" : out; +} + + +} // anonymous namespace + + +/// Creates a new path object from a textual representation of a path. +/// +/// \param text A valid representation of a path in textual form. +/// +/// \throw utils::fs::invalid_path_error If the input text does not represent a +/// valid path. +fs::path::path(const std::string& text) : + _repr(normalize(text)) +{ +} + + +/// Gets a view of the path as an array of characters. +/// +/// \return A \code const char* \endcode representation for the object. +const char* +fs::path::c_str(void) const +{ + return _repr.c_str(); +} + + +/// Gets a view of the path as a std::string. +/// +/// \return A \code std::string& \endcode representation for the object. +const std::string& +fs::path::str(void) const +{ + return _repr; +} + + +/// Gets the branch path (directory name) of the path. +/// +/// The branch path of a path with just one component (no separators) is ".". +/// +/// \return A new path representing the branch path. +fs::path +fs::path::branch_path(void) const +{ + const std::string::size_type end_pos = _repr.rfind('/'); + if (end_pos == std::string::npos) + return fs::path("."); + else if (end_pos == 0) + return fs::path("/"); + else + return fs::path(_repr.substr(0, end_pos)); +} + + +/// Gets the leaf name (base name) of the path. +/// +/// \return A new string representing the leaf name. +std::string +fs::path::leaf_name(void) const +{ + const std::string::size_type beg_pos = _repr.rfind('/'); + + if (beg_pos == std::string::npos) + return _repr; + else + return _repr.substr(beg_pos + 1); +} + + +/// Converts a relative path in the current directory to an absolute path. +/// +/// \pre The path is relative. +/// +/// \return The absolute representation of the relative path. +fs::path +fs::path::to_absolute(void) const +{ + PRE(!is_absolute()); + return fs::current_path() / *this; +} + + +/// \return True if the representation of the path is absolute. +bool +fs::path::is_absolute(void) const +{ + return _repr[0] == '/'; +} + + +/// Checks whether the path is a parent of another path. +/// +/// A path is considered to be a parent of itself. +/// +/// \return True if this path is a parent of p. +bool +fs::path::is_parent_of(path p) const +{ + do { + if ((*this) == p) + return true; + p = p.branch_path(); + } while (p != fs::path(".") && p != fs::path("/")); + return false; +} + + +/// Counts the number of components in the path. +/// +/// \return The number of components. +int +fs::path::ncomponents(void) const +{ + int count = 0; + if (_repr == "/") + return 1; + else { + for (std::string::const_iterator iter = _repr.begin(); + iter != _repr.end(); ++iter) { + if (*iter == '/') + count++; + } + return count + 1; + } +} + + +/// Less-than comparator for paths. +/// +/// This is provided to make identifiers useful as map keys. +/// +/// \param p The path to compare to. +/// +/// \return True if this identifier sorts before the other identifier; false +/// otherwise. +bool +fs::path::operator<(const fs::path& p) const +{ + return _repr < p._repr; +} + + +/// Compares two paths for equality. +/// +/// Given that the paths are internally normalized, input paths such as +/// ///foo/bar and /foo///bar are exactly the same. However, this does NOT +/// check for true equality: i.e. this does not access the file system to check +/// if the paths actually point to the same object my means of links. +/// +/// \param p The path to compare to. +/// +/// \returns A boolean indicating whether the paths are equal. +bool +fs::path::operator==(const fs::path& p) const +{ + return _repr == p._repr; +} + + +/// Compares two paths for inequality. +/// +/// See the description of operator==() for more details on the comparison +/// performed. +/// +/// \param p The path to compare to. +/// +/// \returns A boolean indicating whether the paths are different. +bool +fs::path::operator!=(const fs::path& p) const +{ + return _repr != p._repr; +} + + +/// Concatenates this path with one or more components. +/// +/// \param components The new components to concatenate to the path. These are +/// normalized because, in general, they may come from user input. These +/// components cannot represent an absolute path. +/// +/// \return A new path containing the concatenation of this path and the +/// provided components. +/// +/// \throw utils::fs::invalid_path_error If components does not represent a +/// valid path. +/// \throw utils::fs::join_error If the join operation is invalid because the +/// two paths are incompatible. +fs::path +fs::path::operator/(const std::string& components) const +{ + return (*this) / fs::path(components); +} + + +/// Concatenates this path with another path. +/// +/// \param rest The path to concatenate to this one. Cannot be absolute. +/// +/// \return A new path containing the concatenation of this path and the other +/// path. +/// +/// \throw utils::fs::join_error If the join operation is invalid because the +/// two paths are incompatible. +fs::path +fs::path::operator/(const fs::path& rest) const +{ + if (rest.is_absolute()) + throw fs::join_error(_repr, rest._repr, + "Cannot concatenate a path to an absolute path"); + return fs::path(_repr + '/' + rest._repr); +} + + +/// Formats a path for insertion on a stream. +/// +/// \param os The output stream. +/// \param p The path to inject to the stream. +/// +/// \return The output stream os. +std::ostream& +fs::operator<<(std::ostream& os, const fs::path& p) +{ + return (os << p.str()); +} diff --git a/utils/fs/path.hpp b/utils/fs/path.hpp new file mode 100644 index 000000000000..fe55fd55f234 --- /dev/null +++ b/utils/fs/path.hpp @@ -0,0 +1,87 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/path.hpp +/// Provides the utils::fs::path class. +/// +/// This is a poor man's reimplementation of the path class provided by +/// Boost.Filesystem, in the sense that it tries to follow the same API but is +/// much simplified. + +#if !defined(UTILS_FS_PATH_HPP) +#define UTILS_FS_PATH_HPP + +#include "utils/fs/path_fwd.hpp" + +#include <string> +#include <ostream> + +namespace utils { +namespace fs { + + +/// Representation and manipulation of a file system path. +/// +/// Application code should always use this class to represent a path instead of +/// std::string, because this class is more semantically representative, ensures +/// that the values are valid and provides some useful manipulation functions. +/// +/// Conversions to and from strings are always explicit. +class path { + /// Internal representation of the path. + std::string _repr; + +public: + explicit path(const std::string&); + + const char* c_str(void) const; + const std::string& str(void) const; + + path branch_path(void) const; + std::string leaf_name(void) const; + path to_absolute(void) const; + + bool is_absolute(void) const; + bool is_parent_of(path) const; + int ncomponents(void) const; + + bool operator<(const path&) const; + bool operator==(const path&) const; + bool operator!=(const path&) const; + path operator/(const std::string&) const; + path operator/(const path&) const; +}; + + +std::ostream& operator<<(std::ostream&, const path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_PATH_HPP) diff --git a/utils/fs/path_fwd.hpp b/utils/fs/path_fwd.hpp new file mode 100644 index 000000000000..4e6856073553 --- /dev/null +++ b/utils/fs/path_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/fs/path_fwd.hpp +/// Forward declarations for utils/fs/path.hpp + +#if !defined(UTILS_FS_PATH_FWD_HPP) +#define UTILS_FS_PATH_FWD_HPP + +namespace utils { +namespace fs { + + +class path; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_PATH_FWD_HPP) diff --git a/utils/fs/path_test.cpp b/utils/fs/path_test.cpp new file mode 100644 index 000000000000..30ad3110de31 --- /dev/null +++ b/utils/fs/path_test.cpp @@ -0,0 +1,277 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/fs/path.hpp" + +extern "C" { +#include <unistd.h> +} + +#include <set> + +#include <atf-c++.hpp> + +#include "utils/fs/exceptions.hpp" + +using utils::fs::invalid_path_error; +using utils::fs::join_error; +using utils::fs::path; + + +#define REQUIRE_JOIN_ERROR(path1, path2, expr) \ + try { \ + expr; \ + ATF_FAIL("Expecting join_error but no error raised"); \ + } catch (const join_error& e) { \ + ATF_REQUIRE_EQ(path1, e.textual_path1()); \ + ATF_REQUIRE_EQ(path2, e.textual_path2()); \ + } + + +ATF_TEST_CASE_WITHOUT_HEAD(normalize__ok); +ATF_TEST_CASE_BODY(normalize__ok) +{ + ATF_REQUIRE_EQ(".", path(".").str()); + ATF_REQUIRE_EQ("..", path("..").str()); + ATF_REQUIRE_EQ("/", path("/").str()); + ATF_REQUIRE_EQ("/", path("///").str()); + + ATF_REQUIRE_EQ("foo", path("foo").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/bar").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/bar/").str()); + + ATF_REQUIRE_EQ("/foo", path("/foo").str()); + ATF_REQUIRE_EQ("/foo/bar", path("/foo/bar").str()); + ATF_REQUIRE_EQ("/foo/bar", path("/foo/bar/").str()); + + ATF_REQUIRE_EQ("/foo", path("///foo").str()); + ATF_REQUIRE_EQ("/foo/bar", path("///foo///bar").str()); + ATF_REQUIRE_EQ("/foo/bar", path("///foo///bar///").str()); + + ATF_REQUIRE_EQ("./foo/bar", path("./foo/bar").str()); + ATF_REQUIRE_EQ("./foo/bar", path("./foo/./bar").str()); + ATF_REQUIRE_EQ("./foo/bar", path("././foo/./bar").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/././bar").str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(normalize__invalid); +ATF_TEST_CASE_BODY(normalize__invalid) +{ + try { + path(""); + fail("invalid_path_error not raised"); + } catch (const invalid_path_error& e) { + ATF_REQUIRE(e.invalid_path().empty()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute); +ATF_TEST_CASE_BODY(is_absolute) +{ + ATF_REQUIRE( path("/").is_absolute()); + ATF_REQUIRE( path("////").is_absolute()); + ATF_REQUIRE( path("////a").is_absolute()); + ATF_REQUIRE( path("//a//").is_absolute()); + ATF_REQUIRE(!path("a////").is_absolute()); + ATF_REQUIRE(!path("../foo").is_absolute()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_parent_of); +ATF_TEST_CASE_BODY(is_parent_of) +{ + ATF_REQUIRE( path("/").is_parent_of(path("/"))); + ATF_REQUIRE( path(".").is_parent_of(path("."))); + ATF_REQUIRE( path("/a").is_parent_of(path("/a"))); + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE( path("a").is_parent_of(path("a"))); + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c"))); + + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c/d"))); + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c/d/e"))); + ATF_REQUIRE(!path("/a/b/c").is_parent_of(path("a/b/c"))); + ATF_REQUIRE(!path("/a/b/c").is_parent_of(path("a/b/c/d/e"))); + + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c/d"))); + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c/d/e"))); + ATF_REQUIRE(!path("a/b/c").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("a/b/c").is_parent_of(path("/a/b/c/d/e"))); + + ATF_REQUIRE(!path("/a/b/c/d/e").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("/a/b/c/d/e").is_parent_of(path("a/b/c"))); + ATF_REQUIRE(!path("a/b/c/d/e").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("a/b/c/d/e").is_parent_of(path("a/b/c"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ncomponents); +ATF_TEST_CASE_BODY(ncomponents) +{ + ATF_REQUIRE_EQ(1, path(".").ncomponents()); + ATF_REQUIRE_EQ(1, path("/").ncomponents()); + + ATF_REQUIRE_EQ(1, path("abc").ncomponents()); + ATF_REQUIRE_EQ(1, path("abc/").ncomponents()); + + ATF_REQUIRE_EQ(2, path("/abc").ncomponents()); + ATF_REQUIRE_EQ(3, path("/abc/def").ncomponents()); + + ATF_REQUIRE_EQ(2, path("abc/def").ncomponents()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(branch_path); +ATF_TEST_CASE_BODY(branch_path) +{ + ATF_REQUIRE_EQ(".", path(".").branch_path().str()); + ATF_REQUIRE_EQ(".", path("foo").branch_path().str()); + ATF_REQUIRE_EQ("foo", path("foo/bar").branch_path().str()); + ATF_REQUIRE_EQ("/", path("/foo").branch_path().str()); + ATF_REQUIRE_EQ("/foo", path("/foo/bar").branch_path().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(leaf_name); +ATF_TEST_CASE_BODY(leaf_name) +{ + ATF_REQUIRE_EQ(".", path(".").leaf_name()); + ATF_REQUIRE_EQ("foo", path("foo").leaf_name()); + ATF_REQUIRE_EQ("bar", path("foo/bar").leaf_name()); + ATF_REQUIRE_EQ("foo", path("/foo").leaf_name()); + ATF_REQUIRE_EQ("bar", path("/foo/bar").leaf_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_absolute); +ATF_TEST_CASE_BODY(to_absolute) +{ + ATF_REQUIRE(::chdir("/bin") != -1); + const std::string absolute = path("ls").to_absolute().str(); + // In some systems (e.g. in Fedora 17), /bin is really a symlink to + // /usr/bin. Doing an explicit match of 'absolute' to /bin/ls fails in such + // case. Instead, attempt doing a search in the generated path just for a + // substring containing '/bin/ls'. Note that this can still fail if /bin is + // linked to something arbitrary like /a/b... but let's just assume this + // does not happen. + ATF_REQUIRE(absolute.find("/bin/ls") != std::string::npos); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_less_than); +ATF_TEST_CASE_BODY(compare_less_than) +{ + ATF_REQUIRE(!(path("/") < path("/"))); + ATF_REQUIRE(!(path("/") < path("///"))); + + ATF_REQUIRE(!(path("/a/b/c") < path("/a/b/c"))); + + ATF_REQUIRE( path("/a") < path("/b")); + ATF_REQUIRE(!(path("/b") < path("/a"))); + + ATF_REQUIRE( path("/a") < path("/aa")); + ATF_REQUIRE(!(path("/aa") < path("/a"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_equal); +ATF_TEST_CASE_BODY(compare_equal) +{ + ATF_REQUIRE(path("/") == path("///")); + ATF_REQUIRE(path("/a") == path("///a")); + ATF_REQUIRE(path("/a") == path("///a///")); + + ATF_REQUIRE(path("a/b/c") == path("a//b//c")); + ATF_REQUIRE(path("a/b/c") == path("a//b//c///")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_different); +ATF_TEST_CASE_BODY(compare_different) +{ + ATF_REQUIRE(path("/") != path("//a/")); + ATF_REQUIRE(path("/a") != path("a///")); + + ATF_REQUIRE(path("a/b/c") != path("a/b")); + ATF_REQUIRE(path("a/b/c") != path("a//b")); + ATF_REQUIRE(path("a/b/c") != path("/a/b/c")); + ATF_REQUIRE(path("a/b/c") != path("/a//b//c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(concat__to_string); +ATF_TEST_CASE_BODY(concat__to_string) +{ + ATF_REQUIRE_EQ("foo/bar", (path("foo") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar", (path("foo/") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar/baz", (path("foo/") / "bar//baz///").str()); + + ATF_REQUIRE_THROW(invalid_path_error, path("foo") / ""); + REQUIRE_JOIN_ERROR("foo", "/a/b", path("foo") / "/a/b"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(concat__to_path); +ATF_TEST_CASE_BODY(concat__to_path) +{ + ATF_REQUIRE_EQ("foo/bar", (path("foo") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar", (path("foo/") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar/baz", (path("foo/") / "bar//baz///").str()); + + REQUIRE_JOIN_ERROR("foo", "/a/b", path("foo") / path("/a/b")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(use_as_key); +ATF_TEST_CASE_BODY(use_as_key) +{ + std::set< path > paths; + paths.insert(path("/a")); + ATF_REQUIRE(paths.find(path("//a")) != paths.end()); + ATF_REQUIRE(paths.find(path("a")) == paths.end()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, normalize__ok); + ATF_ADD_TEST_CASE(tcs, normalize__invalid); + ATF_ADD_TEST_CASE(tcs, is_absolute); + ATF_ADD_TEST_CASE(tcs, is_parent_of); + ATF_ADD_TEST_CASE(tcs, ncomponents); + ATF_ADD_TEST_CASE(tcs, branch_path); + ATF_ADD_TEST_CASE(tcs, leaf_name); + ATF_ADD_TEST_CASE(tcs, to_absolute); + ATF_ADD_TEST_CASE(tcs, compare_less_than); + ATF_ADD_TEST_CASE(tcs, compare_equal); + ATF_ADD_TEST_CASE(tcs, compare_different); + ATF_ADD_TEST_CASE(tcs, concat__to_string); + ATF_ADD_TEST_CASE(tcs, concat__to_path); + ATF_ADD_TEST_CASE(tcs, use_as_key); +} diff --git a/utils/logging/Kyuafile b/utils/logging/Kyuafile new file mode 100644 index 000000000000..0853a335c6ae --- /dev/null +++ b/utils/logging/Kyuafile @@ -0,0 +1,6 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="macros_test"} +atf_test_program{name="operations_test"} diff --git a/utils/logging/Makefile.am.inc b/utils/logging/Makefile.am.inc new file mode 100644 index 000000000000..7d88f16859d7 --- /dev/null +++ b/utils/logging/Makefile.am.inc @@ -0,0 +1,53 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +UTILS_CFLAGS += $(LUTOK_CFLAGS) +UTILS_LIBS += $(LUTOK_LIBS) + +libutils_a_CPPFLAGS += $(LUTOK_CFLAGS) +libutils_a_SOURCES += utils/logging/macros.hpp +libutils_a_SOURCES += utils/logging/operations.cpp +libutils_a_SOURCES += utils/logging/operations.hpp +libutils_a_SOURCES += utils/logging/operations_fwd.hpp + +if WITH_ATF +tests_utils_loggingdir = $(pkgtestsdir)/utils/logging + +tests_utils_logging_DATA = utils/logging/Kyuafile +EXTRA_DIST += $(tests_utils_logging_DATA) + +tests_utils_logging_PROGRAMS = utils/logging/macros_test +utils_logging_macros_test_SOURCES = utils/logging/macros_test.cpp +utils_logging_macros_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_logging_macros_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_logging_PROGRAMS += utils/logging/operations_test +utils_logging_operations_test_SOURCES = utils/logging/operations_test.cpp +utils_logging_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_logging_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/logging/macros.hpp b/utils/logging/macros.hpp new file mode 100644 index 000000000000..73dd0a60ef87 --- /dev/null +++ b/utils/logging/macros.hpp @@ -0,0 +1,68 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/logging/macros.hpp +/// Convenience macros to simplify usage of the logging library. +/// +/// This file <em>must not be included from other header files</em>. + +#if !defined(UTILS_LOGGING_MACROS_HPP) +#define UTILS_LOGGING_MACROS_HPP + +#include "utils/logging/operations.hpp" + + +/// Logs a debug message. +/// +/// \param message The message to log. +#define LD(message) utils::logging::log(utils::logging::level_debug, \ + __FILE__, __LINE__, message) + + +/// Logs an error message. +/// +/// \param message The message to log. +#define LE(message) utils::logging::log(utils::logging::level_error, \ + __FILE__, __LINE__, message) + + +/// Logs an informational message. +/// +/// \param message The message to log. +#define LI(message) utils::logging::log(utils::logging::level_info, \ + __FILE__, __LINE__, message) + + +/// Logs a warning message. +/// +/// \param message The message to log. +#define LW(message) utils::logging::log(utils::logging::level_warning, \ + __FILE__, __LINE__, message) + + +#endif // !defined(UTILS_LOGGING_MACROS_HPP) diff --git a/utils/logging/macros_test.cpp b/utils/logging/macros_test.cpp new file mode 100644 index 000000000000..fe3ee63cd533 --- /dev/null +++ b/utils/logging/macros_test.cpp @@ -0,0 +1,115 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/logging/macros.hpp" + +#include <fstream> +#include <string> + +#include <atf-c++.hpp> + +#include "utils/datetime.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; + + +ATF_TEST_CASE_WITHOUT_HEAD(ld); +ATF_TEST_CASE_BODY(ld) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LD("Debug message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 D .*: Debug message", line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(le); +ATF_TEST_CASE_BODY(le) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LE("Error message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 E .*: Error message", line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(li); +ATF_TEST_CASE_BODY(li) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LI("Info message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 I .*: Info message", line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lw); +ATF_TEST_CASE_BODY(lw) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LW("Warning message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 W .*: Warning message", line); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ld); + ATF_ADD_TEST_CASE(tcs, le); + ATF_ADD_TEST_CASE(tcs, li); + ATF_ADD_TEST_CASE(tcs, lw); +} diff --git a/utils/logging/operations.cpp b/utils/logging/operations.cpp new file mode 100644 index 000000000000..88f25361fa18 --- /dev/null +++ b/utils/logging/operations.cpp @@ -0,0 +1,303 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/logging/operations.hpp" + +extern "C" { +#include <unistd.h> +} + +#include <stdexcept> +#include <string> +#include <utility> +#include <vector> + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; + +using utils::none; +using utils::optional; + + +/// The general idea for the application-wide logging goes like this: +/// +/// 1. The application starts. Logging is initialized to capture _all_ log +/// messages into memory regardless of their level by issuing a call to the +/// set_inmemory() function. +/// +/// 2. The application offers the user a way to select the logging level and a +/// file into which to store the log. +/// +/// 3. The application calls set_persistency providing a new log level and a log +/// file. This must be done as early as possible, to minimize the chances of an +/// early crash not capturing any logs. +/// +/// 4. At this point, any log messages stored into memory are flushed to disk +/// respecting the provided log level. +/// +/// 5. The internal state of the logging module is updated to only capture +/// messages that are of the provided log level (or below) and is configured to +/// directly send messages to disk. +/// +/// 6. The user may choose to call set_inmemory() again at a later stage, which +/// will cause the log to be flushed and messages to be recorded in memory +/// again. This is useful in case the logs are being sent to either stdout or +/// stderr and the process forks and wants to keep those child channels +/// unpolluted. +/// +/// The call to set_inmemory() should only be performed by the user-facing +/// application. Tests should skip this call so that the logging messages go to +/// stderr by default, thus generating a useful log to debug the tests. + + +namespace { + + +/// Constant string to strftime to format timestamps. +static const char* timestamp_format = "%Y%m%d-%H%M%S"; + + +/// Mutable global state. +struct global_state { + /// Current log level. + logging::level log_level; + + /// Indicates whether set_persistency() will be called automatically or not. + bool auto_set_persistency; + + /// First time recorded by the logging module. + optional< datetime::timestamp > first_timestamp; + + /// In-memory record of log entries before persistency is enabled. + std::vector< std::pair< logging::level, std::string > > backlog; + + /// Stream to the currently open log file. + std::auto_ptr< std::ostream > logfile; + + global_state() : + log_level(logging::level_debug), + auto_set_persistency(true) + { + } +}; + + +/// Single instance of the mutable global state. +/// +/// Note that this is a raw pointer that we intentionally leak. We must do +/// this, instead of making all of the singleton's members static values, +/// because we want other destructors in the program to be able to log critical +/// conditions. If we use complex types in this translation unit, they may be +/// destroyed before the logging methods in the destructors get a chance to run +/// thus resulting in a premature crash. By using a plain pointer, we ensure +/// this state never gets cleaned up. +static struct global_state* globals_singleton = NULL; + + +/// Gets the singleton instance of global_state. +/// +/// \return A pointer to the unique global_state instance. +static struct global_state* +get_globals(void) +{ + if (globals_singleton == NULL) { + globals_singleton = new global_state(); + } + return globals_singleton; +} + + +/// Converts a level to a printable character. +/// +/// \param level The level to convert. +/// +/// \return The printable character, to be used in log messages. +static char +level_to_char(const logging::level level) +{ + switch (level) { + case logging::level_error: return 'E'; + case logging::level_warning: return 'W'; + case logging::level_info: return 'I'; + case logging::level_debug: return 'D'; + default: UNREACHABLE; + } +} + + +} // anonymous namespace + + +/// Generates a standard log name. +/// +/// This always adds the same timestamp to the log name for a particular run. +/// Also, the timestamp added to the file name corresponds to the first +/// timestamp recorded by the module; it does not necessarily contain the +/// current value of "now". +/// +/// \param logdir The path to the directory in which to place the log. +/// \param progname The name of the program that is generating the log. +/// +/// \return A string representation of the log name based on \p logdir and +/// \p progname. +fs::path +logging::generate_log_name(const fs::path& logdir, const std::string& progname) +{ + struct global_state* globals = get_globals(); + + if (!globals->first_timestamp) + globals->first_timestamp = datetime::timestamp::now(); + // Update kyua(1) if you change the name format. + return logdir / (F("%s.%s.log") % progname % + globals->first_timestamp.get().strftime(timestamp_format)); +} + + +/// Logs an entry to the log file. +/// +/// If the log is not yet set to persistent mode, the entry is recorded in the +/// in-memory backlog. Otherwise, it is just written to disk. +/// +/// \param message_level The level of the entry. +/// \param file The file from which the log message is generated. +/// \param line The line from which the log message is generated. +/// \param user_message The raw message to store. +void +logging::log(const level message_level, const char* file, const int line, + const std::string& user_message) +{ + struct global_state* globals = get_globals(); + + const datetime::timestamp now = datetime::timestamp::now(); + if (!globals->first_timestamp) + globals->first_timestamp = now; + + if (globals->auto_set_persistency) { + // These values are hardcoded here for testing purposes. The + // application should call set_inmemory() by itself during + // initialization to avoid this, so that it has explicit control on how + // the call to set_persistency() happens. + set_persistency("debug", fs::path("/dev/stderr")); + globals->auto_set_persistency = false; + } + + if (message_level > globals->log_level) + return; + + // Update doc/troubleshooting.texi if you change the log format. + const std::string message = F("%s %s %s %s:%s: %s") % + now.strftime(timestamp_format) % level_to_char(message_level) % + ::getpid() % file % line % user_message; + if (globals->logfile.get() == NULL) + globals->backlog.push_back(std::make_pair(message_level, message)); + else { + INV(globals->backlog.empty()); + (*globals->logfile) << message << '\n'; + globals->logfile->flush(); + } +} + + +/// Sets the logging to record messages in memory for later flushing. +/// +/// Can be called after set_persistency to flush logs and set recording to be +/// in-memory again. +void +logging::set_inmemory(void) +{ + struct global_state* globals = get_globals(); + + globals->auto_set_persistency = false; + + if (globals->logfile.get() != NULL) { + INV(globals->backlog.empty()); + globals->logfile->flush(); + globals->logfile.reset(NULL); + } +} + + +/// Makes the log persistent. +/// +/// Calling this function flushes the in-memory log, if any, to disk and sets +/// the logging module to send log entries to disk from this point onwards. +/// There is no way back, and the caller program should execute this function as +/// early as possible to ensure that a crash at startup does not discard too +/// many useful log entries. +/// +/// Any log entries above the provided new_level are discarded. +/// +/// \param new_level The new log level. +/// \param path The file to write the logs to. +/// +/// \throw std::range_error If the given log level is invalid. +/// \throw std::runtime_error If the given file cannot be created. +void +logging::set_persistency(const std::string& new_level, const fs::path& path) +{ + struct global_state* globals = get_globals(); + + globals->auto_set_persistency = false; + + PRE(globals->logfile.get() == NULL); + + // Update doc/troubleshooting.info if you change the log levels. + if (new_level == "debug") + globals->log_level = level_debug; + else if (new_level == "error") + globals->log_level = level_error; + else if (new_level == "info") + globals->log_level = level_info; + else if (new_level == "warning") + globals->log_level = level_warning; + else + throw std::range_error(F("Unrecognized log level '%s'") % new_level); + + try { + globals->logfile = utils::open_ostream(path); + } catch (const std::runtime_error& unused_error) { + throw std::runtime_error(F("Failed to create log file %s") % path); + } + + for (std::vector< std::pair< logging::level, std::string > >::const_iterator + iter = globals->backlog.begin(); iter != globals->backlog.end(); + ++iter) { + if ((*iter).first <= globals->log_level) + (*globals->logfile) << (*iter).second << '\n'; + } + globals->logfile->flush(); + globals->backlog.clear(); +} diff --git a/utils/logging/operations.hpp b/utils/logging/operations.hpp new file mode 100644 index 000000000000..1bb72219dcae --- /dev/null +++ b/utils/logging/operations.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/logging/operations.hpp +/// Stateless logging facilities. + +#if !defined(UTILS_LOGGING_OPERATIONS_HPP) +#define UTILS_LOGGING_OPERATIONS_HPP + +#include "utils/logging/operations_fwd.hpp" + +#include <string> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace logging { + + +fs::path generate_log_name(const fs::path&, const std::string&); +void log(const level, const char*, const int, const std::string&); +void set_inmemory(void); +void set_persistency(const std::string&, const fs::path&); + + +} // namespace logging +} // namespace utils + +#endif // !defined(UTILS_LOGGING_OPERATIONS_HPP) diff --git a/utils/logging/operations_fwd.hpp b/utils/logging/operations_fwd.hpp new file mode 100644 index 000000000000..0e3edd7993ec --- /dev/null +++ b/utils/logging/operations_fwd.hpp @@ -0,0 +1,54 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/logging/operations_fwd.hpp +/// Forward declarations for utils/logging/operations.hpp + +#if !defined(UTILS_LOGGING_OPERATIONS_FWD_HPP) +#define UTILS_LOGGING_OPERATIONS_FWD_HPP + +namespace utils { +namespace logging { + + +/// Severity levels for log messages. +/// +/// This enumeration must be sorted from the most severe message to the least +/// severe. +enum level { + level_error = 0, + level_warning, + level_info, + level_debug, +}; + + +} // namespace logging +} // namespace utils + +#endif // !defined(UTILS_LOGGING_OPERATIONS_FWD_HPP) diff --git a/utils/logging/operations_test.cpp b/utils/logging/operations_test.cpp new file mode 100644 index 000000000000..402f36e62904 --- /dev/null +++ b/utils/logging/operations_test.cpp @@ -0,0 +1,354 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/logging/operations.hpp" + +extern "C" { +#include <unistd.h> +} + +#include <fstream> +#include <string> + +#include <atf-c++.hpp> + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_log_name__before_log); +ATF_TEST_CASE_BODY(generate_log_name__before_log) +{ + datetime::set_mock_now(2011, 2, 21, 18, 10, 0, 0); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181000.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 1, 987654); + logging::log(logging::level_info, "file", 123, "A message"); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 2, 123); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181000.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_log_name__after_log); +ATF_TEST_CASE_BODY(generate_log_name__after_log) +{ + datetime::set_mock_now(2011, 2, 21, 18, 15, 0, 0); + logging::log(logging::level_info, "file", 123, "A message"); + datetime::set_mock_now(2011, 2, 21, 18, 15, 1, 987654); + logging::log(logging::level_info, "file", 123, "A message"); + + datetime::set_mock_now(2011, 2, 21, 18, 15, 2, 123); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181500.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); + + datetime::set_mock_now(2011, 2, 21, 18, 15, 3, 1); + logging::log(logging::level_info, "file", 123, "A message"); + + datetime::set_mock_now(2011, 2, 21, 18, 15, 4, 91); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181500.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(log); +ATF_TEST_CASE_BODY(log) +{ + logging::set_inmemory(); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 0, 0); + logging::log(logging::level_debug, "f1", 1, "Debug message"); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 1, 987654); + logging::log(logging::level_error, "f2", 2, "Error message"); + + logging::set_persistency("debug", fs::path("test.log")); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 2, 123); + logging::log(logging::level_info, "f3", 3, "Info message"); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 3, 456); + logging::log(logging::level_warning, "f4", 4, "Warning message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181000 D %s f1:1: Debug message") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181001 E %s f2:2: Error message") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181002 I %s f3:3: Info message") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181003 W %s f4:4: Warning message") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_inmemory__reset); +ATF_TEST_CASE_BODY(set_inmemory__reset) +{ + logging::set_persistency("debug", fs::path("test.log")); + + datetime::set_mock_now(2011, 2, 21, 18, 20, 0, 654321); + logging::log(logging::level_debug, "file", 123, "Debug message"); + logging::set_inmemory(); + logging::log(logging::level_debug, "file", 123, "Debug message 2"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-182000 D %s file:123: Debug message") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__no_backlog); +ATF_TEST_CASE_BODY(set_persistency__no_backlog) +{ + logging::set_persistency("debug", fs::path("test.log")); + + datetime::set_mock_now(2011, 2, 21, 18, 20, 0, 654321); + logging::log(logging::level_debug, "file", 123, "Debug message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-182000 D %s file:123: Debug message") % pid).str(), line); +} + + +/// Creates a log for testing purposes, buffering messages on start. +/// +/// \param level The level of the desired log. +/// \param path The output file. +static void +create_log(const std::string& level, const std::string& path) +{ + logging::set_inmemory(); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 0, 100); + logging::log(logging::level_debug, "file1", 11, "Debug 1"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 1, 200); + logging::log(logging::level_error, "file2", 22, "Error 1"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 2, 300); + logging::log(logging::level_info, "file3", 33, "Info 1"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 3, 400); + logging::log(logging::level_warning, "file4", 44, "Warning 1"); + + logging::set_persistency(level, fs::path(path)); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 4, 500); + logging::log(logging::level_debug, "file1", 11, "Debug 2"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 5, 600); + logging::log(logging::level_error, "file2", 22, "Error 2"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 6, 700); + logging::log(logging::level_info, "file3", 33, "Info 2"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 7, 800); + logging::log(logging::level_warning, "file4", 44, "Warning 2"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__debug); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__debug) +{ + create_log("debug", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114000 D %s file1:11: Debug 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114002 I %s file3:33: Info 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114003 W %s file4:44: Warning 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114004 D %s file1:11: Debug 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114006 I %s file3:33: Info 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114007 W %s file4:44: Warning 2") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__error); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__error) +{ + create_log("error", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__info); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__info) +{ + create_log("info", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114002 I %s file3:33: Info 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114003 W %s file4:44: Warning 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114006 I %s file3:33: Info 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114007 W %s file4:44: Warning 2") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__warning); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__warning) +{ + create_log("warning", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114003 W %s file4:44: Warning 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114007 W %s file4:44: Warning 2") % pid).str(), line); +} + + +ATF_TEST_CASE(set_persistency__fail); +ATF_TEST_CASE_HEAD(set_persistency__fail) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(set_persistency__fail) +{ + ATF_REQUIRE_THROW_RE(std::range_error, "'foobar'", + logging::set_persistency("foobar", fs::path("log"))); + + fs::mkdir(fs::path("dir"), 0644); + ATF_REQUIRE_THROW_RE(std::runtime_error, "dir/fail.log", + logging::set_persistency("debug", + fs::path("dir/fail.log"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, generate_log_name__before_log); + ATF_ADD_TEST_CASE(tcs, generate_log_name__after_log); + + ATF_ADD_TEST_CASE(tcs, log); + + ATF_ADD_TEST_CASE(tcs, set_inmemory__reset); + + ATF_ADD_TEST_CASE(tcs, set_persistency__no_backlog); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__debug); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__error); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__info); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__warning); + ATF_ADD_TEST_CASE(tcs, set_persistency__fail); +} diff --git a/utils/memory.cpp b/utils/memory.cpp new file mode 100644 index 000000000000..ca121f6f4dec --- /dev/null +++ b/utils/memory.cpp @@ -0,0 +1,158 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/memory.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#if defined(HAVE_SYS_TYPES_H) +# include <sys/types.h> +#endif +#if defined(HAVE_SYS_PARAM_H) +# include <sys/param.h> +#endif +#if defined(HAVE_SYS_SYSCTL_H) +# include <sys/sysctl.h> +#endif +} + +#include <cerrno> +#include <cstddef> +#include <cstring> +#include <stdexcept> + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/units.hpp" +#include "utils/sanity.hpp" + +namespace units = utils::units; + + +namespace { + + +/// Name of the method to query the available memory as detected by configure. +static const char* query_type = MEMORY_QUERY_TYPE; + + +/// Value of query_type when we do not know how to query the memory. +static const char* query_type_unknown = "unknown"; + + +/// Value of query_type when we have to use sysctlbyname(3). +static const char* query_type_sysctlbyname = "sysctlbyname"; + + +/// Name of the sysctl MIB with the physical memory as detected by configure. +/// +/// This should only be used if memory_query_type is 'sysctl'. +static const char* query_sysctl_mib = MEMORY_QUERY_SYSCTL_MIB; + + +#if !defined(HAVE_SYSCTLBYNAME) +/// Stub for sysctlbyname(3) for systems that don't have it. +/// +/// The whole purpose of this fake function is to allow the caller code to be +/// compiled on any machine regardless of the presence of sysctlbyname(3). This +/// will prevent the code from breaking when it is compiled on a machine without +/// this function. It also prevents "unused variable" warnings in the caller +/// code. +/// +/// \return Nothing; this always crashes. +static int +sysctlbyname(const char* /* name */, + void* /* oldp */, + std::size_t* /* oldlenp */, + const void* /* newp */, + std::size_t /* newlen */) +{ + UNREACHABLE; +} +#endif + + +} // anonymous namespace + + +/// Gets the value of an integral sysctl MIB. +/// +/// \pre The system supports the sysctlbyname(3) function. +/// +/// \param mib The name of the sysctl MIB to query. +/// +/// \return The value of the MIB, if found. +/// +/// \throw std::runtime_error If the sysctlbyname(3) call fails. This might be +/// a bit drastic. If it turns out that this causes problems, we could just +/// change the code to log the error instead of raising an exception. +static int64_t +query_sysctl(const char* mib) +{ + // This must be explicitly initialized to 0. If the sysctl query returned a + // value smaller in size than value_length, we would get garbage otherwise. + int64_t value = 0; + std::size_t value_length = sizeof(value); + if (::sysctlbyname(mib, &value, &value_length, NULL, 0) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to get sysctl(%s) value: %s") % + mib % std::strerror(original_errno)); + } + return value; +} + + +/// Queries the total amount of physical memory. +/// +/// The real query is run only once and the result is cached. Further calls to +/// this function will always return the same value. +/// +/// \return The amount of physical memory, in bytes. If the code does not know +/// how to query the memory, this logs a warning and returns 0. +units::bytes +utils::physical_memory(void) +{ + static int64_t amount = -1; + if (amount == -1) { + if (std::strcmp(query_type, query_type_unknown) == 0) { + LW("Don't know how to query the physical memory"); + amount = 0; + } else if (std::strcmp(query_type, query_type_sysctlbyname) == 0) { + amount = query_sysctl(query_sysctl_mib); + } else + UNREACHABLE_MSG("Unimplemented memory query type"); + LI(F("Physical memory as returned by query type '%s': %s") % + query_type % amount); + } + POST(amount > -1); + return units::bytes(amount); +} diff --git a/utils/memory.hpp b/utils/memory.hpp new file mode 100644 index 000000000000..5a956a82005a --- /dev/null +++ b/utils/memory.hpp @@ -0,0 +1,45 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/memory.hpp +/// Utilities to query details of the system memory. + +#if !defined(UTILS_MEMORY_HPP) +#define UTILS_MEMORY_HPP + +#include "utils/units_fwd.hpp" + +namespace utils { + + +units::bytes physical_memory(void); + + +} // namespace utils + +#endif // !defined(UTILS_MEMORY_HPP) diff --git a/utils/memory_test.cpp b/utils/memory_test.cpp new file mode 100644 index 000000000000..66750fbe9c6c --- /dev/null +++ b/utils/memory_test.cpp @@ -0,0 +1,63 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include "utils/memory.hpp" + +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/units.hpp" + +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(physical_memory); +ATF_TEST_CASE_BODY(physical_memory) +{ + const units::bytes memory = utils::physical_memory(); + + if (std::strcmp(MEMORY_QUERY_TYPE, "unknown") == 0) { + ATF_REQUIRE(memory == 0); + } else if (std::strcmp(MEMORY_QUERY_TYPE, "sysctlbyname") == 0) { + ATF_REQUIRE(memory > 0); + ATF_REQUIRE(memory < 100 * units::TB); // Large enough for now... + } else { + fail("Unimplemented memory query type"); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, physical_memory); +} diff --git a/utils/noncopyable.hpp b/utils/noncopyable.hpp new file mode 100644 index 000000000000..6a0ad6bf713a --- /dev/null +++ b/utils/noncopyable.hpp @@ -0,0 +1,75 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/noncopyable.hpp +/// Provides the utils::noncopyable class. +/// +/// The class is provided as a separate module on its own to minimize +/// header-inclusion side-effects. + +#if !defined(UTILS_NONCOPYABLE_HPP) +#define UTILS_NONCOPYABLE_HPP + + +namespace utils { + + +/// Forbids copying a class at compile-time. +/// +/// Inheriting from this class delivers a private copy constructor and an +/// assignment operator that effectively forbid copying the class during +/// compilation. +/// +/// Always use private inheritance. +class noncopyable { + /// Data placeholder. + /// + /// The class cannot be empty; otherwise we get ABI-stability warnings + /// during the build, which will break it due to strict checking. + int _noncopyable_dummy; + + /// Private copy constructor to deny copying of subclasses. + noncopyable(const noncopyable&); + + /// Private assignment constructor to deny copying of subclasses. + /// + /// \return A reference to the object. + noncopyable& operator=(const noncopyable&); + +protected: + // Explicitly needed to provide some non-private functions. Otherwise + // we also get some warnings during the build. + noncopyable(void) {} + ~noncopyable(void) {} +}; + + +} // namespace utils + + +#endif // !defined(UTILS_NONCOPYABLE_HPP) diff --git a/utils/optional.hpp b/utils/optional.hpp new file mode 100644 index 000000000000..a4557bff5dc8 --- /dev/null +++ b/utils/optional.hpp @@ -0,0 +1,90 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/optional.hpp +/// Provides the utils::optional class. +/// +/// The class is provided as a separate module on its own to minimize +/// header-inclusion side-effects. + +#if !defined(UTILS_OPTIONAL_HPP) +#define UTILS_OPTIONAL_HPP + +#include "utils/optional_fwd.hpp" + +#include <ostream> + +namespace utils { + + +/// Holds a data value or none. +/// +/// This class allows users to represent values that may be uninitialized. +/// Instead of having to keep separate variables to track whether a variable is +/// supposed to have a value or not, this class allows multiplexing the +/// behaviors. +/// +/// This class is a simplified version of Boost.Optional. +template< class T > +class optional { + /// Internal representation of the optional data value. + T* _data; + +public: + optional(void); + optional(utils::detail::none_t); + optional(const optional< T >&); + explicit optional(const T&); + ~optional(void); + + optional& operator=(utils::detail::none_t); + optional& operator=(const T&); + optional& operator=(const optional< T >&); + + bool operator==(const optional< T >&) const; + bool operator!=(const optional< T >&) const; + + operator bool(void) const; + + const T& get(void) const; + const T& get_default(const T&) const; + T& get(void); +}; + + +template< class T > +std::ostream& operator<<(std::ostream&, const optional< T >&); + + +template< class T > +optional< T > make_optional(const T&); + + +} // namespace utils + +#endif // !defined(UTILS_OPTIONAL_HPP) diff --git a/utils/optional.ipp b/utils/optional.ipp new file mode 100644 index 000000000000..3e2f3f878f2a --- /dev/null +++ b/utils/optional.ipp @@ -0,0 +1,252 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_OPTIONAL_IPP) +#define UTILS_OPTIONAL_IPP + +#include <cstddef> + +#include "utils/defs.hpp" +#include "utils/optional.hpp" +#include "utils/sanity.hpp" + + +/// Initializes an optional object to the none value. +template< class T > +utils::optional< T >::optional(void) : + _data(NULL) +{ +} + + +/// Explicitly initializes an optional object to the none value. +template< class T > +utils::optional< T >::optional(utils::detail::none_t /* none */) : + _data(NULL) +{ +} + + +/// Initializes an optional object to a non-none value. +/// +/// \param data The initial value for the object. +template< class T > +utils::optional< T >::optional(const T& data) : + _data(new T(data)) +{ +} + + +/// Copy constructor. +/// +/// \param other The optional object to copy from. +template< class T > +utils::optional< T >::optional(const optional< T >& other) : + _data(other._data == NULL ? NULL : new T(*(other._data))) +{ +} + + +/// Destructor. +template< class T > +utils::optional< T >::~optional(void) +{ + if (_data != NULL) + delete _data; + _data = NULL; // Prevent accidental reuse. +} + + +/// Explicitly assigns an optional object to the none value. +/// +/// \return A reference to this. +template< class T > +utils::optional< T >& +utils::optional< T >::operator=(utils::detail::none_t /* none */) +{ + if (_data != NULL) + delete _data; + _data = NULL; + return *this; +} + + +/// Assigns a new value to the optional object. +/// +/// \param data The initial value for the object. +/// +/// \return A reference to this. +template< class T > +utils::optional< T >& +utils::optional< T >::operator=(const T& data) +{ + T* new_data = new T(data); + if (_data != NULL) + delete _data; + _data = new_data; + return *this; +} + + +/// Copies an optional value. +/// +/// \param other The optional object to copy from. +/// +/// \return A reference to this. +template< class T > +utils::optional< T >& +utils::optional< T >::operator=(const optional< T >& other) +{ + T* new_data = other._data == NULL ? NULL : new T(*(other._data)); + if (_data != NULL) + delete _data; + _data = new_data; + return *this; +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +template< class T > +bool +utils::optional< T >::operator==(const optional< T >& other) const +{ + if (_data == NULL && other._data == NULL) { + return true; + } else if (_data == NULL || other._data == NULL) { + return false; + } else { + INV(_data != NULL && other._data != NULL); + return *_data == *other._data; + } +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +template< class T > +bool +utils::optional< T >::operator!=(const optional< T >& other) const +{ + return !(*this == other); +} + + +/// Gets the value hold by the optional object. +/// +/// \pre The optional object must not be none. +/// +/// \return A reference to the data. +template< class T > +const T& +utils::optional< T >::get(void) const +{ + PRE(_data != NULL); + return *_data; +} + + +/// Gets the value of this object with a default fallback. +/// +/// \param default_value The value to return if this object holds no value. +/// +/// \return A reference to the data in the optional object, or the reference +/// passed in as a parameter. +template< class T > +const T& +utils::optional< T >::get_default(const T& default_value) const +{ + if (_data != NULL) + return *_data; + else + return default_value; +} + + +/// Tests whether the optional object contains data or not. +/// +/// \return True if the object is not none; false otherwise. +template< class T > +utils::optional< T >::operator bool(void) const +{ + return _data != NULL; +} + + +/// Tests whether the optional object contains data or not. +/// +/// \return True if the object is not none; false otherwise. +template< class T > +T& +utils::optional< T >::get(void) +{ + PRE(_data != NULL); + return *_data; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< class T > +std::ostream& utils::operator<<(std::ostream& output, + const optional< T >& object) +{ + if (!object) { + output << "none"; + } else { + output << object.get(); + } + return output; +} + + +/// Helper function to instantiate optional objects. +/// +/// \param value The value for the optional object. Shouldn't be none, as +/// optional objects can be constructed from none right away. +/// +/// \return A new optional object. +template< class T > +utils::optional< T > +utils::make_optional(const T& value) +{ + return optional< T >(value); +} + + +#endif // !defined(UTILS_OPTIONAL_IPP) diff --git a/utils/optional_fwd.hpp b/utils/optional_fwd.hpp new file mode 100644 index 000000000000..931dbbfe88da --- /dev/null +++ b/utils/optional_fwd.hpp @@ -0,0 +1,61 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/optional_fwd.hpp +/// Forward declarations for utils/optional.hpp + +#if !defined(UTILS_OPTIONAL_FWD_HPP) +#define UTILS_OPTIONAL_FWD_HPP + +namespace utils { + + +namespace detail { + + +/// Internal type-safe representation for the none type. +struct none_t {}; + + +} // namespace detail + + +/// The none value. +/// +/// This has internal linkage so it is OK to define it in the header file. +/// However, pointers to none from different translation units will be +/// different. Just don't do that. +const detail::none_t none = {}; + + +template< class > class optional; + + +} // namespace utils + +#endif // !defined(UTILS_OPTIONAL_FWD_HPP) diff --git a/utils/optional_test.cpp b/utils/optional_test.cpp new file mode 100644 index 000000000000..debd8949852e --- /dev/null +++ b/utils/optional_test.cpp @@ -0,0 +1,285 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/optional.ipp" + +#include <iostream> +#include <sstream> + +#include <atf-c++.hpp> + +using utils::none; +using utils::optional; + + +namespace { + + +/// Fake class to capture calls to the new and delete operators. +class test_alloc { +public: + /// Value to disambiguate objects after construction. + int value; + + /// Balance of alive instances of this class in dynamic memory. + static size_t instances; + + /// Constructs a new optional object. + /// + /// \param value_ The value to store in this object for disambiguation. + test_alloc(int value_) : value(value_) + { + } + + /// Allocates a new object and records its existence. + /// + /// \param size The amount of memory to allocate. + /// + /// \return A pointer to the allocated memory. + /// + /// \throw std::bad_alloc If the memory allocation fails. + void* + operator new(size_t size) + { + instances++; + std::cout << "test_alloc::operator new called\n"; + return ::operator new(size); + } + + /// Deallocates an existing object and unrecords its existence. + /// + /// \param mem The pointer to the memory to deallocate. + void + operator delete(void* mem) + { + instances--; + std::cout << "test_alloc::operator delete called\n"; + ::operator delete(mem); + } +}; + + +size_t test_alloc::instances = 0; + + +/// Constructs and returns an optional object. +/// +/// This is used by tests to validate that returning an object from within a +/// function works (i.e. the necessary constructors are available). +/// +/// \tparam Type The type of the object included in the optional wrapper. +/// \param value The value to put inside the optional wrapper. +/// +/// \return The constructed optional object. +template< typename Type > +optional< Type > +return_optional(const Type& value) +{ + return optional< Type >(value); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(ctors__native_type); +ATF_TEST_CASE_BODY(ctors__native_type) +{ + const optional< int > no_args; + ATF_REQUIRE(!no_args); + + const optional< int > with_none(none); + ATF_REQUIRE(!with_none); + + const optional< int > with_arg(3); + ATF_REQUIRE(with_arg); + ATF_REQUIRE_EQ(3, with_arg.get()); + + const optional< int > copy_none(with_none); + ATF_REQUIRE(!copy_none); + + const optional< int > copy_arg(with_arg); + ATF_REQUIRE(copy_arg); + ATF_REQUIRE_EQ(3, copy_arg.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ctors__complex_type); +ATF_TEST_CASE_BODY(ctors__complex_type) +{ + const optional< std::string > no_args; + ATF_REQUIRE(!no_args); + + const optional< std::string > with_none(none); + ATF_REQUIRE(!with_none); + + const optional< std::string > with_arg("foo"); + ATF_REQUIRE(with_arg); + ATF_REQUIRE_EQ("foo", with_arg.get()); + + const optional< std::string > copy_none(with_none); + ATF_REQUIRE(!copy_none); + + const optional< std::string > copy_arg(with_arg); + ATF_REQUIRE(copy_arg); + ATF_REQUIRE_EQ("foo", copy_arg.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(assign); +ATF_TEST_CASE_BODY(assign) +{ + optional< int > from_default; + from_default = optional< int >(); + ATF_REQUIRE(!from_default); + + optional< int > from_none(3); + from_none = none; + ATF_REQUIRE(!from_none); + + optional< int > from_int; + from_int = 6; + ATF_REQUIRE_EQ(6, from_int.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(return); +ATF_TEST_CASE_BODY(return) +{ + optional< long > from_return(return_optional< long >(123)); + ATF_REQUIRE(from_return); + ATF_REQUIRE_EQ(123, from_return.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(memory); +ATF_TEST_CASE_BODY(memory) +{ + ATF_REQUIRE_EQ(0, test_alloc::instances); + { + optional< test_alloc > optional1(test_alloc(3)); + ATF_REQUIRE_EQ(1, test_alloc::instances); + ATF_REQUIRE_EQ(3, optional1.get().value); + + { + optional< test_alloc > optional2(optional1); + ATF_REQUIRE_EQ(2, test_alloc::instances); + ATF_REQUIRE_EQ(3, optional2.get().value); + + optional2 = 5; + ATF_REQUIRE_EQ(2, test_alloc::instances); + ATF_REQUIRE_EQ(5, optional2.get().value); + ATF_REQUIRE_EQ(3, optional1.get().value); + } + ATF_REQUIRE_EQ(1, test_alloc::instances); + ATF_REQUIRE_EQ(3, optional1.get().value); + } + ATF_REQUIRE_EQ(0, test_alloc::instances); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_default); +ATF_TEST_CASE_BODY(get_default) +{ + const std::string def_value = "hello"; + optional< std::string > optional; + ATF_REQUIRE(&def_value == &optional.get_default(def_value)); + optional = "bye"; + ATF_REQUIRE_EQ("bye", optional.get_default(def_value)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(make_optional); +ATF_TEST_CASE_BODY(make_optional) +{ + optional< int > opt = utils::make_optional(576); + ATF_REQUIRE(opt); + ATF_REQUIRE_EQ(576, opt.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne); +ATF_TEST_CASE_BODY(operators_eq_and_ne) +{ + optional< int > opt1, opt2; + + opt1 = none; opt2 = none; + ATF_REQUIRE( opt1 == opt2); + ATF_REQUIRE(!(opt1 != opt2)); + + opt1 = utils::make_optional(5); opt2 = none; + ATF_REQUIRE(!(opt1 == opt2)); + ATF_REQUIRE( opt1 != opt2); + + opt1 = none; opt2 = utils::make_optional(5); + ATF_REQUIRE(!(opt1 == opt2)); + ATF_REQUIRE( opt1 != opt2); + + opt1 = utils::make_optional(5); opt2 = utils::make_optional(5); + ATF_REQUIRE( opt1 == opt2); + ATF_REQUIRE(!(opt1 != opt2)); + + opt1 = utils::make_optional(6); opt2 = utils::make_optional(5); + ATF_REQUIRE(!(opt1 == opt2)); + ATF_REQUIRE( opt1 != opt2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output); +ATF_TEST_CASE_BODY(output) +{ + { + std::ostringstream str; + str << optional< int >(none); + ATF_REQUIRE_EQ("none", str.str()); + } + { + std::ostringstream str; + str << optional< int >(5); + ATF_REQUIRE_EQ("5", str.str()); + } + { + std::ostringstream str; + str << optional< std::string >("this is a text"); + ATF_REQUIRE_EQ("this is a text", str.str()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ctors__native_type); + ATF_ADD_TEST_CASE(tcs, ctors__complex_type); + ATF_ADD_TEST_CASE(tcs, assign); + ATF_ADD_TEST_CASE(tcs, return); + ATF_ADD_TEST_CASE(tcs, memory); + ATF_ADD_TEST_CASE(tcs, get_default); + ATF_ADD_TEST_CASE(tcs, make_optional); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne); + ATF_ADD_TEST_CASE(tcs, output); +} diff --git a/utils/passwd.cpp b/utils/passwd.cpp new file mode 100644 index 000000000000..32a16bb4d462 --- /dev/null +++ b/utils/passwd.cpp @@ -0,0 +1,194 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/passwd.hpp" + +extern "C" { +#include <sys/types.h> + +#include <pwd.h> +#include <unistd.h> +} + +#include <stdexcept> + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace passwd_ns = utils::passwd; + + +namespace { + + +/// If defined, replaces the value returned by current_user(). +static utils::optional< passwd_ns::user > fake_current_user; + + +/// If not empty, defines the current set of mock users. +static std::vector< passwd_ns::user > mock_users; + + +/// Formats a user for logging purposes. +/// +/// \param user The user to format. +/// +/// \return The user as a string. +static std::string +format_user(const passwd_ns::user& user) +{ + return F("name=%s, uid=%s, gid=%s") % user.name % user.uid % user.gid; +} + + +} // anonymous namespace + + +/// Constructs a new user. +/// +/// \param name_ The name of the user. +/// \param uid_ The user identifier. +/// \param gid_ The login group identifier. +passwd_ns::user::user(const std::string& name_, const unsigned int uid_, + const unsigned int gid_) : + name(name_), + uid(uid_), + gid(gid_) +{ +} + + +/// Checks if the user has superpowers or not. +/// +/// \return True if the user is root, false otherwise. +bool +passwd_ns::user::is_root(void) const +{ + return uid == 0; +} + + +/// Gets the current user. +/// +/// \return The current user. +passwd_ns::user +passwd_ns::current_user(void) +{ + if (fake_current_user) { + const user u = fake_current_user.get(); + LD(F("Current user is fake: %s") % format_user(u)); + return u; + } else { + const user u = find_user_by_uid(::getuid()); + LD(F("Current user is: %s") % format_user(u)); + return u; + } +} + + +/// Gets information about a user by its name. +/// +/// \param name The name of the user to query. +/// +/// \return The information about the user. +/// +/// \throw std::runtime_error If the user does not exist. +passwd_ns::user +passwd_ns::find_user_by_name(const std::string& name) +{ + if (mock_users.empty()) { + const struct ::passwd* pw = ::getpwnam(name.c_str()); + if (pw == NULL) + throw std::runtime_error(F("Failed to get information about the " + "user '%s'") % name); + INV(pw->pw_name == name); + return user(pw->pw_name, pw->pw_uid, pw->pw_gid); + } else { + for (std::vector< user >::const_iterator iter = mock_users.begin(); + iter != mock_users.end(); iter++) { + if ((*iter).name == name) + return *iter; + } + throw std::runtime_error(F("Failed to get information about the " + "user '%s'") % name); + } +} + + +/// Gets information about a user by its identifier. +/// +/// \param uid The identifier of the user to query. +/// +/// \return The information about the user. +/// +/// \throw std::runtime_error If the user does not exist. +passwd_ns::user +passwd_ns::find_user_by_uid(const unsigned int uid) +{ + if (mock_users.empty()) { + const struct ::passwd* pw = ::getpwuid(uid); + if (pw == NULL) + throw std::runtime_error(F("Failed to get information about the " + "user with UID %s") % uid); + INV(pw->pw_uid == uid); + return user(pw->pw_name, pw->pw_uid, pw->pw_gid); + } else { + for (std::vector< user >::const_iterator iter = mock_users.begin(); + iter != mock_users.end(); iter++) { + if ((*iter).uid == uid) + return *iter; + } + throw std::runtime_error(F("Failed to get information about the " + "user with UID %s") % uid); + } +} + + +/// Overrides the current user for testing purposes. +/// +/// This DOES NOT change the current privileges! +/// +/// \param new_current_user The new current user. +void +passwd_ns::set_current_user_for_testing(const user& new_current_user) +{ + fake_current_user = new_current_user; +} + + +/// Overrides the current set of users for testing purposes. +/// +/// \param users The new users set. Cannot be empty. +void +passwd_ns::set_mock_users_for_testing(const std::vector< user >& users) +{ + PRE(!users.empty()); + mock_users = users; +} diff --git a/utils/passwd.hpp b/utils/passwd.hpp new file mode 100644 index 000000000000..e0b17c547080 --- /dev/null +++ b/utils/passwd.hpp @@ -0,0 +1,72 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/passwd.hpp +/// Querying and manipulation of users and groups. + +#if !defined(UTILS_PASSWD_HPP) +#define UTILS_PASSWD_HPP + +#include "utils/passwd_fwd.hpp" + +#include <string> +#include <vector> + +namespace utils { +namespace passwd { + + +/// Represents a system user. +class user { +public: + /// The name of the user. + std::string name; + + /// The system-wide identifier of the user. + unsigned int uid; + + /// The login group identifier for the user. + unsigned int gid; + + user(const std::string&, const unsigned int, const unsigned int); + + bool is_root(void) const; +}; + + +user current_user(void); +user find_user_by_name(const std::string&); +user find_user_by_uid(const unsigned int); +void set_current_user_for_testing(const user&); +void set_mock_users_for_testing(const std::vector< user >&); + + +} // namespace passwd +} // namespace utils + +#endif // !defined(UTILS_PASSWD_HPP) diff --git a/utils/passwd_fwd.hpp b/utils/passwd_fwd.hpp new file mode 100644 index 000000000000..bedbd34c8af8 --- /dev/null +++ b/utils/passwd_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/passwd_fwd.hpp +/// Forward declarations for utils/passwd.hpp + +#if !defined(UTILS_PASSWD_FWD_HPP) +#define UTILS_PASSWD_FWD_HPP + +namespace utils { +namespace passwd { + + +class user; + + +} // namespace passwd +} // namespace utils + +#endif // !defined(UTILS_PASSWD_FWD_HPP) diff --git a/utils/passwd_test.cpp b/utils/passwd_test.cpp new file mode 100644 index 000000000000..720ecb32e5fe --- /dev/null +++ b/utils/passwd_test.cpp @@ -0,0 +1,179 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/passwd.hpp" + +extern "C" { +#include <sys/wait.h> + +#include <pwd.h> +#include <unistd.h> +} + +#include <cstdlib> +#include <stdexcept> + +#include <atf-c++.hpp> + +namespace passwd_ns = utils::passwd; + + +ATF_TEST_CASE_WITHOUT_HEAD(user__public_fields); +ATF_TEST_CASE_BODY(user__public_fields) +{ + const passwd_ns::user user("the-name", 1, 2); + ATF_REQUIRE_EQ("the-name", user.name); + ATF_REQUIRE_EQ(1, user.uid); + ATF_REQUIRE_EQ(2, user.gid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(user__is_root__true); +ATF_TEST_CASE_BODY(user__is_root__true) +{ + const passwd_ns::user user("i-am-root", 0, 10); + ATF_REQUIRE(user.is_root()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(user__is_root__false); +ATF_TEST_CASE_BODY(user__is_root__false) +{ + const passwd_ns::user user("i-am-not-root", 123, 10); + ATF_REQUIRE(!user.is_root()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_user); +ATF_TEST_CASE_BODY(current_user) +{ + const passwd_ns::user user = passwd_ns::current_user(); + ATF_REQUIRE_EQ(::getuid(), user.uid); + ATF_REQUIRE_EQ(::getgid(), user.gid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_user__fake); +ATF_TEST_CASE_BODY(current_user__fake) +{ + const passwd_ns::user new_user("someone-else", ::getuid() + 1, 0); + passwd_ns::set_current_user_for_testing(new_user); + + const passwd_ns::user user = passwd_ns::current_user(); + ATF_REQUIRE(::getuid() != user.uid); + ATF_REQUIRE_EQ(new_user.uid, user.uid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_name__ok); +ATF_TEST_CASE_BODY(find_user_by_name__ok) +{ + const struct ::passwd* pw = ::getpwuid(::getuid()); + ATF_REQUIRE(pw != NULL); + + const passwd_ns::user user = passwd_ns::find_user_by_name(pw->pw_name); + ATF_REQUIRE_EQ(::getuid(), user.uid); + ATF_REQUIRE_EQ(::getgid(), user.gid); + ATF_REQUIRE_EQ(pw->pw_name, user.name); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_name__fail); +ATF_TEST_CASE_BODY(find_user_by_name__fail) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "Failed.*user 'i-do-not-exist'", + passwd_ns::find_user_by_name("i-do-not-exist")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_name__fake); +ATF_TEST_CASE_BODY(find_user_by_name__fake) +{ + std::vector< passwd_ns::user > users; + users.push_back(passwd_ns::user("myself2", 20, 40)); + users.push_back(passwd_ns::user("myself1", 10, 15)); + users.push_back(passwd_ns::user("myself3", 30, 60)); + passwd_ns::set_mock_users_for_testing(users); + + const passwd_ns::user user = passwd_ns::find_user_by_name("myself1"); + ATF_REQUIRE_EQ(10, user.uid); + ATF_REQUIRE_EQ(15, user.gid); + ATF_REQUIRE_EQ("myself1", user.name); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Failed.*user 'root'", + passwd_ns::find_user_by_name("root")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_uid__ok); +ATF_TEST_CASE_BODY(find_user_by_uid__ok) +{ + const passwd_ns::user user = passwd_ns::find_user_by_uid(::getuid()); + ATF_REQUIRE_EQ(::getuid(), user.uid); + ATF_REQUIRE_EQ(::getgid(), user.gid); + + const struct ::passwd* pw = ::getpwuid(::getuid()); + ATF_REQUIRE(pw != NULL); + ATF_REQUIRE_EQ(pw->pw_name, user.name); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_uid__fake); +ATF_TEST_CASE_BODY(find_user_by_uid__fake) +{ + std::vector< passwd_ns::user > users; + users.push_back(passwd_ns::user("myself2", 20, 40)); + users.push_back(passwd_ns::user("myself1", 10, 15)); + users.push_back(passwd_ns::user("myself3", 30, 60)); + passwd_ns::set_mock_users_for_testing(users); + + const passwd_ns::user user = passwd_ns::find_user_by_uid(10); + ATF_REQUIRE_EQ(10, user.uid); + ATF_REQUIRE_EQ(15, user.gid); + ATF_REQUIRE_EQ("myself1", user.name); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Failed.*user.*UID 0", + passwd_ns::find_user_by_uid(0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, user__public_fields); + ATF_ADD_TEST_CASE(tcs, user__is_root__true); + ATF_ADD_TEST_CASE(tcs, user__is_root__false); + + ATF_ADD_TEST_CASE(tcs, current_user); + ATF_ADD_TEST_CASE(tcs, current_user__fake); + + ATF_ADD_TEST_CASE(tcs, find_user_by_name__ok); + ATF_ADD_TEST_CASE(tcs, find_user_by_name__fail); + ATF_ADD_TEST_CASE(tcs, find_user_by_name__fake); + ATF_ADD_TEST_CASE(tcs, find_user_by_uid__ok); + ATF_ADD_TEST_CASE(tcs, find_user_by_uid__fake); +} diff --git a/utils/process/.gitignore b/utils/process/.gitignore new file mode 100644 index 000000000000..fb3291b39e0c --- /dev/null +++ b/utils/process/.gitignore @@ -0,0 +1 @@ +helpers diff --git a/utils/process/Kyuafile b/utils/process/Kyuafile new file mode 100644 index 000000000000..92e62cfac6fc --- /dev/null +++ b/utils/process/Kyuafile @@ -0,0 +1,13 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="child_test"} +atf_test_program{name="deadline_killer_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="executor_test"} +atf_test_program{name="fdstream_test"} +atf_test_program{name="isolation_test"} +atf_test_program{name="operations_test"} +atf_test_program{name="status_test"} +atf_test_program{name="systembuf_test"} diff --git a/utils/process/Makefile.am.inc b/utils/process/Makefile.am.inc new file mode 100644 index 000000000000..3cff02e7e455 --- /dev/null +++ b/utils/process/Makefile.am.inc @@ -0,0 +1,113 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +libutils_a_SOURCES += utils/process/child.cpp +libutils_a_SOURCES += utils/process/child.hpp +libutils_a_SOURCES += utils/process/child.ipp +libutils_a_SOURCES += utils/process/child_fwd.hpp +libutils_a_SOURCES += utils/process/deadline_killer.cpp +libutils_a_SOURCES += utils/process/deadline_killer.hpp +libutils_a_SOURCES += utils/process/deadline_killer_fwd.hpp +libutils_a_SOURCES += utils/process/exceptions.cpp +libutils_a_SOURCES += utils/process/exceptions.hpp +libutils_a_SOURCES += utils/process/executor.cpp +libutils_a_SOURCES += utils/process/executor.hpp +libutils_a_SOURCES += utils/process/executor.ipp +libutils_a_SOURCES += utils/process/executor_fwd.hpp +libutils_a_SOURCES += utils/process/fdstream.cpp +libutils_a_SOURCES += utils/process/fdstream.hpp +libutils_a_SOURCES += utils/process/fdstream_fwd.hpp +libutils_a_SOURCES += utils/process/isolation.cpp +libutils_a_SOURCES += utils/process/isolation.hpp +libutils_a_SOURCES += utils/process/operations.cpp +libutils_a_SOURCES += utils/process/operations.hpp +libutils_a_SOURCES += utils/process/operations_fwd.hpp +libutils_a_SOURCES += utils/process/status.cpp +libutils_a_SOURCES += utils/process/status.hpp +libutils_a_SOURCES += utils/process/status_fwd.hpp +libutils_a_SOURCES += utils/process/system.cpp +libutils_a_SOURCES += utils/process/system.hpp +libutils_a_SOURCES += utils/process/systembuf.cpp +libutils_a_SOURCES += utils/process/systembuf.hpp +libutils_a_SOURCES += utils/process/systembuf_fwd.hpp + +if WITH_ATF +tests_utils_processdir = $(pkgtestsdir)/utils/process + +tests_utils_process_DATA = utils/process/Kyuafile +EXTRA_DIST += $(tests_utils_process_DATA) + +tests_utils_process_PROGRAMS = utils/process/child_test +utils_process_child_test_SOURCES = utils/process/child_test.cpp +utils_process_child_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_child_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/deadline_killer_test +utils_process_deadline_killer_test_SOURCES = \ + utils/process/deadline_killer_test.cpp +utils_process_deadline_killer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_deadline_killer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/exceptions_test +utils_process_exceptions_test_SOURCES = utils/process/exceptions_test.cpp +utils_process_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/executor_test +utils_process_executor_test_SOURCES = utils/process/executor_test.cpp +utils_process_executor_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_executor_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/fdstream_test +utils_process_fdstream_test_SOURCES = utils/process/fdstream_test.cpp +utils_process_fdstream_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_fdstream_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/isolation_test +utils_process_isolation_test_SOURCES = utils/process/isolation_test.cpp +utils_process_isolation_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_isolation_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/helpers +utils_process_helpers_SOURCES = utils/process/helpers.cpp + +tests_utils_process_PROGRAMS += utils/process/operations_test +utils_process_operations_test_SOURCES = utils/process/operations_test.cpp +utils_process_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/status_test +utils_process_status_test_SOURCES = utils/process/status_test.cpp +utils_process_status_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_status_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/systembuf_test +utils_process_systembuf_test_SOURCES = utils/process/systembuf_test.cpp +utils_process_systembuf_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_systembuf_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/process/child.cpp b/utils/process/child.cpp new file mode 100644 index 000000000000..fef09ccaad3b --- /dev/null +++ b/utils/process/child.cpp @@ -0,0 +1,385 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/child.ipp" + +extern "C" { +#include <sys/stat.h> +#include <sys/wait.h> + +#include <fcntl.h> +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <iostream> +#include <memory> + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/process/exceptions.hpp" +#include "utils/process/fdstream.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/system.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" + + +namespace utils { +namespace process { + + +/// Private implementation fields for child objects. +struct child::impl : utils::noncopyable { + /// The process identifier. + pid_t _pid; + + /// The input stream for the process' stdout and stderr. May be NULL. + std::auto_ptr< process::ifdstream > _output; + + /// Initializes private implementation data. + /// + /// \param pid The process identifier. + /// \param output The input stream. Grabs ownership of the pointer. + impl(const pid_t pid, process::ifdstream* output) : + _pid(pid), _output(output) {} +}; + + +} // namespace process +} // namespace utils + + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +namespace { + + +/// Exception-based version of dup(2). +/// +/// \param old_fd The file descriptor to duplicate. +/// \param new_fd The file descriptor to use as the duplicate. This is +/// closed if it was open before the copy happens. +/// +/// \throw process::system_error If the call to dup2(2) fails. +static void +safe_dup(const int old_fd, const int new_fd) +{ + if (process::detail::syscall_dup2(old_fd, new_fd) == -1) { + const int original_errno = errno; + throw process::system_error(F("dup2(%s, %s) failed") % old_fd % new_fd, + original_errno); + } +} + + +/// Exception-based version of open(2) to open (or create) a file for append. +/// +/// \param filename The file to open in append mode. +/// +/// \return The file descriptor for the opened or created file. +/// +/// \throw process::system_error If the call to open(2) fails. +static int +open_for_append(const fs::path& filename) +{ + const int fd = process::detail::syscall_open( + filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd == -1) { + const int original_errno = errno; + throw process::system_error(F("Failed to create %s because open(2) " + "failed") % filename, original_errno); + } + return fd; +} + + +/// Logs the execution of another program. +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +static void +log_exec(const fs::path& program, const process::args_vector& args) +{ + std::string plain_command = program.str(); + for (process::args_vector::const_iterator iter = args.begin(); + iter != args.end(); ++iter) + plain_command += F(" %s") % *iter; + LD(F("Executing %s") % plain_command); +} + + +} // anonymous namespace + + +/// Prints out a fatal error and aborts. +void +utils::process::detail::report_error_and_abort(void) +{ + std::cerr << "Caught unknown exception\n"; + std::abort(); +} + + +/// Prints out a fatal error and aborts. +/// +/// \param error The error to display. +void +utils::process::detail::report_error_and_abort(const std::runtime_error& error) +{ + std::cerr << "Caught runtime_error: " << error.what() << '\n'; + std::abort(); +} + + +/// Creates a new child. +/// +/// \param implptr A dynamically-allocated impl object with the contents of the +/// new child. +process::child::child(impl *implptr) : + _pimpl(implptr) +{ +} + + +/// Destructor for child. +process::child::~child(void) +{ +} + + +/// Helper function for fork(). +/// +/// Please note: if you update this function to change the return type or to +/// raise different errors, do not forget to update fork() accordingly. +/// +/// \return In the case of the parent, a new child object returned as a +/// dynamically-allocated object because children classes are unique and thus +/// noncopyable. In the case of the child, a NULL pointer. +/// +/// \throw process::system_error If the calls to pipe(2) or fork(2) fail. +std::auto_ptr< process::child > +process::child::fork_capture_aux(void) +{ + std::cout.flush(); + std::cerr.flush(); + + int fds[2]; + if (detail::syscall_pipe(fds) == -1) + throw process::system_error("pipe(2) failed", errno); + + std::auto_ptr< signals::interrupts_inhibiter > inhibiter( + new signals::interrupts_inhibiter); + pid_t pid = detail::syscall_fork(); + if (pid == -1) { + inhibiter.reset(NULL); // Unblock signals. + ::close(fds[0]); + ::close(fds[1]); + throw process::system_error("fork(2) failed", errno); + } else if (pid == 0) { + inhibiter.reset(NULL); // Unblock signals. + ::setsid(); + + try { + ::close(fds[0]); + safe_dup(fds[1], STDOUT_FILENO); + safe_dup(fds[1], STDERR_FILENO); + ::close(fds[1]); + } catch (const system_error& e) { + std::cerr << F("Failed to set up subprocess: %s\n") % e.what(); + std::abort(); + } + return std::auto_ptr< process::child >(NULL); + } else { + ::close(fds[1]); + LD(F("Spawned process %s: stdout and stderr inherited") % pid); + signals::add_pid_to_kill(pid); + inhibiter.reset(NULL); // Unblock signals. + return std::auto_ptr< process::child >( + new process::child(new impl(pid, new process::ifdstream(fds[0])))); + } +} + + +/// Helper function for fork(). +/// +/// Please note: if you update this function to change the return type or to +/// raise different errors, do not forget to update fork() accordingly. +/// +/// \param stdout_file The name of the file in which to store the stdout. +/// If this has the magic value /dev/stdout, then the parent's stdout is +/// reused without applying any redirection. +/// \param stderr_file The name of the file in which to store the stderr. +/// If this has the magic value /dev/stderr, then the parent's stderr is +/// reused without applying any redirection. +/// +/// \return In the case of the parent, a new child object returned as a +/// dynamically-allocated object because children classes are unique and thus +/// noncopyable. In the case of the child, a NULL pointer. +/// +/// \throw process::system_error If the call to fork(2) fails. +std::auto_ptr< process::child > +process::child::fork_files_aux(const fs::path& stdout_file, + const fs::path& stderr_file) +{ + std::cout.flush(); + std::cerr.flush(); + + std::auto_ptr< signals::interrupts_inhibiter > inhibiter( + new signals::interrupts_inhibiter); + pid_t pid = detail::syscall_fork(); + if (pid == -1) { + inhibiter.reset(NULL); // Unblock signals. + throw process::system_error("fork(2) failed", errno); + } else if (pid == 0) { + inhibiter.reset(NULL); // Unblock signals. + ::setsid(); + + try { + if (stdout_file != fs::path("/dev/stdout")) { + const int stdout_fd = open_for_append(stdout_file); + safe_dup(stdout_fd, STDOUT_FILENO); + ::close(stdout_fd); + } + if (stderr_file != fs::path("/dev/stderr")) { + const int stderr_fd = open_for_append(stderr_file); + safe_dup(stderr_fd, STDERR_FILENO); + ::close(stderr_fd); + } + } catch (const system_error& e) { + std::cerr << F("Failed to set up subprocess: %s\n") % e.what(); + std::abort(); + } + return std::auto_ptr< process::child >(NULL); + } else { + LD(F("Spawned process %s: stdout=%s, stderr=%s") % pid % stdout_file % + stderr_file); + signals::add_pid_to_kill(pid); + inhibiter.reset(NULL); // Unblock signals. + return std::auto_ptr< process::child >( + new process::child(new impl(pid, NULL))); + } +} + + +/// Spawns a new binary and multiplexes and captures its stdout and stderr. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +std::auto_ptr< process::child > +process::child::spawn_capture(const fs::path& program, const args_vector& args) +{ + std::auto_ptr< child > child = fork_capture_aux(); + if (child.get() == NULL) + exec(program, args); + log_exec(program, args); + return child; +} + + +/// Spawns a new binary and redirects its stdout and stderr to files. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +/// \param stdout_file The name of the file in which to store the stdout. +/// \param stderr_file The name of the file in which to store the stderr. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +std::auto_ptr< process::child > +process::child::spawn_files(const fs::path& program, + const args_vector& args, + const fs::path& stdout_file, + const fs::path& stderr_file) +{ + std::auto_ptr< child > child = fork_files_aux(stdout_file, stderr_file); + if (child.get() == NULL) + exec(program, args); + log_exec(program, args); + return child; +} + + +/// Returns the process identifier of this child. +/// +/// \return A process identifier. +int +process::child::pid(void) const +{ + return _pimpl->_pid; +} + + +/// Gets the input stream corresponding to the stdout and stderr of the child. +/// +/// \pre The child must have been started by fork_capture(). +/// +/// \return A reference to the input stream connected to the output of the test +/// case. +std::istream& +process::child::output(void) +{ + PRE(_pimpl->_output.get() != NULL); + return *_pimpl->_output; +} + + +/// Blocks to wait for completion. +/// +/// \return The termination status of the child process. +/// +/// \throw process::system_error If the call to waitpid(2) fails. +process::status +process::child::wait(void) +{ + return process::wait(_pimpl->_pid); +} diff --git a/utils/process/child.hpp b/utils/process/child.hpp new file mode 100644 index 000000000000..2c9450f6500a --- /dev/null +++ b/utils/process/child.hpp @@ -0,0 +1,113 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/child.hpp +/// Spawning and manipulation of children processes. +/// +/// The child module provides a set of functions to spawn subprocesses with +/// different settings, and the corresponding set of classes to interact with +/// said subprocesses. The interfaces to fork subprocesses are very simplified +/// and only provide the minimum functionality required by the rest of the +/// project. +/// +/// Be aware that the semantics of the fork and wait methods exposed by this +/// module are slightly different from that of the native calls. Any process +/// spawned by fork here will be isolated in its own session; once any of +/// such children processes is awaited for, its whole process group will be +/// terminated. This is the semantics we want in the above layers to ensure +/// that test programs (and, for that matter, external utilities) do not leak +/// subprocesses on the system. + +#if !defined(UTILS_PROCESS_CHILD_HPP) +#define UTILS_PROCESS_CHILD_HPP + +#include "utils/process/child_fwd.hpp" + +#include <istream> +#include <memory> +#include <stdexcept> + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/process/operations_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { +namespace process { + + +namespace detail { + +void report_error_and_abort(void) UTILS_NORETURN; +void report_error_and_abort(const std::runtime_error&) UTILS_NORETURN; + + +} // namespace detail + + +/// Child process spawner and controller. +class child : noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + + static std::auto_ptr< child > fork_capture_aux(void); + + static std::auto_ptr< child > fork_files_aux(const fs::path&, + const fs::path&); + + explicit child(impl *); + +public: + ~child(void); + + template< typename Hook > + static std::auto_ptr< child > fork_capture(Hook); + std::istream& output(void); + + template< typename Hook > + static std::auto_ptr< child > fork_files(Hook, const fs::path&, + const fs::path&); + + static std::auto_ptr< child > spawn_capture( + const fs::path&, const args_vector&); + static std::auto_ptr< child > spawn_files( + const fs::path&, const args_vector&, const fs::path&, const fs::path&); + + int pid(void) const; + + status wait(void); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_CHILD_HPP) diff --git a/utils/process/child.ipp b/utils/process/child.ipp new file mode 100644 index 000000000000..aa90373652fd --- /dev/null +++ b/utils/process/child.ipp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_PROCESS_CHILD_IPP) +#define UTILS_PROCESS_CHILD_IPP + +#include <cstdlib> + +#include "utils/process/child.hpp" + +namespace utils { +namespace process { + + +/// Spawns a new subprocess and redirects its stdout and stderr to files. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param hook The function to execute in the subprocess. Must not return. +/// \param stdout_file The name of the file in which to store the stdout. +/// \param stderr_file The name of the file in which to store the stderr. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +template< typename Hook > +std::auto_ptr< child > +child::fork_files(Hook hook, const fs::path& stdout_file, + const fs::path& stderr_file) +{ + std::auto_ptr< child > child = fork_files_aux(stdout_file, stderr_file); + if (child.get() == NULL) { + try { + hook(); + std::abort(); + } catch (const std::runtime_error& e) { + detail::report_error_and_abort(e); + } catch (...) { + detail::report_error_and_abort(); + } + } + + return child; +} + + +/// Spawns a new subprocess and multiplexes and captures its stdout and stderr. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param hook The function to execute in the subprocess. Must not return. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +template< typename Hook > +std::auto_ptr< child > +child::fork_capture(Hook hook) +{ + std::auto_ptr< child > child = fork_capture_aux(); + if (child.get() == NULL) { + try { + hook(); + std::abort(); + } catch (const std::runtime_error& e) { + detail::report_error_and_abort(e); + } catch (...) { + detail::report_error_and_abort(); + } + } + + return child; +} + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_CHILD_IPP) diff --git a/utils/process/child_fwd.hpp b/utils/process/child_fwd.hpp new file mode 100644 index 000000000000..4d4caa17d58c --- /dev/null +++ b/utils/process/child_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/child_fwd.hpp +/// Forward declarations for utils/process/child.hpp + +#if !defined(UTILS_PROCESS_CHILD_FWD_HPP) +#define UTILS_PROCESS_CHILD_FWD_HPP + +namespace utils { +namespace process { + + +class child; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_CHILD_FWD_HPP) diff --git a/utils/process/child_test.cpp b/utils/process/child_test.cpp new file mode 100644 index 000000000000..69de9991ae13 --- /dev/null +++ b/utils/process/child_test.cpp @@ -0,0 +1,846 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/child.ipp" + +extern "C" { +#include <sys/stat.h> +#include <sys/wait.h> + +#include <fcntl.h> +#include <signal.h> +#include <unistd.h> +} + +#include <cstdarg> +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <fstream> +#include <iostream> +#include <stdexcept> + +#include <atf-c++.hpp> + +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/process/exceptions.hpp" +#include "utils/process/status.hpp" +#include "utils/process/system.hpp" +#include "utils/sanity.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace process = utils::process; + + +namespace { + + +/// Checks if the current subprocess is in its own session. +static void +child_check_own_session(void) +{ + std::exit((::getsid(::getpid()) == ::getpid()) ? + EXIT_SUCCESS : EXIT_FAILURE); +} + + +/// Body for a process that prints a simple message and exits. +/// +/// \tparam ExitStatus The exit status for the subprocess. +/// \tparam Message A single character that will be prepended to the printed +/// messages. This would ideally be a string, but we cannot templatize a +/// function with an object nor a pointer. +template< int ExitStatus, char Message > +static void +child_simple_function(void) +{ + std::cout << "To stdout: " << Message << "\n"; + std::cerr << "To stderr: " << Message << "\n"; + std::exit(ExitStatus); +} + + +/// Functor for the body of a process that prints a simple message and exits. +class child_simple_functor { + /// The exit status that the subprocess will yield. + int _exitstatus; + + /// The message to print on stdout and stderr. + std::string _message; + +public: + /// Constructs a new functor. + /// + /// \param exitstatus The exit status that the subprocess will yield. + /// \param message The message to print on stdout and stderr. + child_simple_functor(const int exitstatus, const std::string& message) : + _exitstatus(exitstatus), + _message(message) + { + } + + /// Body for the subprocess. + void + operator()(void) + { + std::cout << "To stdout: " << _message << "\n"; + std::cerr << "To stderr: " << _message << "\n"; + std::exit(_exitstatus); + } +}; + + +/// Body for a process that prints many messages to stdout and exits. +/// +/// The goal of this body is to validate that any buffering performed on the +/// parent process to read the output of the subprocess works correctly. +static void +child_printer_function(void) +{ + for (std::size_t i = 0; i < 100; i++) + std::cout << "This is a message to stdout, sequence " << i << "\n"; + std::cout.flush(); + std::cerr << "Exiting\n"; + std::exit(EXIT_SUCCESS); +} + + +/// Functor for the body of a process that runs child_printer_function. +class child_printer_functor { +public: + /// Body for the subprocess. + void + operator()(void) + { + child_printer_function(); + } +}; + + +/// Body for a child process that throws an exception. +static void +child_throw_exception(void) +{ + throw std::runtime_error("A loose exception"); +} + + +/// Body for a child process that creates a pidfile. +static void +child_write_pid(void) +{ + std::ofstream output("pidfile"); + output << ::getpid() << "\n"; + output.close(); + std::exit(EXIT_SUCCESS); +} + + +/// A child process that returns. +/// +/// The fork() wrappers are supposed to capture this condition and terminate the +/// child before the code returns to the fork() call point. +static void +child_return(void) +{ +} + + +/// A child process that raises an exception. +/// +/// The fork() wrappers are supposed to capture this condition and terminate the +/// child before the code returns to the fork() call point. +/// +/// \tparam Type The type of the exception to raise. +/// \tparam Value The value passed to the constructor of the exception type. In +/// general, this only makes sense if Type is a primitive type so that, in +/// the end, the code becomes "throw int(123)". +/// +/// \throw Type An exception of the provided type. +template< class Type, Type Value > +void +child_raise_exception(void) +{ + throw Type(Value); +} + + +/// Calculates the path to the test helpers binary. +/// +/// \param tc A pointer to the caller test case, needed to extract the value of +/// the "srcdir" property. +/// +/// \return The path to the helpers binary. +static fs::path +get_helpers(const atf::tests::tc* tc) +{ + return fs::path(tc->get_config_var("srcdir")) / "helpers"; +} + + +/// Mock fork(2) that just returns an error. +/// +/// \tparam Errno The value to set as the errno of the failed call. +/// +/// \return Always -1. +template< int Errno > +static pid_t +fork_fail(void) throw() +{ + errno = Errno; + return -1; +} + + +/// Mock open(2) that fails if the 'raise-error' file is opened. +/// +/// \tparam Errno The value to set as the errno if the known failure triggers. +/// \param path The path to the file to be opened. +/// \param flags The open flags. +/// \param ... The file mode creation, if flags contains O_CREAT. +/// +/// \return The opened file handle or -1 on error. +template< int Errno > +static int +open_fail(const char* path, const int flags, ...) throw() +{ + if (std::strcmp(path, "raise-error") == 0) { + errno = Errno; + return -1; + } else { + va_list ap; + va_start(ap, flags); + const int mode = va_arg(ap, int); + va_end(ap); + return ::open(path, flags, mode); + } +} + + +/// Mock pipe(2) that just returns an error. +/// +/// \tparam Errno The value to set as the errno of the failed call. +/// +/// \return Always -1. +template< int Errno > +static pid_t +pipe_fail(int* /* fildes */) throw() +{ + errno = Errno; + return -1; +} + + +/// Helper for child tests to validate inheritance of stdout/stderr. +/// +/// This function ensures that passing one of /dev/stdout or /dev/stderr to +/// the child__fork_files fork method does the right thing. The idea is that we +/// call fork with the given parameters and then make our child redirect one of +/// its file descriptors to a specific file without going through the process +/// library. We then validate if this redirection worked and got the expected +/// output. +/// +/// \param fork_stdout The path to pass to the fork call as the stdout file. +/// \param fork_stderr The path to pass to the fork call as the stderr file. +/// \param child_file The file to explicitly in the subchild. +/// \param child_fd The file descriptor to which to attach child_file. +static void +do_inherit_test(const char* fork_stdout, const char* fork_stderr, + const char* child_file, const int child_fd) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + logging::set_inmemory(); + + const int fd = ::open(child_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd != child_fd) { + if (::dup2(fd, child_fd) == -1) + std::abort(); + ::close(fd); + } + + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 123, 'Z' >, + fs::path(fork_stdout), fs::path(fork_stderr)); + const process::status status = child->wait(); + if (!status.exited() || status.exitstatus() != 123) + std::abort(); + std::exit(EXIT_SUCCESS); + } else { + int status; + ATF_REQUIRE(::waitpid(pid, &status, 0) != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + ATF_REQUIRE(atf::utils::grep_file("stdout: Z", "stdout.txt")); + ATF_REQUIRE(atf::utils::grep_file("stderr: Z", "stderr.txt")); + } +} + + +/// Performs a "child__fork_capture__ok_*" test. +/// +/// This test basically ensures that the child__fork_capture class spawns a +/// process whose output is captured in an input stream. +/// +/// \tparam Hook The type of the fork hook to use. +/// \param hook The hook to the fork call. +template< class Hook > +static void +child__fork_capture__ok(Hook hook) +{ + std::cout << "This unflushed message should not propagate to the child"; + std::cerr << "This unflushed message should not propagate to the child"; + std::auto_ptr< process::child > child = process::child::fork_capture(hook); + std::cout.flush(); + std::cerr.flush(); + + std::istream& output = child->output(); + for (std::size_t i = 0; i < 100; i++) { + std::string line; + ATF_REQUIRE(std::getline(output, line).good()); + ATF_REQUIRE_EQ((F("This is a message to stdout, " + "sequence %s") % i).str(), line); + } + + std::string line; + ATF_REQUIRE(std::getline(output, line).good()); + ATF_REQUIRE_EQ("Exiting", line); + + process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__ok_function); +ATF_TEST_CASE_BODY(child__fork_capture__ok_function) +{ + child__fork_capture__ok(child_printer_function); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__ok_functor); +ATF_TEST_CASE_BODY(child__fork_capture__ok_functor) +{ + child__fork_capture__ok(child_printer_functor()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__catch_exceptions); +ATF_TEST_CASE_BODY(child__fork_capture__catch_exceptions) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_throw_exception); + + std::string message; + std::istream& output = child->output(); + ATF_REQUIRE(std::getline(output, message).good()); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + + ATF_REQUIRE_MATCH("Caught.*A loose exception", message); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__new_session); +ATF_TEST_CASE_BODY(child__fork_capture__new_session) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_check_own_session); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__pipe_fail); +ATF_TEST_CASE_BODY(child__fork_capture__pipe_fail) +{ + process::detail::syscall_pipe = pipe_fail< 23 >; + try { + process::child::fork_capture(child_simple_function< 1, 'A' >); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("pipe.*failed", e.what())); + ATF_REQUIRE_EQ(23, e.original_errno()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_cannot_exit); +ATF_TEST_CASE_BODY(child__fork_capture__fork_cannot_exit) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + + std::auto_ptr< process::child > child = process::child::fork_capture( + child_return); + if (::getpid() != parent_pid) { + // If we enter this clause, it is because the hook returned. + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_cannot_unwind); +ATF_TEST_CASE_BODY(child__fork_capture__fork_cannot_unwind) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + try { + std::auto_ptr< process::child > child = process::child::fork_capture( + child_raise_exception< int, 123 >); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); + } catch (const int i) { + // If we enter this clause, it is because an exception leaked from the + // hook. + INV(parent_pid != ::getpid()); + INV(i == 123); + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_fail); +ATF_TEST_CASE_BODY(child__fork_capture__fork_fail) +{ + process::detail::syscall_fork = fork_fail< 89 >; + try { + process::child::fork_capture(child_simple_function< 1, 'A' >); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("fork.*failed", e.what())); + ATF_REQUIRE_EQ(89, e.original_errno()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__ok_function); +ATF_TEST_CASE_BODY(child__fork_files__ok_function) +{ + const fs::path file1("file1.txt"); + const fs::path file2("file2.txt"); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 15, 'Z' >, file1, file2); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(15, status.exitstatus()); + + ATF_REQUIRE( atf::utils::grep_file("^To stdout: Z$", file1.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stdout: Z$", file2.str())); + + ATF_REQUIRE( atf::utils::grep_file("^To stderr: Z$", file2.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stderr: Z$", file1.str())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__ok_functor); +ATF_TEST_CASE_BODY(child__fork_files__ok_functor) +{ + const fs::path filea("fileA.txt"); + const fs::path fileb("fileB.txt"); + + atf::utils::create_file(filea.str(), "Initial stdout\n"); + atf::utils::create_file(fileb.str(), "Initial stderr\n"); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_functor(16, "a functor"), filea, fileb); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(16, status.exitstatus()); + + ATF_REQUIRE( atf::utils::grep_file("^Initial stdout$", filea.str())); + ATF_REQUIRE(!atf::utils::grep_file("^Initial stdout$", fileb.str())); + + ATF_REQUIRE( atf::utils::grep_file("^To stdout: a functor$", filea.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stdout: a functor$", fileb.str())); + + ATF_REQUIRE( atf::utils::grep_file("^Initial stderr$", fileb.str())); + ATF_REQUIRE(!atf::utils::grep_file("^Initial stderr$", filea.str())); + + ATF_REQUIRE( atf::utils::grep_file("^To stderr: a functor$", fileb.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stderr: a functor$", filea.str())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__catch_exceptions); +ATF_TEST_CASE_BODY(child__fork_files__catch_exceptions) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + child_throw_exception, + fs::path("unused.out"), fs::path("stderr")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + + ATF_REQUIRE(atf::utils::grep_file("Caught.*A loose exception", "stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__new_session); +ATF_TEST_CASE_BODY(child__fork_files__new_session) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + child_check_own_session, + fs::path("unused.out"), fs::path("unused.err")); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__inherit_stdout); +ATF_TEST_CASE_BODY(child__fork_files__inherit_stdout) +{ + do_inherit_test("/dev/stdout", "stderr.txt", "stdout.txt", STDOUT_FILENO); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__inherit_stderr); +ATF_TEST_CASE_BODY(child__fork_files__inherit_stderr) +{ + do_inherit_test("stdout.txt", "/dev/stderr", "stderr.txt", STDERR_FILENO); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_cannot_exit); +ATF_TEST_CASE_BODY(child__fork_files__fork_cannot_exit) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_return, fs::path("out"), fs::path("err")); + if (::getpid() != parent_pid) { + // If we enter this clause, it is because the hook returned. + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_cannot_unwind); +ATF_TEST_CASE_BODY(child__fork_files__fork_cannot_unwind) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + try { + std::auto_ptr< process::child > child = process::child::fork_files( + child_raise_exception< int, 123 >, fs::path("out"), + fs::path("err")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); + } catch (const int i) { + // If we enter this clause, it is because an exception leaked from the + // hook. + INV(parent_pid != ::getpid()); + INV(i == 123); + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_fail); +ATF_TEST_CASE_BODY(child__fork_files__fork_fail) +{ + process::detail::syscall_fork = fork_fail< 1234 >; + try { + process::child::fork_files(child_simple_function< 1, 'A' >, + fs::path("a.txt"), fs::path("b.txt")); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("fork.*failed", e.what())); + ATF_REQUIRE_EQ(1234, e.original_errno()); + } + ATF_REQUIRE(!fs::exists(fs::path("a.txt"))); + ATF_REQUIRE(!fs::exists(fs::path("b.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__create_stdout_fail); +ATF_TEST_CASE_BODY(child__fork_files__create_stdout_fail) +{ + process::detail::syscall_open = open_fail< ENOENT >; + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 1, 'A' >, fs::path("raise-error"), + fs::path("created")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(!fs::exists(fs::path("raise-error"))); + ATF_REQUIRE(!fs::exists(fs::path("created"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__create_stderr_fail); +ATF_TEST_CASE_BODY(child__fork_files__create_stderr_fail) +{ + process::detail::syscall_open = open_fail< ENOENT >; + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 1, 'A' >, fs::path("created"), + fs::path("raise-error")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(fs::exists(fs::path("created"))); + ATF_REQUIRE(!fs::exists(fs::path("raise-error"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__absolute_path); +ATF_TEST_CASE_BODY(child__spawn__absolute_path) +{ + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("12"); + + const fs::path program = get_helpers(this); + INV(program.is_absolute()); + std::auto_ptr< process::child > child = process::child::spawn_files( + program, args, fs::path("out"), fs::path("err")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(12, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__relative_path); +ATF_TEST_CASE_BODY(child__spawn__relative_path) +{ + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("13"); + + ATF_REQUIRE(::mkdir("root", 0755) != -1); + ATF_REQUIRE(::symlink(get_helpers(this).c_str(), "root/helpers") != -1); + + std::auto_ptr< process::child > child = process::child::spawn_files( + fs::path("root/helpers"), args, fs::path("out"), fs::path("err")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(13, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__basename_only); +ATF_TEST_CASE_BODY(child__spawn__basename_only) +{ + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("14"); + + ATF_REQUIRE(::symlink(get_helpers(this).c_str(), "helpers") != -1); + + std::auto_ptr< process::child > child = process::child::spawn_files( + fs::path("helpers"), args, fs::path("out"), fs::path("err")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(14, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__no_path); +ATF_TEST_CASE_BODY(child__spawn__no_path) +{ + logging::set_inmemory(); + + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("14"); + + const fs::path helpers = get_helpers(this); + utils::setenv("PATH", helpers.branch_path().c_str()); + std::auto_ptr< process::child > child = process::child::spawn_capture( + fs::path(helpers.leaf_name()), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_MATCH("Failed to execute", line); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__no_args); +ATF_TEST_CASE_BODY(child__spawn__no_args) +{ + std::vector< std::string > args; + std::auto_ptr< process::child > child = process::child::spawn_capture( + get_helpers(this), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_EQ("Must provide a helper name", line); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_FAILURE, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__some_args); +ATF_TEST_CASE_BODY(child__spawn__some_args) +{ + std::vector< std::string > args; + args.push_back("print-args"); + args.push_back("foo"); + args.push_back(" bar baz "); + std::auto_ptr< process::child > child = process::child::spawn_capture( + get_helpers(this), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_EQ("argv[0] = " + get_helpers(this).str(), line); + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_EQ("argv[1] = print-args", line); + ATF_REQUIRE(std::getline(child->output(), line)); + ATF_REQUIRE_EQ("argv[2] = foo", line); + ATF_REQUIRE(std::getline(child->output(), line)); + ATF_REQUIRE_EQ("argv[3] = bar baz ", line); + ATF_REQUIRE(std::getline(child->output(), line)); + ATF_REQUIRE_EQ("argv[4] = NULL", line); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__missing_program); +ATF_TEST_CASE_BODY(child__spawn__missing_program) +{ + std::vector< std::string > args; + std::auto_ptr< process::child > child = process::child::spawn_capture( + fs::path("a/b/c"), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + const std::string exp = "Failed to execute a/b/c: "; + ATF_REQUIRE_EQ(exp, line.substr(0, exp.length())); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__pid); +ATF_TEST_CASE_BODY(child__pid) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_write_pid); + + const int pid = child->pid(); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); + + std::ifstream input("pidfile"); + ATF_REQUIRE(input); + int read_pid; + input >> read_pid; + input.close(); + + ATF_REQUIRE_EQ(read_pid, pid); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + utils::avoid_coredump_on_crash(); + + ATF_ADD_TEST_CASE(tcs, child__fork_capture__ok_function); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__ok_functor); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__catch_exceptions); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__new_session); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__pipe_fail); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_cannot_exit); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_cannot_unwind); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_fail); + + ATF_ADD_TEST_CASE(tcs, child__fork_files__ok_function); + ATF_ADD_TEST_CASE(tcs, child__fork_files__ok_functor); + ATF_ADD_TEST_CASE(tcs, child__fork_files__catch_exceptions); + ATF_ADD_TEST_CASE(tcs, child__fork_files__new_session); + ATF_ADD_TEST_CASE(tcs, child__fork_files__inherit_stdout); + ATF_ADD_TEST_CASE(tcs, child__fork_files__inherit_stderr); + ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_cannot_exit); + ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_cannot_unwind); + ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_fail); + ATF_ADD_TEST_CASE(tcs, child__fork_files__create_stdout_fail); + ATF_ADD_TEST_CASE(tcs, child__fork_files__create_stderr_fail); + + ATF_ADD_TEST_CASE(tcs, child__spawn__absolute_path); + ATF_ADD_TEST_CASE(tcs, child__spawn__relative_path); + ATF_ADD_TEST_CASE(tcs, child__spawn__basename_only); + ATF_ADD_TEST_CASE(tcs, child__spawn__no_path); + ATF_ADD_TEST_CASE(tcs, child__spawn__no_args); + ATF_ADD_TEST_CASE(tcs, child__spawn__some_args); + ATF_ADD_TEST_CASE(tcs, child__spawn__missing_program); + + ATF_ADD_TEST_CASE(tcs, child__pid); +} diff --git a/utils/process/deadline_killer.cpp b/utils/process/deadline_killer.cpp new file mode 100644 index 000000000000..ed733e402f76 --- /dev/null +++ b/utils/process/deadline_killer.cpp @@ -0,0 +1,54 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/deadline_killer.hpp" + +#include "utils/datetime.hpp" +#include "utils/process/operations.hpp" + +namespace datetime = utils::datetime; +namespace process = utils::process; + + +/// Constructor. +/// +/// \param delta Time to the timer activation. +/// \param pid PID of the process (and process group) to kill. +process::deadline_killer::deadline_killer(const datetime::delta& delta, + const int pid) : + signals::timer(delta), _pid(pid) +{ +} + + +/// Timer activation callback. +void +process::deadline_killer::callback(void) +{ + process::terminate_group(_pid); +} diff --git a/utils/process/deadline_killer.hpp b/utils/process/deadline_killer.hpp new file mode 100644 index 000000000000..8b337a0f9d8c --- /dev/null +++ b/utils/process/deadline_killer.hpp @@ -0,0 +1,58 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/deadline_killer.hpp +/// Timer to kill a process on activation. + +#if !defined(UTILS_PROCESS_DEADLINE_KILLER_HPP) +#define UTILS_PROCESS_DEADLINE_KILLER_HPP + +#include "utils/process/deadline_killer_fwd.hpp" + +#include "utils/signals/timer.hpp" + +namespace utils { +namespace process { + + +/// Timer that forcibly kills a process group on activation. +class deadline_killer : public utils::signals::timer { + /// PID of the process (and process group) to kill. + const int _pid; + + void callback(void); + +public: + deadline_killer(const datetime::delta&, const int); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_DEADLINE_KILLER_HPP) diff --git a/utils/process/deadline_killer_fwd.hpp b/utils/process/deadline_killer_fwd.hpp new file mode 100644 index 000000000000..fca3c5dc57c7 --- /dev/null +++ b/utils/process/deadline_killer_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/deadline_killer_fwd.hpp +/// Forward declarations for utils/process/deadline_killer.hpp + +#if !defined(UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP) +#define UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP + +namespace utils { +namespace process { + + +class deadline_killer; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP) diff --git a/utils/process/deadline_killer_test.cpp b/utils/process/deadline_killer_test.cpp new file mode 100644 index 000000000000..06c89660ac31 --- /dev/null +++ b/utils/process/deadline_killer_test.cpp @@ -0,0 +1,108 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/deadline_killer.hpp" + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <cstdlib> + +#include <atf-c++.hpp> + +#include "utils/datetime.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" + +namespace datetime = utils::datetime; +namespace process = utils::process; + + +namespace { + + +/// Body of a child process that sleeps and then exits. +/// +/// \tparam Seconds The delay the subprocess has to sleep for. +template< int Seconds > +static void +child_sleep(void) +{ + ::sleep(Seconds); + std::exit(EXIT_SUCCESS); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(activation); +ATF_TEST_CASE_BODY(activation) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_sleep< 60 >); + + datetime::timestamp start = datetime::timestamp::now(); + process::deadline_killer killer(datetime::delta(1, 0), child->pid()); + const process::status status = child->wait(); + killer.unprogram(); + datetime::timestamp end = datetime::timestamp::now(); + + ATF_REQUIRE(killer.fired()); + ATF_REQUIRE(end - start <= datetime::delta(10, 0)); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGKILL, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(no_activation); +ATF_TEST_CASE_BODY(no_activation) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_sleep< 1 >); + + datetime::timestamp start = datetime::timestamp::now(); + process::deadline_killer killer(datetime::delta(60, 0), child->pid()); + const process::status status = child->wait(); + killer.unprogram(); + datetime::timestamp end = datetime::timestamp::now(); + + ATF_REQUIRE(!killer.fired()); + ATF_REQUIRE(end - start <= datetime::delta(10, 0)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, activation); + ATF_ADD_TEST_CASE(tcs, no_activation); +} diff --git a/utils/process/exceptions.cpp b/utils/process/exceptions.cpp new file mode 100644 index 000000000000..d7590c330499 --- /dev/null +++ b/utils/process/exceptions.cpp @@ -0,0 +1,91 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/exceptions.hpp" + +#include <cstring> + +#include "utils/format/macros.hpp" + +namespace process = utils::process; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +process::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +process::error::~error(void) throw() +{ +} + + +/// Constructs a new error based on an errno code. +/// +/// \param message_ The message describing what caused the error. +/// \param errno_ The error code. +process::system_error::system_error(const std::string& message_, + const int errno_) : + error(F("%s: %s") % message_ % strerror(errno_)), + _original_errno(errno_) +{ +} + + +/// Destructor for the error. +process::system_error::~system_error(void) throw() +{ +} + + +/// \return The original errno value. +int +process::system_error::original_errno(void) const throw() +{ + return _original_errno; +} + + +/// Constructs a new timeout_error. +/// +/// \param message_ The message describing what caused the error. +process::timeout_error::timeout_error(const std::string& message_) : + error(message_) +{ +} + + +/// Destructor for the error. +process::timeout_error::~timeout_error(void) throw() +{ +} diff --git a/utils/process/exceptions.hpp b/utils/process/exceptions.hpp new file mode 100644 index 000000000000..3bf740459864 --- /dev/null +++ b/utils/process/exceptions.hpp @@ -0,0 +1,78 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/exceptions.hpp +/// Exception types raised by the process module. + +#if !defined(UTILS_PROCESS_EXCEPTIONS_HPP) +#define UTILS_PROCESS_EXCEPTIONS_HPP + +#include <stdexcept> + +namespace utils { +namespace process { + + +/// Base exceptions for process errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Exceptions for errno-based errors. +/// +/// TODO(jmmv): This code is duplicated in, at least, utils::fs. Figure +/// out a way to reuse this exception while maintaining the correct inheritance +/// (i.e. be able to keep it as a child of process::error). +class system_error : public error { + /// Error number describing this libc error condition. + int _original_errno; + +public: + explicit system_error(const std::string&, const int); + ~system_error(void) throw(); + + int original_errno(void) const throw(); +}; + + +/// Denotes that a deadline was exceeded. +class timeout_error : public error { +public: + explicit timeout_error(const std::string&); + ~timeout_error(void) throw(); +}; + + +} // namespace process +} // namespace utils + + +#endif // !defined(UTILS_PROCESS_EXCEPTIONS_HPP) diff --git a/utils/process/exceptions_test.cpp b/utils/process/exceptions_test.cpp new file mode 100644 index 000000000000..375b635fc173 --- /dev/null +++ b/utils/process/exceptions_test.cpp @@ -0,0 +1,63 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/exceptions.hpp" + +#include <cerrno> +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" + +namespace process = utils::process; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const process::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(system_error); +ATF_TEST_CASE_BODY(system_error) +{ + const process::system_error e("Call failed", ENOENT); + const std::string expected = F("Call failed: %s") % std::strerror(ENOENT); + ATF_REQUIRE_EQ(expected, e.what()); + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, system_error); +} diff --git a/utils/process/executor.cpp b/utils/process/executor.cpp new file mode 100644 index 000000000000..dbdf31268f86 --- /dev/null +++ b/utils/process/executor.cpp @@ -0,0 +1,869 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/executor.ipp" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +extern "C" { +#include <sys/types.h> +#include <sys/wait.h> + +#include <signal.h> +} + +#include <fstream> +#include <map> +#include <memory> +#include <stdexcept> + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/auto_cleaners.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/logging/operations.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/child.ipp" +#include "utils/process/deadline_killer.hpp" +#include "utils/process/isolation.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" +#include "utils/signals/timer.hpp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace signals = utils::signals; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Template for temporary directories created by the executor. +static const char* work_directory_template = PACKAGE_TARNAME ".XXXXXX"; + + +/// Mapping of active subprocess PIDs to their execution data. +typedef std::map< int, executor::exec_handle > exec_handles_map; + + +} // anonymous namespace + + +/// Basename of the file containing the stdout of the subprocess. +const char* utils::process::executor::detail::stdout_name = "stdout.txt"; + + +/// Basename of the file containing the stderr of the subprocess. +const char* utils::process::executor::detail::stderr_name = "stderr.txt"; + + +/// Basename of the subdirectory in which the subprocess is actually executed. +/// +/// This is a subdirectory of the "unique work directory" generated for the +/// subprocess so that our code can create control files on disk and not +/// get them clobbered by the subprocess's activity. +const char* utils::process::executor::detail::work_subdir = "work"; + + +/// Prepares a subprocess to run a user-provided hook in a controlled manner. +/// +/// \param unprivileged_user User to switch to if not none. +/// \param control_directory Path to the subprocess-specific control directory. +/// \param work_directory Path to the subprocess-specific work directory. +void +utils::process::executor::detail::setup_child( + const optional< passwd::user > unprivileged_user, + const fs::path& control_directory, + const fs::path& work_directory) +{ + logging::set_inmemory(); + process::isolate_path(unprivileged_user, control_directory); + process::isolate_child(unprivileged_user, work_directory); +} + + +/// Internal implementation for the exit_handle class. +struct utils::process::executor::exec_handle::impl : utils::noncopyable { + /// PID of the process being run. + int pid; + + /// Path to the subprocess-specific work directory. + fs::path control_directory; + + /// Path to the subprocess's stdout file. + const fs::path stdout_file; + + /// Path to the subprocess's stderr file. + const fs::path stderr_file; + + /// Start time. + datetime::timestamp start_time; + + /// User the subprocess is running as if different than the current one. + const optional< passwd::user > unprivileged_user; + + /// Timer to kill the subprocess on activation. + process::deadline_killer timer; + + /// Number of owners of the on-disk state. + executor::detail::refcnt_t state_owners; + + /// Constructor. + /// + /// \param pid_ PID of the forked process. + /// \param control_directory_ Path to the subprocess-specific work + /// directory. + /// \param stdout_file_ Path to the subprocess's stdout file. + /// \param stderr_file_ Path to the subprocess's stderr file. + /// \param start_time_ Timestamp of when this object was constructed. + /// \param timeout Maximum amount of time the subprocess can run for. + /// \param unprivileged_user_ User the subprocess is running as if + /// different than the current one. + /// \param [in,out] state_owners_ Number of owners of the on-disk state. + /// For first-time processes, this should be a new counter set to 0; + /// for followup processes, this should point to the same counter used + /// by the preceding process. + impl(const int pid_, + const fs::path& control_directory_, + const fs::path& stdout_file_, + const fs::path& stderr_file_, + const datetime::timestamp& start_time_, + const datetime::delta& timeout, + const optional< passwd::user > unprivileged_user_, + executor::detail::refcnt_t state_owners_) : + pid(pid_), + control_directory(control_directory_), + stdout_file(stdout_file_), + stderr_file(stderr_file_), + start_time(start_time_), + unprivileged_user(unprivileged_user_), + timer(timeout, pid_), + state_owners(state_owners_) + { + (*state_owners)++; + POST(*state_owners > 0); + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed internal implementation. +executor::exec_handle::exec_handle(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +executor::exec_handle::~exec_handle(void) +{ +} + + +/// Returns the PID of the process being run. +/// +/// \return A PID. +int +executor::exec_handle::pid(void) const +{ + return _pimpl->pid; +} + + +/// Returns the path to the subprocess-specific control directory. +/// +/// This is where the executor may store control files. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exec_handle::control_directory(void) const +{ + return _pimpl->control_directory; +} + + +/// Returns the path to the subprocess-specific work directory. +/// +/// This is guaranteed to be clear of files created by the executor. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exec_handle::work_directory(void) const +{ + return _pimpl->control_directory / detail::work_subdir; +} + + +/// Returns the path to the subprocess's stdout file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exec_handle::stdout_file(void) const +{ + return _pimpl->stdout_file; +} + + +/// Returns the path to the subprocess's stderr file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exec_handle::stderr_file(void) const +{ + return _pimpl->stderr_file; +} + + +/// Internal implementation for the exit_handle class. +struct utils::process::executor::exit_handle::impl : utils::noncopyable { + /// Original PID of the terminated subprocess. + /// + /// Note that this PID is no longer valid and cannot be used on system + /// tables! + const int original_pid; + + /// Termination status of the subprocess, or none if it timed out. + const optional< process::status > status; + + /// The user the process ran as, if different than the current one. + const optional< passwd::user > unprivileged_user; + + /// Timestamp of when the subprocess was spawned. + const datetime::timestamp start_time; + + /// Timestamp of when wait() or wait_any() returned this object. + const datetime::timestamp end_time; + + /// Path to the subprocess-specific work directory. + const fs::path control_directory; + + /// Path to the subprocess's stdout file. + const fs::path stdout_file; + + /// Path to the subprocess's stderr file. + const fs::path stderr_file; + + /// Number of owners of the on-disk state. + /// + /// This will be 1 if this exit_handle is the last holder of the on-disk + /// state, in which case cleanup() invocations will wipe the disk state. + /// For all other cases, this will hold a higher value. + detail::refcnt_t state_owners; + + /// Mutable pointer to the corresponding executor state. + /// + /// This object references a member of the executor_handle that yielded this + /// exit_handle instance. We need this direct access to clean up after + /// ourselves when the handle is destroyed. + exec_handles_map& all_exec_handles; + + /// Whether the subprocess state has been cleaned yet or not. + /// + /// Used to keep track of explicit calls to the public cleanup(). + bool cleaned; + + /// Constructor. + /// + /// \param original_pid_ Original PID of the terminated subprocess. + /// \param status_ Termination status of the subprocess, or none if + /// timed out. + /// \param unprivileged_user_ The user the process ran as, if different than + /// the current one. + /// \param start_time_ Timestamp of when the subprocess was spawned. + /// \param end_time_ Timestamp of when wait() or wait_any() returned this + /// object. + /// \param control_directory_ Path to the subprocess-specific work + /// directory. + /// \param stdout_file_ Path to the subprocess's stdout file. + /// \param stderr_file_ Path to the subprocess's stderr file. + /// \param [in,out] state_owners_ Number of owners of the on-disk state. + /// \param [in,out] all_exec_handles_ Global object keeping track of all + /// active executions for an executor. This is a pointer to a member of + /// the executor_handle object. + impl(const int original_pid_, + const optional< process::status > status_, + const optional< passwd::user > unprivileged_user_, + const datetime::timestamp& start_time_, + const datetime::timestamp& end_time_, + const fs::path& control_directory_, + const fs::path& stdout_file_, + const fs::path& stderr_file_, + detail::refcnt_t state_owners_, + exec_handles_map& all_exec_handles_) : + original_pid(original_pid_), status(status_), + unprivileged_user(unprivileged_user_), + start_time(start_time_), end_time(end_time_), + control_directory(control_directory_), + stdout_file(stdout_file_), stderr_file(stderr_file_), + state_owners(state_owners_), + all_exec_handles(all_exec_handles_), cleaned(false) + { + } + + /// Destructor. + ~impl(void) + { + if (!cleaned) { + LW(F("Implicitly cleaning up exit_handle for exec_handle %s; " + "ignoring errors!") % original_pid); + try { + cleanup(); + } catch (const std::runtime_error& error) { + LE(F("Subprocess cleanup failed: %s") % error.what()); + } + } + } + + /// Cleans up the subprocess on-disk state. + /// + /// \throw engine::error If the cleanup fails, especially due to the + /// inability to remove the work directory. + void + cleanup(void) + { + PRE(*state_owners > 0); + if (*state_owners == 1) { + LI(F("Cleaning up exit_handle for exec_handle %s") % original_pid); + fs::rm_r(control_directory); + } else { + LI(F("Not cleaning up exit_handle for exec_handle %s; " + "%s owners left") % original_pid % (*state_owners - 1)); + } + // We must decrease our reference only after we have successfully + // cleaned up the control directory. Otherwise, the rm_r call would + // throw an exception, which would in turn invoke the implicit cleanup + // from the destructor, which would make us crash due to an invalid + // reference count. + (*state_owners)--; + // Marking this object as clean here, even if we did not do actually the + // cleaning above, is fine (albeit a bit confusing). Note that "another + // owner" refers to a handle for a different PID, so that handle will be + // the one issuing the cleanup. + all_exec_handles.erase(original_pid); + cleaned = true; + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed internal implementation. +executor::exit_handle::exit_handle(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +executor::exit_handle::~exit_handle(void) +{ +} + + +/// Cleans up the subprocess status. +/// +/// This function should be called explicitly as it provides the means to +/// control any exceptions raised during cleanup. Do not rely on the destructor +/// to clean things up. +/// +/// \throw engine::error If the cleanup fails, especially due to the inability +/// to remove the work directory. +void +executor::exit_handle::cleanup(void) +{ + PRE(!_pimpl->cleaned); + _pimpl->cleanup(); + POST(_pimpl->cleaned); +} + + +/// Gets the current number of owners of the on-disk data. +/// +/// \return A shared reference counter. Even though this function is marked as +/// const, the return value is intentionally mutable because we need to update +/// reference counts from different but related processes. This is why this +/// method is not public. +std::shared_ptr< std::size_t > +executor::exit_handle::state_owners(void) const +{ + return _pimpl->state_owners; +} + + +/// Returns the original PID corresponding to the terminated subprocess. +/// +/// \return An exec_handle. +int +executor::exit_handle::original_pid(void) const +{ + return _pimpl->original_pid; +} + + +/// Returns the process termination status of the subprocess. +/// +/// \return A process termination status, or none if the subprocess timed out. +const optional< process::status >& +executor::exit_handle::status(void) const +{ + return _pimpl->status; +} + + +/// Returns the user the process ran as if different than the current one. +/// +/// \return None if the credentials of the process were the same as the current +/// one, or else a user. +const optional< passwd::user >& +executor::exit_handle::unprivileged_user(void) const +{ + return _pimpl->unprivileged_user; +} + + +/// Returns the timestamp of when the subprocess was spawned. +/// +/// \return A timestamp. +const datetime::timestamp& +executor::exit_handle::start_time(void) const +{ + return _pimpl->start_time; +} + + +/// Returns the timestamp of when wait() or wait_any() returned this object. +/// +/// \return A timestamp. +const datetime::timestamp& +executor::exit_handle::end_time(void) const +{ + return _pimpl->end_time; +} + + +/// Returns the path to the subprocess-specific control directory. +/// +/// This is where the executor may store control files. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exit_handle::control_directory(void) const +{ + return _pimpl->control_directory; +} + + +/// Returns the path to the subprocess-specific work directory. +/// +/// This is guaranteed to be clear of files created by the executor. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exit_handle::work_directory(void) const +{ + return _pimpl->control_directory / detail::work_subdir; +} + + +/// Returns the path to the subprocess's stdout file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exit_handle::stdout_file(void) const +{ + return _pimpl->stdout_file; +} + + +/// Returns the path to the subprocess's stderr file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exit_handle::stderr_file(void) const +{ + return _pimpl->stderr_file; +} + + +/// Internal implementation for the executor_handle. +/// +/// Because the executor is a singleton, these essentially is a container for +/// global variables. +struct utils::process::executor::executor_handle::impl : utils::noncopyable { + /// Numeric counter of executed subprocesses. + /// + /// This is used to generate a unique identifier for each subprocess as an + /// easy mechanism to discern their unique work directories. + size_t last_subprocess; + + /// Interrupts handler. + std::auto_ptr< signals::interrupts_handler > interrupts_handler; + + /// Root work directory for all executed subprocesses. + std::auto_ptr< fs::auto_directory > root_work_directory; + + /// Mapping of PIDs to the data required at run time. + exec_handles_map all_exec_handles; + + /// Whether the executor state has been cleaned yet or not. + /// + /// Used to keep track of explicit calls to the public cleanup(). + bool cleaned; + + /// Constructor. + impl(void) : + last_subprocess(0), + interrupts_handler(new signals::interrupts_handler()), + root_work_directory(new fs::auto_directory( + fs::auto_directory::mkdtemp_public(work_directory_template))), + cleaned(false) + { + } + + /// Destructor. + ~impl(void) + { + if (!cleaned) { + LW("Implicitly cleaning up executor; ignoring errors!"); + try { + cleanup(); + cleaned = true; + } catch (const std::runtime_error& error) { + LE(F("Executor global cleanup failed: %s") % error.what()); + } + } + } + + /// Cleans up the executor state. + void + cleanup(void) + { + PRE(!cleaned); + + for (exec_handles_map::const_iterator iter = all_exec_handles.begin(); + iter != all_exec_handles.end(); ++iter) { + const int& pid = (*iter).first; + const exec_handle& data = (*iter).second; + + process::terminate_group(pid); + int status; + if (::waitpid(pid, &status, 0) == -1) { + // Should not happen. + LW(F("Failed to wait for PID %s") % pid); + } + + try { + fs::rm_r(data.control_directory()); + } catch (const fs::error& e) { + LE(F("Failed to clean up subprocess work directory %s: %s") % + data.control_directory() % e.what()); + } + } + all_exec_handles.clear(); + + try { + // The following only causes the work directory to be deleted, not + // any of its contents, so we expect this to always succeed. This + // *should* be sufficient because, in the loop above, we have + // individually wiped the subdirectories of any still-unclean + // subprocesses. + root_work_directory->cleanup(); + } catch (const fs::error& e) { + LE(F("Failed to clean up executor work directory %s: %s; this is " + "an internal error") % root_work_directory->directory() + % e.what()); + } + root_work_directory.reset(NULL); + + interrupts_handler->unprogram(); + interrupts_handler.reset(NULL); + } + + /// Common code to run after any of the wait calls. + /// + /// \param original_pid The PID of the terminated subprocess. + /// \param status The exit status of the terminated subprocess. + /// + /// \return A pointer to an object describing the waited-for subprocess. + executor::exit_handle + post_wait(const int original_pid, const process::status& status) + { + PRE(original_pid == status.dead_pid()); + LI(F("Waited for subprocess with exec_handle %s") % original_pid); + + process::terminate_group(status.dead_pid()); + + const exec_handles_map::iterator iter = all_exec_handles.find( + original_pid); + exec_handle& data = (*iter).second; + data._pimpl->timer.unprogram(); + + // It is tempting to assert here (and old code did) that, if the timer + // has fired, the process has been forcibly killed by us. This is not + // always the case though: for short-lived processes and with very short + // timeouts (think 1ms), it is possible for scheduling decisions to + // allow the subprocess to finish while at the same time cause the timer + // to fire. So we do not assert this any longer and just rely on the + // timer expiration to check if the process timed out or not. If the + // process did finish but the timer expired... oh well, we do not detect + // this correctly but we don't care because this should not really + // happen. + + if (!fs::exists(data.stdout_file())) { + std::ofstream new_stdout(data.stdout_file().c_str()); + } + if (!fs::exists(data.stderr_file())) { + std::ofstream new_stderr(data.stderr_file().c_str()); + } + + return exit_handle(std::shared_ptr< exit_handle::impl >( + new exit_handle::impl( + data.pid(), + data._pimpl->timer.fired() ? + none : utils::make_optional(status), + data._pimpl->unprivileged_user, + data._pimpl->start_time, datetime::timestamp::now(), + data.control_directory(), + data.stdout_file(), + data.stderr_file(), + data._pimpl->state_owners, + all_exec_handles))); + } +}; + + +/// Constructor. +executor::executor_handle::executor_handle(void) throw() : _pimpl(new impl()) +{ +} + + +/// Destructor. +executor::executor_handle::~executor_handle(void) +{ +} + + +/// Queries the path to the root of the work directory for all subprocesses. +/// +/// \return A path. +const fs::path& +executor::executor_handle::root_work_directory(void) const +{ + return _pimpl->root_work_directory->directory(); +} + + +/// Cleans up the executor state. +/// +/// This function should be called explicitly as it provides the means to +/// control any exceptions raised during cleanup. Do not rely on the destructor +/// to clean things up. +/// +/// \throw engine::error If there are problems cleaning up the executor. +void +executor::executor_handle::cleanup(void) +{ + PRE(!_pimpl->cleaned); + _pimpl->cleanup(); + _pimpl->cleaned = true; +} + + +/// Initializes the executor. +/// +/// \pre This function can only be called if there is no other executor_handle +/// object alive. +/// +/// \return A handle to the operations of the executor. +executor::executor_handle +executor::setup(void) +{ + return executor_handle(); +} + + +/// Pre-helper for the spawn() method. +/// +/// \return The created control directory for the subprocess. +fs::path +executor::executor_handle::spawn_pre(void) +{ + signals::check_interrupt(); + + ++_pimpl->last_subprocess; + + const fs::path control_directory = + _pimpl->root_work_directory->directory() / + (F("%s") % _pimpl->last_subprocess); + fs::mkdir_p(control_directory / detail::work_subdir, 0755); + + return control_directory; +} + + +/// Post-helper for the spawn() method. +/// +/// \param control_directory Control directory as returned by spawn_pre(). +/// \param stdout_file Path to the subprocess' stdout. +/// \param stderr_file Path to the subprocess' stderr. +/// \param timeout Maximum amount of time the subprocess can run for. +/// \param unprivileged_user If not none, user to switch to before execution. +/// \param child The process created by spawn(). +/// +/// \return The execution handle of the started subprocess. +executor::exec_handle +executor::executor_handle::spawn_post( + const fs::path& control_directory, + const fs::path& stdout_file, + const fs::path& stderr_file, + const datetime::delta& timeout, + const optional< passwd::user > unprivileged_user, + std::auto_ptr< process::child > child) +{ + const exec_handle handle(std::shared_ptr< exec_handle::impl >( + new exec_handle::impl( + child->pid(), + control_directory, + stdout_file, + stderr_file, + datetime::timestamp::now(), + timeout, + unprivileged_user, + detail::refcnt_t(new detail::refcnt_t::element_type(0))))); + INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) == + _pimpl->all_exec_handles.end(), + F("PID %s already in all_exec_handles; not properly cleaned " + "up or reused too fast") % handle.pid());; + _pimpl->all_exec_handles.insert(exec_handles_map::value_type( + handle.pid(), handle)); + LI(F("Spawned subprocess with exec_handle %s") % handle.pid()); + return handle; +} + + +/// Pre-helper for the spawn_followup() method. +void +executor::executor_handle::spawn_followup_pre(void) +{ + signals::check_interrupt(); +} + + +/// Post-helper for the spawn_followup() method. +/// +/// \param base Exit handle of the subprocess to use as context. +/// \param timeout Maximum amount of time the subprocess can run for. +/// \param child The process created by spawn_followup(). +/// +/// \return The execution handle of the started subprocess. +executor::exec_handle +executor::executor_handle::spawn_followup_post( + const exit_handle& base, + const datetime::delta& timeout, + std::auto_ptr< process::child > child) +{ + INV(*base.state_owners() > 0); + const exec_handle handle(std::shared_ptr< exec_handle::impl >( + new exec_handle::impl( + child->pid(), + base.control_directory(), + base.stdout_file(), + base.stderr_file(), + datetime::timestamp::now(), + timeout, + base.unprivileged_user(), + base.state_owners()))); + INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) == + _pimpl->all_exec_handles.end(), + F("PID %s already in all_exec_handles; not properly cleaned " + "up or reused too fast") % handle.pid());; + _pimpl->all_exec_handles.insert(exec_handles_map::value_type( + handle.pid(), handle)); + LI(F("Spawned subprocess with exec_handle %s") % handle.pid()); + return handle; +} + + +/// Waits for completion of any forked process. +/// +/// \param exec_handle The handle of the process to wait for. +/// +/// \return A pointer to an object describing the waited-for subprocess. +executor::exit_handle +executor::executor_handle::wait(const exec_handle exec_handle) +{ + signals::check_interrupt(); + const process::status status = process::wait(exec_handle.pid()); + return _pimpl->post_wait(exec_handle.pid(), status); +} + + +/// Waits for completion of any forked process. +/// +/// \return A pointer to an object describing the waited-for subprocess. +executor::exit_handle +executor::executor_handle::wait_any(void) +{ + signals::check_interrupt(); + const process::status status = process::wait_any(); + return _pimpl->post_wait(status.dead_pid(), status); +} + + +/// Checks if an interrupt has fired. +/// +/// Calls to this function should be sprinkled in strategic places through the +/// code protected by an interrupts_handler object. +/// +/// This is just a wrapper over signals::check_interrupt() to avoid leaking this +/// dependency to the caller. +/// +/// \throw signals::interrupted_error If there has been an interrupt. +void +executor::executor_handle::check_interrupt(void) const +{ + signals::check_interrupt(); +} diff --git a/utils/process/executor.hpp b/utils/process/executor.hpp new file mode 100644 index 000000000000..858ad9c815aa --- /dev/null +++ b/utils/process/executor.hpp @@ -0,0 +1,231 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/executor.hpp +/// Multiprogrammed process executor with isolation guarantees. +/// +/// This module provides a mechanism to invoke more than one process +/// concurrently while at the same time ensuring that each process is run +/// in a clean container and in a "safe" work directory that gets cleaned +/// up automatically on termination. +/// +/// The intended workflow for using this module is the following: +/// +/// 1) Initialize the executor using setup(). Keep the returned object +/// around through the lifetime of the next operations. Only one +/// instance of the executor can be alive at once. +/// 2) Spawn one or more processes with spawn(). On the caller side, keep +/// track of any per-process data you may need using the returned +/// exec_handle, which is unique among the set of active processes. +/// 3) Call wait() or wait_any() to wait for completion of a process started +/// in the previous step. Repeat as desired. +/// 4) Use the returned exit_handle object by wait() or wait_any() to query +/// the status of the terminated process and/or to access any of its +/// data files. +/// 5) Invoke cleanup() on the exit_handle to wipe any stale data. +/// 6) Invoke cleanup() on the object returned by setup(). +/// +/// It is the responsibility of the caller to ensure that calls to +/// spawn() and spawn_followup() are balanced with wait() and wait_any() calls. +/// +/// Processes executed in this manner have access to two different "unique" +/// directories: the first is the "work directory", which is an empty directory +/// that acts as the subprocess' work directory; the second is the "control +/// directory", which is the location where the in-process code may place files +/// that are not clobbered by activities in the work directory. + +#if !defined(UTILS_PROCESS_EXECUTOR_HPP) +#define UTILS_PROCESS_EXECUTOR_HPP + +#include "utils/process/executor_fwd.hpp" + +#include <cstddef> +#include <memory> + +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/passwd_fwd.hpp" +#include "utils/process/child_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { +namespace process { +namespace executor { + + +namespace detail { + + +extern const char* stdout_name; +extern const char* stderr_name; +extern const char* work_subdir; + + +/// Shared reference counter. +typedef std::shared_ptr< std::size_t > refcnt_t; + + +void setup_child(const utils::optional< utils::passwd::user >, + const utils::fs::path&, const utils::fs::path&); + + +} // namespace detail + + +/// Maintenance data held while a subprocess is being executed. +/// +/// This data structure exists from the moment a subprocess is executed via +/// executor::spawn() to when it is cleaned up with exit_handle::cleanup(). +/// +/// The caller NEED NOT maintain this object alive for the execution of the +/// subprocess. However, the PID contained in here can be used to match +/// exec_handle objects with corresponding exit_handle objects via their +/// original_pid() method. +/// +/// Objects of this type can be copied around but their implementation is +/// shared. The implication of this is that only the last copy of a given exit +/// handle will execute the automatic cleanup() on destruction. +class exec_handle { + struct impl; + + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class executor_handle; + exec_handle(std::shared_ptr< impl >); + +public: + ~exec_handle(void); + + int pid(void) const; + utils::fs::path control_directory(void) const; + utils::fs::path work_directory(void) const; + const utils::fs::path& stdout_file(void) const; + const utils::fs::path& stderr_file(void) const; +}; + + +/// Container for the data of a process termination. +/// +/// This handle provides access to the details of the process that terminated +/// and serves as the owner of the remaining on-disk files. The caller is +/// expected to call cleanup() before destruction to remove the on-disk state. +/// +/// Objects of this type can be copied around but their implementation is +/// shared. The implication of this is that only the last copy of a given exit +/// handle will execute the automatic cleanup() on destruction. +class exit_handle { + struct impl; + + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class executor_handle; + exit_handle(std::shared_ptr< impl >); + + detail::refcnt_t state_owners(void) const; + +public: + ~exit_handle(void); + + void cleanup(void); + + int original_pid(void) const; + const utils::optional< utils::process::status >& status(void) const; + const utils::optional< utils::passwd::user >& unprivileged_user(void) const; + const utils::datetime::timestamp& start_time() const; + const utils::datetime::timestamp& end_time() const; + utils::fs::path control_directory(void) const; + utils::fs::path work_directory(void) const; + const utils::fs::path& stdout_file(void) const; + const utils::fs::path& stderr_file(void) const; +}; + + +/// Handler for the livelihood of the executor. +/// +/// Objects of this type can be copied around (because we do not have move +/// semantics...) but their implementation is shared. Only one instance of the +/// executor can exist at any point in time. +class executor_handle { + struct impl; + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend executor_handle setup(void); + executor_handle(void) throw(); + + utils::fs::path spawn_pre(void); + exec_handle spawn_post(const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&, + const utils::datetime::delta&, + const utils::optional< utils::passwd::user >, + std::auto_ptr< utils::process::child >); + + void spawn_followup_pre(void); + exec_handle spawn_followup_post(const exit_handle&, + const utils::datetime::delta&, + std::auto_ptr< utils::process::child >); + +public: + ~executor_handle(void); + + const utils::fs::path& root_work_directory(void) const; + + void cleanup(void); + + template< class Hook > + exec_handle spawn(Hook, + const datetime::delta&, + const utils::optional< utils::passwd::user >, + const utils::optional< utils::fs::path > = utils::none, + const utils::optional< utils::fs::path > = utils::none); + + template< class Hook > + exec_handle spawn_followup(Hook, + const exit_handle&, + const datetime::delta&); + + exit_handle wait(const exec_handle); + exit_handle wait_any(void); + + void check_interrupt(void) const; +}; + + +executor_handle setup(void); + + +} // namespace executor +} // namespace process +} // namespace utils + + +#endif // !defined(UTILS_PROCESS_EXECUTOR_HPP) diff --git a/utils/process/executor.ipp b/utils/process/executor.ipp new file mode 100644 index 000000000000..e91f994673d7 --- /dev/null +++ b/utils/process/executor.ipp @@ -0,0 +1,182 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_PROCESS_EXECUTOR_IPP) +#define UTILS_PROCESS_EXECUTOR_IPP + +#include "utils/process/executor.hpp" + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/child.ipp" + +namespace utils { +namespace process { + + +namespace executor { +namespace detail { + +/// Functor to execute a hook in a child process. +/// +/// The hook is executed after the process has been isolated per the logic in +/// utils::process::isolation based on the input parameters at construction +/// time. +template< class Hook > +class run_child { + /// Function or functor to invoke in the child. + Hook _hook; + + /// Directory where the hook may place control files. + const fs::path& _control_directory; + + /// Directory to enter when running the subprocess. + /// + /// This is a subdirectory of _control_directory but is separate so that + /// subprocess operations do not inadvertently affect our files. + const fs::path& _work_directory; + + /// User to switch to when running the subprocess. + /// + /// If not none, the subprocess will be executed as the provided user and + /// the control and work directories will be writable by this user. + const optional< passwd::user > _unprivileged_user; + +public: + /// Constructor. + /// + /// \param hook Function or functor to invoke in the child. + /// \param control_directory Directory where control files can be placed. + /// \param work_directory Directory to enter when running the subprocess. + /// \param unprivileged_user If set, user to switch to before execution. + run_child(Hook hook, + const fs::path& control_directory, + const fs::path& work_directory, + const optional< passwd::user > unprivileged_user) : + _hook(hook), + _control_directory(control_directory), + _work_directory(work_directory), + _unprivileged_user(unprivileged_user) + { + } + + /// Body of the subprocess. + void + operator()(void) + { + executor::detail::setup_child(_unprivileged_user, + _control_directory, _work_directory); + _hook(_control_directory); + } +}; + +} // namespace detail +} // namespace executor + + +/// Forks and executes a subprocess asynchronously. +/// +/// \tparam Hook Type of the hook. +/// \param hook Function or functor to run in the subprocess. +/// \param timeout Maximum amount of time the subprocess can run for. +/// \param unprivileged_user If not none, user to switch to before execution. +/// \param stdout_target If not none, file to which to write the stdout of the +/// test case. +/// \param stderr_target If not none, file to which to write the stderr of the +/// test case. +/// +/// \return A handle for the background operation. Used to match the result of +/// the execution returned by wait_any() with this invocation. +template< class Hook > +executor::exec_handle +executor::executor_handle::spawn( + Hook hook, + const datetime::delta& timeout, + const optional< passwd::user > unprivileged_user, + const optional< fs::path > stdout_target, + const optional< fs::path > stderr_target) +{ + const fs::path unique_work_directory = spawn_pre(); + + const fs::path stdout_path = stdout_target ? + stdout_target.get() : (unique_work_directory / detail::stdout_name); + const fs::path stderr_path = stderr_target ? + stderr_target.get() : (unique_work_directory / detail::stderr_name); + + std::auto_ptr< process::child > child = process::child::fork_files( + detail::run_child< Hook >(hook, + unique_work_directory, + unique_work_directory / detail::work_subdir, + unprivileged_user), + stdout_path, stderr_path); + + return spawn_post(unique_work_directory, stdout_path, stderr_path, + timeout, unprivileged_user, child); +} + + +/// Forks and executes a subprocess asynchronously in the context of another. +/// +/// By context we understand the on-disk state of a previously-executed process, +/// thus the new subprocess spawned by this function will run with the same +/// control and work directories as another process. +/// +/// \tparam Hook Type of the hook. +/// \param hook Function or functor to run in the subprocess. +/// \param base Context of the subprocess in which to run this one. The +/// exit_handle provided here must remain alive throughout the existence of +/// this other object because the original exit_handle is the one that owns +/// the on-disk state. +/// \param timeout Maximum amount of time the subprocess can run for. +/// +/// \return A handle for the background operation. Used to match the result of +/// the execution returned by wait_any() with this invocation. +template< class Hook > +executor::exec_handle +executor::executor_handle::spawn_followup(Hook hook, + const exit_handle& base, + const datetime::delta& timeout) +{ + spawn_followup_pre(); + + std::auto_ptr< process::child > child = process::child::fork_files( + detail::run_child< Hook >(hook, + base.control_directory(), + base.work_directory(), + base.unprivileged_user()), + base.stdout_file(), base.stderr_file()); + + return spawn_followup_post(base, timeout, child); +} + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_EXECUTOR_IPP) diff --git a/utils/process/executor_fwd.hpp b/utils/process/executor_fwd.hpp new file mode 100644 index 000000000000..ec63227993f3 --- /dev/null +++ b/utils/process/executor_fwd.hpp @@ -0,0 +1,49 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/executor_fwd.hpp +/// Forward declarations for utils/process/executor.hpp + +#if !defined(UTILS_PROCESS_EXECUTOR_FWD_HPP) +#define UTILS_PROCESS_EXECUTOR_FWD_HPP + +namespace utils { +namespace process { +namespace executor { + + +class exec_handle; +class executor_handle; +class exit_handle; + + +} // namespace executor +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_EXECUTOR_FWD_HPP) diff --git a/utils/process/executor_test.cpp b/utils/process/executor_test.cpp new file mode 100644 index 000000000000..13ae69bd44ed --- /dev/null +++ b/utils/process/executor_test.cpp @@ -0,0 +1,940 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/executor.ipp" + +extern "C" { +#include <sys/types.h> +#include <sys/time.h> +#include <sys/wait.h> + +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <fstream> +#include <iostream> +#include <vector> + +#include <atf-c++.hpp> + +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/stacktrace.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace signals = utils::signals; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Large timeout for the processes we spawn. +/// +/// This number is supposed to be (much) larger than the timeout of the test +/// cases that use it so that children processes are not killed spuriously. +static const datetime::delta infinite_timeout(1000000, 0); + + +static void do_exit(const int) UTILS_NORETURN; + + +/// Terminates a subprocess without invoking destructors. +/// +/// This is just a simple wrapper over _exit(2) because we cannot use std::exit +/// on exit from a subprocess. The reason is that we do not want to invoke any +/// destructors as otherwise we'd clear up the global executor state by mistake. +/// This wouldn't be a major problem if it wasn't because doing so deletes +/// on-disk files and we want to leave them in place so that the parent process +/// can test for them! +/// +/// \param exit_code Code to exit with. +static void +do_exit(const int exit_code) +{ + std::cout.flush(); + std::cerr.flush(); + ::_exit(exit_code); +} + + +/// Subprocess that creates a cookie file in its work directory. +class child_create_cookie { + /// Name of the cookie to create. + const std::string _cookie_name; + +public: + /// Constructor. + /// + /// \param cookie_name Name of the cookie to create. + child_create_cookie(const std::string& cookie_name) : + _cookie_name(cookie_name) + { + } + + /// Runs the subprocess. + void + operator()(const fs::path& /* control_directory */) + UTILS_NORETURN + { + std::cout << "Creating cookie: " << _cookie_name << " (stdout)\n"; + std::cerr << "Creating cookie: " << _cookie_name << " (stderr)\n"; + atf::utils::create_file(_cookie_name, ""); + do_exit(EXIT_SUCCESS); + } +}; + + +static void child_delete_all(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that deletes all files in the current directory. +/// +/// This is intended to validate that the test runs in an empty directory, +/// separate from any control files that the executor may have created. +/// +/// \param control_directory Directory where control files separate from the +/// work directory can be placed. +static void +child_delete_all(const fs::path& control_directory) +{ + const fs::path cookie = control_directory / "exec_was_called"; + std::ofstream control_file(cookie.c_str()); + if (!control_file) { + std::cerr << "Failed to create " << cookie << '\n'; + std::abort(); + } + + const int exit_code = ::system("rm *") == -1 + ? EXIT_FAILURE : EXIT_SUCCESS; + do_exit(exit_code); +} + + +static void child_dump_unprivileged_user(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that dumps user configuration. +static void +child_dump_unprivileged_user(const fs::path& /* control_directory */) +{ + const passwd::user current_user = passwd::current_user(); + std::cout << F("UID = %s\n") % current_user.uid; + do_exit(EXIT_SUCCESS); +} + + +/// Subprocess that returns a specific exit code. +class child_exit { + /// Exit code to return. + int _exit_code; + +public: + /// Constructor. + /// + /// \param exit_code Exit code to return. + child_exit(const int exit_code) : _exit_code(exit_code) + { + } + + /// Runs the subprocess. + void + operator()(const fs::path& /* control_directory */) + UTILS_NORETURN + { + do_exit(_exit_code); + } +}; + + +static void child_pause(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that just blocks. +static void +child_pause(const fs::path& /* control_directory */) +{ + sigset_t mask; + sigemptyset(&mask); + for (;;) { + ::sigsuspend(&mask); + } + std::abort(); +} + + +static void child_print(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that writes to stdout and stderr. +static void +child_print(const fs::path& /* control_directory */) +{ + std::cout << "stdout: some text\n"; + std::cerr << "stderr: some other text\n"; + + do_exit(EXIT_SUCCESS); +} + + +/// Subprocess that sleeps for a period of time before exiting. +class child_sleep { + /// Seconds to sleep for before termination. + int _seconds; + +public: + /// Construtor. + /// + /// \param seconds Seconds to sleep for before termination. + child_sleep(const int seconds) : _seconds(seconds) + { + } + + /// Runs the subprocess. + void + operator()(const fs::path& /* control_directory */) + UTILS_NORETURN + { + ::sleep(_seconds); + do_exit(EXIT_SUCCESS); + } +}; + + +static void child_spawn_blocking_child(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that spawns a subchild that gets stuck. +/// +/// Used by the caller to validate that the whole process tree is terminated +/// when this subprocess is killed. +static void +child_spawn_blocking_child( + const fs::path& /* control_directory */) +{ + pid_t pid = ::fork(); + if (pid == -1) { + std::cerr << "Cannot fork subprocess\n"; + do_exit(EXIT_FAILURE); + } else if (pid == 0) { + for (;;) + ::pause(); + } else { + const fs::path name = fs::path(utils::getenv("CONTROL_DIR").get()) / + "pid"; + std::ofstream pidfile(name.c_str()); + if (!pidfile) { + std::cerr << "Failed to create the pidfile\n"; + do_exit(EXIT_FAILURE); + } + pidfile << pid; + pidfile.close(); + do_exit(EXIT_SUCCESS); + } +} + + +static void child_validate_isolation(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that checks if isolate_child() has been called. +static void +child_validate_isolation(const fs::path& /* control_directory */) +{ + if (utils::getenv("HOME").get() == "fake-value") { + std::cerr << "HOME not reset\n"; + do_exit(EXIT_FAILURE); + } + if (utils::getenv("LANG")) { + std::cerr << "LANG not unset\n"; + do_exit(EXIT_FAILURE); + } + do_exit(EXIT_SUCCESS); +} + + +/// Invokes executor::spawn() with default arguments. +/// +/// \param handle The executor on which to invoke spawn(). +/// \param args Arguments to the binary. +/// \param timeout Maximum time the program can run for. +/// \param unprivileged_user If set, user to switch to when running the child +/// program. +/// \param stdout_target If not none, file to which to write the stdout of the +/// test case. +/// \param stderr_target If not none, file to which to write the stderr of the +/// test case. +/// +/// \return The exec handle for the spawned binary. +template< class Hook > +static executor::exec_handle +do_spawn(executor::executor_handle& handle, Hook hook, + const datetime::delta& timeout = infinite_timeout, + const optional< passwd::user > unprivileged_user = none, + const optional< fs::path > stdout_target = none, + const optional< fs::path > stderr_target = none) +{ + const executor::exec_handle exec_handle = handle.spawn< Hook >( + hook, timeout, unprivileged_user, stdout_target, stderr_target); + return exec_handle; +} + + +/// Checks for a specific exit status in the status of a exit_handle. +/// +/// \param exit_status The expected exit status. +/// \param status The value of exit_handle.status(). +/// +/// \post Terminates the calling test case if the status does not match the +/// required value. +static void +require_exit(const int exit_status, const optional< process::status > status) +{ + ATF_REQUIRE(status); + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(exit_status, status.get().exitstatus()); +} + + +/// Ensures that a killed process is gone. +/// +/// The way we do this is by sending an idempotent signal to the given PID +/// and checking if the signal was delivered. If it was, the process is +/// still alive; if it was not, then it is gone. +/// +/// Note that this might be inaccurate for two reasons: +/// +/// 1) The system may have spawned a new process with the same pid as +/// our subchild... but in practice, this does not happen because +/// most systems do not immediately reuse pid numbers. If that +/// happens... well, we get a false test failure. +/// +/// 2) We ran so fast that even if the process was sent a signal to +/// die, it has not had enough time to process it yet. This is why +/// we retry this a few times. +/// +/// \param pid PID of the process to check. +static void +ensure_dead(const pid_t pid) +{ + int attempts = 30; +retry: + if (::kill(pid, SIGCONT) != -1 || errno != ESRCH) { + if (attempts > 0) { + std::cout << "Subprocess not dead yet; retrying wait\n"; + --attempts; + ::usleep(100000); + goto retry; + } + ATF_FAIL(F("The subprocess %s of our child was not killed") % pid); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_one); +ATF_TEST_CASE_BODY(integration__run_one) +{ + executor::executor_handle handle = executor::setup(); + + const executor::exec_handle exec_handle = do_spawn(handle, child_exit(41)); + + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid()); + require_exit(41, exit_handle.status()); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_many); +ATF_TEST_CASE_BODY(integration__run_many) +{ + static const std::size_t num_children = 30; + + executor::executor_handle handle = executor::setup(); + + std::size_t total_children = 0; + std::map< int, int > exp_exit_statuses; + std::map< int, datetime::timestamp > exp_start_times; + for (std::size_t i = 0; i < num_children; ++i) { + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 40, 0, i); + + for (std::size_t j = 0; j < 3; j++) { + const std::size_t id = i * 3 + j; + + datetime::set_mock_now(start_time); + const int pid = do_spawn(handle, child_exit(id)).pid(); + exp_exit_statuses.insert(std::make_pair(pid, id)); + exp_start_times.insert(std::make_pair(pid, start_time)); + ++total_children; + } + } + + for (std::size_t i = 0; i < total_children; ++i) { + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 50, 10, i); + datetime::set_mock_now(end_time); + executor::exit_handle exit_handle = handle.wait_any(); + const int original_pid = exit_handle.original_pid(); + + const int exit_status = exp_exit_statuses.find(original_pid)->second; + const datetime::timestamp& start_time = exp_start_times.find( + original_pid)->second; + + require_exit(exit_status, exit_handle.status()); + + ATF_REQUIRE_EQ(start_time, exit_handle.start_time()); + ATF_REQUIRE_EQ(end_time, exit_handle.end_time()); + + exit_handle.cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists( + exit_handle.stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + exit_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + exit_handle.work_directory().str())); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__parameters_and_output); +ATF_TEST_CASE_BODY(integration__parameters_and_output) +{ + executor::executor_handle handle = executor::setup(); + + const executor::exec_handle exec_handle = do_spawn(handle, child_print); + + executor::exit_handle exit_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid()); + + require_exit(EXIT_SUCCESS, exit_handle.status()); + + const fs::path stdout_file = exit_handle.stdout_file(); + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), "stdout: some text\n")); + const fs::path stderr_file = exit_handle.stderr_file(); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: some other text\n")); + + exit_handle.cleanup(); + ATF_REQUIRE(!fs::exists(stdout_file)); + ATF_REQUIRE(!fs::exists(stderr_file)); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__custom_output_files); +ATF_TEST_CASE_BODY(integration__custom_output_files) +{ + executor::executor_handle handle = executor::setup(); + + const fs::path stdout_file("custom-stdout.txt"); + const fs::path stderr_file("custom-stderr.txt"); + + const executor::exec_handle exec_handle = do_spawn( + handle, child_print, infinite_timeout, none, + utils::make_optional(stdout_file), + utils::make_optional(stderr_file)); + + executor::exit_handle exit_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid()); + + require_exit(EXIT_SUCCESS, exit_handle.status()); + + ATF_REQUIRE_EQ(stdout_file, exit_handle.stdout_file()); + ATF_REQUIRE_EQ(stderr_file, exit_handle.stderr_file()); + + exit_handle.cleanup(); + + handle.cleanup(); + + // Must compare after cleanup to ensure the files did not get deleted. + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), "stdout: some text\n")); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: some other text\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__timestamps); +ATF_TEST_CASE_BODY(integration__timestamps) +{ + executor::executor_handle handle = executor::setup(); + + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 35, 10, 1000); + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 35, 20, 2000); + + datetime::set_mock_now(start_time); + do_spawn(handle, child_exit(70)); + + datetime::set_mock_now(end_time); + executor::exit_handle exit_handle = handle.wait_any(); + + require_exit(70, exit_handle.status()); + + ATF_REQUIRE_EQ(start_time, exit_handle.start_time()); + ATF_REQUIRE_EQ(end_time, exit_handle.end_time()); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__files); +ATF_TEST_CASE_BODY(integration__files) +{ + executor::executor_handle handle = executor::setup(); + + do_spawn(handle, child_create_cookie("cookie.12345")); + + executor::exit_handle exit_handle = handle.wait_any(); + + ATF_REQUIRE(atf::utils::file_exists( + (exit_handle.work_directory() / "cookie.12345").str())); + + exit_handle.cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_handle.work_directory().str())); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__followup); +ATF_TEST_CASE_BODY(integration__followup) +{ + executor::executor_handle handle = executor::setup(); + + (void)handle.spawn(child_create_cookie("cookie.1"), infinite_timeout, none); + executor::exit_handle exit_1_handle = handle.wait_any(); + + (void)handle.spawn_followup(child_create_cookie("cookie.2"), exit_1_handle, + infinite_timeout); + executor::exit_handle exit_2_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_2_handle.stdout_file()); + ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_2_handle.stderr_file()); + ATF_REQUIRE_EQ(exit_1_handle.control_directory(), + exit_2_handle.control_directory()); + ATF_REQUIRE_EQ(exit_1_handle.work_directory(), + exit_2_handle.work_directory()); + + (void)handle.spawn_followup(child_create_cookie("cookie.3"), exit_2_handle, + infinite_timeout); + exit_2_handle.cleanup(); + exit_1_handle.cleanup(); + executor::exit_handle exit_3_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_3_handle.stdout_file()); + ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_3_handle.stderr_file()); + ATF_REQUIRE_EQ(exit_1_handle.control_directory(), + exit_3_handle.control_directory()); + ATF_REQUIRE_EQ(exit_1_handle.work_directory(), + exit_3_handle.work_directory()); + + ATF_REQUIRE(atf::utils::file_exists( + (exit_1_handle.work_directory() / "cookie.1").str())); + ATF_REQUIRE(atf::utils::file_exists( + (exit_1_handle.work_directory() / "cookie.2").str())); + ATF_REQUIRE(atf::utils::file_exists( + (exit_1_handle.work_directory() / "cookie.3").str())); + + ATF_REQUIRE(atf::utils::compare_file( + exit_1_handle.stdout_file().str(), + "Creating cookie: cookie.1 (stdout)\n" + "Creating cookie: cookie.2 (stdout)\n" + "Creating cookie: cookie.3 (stdout)\n")); + + ATF_REQUIRE(atf::utils::compare_file( + exit_1_handle.stderr_file().str(), + "Creating cookie: cookie.1 (stderr)\n" + "Creating cookie: cookie.2 (stderr)\n" + "Creating cookie: cookie.3 (stderr)\n")); + + exit_3_handle.cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.work_directory().str())); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__output_files_always_exist); +ATF_TEST_CASE_BODY(integration__output_files_always_exist) +{ + executor::executor_handle handle = executor::setup(); + + // This test is racy: we specify a very short timeout for the subprocess so + // that we cause the subprocess to exit before it has had time to set up the + // output files. However, for scheduling reasons, the subprocess may + // actually run to completion before the timer triggers. Retry this a few + // times to attempt to catch a "good test". + for (int i = 0; i < 50; i++) { + const executor::exec_handle exec_handle = + do_spawn(handle, child_exit(0), datetime::delta(0, 100000)); + executor::exit_handle exit_handle = handle.wait(exec_handle); + ATF_REQUIRE(fs::exists(exit_handle.stdout_file())); + ATF_REQUIRE(fs::exists(exit_handle.stderr_file())); + exit_handle.cleanup(); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE(integration__timeouts); +ATF_TEST_CASE_HEAD(integration__timeouts) +{ + set_md_var("timeout", "60"); +} +ATF_TEST_CASE_BODY(integration__timeouts) +{ + executor::executor_handle handle = executor::setup(); + + const executor::exec_handle exec_handle1 = + do_spawn(handle, child_sleep(30), datetime::delta(2, 0)); + const executor::exec_handle exec_handle2 = + do_spawn(handle, child_sleep(40), datetime::delta(5, 0)); + const executor::exec_handle exec_handle3 = + do_spawn(handle, child_exit(15)); + + { + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle3.pid(), exit_handle.original_pid()); + require_exit(15, exit_handle.status()); + exit_handle.cleanup(); + } + + { + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle1.pid(), exit_handle.original_pid()); + ATF_REQUIRE(!exit_handle.status()); + const datetime::delta duration = + exit_handle.end_time() - exit_handle.start_time(); + ATF_REQUIRE(duration < datetime::delta(10, 0)); + ATF_REQUIRE(duration >= datetime::delta(2, 0)); + exit_handle.cleanup(); + } + + { + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle2.pid(), exit_handle.original_pid()); + ATF_REQUIRE(!exit_handle.status()); + const datetime::delta duration = + exit_handle.end_time() - exit_handle.start_time(); + ATF_REQUIRE(duration < datetime::delta(10, 0)); + ATF_REQUIRE(duration >= datetime::delta(4, 0)); + exit_handle.cleanup(); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE(integration__unprivileged_user); +ATF_TEST_CASE_HEAD(integration__unprivileged_user) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(integration__unprivileged_user) +{ + executor::executor_handle handle = executor::setup(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + + do_spawn(handle, child_dump_unprivileged_user, + infinite_timeout, utils::make_optional(unprivileged_user)); + + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE(atf::utils::compare_file( + exit_handle.stdout_file().str(), + F("UID = %s\n") % unprivileged_user.uid)); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__auto_cleanup); +ATF_TEST_CASE_BODY(integration__auto_cleanup) +{ + std::vector< int > pids; + std::vector< fs::path > paths; + { + executor::executor_handle handle = executor::setup(); + + pids.push_back(do_spawn(handle, child_exit(10)).pid()); + pids.push_back(do_spawn(handle, child_exit(20)).pid()); + + // This invocation is never waited for below. This is intentional: we + // want the destructor to clean the "leaked" test automatically so that + // the clean up of the parent work directory also happens correctly. + pids.push_back(do_spawn(handle, child_pause).pid()); + + executor::exit_handle exit_handle1 = handle.wait_any(); + paths.push_back(exit_handle1.stdout_file()); + paths.push_back(exit_handle1.stderr_file()); + paths.push_back(exit_handle1.work_directory()); + + executor::exit_handle exit_handle2 = handle.wait_any(); + paths.push_back(exit_handle2.stdout_file()); + paths.push_back(exit_handle2.stderr_file()); + paths.push_back(exit_handle2.work_directory()); + } + for (std::vector< int >::const_iterator iter = pids.begin(); + iter != pids.end(); ++iter) { + ensure_dead(*iter); + } + for (std::vector< fs::path >::const_iterator iter = paths.begin(); + iter != paths.end(); ++iter) { + ATF_REQUIRE(!atf::utils::file_exists((*iter).str())); + } +} + + +/// Ensures that interrupting an executor cleans things up correctly. +/// +/// This test scenario is tricky. We spawn a master child process that runs the +/// executor code and we send a signal to it externally. The child process +/// spawns a bunch of tests that block indefinitely and tries to wait for their +/// results. When the signal is received, we expect an interrupt_error to be +/// raised, which in turn should clean up all test resources and exit the master +/// child process successfully. +/// +/// \param signo Signal to deliver to the executor. +static void +do_signal_handling_test(const int signo) +{ + static const char* cookie = "spawned.txt"; + + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + static const std::size_t num_children = 3; + + optional< fs::path > root_work_directory; + try { + executor::executor_handle handle = executor::setup(); + root_work_directory = handle.root_work_directory(); + + for (std::size_t i = 0; i < num_children; ++i) { + std::cout << "Spawned child number " << i << '\n'; + do_spawn(handle, child_pause); + } + + std::cout << "Creating " << cookie << " cookie\n"; + atf::utils::create_file(cookie, ""); + + std::cout << "Waiting for subprocess termination\n"; + for (std::size_t i = 0; i < num_children; ++i) { + executor::exit_handle exit_handle = handle.wait_any(); + // We may never reach this point in the test, but if we do let's + // make sure the subprocess was terminated as expected. + if (exit_handle.status()) { + if (exit_handle.status().get().signaled() && + exit_handle.status().get().termsig() == SIGKILL) { + // OK. + } else { + std::cerr << "Child exited with unexpected code: " + << exit_handle.status().get(); + std::exit(EXIT_FAILURE); + } + } else { + std::cerr << "Child timed out\n"; + std::exit(EXIT_FAILURE); + } + exit_handle.cleanup(); + } + std::cerr << "Terminating without reception of signal\n"; + std::exit(EXIT_FAILURE); + } catch (const signals::interrupted_error& unused_error) { + std::cerr << "Terminating due to interrupted_error\n"; + // We never kill ourselves until the cookie is created, so it is + // guaranteed that the optional root_work_directory has been + // initialized at this point. + if (atf::utils::file_exists(root_work_directory.get().str())) { + // Some cleanup did not happen; error out. + std::exit(EXIT_FAILURE); + } else { + std::exit(EXIT_SUCCESS); + } + } + std::abort(); + } + + std::cout << "Waiting for " << cookie << " cookie creation\n"; + while (!atf::utils::file_exists(cookie)) { + // Wait for processes. + } + ATF_REQUIRE(::unlink(cookie) != -1); + std::cout << "Killing process\n"; + ATF_REQUIRE(::kill(pid, signo) != -1); + + int status; + std::cout << "Waiting for process termination\n"; + ATF_REQUIRE(::waitpid(pid, &status, 0) != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__signal_handling); +ATF_TEST_CASE_BODY(integration__signal_handling) +{ + // This test scenario is racy so run it multiple times to have higher + // chances of exposing problems. + const std::size_t rounds = 20; + + for (std::size_t i = 0; i < rounds; ++i) { + std::cout << F("Testing round %s\n") % i; + do_signal_handling_test(SIGHUP); + do_signal_handling_test(SIGINT); + do_signal_handling_test(SIGTERM); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__isolate_child_is_called); +ATF_TEST_CASE_BODY(integration__isolate_child_is_called) +{ + executor::executor_handle handle = executor::setup(); + + utils::setenv("HOME", "fake-value"); + utils::setenv("LANG", "es_ES"); + do_spawn(handle, child_validate_isolation); + + executor::exit_handle exit_handle = handle.wait_any(); + require_exit(EXIT_SUCCESS, exit_handle.status()); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__process_group_is_terminated); +ATF_TEST_CASE_BODY(integration__process_group_is_terminated) +{ + utils::setenv("CONTROL_DIR", fs::current_path().str()); + + executor::executor_handle handle = executor::setup(); + do_spawn(handle, child_spawn_blocking_child); + + executor::exit_handle exit_handle = handle.wait_any(); + require_exit(EXIT_SUCCESS, exit_handle.status()); + exit_handle.cleanup(); + + handle.cleanup(); + + if (!fs::exists(fs::path("pid"))) + fail("The pid file was not created"); + + std::ifstream pidfile("pid"); + ATF_REQUIRE(pidfile); + pid_t pid; + pidfile >> pid; + pidfile.close(); + + ensure_dead(pid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__prevent_clobbering_control_files); +ATF_TEST_CASE_BODY(integration__prevent_clobbering_control_files) +{ + executor::executor_handle handle = executor::setup(); + + do_spawn(handle, child_delete_all); + + executor::exit_handle exit_handle = handle.wait_any(); + require_exit(EXIT_SUCCESS, exit_handle.status()); + ATF_REQUIRE(atf::utils::file_exists( + (exit_handle.control_directory() / "exec_was_called").str())); + ATF_REQUIRE(!atf::utils::file_exists( + (exit_handle.work_directory() / "exec_was_called").str())); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, integration__run_one); + ATF_ADD_TEST_CASE(tcs, integration__run_many); + + ATF_ADD_TEST_CASE(tcs, integration__parameters_and_output); + ATF_ADD_TEST_CASE(tcs, integration__custom_output_files); + ATF_ADD_TEST_CASE(tcs, integration__timestamps); + ATF_ADD_TEST_CASE(tcs, integration__files); + + ATF_ADD_TEST_CASE(tcs, integration__followup); + + ATF_ADD_TEST_CASE(tcs, integration__output_files_always_exist); + ATF_ADD_TEST_CASE(tcs, integration__timeouts); + ATF_ADD_TEST_CASE(tcs, integration__unprivileged_user); + ATF_ADD_TEST_CASE(tcs, integration__auto_cleanup); + ATF_ADD_TEST_CASE(tcs, integration__signal_handling); + ATF_ADD_TEST_CASE(tcs, integration__isolate_child_is_called); + ATF_ADD_TEST_CASE(tcs, integration__process_group_is_terminated); + ATF_ADD_TEST_CASE(tcs, integration__prevent_clobbering_control_files); +} diff --git a/utils/process/fdstream.cpp b/utils/process/fdstream.cpp new file mode 100644 index 000000000000..ccd7a1f91b0c --- /dev/null +++ b/utils/process/fdstream.cpp @@ -0,0 +1,76 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/fdstream.hpp" + +#include "utils/noncopyable.hpp" +#include "utils/process/systembuf.hpp" + + +namespace utils { +namespace process { + + +/// Private implementation fields for ifdstream. +struct ifdstream::impl : utils::noncopyable { + /// The systembuf backing this file descriptor. + systembuf _systembuf; + + /// Initializes private implementation data. + /// + /// \param fd The file descriptor. + impl(const int fd) : _systembuf(fd) {} +}; + + +} // namespace process +} // namespace utils + + +namespace process = utils::process; + + +/// Constructs a new ifdstream based on an open file descriptor. +/// +/// This grabs ownership of the file descriptor. +/// +/// \param fd The file descriptor to read from. Must be open and valid. +process::ifdstream::ifdstream(const int fd) : + std::istream(NULL), + _pimpl(new impl(fd)) +{ + rdbuf(&_pimpl->_systembuf); +} + + +/// Destroys an ifdstream object. +/// +/// \post The file descriptor attached to this stream is closed. +process::ifdstream::~ifdstream(void) +{ +} diff --git a/utils/process/fdstream.hpp b/utils/process/fdstream.hpp new file mode 100644 index 000000000000..e785b0ac4282 --- /dev/null +++ b/utils/process/fdstream.hpp @@ -0,0 +1,66 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/fdstream.hpp +/// Provides the utils::process::ifdstream class. + +#if !defined(UTILS_PROCESS_FDSTREAM_HPP) +#define UTILS_PROCESS_FDSTREAM_HPP + +#include "utils/process/fdstream_fwd.hpp" + +#include <istream> +#include <memory> + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace process { + + +/// An input stream backed by a file descriptor. +/// +/// This class grabs ownership of the file descriptor. I.e. when the class is +/// destroyed, the file descriptor is closed unconditionally. +class ifdstream : public std::istream, noncopyable +{ + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + explicit ifdstream(const int); + ~ifdstream(void); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_FDSTREAM_HPP) diff --git a/utils/process/fdstream_fwd.hpp b/utils/process/fdstream_fwd.hpp new file mode 100644 index 000000000000..8d369ea0bfa5 --- /dev/null +++ b/utils/process/fdstream_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/fdstream_fwd.hpp +/// Forward declarations for utils/process/fdstream.hpp + +#if !defined(UTILS_PROCESS_FDSTREAM_FWD_HPP) +#define UTILS_PROCESS_FDSTREAM_FWD_HPP + +namespace utils { +namespace process { + + +class ifdstream; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_FDSTREAM_FWD_HPP) diff --git a/utils/process/fdstream_test.cpp b/utils/process/fdstream_test.cpp new file mode 100644 index 000000000000..8420568216f0 --- /dev/null +++ b/utils/process/fdstream_test.cpp @@ -0,0 +1,73 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/fdstream.hpp" + +extern "C" { +#include <unistd.h> +} + +#include <atf-c++.hpp> + +#include "utils/process/systembuf.hpp" + +using utils::process::ifdstream; +using utils::process::systembuf; + + +ATF_TEST_CASE(ifdstream); +ATF_TEST_CASE_HEAD(ifdstream) +{ + set_md_var("descr", "Tests the ifdstream class"); +} +ATF_TEST_CASE_BODY(ifdstream) +{ + int fds[2]; + ATF_REQUIRE(::pipe(fds) != -1); + + ifdstream rend(fds[0]); + + systembuf wbuf(fds[1]); + std::ostream wend(&wbuf); + + // XXX This assumes that the pipe's buffer is big enough to accept + // the data written without blocking! + wend << "1Test 1message\n"; + wend.flush(); + std::string tmp; + rend >> tmp; + ATF_REQUIRE_EQ(tmp, "1Test"); + rend >> tmp; + ATF_REQUIRE_EQ(tmp, "1message"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ifdstream); +} diff --git a/utils/process/helpers.cpp b/utils/process/helpers.cpp new file mode 100644 index 000000000000..15deecd95f24 --- /dev/null +++ b/utils/process/helpers.cpp @@ -0,0 +1,74 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 <cstdlib> +#include <cstring> +#include <iostream> +#include <sstream> + + +static int +print_args(int argc, char* argv[]) +{ + for (int i = 0; i < argc; i++) + std::cout << "argv[" << i << "] = " << argv[i] << "\n"; + std::cout << "argv[" << argc << "] = NULL"; + return EXIT_SUCCESS; +} + + +static int +return_code(int argc, char* argv[]) +{ + if (argc != 3) + std::abort(); + + std::istringstream iss(argv[2]); + int code; + iss >> code; + return code; +} + + +int +main(int argc, char* argv[]) +{ + if (argc < 2) { + std::cerr << "Must provide a helper name\n"; + std::exit(EXIT_FAILURE); + } + + if (std::strcmp(argv[1], "print-args") == 0) { + return print_args(argc, argv); + } else if (std::strcmp(argv[1], "return-code") == 0) { + return return_code(argc, argv); + } else { + std::cerr << "Unknown helper\n"; + return EXIT_FAILURE; + } +} diff --git a/utils/process/isolation.cpp b/utils/process/isolation.cpp new file mode 100644 index 000000000000..90dd08d5772d --- /dev/null +++ b/utils/process/isolation.cpp @@ -0,0 +1,207 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/isolation.hpp" + +extern "C" { +#include <sys/stat.h> + +#include <grp.h> +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <iostream> + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/env.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/misc.hpp" +#include "utils/stacktrace.hpp" + +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace signals = utils::signals; + +using utils::optional; + + +/// Magic exit code to denote an error while preparing the subprocess. +const int process::exit_isolation_failure = 124; + + +namespace { + + +static void fail(const std::string&, const int) UTILS_NORETURN; + + +/// Fails the process with an errno-based error message. +/// +/// \param message The message to print. The errno-based string will be +/// appended to this, just like in perror(3). +/// \param original_errno The error code to format. +static void +fail(const std::string& message, const int original_errno) +{ + std::cerr << message << ": " << std::strerror(original_errno) << '\n'; + std::exit(process::exit_isolation_failure); +} + + +/// Changes the owner of a path. +/// +/// This function is intended to be called from a subprocess getting ready to +/// invoke an external binary. Therefore, if there is any error during the +/// setup, the new process is terminated with an error code. +/// +/// \param file The path to the file or directory to affect. +/// \param uid The UID to set on the path. +/// \param gid The GID to set on the path. +static void +do_chown(const fs::path& file, const uid_t uid, const gid_t gid) +{ + if (::chown(file.c_str(), uid, gid) == -1) + fail(F("chown(%s, %s, %s) failed; UID is %s and GID is %s") + % file % uid % gid % ::getuid() % ::getgid(), errno); +} + + +/// Resets the environment of the process to a known state. +/// +/// \param work_directory Path to the work directory being used. +/// +/// \throw std::runtime_error If there is a problem setting up the environment. +static void +prepare_environment(const fs::path& work_directory) +{ + const char* to_unset[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", + "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", + "LC_TIME", NULL }; + const char** iter; + for (iter = to_unset; *iter != NULL; ++iter) { + utils::unsetenv(*iter); + } + + utils::setenv("HOME", work_directory.str()); + utils::setenv("TMPDIR", work_directory.str()); + utils::setenv("TZ", "UTC"); +} + + +} // anonymous namespace + + +/// Cleans up the container process to run a new child. +/// +/// If there is any error during the setup, the new process is terminated +/// with an error code. +/// +/// \param unprivileged_user Unprivileged user to run the test case as. +/// \param work_directory Path to the test case-specific work directory. +void +process::isolate_child(const optional< passwd::user >& unprivileged_user, + const fs::path& work_directory) +{ + isolate_path(unprivileged_user, work_directory); + if (::chdir(work_directory.c_str()) == -1) + fail(F("chdir(%s) failed") % work_directory, errno); + + utils::unlimit_core_size(); + if (!signals::reset_all()) { + LW("Failed to reset one or more signals to their default behavior"); + } + prepare_environment(work_directory); + (void)::umask(0022); + + if (unprivileged_user && passwd::current_user().is_root()) { + const passwd::user& user = unprivileged_user.get(); + + if (user.gid != ::getgid()) { + if (::setgid(user.gid) == -1) + fail(F("setgid(%s) failed; UID is %s and GID is %s") + % user.gid % ::getuid() % ::getgid(), errno); + if (::getuid() == 0) { + ::gid_t groups[1]; + groups[0] = user.gid; + if (::setgroups(1, groups) == -1) + fail(F("setgroups(1, [%s]) failed; UID is %s and GID is %s") + % user.gid % ::getuid() % ::getgid(), errno); + } + } + if (user.uid != ::getuid()) { + if (::setuid(user.uid) == -1) + fail(F("setuid(%s) failed; UID is %s and GID is %s") + % user.uid % ::getuid() % ::getgid(), errno); + } + } +} + + +/// Sets up a path to be writable by a child isolated with isolate_child. +/// +/// If there is any error during the setup, the new process is terminated +/// with an error code. +/// +/// The caller should use this to prepare any directory or file that the child +/// should be able to write to *before* invoking isolate_child(). Note that +/// isolate_child() will use isolate_path() on the work directory though. +/// +/// \param unprivileged_user Unprivileged user to run the test case as. +/// \param file Path to the file to modify. +void +process::isolate_path(const optional< passwd::user >& unprivileged_user, + const fs::path& file) +{ + if (!unprivileged_user || !passwd::current_user().is_root()) + return; + const passwd::user& user = unprivileged_user.get(); + + const bool change_group = user.gid != ::getgid(); + const bool change_user = user.uid != ::getuid(); + + if (!change_user && !change_group) { + // Keep same permissions. + } else if (change_user && change_group) { + do_chown(file, user.uid, user.gid); + } else if (!change_user && change_group) { + do_chown(file, ::getuid(), user.gid); + } else { + INV(change_user && !change_group); + do_chown(file, user.uid, ::getgid()); + } +} diff --git a/utils/process/isolation.hpp b/utils/process/isolation.hpp new file mode 100644 index 000000000000..69793a76c7b4 --- /dev/null +++ b/utils/process/isolation.hpp @@ -0,0 +1,60 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/isolation.hpp +/// Utilities to isolate a process. +/// +/// By "isolation" in this context we mean forcing a process to run in a +/// more-or-less deterministic environment. + +#if !defined(UTILS_PROCESS_ISOLATION_HPP) +#define UTILS_PROCESS_ISOLATION_HPP + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/passwd_fwd.hpp" + +namespace utils { +namespace process { + + +extern const int exit_isolation_failure; + + +void isolate_child(const utils::optional< utils::passwd::user >&, + const utils::fs::path&); + +void isolate_path(const utils::optional< utils::passwd::user >&, + const utils::fs::path&); + + +} // namespace process +} // namespace utils + + +#endif // !defined(UTILS_PROCESS_ISOLATION_HPP) diff --git a/utils/process/isolation_test.cpp b/utils/process/isolation_test.cpp new file mode 100644 index 000000000000..dc723cc65c88 --- /dev/null +++ b/utils/process/isolation_test.cpp @@ -0,0 +1,622 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/isolation.hpp" + +extern "C" { +#include <sys/types.h> +#include <sys/resource.h> +#include <sys/stat.h> + +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <fstream> +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Runs the given hook in a subprocess. +/// +/// \param hook The code to run in the subprocess. +/// +/// \return The status of the subprocess for further validation. +/// +/// \post The subprocess.stdout and subprocess.stderr files, created in the +/// current directory, contain the output of the subprocess. +template< typename Hook > +static process::status +fork_and_run(Hook hook) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + hook, fs::path("subprocess.stdout"), fs::path("subprocess.stderr")); + const process::status status = child->wait(); + + atf::utils::cat_file("subprocess.stdout", "isolated child stdout: "); + atf::utils::cat_file("subprocess.stderr", "isolated child stderr: "); + + return status; +} + + +/// Subprocess that validates the cleanliness of the environment. +/// +/// \post Exits with success if the environment is clean; failure otherwise. +static void +check_clean_environment(void) +{ + fs::mkdir(fs::path("some-directory"), 0755); + process::isolate_child(none, fs::path("some-directory")); + + bool failed = false; + + const char* empty[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", + "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", + "LC_TIME", NULL }; + const char** iter; + for (iter = empty; *iter != NULL; ++iter) { + if (utils::getenv(*iter)) { + failed = true; + std::cout << F("%s was not unset\n") % *iter; + } + } + + if (utils::getenv_with_default("HOME", "") != "some-directory") { + failed = true; + std::cout << "HOME was not set to the work directory\n"; + } + + if (utils::getenv_with_default("TMPDIR", "") != "some-directory") { + failed = true; + std::cout << "TMPDIR was not set to the work directory\n"; + } + + if (utils::getenv_with_default("TZ", "") != "UTC") { + failed = true; + std::cout << "TZ was not set to UTC\n"; + } + + if (utils::getenv_with_default("LEAVE_ME_ALONE", "") != "kill-some-day") { + failed = true; + std::cout << "LEAVE_ME_ALONE was modified while it should not have " + "been\n"; + } + + std::exit(failed ? EXIT_FAILURE : EXIT_SUCCESS); +} + + +/// Subprocess that checks if user privileges are dropped. +class check_drop_privileges { + /// The user to drop the privileges to. + const passwd::user _unprivileged_user; + +public: + /// Constructor. + /// + /// \param unprivileged_user The user to drop the privileges to. + check_drop_privileges(const passwd::user& unprivileged_user) : + _unprivileged_user(unprivileged_user) + { + } + + /// Body of the subprocess. + /// + /// \post Exits with success if the process has dropped privileges as + /// expected. + void + operator()(void) const + { + fs::mkdir(fs::path("subdir"), 0755); + process::isolate_child(utils::make_optional(_unprivileged_user), + fs::path("subdir")); + + if (::getuid() == 0) { + std::cout << "UID is still 0\n"; + std::exit(EXIT_FAILURE); + } + + if (::getgid() == 0) { + std::cout << "GID is still 0\n"; + std::exit(EXIT_FAILURE); + } + + ::gid_t groups[1]; + if (::getgroups(1, groups) == -1) { + // Should only fail if we get more than one group notifying about + // not enough space in the groups variable to store the whole + // result. + INV(errno == EINVAL); + std::exit(EXIT_FAILURE); + } + if (groups[0] == 0) { + std::cout << "Primary group is still 0\n"; + std::exit(EXIT_FAILURE); + } + + std::ofstream output("file.txt"); + if (!output) { + std::cout << "Cannot write to isolated directory; owner not " + "changed?\n"; + std::exit(EXIT_FAILURE); + } + + std::exit(EXIT_SUCCESS); + } +}; + + +/// Subprocess that dumps core to validate core dumping abilities. +static void +check_enable_core_dumps(void) +{ + process::isolate_child(none, fs::path(".")); + std::abort(); +} + + +/// Subprocess that checks if the work directory is entered. +class check_enter_work_directory { + /// Directory to enter. May be releative. + const fs::path _directory; + +public: + /// Constructor. + /// + /// \param directory Directory to enter. + check_enter_work_directory(const fs::path& directory) : + _directory(directory) + { + } + + /// Body of the subprocess. + /// + /// \post Exits with success if the process has entered the given work + /// directory; false otherwise. + void + operator()(void) const + { + const fs::path exp_subdir = fs::current_path() / _directory; + process::isolate_child(none, _directory); + std::exit(fs::current_path() == exp_subdir ? + EXIT_SUCCESS : EXIT_FAILURE); + } +}; + + +/// Subprocess that validates that it owns a session. +/// +/// \post Exits with success if the process lives in its own session; +/// failure otherwise. +static void +check_new_session(void) +{ + process::isolate_child(none, fs::path(".")); + std::exit(::getsid(::getpid()) == ::getpid() ? EXIT_SUCCESS : EXIT_FAILURE); +} + + +/// Subprocess that validates the disconnection from any terminal. +/// +/// \post Exits with success if the environment is clean; failure otherwise. +static void +check_no_terminal(void) +{ + process::isolate_child(none, fs::path(".")); + + const char* const args[] = { + "/bin/sh", + "-i", + "-c", + "echo success", + NULL + }; + ::execv("/bin/sh", UTILS_UNCONST(char*, args)); + std::abort(); +} + + +/// Subprocess that validates that it has become the leader of a process group. +/// +/// \post Exits with success if the process lives in its own process group; +/// failure otherwise. +static void +check_process_group(void) +{ + process::isolate_child(none, fs::path(".")); + std::exit(::getpgid(::getpid()) == ::getpid() ? + EXIT_SUCCESS : EXIT_FAILURE); +} + + +/// Subprocess that validates that the umask has been reset. +/// +/// \post Exits with success if the umask matches the expected value; failure +/// otherwise. +static void +check_umask(void) +{ + process::isolate_child(none, fs::path(".")); + std::exit(::umask(0) == 0022 ? EXIT_SUCCESS : EXIT_FAILURE); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__clean_environment); +ATF_TEST_CASE_BODY(isolate_child__clean_environment) +{ + utils::setenv("HOME", "/non-existent/directory"); + utils::setenv("TMPDIR", "/non-existent/directory"); + utils::setenv("LANG", "C"); + utils::setenv("LC_ALL", "C"); + utils::setenv("LC_COLLATE", "C"); + utils::setenv("LC_CTYPE", "C"); + utils::setenv("LC_MESSAGES", "C"); + utils::setenv("LC_MONETARY", "C"); + utils::setenv("LC_NUMERIC", "C"); + utils::setenv("LC_TIME", "C"); + utils::setenv("LEAVE_ME_ALONE", "kill-some-day"); + utils::setenv("TZ", "EST+5"); + + const process::status status = fork_and_run(check_clean_environment); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE(isolate_child__other_user_when_unprivileged); +ATF_TEST_CASE_HEAD(isolate_child__other_user_when_unprivileged) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_child__other_user_when_unprivileged) +{ + const passwd::user user = passwd::current_user(); + + passwd::user other_user = user; + other_user.uid += 1; + other_user.gid += 1; + process::isolate_child(utils::make_optional(other_user), fs::path(".")); + + ATF_REQUIRE_EQ(user.uid, ::getuid()); + ATF_REQUIRE_EQ(user.gid, ::getgid()); +} + + +ATF_TEST_CASE(isolate_child__drop_privileges); +ATF_TEST_CASE_HEAD(isolate_child__drop_privileges) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_child__drop_privileges) +{ + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + + const process::status status = fork_and_run(check_drop_privileges( + unprivileged_user)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE(isolate_child__drop_privileges_fail_uid); +ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_uid) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_uid) +{ + // Fake the current user as root so that we bypass the protections in + // isolate_child that prevent us from attempting a user switch when we are + // not root. We do this so we can trigger the setuid failure. + passwd::user root = passwd::user("root", 0, 0); + ATF_REQUIRE(root.is_root()); + passwd::set_current_user_for_testing(root); + + passwd::user unprivileged_user = passwd::current_user(); + unprivileged_user.uid += 1; + + const process::status status = fork_and_run(check_drop_privileges( + unprivileged_user)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("(chown|setuid).*failed", + "subprocess.stderr")); +} + + +ATF_TEST_CASE(isolate_child__drop_privileges_fail_gid); +ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_gid) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_gid) +{ + // Fake the current user as root so that we bypass the protections in + // isolate_child that prevent us from attempting a user switch when we are + // not root. We do this so we can trigger the setgid failure. + passwd::user root = passwd::user("root", 0, 0); + ATF_REQUIRE(root.is_root()); + passwd::set_current_user_for_testing(root); + + passwd::user unprivileged_user = passwd::current_user(); + unprivileged_user.gid += 1; + + const process::status status = fork_and_run(check_drop_privileges( + unprivileged_user)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("(chown|setgid).*failed", + "subprocess.stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enable_core_dumps); +ATF_TEST_CASE_BODY(isolate_child__enable_core_dumps) +{ + utils::require_run_coredump_tests(this); + + struct ::rlimit rl; + if (::getrlimit(RLIMIT_CORE, &rl) == -1) + fail("Failed to query the core size limit"); + if (rl.rlim_cur == 0 || rl.rlim_max == 0) + skip("Maximum core size is zero; cannot run test"); + rl.rlim_cur = 0; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) + fail("Failed to lower the core size limit"); + + const process::status status = fork_and_run(check_enable_core_dumps); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(status.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory); +ATF_TEST_CASE_BODY(isolate_child__enter_work_directory) +{ + const fs::path directory("some/sub/directory"); + fs::mkdir_p(directory, 0755); + const process::status status = fork_and_run( + check_enter_work_directory(directory)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory_failure); +ATF_TEST_CASE_BODY(isolate_child__enter_work_directory_failure) +{ + const fs::path directory("some/sub/directory"); + const process::status status = fork_and_run( + check_enter_work_directory(directory)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("chdir\\(some/sub/directory\\) failed", + "subprocess.stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__new_session); +ATF_TEST_CASE_BODY(isolate_child__new_session) +{ + const process::status status = fork_and_run(check_new_session); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__no_terminal); +ATF_TEST_CASE_BODY(isolate_child__no_terminal) +{ + const process::status status = fork_and_run(check_no_terminal); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__process_group); +ATF_TEST_CASE_BODY(isolate_child__process_group) +{ + const process::status status = fork_and_run(check_process_group); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__reset_umask); +ATF_TEST_CASE_BODY(isolate_child__reset_umask) +{ + const process::status status = fork_and_run(check_umask); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +/// Executes isolate_path() and compares the on-disk changes to expected values. +/// +/// \param unprivileged_user The user to pass to isolate_path; may be none. +/// \param exp_uid Expected UID or none to expect the old value. +/// \param exp_gid Expected GID or none to expect the old value. +static void +do_isolate_path_test(const optional< passwd::user >& unprivileged_user, + const optional< uid_t >& exp_uid, + const optional< gid_t >& exp_gid) +{ + const fs::path dir("dir"); + fs::mkdir(dir, 0755); + struct ::stat old_sb; + ATF_REQUIRE(::stat(dir.c_str(), &old_sb) != -1); + + process::isolate_path(unprivileged_user, dir); + + struct ::stat new_sb; + ATF_REQUIRE(::stat(dir.c_str(), &new_sb) != -1); + + if (exp_uid) + ATF_REQUIRE_EQ(exp_uid.get(), new_sb.st_uid); + else + ATF_REQUIRE_EQ(old_sb.st_uid, new_sb.st_uid); + + if (exp_gid) + ATF_REQUIRE_EQ(exp_gid.get(), new_sb.st_gid); + else + ATF_REQUIRE_EQ(old_sb.st_gid, new_sb.st_gid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__no_user); +ATF_TEST_CASE_BODY(isolate_path__no_user) +{ + do_isolate_path_test(none, none, none); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__same_user); +ATF_TEST_CASE_BODY(isolate_path__same_user) +{ + do_isolate_path_test(utils::make_optional(passwd::current_user()), + none, none); +} + + +ATF_TEST_CASE(isolate_path__other_user_when_unprivileged); +ATF_TEST_CASE_HEAD(isolate_path__other_user_when_unprivileged) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_path__other_user_when_unprivileged) +{ + passwd::user user = passwd::current_user(); + user.uid += 1; + user.gid += 1; + + do_isolate_path_test(utils::make_optional(user), none, none); +} + + +ATF_TEST_CASE(isolate_path__drop_privileges); +ATF_TEST_CASE_HEAD(isolate_path__drop_privileges) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_path__drop_privileges) +{ + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + do_isolate_path_test(utils::make_optional(unprivileged_user), + utils::make_optional(unprivileged_user.uid), + utils::make_optional(unprivileged_user.gid)); +} + + +ATF_TEST_CASE(isolate_path__drop_privileges_only_uid); +ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_uid) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_uid) +{ + passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + unprivileged_user.gid = ::getgid(); + do_isolate_path_test(utils::make_optional(unprivileged_user), + utils::make_optional(unprivileged_user.uid), + none); +} + + +ATF_TEST_CASE(isolate_path__drop_privileges_only_gid); +ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_gid) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_gid) +{ + passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + unprivileged_user.uid = ::getuid(); + do_isolate_path_test(utils::make_optional(unprivileged_user), + none, + utils::make_optional(unprivileged_user.gid)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, isolate_child__clean_environment); + ATF_ADD_TEST_CASE(tcs, isolate_child__other_user_when_unprivileged); + ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges); + ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_uid); + ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_gid); + ATF_ADD_TEST_CASE(tcs, isolate_child__enable_core_dumps); + ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory); + ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory_failure); + ATF_ADD_TEST_CASE(tcs, isolate_child__new_session); + ATF_ADD_TEST_CASE(tcs, isolate_child__no_terminal); + ATF_ADD_TEST_CASE(tcs, isolate_child__process_group); + ATF_ADD_TEST_CASE(tcs, isolate_child__reset_umask); + + ATF_ADD_TEST_CASE(tcs, isolate_path__no_user); + ATF_ADD_TEST_CASE(tcs, isolate_path__same_user); + ATF_ADD_TEST_CASE(tcs, isolate_path__other_user_when_unprivileged); + ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges); + ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_uid); + ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_gid); +} diff --git a/utils/process/operations.cpp b/utils/process/operations.cpp new file mode 100644 index 000000000000..abcc49f2a443 --- /dev/null +++ b/utils/process/operations.cpp @@ -0,0 +1,273 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/operations.hpp" + +extern "C" { +#include <sys/types.h> +#include <sys/wait.h> + +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <iostream> + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/process/exceptions.hpp" +#include "utils/process/system.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +/// Maximum number of arguments supported by exec. +/// +/// We need this limit to avoid having to allocate dynamic memory in the child +/// process to construct the arguments list, which would have side-effects in +/// the parent's memory if we use vfork(). +#define MAX_ARGS 128 + + +namespace { + + +/// Exception-based, type-improved version of wait(2). +/// +/// \return The PID of the terminated process and its termination status. +/// +/// \throw process::system_error If the call to wait(2) fails. +static process::status +safe_wait(void) +{ + LD("Waiting for any child process"); + int stat_loc; + const pid_t pid = ::wait(&stat_loc); + if (pid == -1) { + const int original_errno = errno; + throw process::system_error("Failed to wait for any child process", + original_errno); + } + return process::status(pid, stat_loc); +} + + +/// Exception-based, type-improved version of waitpid(2). +/// +/// \param pid The identifier of the process to wait for. +/// +/// \return The termination status of the process. +/// +/// \throw process::system_error If the call to waitpid(2) fails. +static process::status +safe_waitpid(const pid_t pid) +{ + LD(F("Waiting for pid=%s") % pid); + int stat_loc; + if (process::detail::syscall_waitpid(pid, &stat_loc, 0) == -1) { + const int original_errno = errno; + throw process::system_error(F("Failed to wait for PID %s") % pid, + original_errno); + } + return process::status(pid, stat_loc); +} + + +} // anonymous namespace + + +/// Executes an external binary and replaces the current process. +/// +/// This function must not use any of the logging features so that the output +/// of the subprocess is not "polluted" by our own messages. +/// +/// This function must also not affect the global state of the current process +/// as otherwise we would not be able to use vfork(). Only state stored in the +/// stack can be touched. +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +void +process::exec(const fs::path& program, const args_vector& args) throw() +{ + try { + exec_unsafe(program, args); + } catch (const system_error& error) { + // Error message already printed by exec_unsafe. + std::abort(); + } +} + + +/// Executes an external binary and replaces the current process. +/// +/// This differs from process::exec() in that this function reports errors +/// caused by the exec(2) system call to let the caller decide how to handle +/// them. +/// +/// This function must not use any of the logging features so that the output +/// of the subprocess is not "polluted" by our own messages. +/// +/// This function must also not affect the global state of the current process +/// as otherwise we would not be able to use vfork(). Only state stored in the +/// stack can be touched. +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +/// +/// \throw system_error If the exec(2) call fails. +void +process::exec_unsafe(const fs::path& program, const args_vector& args) +{ + PRE(args.size() < MAX_ARGS); + int original_errno = 0; + try { + const char* argv[MAX_ARGS + 1]; + + argv[0] = program.c_str(); + for (args_vector::size_type i = 0; i < args.size(); i++) + argv[1 + i] = args[i].c_str(); + argv[1 + args.size()] = NULL; + + const int ret = ::execv(program.c_str(), + (char* const*)(unsigned long)(const void*)argv); + original_errno = errno; + INV(ret == -1); + std::cerr << "Failed to execute " << program << ": " + << std::strerror(original_errno) << "\n"; + } catch (const std::runtime_error& error) { + std::cerr << "Failed to execute " << program << ": " + << error.what() << "\n"; + std::abort(); + } catch (...) { + std::cerr << "Failed to execute " << program << "; got unexpected " + "exception during exec\n"; + std::abort(); + } + + // We must do this here to prevent our exception from being caught by the + // generic handlers above. + INV(original_errno != 0); + throw system_error("Failed to execute " + program.str(), original_errno); +} + + +/// Forcibly kills a process group started by us. +/// +/// This function is safe to call from an signal handler context. +/// +/// Pretty much all of our subprocesses run in their own process group so that +/// we can terminate them and thier children should we need to. Because of +/// this, the very first thing our subprocesses do is create a new process group +/// for themselves. +/// +/// The implication of the above is that simply issuing a killpg() call on the +/// process group is racy: if the subprocess has not yet had a chance to prepare +/// its own process group, then we will not be killing anything. To solve this, +/// we must also kill() the process group leader itself, and we must do so after +/// the call to killpg(). Doing this is safe because: 1) the process group must +/// have the same ID as the PID of the process that created it; and 2) we have +/// not yet issued a wait() call so we still own the PID. +/// +/// The sideffect of doing what we do here is that the process group leader may +/// receive a signal twice. But we don't care because we are forcibly +/// terminating the process group and none of the processes can controlledly +/// react to SIGKILL. +/// +/// \param pgid PID or process group ID to terminate. +void +process::terminate_group(const int pgid) +{ + (void)::killpg(pgid, SIGKILL); + (void)::kill(pgid, SIGKILL); +} + + +/// Terminates the current process reproducing the given status. +/// +/// The caller process is abruptly terminated. In particular, no output streams +/// are flushed, no destructors are called, and no atexit(2) handlers are run. +/// +/// \param status The status to "re-deliver" to the caller process. +void +process::terminate_self_with(const status& status) +{ + if (status.exited()) { + ::_exit(status.exitstatus()); + } else { + INV(status.signaled()); + (void)::kill(::getpid(), status.termsig()); + UNREACHABLE_MSG(F("Signal %s terminated %s but did not terminate " + "ourselves") % status.termsig() % status.dead_pid()); + } +} + + +/// Blocks to wait for completion of a subprocess. +/// +/// \param pid Identifier of the process to wait for. +/// +/// \return The termination status of the child process that terminated. +/// +/// \throw process::system_error If the call to wait(2) fails. +process::status +process::wait(const int pid) +{ + const process::status status = safe_waitpid(pid); + { + signals::interrupts_inhibiter inhibiter; + signals::remove_pid_to_kill(pid); + } + return status; +} + + +/// Blocks to wait for completion of any subprocess. +/// +/// \return The termination status of the child process that terminated. +/// +/// \throw process::system_error If the call to wait(2) fails. +process::status +process::wait_any(void) +{ + const process::status status = safe_wait(); + { + signals::interrupts_inhibiter inhibiter; + signals::remove_pid_to_kill(status.dead_pid()); + } + return status; +} diff --git a/utils/process/operations.hpp b/utils/process/operations.hpp new file mode 100644 index 000000000000..773f9d38bb74 --- /dev/null +++ b/utils/process/operations.hpp @@ -0,0 +1,56 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/operations.hpp +/// Collection of utilities for process management. + +#if !defined(UTILS_PROCESS_OPERATIONS_HPP) +#define UTILS_PROCESS_OPERATIONS_HPP + +#include "utils/process/operations_fwd.hpp" + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { +namespace process { + + +void exec(const utils::fs::path&, const args_vector&) throw() UTILS_NORETURN; +void exec_unsafe(const utils::fs::path&, const args_vector&) UTILS_NORETURN; +void terminate_group(const int); +void terminate_self_with(const status&) UTILS_NORETURN; +status wait(const int); +status wait_any(void); + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_OPERATIONS_HPP) diff --git a/utils/process/operations_fwd.hpp b/utils/process/operations_fwd.hpp new file mode 100644 index 000000000000..bd23fdc2c691 --- /dev/null +++ b/utils/process/operations_fwd.hpp @@ -0,0 +1,49 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/operations_fwd.hpp +/// Forward declarations for utils/process/operations.hpp + +#if !defined(UTILS_PROCESS_OPERATIONS_FWD_HPP) +#define UTILS_PROCESS_OPERATIONS_FWD_HPP + +#include <string> +#include <vector> + +namespace utils { +namespace process { + + +/// Arguments to a program, without the program name. +typedef std::vector< std::string > args_vector; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_OPERATIONS_FWD_HPP) diff --git a/utils/process/operations_test.cpp b/utils/process/operations_test.cpp new file mode 100644 index 000000000000..e9c1ebb65a3d --- /dev/null +++ b/utils/process/operations_test.cpp @@ -0,0 +1,471 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/operations.hpp" + +extern "C" { +#include <sys/types.h> +#include <sys/wait.h> + +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/defs.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/exceptions.hpp" +#include "utils/process/status.hpp" +#include "utils/stacktrace.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace process = utils::process; + + +namespace { + + +/// Type of the process::exec() and process::exec_unsafe() functions. +typedef void (*exec_function)(const fs::path&, const process::args_vector&); + + +/// Calculates the path to the test helpers binary. +/// +/// \param tc A pointer to the caller test case, needed to extract the value of +/// the "srcdir" property. +/// +/// \return The path to the helpers binary. +static fs::path +get_helpers(const atf::tests::tc* tc) +{ + return fs::path(tc->get_config_var("srcdir")) / "helpers"; +} + + +/// Body for a subprocess that runs exec(). +class child_exec { + /// Function to do the exec. + const exec_function _do_exec; + + /// Path to the binary to exec. + const fs::path& _program; + + /// Arguments to the binary, not including argv[0]. + const process::args_vector& _args; + +public: + /// Constructor. + /// + /// \param do_exec Function to do the exec. + /// \param program Path to the binary to exec. + /// \param args Arguments to the binary, not including argv[0]. + child_exec(const exec_function do_exec, const fs::path& program, + const process::args_vector& args) : + _do_exec(do_exec), _program(program), _args(args) + { + } + + /// Body for the subprocess. + void + operator()(void) + { + _do_exec(_program, _args); + } +}; + + +/// Body for a process that returns a specific exit code. +/// +/// \tparam ExitStatus The exit status for the subprocess. +template< int ExitStatus > +static void +child_exit(void) +{ + std::exit(ExitStatus); +} + + +static void suspend(void) UTILS_NORETURN; + + +/// Blocks a subprocess from running indefinitely. +static void +suspend(void) +{ + sigset_t mask; + sigemptyset(&mask); + for (;;) { + ::sigsuspend(&mask); + } +} + + +static void write_loop(const int) UTILS_NORETURN; + + +/// Provides an infinite stream of data in a subprocess. +/// +/// \param fd Descriptor into which to write. +static void +write_loop(const int fd) +{ + const int cookie = 0x12345678; + for (;;) { + std::cerr << "Still alive in PID " << ::getpid() << '\n'; + if (::write(fd, &cookie, sizeof(cookie)) != sizeof(cookie)) + std::exit(EXIT_FAILURE); + ::sleep(1); + } +} + + +} // anonymous namespace + + +/// Tests an exec function with no arguments. +/// +/// \param tc The calling test case. +/// \param do_exec The exec function to test. +static void +check_exec_no_args(const atf::tests::tc* tc, const exec_function do_exec) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + child_exec(do_exec, get_helpers(tc), process::args_vector()), + fs::path("stdout"), fs::path("stderr")); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_FAILURE, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("Must provide a helper name", "stderr")); +} + + +/// Tests an exec function with some arguments. +/// +/// \param tc The calling test case. +/// \param do_exec The exec function to test. +static void +check_exec_some_args(const atf::tests::tc* tc, const exec_function do_exec) +{ + process::args_vector args; + args.push_back("print-args"); + args.push_back("foo"); + args.push_back("bar"); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_exec(do_exec, get_helpers(tc), args), + fs::path("stdout"), fs::path("stderr")); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("argv\\[1\\] = print-args", "stdout")); + ATF_REQUIRE(atf::utils::grep_file("argv\\[2\\] = foo", "stdout")); + ATF_REQUIRE(atf::utils::grep_file("argv\\[3\\] = bar", "stdout")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__no_args); +ATF_TEST_CASE_BODY(exec__no_args) +{ + check_exec_no_args(this, process::exec); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__some_args); +ATF_TEST_CASE_BODY(exec__some_args) +{ + check_exec_some_args(this, process::exec); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__fail); +ATF_TEST_CASE_BODY(exec__fail) +{ + utils::avoid_coredump_on_crash(); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_exec(process::exec, fs::path("non-existent"), + process::args_vector()), + fs::path("stdout"), fs::path("stderr")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file("Failed to execute non-existent", + "stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__no_args); +ATF_TEST_CASE_BODY(exec_unsafe__no_args) +{ + check_exec_no_args(this, process::exec_unsafe); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__some_args); +ATF_TEST_CASE_BODY(exec_unsafe__some_args) +{ + check_exec_some_args(this, process::exec_unsafe); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__fail); +ATF_TEST_CASE_BODY(exec_unsafe__fail) +{ + ATF_REQUIRE_THROW_RE( + process::system_error, "Failed to execute missing-program", + process::exec_unsafe(fs::path("missing-program"), + process::args_vector())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_group__setpgrp_executed); +ATF_TEST_CASE_BODY(terminate_group__setpgrp_executed) +{ + int first_fds[2], second_fds[2]; + ATF_REQUIRE(::pipe(first_fds) != -1); + ATF_REQUIRE(::pipe(second_fds) != -1); + + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + ::setpgid(::getpid(), ::getpid()); + const pid_t pid2 = ::fork(); + if (pid2 == -1) { + std::exit(EXIT_FAILURE); + } else if (pid2 == 0) { + ::close(first_fds[0]); + ::close(first_fds[1]); + ::close(second_fds[0]); + write_loop(second_fds[1]); + } + ::close(first_fds[0]); + ::close(second_fds[0]); + ::close(second_fds[1]); + write_loop(first_fds[1]); + } + ::close(first_fds[1]); + ::close(second_fds[1]); + + int dummy; + std::cerr << "Waiting for children to start\n"; + while (::read(first_fds[0], &dummy, sizeof(dummy)) <= 0 || + ::read(second_fds[0], &dummy, sizeof(dummy)) <= 0) { + // Wait for children to come up. + } + + process::terminate_group(pid); + std::cerr << "Waiting for children to die\n"; + while (::read(first_fds[0], &dummy, sizeof(dummy)) > 0 || + ::read(second_fds[0], &dummy, sizeof(dummy)) > 0) { + // Wait for children to terminate. If they don't, then the test case + // will time out. + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGKILL); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_group__setpgrp_not_executed); +ATF_TEST_CASE_BODY(terminate_group__setpgrp_not_executed) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + // We do not call setgprp() here to simulate the race that happens when + // we invoke terminate_group on a process that has not yet had a chance + // to run the setpgrp() call. + suspend(); + } + + process::terminate_group(pid); + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGKILL); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__exitstatus); +ATF_TEST_CASE_BODY(terminate_self_with__exitstatus) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + const process::status status = process::status::fake_exited(123); + process::terminate_self_with(status); + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE(WEXITSTATUS(status) == 123); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__termsig); +ATF_TEST_CASE_BODY(terminate_self_with__termsig) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + const process::status status = process::status::fake_signaled( + SIGKILL, false); + process::terminate_self_with(status); + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGKILL); + ATF_REQUIRE(!WCOREDUMP(status)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__termsig_and_core); +ATF_TEST_CASE_BODY(terminate_self_with__termsig_and_core) +{ + utils::prepare_coredump_test(this); + + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + const process::status status = process::status::fake_signaled( + SIGABRT, true); + process::terminate_self_with(status); + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGABRT); + ATF_REQUIRE(WCOREDUMP(status)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait__ok); +ATF_TEST_CASE_BODY(wait__ok) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_exit< 15 >); + const pid_t pid = child->pid(); + child.reset(); // Ensure there is no conflict between destructor and wait. + + const process::status status = process::wait(pid); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(15, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait__fail); +ATF_TEST_CASE_BODY(wait__fail) +{ + ATF_REQUIRE_THROW(process::system_error, process::wait(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait_any__one); +ATF_TEST_CASE_BODY(wait_any__one) +{ + process::child::fork_capture(child_exit< 15 >); + + const process::status status = process::wait_any(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(15, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait_any__many); +ATF_TEST_CASE_BODY(wait_any__many) +{ + process::child::fork_capture(child_exit< 15 >); + process::child::fork_capture(child_exit< 30 >); + process::child::fork_capture(child_exit< 45 >); + + std::set< int > exit_codes; + for (int i = 0; i < 3; i++) { + const process::status status = process::wait_any(); + ATF_REQUIRE(status.exited()); + exit_codes.insert(status.exitstatus()); + } + + std::set< int > exp_exit_codes; + exp_exit_codes.insert(15); + exp_exit_codes.insert(30); + exp_exit_codes.insert(45); + ATF_REQUIRE_EQ(exp_exit_codes, exit_codes); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait_any__none_is_failure); +ATF_TEST_CASE_BODY(wait_any__none_is_failure) +{ + try { + const process::status status = process::wait_any(); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("Failed to wait", e.what())); + ATF_REQUIRE_EQ(ECHILD, e.original_errno()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, exec__no_args); + ATF_ADD_TEST_CASE(tcs, exec__some_args); + ATF_ADD_TEST_CASE(tcs, exec__fail); + + ATF_ADD_TEST_CASE(tcs, exec_unsafe__no_args); + ATF_ADD_TEST_CASE(tcs, exec_unsafe__some_args); + ATF_ADD_TEST_CASE(tcs, exec_unsafe__fail); + + ATF_ADD_TEST_CASE(tcs, terminate_group__setpgrp_executed); + ATF_ADD_TEST_CASE(tcs, terminate_group__setpgrp_not_executed); + + ATF_ADD_TEST_CASE(tcs, terminate_self_with__exitstatus); + ATF_ADD_TEST_CASE(tcs, terminate_self_with__termsig); + ATF_ADD_TEST_CASE(tcs, terminate_self_with__termsig_and_core); + + ATF_ADD_TEST_CASE(tcs, wait__ok); + ATF_ADD_TEST_CASE(tcs, wait__fail); + + ATF_ADD_TEST_CASE(tcs, wait_any__one); + ATF_ADD_TEST_CASE(tcs, wait_any__many); + ATF_ADD_TEST_CASE(tcs, wait_any__none_is_failure); +} diff --git a/utils/process/status.cpp b/utils/process/status.cpp new file mode 100644 index 000000000000..a3cea8e09ebd --- /dev/null +++ b/utils/process/status.cpp @@ -0,0 +1,200 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/status.hpp" + +extern "C" { +#include <sys/wait.h> +} + +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace process = utils::process; + +using utils::none; +using utils::optional; + +#if !defined(WCOREDUMP) +# define WCOREDUMP(x) false +#endif + + +/// Constructs a new status object based on the status value of waitpid(2). +/// +/// \param dead_pid_ The PID of the process this status belonged to. +/// \param stat_loc The status value returnd by waitpid(2). +process::status::status(const int dead_pid_, int stat_loc) : + _dead_pid(dead_pid_), + _exited(WIFEXITED(stat_loc) ? + optional< int >(WEXITSTATUS(stat_loc)) : none), + _signaled(WIFSIGNALED(stat_loc) ? + optional< std::pair< int, bool > >( + std::make_pair(WTERMSIG(stat_loc), WCOREDUMP(stat_loc))) : + none) +{ +} + + +/// Constructs a new status object based on fake values. +/// +/// \param exited_ If not none, specifies the exit status of the program. +/// \param signaled_ If not none, specifies the termination signal and whether +/// the process dumped core or not. +process::status::status(const optional< int >& exited_, + const optional< std::pair< int, bool > >& signaled_) : + _dead_pid(-1), + _exited(exited_), + _signaled(signaled_) +{ +} + + +/// Constructs a new status object based on a fake exit status. +/// +/// \param exitstatus_ The exit code of the process. +/// +/// \return A status object with fake data. +process::status +process::status::fake_exited(const int exitstatus_) +{ + return status(utils::make_optional(exitstatus_), none); +} + + +/// Constructs a new status object based on a fake exit status. +/// +/// \param termsig_ The termination signal of the process. +/// \param coredump_ Whether the process dumped core or not. +/// +/// \return A status object with fake data. +process::status +process::status::fake_signaled(const int termsig_, const bool coredump_) +{ + return status(none, utils::make_optional(std::make_pair(termsig_, + coredump_))); +} + + +/// Returns the PID of the process this status was taken from. +/// +/// Please note that the process is already dead and gone from the system. This +/// PID can only be used for informational reasons and not to address the +/// process in any way. +/// +/// \return The PID of the original process. +int +process::status::dead_pid(void) const +{ + return _dead_pid; +} + + +/// Returns whether the process exited cleanly or not. +/// +/// \return True if the process exited cleanly, false otherwise. +bool +process::status::exited(void) const +{ + return _exited; +} + + +/// Returns the exit code of the process. +/// +/// \pre The process must have exited cleanly (i.e. exited() must be true). +/// +/// \return The exit code. +int +process::status::exitstatus(void) const +{ + PRE(exited()); + return _exited.get(); +} + + +/// Returns whether the process terminated due to a signal or not. +/// +/// \return True if the process terminated due to a signal, false otherwise. +bool +process::status::signaled(void) const +{ + return _signaled; +} + + +/// Returns the signal that terminated the process. +/// +/// \pre The process must have terminated by a signal (i.e. signaled() must be +/// true. +/// +/// \return The signal number. +int +process::status::termsig(void) const +{ + PRE(signaled()); + return _signaled.get().first; +} + + +/// Returns whether the process core dumped or not. +/// +/// This functionality may be unsupported in some platforms. In such cases, +/// this method returns false unconditionally. +/// +/// \pre The process must have terminated by a signal (i.e. signaled() must be +/// true. +/// +/// \return True if the process dumped core, false otherwise. +bool +process::status::coredump(void) const +{ + PRE(signaled()); + return _signaled.get().second; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param status The object to format. +/// +/// \return The output stream. +std::ostream& +process::operator<<(std::ostream& output, const status& status) +{ + if (status.exited()) { + output << F("status{exitstatus=%s}") % status.exitstatus(); + } else { + INV(status.signaled()); + output << F("status{termsig=%s, coredump=%s}") % status.termsig() % + status.coredump(); + } + return output; +} diff --git a/utils/process/status.hpp b/utils/process/status.hpp new file mode 100644 index 000000000000..b14ff55c01a2 --- /dev/null +++ b/utils/process/status.hpp @@ -0,0 +1,84 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/status.hpp +/// Provides the utils::process::status class. + +#if !defined(UTILS_PROCESS_STATUS_HPP) +#define UTILS_PROCESS_STATUS_HPP + +#include "utils/process/status_fwd.hpp" + +#include <ostream> +#include <utility> + +#include "utils/optional.ipp" + +namespace utils { +namespace process { + + +/// Representation of the termination status of a process. +class status { + /// The PID of the process that generated this status. + /// + /// Note that the process has exited already and been awaited for, so the + /// PID cannot be used to address the process. + int _dead_pid; + + /// The exit status of the process, if it exited cleanly. + optional< int > _exited; + + /// The signal that terminated the program, if any, and if it dumped core. + optional< std::pair< int, bool > > _signaled; + + status(const optional< int >&, const optional< std::pair< int, bool > >&); + +public: + status(const int, int); + static status fake_exited(const int); + static status fake_signaled(const int, const bool); + + int dead_pid(void) const; + + bool exited(void) const; + int exitstatus(void) const; + + bool signaled(void) const; + int termsig(void) const; + bool coredump(void) const; +}; + + +std::ostream& operator<<(std::ostream&, const status&); + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_STATUS_HPP) diff --git a/utils/process/status_fwd.hpp b/utils/process/status_fwd.hpp new file mode 100644 index 000000000000..3a14683dc15c --- /dev/null +++ b/utils/process/status_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/status_fwd.hpp +/// Forward declarations for utils/process/status.hpp + +#if !defined(UTILS_PROCESS_STATUS_FWD_HPP) +#define UTILS_PROCESS_STATUS_FWD_HPP + +namespace utils { +namespace process { + + +class status; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_STATUS_FWD_HPP) diff --git a/utils/process/status_test.cpp b/utils/process/status_test.cpp new file mode 100644 index 000000000000..5a3e19eeaf18 --- /dev/null +++ b/utils/process/status_test.cpp @@ -0,0 +1,209 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/status.hpp" + +extern "C" { +#include <sys/wait.h> + +#include <signal.h> +#include <unistd.h> +} + +#include <cstdlib> + +#include <atf-c++.hpp> + +#include "utils/test_utils.ipp" + +using utils::process::status; + + +namespace { + + +/// Body of a subprocess that exits with a particular exit status. +/// +/// \tparam ExitStatus The status to exit with. +template< int ExitStatus > +void child_exit(void) +{ + std::exit(ExitStatus); +} + + +/// Body of a subprocess that sends a particular signal to itself. +/// +/// \tparam Signo The signal to send to self. +template< int Signo > +void child_signal(void) +{ + ::kill(::getpid(), Signo); +} + + +/// Spawns a process and waits for completion. +/// +/// \param hook The function to run within the child. Should not return. +/// +/// \return The termination status of the spawned subprocess. +status +fork_and_wait(void (*hook)(void)) +{ + pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + hook(); + std::abort(); + } else { + int stat_loc; + ATF_REQUIRE(::waitpid(pid, &stat_loc, 0) != -1); + const status s = status(pid, stat_loc); + ATF_REQUIRE_EQ(pid, s.dead_pid()); + return s; + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(fake_exited) +ATF_TEST_CASE_BODY(fake_exited) +{ + const status fake = status::fake_exited(123); + ATF_REQUIRE_EQ(-1, fake.dead_pid()); + ATF_REQUIRE(fake.exited()); + ATF_REQUIRE_EQ(123, fake.exitstatus()); + ATF_REQUIRE(!fake.signaled()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(fake_signaled) +ATF_TEST_CASE_BODY(fake_signaled) +{ + const status fake = status::fake_signaled(567, true); + ATF_REQUIRE_EQ(-1, fake.dead_pid()); + ATF_REQUIRE(!fake.exited()); + ATF_REQUIRE(fake.signaled()); + ATF_REQUIRE_EQ(567, fake.termsig()); + ATF_REQUIRE(fake.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__exitstatus); +ATF_TEST_CASE_BODY(output__exitstatus) +{ + const status fake = status::fake_exited(123); + std::ostringstream str; + str << fake; + ATF_REQUIRE_EQ("status{exitstatus=123}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__signaled_without_core); +ATF_TEST_CASE_BODY(output__signaled_without_core) +{ + const status fake = status::fake_signaled(8, false); + std::ostringstream str; + str << fake; + ATF_REQUIRE_EQ("status{termsig=8, coredump=false}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__signaled_with_core); +ATF_TEST_CASE_BODY(output__signaled_with_core) +{ + const status fake = status::fake_signaled(9, true); + std::ostringstream str; + str << fake; + ATF_REQUIRE_EQ("status{termsig=9, coredump=true}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__exited); +ATF_TEST_CASE_BODY(integration__exited) +{ + const status exit_success = fork_and_wait(child_exit< EXIT_SUCCESS >); + ATF_REQUIRE(exit_success.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, exit_success.exitstatus()); + ATF_REQUIRE(!exit_success.signaled()); + + const status exit_failure = fork_and_wait(child_exit< EXIT_FAILURE >); + ATF_REQUIRE(exit_failure.exited()); + ATF_REQUIRE_EQ(EXIT_FAILURE, exit_failure.exitstatus()); + ATF_REQUIRE(!exit_failure.signaled()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__signaled); +ATF_TEST_CASE_BODY(integration__signaled) +{ + const status sigterm = fork_and_wait(child_signal< SIGTERM >); + ATF_REQUIRE(!sigterm.exited()); + ATF_REQUIRE(sigterm.signaled()); + ATF_REQUIRE_EQ(SIGTERM, sigterm.termsig()); + ATF_REQUIRE(!sigterm.coredump()); + + const status sigkill = fork_and_wait(child_signal< SIGKILL >); + ATF_REQUIRE(!sigkill.exited()); + ATF_REQUIRE(sigkill.signaled()); + ATF_REQUIRE_EQ(SIGKILL, sigkill.termsig()); + ATF_REQUIRE(!sigkill.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__coredump); +ATF_TEST_CASE_BODY(integration__coredump) +{ + utils::prepare_coredump_test(this); + + const status coredump = fork_and_wait(child_signal< SIGQUIT >); + ATF_REQUIRE(!coredump.exited()); + ATF_REQUIRE(coredump.signaled()); + ATF_REQUIRE_EQ(SIGQUIT, coredump.termsig()); +#if !defined(WCOREDUMP) + expect_fail("Platform does not support checking for coredump"); +#endif + ATF_REQUIRE(coredump.coredump()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, fake_exited); + ATF_ADD_TEST_CASE(tcs, fake_signaled); + + ATF_ADD_TEST_CASE(tcs, output__exitstatus); + ATF_ADD_TEST_CASE(tcs, output__signaled_without_core); + ATF_ADD_TEST_CASE(tcs, output__signaled_with_core); + + ATF_ADD_TEST_CASE(tcs, integration__exited); + ATF_ADD_TEST_CASE(tcs, integration__signaled); + ATF_ADD_TEST_CASE(tcs, integration__coredump); +} diff --git a/utils/process/system.cpp b/utils/process/system.cpp new file mode 100644 index 000000000000..ac41ddb7daa7 --- /dev/null +++ b/utils/process/system.cpp @@ -0,0 +1,59 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/system.hpp" + +extern "C" { +#include <sys/types.h> +#include <sys/wait.h> + +#include <fcntl.h> +#include <unistd.h> +} + +namespace detail = utils::process::detail; + + +/// Indirection to execute the dup2(2) system call. +int (*detail::syscall_dup2)(const int, const int) = ::dup2; + + +/// Indirection to execute the fork(2) system call. +pid_t (*detail::syscall_fork)(void) = ::fork; + + +/// Indirection to execute the open(2) system call. +int (*detail::syscall_open)(const char*, const int, ...) = ::open; + + +/// Indirection to execute the pipe(2) system call. +int (*detail::syscall_pipe)(int[2]) = ::pipe; + + +/// Indirection to execute the waitpid(2) system call. +pid_t (*detail::syscall_waitpid)(const pid_t, int*, const int) = ::waitpid; diff --git a/utils/process/system.hpp b/utils/process/system.hpp new file mode 100644 index 000000000000..a794876f3579 --- /dev/null +++ b/utils/process/system.hpp @@ -0,0 +1,66 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/system.hpp +/// Indirection to perform system calls. +/// +/// The indirections exposed in this file are provided to allow unit-testing of +/// particular system behaviors (e.g. failures). The caller of a routine in +/// this library is allowed, for testing purposes only, to explicitly replace +/// the pointers in this file with custom functions to inject a particular +/// behavior into the library code. +/// +/// Do not include this header from other header files. +/// +/// It may be nice to go one step further and completely abstract the library +/// functions in here to provide exception-based error reporting. + +#if !defined(UTILS_PROCESS_SYSTEM_HPP) +#define UTILS_PROCESS_SYSTEM_HPP + +extern "C" { +#include <unistd.h> +} + +namespace utils { +namespace process { +namespace detail { + + +extern int (*syscall_dup2)(const int, const int); +extern pid_t (*syscall_fork)(void); +extern int (*syscall_open)(const char*, const int, ...); +extern int (*syscall_pipe)(int[2]); +extern pid_t (*syscall_waitpid)(const pid_t, int*, const int); + + +} // namespace detail +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_SYSTEM_HPP) diff --git a/utils/process/systembuf.cpp b/utils/process/systembuf.cpp new file mode 100644 index 000000000000..661b336221ac --- /dev/null +++ b/utils/process/systembuf.cpp @@ -0,0 +1,152 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/systembuf.hpp" + +extern "C" { +#include <unistd.h> +} + +#include "utils/auto_array.ipp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" + +using utils::process::systembuf; + + +/// Private implementation fields for systembuf. +struct systembuf::impl : utils::noncopyable { + /// File descriptor attached to the systembuf. + int _fd; + + /// Size of the _read_buf and _write_buf buffers. + std::size_t _bufsize; + + /// In-memory buffer for read operations. + utils::auto_array< char > _read_buf; + + /// In-memory buffer for write operations. + utils::auto_array< char > _write_buf; + + /// Initializes private implementation data. + /// + /// \param fd The file descriptor. + /// \param bufsize The size of the created read and write buffers. + impl(const int fd, const std::size_t bufsize) : + _fd(fd), + _bufsize(bufsize), + _read_buf(new char[bufsize]), + _write_buf(new char[bufsize]) + { + } +}; + + +/// Constructs a new systembuf based on an open file descriptor. +/// +/// This grabs ownership of the file descriptor. +/// +/// \param fd The file descriptor to wrap. Must be open and valid. +/// \param bufsize The size to use for the internal read/write buffers. +systembuf::systembuf(const int fd, std::size_t bufsize) : + _pimpl(new impl(fd, bufsize)) +{ + setp(_pimpl->_write_buf.get(), _pimpl->_write_buf.get() + _pimpl->_bufsize); +} + + +/// Destroys a systembuf object. +/// +/// \post The file descriptor attached to this systembuf is closed. +systembuf::~systembuf(void) +{ + ::close(_pimpl->_fd); +} + + +/// Reads new data when the systembuf read buffer underflows. +/// +/// \return The new character to be read, or EOF if no more. +systembuf::int_type +systembuf::underflow(void) +{ + PRE(gptr() >= egptr()); + + bool ok; + ssize_t cnt = ::read(_pimpl->_fd, _pimpl->_read_buf.get(), + _pimpl->_bufsize); + ok = (cnt != -1 && cnt != 0); + + if (!ok) + return traits_type::eof(); + else { + setg(_pimpl->_read_buf.get(), _pimpl->_read_buf.get(), + _pimpl->_read_buf.get() + cnt); + return traits_type::to_int_type(*gptr()); + } +} + + +/// Writes data to the file descriptor when the write buffer overflows. +/// +/// \param c The character that causes the overflow. +/// +/// \return EOF if error, some other value for success. +/// +/// \throw something TODO(jmmv): According to the documentation, it is OK for +/// this method to throw in case of errors. Revisit this code to see if we +/// can do better. +systembuf::int_type +systembuf::overflow(int c) +{ + PRE(pptr() >= epptr()); + if (sync() == -1) + return traits_type::eof(); + if (!traits_type::eq_int_type(c, traits_type::eof())) { + traits_type::assign(*pptr(), c); + pbump(1); + } + return traits_type::not_eof(c); +} + + +/// Synchronizes the stream with the file descriptor. +/// +/// \return 0 on success, -1 on error. +int +systembuf::sync(void) +{ + ssize_t cnt = pptr() - pbase(); + + bool ok; + ok = ::write(_pimpl->_fd, pbase(), cnt) == cnt; + + if (ok) + pbump(-cnt); + return ok ? 0 : -1; +} diff --git a/utils/process/systembuf.hpp b/utils/process/systembuf.hpp new file mode 100644 index 000000000000..c89c9108dc4b --- /dev/null +++ b/utils/process/systembuf.hpp @@ -0,0 +1,71 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/systembuf.hpp +/// Provides the utils::process::systembuf class. + +#if !defined(UTILS_PROCESS_SYSTEMBUF_HPP) +#define UTILS_PROCESS_SYSTEMBUF_HPP + +#include "utils/process/systembuf_fwd.hpp" + +#include <cstddef> +#include <memory> +#include <streambuf> + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace process { + + +/// A std::streambuf implementation for raw file descriptors. +/// +/// This class grabs ownership of the file descriptor. I.e. when the class is +/// destroyed, the file descriptor is closed unconditionally. +class systembuf : public std::streambuf, noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +protected: + int_type underflow(void); + int_type overflow(int); + int sync(void); + +public: + explicit systembuf(const int, std::size_t = 8192); + ~systembuf(void); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_SYSTEMBUF_HPP) diff --git a/utils/process/systembuf_fwd.hpp b/utils/process/systembuf_fwd.hpp new file mode 100644 index 000000000000..b3e341336b1d --- /dev/null +++ b/utils/process/systembuf_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/process/systembuf_fwd.hpp +/// Forward declarations for utils/process/systembuf.hpp + +#if !defined(UTILS_PROCESS_SYSTEMBUF_FWD_HPP) +#define UTILS_PROCESS_SYSTEMBUF_FWD_HPP + +namespace utils { +namespace process { + + +class systembuf; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_SYSTEMBUF_FWD_HPP) diff --git a/utils/process/systembuf_test.cpp b/utils/process/systembuf_test.cpp new file mode 100644 index 000000000000..ef9ff1930cf6 --- /dev/null +++ b/utils/process/systembuf_test.cpp @@ -0,0 +1,166 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/process/systembuf.hpp" + +extern "C" { +#include <sys/stat.h> + +#include <fcntl.h> +#include <unistd.h> +} + +#include <fstream> + +#include <atf-c++.hpp> + +using utils::process::systembuf; + + +static void +check_data(std::istream& is, std::size_t length) +{ + char ch = 'A', chr; + std::size_t cnt = 0; + while (is >> chr) { + ATF_REQUIRE_EQ(ch, chr); + if (ch == 'Z') + ch = 'A'; + else + ch++; + cnt++; + } + ATF_REQUIRE_EQ(cnt, length); +} + + +static void +write_data(std::ostream& os, std::size_t length) +{ + char ch = 'A'; + for (std::size_t i = 0; i < length; i++) { + os << ch; + if (ch == 'Z') + ch = 'A'; + else + ch++; + } + os.flush(); +} + + +static void +test_read(std::size_t length, std::size_t bufsize) +{ + std::ofstream f("test_read.txt"); + write_data(f, length); + f.close(); + + int fd = ::open("test_read.txt", O_RDONLY); + ATF_REQUIRE(fd != -1); + systembuf sb(fd, bufsize); + std::istream is(&sb); + check_data(is, length); + ::close(fd); + ::unlink("test_read.txt"); +} + + +static void +test_write(std::size_t length, std::size_t bufsize) +{ + int fd = ::open("test_write.txt", O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + ATF_REQUIRE(fd != -1); + systembuf sb(fd, bufsize); + std::ostream os(&sb); + write_data(os, length); + ::close(fd); + + std::ifstream is("test_write.txt"); + check_data(is, length); + is.close(); + ::unlink("test_write.txt"); +} + + +ATF_TEST_CASE(short_read); +ATF_TEST_CASE_HEAD(short_read) +{ + set_md_var("descr", "Tests that a short read (one that fits in the " + "internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(short_read) +{ + test_read(64, 1024); +} + + +ATF_TEST_CASE(long_read); +ATF_TEST_CASE_HEAD(long_read) +{ + set_md_var("descr", "Tests that a long read (one that does not fit in " + "the internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(long_read) +{ + test_read(64 * 1024, 1024); +} + + +ATF_TEST_CASE(short_write); +ATF_TEST_CASE_HEAD(short_write) +{ + set_md_var("descr", "Tests that a short write (one that fits in the " + "internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(short_write) +{ + test_write(64, 1024); +} + + +ATF_TEST_CASE(long_write); +ATF_TEST_CASE_HEAD(long_write) +{ + set_md_var("descr", "Tests that a long write (one that does not fit " + "in the internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(long_write) +{ + test_write(64 * 1024, 1024); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, short_read); + ATF_ADD_TEST_CASE(tcs, long_read); + ATF_ADD_TEST_CASE(tcs, short_write); + ATF_ADD_TEST_CASE(tcs, long_write); +} diff --git a/utils/sanity.cpp b/utils/sanity.cpp new file mode 100644 index 000000000000..7978167d83ff --- /dev/null +++ b/utils/sanity.cpp @@ -0,0 +1,194 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <iostream> + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" + + +namespace { + + +/// List of fatal signals to be intercepted by the sanity code. +/// +/// The tests hardcode this list; update them whenever the list gets updated. +static int fatal_signals[] = { SIGABRT, SIGBUS, SIGSEGV, 0 }; + + +/// The path to the log file to report on crashes. Be aware that this is empty +/// until install_crash_handlers() is called. +static std::string logfile; + + +/// Prints a message to stderr. +/// +/// Note that this runs from a signal handler. Calling write() is OK. +/// +/// \param message The message to print. +static void +err_write(const std::string& message) +{ + if (::write(STDERR_FILENO, message.c_str(), message.length()) == -1) { + // We are crashing. If ::write fails, there is not much we could do, + // specially considering that we are running within a signal handler. + // Just ignore the error. + } +} + + +/// The crash handler for fatal signals. +/// +/// The sole purpose of this is to print some informational data before +/// reraising the original signal. +/// +/// \param signo The received signal. +static void +crash_handler(const int signo) +{ + PRE(!logfile.empty()); + + err_write(F("*** Fatal signal %s received\n") % signo); + err_write(F("*** Log file is %s\n") % logfile); + err_write(F("*** Please report this problem to %s detailing what you were " + "doing before the crash happened; if possible, include the log " + "file mentioned above\n") % PACKAGE_BUGREPORT); + + /// The handler is installed with SA_RESETHAND, so this is safe to do. We + /// really want to call the default handler to generate any possible core + /// dumps. + ::kill(::getpid(), signo); +} + + +/// Installs a handler for a fatal signal representing a crash. +/// +/// When the specified signal is captured, the crash_handler() will be called to +/// print some informational details to the user and, later, the signal will be +/// redelivered using the default handler to obtain a core dump. +/// +/// \param signo The fatal signal for which to install a handler. +static void +install_one_crash_handler(const int signo) +{ + struct ::sigaction sa; + sa.sa_handler = crash_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESETHAND; + + if (::sigaction(signo, &sa, NULL) == -1) { + const int original_errno = errno; + LW(F("Could not install crash handler for signal %s: %s") % + signo % std::strerror(original_errno)); + } else + LD(F("Installed crash handler for signal %s") % signo); +} + + +/// Returns a textual representation of an assertion type. +/// +/// The textual representation is user facing. +/// +/// \param type The type of the assertion. If the type is unknown for whatever +/// reason, a special message is returned. The code cannot abort in such a +/// case because this code is dealing for assertion errors. +/// +/// \return A textual description of the assertion type. +static std::string +format_type(const utils::assert_type type) +{ + switch (type) { + case utils::invariant: return "Invariant check failed"; + case utils::postcondition: return "Postcondition check failed"; + case utils::precondition: return "Precondition check failed"; + case utils::unreachable: return "Unreachable point reached"; + default: return "UNKNOWN ASSERTION TYPE"; + } +} + + +} // anonymous namespace + + +/// Raises an assertion error. +/// +/// This function prints information about the assertion failure and terminates +/// execution immediately by calling std::abort(). This ensures a coredump so +/// that the failure can be analyzed later. +/// +/// \param type The assertion type; this influences the printed message. +/// \param file The file in which the assertion failed. +/// \param line The line in which the assertion failed. +/// \param message The failure message associated to the condition. +void +utils::sanity_failure(const assert_type type, const char* file, + const size_t line, const std::string& message) +{ + std::cerr << "*** " << file << ":" << line << ": " << format_type(type); + if (!message.empty()) + std::cerr << ": " << message << "\n"; + else + std::cerr << "\n"; + std::abort(); +} + + +/// Installs persistent handlers for crash signals. +/// +/// Should be called at the very beginning of the execution of the program to +/// ensure that a signal handler for fatal crash signals is installed. +/// +/// \pre The function has not been called before. +/// +/// \param logfile_ The path to the log file to report during a crash. +void +utils::install_crash_handlers(const std::string& logfile_) +{ + static bool installed = false; + PRE(!installed); + logfile = logfile_; + + for (const int* iter = &fatal_signals[0]; *iter != 0; iter++) + install_one_crash_handler(*iter); + + installed = true; +} diff --git a/utils/sanity.hpp b/utils/sanity.hpp new file mode 100644 index 000000000000..6b126f984999 --- /dev/null +++ b/utils/sanity.hpp @@ -0,0 +1,183 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sanity.hpp +/// +/// Set of macros that replace the standard assert macro with more semantical +/// expressivity and meaningful diagnostics. Code should never use assert +/// directly. +/// +/// In general, the checks performed by the macros in this code are only +/// executed if the code is built with debugging support (that is, if the NDEBUG +/// macro is NOT defined). + +#if !defined(UTILS_SANITY_HPP) +#define UTILS_SANITY_HPP + +#include "utils/sanity_fwd.hpp" + +#include <cstddef> +#include <string> + +#include "utils/defs.hpp" + +namespace utils { + + +void sanity_failure(const assert_type, const char*, const size_t, + const std::string&) UTILS_NORETURN; + + +void install_crash_handlers(const std::string&); + + +} // namespace utils + + +/// \def _UTILS_ASSERT(type, expr, message) +/// \brief Performs an assertion check. +/// +/// This macro is internal and should not be used directly. +/// +/// Ensures that the given expression expr is true and, if not, terminates +/// execution by calling utils::sanity_failure(). The check is only performed +/// in debug builds. +/// +/// \param type The assertion type as defined by assert_type. +/// \param expr A boolean expression. +/// \param message A string describing the nature of the error. +#if !defined(NDEBUG) +# define _UTILS_ASSERT(type, expr, message) \ + do { \ + if (!(expr)) \ + utils::sanity_failure(type, __FILE__, __LINE__, message); \ + } while (0) +#else // defined(NDEBUG) +# define _UTILS_ASSERT(type, expr, message) do {} while (0) +#endif // !defined(NDEBUG) + + +/// Ensures that an invariant holds. +/// +/// If the invariant does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// INV_MSG instead. +/// +/// \param expr A boolean expression describing the invariant. +#define INV(expr) _UTILS_ASSERT(utils::invariant, expr, #expr) + + +/// Ensures that an invariant holds using a custom error message. +/// +/// If the invariant does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// \param expr A boolean expression describing the invariant. +/// \param msg The error message to print if the condition is false. +#define INV_MSG(expr, msg) _UTILS_ASSERT(utils::invariant, expr, msg) + + +/// Ensures that a precondition holds. +/// +/// If the precondition does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// PRE_MSG instead. +/// +/// \param expr A boolean expression describing the precondition. +#define PRE(expr) _UTILS_ASSERT(utils::precondition, expr, #expr) + + +/// Ensures that a precondition holds using a custom error message. +/// +/// If the precondition does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// \param expr A boolean expression describing the precondition. +/// \param msg The error message to print if the condition is false. +#define PRE_MSG(expr, msg) _UTILS_ASSERT(utils::precondition, expr, msg) + + +/// Ensures that an postcondition holds. +/// +/// If the postcondition does not hold, execution is immediately terminated. +/// The check is only performed in debug builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// POST_MSG instead. +/// +/// \param expr A boolean expression describing the postcondition. +#define POST(expr) _UTILS_ASSERT(utils::postcondition, expr, #expr) + + +/// Ensures that a postcondition holds using a custom error message. +/// +/// If the postcondition does not hold, execution is immediately terminated. +/// The check is only performed in debug builds. +/// +/// \param expr A boolean expression describing the postcondition. +/// \param msg The error message to print if the condition is false. +#define POST_MSG(expr, msg) _UTILS_ASSERT(utils::postcondition, expr, msg) + + +/// Ensures that a code path is not reached. +/// +/// If the code path in which this macro is located is reached, execution is +/// immediately terminated. Given that such a condition is critical for the +/// execution of the program (and to prevent build failures due to some code +/// paths not initializing variables, for example), this condition is fatal both +/// in debug and production builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// POST_MSG instead. +#define UNREACHABLE UNREACHABLE_MSG("") + + +/// Ensures that a code path is not reached using a custom error message. +/// +/// If the code path in which this macro is located is reached, execution is +/// immediately terminated. Given that such a condition is critical for the +/// execution of the program (and to prevent build failures due to some code +/// paths not initializing variables, for example), this condition is fatal both +/// in debug and production builds. +/// +/// \param msg The error message to print if the condition is false. +#define UNREACHABLE_MSG(msg) \ + do { \ + utils::sanity_failure(utils::unreachable, __FILE__, __LINE__, msg); \ + } while (0) + + +#endif // !defined(UTILS_SANITY_HPP) diff --git a/utils/sanity_fwd.hpp b/utils/sanity_fwd.hpp new file mode 100644 index 000000000000..98a897c0ff39 --- /dev/null +++ b/utils/sanity_fwd.hpp @@ -0,0 +1,52 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sanity_fwd.hpp +/// Forward declarations for utils/sanity.hpp + +#if !defined(UTILS_SANITY_FWD_HPP) +#define UTILS_SANITY_FWD_HPP + +namespace utils { + + +/// Enumeration to define the assertion type. +/// +/// The assertion type is used by the module to format the assertion messages +/// appropriately. +enum assert_type { + invariant, + postcondition, + precondition, + unreachable, +}; + + +} // namespace utils + +#endif // !defined(UTILS_SANITY_FWD_HPP) diff --git a/utils/sanity_test.cpp b/utils/sanity_test.cpp new file mode 100644 index 000000000000..54844fb75d64 --- /dev/null +++ b/utils/sanity_test.cpp @@ -0,0 +1,322 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sanity.hpp" + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <cstdlib> +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace process = utils::process; + + +#define FILE_REGEXP __FILE__ ":[0-9]+: " + + +static const fs::path Stdout_File("stdout.txt"); +static const fs::path Stderr_File("stderr.txt"); + + +#if NDEBUG +static bool NDebug = true; +#else +static bool NDebug = false; +#endif + + +template< typename Function > +static process::status +run_test(Function function) +{ + utils::avoid_coredump_on_crash(); + + const process::status status = process::child::fork_files( + function, Stdout_File, Stderr_File)->wait(); + atf::utils::cat_file(Stdout_File.str(), "Helper stdout: "); + atf::utils::cat_file(Stderr_File.str(), "Helper stderr: "); + return status; +} + + +static void +verify_success(const process::status& status) +{ + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("Before test", Stdout_File.str())); + ATF_REQUIRE(atf::utils::grep_file("After test", Stdout_File.str())); +} + + +static void +verify_failed(const process::status& status, const char* type, + const char* exp_message, const bool check_ndebug) +{ + if (check_ndebug && NDebug) { + std::cout << "Built with NDEBUG; skipping verification\n"; + verify_success(status); + } else { + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file("Before test", Stdout_File.str())); + ATF_REQUIRE(!atf::utils::grep_file("After test", Stdout_File.str())); + if (exp_message != NULL) + ATF_REQUIRE(atf::utils::grep_file(F(FILE_REGEXP "%s: %s") % + type % exp_message, + Stderr_File.str())); + else + ATF_REQUIRE(atf::utils::grep_file(F(FILE_REGEXP "%s") % type, + Stderr_File.str())); + } +} + + +template< bool Expression, bool WithMessage > +static void +do_inv_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + INV_MSG(Expression, "Custom message"); + else + INV(Expression); + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(inv__holds); +ATF_TEST_CASE_BODY(inv__holds) +{ + const process::status status = run_test(do_inv_test< true, false >); + verify_success(status); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(inv__triggers_default_message); +ATF_TEST_CASE_BODY(inv__triggers_default_message) +{ + const process::status status = run_test(do_inv_test< false, false >); + verify_failed(status, "Invariant check failed", "Expression", true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(inv__triggers_custom_message); +ATF_TEST_CASE_BODY(inv__triggers_custom_message) +{ + const process::status status = run_test(do_inv_test< false, true >); + verify_failed(status, "Invariant check failed", "Custom", true); +} + + +template< bool Expression, bool WithMessage > +static void +do_pre_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + PRE_MSG(Expression, "Custom message"); + else + PRE(Expression); + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pre__holds); +ATF_TEST_CASE_BODY(pre__holds) +{ + const process::status status = run_test(do_pre_test< true, false >); + verify_success(status); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pre__triggers_default_message); +ATF_TEST_CASE_BODY(pre__triggers_default_message) +{ + const process::status status = run_test(do_pre_test< false, false >); + verify_failed(status, "Precondition check failed", "Expression", true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pre__triggers_custom_message); +ATF_TEST_CASE_BODY(pre__triggers_custom_message) +{ + const process::status status = run_test(do_pre_test< false, true >); + verify_failed(status, "Precondition check failed", "Custom", true); +} + + +template< bool Expression, bool WithMessage > +static void +do_post_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + POST_MSG(Expression, "Custom message"); + else + POST(Expression); + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(post__holds); +ATF_TEST_CASE_BODY(post__holds) +{ + const process::status status = run_test(do_post_test< true, false >); + verify_success(status); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(post__triggers_default_message); +ATF_TEST_CASE_BODY(post__triggers_default_message) +{ + const process::status status = run_test(do_post_test< false, false >); + verify_failed(status, "Postcondition check failed", "Expression", true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(post__triggers_custom_message); +ATF_TEST_CASE_BODY(post__triggers_custom_message) +{ + const process::status status = run_test(do_post_test< false, true >); + verify_failed(status, "Postcondition check failed", "Custom", true); +} + + +template< bool WithMessage > +static void +do_unreachable_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + UNREACHABLE_MSG("Custom message"); + else + UNREACHABLE; + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unreachable__default_message); +ATF_TEST_CASE_BODY(unreachable__default_message) +{ + const process::status status = run_test(do_unreachable_test< false >); + verify_failed(status, "Unreachable point reached", NULL, false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unreachable__custom_message); +ATF_TEST_CASE_BODY(unreachable__custom_message) +{ + const process::status status = run_test(do_unreachable_test< true >); + verify_failed(status, "Unreachable point reached", "Custom", false); +} + + +template< int Signo > +static void +do_crash_handler_test(void) +{ + utils::install_crash_handlers("test-log.txt"); + ::kill(::getpid(), Signo); + std::cout << "After signal\n"; + std::exit(EXIT_FAILURE); +} + + +template< int Signo > +static void +crash_handler_test(void) +{ + utils::avoid_coredump_on_crash(); + + const process::status status = run_test(do_crash_handler_test< Signo >); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(Signo, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file(F("Fatal signal %s") % Signo, + Stderr_File.str())); + ATF_REQUIRE(atf::utils::grep_file("Log file is test-log.txt", + Stderr_File.str())); + ATF_REQUIRE(!atf::utils::grep_file("After signal", Stdout_File.str())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(install_crash_handlers__sigabrt); +ATF_TEST_CASE_BODY(install_crash_handlers__sigabrt) +{ + crash_handler_test< SIGABRT >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(install_crash_handlers__sigbus); +ATF_TEST_CASE_BODY(install_crash_handlers__sigbus) +{ + crash_handler_test< SIGBUS >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(install_crash_handlers__sigsegv); +ATF_TEST_CASE_BODY(install_crash_handlers__sigsegv) +{ + crash_handler_test< SIGSEGV >(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, inv__holds); + ATF_ADD_TEST_CASE(tcs, inv__triggers_default_message); + ATF_ADD_TEST_CASE(tcs, inv__triggers_custom_message); + ATF_ADD_TEST_CASE(tcs, pre__holds); + ATF_ADD_TEST_CASE(tcs, pre__triggers_default_message); + ATF_ADD_TEST_CASE(tcs, pre__triggers_custom_message); + ATF_ADD_TEST_CASE(tcs, post__holds); + ATF_ADD_TEST_CASE(tcs, post__triggers_default_message); + ATF_ADD_TEST_CASE(tcs, post__triggers_custom_message); + ATF_ADD_TEST_CASE(tcs, unreachable__default_message); + ATF_ADD_TEST_CASE(tcs, unreachable__custom_message); + + ATF_ADD_TEST_CASE(tcs, install_crash_handlers__sigabrt); + ATF_ADD_TEST_CASE(tcs, install_crash_handlers__sigbus); + ATF_ADD_TEST_CASE(tcs, install_crash_handlers__sigsegv); +} diff --git a/utils/signals/Kyuafile b/utils/signals/Kyuafile new file mode 100644 index 000000000000..09da3e166cd2 --- /dev/null +++ b/utils/signals/Kyuafile @@ -0,0 +1,9 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="exceptions_test"} +atf_test_program{name="interrupts_test"} +atf_test_program{name="misc_test"} +atf_test_program{name="programmer_test"} +atf_test_program{name="timer_test"} diff --git a/utils/signals/Makefile.am.inc b/utils/signals/Makefile.am.inc new file mode 100644 index 000000000000..b01089c80fea --- /dev/null +++ b/utils/signals/Makefile.am.inc @@ -0,0 +1,73 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +libutils_a_SOURCES += utils/signals/exceptions.cpp +libutils_a_SOURCES += utils/signals/exceptions.hpp +libutils_a_SOURCES += utils/signals/interrupts.cpp +libutils_a_SOURCES += utils/signals/interrupts.hpp +libutils_a_SOURCES += utils/signals/interrupts_fwd.hpp +libutils_a_SOURCES += utils/signals/misc.cpp +libutils_a_SOURCES += utils/signals/misc.hpp +libutils_a_SOURCES += utils/signals/programmer.cpp +libutils_a_SOURCES += utils/signals/programmer.hpp +libutils_a_SOURCES += utils/signals/programmer_fwd.hpp +libutils_a_SOURCES += utils/signals/timer.cpp +libutils_a_SOURCES += utils/signals/timer.hpp +libutils_a_SOURCES += utils/signals/timer_fwd.hpp + +if WITH_ATF +tests_utils_signalsdir = $(pkgtestsdir)/utils/signals + +tests_utils_signals_DATA = utils/signals/Kyuafile +EXTRA_DIST += $(tests_utils_signals_DATA) + +tests_utils_signals_PROGRAMS = utils/signals/exceptions_test +utils_signals_exceptions_test_SOURCES = utils/signals/exceptions_test.cpp +utils_signals_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/interrupts_test +utils_signals_interrupts_test_SOURCES = utils/signals/interrupts_test.cpp +utils_signals_interrupts_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_interrupts_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/misc_test +utils_signals_misc_test_SOURCES = utils/signals/misc_test.cpp +utils_signals_misc_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_misc_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/programmer_test +utils_signals_programmer_test_SOURCES = utils/signals/programmer_test.cpp +utils_signals_programmer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_programmer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/timer_test +utils_signals_timer_test_SOURCES = utils/signals/timer_test.cpp +utils_signals_timer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_timer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/signals/exceptions.cpp b/utils/signals/exceptions.cpp new file mode 100644 index 000000000000..70e0dbe8a5d1 --- /dev/null +++ b/utils/signals/exceptions.cpp @@ -0,0 +1,102 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/exceptions.hpp" + +#include <cstring> + +#include "utils/format/macros.hpp" + +namespace signals = utils::signals; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +signals::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +signals::error::~error(void) throw() +{ +} + + +/// Constructs a new interrupted error. +/// +/// \param signo_ The signal that caused the interrupt. +signals::interrupted_error::interrupted_error(const int signo_) : + error(F("Interrupted by signal %s") % signo_), + _signo(signo_) +{ +} + + +/// Destructor for the error. +signals::interrupted_error::~interrupted_error(void) throw() +{ +} + + +/// Queries the signal number of the interruption. +/// +/// \return A signal number. +int +signals::interrupted_error::signo(void) const +{ + return _signo; +} + + +/// Constructs a new error based on an errno code. +/// +/// \param message_ The message describing what caused the error. +/// \param errno_ The error code. +signals::system_error::system_error(const std::string& message_, + const int errno_) : + error(F("%s: %s") % message_ % strerror(errno_)), + _original_errno(errno_) +{ +} + + +/// Destructor for the error. +signals::system_error::~system_error(void) throw() +{ +} + + +/// \return The original errno value. +int +signals::system_error::original_errno(void) const throw() +{ + return _original_errno; +} diff --git a/utils/signals/exceptions.hpp b/utils/signals/exceptions.hpp new file mode 100644 index 000000000000..35cd2c9e8168 --- /dev/null +++ b/utils/signals/exceptions.hpp @@ -0,0 +1,83 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/exceptions.hpp +/// Exception types raised by the signals module. + +#if !defined(UTILS_SIGNALS_EXCEPTIONS_HPP) +#define UTILS_SIGNALS_EXCEPTIONS_HPP + +#include <stdexcept> + +namespace utils { +namespace signals { + + +/// Base exceptions for signals errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Denotes the reception of a signal to controlledly terminate execution. +class interrupted_error : public error { + /// Signal that caused the interrupt. + int _signo; + +public: + explicit interrupted_error(const int signo_); + ~interrupted_error(void) throw(); + + int signo(void) const; +}; + + +/// Exceptions for errno-based errors. +/// +/// TODO(jmmv): This code is duplicated in, at least, utils::fs. Figure +/// out a way to reuse this exception while maintaining the correct inheritance +/// (i.e. be able to keep it as a child of signals::error). +class system_error : public error { + /// Error number describing this libc error condition. + int _original_errno; + +public: + explicit system_error(const std::string&, const int); + ~system_error(void) throw(); + + int original_errno(void) const throw(); +}; + + +} // namespace signals +} // namespace utils + + +#endif // !defined(UTILS_SIGNALS_EXCEPTIONS_HPP) diff --git a/utils/signals/exceptions_test.cpp b/utils/signals/exceptions_test.cpp new file mode 100644 index 000000000000..40db536f1a8c --- /dev/null +++ b/utils/signals/exceptions_test.cpp @@ -0,0 +1,73 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/exceptions.hpp" + +#include <cerrno> +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" + +namespace signals = utils::signals; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const signals::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupted_error); +ATF_TEST_CASE_BODY(interrupted_error) +{ + const signals::interrupted_error e(5); + ATF_REQUIRE(std::strcmp("Interrupted by signal 5", e.what()) == 0); + ATF_REQUIRE_EQ(5, e.signo()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(system_error); +ATF_TEST_CASE_BODY(system_error) +{ + const signals::system_error e("Call failed", ENOENT); + const std::string expected = F("Call failed: %s") % std::strerror(ENOENT); + ATF_REQUIRE_EQ(expected, e.what()); + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, interrupted_error); + ATF_ADD_TEST_CASE(tcs, system_error); +} diff --git a/utils/signals/interrupts.cpp b/utils/signals/interrupts.cpp new file mode 100644 index 000000000000..956a83c66802 --- /dev/null +++ b/utils/signals/interrupts.cpp @@ -0,0 +1,309 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/interrupts.hpp" + +extern "C" { +#include <sys/types.h> + +#include <signal.h> +#include <unistd.h> +} + +#include <cstdlib> +#include <cstring> +#include <set> + +#include "utils/logging/macros.hpp" +#include "utils/process/operations.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/signals/programmer.hpp" + +namespace signals = utils::signals; +namespace process = utils::process; + + +namespace { + + +/// The interrupt signal that fired, or -1 if none. +static volatile int fired_signal = -1; + + +/// Collection of PIDs. +typedef std::set< pid_t > pids_set; + + +/// List of processes to kill upon reception of a signal. +static pids_set pids_to_kill; + + +/// Programmer status for the SIGHUP signal. +static std::auto_ptr< signals::programmer > sighup_handler; +/// Programmer status for the SIGINT signal. +static std::auto_ptr< signals::programmer > sigint_handler; +/// Programmer status for the SIGTERM signal. +static std::auto_ptr< signals::programmer > sigterm_handler; + + +/// Signal mask to restore after exiting a signal inhibited section. +static sigset_t global_old_sigmask; + + +/// Whether there is an interrupts_handler object in existence or not. +static bool interrupts_handler_active = false; + + +/// Whether there is an interrupts_inhibiter object in existence or not. +static std::size_t interrupts_inhibiter_active = 0; + + +/// Generic handler to capture interrupt signals. +/// +/// From this handler, we record that an interrupt has happened so that +/// check_interrupt() can know whether there execution has to be stopped or not. +/// We also terminate any of our child processes (started by the +/// utils::process::children class) so that any ongoing wait(2) system calls +/// terminate. +/// +/// \param signo The signal that caused this handler to be called. +static void +signal_handler(const int signo) +{ + static const char* message = "[-- Signal caught; please wait for " + "cleanup --]\n"; + if (::write(STDERR_FILENO, message, std::strlen(message)) == -1) { + // We are exiting: the message printed here is only for informational + // purposes. If we fail to print it (which probably means something + // is really bad), there is not much we can do within the signal + // handler, so just ignore this. + } + + fired_signal = signo; + + for (pids_set::const_iterator iter = pids_to_kill.begin(); + iter != pids_to_kill.end(); ++iter) { + process::terminate_group(*iter); + } +} + + +/// Installs signal handlers for potential interrupts. +/// +/// \pre Must not have been called before. +/// \post The various sig*_handler global variables are atomically updated. +static void +setup_handlers(void) +{ + PRE(sighup_handler.get() == NULL); + PRE(sigint_handler.get() == NULL); + PRE(sigterm_handler.get() == NULL); + + // Create the handlers on the stack first so that, if any of them fails, the + // stack unwinding cleans things up. + std::auto_ptr< signals::programmer > tmp_sighup_handler( + new signals::programmer(SIGHUP, signal_handler)); + std::auto_ptr< signals::programmer > tmp_sigint_handler( + new signals::programmer(SIGINT, signal_handler)); + std::auto_ptr< signals::programmer > tmp_sigterm_handler( + new signals::programmer(SIGTERM, signal_handler)); + + // Now, update the global pointers, which is an operation that cannot fail. + sighup_handler = tmp_sighup_handler; + sigint_handler = tmp_sigint_handler; + sigterm_handler = tmp_sigterm_handler; +} + + +/// Uninstalls the signal handlers installed by setup_handlers(). +static void +cleanup_handlers(void) +{ + sighup_handler->unprogram(); sighup_handler.reset(NULL); + sigint_handler->unprogram(); sigint_handler.reset(NULL); + sigterm_handler->unprogram(); sigterm_handler.reset(NULL); +} + + + +/// Masks the signals installed by setup_handlers(). +/// +/// \param[out] old_sigmask The old signal mask to save via the +/// \code oset \endcode argument with sigprocmask(2). +static void +mask_signals(sigset_t* old_sigmask) +{ + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGALRM); + sigaddset(&mask, SIGHUP); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + const int ret = ::sigprocmask(SIG_BLOCK, &mask, old_sigmask); + INV(ret != -1); +} + + +/// Resets the signal masking put in place by mask_signals(). +/// +/// \param[in] old_sigmask The old signal mask to restore via the +/// \code set \endcode argument with sigprocmask(2). +static void +unmask_signals(sigset_t* old_sigmask) +{ + const int ret = ::sigprocmask(SIG_SETMASK, old_sigmask, NULL); + INV(ret != -1); +} + + +} // anonymous namespace + + +/// Constructor that sets up the signal handlers. +signals::interrupts_handler::interrupts_handler(void) : + _programmed(false) +{ + PRE(!interrupts_handler_active); + setup_handlers(); + _programmed = true; + interrupts_handler_active = true; +} + + +/// Destructor that removes the signal handlers. +/// +/// Given that this is a destructor and it can't report errors back to the +/// caller, the caller must attempt to call unprogram() on its own. +signals::interrupts_handler::~interrupts_handler(void) +{ + if (_programmed) { + LW("Destroying still-programmed signals::interrupts_handler object"); + try { + unprogram(); + } catch (const error& e) { + UNREACHABLE; + } + } +} + + +/// Unprograms all signals captured by the interrupts handler. +/// +/// \throw system_error If the unprogramming of any signal fails. +void +signals::interrupts_handler::unprogram(void) +{ + PRE(_programmed); + + // Modify the control variables first before unprogramming the handlers. If + // we fail to do the latter, we do not want to try again because we will not + // succeed (and we'll cause a crash due to failed preconditions). + _programmed = false; + interrupts_handler_active = false; + + cleanup_handlers(); + fired_signal = -1; +} + + +/// Constructor that sets up signal masking. +signals::interrupts_inhibiter::interrupts_inhibiter(void) +{ + sigset_t old_sigmask; + mask_signals(&old_sigmask); + if (interrupts_inhibiter_active == 0) { + global_old_sigmask = old_sigmask; + } + ++interrupts_inhibiter_active; +} + + +/// Destructor that removes signal masking. +signals::interrupts_inhibiter::~interrupts_inhibiter(void) +{ + if (interrupts_inhibiter_active > 1) { + --interrupts_inhibiter_active; + } else { + interrupts_inhibiter_active = false; + unmask_signals(&global_old_sigmask); + } +} + + +/// Checks if an interrupt has fired. +/// +/// Calls to this function should be sprinkled in strategic places through the +/// code protected by an interrupts_handler object. +/// +/// Only one call to this function will raise an exception per signal received. +/// This is to allow executing cleanup actions without reraising interrupt +/// exceptions unless the user has fired another interrupt. +/// +/// \throw interrupted_error If there has been an interrupt. +void +signals::check_interrupt(void) +{ + if (fired_signal != -1) { + const int original_fired_signal = fired_signal; + fired_signal = -1; + throw interrupted_error(original_fired_signal); + } +} + + +/// Registers a child process to be killed upon reception of an interrupt. +/// +/// \pre Must be called with interrupts being inhibited. The caller must ensure +/// that the call call to fork() and the addition of the PID happen atomically. +/// +/// \param pid The PID of the child process. Must not have been yet regsitered. +void +signals::add_pid_to_kill(const pid_t pid) +{ + PRE(interrupts_inhibiter_active); + PRE(pids_to_kill.find(pid) == pids_to_kill.end()); + pids_to_kill.insert(pid); +} + + +/// Unregisters a child process previously registered via add_pid_to_kill(). +/// +/// \pre Must be called with interrupts being inhibited. This is not necessary, +/// but pushing this to the caller simplifies our logic and provides consistency +/// with the add_pid_to_kill() call. +/// +/// \param pid The PID of the child process. Must have been registered +/// previously, and the process must have already been awaited for. +void +signals::remove_pid_to_kill(const pid_t pid) +{ + PRE(interrupts_inhibiter_active); + PRE(pids_to_kill.find(pid) != pids_to_kill.end()); + pids_to_kill.erase(pid); +} diff --git a/utils/signals/interrupts.hpp b/utils/signals/interrupts.hpp new file mode 100644 index 000000000000..b181114bb245 --- /dev/null +++ b/utils/signals/interrupts.hpp @@ -0,0 +1,83 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/interrupts.hpp +/// Handling of interrupts. + +#if !defined(UTILS_SIGNALS_INTERRUPTS_HPP) +#define UTILS_SIGNALS_INTERRUPTS_HPP + +#include "utils/signals/interrupts_fwd.hpp" + +#include <unistd.h> + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace signals { + + +/// Provides a scope in which interrupts can be detected and handled. +/// +/// This RAII-modeled object installs signal handler when instantiated and +/// removes them upon destruction. While this object is active, the +/// check_interrupt() free function can be used to determine if an interrupt has +/// happened. +class interrupts_handler : noncopyable { + /// Whether the interrupts are still programmed or not. + /// + /// Used by the destructor to prevent double-unprogramming when unprogram() + /// is explicitly called by the user. + bool _programmed; + +public: + interrupts_handler(void); + ~interrupts_handler(void); + + void unprogram(void); +}; + + +/// Disables interrupts while the object is alive. +class interrupts_inhibiter : noncopyable { +public: + interrupts_inhibiter(void); + ~interrupts_inhibiter(void); +}; + + +void check_interrupt(void); + +void add_pid_to_kill(const pid_t); +void remove_pid_to_kill(const pid_t); + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_INTERRUPTS_HPP) diff --git a/utils/signals/interrupts_fwd.hpp b/utils/signals/interrupts_fwd.hpp new file mode 100644 index 000000000000..e4dfe68d54e2 --- /dev/null +++ b/utils/signals/interrupts_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/interrupts_fwd.hpp +/// Forward declarations for utils/signals/interrupts.hpp + +#if !defined(UTILS_SIGNALS_INTERRUPTS_FWD_HPP) +#define UTILS_SIGNALS_INTERRUPTS_FWD_HPP + +namespace utils { +namespace signals { + + +class interrupts_handler; +class interrupts_inhibiter; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_INTERRUPTS_FWD_HPP) diff --git a/utils/signals/interrupts_test.cpp b/utils/signals/interrupts_test.cpp new file mode 100644 index 000000000000..ef8758d8d5f1 --- /dev/null +++ b/utils/signals/interrupts_test.cpp @@ -0,0 +1,266 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/interrupts.hpp" + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <cstdlib> +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/signals/programmer.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +namespace { + + +/// Set to the signal that fired; -1 if none. +static volatile int fired_signal = -1; + + +/// Test handler for signals. +/// +/// \post fired_signal is set to the signal that triggered the handler. +/// +/// \param signo The signal that triggered the handler. +static void +signal_handler(const int signo) +{ + PRE(fired_signal == -1 || fired_signal == signo); + fired_signal = signo; +} + + +/// Child process that pauses waiting to be killed. +static void +pause_child(void) +{ + sigset_t mask; + sigemptyset(&mask); + // We loop waiting for signals because we want the parent process to send us + // a SIGKILL that we cannot handle, not just any non-deadly signal. + for (;;) { + std::cerr << F("Waiting for any signal; pid=%s\n") % ::getpid(); + ::sigsuspend(&mask); + std::cerr << F("Signal received; pid=%s\n") % ::getpid(); + } +} + + +/// Checks that interrupts_handler() handles a particular signal. +/// +/// This indirectly checks the check_interrupt() function, which is not part of +/// the class but is tightly related. +/// +/// \param signo The signal to check. +/// \param explicit_unprogram Whether to call interrupts_handler::unprogram() +/// explicitly before letting the object go out of scope. +static void +check_interrupts_handler(const int signo, const bool explicit_unprogram) +{ + fired_signal = -1; + + signals::programmer test_handler(signo, signal_handler); + + { + signals::interrupts_handler interrupts; + + // No pending interrupts at first. + signals::check_interrupt(); + + // Send us an interrupt and check for it. + ::kill(getpid(), signo); + ATF_REQUIRE_THROW_RE(signals::interrupted_error, + F("Interrupted by signal %s") % signo, + signals::check_interrupt()); + + // Interrupts should have been cleared now, so this should not throw. + signals::check_interrupt(); + + // Check to see if a second interrupt is detected. + ::kill(getpid(), signo); + ATF_REQUIRE_THROW_RE(signals::interrupted_error, + F("Interrupted by signal %s") % signo, + signals::check_interrupt()); + + // And ensure the interrupt was cleared again. + signals::check_interrupt(); + + if (explicit_unprogram) { + interrupts.unprogram(); + } + } + + ATF_REQUIRE_EQ(-1, fired_signal); + ::kill(getpid(), signo); + ATF_REQUIRE_EQ(signo, fired_signal); + + test_handler.unprogram(); +} + + +/// Checks that interrupts_inhibiter() handles a particular signal. +/// +/// \param signo The signal to check. +static void +check_interrupts_inhibiter(const int signo) +{ + signals::programmer test_handler(signo, signal_handler); + + { + signals::interrupts_inhibiter inhibiter; + { + signals::interrupts_inhibiter nested_inhibiter; + ::kill(::getpid(), signo); + ATF_REQUIRE_EQ(-1, fired_signal); + } + ::kill(::getpid(), signo); + ATF_REQUIRE_EQ(-1, fired_signal); + } + ATF_REQUIRE_EQ(signo, fired_signal); + + test_handler.unprogram(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_handler__sighup); +ATF_TEST_CASE_BODY(interrupts_handler__sighup) +{ + // We run this twice in sequence to ensure that we can actually program two + // interrupts handlers in a row. + check_interrupts_handler(SIGHUP, true); + check_interrupts_handler(SIGHUP, false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_handler__sigint); +ATF_TEST_CASE_BODY(interrupts_handler__sigint) +{ + // We run this twice in sequence to ensure that we can actually program two + // interrupts handlers in a row. + check_interrupts_handler(SIGINT, true); + check_interrupts_handler(SIGINT, false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_handler__sigterm); +ATF_TEST_CASE_BODY(interrupts_handler__sigterm) +{ + // We run this twice in sequence to ensure that we can actually program two + // interrupts handlers in a row. + check_interrupts_handler(SIGTERM, true); + check_interrupts_handler(SIGTERM, false); +} + + +ATF_TEST_CASE(interrupts_handler__kill_children); +ATF_TEST_CASE_HEAD(interrupts_handler__kill_children) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(interrupts_handler__kill_children) +{ + std::auto_ptr< process::child > child1(process::child::fork_files( + pause_child, fs::path("/dev/stdout"), fs::path("/dev/stderr"))); + std::auto_ptr< process::child > child2(process::child::fork_files( + pause_child, fs::path("/dev/stdout"), fs::path("/dev/stderr"))); + + signals::interrupts_handler interrupts; + + // Our children pause until the reception of a signal. Interrupting + // ourselves will cause the signal to be re-delivered to our children due to + // the interrupts_handler semantics. If this does not happen, the wait + // calls below would block indefinitely and cause our test to time out. + ::kill(::getpid(), SIGHUP); + + const process::status status1 = child1->wait(); + ATF_REQUIRE(status1.signaled()); + ATF_REQUIRE_EQ(SIGKILL, status1.termsig()); + const process::status status2 = child2->wait(); + ATF_REQUIRE(status2.signaled()); + ATF_REQUIRE_EQ(SIGKILL, status2.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sigalrm); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sigalrm) +{ + check_interrupts_inhibiter(SIGALRM); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sighup); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sighup) +{ + check_interrupts_inhibiter(SIGHUP); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sigint); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sigint) +{ + check_interrupts_inhibiter(SIGINT); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sigterm); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sigterm) +{ + check_interrupts_inhibiter(SIGTERM); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, interrupts_handler__sighup); + ATF_ADD_TEST_CASE(tcs, interrupts_handler__sigint); + ATF_ADD_TEST_CASE(tcs, interrupts_handler__sigterm); + ATF_ADD_TEST_CASE(tcs, interrupts_handler__kill_children); + + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sigalrm); + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sighup); + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sigint); + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sigterm); +} diff --git a/utils/signals/misc.cpp b/utils/signals/misc.cpp new file mode 100644 index 000000000000..b9eb1c402a28 --- /dev/null +++ b/utils/signals/misc.cpp @@ -0,0 +1,106 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/misc.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include <signal.h> +} + +#include <cerrno> +#include <cstddef> + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/signals/exceptions.hpp" + +namespace signals = utils::signals; + + +/// Number of the last valid signal. +const int utils::signals::last_signo = LAST_SIGNO; + + +/// Resets a signal handler to its default behavior. +/// +/// \param signo The number of the signal handler to reset. +/// +/// \throw signals::system_error If there is a problem trying to reset the +/// signal handler to its default behavior. +void +signals::reset(const int signo) +{ + struct ::sigaction sa; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + if (::sigaction(signo, &sa, NULL) == -1) { + const int original_errno = errno; + throw system_error(F("Failed to reset signal %s") % signo, + original_errno); + } +} + + +/// Resets all signals to their default handlers. +/// +/// \return True if all signals could be reset properly; false otherwise. +bool +signals::reset_all(void) +{ + bool ok = true; + + for (int signo = 1; signo <= signals::last_signo; ++signo) { + if (signo == SIGKILL || signo == SIGSTOP) { + // Don't attempt to reset immutable signals. + } else { + try { + signals::reset(signo); + } catch (const signals::error& e) { +#if defined(SIGTHR) + if (signo == SIGTHR) { + // If FreeBSD's libthr is loaded, it prevents us from + // modifying SIGTHR (at least in 11.0-CURRENT as of + // 2015-01-28). Skip failures for this signal if they + // happen to avoid this corner case. + continue; + } +#endif + LW(e.what()); + ok = false; + } + } + } + + return ok; +} diff --git a/utils/signals/misc.hpp b/utils/signals/misc.hpp new file mode 100644 index 000000000000..ad3763feabc4 --- /dev/null +++ b/utils/signals/misc.hpp @@ -0,0 +1,49 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/misc.hpp +/// Free functions and globals. + +#if !defined(UTILS_SIGNALS_MISC_HPP) +#define UTILS_SIGNALS_MISC_HPP + +namespace utils { +namespace signals { + + +extern const int last_signo; + + +void reset(const int); +bool reset_all(void); + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_MISC_HPP) diff --git a/utils/signals/misc_test.cpp b/utils/signals/misc_test.cpp new file mode 100644 index 000000000000..76f36b0e5082 --- /dev/null +++ b/utils/signals/misc_test.cpp @@ -0,0 +1,133 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/misc.hpp" + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <cstdlib> + +#include <atf-c++.hpp> + +#include "utils/defs.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/signals/exceptions.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +namespace { + + +static void program_reset_raise(void) UTILS_NORETURN; + + +/// Body of a subprocess that tests the signals::reset function. +/// +/// This function programs a signal to be ignored, then uses signal::reset to +/// bring it back to its default handler and then delivers the signal to self. +/// The default behavior of the signal is for the process to die, so this +/// function should never return correctly (and thus the child process should +/// always die due to a signal if all goes well). +static void +program_reset_raise(void) +{ + struct ::sigaction sa; + sa.sa_handler = SIG_IGN; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + if (::sigaction(SIGUSR1, &sa, NULL) == -1) + std::exit(EXIT_FAILURE); + + signals::reset(SIGUSR1); + ::kill(::getpid(), SIGUSR1); + + // Should not be reached, but we do not assert this condition because we + // want to exit cleanly if the signal does not abort our execution to let + // the parent easily know what happened. + std::exit(EXIT_SUCCESS); +} + + +/// Body of a subprocess that executes the signals::reset_all function. +/// +/// The process exits with success if the function worked, or with a failure if +/// an error is reported. No signals are tested. +static void +run_reset_all(void) +{ + const bool ok = signals::reset_all(); + std::exit(ok ? EXIT_SUCCESS : EXIT_FAILURE); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(reset__ok); +ATF_TEST_CASE_BODY(reset__ok) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + program_reset_raise, fs::path("/dev/stdout"), fs::path("/dev/stderr")); + process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGUSR1, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(reset__invalid); +ATF_TEST_CASE_BODY(reset__invalid) +{ + ATF_REQUIRE_THROW(signals::system_error, signals::reset(-1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(reset_all); +ATF_TEST_CASE_BODY(reset_all) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + run_reset_all, fs::path("/dev/stdout"), fs::path("/dev/stderr")); + process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, reset__ok); + ATF_ADD_TEST_CASE(tcs, reset__invalid); + ATF_ADD_TEST_CASE(tcs, reset_all); +} diff --git a/utils/signals/programmer.cpp b/utils/signals/programmer.cpp new file mode 100644 index 000000000000..c47d1cf85038 --- /dev/null +++ b/utils/signals/programmer.cpp @@ -0,0 +1,138 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/programmer.hpp" + +extern "C" { +#include <signal.h> +} + +#include <cerrno> + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" + + +namespace utils { +namespace signals { + + +/// Internal implementation for the signals::programmer class. +struct programmer::impl : utils::noncopyable { + /// The number of the signal managed by this programmer. + int signo; + + /// Whether the signal is currently programmed by us or not. + bool programmed; + + /// The signal handler that we replaced; to be restored on unprogramming. + struct ::sigaction old_sa; + + /// Initializes the internal implementation of the programmer. + /// + /// \param signo_ The signal number. + impl(const int signo_) : + signo(signo_), + programmed(false) + { + } +}; + + +} // namespace signals +} // namespace utils + + +namespace signals = utils::signals; + + +/// Programs a signal handler. +/// +/// \param signo The signal for which to install the handler. +/// \param handler The handler to install. +/// +/// \throw signals::system_error If there is an error programming the signal. +signals::programmer::programmer(const int signo, const handler_type handler) : + _pimpl(new impl(signo)) +{ + struct ::sigaction sa; + sa.sa_handler = handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + + if (::sigaction(_pimpl->signo, &sa, &_pimpl->old_sa) == -1) { + const int original_errno = errno; + throw system_error(F("Could not install handler for signal %s") % + _pimpl->signo, original_errno); + } else + _pimpl->programmed = true; +} + + +/// Destructor; unprograms the signal handler if still programmed. +/// +/// Given that this is a destructor and it can't report errors back to the +/// caller, the caller must attempt to call unprogram() on its own. +signals::programmer::~programmer(void) +{ + if (_pimpl->programmed) { + LW("Destroying still-programmed signals::programmer object"); + try { + unprogram(); + } catch (const system_error& e) { + UNREACHABLE; + } + } +} + + +/// Unprograms the signal handler. +/// +/// \pre The signal handler is programmed (i.e. this can only be called once). +/// +/// \throw system_error If unprogramming the signal failed. If this happens, +/// the signal is left programmed, this object forgets about the signal and +/// therefore there is no way to restore the original handler. +void +signals::programmer::unprogram(void) +{ + PRE(_pimpl->programmed); + + // If we fail, we don't want the destructor to attempt to unprogram the + // handler again, as it would result in a crash. + _pimpl->programmed = false; + + if (::sigaction(_pimpl->signo, &_pimpl->old_sa, NULL) == -1) { + const int original_errno = errno; + throw system_error(F("Could not reset handler for signal %s") % + _pimpl->signo, original_errno); + } +} diff --git a/utils/signals/programmer.hpp b/utils/signals/programmer.hpp new file mode 100644 index 000000000000..5ac5318f0bb9 --- /dev/null +++ b/utils/signals/programmer.hpp @@ -0,0 +1,63 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/programmer.hpp +/// Provides the signals::programmer class. + +#if !defined(UTILS_SIGNALS_PROGRAMMER_HPP) +#define UTILS_SIGNALS_PROGRAMMER_HPP + +#include "utils/signals/programmer_fwd.hpp" + +#include <memory> + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace signals { + + +/// A RAII class to program signal handlers. +class programmer : noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + programmer(const int, const handler_type); + ~programmer(void); + + void unprogram(void); +}; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_PROGRAMMER_HPP) diff --git a/utils/signals/programmer_fwd.hpp b/utils/signals/programmer_fwd.hpp new file mode 100644 index 000000000000..55dfd34af2eb --- /dev/null +++ b/utils/signals/programmer_fwd.hpp @@ -0,0 +1,49 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/programmer_fwd.hpp +/// Forward declarations for utils/signals/programmer.hpp + +#if !defined(UTILS_SIGNALS_PROGRAMMER_FWD_HPP) +#define UTILS_SIGNALS_PROGRAMMER_FWD_HPP + +namespace utils { +namespace signals { + + +/// Function type for signal handlers. +typedef void (*handler_type)(const int); + + +class programmer; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_PROGRAMMER_FWD_HPP) diff --git a/utils/signals/programmer_test.cpp b/utils/signals/programmer_test.cpp new file mode 100644 index 000000000000..0e95f84974b1 --- /dev/null +++ b/utils/signals/programmer_test.cpp @@ -0,0 +1,140 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/programmer.hpp" + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <atf-c++.hpp> + +#include "utils/sanity.hpp" + +namespace signals = utils::signals; + + +namespace { + + +namespace sigchld { + + +static bool happened_1; +static bool happened_2; + + +void handler_1(const int signo) { + PRE(signo == SIGCHLD); + happened_1 = true; +} + + +void handler_2(const int signo) { + PRE(signo == SIGCHLD); + happened_2 = true; +} + + +} // namespace sigchld + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(program_unprogram); +ATF_TEST_CASE_BODY(program_unprogram) +{ + signals::programmer programmer(SIGCHLD, sigchld::handler_1); + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + + programmer.unprogram(); + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scope); +ATF_TEST_CASE_BODY(scope) +{ + { + signals::programmer programmer(SIGCHLD, sigchld::handler_1); + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + } + + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(nested); +ATF_TEST_CASE_BODY(nested) +{ + signals::programmer programmer_1(SIGCHLD, sigchld::handler_1); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + ATF_REQUIRE(!sigchld::happened_2); + + signals::programmer programmer_2(SIGCHLD, sigchld::handler_2); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); + ATF_REQUIRE(sigchld::happened_2); + + programmer_2.unprogram(); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + ATF_REQUIRE(!sigchld::happened_2); + + programmer_1.unprogram(); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); + ATF_REQUIRE(!sigchld::happened_2); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, program_unprogram); + ATF_ADD_TEST_CASE(tcs, scope); + ATF_ADD_TEST_CASE(tcs, nested); +} diff --git a/utils/signals/timer.cpp b/utils/signals/timer.cpp new file mode 100644 index 000000000000..698b9835dc10 --- /dev/null +++ b/utils/signals/timer.cpp @@ -0,0 +1,547 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/timer.hpp" + +extern "C" { +#include <sys/time.h> + +#include <signal.h> +} + +#include <cerrno> +#include <map> +#include <set> +#include <vector> + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/signals/interrupts.hpp" +#include "utils/signals/programmer.hpp" + +namespace datetime = utils::datetime; +namespace signals = utils::signals; + +using utils::none; +using utils::optional; + +namespace { + + +static void sigalrm_handler(const int); + + +/// Calls setitimer(2) with exception-based error reporting. +/// +/// This does not currently support intervals. +/// +/// \param delta The time to the first activation of the programmed timer. +/// \param old_timeval If not NULL, pointer to a timeval into which to store the +/// existing system timer. +/// +/// \throw system_error If the call to setitimer(2) fails. +static void +safe_setitimer(const datetime::delta& delta, itimerval* old_timeval) +{ + ::itimerval timeval; + timerclear(&timeval.it_interval); + timeval.it_value.tv_sec = delta.seconds; + timeval.it_value.tv_usec = delta.useconds; + + if (::setitimer(ITIMER_REAL, &timeval, old_timeval) == -1) { + const int original_errno = errno; + throw signals::system_error("Failed to program system's interval timer", + original_errno); + } +} + + +/// Deadline scheduler for all user timers on top of the unique system timer. +class global_state : utils::noncopyable { + /// Collection of active timers. + /// + /// Because this is a collection of pointers, all timers are guaranteed to + /// be unique, and we want all of these pointers to be valid. + typedef std::set< signals::timer* > timers_set; + + /// Sequence of ordered timers. + typedef std::vector< signals::timer* > timers_vector; + + /// Collection of active timestamps by their activation timestamp. + /// + /// This collection is ordered intentionally so that it can be scanned + /// sequentially to find either expired or expiring-now timers. + typedef std::map< datetime::timestamp, timers_set > timers_by_timestamp_map; + + /// The original timer before any timer was programmed. + ::itimerval _old_timeval; + + /// Programmer for the SIGALRM handler. + std::auto_ptr< signals::programmer > _sigalrm_programmer; + + /// Time of the current activation of the timer. + datetime::timestamp _timer_activation; + + /// Mapping of all active timers using their timestamp as the key. + timers_by_timestamp_map _all_timers; + + /// Adds a timer to the _all_timers map. + /// + /// \param timer The timer to add. + void + add_to_all_timers(signals::timer* timer) + { + timers_set& timers = _all_timers[timer->when()]; + INV(timers.find(timer) == timers.end()); + timers.insert(timer); + } + + /// Removes a timer from the _all_timers map. + /// + /// This ensures that empty vectors are removed from _all_timers if the + /// removal of the timer causes its bucket to be emptied. + /// + /// \param timer The timer to remove. + void + remove_from_all_timers(signals::timer* timer) + { + // We may not find the timer in _all_timers if the timer has fired, + // because fire() took it out from the map. + timers_by_timestamp_map::iterator iter = _all_timers.find( + timer->when()); + if (iter != _all_timers.end()) { + timers_set& timers = (*iter).second; + INV(timers.find(timer) != timers.end()); + timers.erase(timer); + if (timers.empty()) { + _all_timers.erase(iter); + } + } + } + + /// Calculates all timers to execute at this timestamp. + /// + /// \param now The current timestamp. + /// + /// \post _all_timers is updated to contain only the timers that are + /// strictly in the future. + /// + /// \return A sequence of valid timers that need to be invoked in the order + /// of activation. These are all previously registered timers with + /// activations in the past. + timers_vector + compute_timers_to_run_and_prune_old( + const datetime::timestamp& now, + const signals::interrupts_inhibiter& /* inhibiter */) + { + timers_vector to_run; + + timers_by_timestamp_map::iterator iter = _all_timers.begin(); + while (iter != _all_timers.end() && (*iter).first <= now) { + const timers_set& timers = (*iter).second; + to_run.insert(to_run.end(), timers.begin(), timers.end()); + + // Remove expired entries here so that we can always assume that + // the first entry in all_timers corresponds to the next + // activation. + const timers_by_timestamp_map::iterator previous_iter = iter; + ++iter; + _all_timers.erase(previous_iter); + } + + return to_run; + } + + /// Adjusts the global system timer to point to the next activation. + /// + /// \param now The current timestamp. + /// + /// \throw system_error If the programming fails. + void + reprogram_system_timer( + const datetime::timestamp& now, + const signals::interrupts_inhibiter& /* inhibiter */) + { + if (_all_timers.empty()) { + // Nothing to do. We can reach this case if all the existing timers + // are in the past and they all fired. Just ignore the request and + // leave the global timer as is. + return; + } + + // While fire() prunes old entries from the list of timers, it is + // possible for this routine to run with "expired" timers (i.e. timers + // whose deadline lies in the past but that have not yet fired for + // whatever reason that is out of our control) in the list. We have to + // iterate until we find the next activation instead of assuming that + // the first entry represents the desired value. + timers_by_timestamp_map::const_iterator iter = _all_timers.begin(); + PRE(!(*iter).second.empty()); + datetime::timestamp next = (*iter).first; + while (next < now) { + ++iter; + if (iter == _all_timers.end()) { + // Nothing to do. We can reach this case if all the existing + // timers are in the past but they have not yet fired. + return; + } + PRE(!(*iter).second.empty()); + next = (*iter).first; + } + + if (next < _timer_activation || now > _timer_activation) { + INV(next >= now); + const datetime::delta delta = next - now; + LD(F("Reprogramming timer; firing on %s; now is %s") % next % now); + safe_setitimer(delta, NULL); + _timer_activation = next; + } + } + +public: + /// Programs the first timer. + /// + /// The programming of the first timer involves setting up the SIGALRM + /// handler and installing a timer handler for the first time, which in turn + /// involves keeping track of the old handlers so that we can restore them. + /// + /// \param timer The timer being programmed. + /// \param now The current timestamp. + /// + /// \throw system_error If the programming fails. + global_state(signals::timer* timer, const datetime::timestamp& now) : + _timer_activation(timer->when()) + { + PRE(now < timer->when()); + + signals::interrupts_inhibiter inhibiter; + + const datetime::delta delta = timer->when() - now; + LD(F("Installing first timer; firing on %s; now is %s") % + timer->when() % now); + + _sigalrm_programmer.reset( + new signals::programmer(SIGALRM, sigalrm_handler)); + try { + safe_setitimer(delta, &_old_timeval); + _timer_activation = timer->when(); + add_to_all_timers(timer); + } catch (...) { + _sigalrm_programmer.reset(NULL); + throw; + } + } + + /// Unprograms all timers. + /// + /// This clears the global system timer and unsets the SIGALRM handler. + ~global_state(void) + { + signals::interrupts_inhibiter inhibiter; + + LD("Unprogramming all timers"); + + if (::setitimer(ITIMER_REAL, &_old_timeval, NULL) == -1) { + UNREACHABLE_MSG("Failed to restore original timer"); + } + + _sigalrm_programmer->unprogram(); + _sigalrm_programmer.reset(NULL); + } + + /// Programs a new timer, possibly adjusting the global system timer. + /// + /// Programming any timer other than the first one only involves reloading + /// the existing timer, not backing up the previous handler nor installing a + /// handler for SIGALRM. + /// + /// \param timer The timer being programmed. + /// \param now The current timestamp. + /// + /// \throw system_error If the programming fails. + void + program_new(signals::timer* timer, const datetime::timestamp& now) + { + signals::interrupts_inhibiter inhibiter; + + add_to_all_timers(timer); + reprogram_system_timer(now, inhibiter); + } + + /// Unprograms a timer. + /// + /// This removes the timer from the global state and reprograms the global + /// system timer if necessary. + /// + /// \param timer The timer to unprogram. + /// + /// \return True if the system interval timer has been reprogrammed to + /// another future timer; false if there are no more active timers. + bool + unprogram(signals::timer* timer) + { + signals::interrupts_inhibiter inhibiter; + + LD(F("Unprogramming timer; previously firing on %s") % timer->when()); + + remove_from_all_timers(timer); + if (_all_timers.empty()) { + return false; + } else { + reprogram_system_timer(datetime::timestamp::now(), inhibiter); + return true; + } + } + + /// Executes active timers. + /// + /// Active timers are all those that fire on or before 'now'. + /// + /// \param now The current time. + void + fire(const datetime::timestamp& now) + { + timers_vector to_run; + { + signals::interrupts_inhibiter inhibiter; + to_run = compute_timers_to_run_and_prune_old(now, inhibiter); + reprogram_system_timer(now, inhibiter); + } + + for (timers_vector::iterator iter = to_run.begin(); + iter != to_run.end(); ++iter) { + signals::detail::invoke_do_fired(*iter); + } + } +}; + + +/// Unique instance of the global state. +static std::auto_ptr< global_state > globals; + + +/// SIGALRM handler for the timer implementation. +/// +/// \param signo The signal received; must be SIGALRM. +static void +sigalrm_handler(const int signo) +{ + PRE(signo == SIGALRM); + globals->fire(datetime::timestamp::now()); +} + + +} // anonymous namespace + + +/// Indirection to invoke the private do_fired() method of a timer. +/// +/// \param timer The timer on which to run do_fired(). +void +utils::signals::detail::invoke_do_fired(timer* timer) +{ + timer->do_fired(); +} + + +/// Internal implementation for the timer. +/// +/// We assume that there is a 1-1 mapping between timer objects and impl +/// objects. If this assumption breaks, then the rest of the code in this +/// module breaks as well because we use pointers to the parent timer as the +/// identifier of the timer. +struct utils::signals::timer::impl : utils::noncopyable { + /// Timestamp when this timer is expected to fire. + /// + /// Note that the timer might be processed after this timestamp, so users of + /// this field need to check for timers that fire on or before the + /// activation time. + datetime::timestamp when; + + /// True until unprogram() is called. + bool programmed; + + /// Whether this timer has fired already or not. + /// + /// This is updated from an interrupt context, hence why it is marked + /// volatile. + volatile bool fired; + + /// Constructor. + /// + /// \param when_ Timestamp when this timer is expected to fire. + impl(const datetime::timestamp& when_) : + when(when_), programmed(true), fired(false) + { + } + + /// Destructor. + ~impl(void) { + } +}; + + +/// Constructor; programs a run-once timer. +/// +/// This programs the global timer and signal handler if this is the first timer +/// being installed. Otherwise, reprograms the global timer if this timer +/// expires earlier than all other active timers. +/// +/// \param delta The time until the timer fires. +signals::timer::timer(const datetime::delta& delta) +{ + signals::interrupts_inhibiter inhibiter; + + const datetime::timestamp now = datetime::timestamp::now(); + _pimpl.reset(new impl(now + delta)); + if (globals.get() == NULL) { + globals.reset(new global_state(this, now)); + } else { + globals->program_new(this, now); + } +} + + +/// Destructor; unprograms the timer if still programmed. +/// +/// Given that this is a destructor and it can't report errors back to the +/// caller, the caller must attempt to call unprogram() on its own. This is +/// extremely important because, otherwise, expired timers will never run! +signals::timer::~timer(void) +{ + signals::interrupts_inhibiter inhibiter; + + if (_pimpl->programmed) { + LW("Auto-destroying still-programmed signals::timer object"); + try { + unprogram(); + } catch (const system_error& e) { + UNREACHABLE; + } + } + + if (!_pimpl->fired) { + const datetime::timestamp now = datetime::timestamp::now(); + if (now > _pimpl->when) { + LW("Expired timer never fired; the code never called unprogram()!"); + } + } +} + + +/// Returns the time of the timer activation. +/// +/// \return A timestamp that has no relation to the current time (i.e. can be in +/// the future or in the past) nor the timer's activation status. +const datetime::timestamp& +signals::timer::when(void) const +{ + return _pimpl->when; +} + + +/// Callback for the SIGALRM handler when this timer expires. +/// +/// \warning This is executed from a signal handler context without signals +/// inhibited. See signal(7) for acceptable system calls. +void +signals::timer::do_fired(void) +{ + PRE(!_pimpl->fired); + _pimpl->fired = true; + callback(); +} + + +/// User-provided callback to run when the timer expires. +/// +/// The default callback does nothing. We record the activation of the timer +/// separately, which may be appropriate in the majority of the cases. +/// +/// \warning This is executed from a signal handler context without signals +/// inhibited. See signal(7) for acceptable system calls. +void +signals::timer::callback(void) +{ + // Intentionally left blank. +} + + +/// Checks whether the timer has fired already or not. +/// +/// \return Returns true if the timer has fired. +bool +signals::timer::fired(void) const +{ + return _pimpl->fired; +} + + +/// Unprograms the timer. +/// +/// \pre The timer is programmed (i.e. this can only be called once). +/// +/// \post If the timer never fired asynchronously because the signal delivery +/// did not arrive on time, make sure we invoke the timer's callback here. +/// +/// \throw system_error If unprogramming the timer failed. +void +signals::timer::unprogram(void) +{ + signals::interrupts_inhibiter inhibiter; + + if (!_pimpl->programmed) { + // We cannot assert that the timer is not programmed because it might + // have been unprogrammed asynchronously between the time we called + // unprogram() and the time we reach this. Simply return in this case. + LD("Called unprogram on already-unprogrammed timer; possibly just " + "a race"); + return; + } + + if (!globals->unprogram(this)) { + globals.reset(NULL); + } + _pimpl->programmed = false; + + // Handle the case where the timer has expired before we ever got its + // corresponding signal. Do so by invoking its callback now. + if (!_pimpl->fired) { + const datetime::timestamp now = datetime::timestamp::now(); + if (now > _pimpl->when) { + LW(F("Firing expired timer on destruction (was to fire on %s)") % + _pimpl->when); + do_fired(); + } + } +} diff --git a/utils/signals/timer.hpp b/utils/signals/timer.hpp new file mode 100644 index 000000000000..1174effe2b48 --- /dev/null +++ b/utils/signals/timer.hpp @@ -0,0 +1,86 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/timer.hpp +/// Multiprogrammed support for timers. +/// +/// The timer module and class implement a mechanism to program multiple timers +/// concurrently by using a deadline scheduler and leveraging the "single timer" +/// features of the underlying operating system. + +#if !defined(UTILS_SIGNALS_TIMER_HPP) +#define UTILS_SIGNALS_TIMER_HPP + +#include "utils/signals/timer_fwd.hpp" + +#include <memory> + +#include "utils/datetime_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace utils { +namespace signals { + + +namespace detail { +void invoke_do_fired(timer*); +} // namespace detail + + +/// Individual timer. +/// +/// Asynchronously executes its callback() method, which can be overridden by +/// subclasses, when the timeout given at construction expires. +class timer : noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + + friend void detail::invoke_do_fired(timer*); + void do_fired(void); + +protected: + virtual void callback(void); + +public: + timer(const utils::datetime::delta&); + virtual ~timer(void); + + const utils::datetime::timestamp& when(void) const; + + bool fired(void) const; + + void unprogram(void); +}; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_TIMER_HPP) diff --git a/utils/signals/timer_fwd.hpp b/utils/signals/timer_fwd.hpp new file mode 100644 index 000000000000..a3cf3e205d70 --- /dev/null +++ b/utils/signals/timer_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/signals/timer_fwd.hpp +/// Forward declarations for utils/signals/timer.hpp + +#if !defined(UTILS_SIGNALS_TIMER_FWD_HPP) +#define UTILS_SIGNALS_TIMER_FWD_HPP + +namespace utils { +namespace signals { + + +class timer; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_TIMER_FWD_HPP) diff --git a/utils/signals/timer_test.cpp b/utils/signals/timer_test.cpp new file mode 100644 index 000000000000..61e9cac6b088 --- /dev/null +++ b/utils/signals/timer_test.cpp @@ -0,0 +1,426 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/signals/timer.hpp" + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <cstddef> +#include <iostream> +#include <vector> + +#include <atf-c++.hpp> + +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/signals/interrupts.hpp" +#include "utils/signals/programmer.hpp" + +namespace datetime = utils::datetime; +namespace signals = utils::signals; + + +namespace { + + +/// A timer that inserts an element into a vector on activation. +class delayed_inserter : public signals::timer { + /// Vector into which to insert the element. + std::vector< int >& _destination; + + /// Element to insert into _destination on activation. + const int _item; + + /// Timer activation callback. + void + callback(void) + { + signals::interrupts_inhibiter inhibiter; + _destination.push_back(_item); + } + +public: + /// Constructor. + /// + /// \param delta Time to the timer activation. + /// \param destination Vector into which to insert the element. + /// \param item Element to insert into destination on activation. + delayed_inserter(const datetime::delta& delta, + std::vector< int >& destination, const int item) : + signals::timer(delta), _destination(destination), _item(item) + { + } +}; + + +/// Signal handler that does nothing. +static void +null_handler(const int /* signo */) +{ +} + + +/// Waits for the activation of all given timers. +/// +/// \param timers Pointers to all the timers to wait for. +static void +wait_timers(const std::vector< signals::timer* >& timers) +{ + std::size_t n_fired, old_n_fired = 0; + do { + n_fired = 0; + for (std::vector< signals::timer* >::const_iterator + iter = timers.begin(); iter != timers.end(); ++iter) { + const signals::timer* timer = *iter; + if (timer->fired()) + ++n_fired; + } + if (old_n_fired < n_fired) { + std::cout << "Waiting; " << n_fired << " timers fired so far\n"; + old_n_fired = n_fired; + } + ::usleep(100); + } while (n_fired < timers.size()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE(program_seconds); +ATF_TEST_CASE_HEAD(program_seconds) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(program_seconds) +{ + signals::timer timer(datetime::delta(1, 0)); + ATF_REQUIRE(!timer.fired()); + while (!timer.fired()) + ::usleep(1000); +} + + +ATF_TEST_CASE(program_useconds); +ATF_TEST_CASE_HEAD(program_useconds) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(program_useconds) +{ + signals::timer timer(datetime::delta(0, 500000)); + ATF_REQUIRE(!timer.fired()); + while (!timer.fired()) + ::usleep(1000); +} + + +ATF_TEST_CASE(multiprogram_ordered); +ATF_TEST_CASE_HEAD(multiprogram_ordered) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_ordered) +{ + static const std::size_t n_timers = 100; + + std::vector< signals::timer* > timers; + std::vector< int > items, exp_items; + + const int initial_delay_ms = 1000000; + for (std::size_t i = 0; i < n_timers; ++i) { + exp_items.push_back(i); + + timers.push_back(new delayed_inserter( + datetime::delta(0, initial_delay_ms + (i + 1) * 10000), + items, i)); + ATF_REQUIRE(!timers[i]->fired()); + } + + wait_timers(timers); + + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(multiprogram_reorder_next_activations); +ATF_TEST_CASE_HEAD(multiprogram_reorder_next_activations) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_reorder_next_activations) +{ + std::vector< signals::timer* > timers; + std::vector< int > items; + + // First timer with an activation in the future. + timers.push_back(new delayed_inserter( + datetime::delta(0, 100000), items, 1)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation earlier than the previous one. + timers.push_back(new delayed_inserter( + datetime::delta(0, 50000), items, 2)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation later than all others. + timers.push_back(new delayed_inserter( + datetime::delta(0, 200000), items, 3)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation in between. + timers.push_back(new delayed_inserter( + datetime::delta(0, 150000), items, 4)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + wait_timers(timers); + + std::vector< int > exp_items; + exp_items.push_back(2); + exp_items.push_back(1); + exp_items.push_back(4); + exp_items.push_back(3); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(multiprogram_and_cancel_some); +ATF_TEST_CASE_HEAD(multiprogram_and_cancel_some) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_and_cancel_some) +{ + std::vector< signals::timer* > timers; + std::vector< int > items; + + // First timer with an activation in the future. + timers.push_back(new delayed_inserter( + datetime::delta(0, 100000), items, 1)); + + // Timer with an activation earlier than the previous one. + timers.push_back(new delayed_inserter( + datetime::delta(0, 50000), items, 2)); + + // Timer with an activation later than all others. + timers.push_back(new delayed_inserter( + datetime::delta(0, 200000), items, 3)); + + // Timer with an activation in between. + timers.push_back(new delayed_inserter( + datetime::delta(0, 150000), items, 4)); + + // Cancel the first timer to reprogram next activation. + timers[1]->unprogram(); delete timers[1]; timers.erase(timers.begin() + 1); + + // Cancel another timer without reprogramming next activation. + timers[2]->unprogram(); delete timers[2]; timers.erase(timers.begin() + 2); + + wait_timers(timers); + + std::vector< int > exp_items; + exp_items.push_back(1); + exp_items.push_back(3); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(multiprogram_and_expire_before_activations); +ATF_TEST_CASE_HEAD(multiprogram_and_expire_before_activations) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_and_expire_before_activations) +{ + std::vector< signals::timer* > timers; + std::vector< int > items; + + { + signals::interrupts_inhibiter inhibiter; + + // First timer with an activation in the future. + timers.push_back(new delayed_inserter( + datetime::delta(0, 100000), items, 1)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation earlier than the previous one. + timers.push_back(new delayed_inserter( + datetime::delta(0, 50000), items, 2)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + ::sleep(1); + + // Timer with an activation later than all others. + timers.push_back(new delayed_inserter( + datetime::delta(0, 200000), items, 3)); + + ::sleep(1); + } + + wait_timers(timers); + + std::vector< int > exp_items; + exp_items.push_back(2); + exp_items.push_back(1); + exp_items.push_back(3); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(expire_before_firing); +ATF_TEST_CASE_HEAD(expire_before_firing) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(expire_before_firing) +{ + std::vector< int > items; + + // The code below causes a signal to go pending. Make sure we ignore it + // when we unblock signals. + signals::programmer sigalrm(SIGALRM, null_handler); + + { + signals::interrupts_inhibiter inhibiter; + + delayed_inserter* timer = new delayed_inserter( + datetime::delta(0, 1000), items, 1234); + ::sleep(1); + // Interrupts are inhibited so we never got a chance to execute the + // timer before it was destroyed. However, the handler should run + // regardless at some point, possibly during deletion. + timer->unprogram(); + delete timer; + } + + std::vector< int > exp_items; + exp_items.push_back(1234); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(reprogram_from_scratch); +ATF_TEST_CASE_HEAD(reprogram_from_scratch) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(reprogram_from_scratch) +{ + std::vector< int > items; + + delayed_inserter* timer1 = new delayed_inserter( + datetime::delta(0, 100000), items, 1); + timer1->unprogram(); delete timer1; + + // All constructed timers are now dead, so the interval timer should have + // been reprogrammed. Let's start over. + + delayed_inserter* timer2 = new delayed_inserter( + datetime::delta(0, 200000), items, 2); + while (!timer2->fired()) + ::usleep(1000); + timer2->unprogram(); delete timer2; + + std::vector< int > exp_items; + exp_items.push_back(2); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(unprogram); +ATF_TEST_CASE_HEAD(unprogram) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(unprogram) +{ + signals::timer timer(datetime::delta(0, 500000)); + timer.unprogram(); + usleep(500000); + ATF_REQUIRE(!timer.fired()); +} + + +ATF_TEST_CASE(infinitesimal); +ATF_TEST_CASE_HEAD(infinitesimal) +{ + set_md_var("descr", "Ensure that the ordering in which the signal, the " + "timer and the global state are programmed is correct; do so " + "by setting an extremely small delay for the timer hoping that " + "it can trigger such conditions"); + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(infinitesimal) +{ + const std::size_t rounds = 100; + const std::size_t exp_good = 90; + + std::size_t good = 0; + for (std::size_t i = 0; i < rounds; i++) { + signals::timer timer(datetime::delta(0, 1)); + + // From the setitimer(2) documentation: + // + // Time values smaller than the resolution of the system clock are + // rounded up to this resolution (typically 10 milliseconds). + // + // We don't know what this resolution is but we must wait for longer + // than we programmed; do a rough guess and hope it is good. This may + // be obviously wrong and thus lead to mysterious test failures in some + // systems, hence why we only expect a percentage of successes below. + // Still, we can fail... + ::usleep(1000); + + if (timer.fired()) + ++good; + timer.unprogram(); + } + std::cout << F("Ran %s tests, %s passed; threshold is %s\n") + % rounds % good % exp_good; + ATF_REQUIRE(good >= exp_good); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, program_seconds); + ATF_ADD_TEST_CASE(tcs, program_useconds); + ATF_ADD_TEST_CASE(tcs, multiprogram_ordered); + ATF_ADD_TEST_CASE(tcs, multiprogram_reorder_next_activations); + ATF_ADD_TEST_CASE(tcs, multiprogram_and_cancel_some); + ATF_ADD_TEST_CASE(tcs, multiprogram_and_expire_before_activations); + ATF_ADD_TEST_CASE(tcs, expire_before_firing); + ATF_ADD_TEST_CASE(tcs, reprogram_from_scratch); + ATF_ADD_TEST_CASE(tcs, unprogram); + ATF_ADD_TEST_CASE(tcs, infinitesimal); +} diff --git a/utils/sqlite/Kyuafile b/utils/sqlite/Kyuafile new file mode 100644 index 000000000000..47a8b95dac92 --- /dev/null +++ b/utils/sqlite/Kyuafile @@ -0,0 +1,9 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="c_gate_test"} +atf_test_program{name="database_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="statement_test"} +atf_test_program{name="transaction_test"} diff --git a/utils/sqlite/Makefile.am.inc b/utils/sqlite/Makefile.am.inc new file mode 100644 index 000000000000..6064a641c14f --- /dev/null +++ b/utils/sqlite/Makefile.am.inc @@ -0,0 +1,82 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +UTILS_CFLAGS += $(SQLITE3_CFLAGS) +UTILS_LIBS += $(SQLITE3_LIBS) + +libutils_a_CPPFLAGS += $(SQLITE3_CFLAGS) +libutils_a_SOURCES += utils/sqlite/c_gate.cpp +libutils_a_SOURCES += utils/sqlite/c_gate.hpp +libutils_a_SOURCES += utils/sqlite/c_gate_fwd.hpp +libutils_a_SOURCES += utils/sqlite/database.cpp +libutils_a_SOURCES += utils/sqlite/database.hpp +libutils_a_SOURCES += utils/sqlite/database_fwd.hpp +libutils_a_SOURCES += utils/sqlite/exceptions.cpp +libutils_a_SOURCES += utils/sqlite/exceptions.hpp +libutils_a_SOURCES += utils/sqlite/statement.cpp +libutils_a_SOURCES += utils/sqlite/statement.hpp +libutils_a_SOURCES += utils/sqlite/statement_fwd.hpp +libutils_a_SOURCES += utils/sqlite/statement.ipp +libutils_a_SOURCES += utils/sqlite/transaction.cpp +libutils_a_SOURCES += utils/sqlite/transaction.hpp +libutils_a_SOURCES += utils/sqlite/transaction_fwd.hpp + +if WITH_ATF +tests_utils_sqlitedir = $(pkgtestsdir)/utils/sqlite + +tests_utils_sqlite_DATA = utils/sqlite/Kyuafile +EXTRA_DIST += $(tests_utils_sqlite_DATA) + +tests_utils_sqlite_PROGRAMS = utils/sqlite/c_gate_test +utils_sqlite_c_gate_test_SOURCES = utils/sqlite/c_gate_test.cpp \ + utils/sqlite/test_utils.hpp +utils_sqlite_c_gate_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_c_gate_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/database_test +utils_sqlite_database_test_SOURCES = utils/sqlite/database_test.cpp \ + utils/sqlite/test_utils.hpp +utils_sqlite_database_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_database_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/exceptions_test +utils_sqlite_exceptions_test_SOURCES = utils/sqlite/exceptions_test.cpp +utils_sqlite_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/statement_test +utils_sqlite_statement_test_SOURCES = utils/sqlite/statement_test.cpp \ + utils/sqlite/test_utils.hpp +utils_sqlite_statement_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_statement_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/transaction_test +utils_sqlite_transaction_test_SOURCES = utils/sqlite/transaction_test.cpp +utils_sqlite_transaction_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_transaction_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/sqlite/c_gate.cpp b/utils/sqlite/c_gate.cpp new file mode 100644 index 000000000000..e89ac5332ea0 --- /dev/null +++ b/utils/sqlite/c_gate.cpp @@ -0,0 +1,83 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/c_gate.hpp" + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" + +namespace sqlite = utils::sqlite; + +using utils::none; + + +/// Creates a new gateway to an existing C++ SQLite database. +/// +/// \param database_ The database to connect to. This object must remain alive +/// while the newly-constructed database_c_gate is alive. +sqlite::database_c_gate::database_c_gate(database& database_) : + _database(database_) +{ +} + + +/// Destructor. +/// +/// Destroying this object has no implications on the life cycle of the SQLite +/// database. Only the corresponding database object controls when the SQLite 3 +/// database is closed. +sqlite::database_c_gate::~database_c_gate(void) +{ +} + + +/// Creates a C++ database for a C SQLite 3 database. +/// +/// \warning The created database object does NOT own the C database. You must +/// take care to properly destroy the input sqlite3 when you are done with it to +/// not leak resources. +/// +/// \param raw_database The raw database to wrap temporarily. +/// +/// \return The wrapped database without strong ownership on the input database. +sqlite::database +sqlite::database_c_gate::connect(::sqlite3* raw_database) +{ + return database(none, static_cast< void* >(raw_database), false); +} + + +/// Returns the C native SQLite 3 database. +/// +/// \return A native sqlite3 object holding the SQLite 3 C API database. +::sqlite3* +sqlite::database_c_gate::c_database(void) +{ + return static_cast< ::sqlite3* >(_database.raw_database()); +} diff --git a/utils/sqlite/c_gate.hpp b/utils/sqlite/c_gate.hpp new file mode 100644 index 000000000000..0ca9d79c4815 --- /dev/null +++ b/utils/sqlite/c_gate.hpp @@ -0,0 +1,74 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file c_gate.hpp +/// Provides direct access to the C state of the SQLite wrappers. + +#if !defined(UTILS_SQLITE_C_GATE_HPP) +#define UTILS_SQLITE_C_GATE_HPP + +#include "utils/sqlite/c_gate_fwd.hpp" + +extern "C" { +#include <sqlite3.h> +} + +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Gateway to the raw C database of SQLite 3. +/// +/// This class provides a mechanism to muck with the internals of the database +/// wrapper class. +/// +/// \warning The use of this class is discouraged. By using this class, you are +/// entering the world of unsafety. Anything you do through the objects exposed +/// through this class will not be controlled by RAII patterns not validated in +/// any other way, so you can end up corrupting the SQLite 3 state and later get +/// crashes on otherwise perfectly-valid C++ code. +class database_c_gate { + /// The C++ database that this class wraps. + database& _database; + +public: + database_c_gate(database&); + ~database_c_gate(void); + + static database connect(::sqlite3*); + + ::sqlite3* c_database(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_C_GATE_HPP) diff --git a/utils/sqlite/c_gate_fwd.hpp b/utils/sqlite/c_gate_fwd.hpp new file mode 100644 index 000000000000..771efeeff463 --- /dev/null +++ b/utils/sqlite/c_gate_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/c_gate_fwd.hpp +/// Forward declarations for utils/sqlite/c_gate.hpp + +#if !defined(UTILS_SQLITE_C_GATE_FWD_HPP) +#define UTILS_SQLITE_C_GATE_FWD_HPP + +namespace utils { +namespace sqlite { + + +class database_c_gate; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_C_GATE_FWD_HPP) diff --git a/utils/sqlite/c_gate_test.cpp b/utils/sqlite/c_gate_test.cpp new file mode 100644 index 000000000000..edf46f76c902 --- /dev/null +++ b/utils/sqlite/c_gate_test.cpp @@ -0,0 +1,96 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/c_gate.hpp" + +#include <atf-c++.hpp> + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/test_utils.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE_WITHOUT_HEAD(connect); +ATF_TEST_CASE_BODY(connect) +{ + ::sqlite3* raw_db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2(":memory:", &raw_db, + SQLITE_OPEN_READWRITE, NULL)); + { + sqlite::database database = sqlite::database_c_gate::connect(raw_db); + create_test_table(raw(database)); + } + // If the wrapper object has closed the SQLite 3 database, we will misbehave + // here either by crashing or not finding our test table. + verify_test_table(raw_db); + ::sqlite3_close(raw_db); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(c_database); +ATF_TEST_CASE_BODY(c_database) +{ + sqlite::database db = sqlite::database::in_memory(); + create_test_table(raw(db)); + { + sqlite::database_c_gate gate(db); + ::sqlite3* raw_db = gate.c_database(); + verify_test_table(raw_db); + } +} + + +ATF_TEST_CASE(database__db_filename); +ATF_TEST_CASE_HEAD(database__db_filename) +{ + set_md_var("descr", "The current implementation of db_filename() has no " + "means to access the filename of a database connected to a raw " + "sqlite3 object"); +} +ATF_TEST_CASE_BODY(database__db_filename) +{ + ::sqlite3* raw_db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2( + "test.db", &raw_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL)); + + sqlite::database database = sqlite::database_c_gate::connect(raw_db); + ATF_REQUIRE(!database.db_filename()); + ::sqlite3_close(raw_db); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, c_database); + ATF_ADD_TEST_CASE(tcs, connect); + ATF_ADD_TEST_CASE(tcs, database__db_filename); +} diff --git a/utils/sqlite/database.cpp b/utils/sqlite/database.cpp new file mode 100644 index 000000000000..41935c3b017d --- /dev/null +++ b/utils/sqlite/database.cpp @@ -0,0 +1,328 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/database.hpp" + +extern "C" { +#include <sqlite3.h> +} + +#include <cstring> +#include <stdexcept> + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/transaction.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; +using utils::optional; + + +/// Internal implementation for sqlite::database. +struct utils::sqlite::database::impl : utils::noncopyable { + /// Path to the database as seen at construction time. + optional< fs::path > db_filename; + + /// The SQLite 3 internal database. + ::sqlite3* db; + + /// Whether we own the database or not (to decide if we close it). + bool owned; + + /// Constructor. + /// + /// \param db_filename_ The path to the database as seen at construction + /// time, if any, or none for in-memory databases. We should use + /// sqlite3_db_filename instead, but this function appeared in 3.7.10 + /// and Ubuntu 12.04 LTS (which we support for Travis CI builds as of + /// 2015-07-07) ships with 3.7.9. + /// \param db_ The SQLite internal database. + /// \param owned_ Whether this object owns the db_ object or not. If it + /// does, the internal db_ will be released during destruction. + impl(optional< fs::path > db_filename_, ::sqlite3* db_, const bool owned_) : + db_filename(db_filename_), db(db_), owned(owned_) + { + } + + /// Destructor. + /// + /// It is important to keep this as part of the 'impl' class instead of the + /// container class. The 'impl' class is destroyed exactly once (because it + /// is managed by a shared_ptr) and thus releasing the resources here is + /// OK. However, the container class is potentially released many times, + /// which means that we would be double-freeing the internal object and + /// reusing invalid data. + ~impl(void) + { + if (owned && db != NULL) + close(); + } + + /// Exception-safe version of sqlite3_open_v2. + /// + /// \param file The path to the database file to be opened. + /// \param flags The flags to be passed to the open routine. + /// + /// \return The opened database. + /// + /// \throw std::bad_alloc If there is not enough memory to open the + /// database. + /// \throw api_error If there is any problem opening the database. + static ::sqlite3* + safe_open(const char* file, const int flags) + { + ::sqlite3* db; + const int error = ::sqlite3_open_v2(file, &db, flags, NULL); + if (error != SQLITE_OK) { + if (db == NULL) + throw std::bad_alloc(); + else { + sqlite::database error_db(utils::make_optional(fs::path(file)), + db, true); + throw sqlite::api_error::from_database(error_db, + "sqlite3_open_v2"); + } + } + INV(db != NULL); + return db; + } + + /// Shared code for the public close() method. + void + close(void) + { + PRE(db != NULL); + int error = ::sqlite3_close(db); + // For now, let's consider a return of SQLITE_BUSY an error. We should + // not be trying to close a busy database in our code. Maybe revisit + // this later to raise busy errors as exceptions. + PRE(error == SQLITE_OK); + db = NULL; + } +}; + + +/// Initializes the SQLite database. +/// +/// You must share the same database object alongside the lifetime of your +/// SQLite session. As soon as the object is destroyed, the session is +/// terminated. +/// +/// \param db_filename_ The path to the database as seen at construction +/// time, if any, or none for in-memory databases. +/// \param db_ Raw pointer to the C SQLite 3 object. +/// \param owned_ Whether this instance will own the pointer or not. +sqlite::database::database( + const utils::optional< utils::fs::path >& db_filename_, void* db_, + const bool owned_) : + _pimpl(new impl(db_filename_, static_cast< ::sqlite3* >(db_), owned_)) +{ +} + + +/// Destructor for the SQLite 3 database. +/// +/// Closes the session unless it has already been closed by calling the +/// close() method. It is recommended to explicitly close the session in the +/// code. +sqlite::database::~database(void) +{ +} + + +/// Opens a memory-based temporary SQLite database. +/// +/// \return An in-memory database instance. +/// +/// \throw std::bad_alloc If there is not enough memory to open the database. +/// \throw api_error If there is any problem opening the database. +sqlite::database +sqlite::database::in_memory(void) +{ + return database(none, impl::safe_open(":memory:", SQLITE_OPEN_READWRITE), + true); +} + + +/// Opens a named on-disk SQLite database. +/// +/// \param file The path to the database file to be opened. This does not +/// accept the values "" and ":memory:"; use temporary() and in_memory() +/// instead. +/// \param open_flags The flags to be passed to the open routine. +/// +/// \return A file-backed database instance. +/// +/// \throw std::bad_alloc If there is not enough memory to open the database. +/// \throw api_error If there is any problem opening the database. +sqlite::database +sqlite::database::open(const fs::path& file, int open_flags) +{ + PRE_MSG(!file.str().empty(), "Use database::temporary() instead"); + PRE_MSG(file.str() != ":memory:", "Use database::in_memory() instead"); + + int flags = 0; + if (open_flags & open_readonly) { + flags |= SQLITE_OPEN_READONLY; + open_flags &= ~open_readonly; + } + if (open_flags & open_readwrite) { + flags |= SQLITE_OPEN_READWRITE; + open_flags &= ~open_readwrite; + } + if (open_flags & open_create) { + flags |= SQLITE_OPEN_CREATE; + open_flags &= ~open_create; + } + PRE(open_flags == 0); + + return database(utils::make_optional(file), + impl::safe_open(file.c_str(), flags), true); +} + + +/// Opens an unnamed on-disk SQLite database. +/// +/// \return A file-backed database instance. +/// +/// \throw std::bad_alloc If there is not enough memory to open the database. +/// \throw api_error If there is any problem opening the database. +sqlite::database +sqlite::database::temporary(void) +{ + return database(none, impl::safe_open("", SQLITE_OPEN_READWRITE), true); +} + + +/// Gets the internal sqlite3 object. +/// +/// \return The raw SQLite 3 database. This is returned as a void pointer to +/// prevent including the sqlite3.h header file from our public interface. The +/// only way to call this method is by using the c_gate module, and c_gate takes +/// care of casting this object to the appropriate type. +void* +sqlite::database::raw_database(void) +{ + return _pimpl->db; +} + + +/// Terminates the connection to the database. +/// +/// It is recommended to call this instead of relying on the destructor to do +/// the cleanup, but it is not a requirement to use close(). +/// +/// \pre close() has not yet been called. +void +sqlite::database::close(void) +{ + _pimpl->close(); +} + + +/// Returns the path to the connected database. +/// +/// It is OK to call this function on a live database object, even after close() +/// has been called. The returned value is consistent at all times. +/// +/// \return The path to the file that matches the connected database or none if +/// the connection points to a transient database. +const optional< fs::path >& +sqlite::database::db_filename(void) const +{ + return _pimpl->db_filename; +} + + +/// Executes an arbitrary SQL string. +/// +/// As the documentation explains, this is unsafe. The code should really be +/// preparing statements and executing them step by step. However, it is +/// perfectly fine to use this function for, e.g. the initial creation of +/// tables in a database and in tests. +/// +/// \param sql The SQL commands to be executed. +/// +/// \throw api_error If there is any problem while processing the SQL. +void +sqlite::database::exec(const std::string& sql) +{ + const int error = ::sqlite3_exec(_pimpl->db, sql.c_str(), NULL, NULL, NULL); + if (error != SQLITE_OK) + throw api_error::from_database(*this, "sqlite3_exec"); +} + + +/// Opens a new transaction. +/// +/// \return An object representing the state of the transaction. +/// +/// \throw api_error If there is any problem while opening the transaction. +sqlite::transaction +sqlite::database::begin_transaction(void) +{ + exec("BEGIN TRANSACTION"); + return transaction(*this); +} + + +/// Prepares a new statement. +/// +/// \param sql The SQL statement to prepare. +/// +/// \return The prepared statement. +sqlite::statement +sqlite::database::create_statement(const std::string& sql) +{ + LD(F("Creating statement: %s") % sql); + sqlite3_stmt* stmt; + const int error = ::sqlite3_prepare_v2(_pimpl->db, sql.c_str(), + sql.length() + 1, &stmt, NULL); + if (error != SQLITE_OK) + throw api_error::from_database(*this, "sqlite3_prepare_v2"); + return statement(*this, static_cast< void* >(stmt)); +} + + +/// Returns the row identifier of the last insert. +/// +/// \return A row identifier. +int64_t +sqlite::database::last_insert_rowid(void) +{ + return ::sqlite3_last_insert_rowid(_pimpl->db); +} diff --git a/utils/sqlite/database.hpp b/utils/sqlite/database.hpp new file mode 100644 index 000000000000..ca91a6a360c6 --- /dev/null +++ b/utils/sqlite/database.hpp @@ -0,0 +1,111 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/database.hpp +/// Wrapper classes and utilities for the SQLite database state. +/// +/// This module contains thin RAII wrappers around the SQLite 3 structures +/// representing the database, and lightweight. + +#if !defined(UTILS_SQLITE_DATABASE_HPP) +#define UTILS_SQLITE_DATABASE_HPP + +#include "utils/sqlite/database_fwd.hpp" + +extern "C" { +#include <stdint.h> +} + +#include <cstddef> +#include <memory> + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/sqlite/c_gate_fwd.hpp" +#include "utils/sqlite/statement_fwd.hpp" +#include "utils/sqlite/transaction_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Constant for the database::open flags: open in read-only mode. +static const int open_readonly = 1 << 0; +/// Constant for the database::open flags: open in read-write mode. +static const int open_readwrite = 1 << 1; +/// Constant for the database::open flags: create on open. +static const int open_create = 1 << 2; + + +/// A RAII model for the SQLite 3 database. +/// +/// This class holds the database of the SQLite 3 interface during its existence +/// and provides wrappers around several SQLite 3 library functions that operate +/// on such database. +/// +/// These wrapper functions differ from the C versions in that they use the +/// implicit database hold by the class, they use C++ types where appropriate +/// and they use exceptions to report errors. +/// +/// The wrappers intend to be as lightweight as possible but, in some +/// situations, they are pretty complex because of the workarounds needed to +/// make the SQLite 3 more C++ friendly. We prefer a clean C++ interface over +/// optimal efficiency, so this is OK. +class database { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class database_c_gate; + database(const utils::optional< utils::fs::path >&, void*, const bool); + void* raw_database(void); + +public: + ~database(void); + + static database in_memory(void); + static database open(const fs::path&, int); + static database temporary(void); + void close(void); + + const utils::optional< utils::fs::path >& db_filename(void) const; + + void exec(const std::string&); + + transaction begin_transaction(void); + statement create_statement(const std::string&); + + int64_t last_insert_rowid(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_DATABASE_HPP) diff --git a/utils/sqlite/database_fwd.hpp b/utils/sqlite/database_fwd.hpp new file mode 100644 index 000000000000..209342f159d6 --- /dev/null +++ b/utils/sqlite/database_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/database_fwd.hpp +/// Forward declarations for utils/sqlite/database.hpp + +#if !defined(UTILS_SQLITE_DATABASE_FWD_HPP) +#define UTILS_SQLITE_DATABASE_FWD_HPP + +namespace utils { +namespace sqlite { + + +class database; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_DATABASE_FWD_HPP) diff --git a/utils/sqlite/database_test.cpp b/utils/sqlite/database_test.cpp new file mode 100644 index 000000000000..70f057b9b793 --- /dev/null +++ b/utils/sqlite/database_test.cpp @@ -0,0 +1,287 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/database.hpp" + +#include <atf-c++.hpp> + +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/test_utils.hpp" +#include "utils/sqlite/transaction.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +ATF_TEST_CASE_WITHOUT_HEAD(in_memory); +ATF_TEST_CASE_BODY(in_memory) +{ + sqlite::database db = sqlite::database::in_memory(); + create_test_table(raw(db)); + verify_test_table(raw(db)); + + ATF_REQUIRE(!fs::exists(fs::path(":memory:"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open__readonly__ok); +ATF_TEST_CASE_BODY(open__readonly__ok) +{ + { + ::sqlite3* db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2("test.db", &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL)); + create_test_table(db); + ::sqlite3_close(db); + } + { + sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readonly); + verify_test_table(raw(db)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open__readonly__fail); +ATF_TEST_CASE_BODY(open__readonly__fail) +{ + REQUIRE_API_ERROR("sqlite3_open_v2", + sqlite::database::open(fs::path("missing.db"), sqlite::open_readonly)); + ATF_REQUIRE(!fs::exists(fs::path("missing.db"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open__create__ok); +ATF_TEST_CASE_BODY(open__create__ok) +{ + { + sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readwrite | sqlite::open_create); + ATF_REQUIRE(fs::exists(fs::path("test.db"))); + create_test_table(raw(db)); + } + { + ::sqlite3* db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2("test.db", &db, + SQLITE_OPEN_READONLY, NULL)); + verify_test_table(db); + ::sqlite3_close(db); + } +} + + +ATF_TEST_CASE(open__create__fail); +ATF_TEST_CASE_HEAD(open__create__fail) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(open__create__fail) +{ + fs::mkdir(fs::path("protected"), 0555); + REQUIRE_API_ERROR("sqlite3_open_v2", + sqlite::database::open(fs::path("protected/test.db"), + sqlite::open_readwrite | sqlite::open_create)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(temporary); +ATF_TEST_CASE_BODY(temporary) +{ + // We could validate if files go to disk by setting the temp_store_directory + // PRAGMA to a subdirectory of pwd, and then ensuring the subdirectory is + // not empty. However, there does not seem to be a way to force SQLite to + // unconditionally write the temporary database to disk (even with + // temp_store = FILE), so this scenary is hard to reproduce. + sqlite::database db = sqlite::database::temporary(); + create_test_table(raw(db)); + verify_test_table(raw(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(close); +ATF_TEST_CASE_BODY(close) +{ + sqlite::database db = sqlite::database::in_memory(); + db.close(); + // The destructor for the database will run now. If it does a second close, + // we may crash, so let's see if we don't. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(copy); +ATF_TEST_CASE_BODY(copy) +{ + sqlite::database db1 = sqlite::database::in_memory(); + { + sqlite::database db2 = sqlite::database::in_memory(); + create_test_table(raw(db2)); + db1 = db2; + verify_test_table(raw(db1)); + } + // db2 went out of scope. If the destruction is not properly managed, the + // memory of db1 may have been invalidated and this would not work. + verify_test_table(raw(db1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__in_memory); +ATF_TEST_CASE_BODY(db_filename__in_memory) +{ + const sqlite::database db = sqlite::database::in_memory(); + ATF_REQUIRE(!db.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__file); +ATF_TEST_CASE_BODY(db_filename__file) +{ + const sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readwrite | sqlite::open_create); + ATF_REQUIRE(db.db_filename()); + ATF_REQUIRE_EQ(fs::path("test.db"), db.db_filename().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__temporary); +ATF_TEST_CASE_BODY(db_filename__temporary) +{ + const sqlite::database db = sqlite::database::temporary(); + ATF_REQUIRE(!db.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__ok_after_close); +ATF_TEST_CASE_BODY(db_filename__ok_after_close) +{ + sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readwrite | sqlite::open_create); + const optional< fs::path > db_filename = db.db_filename(); + ATF_REQUIRE(db_filename); + db.close(); + ATF_REQUIRE_EQ(db_filename, db.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__ok); +ATF_TEST_CASE_BODY(exec__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec(create_test_table_sql); + verify_test_table(raw(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__fail); +ATF_TEST_CASE_BODY(exec__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + REQUIRE_API_ERROR("sqlite3_exec", + db.exec("SELECT * FROM test")); + REQUIRE_API_ERROR("sqlite3_exec", + db.exec("CREATE TABLE test (col INTEGER PRIMARY KEY);" + "FOO BAR")); + db.exec("SELECT * FROM test"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_statement__ok); +ATF_TEST_CASE_BODY(create_statement__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3"); + // Statement testing happens in statement_test. We are only interested here + // in ensuring that the API call exists and runs. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(begin_transaction); +ATF_TEST_CASE_BODY(begin_transaction) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::transaction stmt = db.begin_transaction(); + // Transaction testing happens in transaction_test. We are only interested + // here in ensuring that the API call exists and runs. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_statement__fail); +ATF_TEST_CASE_BODY(create_statement__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + REQUIRE_API_ERROR("sqlite3_prepare_v2", + db.create_statement("SELECT * FROM missing")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(last_insert_rowid); +ATF_TEST_CASE_BODY(last_insert_rowid) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE test (a INTEGER PRIMARY KEY, b INTEGER)"); + db.exec("INSERT INTO test VALUES (723, 5)"); + ATF_REQUIRE_EQ(723, db.last_insert_rowid()); + db.exec("INSERT INTO test VALUES (145, 20)"); + ATF_REQUIRE_EQ(145, db.last_insert_rowid()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, in_memory); + + ATF_ADD_TEST_CASE(tcs, open__readonly__ok); + ATF_ADD_TEST_CASE(tcs, open__readonly__fail); + ATF_ADD_TEST_CASE(tcs, open__create__ok); + ATF_ADD_TEST_CASE(tcs, open__create__fail); + + ATF_ADD_TEST_CASE(tcs, temporary); + + ATF_ADD_TEST_CASE(tcs, close); + + ATF_ADD_TEST_CASE(tcs, copy); + + ATF_ADD_TEST_CASE(tcs, db_filename__in_memory); + ATF_ADD_TEST_CASE(tcs, db_filename__file); + ATF_ADD_TEST_CASE(tcs, db_filename__temporary); + ATF_ADD_TEST_CASE(tcs, db_filename__ok_after_close); + + ATF_ADD_TEST_CASE(tcs, exec__ok); + ATF_ADD_TEST_CASE(tcs, exec__fail); + + ATF_ADD_TEST_CASE(tcs, begin_transaction); + + ATF_ADD_TEST_CASE(tcs, create_statement__ok); + ATF_ADD_TEST_CASE(tcs, create_statement__fail); + + ATF_ADD_TEST_CASE(tcs, last_insert_rowid); +} diff --git a/utils/sqlite/exceptions.cpp b/utils/sqlite/exceptions.cpp new file mode 100644 index 000000000000..cc2d42cab16c --- /dev/null +++ b/utils/sqlite/exceptions.cpp @@ -0,0 +1,175 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/exceptions.hpp" + +extern "C" { +#include <sqlite3.h> +} + +#include <string> + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/database.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +namespace { + + +/// Formats the database filename returned by sqlite for user consumption. +/// +/// \param db_filename An optional database filename. +/// +/// \return A string describing the filename. +static std::string +format_db_filename(const optional< fs::path >& db_filename) +{ + if (db_filename) + return db_filename.get().str(); + else + return "in-memory or temporary"; +} + + +} // anonymous namespace + + +/// Constructs a new error with a plain-text message. +/// +/// \param db_filename_ Database filename as returned by database::db_filename() +/// for error reporting purposes. +/// \param message The plain-text error message. +sqlite::error::error(const optional< fs::path >& db_filename_, + const std::string& message) : + std::runtime_error(F("%s (sqlite db: %s)") % message % + format_db_filename(db_filename_)), + _db_filename(db_filename_) +{ +} + + +/// Destructor for the error. +sqlite::error::~error(void) throw() +{ +} + + +/// Returns the path to the database that raised this error. +/// +/// \return A database filename as returned by database::db_filename(). +const optional< fs::path >& +sqlite::error::db_filename(void) const +{ + return _db_filename; +} + + +/// Constructs a new error. +/// +/// \param db_filename_ Database filename as returned by database::db_filename() +/// for error reporting purposes. +/// \param api_function_ The name of the API function that caused the error. +/// \param message_ The plain-text error message provided by SQLite. +sqlite::api_error::api_error(const optional< fs::path >& db_filename_, + const std::string& api_function_, + const std::string& message_) : + error(db_filename_, F("%s (sqlite op: %s)") % message_ % api_function_), + _api_function(api_function_) +{ +} + + +/// Destructor for the error. +sqlite::api_error::~api_error(void) throw() +{ +} + + +/// Constructs a new api_error with the message in the SQLite database. +/// +/// \param database_ The SQLite database. +/// \param api_function_ The name of the SQLite C API function that caused the +/// error. +/// +/// \return A new api_error with the retrieved message. +sqlite::api_error +sqlite::api_error::from_database(database& database_, + const std::string& api_function_) +{ + ::sqlite3* c_db = database_c_gate(database_).c_database(); + return api_error(database_.db_filename(), api_function_, + ::sqlite3_errmsg(c_db)); +} + + +/// Gets the name of the SQlite C API function that caused this error. +/// +/// \return The name of the function. +const std::string& +sqlite::api_error::api_function(void) const +{ + return _api_function; +} + + +/// Constructs a new error. +/// +/// \param db_filename_ Database filename as returned by database::db_filename() +/// for error reporting purposes. +/// \param name_ The name of the unknown column. +sqlite::invalid_column_error::invalid_column_error( + const optional< fs::path >& db_filename_, + const std::string& name_) : + error(db_filename_, F("Unknown column '%s'") % name_), + _column_name(name_) +{ +} + + +/// Destructor for the error. +sqlite::invalid_column_error::~invalid_column_error(void) throw() +{ +} + + +/// Gets the name of the column that could not be found. +/// +/// \return The name of the column requested by the user. +const std::string& +sqlite::invalid_column_error::column_name(void) const +{ + return _column_name; +} diff --git a/utils/sqlite/exceptions.hpp b/utils/sqlite/exceptions.hpp new file mode 100644 index 000000000000..a9450fce5c33 --- /dev/null +++ b/utils/sqlite/exceptions.hpp @@ -0,0 +1,94 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/exceptions.hpp +/// Exception types raised by the sqlite module. + +#if !defined(UTILS_SQLITE_EXCEPTIONS_HPP) +#define UTILS_SQLITE_EXCEPTIONS_HPP + +#include <stdexcept> +#include <string> + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Base exception for sqlite errors. +class error : public std::runtime_error { + /// Path to the database that raised this error. + utils::optional< utils::fs::path > _db_filename; + +public: + explicit error(const utils::optional< utils::fs::path >&, + const std::string&); + virtual ~error(void) throw(); + + const utils::optional< utils::fs::path >& db_filename(void) const; +}; + + +/// Exception for errors raised by the SQLite 3 API library. +class api_error : public error { + /// The name of the SQLite 3 C API function that caused this error. + std::string _api_function; + +public: + explicit api_error(const utils::optional< utils::fs::path >&, + const std::string&, const std::string&); + virtual ~api_error(void) throw(); + + static api_error from_database(database&, const std::string&); + + const std::string& api_function(void) const; +}; + + +/// The caller requested a non-existent column name. +class invalid_column_error : public error { + /// The name of the invalid column. + std::string _column_name; + +public: + explicit invalid_column_error(const utils::optional< utils::fs::path >&, + const std::string&); + virtual ~invalid_column_error(void) throw(); + + const std::string& column_name(void) const; +}; + + +} // namespace sqlite +} // namespace utils + + +#endif // !defined(UTILS_SQLITE_EXCEPTIONS_HPP) diff --git a/utils/sqlite/exceptions_test.cpp b/utils/sqlite/exceptions_test.cpp new file mode 100644 index 000000000000..d9e81038cc2f --- /dev/null +++ b/utils/sqlite/exceptions_test.cpp @@ -0,0 +1,129 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/exceptions.hpp" + +extern "C" { +#include <sqlite3.h> +} + +#include <cstring> +#include <string> + +#include <atf-c++.hpp> + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/database.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; + + +ATF_TEST_CASE_WITHOUT_HEAD(error__no_filename); +ATF_TEST_CASE_BODY(error__no_filename) +{ + const sqlite::database db = sqlite::database::in_memory(); + const sqlite::error e(db.db_filename(), "Some text"); + ATF_REQUIRE_EQ("Some text (sqlite db: in-memory or temporary)", + std::string(e.what())); + ATF_REQUIRE_EQ(db.db_filename(), e.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(error__with_filename); +ATF_TEST_CASE_BODY(error__with_filename) +{ + const sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + const sqlite::error e(db.db_filename(), "Some text"); + ATF_REQUIRE_EQ("Some text (sqlite db: test.db)", std::string(e.what())); + ATF_REQUIRE_EQ(db.db_filename(), e.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(api_error__explicit); +ATF_TEST_CASE_BODY(api_error__explicit) +{ + const sqlite::api_error e(none, "some_function", "Some text"); + ATF_REQUIRE_EQ( + "Some text (sqlite op: some_function) " + "(sqlite db: in-memory or temporary)", + std::string(e.what())); + ATF_REQUIRE_EQ("some_function", e.api_function()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(api_error__from_database); +ATF_TEST_CASE_BODY(api_error__from_database) +{ + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + + // Use the raw sqlite3 API to cause an error. Our C++ wrappers catch all + // errors and reraise them as exceptions, but here we want to handle the raw + // error directly for testing purposes. + sqlite::database_c_gate gate(db); + ::sqlite3_stmt* dummy_stmt; + const char* query = "ABCDE INVALID QUERY"; + (void)::sqlite3_prepare_v2(gate.c_database(), query, std::strlen(query), + &dummy_stmt, NULL); + + const sqlite::api_error e = sqlite::api_error::from_database( + db, "real_function"); + ATF_REQUIRE_MATCH( + ".*ABCDE.*\\(sqlite op: real_function\\) \\(sqlite db: test.db\\)", + std::string(e.what())); + ATF_REQUIRE_EQ("real_function", e.api_function()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_column_error); +ATF_TEST_CASE_BODY(invalid_column_error) +{ + const sqlite::invalid_column_error e(none, "some_name"); + ATF_REQUIRE_EQ("Unknown column 'some_name' " + "(sqlite db: in-memory or temporary)", + std::string(e.what())); + ATF_REQUIRE_EQ("some_name", e.column_name()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error__no_filename); + ATF_ADD_TEST_CASE(tcs, error__with_filename); + + ATF_ADD_TEST_CASE(tcs, api_error__explicit); + ATF_ADD_TEST_CASE(tcs, api_error__from_database); + + ATF_ADD_TEST_CASE(tcs, invalid_column_error); +} diff --git a/utils/sqlite/statement.cpp b/utils/sqlite/statement.cpp new file mode 100644 index 000000000000..0ae2af2d57ca --- /dev/null +++ b/utils/sqlite/statement.cpp @@ -0,0 +1,621 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/statement.hpp" + +extern "C" { +#include <sqlite3.h> +} + +#include <map> + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +static sqlite::type c_type_to_cxx(const int) UTILS_PURE; + + +/// Maps a SQLite 3 data type to our own representation. +/// +/// \param original The native SQLite 3 data type. +/// +/// \return Our internal representation for the native data type. +static sqlite::type +c_type_to_cxx(const int original) +{ + switch (original) { + case SQLITE_BLOB: return sqlite::type_blob; + case SQLITE_FLOAT: return sqlite::type_float; + case SQLITE_INTEGER: return sqlite::type_integer; + case SQLITE_NULL: return sqlite::type_null; + case SQLITE_TEXT: return sqlite::type_text; + default: UNREACHABLE_MSG("Unknown data type returned by SQLite 3"); + } + UNREACHABLE; +} + + +/// Handles the return value of a sqlite3_bind_* call. +/// +/// \param db The database the call was made on. +/// \param api_function The name of the sqlite3_bind_* function called. +/// \param error The error code returned by the function; can be SQLITE_OK. +/// +/// \throw std::bad_alloc If there was no memory for the binding. +/// \throw api_error If the binding fails for any other reason. +static void +handle_bind_error(sqlite::database& db, const char* api_function, + const int error) +{ + switch (error) { + case SQLITE_OK: + return; + case SQLITE_RANGE: + UNREACHABLE_MSG("Invalid index for bind argument"); + case SQLITE_NOMEM: + throw std::bad_alloc(); + default: + throw sqlite::api_error::from_database(db, api_function); + } +} + + +} // anonymous namespace + + +/// Internal implementation for sqlite::statement. +struct utils::sqlite::statement::impl : utils::noncopyable { + /// The database this statement belongs to. + sqlite::database& db; + + /// The SQLite 3 internal statement. + ::sqlite3_stmt* stmt; + + /// Cache for the column names in a statement; lazily initialized. + std::map< std::string, int > column_cache; + + /// Constructor. + /// + /// \param db_ The database this statement belongs to. Be aware that we + /// keep a *reference* to the database; in other words, if the database + /// vanishes, this object will become invalid. (It'd be trivial to keep + /// a shallow copy here instead, but I feel that statements that outlive + /// their database represents sloppy programming.) + /// \param stmt_ The SQLite internal statement. + impl(database& db_, ::sqlite3_stmt* stmt_) : + db(db_), + stmt(stmt_) + { + } + + /// Destructor. + /// + /// It is important to keep this as part of the 'impl' class instead of the + /// container class. The 'impl' class is destroyed exactly once (because it + /// is managed by a shared_ptr) and thus releasing the resources here is + /// OK. However, the container class is potentially released many times, + /// which means that we would be double-freeing the internal object and + /// reusing invalid data. + ~impl(void) + { + (void)::sqlite3_finalize(stmt); + } +}; + + +/// Initializes a statement object. +/// +/// This is an internal function. Use database::create_statement() to +/// instantiate one of these objects. +/// +/// \param db The database this statement belongs to. +/// \param raw_stmt A void pointer representing a SQLite native statement of +/// type sqlite3_stmt. +sqlite::statement::statement(database& db, void* raw_stmt) : + _pimpl(new impl(db, static_cast< ::sqlite3_stmt* >(raw_stmt))) +{ +} + + +/// Destructor for the statement. +/// +/// Remember that statements are reference-counted, so the statement will only +/// cease to be valid once its last copy is destroyed. +sqlite::statement::~statement(void) +{ +} + + +/// Executes a statement that is not supposed to return any data. +/// +/// Use this function to execute DDL and INSERT statements; i.e. statements that +/// only have one processing step and deliver no rows. This frees the caller +/// from having to deal with the return value of the step() function. +/// +/// \pre The statement to execute will not produce any rows. +void +sqlite::statement::step_without_results(void) +{ + const bool data = step(); + INV_MSG(!data, "The statement should not have produced any rows, but it " + "did"); +} + + +/// Performs a processing step on the statement. +/// +/// \return True if the statement returned a row; false if the processing has +/// finished. +/// +/// \throw api_error If the processing of the step raises an error. +bool +sqlite::statement::step(void) +{ + const int error = ::sqlite3_step(_pimpl->stmt); + switch (error) { + case SQLITE_DONE: + LD("Step statement; no more rows"); + return false; + case SQLITE_ROW: + LD("Step statement; row available for processing"); + return true; + default: + throw api_error::from_database(_pimpl->db, "sqlite3_step"); + } + UNREACHABLE; +} + + +/// Returns the number of columns in the step result. +/// +/// \return The number of columns available for data retrieval. +int +sqlite::statement::column_count(void) +{ + return ::sqlite3_column_count(_pimpl->stmt); +} + + +/// Returns the name of a particular column in the result. +/// +/// \param index The column to request the name of. +/// +/// \return The name of the requested column. +std::string +sqlite::statement::column_name(const int index) +{ + const char* name = ::sqlite3_column_name(_pimpl->stmt, index); + if (name == NULL) + throw api_error::from_database(_pimpl->db, "sqlite3_column_name"); + return name; +} + + +/// Returns the type of a particular column in the result. +/// +/// \param index The column to request the type of. +/// +/// \return The type of the requested column. +sqlite::type +sqlite::statement::column_type(const int index) +{ + return c_type_to_cxx(::sqlite3_column_type(_pimpl->stmt, index)); +} + + +/// Finds a column by name. +/// +/// \param name The name of the column to search for. +/// +/// \return The column identifier. +/// +/// \throw value_error If the name cannot be found. +int +sqlite::statement::column_id(const char* name) +{ + std::map< std::string, int >& cache = _pimpl->column_cache; + + if (cache.empty()) { + for (int i = 0; i < column_count(); i++) { + const std::string aux_name = column_name(i); + INV(cache.find(aux_name) == cache.end()); + cache[aux_name] = i; + } + } + + const std::map< std::string, int >::const_iterator iter = cache.find(name); + if (iter == cache.end()) + throw invalid_column_error(_pimpl->db.db_filename(), name); + else + return (*iter).second; +} + + +/// Returns a particular column in the result as a blob. +/// +/// \param index The column to retrieve. +/// +/// \return A block of memory with the blob contents. Note that the pointer +/// returned by this call will be invalidated on the next call to any SQLite API +/// function. +sqlite::blob +sqlite::statement::column_blob(const int index) +{ + PRE(column_type(index) == type_blob); + return blob(::sqlite3_column_blob(_pimpl->stmt, index), + ::sqlite3_column_bytes(_pimpl->stmt, index)); +} + + +/// Returns a particular column in the result as a double. +/// +/// \param index The column to retrieve. +/// +/// \return The double value. +double +sqlite::statement::column_double(const int index) +{ + PRE(column_type(index) == type_float); + return ::sqlite3_column_double(_pimpl->stmt, index); +} + + +/// Returns a particular column in the result as an integer. +/// +/// \param index The column to retrieve. +/// +/// \return The integer value. Note that the value may not fit in an integer +/// depending on the platform. Use column_int64 to retrieve the integer without +/// truncation. +int +sqlite::statement::column_int(const int index) +{ + PRE(column_type(index) == type_integer); + return ::sqlite3_column_int(_pimpl->stmt, index); +} + + +/// Returns a particular column in the result as a 64-bit integer. +/// +/// \param index The column to retrieve. +/// +/// \return The integer value. +int64_t +sqlite::statement::column_int64(const int index) +{ + PRE(column_type(index) == type_integer); + return ::sqlite3_column_int64(_pimpl->stmt, index); +} + + +/// Returns a particular column in the result as a double. +/// +/// \param index The column to retrieve. +/// +/// \return A C string with the contents. Note that the pointer returned by +/// this call will be invalidated on the next call to any SQLite API function. +/// If you want to be extra safe, store the result in a std::string to not worry +/// about this. +std::string +sqlite::statement::column_text(const int index) +{ + PRE(column_type(index) == type_text); + return reinterpret_cast< const char* >(::sqlite3_column_text( + _pimpl->stmt, index)); +} + + +/// Returns the number of bytes stored in the column. +/// +/// \pre This is only valid for columns of type blob and text. +/// +/// \param index The column to retrieve the size of. +/// +/// \return The number of bytes in the column. Remember that strings are stored +/// in their UTF-8 representation; this call returns the number of *bytes*, not +/// characters. +int +sqlite::statement::column_bytes(const int index) +{ + PRE(column_type(index) == type_blob || column_type(index) == type_text); + return ::sqlite3_column_bytes(_pimpl->stmt, index); +} + + +/// Type-checked version of column_blob. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_blob if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +sqlite::blob +sqlite::statement::safe_column_blob(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_blob) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a blob") % name); + return column_blob(column); +} + + +/// Type-checked version of column_double. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_double if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +double +sqlite::statement::safe_column_double(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_float) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a float") % name); + return column_double(column); +} + + +/// Type-checked version of column_int. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_int if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +int +sqlite::statement::safe_column_int(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_integer) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not an integer") % name); + return column_int(column); +} + + +/// Type-checked version of column_int64. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_int64 if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +int64_t +sqlite::statement::safe_column_int64(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_integer) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not an integer") % name); + return column_int64(column); +} + + +/// Type-checked version of column_text. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_text if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +std::string +sqlite::statement::safe_column_text(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_text) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a string") % name); + return column_text(column); +} + + +/// Type-checked version of column_bytes. +/// +/// \param name The name of the column to retrieve the size of. +/// +/// \return The same as column_bytes if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve the size of is invalid. +/// \throw invalid_column_error If name is invalid. +int +sqlite::statement::safe_column_bytes(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_blob && + column_type(column) != sqlite::type_text) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a blob or a string") % name); + return column_bytes(column); +} + + +/// Resets a statement to allow further processing. +void +sqlite::statement::reset(void) +{ + (void)::sqlite3_reset(_pimpl->stmt); +} + + +/// Binds a blob to a prepared statement. +/// +/// \param index The index of the binding. +/// \param b Description of the blob, which must remain valid during the +/// execution of the statement. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const blob& b) +{ + const int error = ::sqlite3_bind_blob(_pimpl->stmt, index, b.memory, b.size, + SQLITE_STATIC); + handle_bind_error(_pimpl->db, "sqlite3_bind_blob", error); +} + + +/// Binds a double value to a prepared statement. +/// +/// \param index The index of the binding. +/// \param value The double value to bind. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const double value) +{ + const int error = ::sqlite3_bind_double(_pimpl->stmt, index, value); + handle_bind_error(_pimpl->db, "sqlite3_bind_double", error); +} + + +/// Binds an integer value to a prepared statement. +/// +/// \param index The index of the binding. +/// \param value The integer value to bind. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const int value) +{ + const int error = ::sqlite3_bind_int(_pimpl->stmt, index, value); + handle_bind_error(_pimpl->db, "sqlite3_bind_int", error); +} + + +/// Binds a 64-bit integer value to a prepared statement. +/// +/// \param index The index of the binding. +/// \param value The 64-bin integer value to bind. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const int64_t value) +{ + const int error = ::sqlite3_bind_int64(_pimpl->stmt, index, value); + handle_bind_error(_pimpl->db, "sqlite3_bind_int64", error); +} + + +/// Binds a NULL value to a prepared statement. +/// +/// \param index The index of the binding. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const null& /* null */) +{ + const int error = ::sqlite3_bind_null(_pimpl->stmt, index); + handle_bind_error(_pimpl->db, "sqlite3_bind_null", error); +} + + +/// Binds a text string to a prepared statement. +/// +/// \param index The index of the binding. +/// \param text The string to bind. SQLite generates an internal copy of this +/// string, so the original string object does not have to remain live. We +/// do this because handling the lifetime of std::string objects is very +/// hard (think about implicit conversions), so it is very easy to shoot +/// ourselves in the foot if we don't do this. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const std::string& text) +{ + const int error = ::sqlite3_bind_text(_pimpl->stmt, index, text.c_str(), + text.length(), SQLITE_TRANSIENT); + handle_bind_error(_pimpl->db, "sqlite3_bind_text", error); +} + + +/// Returns the index of the highest parameter. +/// +/// \return A parameter index. +int +sqlite::statement::bind_parameter_count(void) +{ + return ::sqlite3_bind_parameter_count(_pimpl->stmt); +} + + +/// Returns the index of a named parameter. +/// +/// \param name The name of the parameter to be queried; must exist. +/// +/// \return A parameter index. +int +sqlite::statement::bind_parameter_index(const std::string& name) +{ + const int index = ::sqlite3_bind_parameter_index(_pimpl->stmt, + name.c_str()); + PRE_MSG(index > 0, "Parameter name not in statement"); + return index; +} + + +/// Returns the name of a parameter by index. +/// +/// \param index The index to query; must be valid. +/// +/// \return The name of the parameter. +std::string +sqlite::statement::bind_parameter_name(const int index) +{ + const char* name = ::sqlite3_bind_parameter_name(_pimpl->stmt, index); + PRE_MSG(name != NULL, "Index value out of range or nameless parameter"); + return std::string(name); +} + + +/// Clears any bindings and releases their memory. +void +sqlite::statement::clear_bindings(void) +{ + const int error = ::sqlite3_clear_bindings(_pimpl->stmt); + PRE_MSG(error == SQLITE_OK, "SQLite3 contract has changed; it should " + "only return SQLITE_OK"); +} diff --git a/utils/sqlite/statement.hpp b/utils/sqlite/statement.hpp new file mode 100644 index 000000000000..bcd1831e4841 --- /dev/null +++ b/utils/sqlite/statement.hpp @@ -0,0 +1,137 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/statement.hpp +/// Wrapper classes and utilities for SQLite statement processing. +/// +/// This module contains thin RAII wrappers around the SQLite 3 structures +/// representing statements. + +#if !defined(UTILS_SQLITE_STATEMENT_HPP) +#define UTILS_SQLITE_STATEMENT_HPP + +#include "utils/sqlite/statement_fwd.hpp" + +extern "C" { +#include <stdint.h> +} + +#include <memory> +#include <string> + +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Representation of a BLOB. +class blob { +public: + /// Memory representing the contents of the blob, or NULL if empty. + /// + /// This memory must remain valid throughout the life of this object, as we + /// do not grab ownership of the memory. + const void* memory; + + /// Number of bytes in memory. + int size; + + /// Constructs a new blob. + /// + /// \param memory_ Pointer to the contents of the blob. + /// \param size_ The size of memory_. + blob(const void* memory_, const int size_) : + memory(memory_), size(size_) + { + } +}; + + +/// Representation of a SQL NULL value. +class null { +}; + + +/// A RAII model for an SQLite 3 statement. +class statement { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + statement(database&, void*); + friend class database; + +public: + ~statement(void); + + bool step(void); + void step_without_results(void); + + int column_count(void); + std::string column_name(const int); + type column_type(const int); + int column_id(const char*); + + blob column_blob(const int); + double column_double(const int); + int column_int(const int); + int64_t column_int64(const int); + std::string column_text(const int); + int column_bytes(const int); + + blob safe_column_blob(const char*); + double safe_column_double(const char*); + int safe_column_int(const char*); + int64_t safe_column_int64(const char*); + std::string safe_column_text(const char*); + int safe_column_bytes(const char*); + + void reset(void); + + void bind(const int, const blob&); + void bind(const int, const double); + void bind(const int, const int); + void bind(const int, const int64_t); + void bind(const int, const null&); + void bind(const int, const std::string&); + template< class T > void bind(const char*, const T&); + + int bind_parameter_count(void); + int bind_parameter_index(const std::string&); + std::string bind_parameter_name(const int); + + void clear_bindings(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_STATEMENT_HPP) diff --git a/utils/sqlite/statement.ipp b/utils/sqlite/statement.ipp new file mode 100644 index 000000000000..3f219016a2a9 --- /dev/null +++ b/utils/sqlite/statement.ipp @@ -0,0 +1,52 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_SQLITE_STATEMENT_IPP) +#define UTILS_SQLITE_STATEMENT_IPP + +#include "utils/sqlite/statement.hpp" + + +/// Binds a value to a parameter of a prepared statement. +/// +/// \param parameter The name of the parameter; must exist. This is a raw C +/// string instead of a std::string because statement parameter names are +/// known at compilation time and the program should really not be +/// constructing them dynamically. +/// \param value The value to bind to the parameter. +/// +/// \throw api_error If the binding fails. +template< class T > +void +utils::sqlite::statement::bind(const char* parameter, const T& value) +{ + bind(bind_parameter_index(parameter), value); +} + + +#endif // !defined(UTILS_SQLITE_STATEMENT_IPP) diff --git a/utils/sqlite/statement_fwd.hpp b/utils/sqlite/statement_fwd.hpp new file mode 100644 index 000000000000..26634c965018 --- /dev/null +++ b/utils/sqlite/statement_fwd.hpp @@ -0,0 +1,57 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/statement_fwd.hpp +/// Forward declarations for utils/sqlite/statement.hpp + +#if !defined(UTILS_SQLITE_STATEMENT_FWD_HPP) +#define UTILS_SQLITE_STATEMENT_FWD_HPP + +namespace utils { +namespace sqlite { + + +/// Representation of the SQLite data types. +enum type { + type_blob, + type_float, + type_integer, + type_null, + type_text, +}; + + +class blob; +class null; +class statement; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_STATEMENT_FWD_HPP) diff --git a/utils/sqlite/statement_test.cpp b/utils/sqlite/statement_test.cpp new file mode 100644 index 000000000000..40bc92cb5c0e --- /dev/null +++ b/utils/sqlite/statement_test.cpp @@ -0,0 +1,784 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/statement.ipp" + +extern "C" { +#include <stdint.h> +} + +#include <cstring> +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/test_utils.hpp" + +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE_WITHOUT_HEAD(step__ok); +ATF_TEST_CASE_BODY(step__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement( + "CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + ATF_REQUIRE_THROW(sqlite::error, db.exec("SELECT * FROM foo")); + ATF_REQUIRE(!stmt.step()); + db.exec("SELECT * FROM foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step__many); +ATF_TEST_CASE_BODY(step__many) +{ + sqlite::database db = sqlite::database::in_memory(); + create_test_table(raw(db)); + sqlite::statement stmt = db.create_statement( + "SELECT prime FROM test ORDER BY prime"); + for (int i = 0; i < 5; i++) + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step__fail); +ATF_TEST_CASE_BODY(step__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement( + "CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + ATF_REQUIRE(!stmt.step()); + REQUIRE_API_ERROR("sqlite3_step", stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step_without_results__ok); +ATF_TEST_CASE_BODY(step_without_results__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement( + "CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + ATF_REQUIRE_THROW(sqlite::error, db.exec("SELECT * FROM foo")); + stmt.step_without_results(); + db.exec("SELECT * FROM foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step_without_results__fail); +ATF_TEST_CASE_BODY(step_without_results__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO foo VALUES (3)"); + sqlite::statement stmt = db.create_statement( + "INSERT INTO foo VALUES (3)"); + REQUIRE_API_ERROR("sqlite3_step", stmt.step_without_results()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_count); +ATF_TEST_CASE_BODY(column_count) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER PRIMARY KEY, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (5, 3, 'foo');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(3, stmt.column_count()); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_name__ok); +ATF_TEST_CASE_BODY(column_name__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (first INTEGER PRIMARY KEY, second TEXT);" + "INSERT INTO foo VALUES (5, 'foo');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("first", stmt.column_name(0)); + ATF_REQUIRE_EQ("second", stmt.column_name(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_name__fail); +ATF_TEST_CASE_BODY(column_name__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (first INTEGER PRIMARY KEY);" + "INSERT INTO foo VALUES (5);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("first", stmt.column_name(0)); + REQUIRE_API_ERROR("sqlite3_column_name", stmt.column_name(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_type__ok); +ATF_TEST_CASE_BODY(column_type__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a_blob BLOB," + " a_float FLOAT," + " an_integer INTEGER," + " a_null BLOB," + " a_text TEXT);" + "INSERT INTO foo VALUES (x'0102', 0.3, 5, NULL, 'foo bar');" + "INSERT INTO foo VALUES (NULL, NULL, NULL, NULL, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_blob == stmt.column_type(0)); + ATF_REQUIRE(sqlite::type_float == stmt.column_type(1)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(2)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(3)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(4)); + ATF_REQUIRE(stmt.step()); + for (int i = 0; i < stmt.column_count(); i++) + ATF_REQUIRE(sqlite::type_null == stmt.column_type(i)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_type__out_of_range); +ATF_TEST_CASE_BODY(column_type__out_of_range) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER PRIMARY KEY);" + "INSERT INTO foo VALUES (1);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(1)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(512)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_id__ok); +ATF_TEST_CASE_BODY(column_id__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (bar INTEGER PRIMARY KEY, " + " baz INTEGER);" + "INSERT INTO foo VALUES (1, 2);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0, stmt.column_id("bar")); + ATF_REQUIRE_EQ(1, stmt.column_id("baz")); + ATF_REQUIRE_EQ(0, stmt.column_id("bar")); + ATF_REQUIRE_EQ(1, stmt.column_id("baz")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_id__missing); +ATF_TEST_CASE_BODY(column_id__missing) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (bar INTEGER PRIMARY KEY, " + " baz INTEGER);" + "INSERT INTO foo VALUES (1, 2);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0, stmt.column_id("bar")); + try { + stmt.column_id("bazo"); + fail("invalid_column_error not raised"); + } catch (const sqlite::invalid_column_error& e) { + ATF_REQUIRE_EQ("bazo", e.column_name()); + } + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_blob); +ATF_TEST_CASE_BODY(column_blob) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b BLOB, c INTEGER);" + "INSERT INTO foo VALUES (NULL, x'cafe', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + const sqlite::blob blob = stmt.column_blob(1); + ATF_REQUIRE_EQ(0xca, static_cast< const uint8_t* >(blob.memory)[0]); + ATF_REQUIRE_EQ(0xfe, static_cast< const uint8_t* >(blob.memory)[1]); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_double); +ATF_TEST_CASE_BODY(column_double) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b DOUBLE, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 0.5, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0.5, stmt.column_double(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_int__ok); +ATF_TEST_CASE_BODY(column_int__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 987, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(987, stmt.column_int(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_int__overflow); +ATF_TEST_CASE_BODY(column_int__overflow) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 4294967419, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(123, stmt.column_int(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_int64); +ATF_TEST_CASE_BODY(column_int64) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 4294967419, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4294967419LL, stmt.column_int64(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_text); +ATF_TEST_CASE_BODY(column_text) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b TEXT, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 'foo bar', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("foo bar", stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_bytes__blob); +ATF_TEST_CASE_BODY(column_bytes__blob) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a BLOB);" + "INSERT INTO foo VALUES (x'12345678');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4, stmt.column_bytes(0)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_bytes__text); +ATF_TEST_CASE_BODY(column_bytes__text) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('foo bar');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(7, stmt.column_bytes(0)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_blob__ok); +ATF_TEST_CASE_BODY(safe_column_blob__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b BLOB, c INTEGER);" + "INSERT INTO foo VALUES (NULL, x'cafe', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + const sqlite::blob blob = stmt.safe_column_blob("b"); + ATF_REQUIRE_EQ(0xca, static_cast< const uint8_t* >(blob.memory)[0]); + ATF_REQUIRE_EQ(0xfe, static_cast< const uint8_t* >(blob.memory)[1]); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_blob__fail); +ATF_TEST_CASE_BODY(safe_column_blob__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER);" + "INSERT INTO foo VALUES (123);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_blob("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a blob", + stmt.safe_column_blob("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_double__ok); +ATF_TEST_CASE_BODY(safe_column_double__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b DOUBLE, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 0.5, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0.5, stmt.safe_column_double("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_double__fail); +ATF_TEST_CASE_BODY(safe_column_double__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER);" + "INSERT INTO foo VALUES (NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_double("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a float", + stmt.safe_column_double("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int__ok); +ATF_TEST_CASE_BODY(safe_column_int__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 987, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(987, stmt.safe_column_int("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int__fail); +ATF_TEST_CASE_BODY(safe_column_int__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('def');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_int("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not an integer", + stmt.safe_column_int("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int64__ok); +ATF_TEST_CASE_BODY(safe_column_int64__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 4294967419, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4294967419LL, stmt.safe_column_int64("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int64__fail); +ATF_TEST_CASE_BODY(safe_column_int64__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('abc');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_int64("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not an integer", + stmt.safe_column_int64("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_text__ok); +ATF_TEST_CASE_BODY(safe_column_text__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b TEXT, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 'foo bar', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("foo bar", stmt.safe_column_text("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_text__fail); +ATF_TEST_CASE_BODY(safe_column_text__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER);" + "INSERT INTO foo VALUES (NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_text("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a string", + stmt.safe_column_text("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_bytes__ok__blob); +ATF_TEST_CASE_BODY(safe_column_bytes__ok__blob) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a BLOB);" + "INSERT INTO foo VALUES (x'12345678');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4, stmt.safe_column_bytes("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_bytes__ok__text); +ATF_TEST_CASE_BODY(safe_column_bytes__ok__text) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('foo bar');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(7, stmt.safe_column_bytes("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_bytes__fail); +ATF_TEST_CASE_BODY(safe_column_bytes__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES (NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_bytes("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a blob or a string", + stmt.safe_column_bytes("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(reset); +ATF_TEST_CASE_BODY(reset) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('foo bar');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(!stmt.step()); + stmt.reset(); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__blob); +ATF_TEST_CASE_BODY(bind__blob) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + const unsigned char blob[] = {0xca, 0xfe}; + stmt.bind(1, sqlite::blob(static_cast< const void* >(blob), 2)); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_blob == stmt.column_type(1)); + const unsigned char* ret_blob = + static_cast< const unsigned char* >(stmt.column_blob(1).memory); + ATF_REQUIRE(std::memcmp(blob, ret_blob, 2) == 0); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__double); +ATF_TEST_CASE_BODY(bind__double) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, 0.5); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_float == stmt.column_type(1)); + ATF_REQUIRE_EQ(0.5, stmt.column_double(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__int); +ATF_TEST_CASE_BODY(bind__int) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, 123); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(1)); + ATF_REQUIRE_EQ(123, stmt.column_int(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__int64); +ATF_TEST_CASE_BODY(bind__int64) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, static_cast< int64_t >(4294967419LL)); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(1)); + ATF_REQUIRE_EQ(4294967419LL, stmt.column_int64(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__null); +ATF_TEST_CASE_BODY(bind__null) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, sqlite::null()); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__text); +ATF_TEST_CASE_BODY(bind__text) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + const std::string str = "Hello"; + stmt.bind(1, str); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(1)); + ATF_REQUIRE_EQ(str, stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__text__transient); +ATF_TEST_CASE_BODY(bind__text__transient) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo"); + + { + const std::string str = "Hello"; + stmt.bind(":foo", str); + } + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(1)); + ATF_REQUIRE_EQ(std::string("Hello"), stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__by_name); +ATF_TEST_CASE_BODY(bind__by_name) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo"); + + const std::string str = "Hello"; + stmt.bind(":foo", str); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(1)); + ATF_REQUIRE_EQ(str, stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind_parameter_count); +ATF_TEST_CASE_BODY(bind_parameter_count) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?, ?"); + ATF_REQUIRE_EQ(2, stmt.bind_parameter_count()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind_parameter_index); +ATF_TEST_CASE_BODY(bind_parameter_index) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo, ?, :bar"); + ATF_REQUIRE_EQ(1, stmt.bind_parameter_index(":foo")); + ATF_REQUIRE_EQ(3, stmt.bind_parameter_index(":bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind_parameter_name); +ATF_TEST_CASE_BODY(bind_parameter_name) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo, ?, :bar"); + ATF_REQUIRE_EQ(":foo", stmt.bind_parameter_name(1)); + ATF_REQUIRE_EQ(":bar", stmt.bind_parameter_name(3)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(clear_bindings); +ATF_TEST_CASE_BODY(clear_bindings) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, 5); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(1)); + ATF_REQUIRE_EQ(5, stmt.column_int(1)); + stmt.clear_bindings(); + stmt.reset(); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(1)); + + ATF_REQUIRE(!stmt.step()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, step__ok); + ATF_ADD_TEST_CASE(tcs, step__many); + ATF_ADD_TEST_CASE(tcs, step__fail); + + ATF_ADD_TEST_CASE(tcs, step_without_results__ok); + ATF_ADD_TEST_CASE(tcs, step_without_results__fail); + + ATF_ADD_TEST_CASE(tcs, column_count); + + ATF_ADD_TEST_CASE(tcs, column_name__ok); + ATF_ADD_TEST_CASE(tcs, column_name__fail); + + ATF_ADD_TEST_CASE(tcs, column_type__ok); + ATF_ADD_TEST_CASE(tcs, column_type__out_of_range); + + ATF_ADD_TEST_CASE(tcs, column_id__ok); + ATF_ADD_TEST_CASE(tcs, column_id__missing); + + ATF_ADD_TEST_CASE(tcs, column_blob); + ATF_ADD_TEST_CASE(tcs, column_double); + ATF_ADD_TEST_CASE(tcs, column_int__ok); + ATF_ADD_TEST_CASE(tcs, column_int__overflow); + ATF_ADD_TEST_CASE(tcs, column_int64); + ATF_ADD_TEST_CASE(tcs, column_text); + + ATF_ADD_TEST_CASE(tcs, column_bytes__blob); + ATF_ADD_TEST_CASE(tcs, column_bytes__text); + + ATF_ADD_TEST_CASE(tcs, safe_column_blob__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_blob__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_double__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_double__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_int__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_int__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_int64__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_int64__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_text__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_text__fail); + + ATF_ADD_TEST_CASE(tcs, safe_column_bytes__ok__blob); + ATF_ADD_TEST_CASE(tcs, safe_column_bytes__ok__text); + ATF_ADD_TEST_CASE(tcs, safe_column_bytes__fail); + + ATF_ADD_TEST_CASE(tcs, reset); + + ATF_ADD_TEST_CASE(tcs, bind__blob); + ATF_ADD_TEST_CASE(tcs, bind__double); + ATF_ADD_TEST_CASE(tcs, bind__int64); + ATF_ADD_TEST_CASE(tcs, bind__int); + ATF_ADD_TEST_CASE(tcs, bind__null); + ATF_ADD_TEST_CASE(tcs, bind__text); + ATF_ADD_TEST_CASE(tcs, bind__text__transient); + ATF_ADD_TEST_CASE(tcs, bind__by_name); + + ATF_ADD_TEST_CASE(tcs, bind_parameter_count); + ATF_ADD_TEST_CASE(tcs, bind_parameter_index); + ATF_ADD_TEST_CASE(tcs, bind_parameter_name); + + ATF_ADD_TEST_CASE(tcs, clear_bindings); +} diff --git a/utils/sqlite/test_utils.hpp b/utils/sqlite/test_utils.hpp new file mode 100644 index 000000000000..bf35d209a164 --- /dev/null +++ b/utils/sqlite/test_utils.hpp @@ -0,0 +1,151 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/test_utils.hpp +/// Utilities for tests of the sqlite modules. +/// +/// This file is intended to be included once, and only once, for every test +/// program that needs it. All the code is herein contained to simplify the +/// dependency chain in the build rules. + +#if !defined(UTILS_SQLITE_TEST_UTILS_HPP) +# define UTILS_SQLITE_TEST_UTILS_HPP +#else +# error "test_utils.hpp can only be included once" +#endif + +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/defs.hpp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/exceptions.hpp" + + +namespace { + + +/// Checks that a given expression raises a particular sqlite::api_error. +/// +/// We cannot make any assumptions regarding the error text provided by SQLite, +/// so we resort to checking only which API function raised the error (because +/// our code is the one hardcoding these strings). +/// +/// \param exp_api_function The name of the SQLite 3 C API function that causes +/// the error. +/// \param statement The statement to execute. +#define REQUIRE_API_ERROR(exp_api_function, statement) \ + do { \ + try { \ + statement; \ + ATF_FAIL("api_error not raised by " #statement); \ + } catch (const utils::sqlite::api_error& api_error) { \ + ATF_REQUIRE_EQ(exp_api_function, api_error.api_function()); \ + } \ + } while (0) + + +/// Gets the pointer to the internal sqlite3 of a database object. +/// +/// This is pure syntactic sugar to simplify typing in the test cases. +/// +/// \param db The SQLite database. +/// +/// \return The internal sqlite3 of the input database. +static inline ::sqlite3* +raw(utils::sqlite::database& db) +{ + return utils::sqlite::database_c_gate(db).c_database(); +} + + +/// SQL commands to create a test table. +/// +/// See create_test_table() for more details. +static const char* create_test_table_sql = + "CREATE TABLE test (prime INTEGER PRIMARY KEY);" + "INSERT INTO test (prime) VALUES (1);\n" + "INSERT INTO test (prime) VALUES (2);\n" + "INSERT INTO test (prime) VALUES (7);\n" + "INSERT INTO test (prime) VALUES (5);\n" + "INSERT INTO test (prime) VALUES (3);\n"; + + +static void create_test_table(::sqlite3*) UTILS_UNUSED; + + +/// Creates a 'test' table in a database. +/// +/// The created 'test' table is populated with a few rows. If there are any +/// problems creating the database, this fails the test case. +/// +/// Use the verify_test_table() function on the same database to verify that +/// the table is present and contains the expected data. +/// +/// \param db The database in which to create the test table. +static void +create_test_table(::sqlite3* db) +{ + std::cout << "Creating 'test' table in the database\n"; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_exec(db, create_test_table_sql, + NULL, NULL, NULL)); +} + + +static void verify_test_table(::sqlite3*) UTILS_UNUSED; + + +/// Verifies that the specified database contains the 'test' table. +/// +/// This function ensures that the provided database contains the 'test' table +/// created by the create_test_table() function on the same database. If it +/// doesn't, this fails the caller test case. +/// +/// \param db The database to validate. +static void +verify_test_table(::sqlite3* db) +{ + std::cout << "Verifying that the 'test' table is in the database\n"; + char **result; + int rows, columns; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_get_table(db, + "SELECT * FROM test ORDER BY prime", &result, &rows, &columns, NULL)); + ATF_REQUIRE_EQ(5, rows); + ATF_REQUIRE_EQ(1, columns); + ATF_REQUIRE_EQ("prime", std::string(result[0])); + ATF_REQUIRE_EQ("1", std::string(result[1])); + ATF_REQUIRE_EQ("2", std::string(result[2])); + ATF_REQUIRE_EQ("3", std::string(result[3])); + ATF_REQUIRE_EQ("5", std::string(result[4])); + ATF_REQUIRE_EQ("7", std::string(result[5])); + ::sqlite3_free_table(result); +} + + +} // anonymous namespace diff --git a/utils/sqlite/transaction.cpp b/utils/sqlite/transaction.cpp new file mode 100644 index 000000000000..e0235fef9c57 --- /dev/null +++ b/utils/sqlite/transaction.cpp @@ -0,0 +1,142 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/transaction.hpp" + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +/// Internal implementation for the transaction. +struct utils::sqlite::transaction::impl : utils::noncopyable { + /// The database this transaction belongs to. + database& db; + + /// Possible statuses of a transaction. + enum statuses { + open_status, + committed_status, + rolled_back_status, + }; + + /// The current status of the transaction. + statuses status; + + /// Constructs a new transaction. + /// + /// \param db_ The database this transaction belongs to. + /// \param status_ The status of the new transaction. + impl(database& db_, const statuses status_) : + db(db_), + status(status_) + { + } + + /// Destroys the transaction. + /// + /// This rolls back the transaction if it is open. + ~impl(void) + { + if (status == impl::open_status) { + try { + rollback(); + } catch (const sqlite::error& e) { + LW(F("Error while rolling back a transaction: %s") % e.what()); + } + } + } + + /// Commits the transaction. + /// + /// \throw api_error If there is any problem while committing the + /// transaction. + void + commit(void) + { + PRE(status == impl::open_status); + db.exec("COMMIT"); + status = impl::committed_status; + } + + /// Rolls the transaction back. + /// + /// \throw api_error If there is any problem while rolling the + /// transaction back. + void + rollback(void) + { + PRE(status == impl::open_status); + db.exec("ROLLBACK"); + status = impl::rolled_back_status; + } +}; + + +/// Initializes a transaction object. +/// +/// This is an internal function. Use database::begin_transaction() to +/// instantiate one of these objects. +/// +/// \param db The database this transaction belongs to. +sqlite::transaction::transaction(database& db) : + _pimpl(new impl(db, impl::open_status)) +{ +} + + +/// Destructor for the transaction. +sqlite::transaction::~transaction(void) +{ +} + + +/// Commits the transaction. +/// +/// \throw api_error If there is any problem while committing the transaction. +void +sqlite::transaction::commit(void) +{ + _pimpl->commit(); +} + + +/// Rolls the transaction back. +/// +/// \throw api_error If there is any problem while rolling the transaction back. +void +sqlite::transaction::rollback(void) +{ + _pimpl->rollback(); +} diff --git a/utils/sqlite/transaction.hpp b/utils/sqlite/transaction.hpp new file mode 100644 index 000000000000..71f3b0c93f4a --- /dev/null +++ b/utils/sqlite/transaction.hpp @@ -0,0 +1,69 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/transaction.hpp +/// A RAII model for SQLite transactions. + +#if !defined(UTILS_SQLITE_TRANSACTION_HPP) +#define UTILS_SQLITE_TRANSACTION_HPP + +#include "utils/sqlite/transaction_fwd.hpp" + +#include <memory> + +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// A RAII model for an SQLite 3 statement. +/// +/// A transaction is automatically rolled back when it goes out of scope unless +/// it has been explicitly committed. +class transaction { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + explicit transaction(database&); + friend class database; + +public: + ~transaction(void); + + void commit(void); + void rollback(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_TRANSACTION_HPP) diff --git a/utils/sqlite/transaction_fwd.hpp b/utils/sqlite/transaction_fwd.hpp new file mode 100644 index 000000000000..7773d8380458 --- /dev/null +++ b/utils/sqlite/transaction_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/sqlite/transaction_fwd.hpp +/// Forward declarations for utils/sqlite/transaction.hpp + +#if !defined(UTILS_SQLITE_TRANSACTION_FWD_HPP) +#define UTILS_SQLITE_TRANSACTION_FWD_HPP + +namespace utils { +namespace sqlite { + + +class transaction; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_TRANSACTION_FWD_HPP) diff --git a/utils/sqlite/transaction_test.cpp b/utils/sqlite/transaction_test.cpp new file mode 100644 index 000000000000..d53e6fba4378 --- /dev/null +++ b/utils/sqlite/transaction_test.cpp @@ -0,0 +1,135 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/sqlite/transaction.hpp" + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Ensures that a table has a single specific value in a column. +/// +/// \param db The SQLite database. +/// \param table_name The table to be checked. +/// \param column_name The column to be checked. +/// \param exp_value The value expected to be found in the column. +/// +/// \return True if the column contains a single value and it matches exp_value; +/// false if not. If the query fails, the calling test is marked as bad. +static bool +check_in_table(sqlite::database& db, const char* table_name, + const char* column_name, int exp_value) +{ + sqlite::statement stmt = db.create_statement( + F("SELECT * FROM %s WHERE %s == %s") % table_name % column_name % + exp_value); + if (!stmt.step()) + return false; + if (stmt.step()) + ATF_FAIL("More than one value found in table"); + return true; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(automatic_rollback); +ATF_TEST_CASE_BODY(automatic_rollback) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE t (col INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO t VALUES (3)"); + { + sqlite::transaction tx = db.begin_transaction(); + db.exec("INSERT INTO t VALUES (5)"); + } + ATF_REQUIRE( check_in_table(db, "t", "col", 3)); + ATF_REQUIRE(!check_in_table(db, "t", "col", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(explicit_commit); +ATF_TEST_CASE_BODY(explicit_commit) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE t (col INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO t VALUES (3)"); + { + sqlite::transaction tx = db.begin_transaction(); + db.exec("INSERT INTO t VALUES (5)"); + tx.commit(); + } + ATF_REQUIRE(check_in_table(db, "t", "col", 3)); + ATF_REQUIRE(check_in_table(db, "t", "col", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(explicit_rollback); +ATF_TEST_CASE_BODY(explicit_rollback) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE t (col INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO t VALUES (3)"); + { + sqlite::transaction tx = db.begin_transaction(); + db.exec("INSERT INTO t VALUES (5)"); + tx.rollback(); + } + ATF_REQUIRE( check_in_table(db, "t", "col", 3)); + ATF_REQUIRE(!check_in_table(db, "t", "col", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(nested_fail); +ATF_TEST_CASE_BODY(nested_fail) +{ + sqlite::database db = sqlite::database::in_memory(); + { + sqlite::transaction tx = db.begin_transaction(); + ATF_REQUIRE_THROW(sqlite::error, db.begin_transaction()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, automatic_rollback); + ATF_ADD_TEST_CASE(tcs, explicit_commit); + ATF_ADD_TEST_CASE(tcs, explicit_rollback); + ATF_ADD_TEST_CASE(tcs, nested_fail); +} diff --git a/utils/stacktrace.cpp b/utils/stacktrace.cpp new file mode 100644 index 000000000000..11636b31959f --- /dev/null +++ b/utils/stacktrace.cpp @@ -0,0 +1,370 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/stacktrace.hpp" + +extern "C" { +#include <sys/param.h> +#include <sys/resource.h> + +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <fstream> +#include <iostream> +#include <stdexcept> +#include <string> +#include <vector> + +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/process/executor.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +/// Built-in path to GDB. +/// +/// This is the value that should be passed to the find_gdb() function. If this +/// is an absolute path, then we use the binary specified by the variable; if it +/// is a relative path, we look for the binary in the path. +/// +/// Test cases can override the value of this built-in constant to unit-test the +/// behavior of the functions below. +const char* utils::builtin_gdb = GDB; + + +/// Maximum time the external GDB process is allowed to run for. +datetime::delta utils::gdb_timeout(60, 0); + + +namespace { + + +/// Maximum length of the core file name, if known. +/// +/// Some operating systems impose a maximum length on the basename of the core +/// file. If MAXCOMLEN is defined, then we need to truncate the program name to +/// this length before searching for the core file. If no such limit is known, +/// this is infinite. +static const std::string::size_type max_core_name_length = +#if defined(MAXCOMLEN) + MAXCOMLEN +#else + std::string::npos +#endif + ; + + +/// Functor to execute GDB in a subprocess. +class run_gdb { + /// Path to the GDB binary to use. + const fs::path& _gdb; + + /// Path to the program being debugged. + const fs::path& _program; + + /// Path to the dumped core. + const fs::path& _core_name; + +public: + /// Constructs the functor. + /// + /// \param gdb_ Path to the GDB binary to use. + /// \param program_ Path to the program being debugged. Can be relative to + /// the given work directory. + /// \param core_name_ Path to the dumped core. Use find_core() to deduce + /// a valid candidate. Can be relative to the given work directory. + run_gdb(const fs::path& gdb_, const fs::path& program_, + const fs::path& core_name_) : + _gdb(gdb_), _program(program_), _core_name(core_name_) + { + } + + /// Executes GDB. + /// + /// \param control_directory Directory where we can store control files to + /// not clobber any files created by the program being debugged. + void + operator()(const fs::path& control_directory) + { + const fs::path gdb_script_path = control_directory / "gdb.script"; + + // Old versions of GDB, such as the one shipped by FreeBSD as of + // 11.0-CURRENT on 2014-11-26, do not support scripts on the command + // line via the '-ex' flag. Instead, we have to create a script file + // and use that instead. + std::ofstream gdb_script(gdb_script_path.c_str()); + if (!gdb_script) { + std::cerr << "Cannot create GDB script\n"; + ::_exit(EXIT_FAILURE); + } + gdb_script << "backtrace\n"; + gdb_script.close(); + + utils::unsetenv("TERM"); + + std::vector< std::string > args; + args.push_back("-batch"); + args.push_back("-q"); + args.push_back("-x"); + args.push_back(gdb_script_path.str()); + args.push_back(_program.str()); + args.push_back(_core_name.str()); + + // Force all GDB output to go to stderr. We print messages to stderr + // when grabbing the stacktrace and we do not want GDB's output to end + // up split in two different files. + if (::dup2(STDERR_FILENO, STDOUT_FILENO) == -1) { + std::cerr << "Cannot redirect stdout to stderr\n"; + ::_exit(EXIT_FAILURE); + } + + process::exec(_gdb, args); + } +}; + + +} // anonymous namespace + + +/// Looks for the path to the GDB binary. +/// +/// \return The absolute path to the GDB binary if any, otherwise none. Note +/// that the returned path may or may not be valid: there is no guarantee that +/// the path exists and is executable. +optional< fs::path > +utils::find_gdb(void) +{ + if (std::strlen(builtin_gdb) == 0) { + LW("The builtin path to GDB is bogus, which probably indicates a bug " + "in the build system; cannot gather stack traces"); + return none; + } + + const fs::path gdb(builtin_gdb); + if (gdb.is_absolute()) + return utils::make_optional(gdb); + else + return fs::find_in_path(gdb.c_str()); +} + + +/// Looks for a core file for the given program. +/// +/// \param program The name of the binary that generated the core file. Can be +/// either absolute or relative. +/// \param status The exit status of the program. This is necessary to gather +/// the PID. +/// \param work_directory The directory from which the program was run. +/// +/// \return The path to the core file, if found; otherwise none. +optional< fs::path > +utils::find_core(const fs::path& program, const process::status& status, + const fs::path& work_directory) +{ + std::vector< fs::path > candidates; + + candidates.push_back(work_directory / + (program.leaf_name().substr(0, max_core_name_length) + ".core")); + if (program.is_absolute()) { + candidates.push_back(program.branch_path() / + (program.leaf_name().substr(0, max_core_name_length) + ".core")); + } + candidates.push_back(work_directory / (F("core.%s") % status.dead_pid())); + candidates.push_back(fs::path("/cores") / + (F("core.%s") % status.dead_pid())); + + for (std::vector< fs::path >::const_iterator iter = candidates.begin(); + iter != candidates.end(); ++iter) { + if (fs::exists(*iter)) { + LD(F("Attempting core file candidate %s: found") % *iter); + return utils::make_optional(*iter); + } else { + LD(F("Attempting core file candidate %s: not found") % *iter); + } + } + return none; +} + + +/// Raises core size limit to its possible maximum. +/// +/// This is a best-effort operation. There is no guarantee that the operation +/// will yield a large-enough limit to generate any possible core file. +/// +/// \return True if the core size could be unlimited; false otherwise. +bool +utils::unlimit_core_size(void) +{ + bool ok; + + struct ::rlimit rl; + if (::getrlimit(RLIMIT_CORE, &rl) == -1) { + const int original_errno = errno; + LW(F("getrlimit should not have failed but got: %s") % + std::strerror(original_errno)); + ok = false; + } else { + if (rl.rlim_max == 0) { + LW("getrlimit returned 0 for RLIMIT_CORE rlim_max; cannot raise " + "soft core limit"); + ok = false; + } else { + rl.rlim_cur = rl.rlim_max; + LD(F("Raising soft core size limit to %s (hard value)") % + rl.rlim_cur); + if (::setrlimit(RLIMIT_CORE, &rl) == -1) { + const int original_errno = errno; + LW(F("setrlimit should not have failed but got: %s") % + std::strerror(original_errno)); + ok = false; + } else { + ok = true; + } + } + } + + return ok; +} + + +/// Gathers a stacktrace of a crashed program. +/// +/// \param program The name of the binary that crashed and dumped a core file. +/// Can be either absolute or relative. +/// \param executor_handle The executor handler to get the status from and +/// gdb handler from. +/// \param exit_handle The exit handler to stream additional diagnostic +/// information from (stderr) and for redirecting to additional +/// information to gdb from. +/// +/// \post If anything goes wrong, the diagnostic messages are written to the +/// output. This function should not throw. +void +utils::dump_stacktrace(const fs::path& program, + executor::executor_handle& executor_handle, + const executor::exit_handle& exit_handle) +{ + PRE(exit_handle.status()); + const process::status& status = exit_handle.status().get(); + PRE(status.signaled() && status.coredump()); + + std::ofstream gdb_err(exit_handle.stderr_file().c_str(), std::ios::app); + if (!gdb_err) { + LW(F("Failed to open %s to append GDB's output") % + exit_handle.stderr_file()); + return; + } + + gdb_err << F("Process with PID %s exited with signal %s and dumped core; " + "attempting to gather stack trace\n") % + status.dead_pid() % status.termsig(); + + const optional< fs::path > gdb = utils::find_gdb(); + if (!gdb) { + gdb_err << F("Cannot find GDB binary; builtin was '%s'\n") % + builtin_gdb; + return; + } + + const optional< fs::path > core_file = find_core( + program, status, exit_handle.work_directory()); + if (!core_file) { + gdb_err << F("Cannot find any core file\n"); + return; + } + + gdb_err.flush(); + const executor::exec_handle exec_handle = + executor_handle.spawn_followup( + run_gdb(gdb.get(), program, core_file.get()), + exit_handle, gdb_timeout); + const executor::exit_handle gdb_exit_handle = + executor_handle.wait(exec_handle); + + const optional< process::status >& gdb_status = gdb_exit_handle.status(); + if (!gdb_status) { + gdb_err << "GDB timed out\n"; + } else { + if (gdb_status.get().exited() && + gdb_status.get().exitstatus() == EXIT_SUCCESS) { + gdb_err << "GDB exited successfully\n"; + } else { + gdb_err << "GDB failed; see output above for details\n"; + } + } +} + + +/// Gathers a stacktrace of a program if it crashed. +/// +/// This is just a convenience function to allow appending the stacktrace to an +/// existing file and to permit reusing the status as returned by auxiliary +/// process-spawning functions. +/// +/// \param program The name of the binary that crashed and dumped a core file. +/// Can be either absolute or relative. +/// \param executor_handle The executor handler to get the status from and +/// gdb handler from. +/// \param exit_handle The exit handler to stream additional diagnostic +/// information from (stderr) and for redirecting to additional +/// information to gdb from. +/// +/// \throw std::runtime_error If the output file cannot be opened. +/// +/// \post If anything goes wrong with the stack gatheringq, the diagnostic +/// messages are written to the output. +void +utils::dump_stacktrace_if_available(const fs::path& program, + executor::executor_handle& executor_handle, + const executor::exit_handle& exit_handle) +{ + const optional< process::status >& status = exit_handle.status(); + if (!status || !status.get().signaled() || !status.get().coredump()) + return; + + dump_stacktrace(program, executor_handle, exit_handle); +} diff --git a/utils/stacktrace.hpp b/utils/stacktrace.hpp new file mode 100644 index 000000000000..13648e2c71cb --- /dev/null +++ b/utils/stacktrace.hpp @@ -0,0 +1,68 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/stacktrace.hpp +/// Utilities to gather a stacktrace of a crashing binary. + +#if !defined(ENGINE_STACKTRACE_HPP) +#define ENGINE_STACKTRACE_HPP + +#include <ostream> + +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/process/executor_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { + + +extern const char* builtin_gdb; +extern utils::datetime::delta gdb_timeout; + +utils::optional< utils::fs::path > find_gdb(void); + +utils::optional< utils::fs::path > find_core(const utils::fs::path&, + const utils::process::status&, + const utils::fs::path&); + +bool unlimit_core_size(void); + +void dump_stacktrace(const utils::fs::path&, + utils::process::executor::executor_handle&, + const utils::process::executor::exit_handle&); + +void dump_stacktrace_if_available(const utils::fs::path&, + utils::process::executor::executor_handle&, + const utils::process::executor::exit_handle&); + + +} // namespace utils + +#endif // !defined(ENGINE_STACKTRACE_HPP) diff --git a/utils/stacktrace_helper.cpp b/utils/stacktrace_helper.cpp new file mode 100644 index 000000000000..f01e8c809797 --- /dev/null +++ b/utils/stacktrace_helper.cpp @@ -0,0 +1,36 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 <cstdlib> + + +int +main(void) +{ + std::abort(); +} diff --git a/utils/stacktrace_test.cpp b/utils/stacktrace_test.cpp new file mode 100644 index 000000000000..ca87e7087f5a --- /dev/null +++ b/utils/stacktrace_test.cpp @@ -0,0 +1,620 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/stacktrace.hpp" + +extern "C" { +#include <sys/resource.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include <signal.h> +#include <unistd.h> +} + +#include <iostream> +#include <sstream> + +#include <atf-c++.hpp> + +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/process/executor.ipp" +#include "utils/process/child.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/test_utils.ipp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Functor to execute a binary in a subprocess. +/// +/// The provided binary is copied to the current work directory before being +/// executed and the copy is given the name chosen by the user. The copy is +/// necessary so that we have a deterministic location for where core files may +/// be dumped (if they happen to be dumped in the current directory). +class crash_me { + /// Path to the binary to execute. + const fs::path _binary; + + /// Name of the binary after being copied. + const fs::path _copy_name; + +public: + /// Constructor. + /// + /// \param binary_ Path to binary to execute. + /// \param copy_name_ Name of the binary after being copied. If empty, + /// use the leaf name of binary_. + explicit crash_me(const fs::path& binary_, + const std::string& copy_name_ = "") : + _binary(binary_), + _copy_name(copy_name_.empty() ? binary_.leaf_name() : copy_name_) + { + } + + /// Runs the binary. + void + operator()(void) const UTILS_NORETURN + { + atf::utils::copy_file(_binary.str(), _copy_name.str()); + + const std::vector< std::string > args; + process::exec(_copy_name, args); + } + + /// Runs the binary. + /// + /// This interface is exposed to support passing crash_me to the executor. + void + operator()(const fs::path& /* control_directory */) const + UTILS_NORETURN + { + (*this)(); // Delegate to ensure the two entry points remain in sync. + } +}; + + +static void child_exit(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that exits cleanly. +static void +child_exit(const fs::path& /* control_directory */) +{ + ::_exit(EXIT_SUCCESS); +} + + +static void child_pause(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that just blocks. +static void +child_pause(const fs::path& /* control_directory */) +{ + sigset_t mask; + sigemptyset(&mask); + for (;;) { + ::sigsuspend(&mask); + } + std::abort(); +} + + +/// Generates a core dump, if possible. +/// +/// \post If this fails to generate a core file, the test case is marked as +/// skipped. The caller can rely on this when attempting further checks on the +/// core dump by assuming that the core dump exists somewhere. +/// +/// \param test_case Pointer to the caller test case, needed to obtain the path +/// to the source directory. +/// \param base_name Name of the binary to execute, which will be a copy of a +/// helper binary that always crashes. This name should later be part of +/// the core filename. +/// +/// \return The status of the crashed binary. +static process::status +generate_core(const atf::tests::tc* test_case, const char* base_name) +{ + utils::prepare_coredump_test(test_case); + + const fs::path helper = fs::path(test_case->get_config_var("srcdir")) / + "stacktrace_helper"; + + const process::status status = process::child::fork_files( + crash_me(helper, base_name), + fs::path("unused.out"), fs::path("unused.err"))->wait(); + ATF_REQUIRE(status.signaled()); + if (!status.coredump()) + ATF_SKIP("Test failed to generate core dump"); + return status; +} + + +/// Generates a core dump, if possible. +/// +/// \post If this fails to generate a core file, the test case is marked as +/// skipped. The caller can rely on this when attempting further checks on the +/// core dump by assuming that the core dump exists somewhere. +/// +/// \param test_case Pointer to the caller test case, needed to obtain the path +/// to the source directory. +/// \param base_name Name of the binary to execute, which will be a copy of a +/// helper binary that always crashes. This name should later be part of +/// the core filename. +/// \param executor_handle Executor to use to generate the core dump. +/// +/// \return The exit handle of the subprocess so that a stacktrace can be +/// executed reusing this context later on. +static executor::exit_handle +generate_core(const atf::tests::tc* test_case, const char* base_name, + executor::executor_handle& executor_handle) +{ + utils::prepare_coredump_test(test_case); + + const fs::path helper = fs::path(test_case->get_config_var("srcdir")) / + "stacktrace_helper"; + + const executor::exec_handle exec_handle = executor_handle.spawn( + crash_me(helper, base_name), datetime::delta(60, 0), none, none, none); + const executor::exit_handle exit_handle = executor_handle.wait(exec_handle); + + if (!exit_handle.status()) + ATF_SKIP("Test failed to generate core dump (timed out)"); + const process::status& status = exit_handle.status().get(); + ATF_REQUIRE(status.signaled()); + if (!status.coredump()) + ATF_SKIP("Test failed to generate core dump"); + + return exit_handle; +} + + +/// Creates a script. +/// +/// \param script Path to the script to create. +/// \param contents Contents of the script. +static void +create_script(const char* script, const std::string& contents) +{ + atf::utils::create_file(script, "#! /bin/sh\n\n" + contents); + ATF_REQUIRE(::chmod(script, 0755) != -1); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(unlimit_core_size); +ATF_TEST_CASE_BODY(unlimit_core_size) +{ + utils::require_run_coredump_tests(this); + + struct rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = RLIM_INFINITY; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) + skip("Failed to lower the core size limit"); + + ATF_REQUIRE(utils::unlimit_core_size()); + + const fs::path helper = fs::path(get_config_var("srcdir")) / + "stacktrace_helper"; + const process::status status = process::child::fork_files( + crash_me(helper), + fs::path("unused.out"), fs::path("unused.err"))->wait(); + ATF_REQUIRE(status.signaled()); + if (!status.coredump()) + fail("Core not dumped as expected"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlimit_core_size__hard_is_zero); +ATF_TEST_CASE_BODY(unlimit_core_size__hard_is_zero) +{ + utils::require_run_coredump_tests(this); + + struct rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = 0; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) + skip("Failed to lower the core size limit"); + + ATF_REQUIRE(!utils::unlimit_core_size()); + + const fs::path helper = fs::path(get_config_var("srcdir")) / + "stacktrace_helper"; + const process::status status = process::child::fork_files( + crash_me(helper), + fs::path("unused.out"), fs::path("unused.err"))->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(!status.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__use_builtin); +ATF_TEST_CASE_BODY(find_gdb__use_builtin) +{ + utils::builtin_gdb = "/path/to/gdb"; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(gdb); + ATF_REQUIRE_EQ("/path/to/gdb", gdb.get().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__search_builtin__ok); +ATF_TEST_CASE_BODY(find_gdb__search_builtin__ok) +{ + atf::utils::create_file("custom-name", ""); + ATF_REQUIRE(::chmod("custom-name", 0755) != -1); + const fs::path exp_gdb = fs::path("custom-name").to_absolute(); + + utils::setenv("PATH", "/non-existent/location:.:/bin"); + + utils::builtin_gdb = "custom-name"; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(gdb); + ATF_REQUIRE_EQ(exp_gdb, gdb.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__search_builtin__fail); +ATF_TEST_CASE_BODY(find_gdb__search_builtin__fail) +{ + utils::setenv("PATH", "."); + utils::builtin_gdb = "foo"; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(!gdb); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__bogus_value); +ATF_TEST_CASE_BODY(find_gdb__bogus_value) +{ + utils::builtin_gdb = ""; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(!gdb); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_core__found__short); +ATF_TEST_CASE_BODY(find_core__found__short) +{ + const process::status status = generate_core(this, "short"); + INV(status.coredump()); + const optional< fs::path > core_name = utils::find_core( + fs::path("short"), status, fs::path(".")); + if (!core_name) + fail("Core dumped, but no candidates found"); + ATF_REQUIRE(core_name.get().str().find("core") != std::string::npos); + ATF_REQUIRE(fs::exists(core_name.get())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_core__found__long); +ATF_TEST_CASE_BODY(find_core__found__long) +{ + const process::status status = generate_core( + this, "long-name-that-may-be-truncated-in-some-systems"); + INV(status.coredump()); + const optional< fs::path > core_name = utils::find_core( + fs::path("long-name-that-may-be-truncated-in-some-systems"), + status, fs::path(".")); + if (!core_name) + fail("Core dumped, but no candidates found"); + ATF_REQUIRE(core_name.get().str().find("core") != std::string::npos); + ATF_REQUIRE(fs::exists(core_name.get())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_core__not_found); +ATF_TEST_CASE_BODY(find_core__not_found) +{ + const process::status status = process::status::fake_signaled(SIGILL, true); + const optional< fs::path > core_name = utils::find_core( + fs::path("missing"), status, fs::path(".")); + if (core_name) + fail("Core not dumped, but candidate found: " + core_name.get().str()); +} + + +ATF_TEST_CASE(dump_stacktrace__integration); +ATF_TEST_CASE_HEAD(dump_stacktrace__integration) +{ + set_md_var("require.progs", utils::builtin_gdb); +} +ATF_TEST_CASE_BODY(dump_stacktrace__integration) +{ + executor::executor_handle handle = executor::setup(); + + executor::exit_handle exit_handle = generate_core(this, "short", handle); + INV(exit_handle.status()); + INV(exit_handle.status().get().coredump()); + + std::ostringstream output; + utils::dump_stacktrace(fs::path("short"), handle, exit_handle); + + // It is hard to validate the execution of an arbitrary GDB of which we do + // not know anything. Just assume that the backtrace, at the very least, + // prints a couple of frame identifiers. + ATF_REQUIRE(!atf::utils::grep_file("#0", exit_handle.stdout_file().str())); + ATF_REQUIRE( atf::utils::grep_file("#0", exit_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::grep_file("#1", exit_handle.stdout_file().str())); + ATF_REQUIRE( atf::utils::grep_file("#1", exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__ok); +ATF_TEST_CASE_BODY(dump_stacktrace__ok) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "echo 'frame 1'; echo 'frame 2'; " + "echo 'some warning' 1>&2; exit 0"); + utils::builtin_gdb = "fake-gdb"; + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + INV(exit_handle.status()); + INV(exit_handle.status().get().coredump()); + + utils::dump_stacktrace(fs::path("short"), handle, exit_handle); + + // Note how all output is expected on stderr even for the messages that the + // script decided to send to stdout. + ATF_REQUIRE(atf::utils::grep_file("exited with signal [0-9]* and dumped", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^frame 1$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^frame 2$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^some warning$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("GDB exited successfully", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__cannot_find_core); +ATF_TEST_CASE_BODY(dump_stacktrace__cannot_find_core) +{ + // Make sure we can find a GDB binary so that we don't fail the test for + // the wrong reason. + utils::setenv("PATH", "."); + utils::builtin_gdb = "fake-gdb"; + atf::utils::create_file("fake-gdb", "unused"); + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + const optional< fs::path > core_name = utils::find_core( + fs::path("short"), + exit_handle.status().get(), + exit_handle.work_directory()); + if (core_name) { + // This is needed even if we provide a different basename to + // dump_stacktrace below because the system policies may be generating + // core dumps by PID, not binary name. + std::cout << "Removing core dump: " << core_name << '\n'; + fs::unlink(core_name.get()); + } + + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + atf::utils::cat_file(exit_handle.stdout_file().str(), "stdout: "); + atf::utils::cat_file(exit_handle.stderr_file().str(), "stderr: "); + ATF_REQUIRE(atf::utils::grep_file("Cannot find any core file", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__cannot_find_gdb); +ATF_TEST_CASE_BODY(dump_stacktrace__cannot_find_gdb) +{ + utils::setenv("PATH", "."); + utils::builtin_gdb = "missing-gdb"; + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file( + "Cannot find GDB binary; builtin was 'missing-gdb'", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__gdb_fail); +ATF_TEST_CASE_BODY(dump_stacktrace__gdb_fail) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "echo 'foo'; echo 'bar' 1>&2; exit 1"); + const std::string gdb = (fs::current_path() / "fake-gdb").str(); + utils::builtin_gdb = gdb.c_str(); + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + atf::utils::create_file((exit_handle.work_directory() / "fake.core").str(), + "Invalid core file, but not read"); + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file("^foo$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^bar$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("GDB failed; see output above", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__gdb_timeout); +ATF_TEST_CASE_BODY(dump_stacktrace__gdb_timeout) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "while :; do sleep 1; done"); + const std::string gdb = (fs::current_path() / "fake-gdb").str(); + utils::builtin_gdb = gdb.c_str(); + utils::gdb_timeout = datetime::delta(1, 0); + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + atf::utils::create_file((exit_handle.work_directory() / "fake.core").str(), + "Invalid core file, but not read"); + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file("GDB timed out", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace_if_available__append); +ATF_TEST_CASE_BODY(dump_stacktrace_if_available__append) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "echo 'frame 1'; exit 0"); + utils::builtin_gdb = "fake-gdb"; + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + atf::utils::create_file(exit_handle.stdout_file().str(), "Pre-stdout"); + atf::utils::create_file(exit_handle.stderr_file().str(), "Pre-stderr"); + + utils::dump_stacktrace_if_available(fs::path("short"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file("Pre-stdout", + exit_handle.stdout_file().str())); + ATF_REQUIRE(atf::utils::grep_file("Pre-stderr", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("frame 1", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace_if_available__no_status); +ATF_TEST_CASE_BODY(dump_stacktrace_if_available__no_status) +{ + executor::executor_handle handle = executor::setup(); + const executor::exec_handle exec_handle = handle.spawn( + child_pause, datetime::delta(0, 100000), none, none, none); + executor::exit_handle exit_handle = handle.wait(exec_handle); + INV(!exit_handle.status()); + + utils::dump_stacktrace_if_available(fs::path("short"), handle, exit_handle); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stdout_file().str(), "")); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stderr_file().str(), "")); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace_if_available__no_coredump); +ATF_TEST_CASE_BODY(dump_stacktrace_if_available__no_coredump) +{ + executor::executor_handle handle = executor::setup(); + const executor::exec_handle exec_handle = handle.spawn( + child_exit, datetime::delta(60, 0), none, none, none); + executor::exit_handle exit_handle = handle.wait(exec_handle); + INV(exit_handle.status()); + INV(exit_handle.status().get().exited()); + INV(exit_handle.status().get().exitstatus() == EXIT_SUCCESS); + + utils::dump_stacktrace_if_available(fs::path("short"), handle, exit_handle); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stdout_file().str(), "")); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stderr_file().str(), "")); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, unlimit_core_size); + ATF_ADD_TEST_CASE(tcs, unlimit_core_size__hard_is_zero); + + ATF_ADD_TEST_CASE(tcs, find_gdb__use_builtin); + ATF_ADD_TEST_CASE(tcs, find_gdb__search_builtin__ok); + ATF_ADD_TEST_CASE(tcs, find_gdb__search_builtin__fail); + ATF_ADD_TEST_CASE(tcs, find_gdb__bogus_value); + + ATF_ADD_TEST_CASE(tcs, find_core__found__short); + ATF_ADD_TEST_CASE(tcs, find_core__found__long); + ATF_ADD_TEST_CASE(tcs, find_core__not_found); + + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__integration); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__ok); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__cannot_find_core); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__cannot_find_gdb); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__gdb_fail); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__gdb_timeout); + + ATF_ADD_TEST_CASE(tcs, dump_stacktrace_if_available__append); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace_if_available__no_status); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace_if_available__no_coredump); +} diff --git a/utils/stream.cpp b/utils/stream.cpp new file mode 100644 index 000000000000..ee3ab417f753 --- /dev/null +++ b/utils/stream.cpp @@ -0,0 +1,149 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/stream.hpp" + +#include <fstream> +#include <iostream> +#include <sstream> +#include <stdexcept> + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Constant that represents the path to stdout. +static const fs::path stdout_path("/dev/stdout"); + + +/// Constant that represents the path to stderr. +static const fs::path stderr_path("/dev/stderr"); + + +} // anonymous namespace + + +/// Opens a new file for output, respecting the stdout and stderr streams. +/// +/// \param path The path to the output file to be created. +/// +/// \return A pointer to a new output stream. +std::auto_ptr< std::ostream > +utils::open_ostream(const fs::path& path) +{ + std::auto_ptr< std::ostream > out; + if (path == stdout_path) { + out.reset(new std::ofstream()); + out->copyfmt(std::cout); + out->clear(std::cout.rdstate()); + out->rdbuf(std::cout.rdbuf()); + } else if (path == stderr_path) { + out.reset(new std::ofstream()); + out->copyfmt(std::cerr); + out->clear(std::cerr.rdstate()); + out->rdbuf(std::cerr.rdbuf()); + } else { + out.reset(new std::ofstream(path.c_str())); + if (!(*out)) { + throw std::runtime_error(F("Cannot open output file %s") % path); + } + } + INV(out.get() != NULL); + return out; +} + + +/// Gets the length of a stream. +/// +/// \param is The input stream for which to calculate its length. +/// +/// \return The length of the stream. This is of size_t type instead of +/// directly std::streampos to simplify the caller. Some systems do not +/// support comparing a std::streampos directly to an integer (see +/// NetBSD 1.5.x), which is what we often want to do. +/// +/// \throw std::exception If calculating the length fails due to a stream error. +std::size_t +utils::stream_length(std::istream& is) +{ + const std::streampos current_pos = is.tellg(); + try { + is.seekg(0, std::ios::end); + const std::streampos length = is.tellg(); + is.seekg(current_pos, std::ios::beg); + return static_cast< std::size_t >(length); + } catch (...) { + is.seekg(current_pos, std::ios::beg); + throw; + } +} + + +/// Reads a whole file into memory. +/// +/// \param path The file to read. +/// +/// \return A plain string containing the raw contents of the file. +/// +/// \throw std::runtime_error If the file cannot be opened. +std::string +utils::read_file(const fs::path& path) +{ + std::ifstream input(path.c_str()); + if (!input) + throw std::runtime_error(F("Failed to open '%s' for read") % path); + return read_stream(input); +} + + +/// Reads the whole contents of a stream into memory. +/// +/// \param input The input stream from which to read. +/// +/// \return A plain string containing the raw contents of the file. +std::string +utils::read_stream(std::istream& input) +{ + std::ostringstream buffer; + + char tmp[1024]; + while (input.good()) { + input.read(tmp, sizeof(tmp)); + if (input.good() || input.eof()) { + buffer.write(tmp, input.gcount()); + } + } + + return buffer.str(); +} diff --git a/utils/stream.hpp b/utils/stream.hpp new file mode 100644 index 000000000000..5c9316e72810 --- /dev/null +++ b/utils/stream.hpp @@ -0,0 +1,57 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/stream.hpp +/// Stream manipulation utilities. +/// +/// Note that file-manipulation utilities live in utils::fs instead. The +/// utilities here deal with already-open streams. + +#if !defined(UTILS_STREAM_HPP) +#define UTILS_STREAM_HPP + +#include <cstddef> +#include <istream> +#include <memory> +#include <ostream> +#include <string> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { + + +std::auto_ptr< std::ostream > open_ostream(const utils::fs::path&); +std::size_t stream_length(std::istream&); +std::string read_file(const utils::fs::path&); +std::string read_stream(std::istream&); + + +} // namespace utils + +#endif // !defined(UTILS_STREAM_HPP) diff --git a/utils/stream_test.cpp b/utils/stream_test.cpp new file mode 100644 index 000000000000..7c4f3b5c6b4a --- /dev/null +++ b/utils/stream_test.cpp @@ -0,0 +1,157 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/stream.hpp" + +#include <cstdlib> +#include <sstream> + +#include <atf-c++.hpp> + +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(open_ostream__stdout); +ATF_TEST_CASE_BODY(open_ostream__stdout) +{ + const pid_t pid = atf::utils::fork(); + if (pid == 0) { + std::auto_ptr< std::ostream > output = utils::open_ostream( + fs::path("/dev/stdout")); + (*output) << "Message to stdout\n"; + output.reset(); + std::exit(EXIT_SUCCESS); + } + atf::utils::wait(pid, EXIT_SUCCESS, "Message to stdout\n", ""); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open_ostream__stderr); +ATF_TEST_CASE_BODY(open_ostream__stderr) +{ + const pid_t pid = atf::utils::fork(); + if (pid == 0) { + std::auto_ptr< std::ostream > output = utils::open_ostream( + fs::path("/dev/stderr")); + (*output) << "Message to stderr\n"; + output.reset(); + std::exit(EXIT_SUCCESS); + } + atf::utils::wait(pid, EXIT_SUCCESS, "", "Message to stderr\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open_ostream__other); +ATF_TEST_CASE_BODY(open_ostream__other) +{ + const pid_t pid = atf::utils::fork(); + if (pid == 0) { + std::auto_ptr< std::ostream > output = utils::open_ostream( + fs::path("some-file.txt")); + (*output) << "Message to other file\n"; + output.reset(); + std::exit(EXIT_SUCCESS); + } + atf::utils::wait(pid, EXIT_SUCCESS, "", ""); + atf::utils::compare_file("some-file.txt", "Message to other file\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(stream_length__empty); +ATF_TEST_CASE_BODY(stream_length__empty) +{ + std::istringstream input(""); + ATF_REQUIRE_EQ(0, utils::stream_length(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(stream_length__some); +ATF_TEST_CASE_BODY(stream_length__some) +{ + const std::string contents(8192, 'x'); + std::istringstream input(contents); + ATF_REQUIRE_EQ( + contents.length(), + static_cast< std::string::size_type >(utils::stream_length(input))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_file__ok); +ATF_TEST_CASE_BODY(read_file__ok) +{ + const char* contents = "These are\nsome file contents"; + atf::utils::create_file("input.txt", contents); + ATF_REQUIRE_EQ(contents, utils::read_file(fs::path("input.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_file__missing_file); +ATF_TEST_CASE_BODY(read_file__missing_file) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, + "Failed to open 'foo.txt' for read", + utils::read_file(fs::path("foo.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_stream__empty); +ATF_TEST_CASE_BODY(read_stream__empty) +{ + std::istringstream input(""); + ATF_REQUIRE_EQ("", utils::read_stream(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_stream__some); +ATF_TEST_CASE_BODY(read_stream__some) +{ + std::string contents; + for (int i = 0; i < 1000; i++) + contents += "abcdef"; + std::istringstream input(contents); + ATF_REQUIRE_EQ(contents, utils::read_stream(input)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, open_ostream__stdout); + ATF_ADD_TEST_CASE(tcs, open_ostream__stderr); + ATF_ADD_TEST_CASE(tcs, open_ostream__other); + + ATF_ADD_TEST_CASE(tcs, stream_length__empty); + ATF_ADD_TEST_CASE(tcs, stream_length__some); + + ATF_ADD_TEST_CASE(tcs, read_file__ok); + ATF_ADD_TEST_CASE(tcs, read_file__missing_file); + + ATF_ADD_TEST_CASE(tcs, read_stream__empty); + ATF_ADD_TEST_CASE(tcs, read_stream__some); +} diff --git a/utils/test_utils.ipp b/utils/test_utils.ipp new file mode 100644 index 000000000000..f21d0f4cc172 --- /dev/null +++ b/utils/test_utils.ipp @@ -0,0 +1,113 @@ +// Copyright 2016 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/test_utils.ipp +/// Provides test-only convenience utilities. + +#if defined(UTILS_TEST_UTILS_IPP) +# error "utils/test_utils.hpp can only be included once" +#endif +#define UTILS_TEST_UTILS_IPP + +extern "C" { +#include <sys/resource.h> +} + +#include <cstdlib> +#include <iostream> + +#include <atf-c++.hpp> + +#include "utils/defs.hpp" +#include "utils/stacktrace.hpp" +#include "utils/text/operations.ipp" + +namespace utils { + + +/// Tries to prevent dumping core if we do not need one on a crash. +/// +/// This is a best-effort operation provided so that tests that will cause +/// a crash do not collect an unnecessary core dump, which can be slow on +/// some systems (e.g. on macOS). +inline void +avoid_coredump_on_crash(void) +{ + struct ::rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = 0; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) { + std::cerr << "Failed to zero core size limit; may dump core\n"; + } +} + + +inline void abort_without_coredump(void) UTILS_NORETURN; + + +/// Aborts execution and tries to not dump core. +/// +/// The coredump avoidance is a best-effort operation provided so that tests +/// that will cause a crash do not collect an unnecessary core dump, which can +/// be slow on some systems (e.g. on macOS). +inline void +abort_without_coredump(void) +{ + avoid_coredump_on_crash(); + std::abort(); +} + + +/// Skips the test if coredump tests have been disabled by the user. +/// +/// \param tc The calling test. +inline void +require_run_coredump_tests(const atf::tests::tc* tc) +{ + if (tc->has_config_var("run_coredump_tests") && + !text::to_type< bool >(tc->get_config_var("run_coredump_tests"))) { + tc->skip("run_coredump_tests=false; not running test"); + } +} + + +/// Prepares the test so that it can dump core, or skips it otherwise. +/// +/// \param tc The calling test. +inline void +prepare_coredump_test(const atf::tests::tc* tc) +{ + require_run_coredump_tests(tc); + + if (!unlimit_core_size()) { + tc->skip("Cannot unlimit the core file size; check limits manually"); + } +} + + +} // namespace utils diff --git a/utils/text/Kyuafile b/utils/text/Kyuafile new file mode 100644 index 000000000000..e4e870e9c648 --- /dev/null +++ b/utils/text/Kyuafile @@ -0,0 +1,9 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="exceptions_test"} +atf_test_program{name="operations_test"} +atf_test_program{name="regex_test"} +atf_test_program{name="table_test"} +atf_test_program{name="templates_test"} diff --git a/utils/text/Makefile.am.inc b/utils/text/Makefile.am.inc new file mode 100644 index 000000000000..d474ae191bf5 --- /dev/null +++ b/utils/text/Makefile.am.inc @@ -0,0 +1,74 @@ +# Copyright 2012 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. + +libutils_a_SOURCES += utils/text/exceptions.cpp +libutils_a_SOURCES += utils/text/exceptions.hpp +libutils_a_SOURCES += utils/text/operations.cpp +libutils_a_SOURCES += utils/text/operations.hpp +libutils_a_SOURCES += utils/text/operations.ipp +libutils_a_SOURCES += utils/text/regex.cpp +libutils_a_SOURCES += utils/text/regex.hpp +libutils_a_SOURCES += utils/text/regex_fwd.hpp +libutils_a_SOURCES += utils/text/table.cpp +libutils_a_SOURCES += utils/text/table.hpp +libutils_a_SOURCES += utils/text/table_fwd.hpp +libutils_a_SOURCES += utils/text/templates.cpp +libutils_a_SOURCES += utils/text/templates.hpp +libutils_a_SOURCES += utils/text/templates_fwd.hpp + +if WITH_ATF +tests_utils_textdir = $(pkgtestsdir)/utils/text + +tests_utils_text_DATA = utils/text/Kyuafile +EXTRA_DIST += $(tests_utils_text_DATA) + +tests_utils_text_PROGRAMS = utils/text/exceptions_test +utils_text_exceptions_test_SOURCES = utils/text/exceptions_test.cpp +utils_text_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/operations_test +utils_text_operations_test_SOURCES = utils/text/operations_test.cpp +utils_text_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/regex_test +utils_text_regex_test_SOURCES = utils/text/regex_test.cpp +utils_text_regex_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_regex_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/table_test +utils_text_table_test_SOURCES = utils/text/table_test.cpp +utils_text_table_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_table_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/templates_test +utils_text_templates_test_SOURCES = utils/text/templates_test.cpp +utils_text_templates_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_templates_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/text/exceptions.cpp b/utils/text/exceptions.cpp new file mode 100644 index 000000000000..1692cfea7edb --- /dev/null +++ b/utils/text/exceptions.cpp @@ -0,0 +1,91 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +text::error::~error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::regex_error::regex_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +text::regex_error::~regex_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::syntax_error::syntax_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +text::syntax_error::~syntax_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::value_error::value_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +text::value_error::~value_error(void) throw() +{ +} diff --git a/utils/text/exceptions.hpp b/utils/text/exceptions.hpp new file mode 100644 index 000000000000..da0cfd98fb88 --- /dev/null +++ b/utils/text/exceptions.hpp @@ -0,0 +1,77 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/exceptions.hpp +/// Exception types raised by the text module. + +#if !defined(UTILS_TEXT_EXCEPTIONS_HPP) +#define UTILS_TEXT_EXCEPTIONS_HPP + +#include <stdexcept> + +namespace utils { +namespace text { + + +/// Base exceptions for text errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Exception denoting an error in a regular expression. +class regex_error : public error { +public: + explicit regex_error(const std::string&); + ~regex_error(void) throw(); +}; + + +/// Exception denoting an error while parsing templates. +class syntax_error : public error { +public: + explicit syntax_error(const std::string&); + ~syntax_error(void) throw(); +}; + + +/// Exception denoting an error in a text value format. +class value_error : public error { +public: + explicit value_error(const std::string&); + ~value_error(void) throw(); +}; + + +} // namespace text +} // namespace utils + + +#endif // !defined(UTILS_TEXT_EXCEPTIONS_HPP) diff --git a/utils/text/exceptions_test.cpp b/utils/text/exceptions_test.cpp new file mode 100644 index 000000000000..1d3c3910900a --- /dev/null +++ b/utils/text/exceptions_test.cpp @@ -0,0 +1,76 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/exceptions.hpp" + +#include <cstring> + +#include <atf-c++.hpp> + +namespace text = utils::text; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const text::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(regex_error); +ATF_TEST_CASE_BODY(regex_error) +{ + const text::regex_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_error); +ATF_TEST_CASE_BODY(syntax_error) +{ + const text::syntax_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(value_error); +ATF_TEST_CASE_BODY(value_error) +{ + const text::value_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, regex_error); + ATF_ADD_TEST_CASE(tcs, syntax_error); + ATF_ADD_TEST_CASE(tcs, value_error); +} diff --git a/utils/text/operations.cpp b/utils/text/operations.cpp new file mode 100644 index 000000000000..5a4345d979c7 --- /dev/null +++ b/utils/text/operations.cpp @@ -0,0 +1,261 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/operations.ipp" + +#include <sstream> + +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace text = utils::text; + + +/// Replaces XML special characters from an input string. +/// +/// The list of XML special characters is specified here: +/// http://www.w3.org/TR/xml11/#charsets +/// +/// \param in The input to quote. +/// +/// \return A quoted string without any XML special characters. +std::string +text::escape_xml(const std::string& in) +{ + std::ostringstream quoted; + + for (std::string::const_iterator it = in.begin(); + it != in.end(); ++it) { + unsigned char c = (unsigned char)*it; + if (c == '"') { + quoted << """; + } else if (c == '&') { + quoted << "&"; + } else if (c == '<') { + quoted << "<"; + } else if (c == '>') { + quoted << ">"; + } else if (c == '\'') { + quoted << "'"; + } else if ((c >= 0x01 && c <= 0x08) || + (c >= 0x0B && c <= 0x0C) || + (c >= 0x0E && c <= 0x1F) || + (c >= 0x7F && c <= 0x84) || + (c >= 0x86 && c <= 0x9F)) { + // for RestrictedChar characters, escape them + // as '&#[decimal ASCII value];' + // so that in the XML file we will see the escaped + // character. + quoted << "&#" << static_cast< std::string::size_type >(*it) + << ";"; + } else { + quoted << *it; + } + } + return quoted.str(); +} + + +/// Surrounds a string with quotes, escaping the quote itself if needed. +/// +/// \param text The string to quote. +/// \param quote The quote character to use. +/// +/// \return The quoted string. +std::string +text::quote(const std::string& text, const char quote) +{ + std::ostringstream quoted; + quoted << quote; + + std::string::size_type start_pos = 0; + std::string::size_type last_pos = text.find(quote); + while (last_pos != std::string::npos) { + quoted << text.substr(start_pos, last_pos - start_pos) << '\\'; + start_pos = last_pos; + last_pos = text.find(quote, start_pos + 1); + } + quoted << text.substr(start_pos); + + quoted << quote; + return quoted.str(); +} + + +/// Fills a paragraph to the specified length. +/// +/// This preserves any sequence of spaces in the input and any possible +/// newlines. Sequences of spaces may be split in half (and thus one space is +/// lost), but the rest of the spaces will be preserved as either trailing or +/// leading spaces. +/// +/// \param input The string to refill. +/// \param target_width The width to refill the paragraph to. +/// +/// \return The refilled paragraph as a sequence of independent lines. +std::vector< std::string > +text::refill(const std::string& input, const std::size_t target_width) +{ + std::vector< std::string > output; + + std::string::size_type start = 0; + while (start < input.length()) { + std::string::size_type width; + if (start + target_width >= input.length()) + width = input.length() - start; + else { + if (input[start + target_width] == ' ') { + width = target_width; + } else { + const std::string::size_type pos = input.find_last_of( + " ", start + target_width - 1); + if (pos == std::string::npos || pos < start + 1) { + width = input.find_first_of(" ", start + target_width); + if (width == std::string::npos) + width = input.length() - start; + else + width -= start; + } else { + width = pos - start; + } + } + } + INV(width != std::string::npos); + INV(start + width <= input.length()); + INV(input[start + width] == ' ' || input[start + width] == '\0'); + output.push_back(input.substr(start, width)); + + start += width + 1; + } + + if (input.empty()) { + INV(output.empty()); + output.push_back(""); + } + + return output; +} + + +/// Fills a paragraph to the specified length. +/// +/// See the documentation for refill() for additional details. +/// +/// \param input The string to refill. +/// \param target_width The width to refill the paragraph to. +/// +/// \return The refilled paragraph as a string with embedded newlines. +std::string +text::refill_as_string(const std::string& input, const std::size_t target_width) +{ + return join(refill(input, target_width), "\n"); +} + + +/// Replaces all occurrences of a substring in a string. +/// +/// \param input The string in which to perform the replacement. +/// \param search The pattern to be replaced. +/// \param replacement The substring to replace search with. +/// +/// \return A copy of input with the replacements performed. +std::string +text::replace_all(const std::string& input, const std::string& search, + const std::string& replacement) +{ + std::string output; + + std::string::size_type pos, lastpos = 0; + while ((pos = input.find(search, lastpos)) != std::string::npos) { + output += input.substr(lastpos, pos - lastpos); + output += replacement; + lastpos = pos + search.length(); + } + output += input.substr(lastpos); + + return output; +} + + +/// Splits a string into different components. +/// +/// \param str The string to split. +/// \param delimiter The separator to use to split the words. +/// +/// \return The different words in the input string as split by the provided +/// delimiter. +std::vector< std::string > +text::split(const std::string& str, const char delimiter) +{ + std::vector< std::string > words; + if (!str.empty()) { + std::string::size_type pos = str.find(delimiter); + words.push_back(str.substr(0, pos)); + while (pos != std::string::npos) { + ++pos; + const std::string::size_type next = str.find(delimiter, pos); + words.push_back(str.substr(pos, next - pos)); + pos = next; + } + } + return words; +} + + +/// Converts a string to a boolean. +/// +/// \param str The string to convert. +/// +/// \return The converted string, if the input string was valid. +/// +/// \throw std::value_error If the input string does not represent a valid +/// boolean value. +template<> +bool +text::to_type(const std::string& str) +{ + if (str == "true") + return true; + else if (str == "false") + return false; + else + throw value_error(F("Invalid boolean value '%s'") % str); +} + + +/// Identity function for to_type, for genericity purposes. +/// +/// \param str The string to convert. +/// +/// \return The input string. +template<> +std::string +text::to_type(const std::string& str) +{ + return str; +} diff --git a/utils/text/operations.hpp b/utils/text/operations.hpp new file mode 100644 index 000000000000..6d15be553b06 --- /dev/null +++ b/utils/text/operations.hpp @@ -0,0 +1,68 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/operations.hpp +/// Utilities to manipulate strings. + +#if !defined(UTILS_TEXT_OPERATIONS_HPP) +#define UTILS_TEXT_OPERATIONS_HPP + +#include <cstddef> +#include <string> +#include <vector> + +namespace utils { +namespace text { + + +std::string escape_xml(const std::string&); +std::string quote(const std::string&, const char); + + +std::vector< std::string > refill(const std::string&, const std::size_t); +std::string refill_as_string(const std::string&, const std::size_t); + +std::string replace_all(const std::string&, const std::string&, + const std::string&); + +template< typename Collection > +std::string join(const Collection&, const std::string&); +std::vector< std::string > split(const std::string&, const char); + +template< typename Type > +Type to_type(const std::string&); +template<> +bool to_type(const std::string&); +template<> +std::string to_type(const std::string&); + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_OPERATIONS_HPP) diff --git a/utils/text/operations.ipp b/utils/text/operations.ipp new file mode 100644 index 000000000000..511cd6840a08 --- /dev/null +++ b/utils/text/operations.ipp @@ -0,0 +1,91 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +#if !defined(UTILS_TEXT_OPERATIONS_IPP) +#define UTILS_TEXT_OPERATIONS_IPP + +#include "utils/text/operations.hpp" + +#include <sstream> + +#include "utils/text/exceptions.hpp" + + +/// Concatenates a collection of strings into a single string. +/// +/// \param strings The collection of strings to concatenate. If the collection +/// is unordered, the ordering in the output is undefined. +/// \param delimiter The delimiter to use to separate the strings. +/// +/// \return The concatenated strings. +template< typename Collection > +std::string +utils::text::join(const Collection& strings, const std::string& delimiter) +{ + std::ostringstream output; + if (strings.size() > 1) { + for (typename Collection::const_iterator iter = strings.begin(); + iter != --strings.end(); ++iter) + output << (*iter) << delimiter; + } + if (strings.size() > 0) + output << *(--strings.end()); + return output.str(); +} + + +/// Converts a string to a native type. +/// +/// \tparam Type The type to convert the string to. An input stream operator +/// must exist to extract such a type from an std::istream. +/// \param str The string to convert. +/// +/// \return The converted string, if the input string was valid. +/// +/// \throw std::value_error If the input string does not represent a valid +/// target type. This exception does not include any details, so the caller +/// must take care to re-raise it with appropriate details. +template< typename Type > +Type +utils::text::to_type(const std::string& str) +{ + if (str.empty()) + throw text::value_error("Empty string"); + if (str[0] == ' ') + throw text::value_error("Invalid value"); + + std::istringstream input(str); + Type value; + input >> value; + if (!input.eof() || input.bad() || input.fail()) + throw text::value_error("Invalid value"); + return value; +} + + +#endif // !defined(UTILS_TEXT_OPERATIONS_IPP) diff --git a/utils/text/operations_test.cpp b/utils/text/operations_test.cpp new file mode 100644 index 000000000000..2d5ab36c9090 --- /dev/null +++ b/utils/text/operations_test.cpp @@ -0,0 +1,435 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/operations.ipp" + +#include <iostream> +#include <set> +#include <string> +#include <vector> + +#include <atf-c++.hpp> + +#include "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +namespace { + + +/// Tests text::refill() on an input string with a range of widths. +/// +/// \param expected The expected refilled paragraph. +/// \param input The input paragraph to be refilled. +/// \param first_width The first width to validate. +/// \param last_width The last width to validate (inclusive). +static void +refill_test(const char* expected, const char* input, + const std::size_t first_width, const std::size_t last_width) +{ + for (std::size_t width = first_width; width <= last_width; ++width) { + const std::vector< std::string > lines = text::split(expected, '\n'); + std::cout << "Breaking at width " << width << '\n'; + ATF_REQUIRE_EQ(expected, text::refill_as_string(input, width)); + ATF_REQUIRE(lines == text::refill(input, width)); + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(escape_xml__empty); +ATF_TEST_CASE_BODY(escape_xml__empty) +{ + ATF_REQUIRE_EQ("", text::escape_xml("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(escape_xml__no_escaping); +ATF_TEST_CASE_BODY(escape_xml__no_escaping) +{ + ATF_REQUIRE_EQ("a", text::escape_xml("a")); + ATF_REQUIRE_EQ("Some text!", text::escape_xml("Some text!")); + ATF_REQUIRE_EQ("\n\t\r", text::escape_xml("\n\t\r")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(escape_xml__some_escaping); +ATF_TEST_CASE_BODY(escape_xml__some_escaping) +{ + ATF_REQUIRE_EQ("'", text::escape_xml("'")); + + ATF_REQUIRE_EQ("foo "bar& <tag> yay' baz", + text::escape_xml("foo \"bar& <tag> yay' baz")); + + ATF_REQUIRE_EQ(""&<>'", text::escape_xml("\"&<>'")); + ATF_REQUIRE_EQ("&&&", text::escape_xml("&&&")); + ATF_REQUIRE_EQ("&#8;&#11;", text::escape_xml("\b\v")); + ATF_REQUIRE_EQ("\t&#127;BAR&", text::escape_xml("\t\x7f""BAR&")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(quote__empty); +ATF_TEST_CASE_BODY(quote__empty) +{ + ATF_REQUIRE_EQ("''", text::quote("", '\'')); + ATF_REQUIRE_EQ("##", text::quote("", '#')); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(quote__no_escaping); +ATF_TEST_CASE_BODY(quote__no_escaping) +{ + ATF_REQUIRE_EQ("'Some text\"'", text::quote("Some text\"", '\'')); + ATF_REQUIRE_EQ("#Another'string#", text::quote("Another'string", '#')); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(quote__some_escaping); +ATF_TEST_CASE_BODY(quote__some_escaping) +{ + ATF_REQUIRE_EQ("'Some\\'text'", text::quote("Some'text", '\'')); + ATF_REQUIRE_EQ("#Some\\#text#", text::quote("Some#text", '#')); + + ATF_REQUIRE_EQ("'More than one\\' quote\\''", + text::quote("More than one' quote'", '\'')); + ATF_REQUIRE_EQ("'Multiple quotes \\'\\'\\' together'", + text::quote("Multiple quotes ''' together", '\'')); + + ATF_REQUIRE_EQ("'\\'escape at the beginning'", + text::quote("'escape at the beginning", '\'')); + ATF_REQUIRE_EQ("'escape at the end\\''", + text::quote("escape at the end'", '\'')); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__empty); +ATF_TEST_CASE_BODY(refill__empty) +{ + ATF_REQUIRE_EQ(1, text::refill("", 0).size()); + ATF_REQUIRE(text::refill("", 0)[0].empty()); + ATF_REQUIRE_EQ("", text::refill_as_string("", 0)); + + ATF_REQUIRE_EQ(1, text::refill("", 10).size()); + ATF_REQUIRE(text::refill("", 10)[0].empty()); + ATF_REQUIRE_EQ("", text::refill_as_string("", 10)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__no_changes); +ATF_TEST_CASE_BODY(refill__no_changes) +{ + std::vector< std::string > exp_lines; + exp_lines.push_back("foo bar\nbaz"); + + ATF_REQUIRE(exp_lines == text::refill("foo bar\nbaz", 12)); + ATF_REQUIRE_EQ("foo bar\nbaz", text::refill_as_string("foo bar\nbaz", 12)); + + ATF_REQUIRE(exp_lines == text::refill("foo bar\nbaz", 18)); + ATF_REQUIRE_EQ("foo bar\nbaz", text::refill_as_string("foo bar\nbaz", 80)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__break_one); +ATF_TEST_CASE_BODY(refill__break_one) +{ + refill_test("only break the\nfirst line", "only break the first line", + 14, 19); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__break_one__not_first_word); +ATF_TEST_CASE_BODY(refill__break_one__not_first_word) +{ + refill_test("first-long-word\nother\nwords", "first-long-word other words", + 6, 10); + refill_test("first-long-word\nother words", "first-long-word other words", + 11, 20); + refill_test("first-long-word other\nwords", "first-long-word other words", + 21, 26); + refill_test("first-long-word other words", "first-long-word other words", + 27, 28); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__break_many); +ATF_TEST_CASE_BODY(refill__break_many) +{ + refill_test("this is a long\nparagraph to be\nsplit into\npieces", + "this is a long paragraph to be split into pieces", + 15, 15); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__cannot_break); +ATF_TEST_CASE_BODY(refill__cannot_break) +{ + refill_test("this-is-a-long-string", "this-is-a-long-string", 5, 5); + + refill_test("this is\na-string-with-long-words", + "this is a-string-with-long-words", 10, 10); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__preserve_whitespace); +ATF_TEST_CASE_BODY(refill__preserve_whitespace) +{ + refill_test("foo bar baz ", "foo bar baz ", 80, 80); + refill_test("foo \n bar", "foo bar", 5, 5); + + std::vector< std::string > exp_lines; + exp_lines.push_back("foo \n"); + exp_lines.push_back(" bar"); + ATF_REQUIRE(exp_lines == text::refill("foo \n bar", 5)); + ATF_REQUIRE_EQ("foo \n\n bar", text::refill_as_string("foo \n bar", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__empty); +ATF_TEST_CASE_BODY(join__empty) +{ + std::vector< std::string > lines; + ATF_REQUIRE_EQ("", text::join(lines, " ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__one); +ATF_TEST_CASE_BODY(join__one) +{ + std::vector< std::string > lines; + lines.push_back("first line"); + ATF_REQUIRE_EQ("first line", text::join(lines, "*")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__several); +ATF_TEST_CASE_BODY(join__several) +{ + std::vector< std::string > lines; + lines.push_back("first abc"); + lines.push_back("second"); + lines.push_back("and last line"); + ATF_REQUIRE_EQ("first abc second and last line", text::join(lines, " ")); + ATF_REQUIRE_EQ("first abc***second***and last line", + text::join(lines, "***")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__unordered); +ATF_TEST_CASE_BODY(join__unordered) +{ + std::set< std::string > lines; + lines.insert("first"); + lines.insert("second"); + const std::string joined = text::join(lines, " "); + ATF_REQUIRE(joined == "first second" || joined == "second first"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__empty); +ATF_TEST_CASE_BODY(split__empty) +{ + std::vector< std::string > words = text::split("", ' '); + std::vector< std::string > exp_words; + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__one); +ATF_TEST_CASE_BODY(split__one) +{ + std::vector< std::string > words = text::split("foo", ' '); + std::vector< std::string > exp_words; + exp_words.push_back("foo"); + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__several__simple); +ATF_TEST_CASE_BODY(split__several__simple) +{ + std::vector< std::string > words = text::split("foo bar baz", ' '); + std::vector< std::string > exp_words; + exp_words.push_back("foo"); + exp_words.push_back("bar"); + exp_words.push_back("baz"); + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__several__delimiters); +ATF_TEST_CASE_BODY(split__several__delimiters) +{ + std::vector< std::string > words = text::split("XfooXXbarXXXbazXX", 'X'); + std::vector< std::string > exp_words; + exp_words.push_back(""); + exp_words.push_back("foo"); + exp_words.push_back(""); + exp_words.push_back("bar"); + exp_words.push_back(""); + exp_words.push_back(""); + exp_words.push_back("baz"); + exp_words.push_back(""); + exp_words.push_back(""); + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__empty); +ATF_TEST_CASE_BODY(replace_all__empty) +{ + ATF_REQUIRE_EQ("", text::replace_all("", "search", "replacement")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__none); +ATF_TEST_CASE_BODY(replace_all__none) +{ + ATF_REQUIRE_EQ("string without matches", + text::replace_all("string without matches", + "WITHOUT", "replacement")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__one); +ATF_TEST_CASE_BODY(replace_all__one) +{ + ATF_REQUIRE_EQ("string replacement matches", + text::replace_all("string without matches", + "without", "replacement")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__several); +ATF_TEST_CASE_BODY(replace_all__several) +{ + ATF_REQUIRE_EQ("OO fOO bar OOf baz OO", + text::replace_all("oo foo bar oof baz oo", + "oo", "OO")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__ok__bool); +ATF_TEST_CASE_BODY(to_type__ok__bool) +{ + ATF_REQUIRE( text::to_type< bool >("true")); + ATF_REQUIRE(!text::to_type< bool >("false")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__ok__numerical); +ATF_TEST_CASE_BODY(to_type__ok__numerical) +{ + ATF_REQUIRE_EQ(12, text::to_type< int >("12")); + ATF_REQUIRE_EQ(18745, text::to_type< int >("18745")); + ATF_REQUIRE_EQ(-12345, text::to_type< int >("-12345")); + + ATF_REQUIRE_EQ(12.0, text::to_type< double >("12")); + ATF_REQUIRE_EQ(12.5, text::to_type< double >("12.5")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__ok__string); +ATF_TEST_CASE_BODY(to_type__ok__string) +{ + // While this seems redundant, having this particular specialization that + // does nothing allows callers to delegate work to to_type without worrying + // about the particular type being converted. + ATF_REQUIRE_EQ("", text::to_type< std::string >("")); + ATF_REQUIRE_EQ(" abcd ", text::to_type< std::string >(" abcd ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__empty); +ATF_TEST_CASE_BODY(to_type__empty) +{ + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__invalid__bool); +ATF_TEST_CASE_BODY(to_type__invalid__bool) +{ + ATF_REQUIRE_THROW(text::value_error, text::to_type< bool >("")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< bool >("true ")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< bool >("foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__invalid__numerical); +ATF_TEST_CASE_BODY(to_type__invalid__numerical) +{ + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >(" 3")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("3 ")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("3a")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("a3")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, escape_xml__empty); + ATF_ADD_TEST_CASE(tcs, escape_xml__no_escaping); + ATF_ADD_TEST_CASE(tcs, escape_xml__some_escaping); + + ATF_ADD_TEST_CASE(tcs, quote__empty); + ATF_ADD_TEST_CASE(tcs, quote__no_escaping); + ATF_ADD_TEST_CASE(tcs, quote__some_escaping); + + ATF_ADD_TEST_CASE(tcs, refill__empty); + ATF_ADD_TEST_CASE(tcs, refill__no_changes); + ATF_ADD_TEST_CASE(tcs, refill__break_one); + ATF_ADD_TEST_CASE(tcs, refill__break_one__not_first_word); + ATF_ADD_TEST_CASE(tcs, refill__break_many); + ATF_ADD_TEST_CASE(tcs, refill__cannot_break); + ATF_ADD_TEST_CASE(tcs, refill__preserve_whitespace); + + ATF_ADD_TEST_CASE(tcs, join__empty); + ATF_ADD_TEST_CASE(tcs, join__one); + ATF_ADD_TEST_CASE(tcs, join__several); + ATF_ADD_TEST_CASE(tcs, join__unordered); + + ATF_ADD_TEST_CASE(tcs, split__empty); + ATF_ADD_TEST_CASE(tcs, split__one); + ATF_ADD_TEST_CASE(tcs, split__several__simple); + ATF_ADD_TEST_CASE(tcs, split__several__delimiters); + + ATF_ADD_TEST_CASE(tcs, replace_all__empty); + ATF_ADD_TEST_CASE(tcs, replace_all__none); + ATF_ADD_TEST_CASE(tcs, replace_all__one); + ATF_ADD_TEST_CASE(tcs, replace_all__several); + + ATF_ADD_TEST_CASE(tcs, to_type__ok__bool); + ATF_ADD_TEST_CASE(tcs, to_type__ok__numerical); + ATF_ADD_TEST_CASE(tcs, to_type__ok__string); + ATF_ADD_TEST_CASE(tcs, to_type__empty); + ATF_ADD_TEST_CASE(tcs, to_type__invalid__bool); + ATF_ADD_TEST_CASE(tcs, to_type__invalid__numerical); +} diff --git a/utils/text/regex.cpp b/utils/text/regex.cpp new file mode 100644 index 000000000000..b078ba88f6b4 --- /dev/null +++ b/utils/text/regex.cpp @@ -0,0 +1,302 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/regex.hpp" + +extern "C" { +#include <sys/types.h> + +#include <regex.h> +} + +#include "utils/auto_array.ipp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +namespace { + + +static void throw_regex_error(const int, const ::regex_t*, const std::string&) + UTILS_NORETURN; + + +/// Constructs and raises a regex_error. +/// +/// \param error The error code returned by regcomp(3) or regexec(3). +/// \param preg The native regex object that caused this error. +/// \param prefix Error message prefix string. +/// +/// \throw regex_error The constructed exception. +static void +throw_regex_error(const int error, const ::regex_t* preg, + const std::string& prefix) +{ + char buffer[1024]; + + // TODO(jmmv): Would be nice to handle the case where the message does + // not fit in the temporary buffer. + (void)::regerror(error, preg, buffer, sizeof(buffer)); + + throw text::regex_error(F("%s: %s") % prefix % buffer); +} + + +} // anonymous namespace + + +/// Internal implementation for regex_matches. +struct utils::text::regex_matches::impl : utils::noncopyable { + /// String on which we are matching. + /// + /// In theory, we could take a reference here instead of a copy, and make + /// it a requirement for the caller to ensure that the lifecycle of the + /// input string outlasts the lifecycle of the regex_matches. However, that + /// contract is very easy to break with hardcoded strings (as we do in + /// tests). Just go for the safer case here. + const std::string _string; + + /// Maximum number of matching groups we expect, including the full match. + /// + /// In other words, this is the size of the _matches array. + const std::size_t _nmatches; + + /// Native regular expression match representation. + utils::auto_array< ::regmatch_t > _matches; + + /// Constructor. + /// + /// This executes the regex on the given string and sets up the internal + /// class state based on the results. + /// + /// \param preg The native regex object. + /// \param str The string on which to execute the regex. + /// \param ngroups Number of capture groups in the regex. This is an upper + /// bound and may be greater than the actual matches. + /// + /// \throw regex_error If the call to regexec(3) fails. + impl(const ::regex_t* preg, const std::string& str, + const std::size_t ngroups) : + _string(str), + _nmatches(ngroups + 1), + _matches(new ::regmatch_t[_nmatches]) + { + const int error = ::regexec(preg, _string.c_str(), _nmatches, + _matches.get(), 0); + if (error == REG_NOMATCH) { + _matches.reset(NULL); + } else if (error != 0) { + throw_regex_error(error, preg, + F("regexec on '%s' failed") % _string); + } + } + + /// Destructor. + ~impl(void) + { + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed implementation of the object. +text::regex_matches::regex_matches(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +text::regex_matches::~regex_matches(void) +{ +} + + +/// Returns the number of matches in this object. +/// +/// Note that this does not correspond to the number of groups provided at +/// construction time. The returned value here accounts for only the returned +/// valid matches. +/// +/// \return Number of matches, including the full match. +std::size_t +text::regex_matches::count(void) const +{ + std::size_t total = 0; + if (_pimpl->_matches.get() != NULL) { + for (std::size_t i = 0; i < _pimpl->_nmatches; ++i) { + if (_pimpl->_matches[i].rm_so != -1) + ++total; + } + INV(total <= _pimpl->_nmatches); + } + return total; +} + + +/// Gets a match. +/// +/// \param index Number of the match to get. Index 0 always contains the match +/// of the whole regex. +/// +/// \pre There regex must have matched the input string. +/// \pre index must be lower than count(). +/// +/// \return The textual match. +std::string +text::regex_matches::get(const std::size_t index) const +{ + PRE(*this); + PRE(index < count()); + + const ::regmatch_t* match = &_pimpl->_matches[index]; + + return std::string(_pimpl->_string.c_str() + match->rm_so, + match->rm_eo - match->rm_so); +} + + +/// Checks if there are any matches. +/// +/// \return True if the object contains one or more matches; false otherwise. +text::regex_matches::operator bool(void) const +{ + return _pimpl->_matches.get() != NULL; +} + + +/// Internal implementation for regex. +struct utils::text::regex::impl : utils::noncopyable { + /// Native regular expression representation. + ::regex_t _preg; + + /// Number of capture groups in the regular expression. This is an upper + /// bound and does NOT include the default full string match. + std::size_t _ngroups; + + /// Constructor. + /// + /// This compiles the given regular expression. + /// + /// \param regex_ The regular expression to compile. + /// \param ngroups Number of capture groups in the regular expression. This + /// is an upper bound and does NOT include the default full string + /// match. + /// \param ignore_case Whether to ignore case during matching. + /// + /// \throw regex_error If the call to regcomp(3) fails. + impl(const std::string& regex_, const std::size_t ngroups, + const bool ignore_case) : + _ngroups(ngroups) + { + const int flags = REG_EXTENDED | (ignore_case ? REG_ICASE : 0); + const int error = ::regcomp(&_preg, regex_.c_str(), flags); + if (error != 0) + throw_regex_error(error, &_preg, F("regcomp on '%s' failed") + % regex_); + } + + /// Destructor. + ~impl(void) + { + ::regfree(&_preg); + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed implementation of the object. +text::regex::regex(std::shared_ptr< impl > pimpl) : _pimpl(pimpl) +{ +} + + +/// Destructor. +text::regex::~regex(void) +{ +} + + +/// Compiles a new regular expression. +/// +/// \param regex_ The regular expression to compile. +/// \param ngroups Number of capture groups in the regular expression. This is +/// an upper bound and does NOT include the default full string match. +/// \param ignore_case Whether to ignore case during matching. +/// +/// \return A new regular expression, ready to match strings. +/// +/// \throw regex_error If the regular expression is invalid and cannot be +/// compiled. +text::regex +text::regex::compile(const std::string& regex_, const std::size_t ngroups, + const bool ignore_case) +{ + return regex(std::shared_ptr< impl >(new impl(regex_, ngroups, + ignore_case))); +} + + +/// Matches the regular expression against a string. +/// +/// \param str String to match the regular expression against. +/// +/// \return A new regex_matches object with the results of the match. +text::regex_matches +text::regex::match(const std::string& str) const +{ + std::shared_ptr< regex_matches::impl > pimpl(new regex_matches::impl( + &_pimpl->_preg, str, _pimpl->_ngroups)); + return regex_matches(pimpl); +} + + +/// Compiles and matches a regular expression once. +/// +/// This is syntactic sugar to simplify the instantiation of a new regex object +/// and its subsequent match on a string. +/// +/// \param regex_ The regular expression to compile and match. +/// \param str String to match the regular expression against. +/// \param ngroups Number of capture groups in the regular expression. +/// \param ignore_case Whether to ignore case during matching. +/// +/// \return A new regex_matches object with the results of the match. +text::regex_matches +text::match_regex(const std::string& regex_, const std::string& str, + const std::size_t ngroups, const bool ignore_case) +{ + return regex::compile(regex_, ngroups, ignore_case).match(str); +} diff --git a/utils/text/regex.hpp b/utils/text/regex.hpp new file mode 100644 index 000000000000..b3d20c246735 --- /dev/null +++ b/utils/text/regex.hpp @@ -0,0 +1,92 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/regex.hpp +/// Utilities to build and match regular expressions. + +#if !defined(UTILS_TEXT_REGEX_HPP) +#define UTILS_TEXT_REGEX_HPP + +#include "utils/text/regex_fwd.hpp" + +#include <cstddef> +#include <memory> + + +namespace utils { +namespace text { + + +/// Container for regex match results. +class regex_matches { + struct impl; + + /// Pointer to shared implementation. + std::shared_ptr< impl > _pimpl; + + friend class regex; + regex_matches(std::shared_ptr< impl >); + +public: + ~regex_matches(void); + + std::size_t count(void) const; + std::string get(const std::size_t) const; + + operator bool(void) const; +}; + + +/// Regular expression compiler and executor. +/// +/// All regular expressions handled by this class are "extended". +class regex { + struct impl; + + /// Pointer to shared implementation. + std::shared_ptr< impl > _pimpl; + + regex(std::shared_ptr< impl >); + +public: + ~regex(void); + + static regex compile(const std::string&, const std::size_t, + const bool = false); + regex_matches match(const std::string&) const; +}; + + +regex_matches match_regex(const std::string&, const std::string&, + const std::size_t, const bool = false); + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_REGEX_HPP) diff --git a/utils/text/regex_fwd.hpp b/utils/text/regex_fwd.hpp new file mode 100644 index 000000000000..e9010324c10d --- /dev/null +++ b/utils/text/regex_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/regex_fwd.hpp +/// Forward declarations for utils/text/regex.hpp + +#if !defined(UTILS_TEXT_REGEX_FWD_HPP) +#define UTILS_TEXT_REGEX_FWD_HPP + +namespace utils { +namespace text { + + +class regex_matches; +class regex; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_REGEX_FWD_HPP) diff --git a/utils/text/regex_test.cpp b/utils/text/regex_test.cpp new file mode 100644 index 000000000000..7ea5ee485aad --- /dev/null +++ b/utils/text/regex_test.cpp @@ -0,0 +1,177 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/regex.hpp" + +#include <atf-c++.hpp> + +#include "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__no_matches); +ATF_TEST_CASE_BODY(integration__no_matches) +{ + const text::regex_matches matches = text::match_regex( + "foo.*bar", "this is a string without the searched text", 0); + ATF_REQUIRE(!matches); + ATF_REQUIRE_EQ(0, matches.count()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__no_capture_groups); +ATF_TEST_CASE_BODY(integration__no_capture_groups) +{ + const text::regex_matches matches = text::match_regex( + "foo.*bar", "this is a string with foo and bar embedded in it", 0); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(1, matches.count()); + ATF_REQUIRE_EQ("foo and bar", matches.get(0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__one_capture_group); +ATF_TEST_CASE_BODY(integration__one_capture_group) +{ + const text::regex_matches matches = text::match_regex( + "^([^ ]*) ", "the string", 1); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("the ", matches.get(0)); + ATF_REQUIRE_EQ("the", matches.get(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__many_capture_groups); +ATF_TEST_CASE_BODY(integration__many_capture_groups) +{ + const text::regex_matches matches = text::match_regex( + "is ([^ ]*) ([a-z]*) to", "this is another string to parse", 2); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(3, matches.count()); + ATF_REQUIRE_EQ("is another string to", matches.get(0)); + ATF_REQUIRE_EQ("another", matches.get(1)); + ATF_REQUIRE_EQ("string", matches.get(2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__capture_groups_underspecified); +ATF_TEST_CASE_BODY(integration__capture_groups_underspecified) +{ + const text::regex_matches matches = text::match_regex( + "is ([^ ]*) ([a-z]*) to", "this is another string to parse", 1); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("is another string to", matches.get(0)); + ATF_REQUIRE_EQ("another", matches.get(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__capture_groups_overspecified); +ATF_TEST_CASE_BODY(integration__capture_groups_overspecified) +{ + const text::regex_matches matches = text::match_regex( + "is ([^ ]*) ([a-z]*) to", "this is another string to parse", 10); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(3, matches.count()); + ATF_REQUIRE_EQ("is another string to", matches.get(0)); + ATF_REQUIRE_EQ("another", matches.get(1)); + ATF_REQUIRE_EQ("string", matches.get(2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__reuse_regex_in_multiple_matches); +ATF_TEST_CASE_BODY(integration__reuse_regex_in_multiple_matches) +{ + const text::regex regex = text::regex::compile("number is ([0-9]+)", 1); + + { + const text::regex_matches matches = regex.match("my number is 581."); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("number is 581", matches.get(0)); + ATF_REQUIRE_EQ("581", matches.get(1)); + } + + { + const text::regex_matches matches = regex.match("your number is 6"); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("number is 6", matches.get(0)); + ATF_REQUIRE_EQ("6", matches.get(1)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__ignore_case); +ATF_TEST_CASE_BODY(integration__ignore_case) +{ + const text::regex regex1 = text::regex::compile("foo", 0, false); + ATF_REQUIRE(!regex1.match("bar Foo bar")); + ATF_REQUIRE(!regex1.match("bar foO bar")); + ATF_REQUIRE(!regex1.match("bar FOO bar")); + + ATF_REQUIRE(!text::match_regex("foo", "bar Foo bar", 0, false)); + ATF_REQUIRE(!text::match_regex("foo", "bar foO bar", 0, false)); + ATF_REQUIRE(!text::match_regex("foo", "bar FOO bar", 0, false)); + + const text::regex regex2 = text::regex::compile("foo", 0, true); + ATF_REQUIRE( regex2.match("bar foo bar")); + ATF_REQUIRE( regex2.match("bar Foo bar")); + ATF_REQUIRE( regex2.match("bar foO bar")); + ATF_REQUIRE( regex2.match("bar FOO bar")); + + ATF_REQUIRE( text::match_regex("foo", "bar foo bar", 0, true)); + ATF_REQUIRE( text::match_regex("foo", "bar Foo bar", 0, true)); + ATF_REQUIRE( text::match_regex("foo", "bar foO bar", 0, true)); + ATF_REQUIRE( text::match_regex("foo", "bar FOO bar", 0, true)); +} + +ATF_TEST_CASE_WITHOUT_HEAD(integration__invalid_regex); +ATF_TEST_CASE_BODY(integration__invalid_regex) +{ + ATF_REQUIRE_THROW(text::regex_error, + text::regex::compile("this is (unbalanced", 0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + // regex and regex_matches are so coupled that it makes no sense to test + // them independently. Just validate their integration. + ATF_ADD_TEST_CASE(tcs, integration__no_matches); + ATF_ADD_TEST_CASE(tcs, integration__no_capture_groups); + ATF_ADD_TEST_CASE(tcs, integration__one_capture_group); + ATF_ADD_TEST_CASE(tcs, integration__many_capture_groups); + ATF_ADD_TEST_CASE(tcs, integration__capture_groups_underspecified); + ATF_ADD_TEST_CASE(tcs, integration__capture_groups_overspecified); + ATF_ADD_TEST_CASE(tcs, integration__reuse_regex_in_multiple_matches); + ATF_ADD_TEST_CASE(tcs, integration__ignore_case); + ATF_ADD_TEST_CASE(tcs, integration__invalid_regex); +} diff --git a/utils/text/table.cpp b/utils/text/table.cpp new file mode 100644 index 000000000000..4a2c72f8053f --- /dev/null +++ b/utils/text/table.cpp @@ -0,0 +1,428 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/table.hpp" + +#include <algorithm> +#include <iterator> +#include <limits> +#include <sstream> + +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +namespace { + + +/// Applies user overrides to the column widths of a table. +/// +/// \param table The table from which to calculate the column widths. +/// \param user_widths The column widths provided by the user. This vector must +/// have less or the same number of elements as the columns of the table. +/// Values of width_auto are ignored; any other explicit values are copied +/// to the output widths vector, including width_refill. +/// +/// \return A vector with the widths of the columns of the input table with any +/// user overrides applied. +static text::widths_vector +override_column_widths(const text::table& table, + const text::widths_vector& user_widths) +{ + PRE(user_widths.size() <= table.ncolumns()); + text::widths_vector widths = table.column_widths(); + + // Override the actual width of the columns based on user-specified widths. + for (text::widths_vector::size_type i = 0; i < user_widths.size(); ++i) { + const text::widths_vector::value_type& user_width = user_widths[i]; + if (user_width != text::table_formatter::width_auto) { + PRE_MSG(user_width == text::table_formatter::width_refill || + user_width >= widths[i], + "User-provided column widths must be larger than the " + "column contents (except for the width_refill column)"); + widths[i] = user_width; + } + } + + return widths; +} + + +/// Locates the refill column, if any. +/// +/// \param widths The widths of the columns as returned by +/// override_column_widths(). Note that one of the columns may or may not +/// be width_refill, which is the column we are looking for. +/// +/// \return The index of the refill column with a width_refill width if any, or +/// otherwise the index of the last column (which is the default refill column). +static text::widths_vector::size_type +find_refill_column(const text::widths_vector& widths) +{ + text::widths_vector::size_type i = 0; + for (; i < widths.size(); ++i) { + if (widths[i] == text::table_formatter::width_refill) + return i; + } + return i - 1; +} + + +/// Pads the widths of the table to fit within a maximum width. +/// +/// On output, a column of the widths vector is truncated to a shorter length +/// than its current value, if the total width of the table would exceed the +/// maximum table width. +/// +/// \param [in,out] widths The widths of the columns as returned by +/// override_column_widths(). One of these columns should have a value of +/// width_refill; if not, a default column is refilled. +/// \param user_max_width The target width of the table; must not be zero. +/// \param column_padding The padding between the cells, if any. The target +/// width should be larger than the padding times the number of columns; if +/// that is not the case, we attempt a readjustment here. +static void +refill_widths(text::widths_vector& widths, + const text::widths_vector::value_type user_max_width, + const std::size_t column_padding) +{ + PRE(user_max_width != 0); + + // widths.size() is a proxy for the number of columns of the table. + const std::size_t total_padding = column_padding * (widths.size() - 1); + const text::widths_vector::value_type max_width = std::max( + user_max_width, total_padding) - total_padding; + + const text::widths_vector::size_type refill_column = + find_refill_column(widths); + INV(refill_column < widths.size()); + + text::widths_vector::value_type width = 0; + for (text::widths_vector::size_type i = 0; i < widths.size(); ++i) { + if (i != refill_column) + width += widths[i]; + } + widths[refill_column] = max_width - width; +} + + +/// Pads an input text to a specified width with spaces. +/// +/// \param input The text to add padding to (may be empty). +/// \param length The desired length of the output. +/// \param is_last Whether the text being processed belongs to the last column +/// of a row or not. Values in the last column should not be padded to +/// prevent trailing whitespace on the screen (which affects copy/pasting +/// for example). +/// +/// \return The padded cell. If the input string is longer than the desired +/// length, the input string is returned verbatim. The padded table won't be +/// correct, but we don't expect this to be a common case to worry about. +static std::string +pad_cell(const std::string& input, const std::size_t length, const bool is_last) +{ + if (is_last) + return input; + else { + if (input.length() < length) + return input + std::string(length - input.length(), ' '); + else + return input; + } +} + + +/// Refills a cell and adds it to the output lines. +/// +/// \param row The row containing the cell to be refilled. +/// \param widths The widths of the row. +/// \param column The column being refilled. +/// \param [in,out] textual_rows The output lines as processed so far. This is +/// updated to accomodate for the contents of the refilled cell, extending +/// the rows as necessary. +static void +refill_cell(const text::table_row& row, const text::widths_vector& widths, + const text::table_row::size_type column, + std::vector< text::table_row >& textual_rows) +{ + const std::vector< std::string > rows = text::refill(row[column], + widths[column]); + + if (textual_rows.size() < rows.size()) + textual_rows.resize(rows.size(), text::table_row(row.size())); + + for (std::vector< std::string >::size_type i = 0; i < rows.size(); ++i) { + for (text::table_row::size_type j = 0; j < row.size(); ++j) { + const bool is_last = j == row.size() - 1; + if (j == column) + textual_rows[i][j] = pad_cell(rows[i], widths[j], is_last); + else { + if (textual_rows[i][j].empty()) + textual_rows[i][j] = pad_cell("", widths[j], is_last); + } + } + } +} + + +/// Formats a single table row. +/// +/// \param row The row to format. +/// \param widths The widths of the columns to apply during formatting. Cells +/// wider than the specified width are refilled to attempt to fit in the +/// cell. Cells narrower than the width are right-padded with spaces. +/// \param separator The column separator to use. +/// +/// \return The textual lines that contain the formatted row. +static std::vector< std::string > +format_row(const text::table_row& row, const text::widths_vector& widths, + const std::string& separator) +{ + PRE(row.size() == widths.size()); + + std::vector< text::table_row > textual_rows(1, text::table_row(row.size())); + + for (text::table_row::size_type column = 0; column < row.size(); ++column) { + if (widths[column] > row[column].length()) + textual_rows[0][column] = pad_cell(row[column], widths[column], + column == row.size() - 1); + else + refill_cell(row, widths, column, textual_rows); + } + + std::vector< std::string > lines; + for (std::vector< text::table_row >::const_iterator + iter = textual_rows.begin(); iter != textual_rows.end(); ++iter) { + lines.push_back(text::join(*iter, separator)); + } + return lines; +} + + +} // anonymous namespace + + +/// Constructs a new table. +/// +/// \param ncolumns_ The number of columns that the table will have. +text::table::table(const table_row::size_type ncolumns_) +{ + _column_widths.resize(ncolumns_, 0); +} + + +/// Gets the number of columns in the table. +/// +/// \return The number of columns in the table. This value remains constant +/// during the existence of the table. +text::widths_vector::size_type +text::table::ncolumns(void) const +{ + return _column_widths.size(); +} + + +/// Gets the width of a column. +/// +/// The returned value is not valid if add_row() is called again, as the column +/// may have grown in width. +/// +/// \param column The index of the column of which to get the width. Must be +/// less than the total number of columns. +/// +/// \return The width of a column. +text::widths_vector::value_type +text::table::column_width(const widths_vector::size_type column) const +{ + PRE(column < _column_widths.size()); + return _column_widths[column]; +} + + +/// Gets the widths of all columns. +/// +/// The returned value is not valid if add_row() is called again, as the columns +/// may have grown in width. +/// +/// \return A vector with the width of all columns. +const text::widths_vector& +text::table::column_widths(void) const +{ + return _column_widths; +} + + +/// Checks whether the table is empty or not. +/// +/// \return True if the table is empty; false otherwise. +bool +text::table::empty(void) const +{ + return _rows.empty(); +} + + +/// Adds a row to the table. +/// +/// \param row The row to be added. This row must have the same amount of +/// columns as defined during the construction of the table. +void +text::table::add_row(const table_row& row) +{ + PRE(row.size() == _column_widths.size()); + _rows.push_back(row); + + for (table_row::size_type i = 0; i < row.size(); ++i) + if (_column_widths[i] < row[i].length()) + _column_widths[i] = row[i].length(); +} + + +/// Gets an iterator pointing to the beginning of the rows of the table. +/// +/// \return An iterator on the rows. +text::table::const_iterator +text::table::begin(void) const +{ + return _rows.begin(); +} + + +/// Gets an iterator pointing to the end of the rows of the table. +/// +/// \return An iterator on the rows. +text::table::const_iterator +text::table::end(void) const +{ + return _rows.end(); +} + + +/// Column width to denote that the column has to fit all of its cells. +const std::size_t text::table_formatter::width_auto = 0; + + +/// Column width to denote that the column can be refilled to fit the table. +const std::size_t text::table_formatter::width_refill = + std::numeric_limits< std::size_t >::max(); + + +/// Constructs a new table formatter. +text::table_formatter::table_formatter(void) : + _separator(""), + _table_width(0) +{ +} + + +/// Sets the width of a column. +/// +/// All columns except one must have a width that is, at least, as wide as the +/// widest cell in the column. One of the columns can have a width of +/// width_refill, which indicates that the column will be refilled if the table +/// does not fit in its maximum width. +/// +/// \param column The index of the column to set the width for. +/// \param width The width to set the column to. +/// +/// \return A reference to this formatter to allow using the builder pattern. +text::table_formatter& +text::table_formatter::set_column_width(const table_row::size_type column, + const std::size_t width) +{ +#if !defined(NDEBUG) + if (width == width_refill) { + for (widths_vector::size_type i = 0; i < _column_widths.size(); i++) { + if (i != column) + PRE_MSG(_column_widths[i] != width_refill, + "Only one column width can be set to width_refill"); + } + } +#endif + + if (_column_widths.size() < column + 1) + _column_widths.resize(column + 1, width_auto); + _column_widths[column] = width; + return *this; +} + + +/// Sets the separator to use between the cells. +/// +/// \param separator The separator to use. +/// +/// \return A reference to this formatter to allow using the builder pattern. +text::table_formatter& +text::table_formatter::set_separator(const char* separator) +{ + _separator = separator; + return *this; +} + + +/// Sets the maximum width of the table. +/// +/// \param table_width The maximum width of the table; cannot be zero. +/// +/// \return A reference to this formatter to allow using the builder pattern. +text::table_formatter& +text::table_formatter::set_table_width(const std::size_t table_width) +{ + PRE(table_width > 0); + _table_width = table_width; + return *this; +} + + +/// Formats a table into a collection of textual lines. +/// +/// \param t Table to format. +/// +/// \return A collection of textual lines. +std::vector< std::string > +text::table_formatter::format(const table& t) const +{ + std::vector< std::string > lines; + + if (!t.empty()) { + widths_vector widths = override_column_widths(t, _column_widths); + if (_table_width != 0) + refill_widths(widths, _table_width, _separator.length()); + + for (table::const_iterator iter = t.begin(); iter != t.end(); ++iter) { + const std::vector< std::string > sublines = + format_row(*iter, widths, _separator); + std::copy(sublines.begin(), sublines.end(), + std::back_inserter(lines)); + } + } + + return lines; +} diff --git a/utils/text/table.hpp b/utils/text/table.hpp new file mode 100644 index 000000000000..5fd7c50c991c --- /dev/null +++ b/utils/text/table.hpp @@ -0,0 +1,125 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/table.hpp +/// Table construction and formatting. + +#if !defined(UTILS_TEXT_TABLE_HPP) +#define UTILS_TEXT_TABLE_HPP + +#include "utils/text/table_fwd.hpp" + +#include <cstddef> +#include <string> +#include <vector> + +namespace utils { +namespace text { + + +/// Representation of a table. +/// +/// A table is nothing more than a matrix of rows by columns. The number of +/// columns is hardcoded at construction times, and the rows can be accumulated +/// at a later stage. +/// +/// The only value of this class is a simpler and more natural mechanism of the +/// construction of a table, with additional sanity checks. We could as well +/// just expose the internal data representation to our users. +class table { + /// Widths of the table columns so far. + widths_vector _column_widths; + + /// Type defining the collection of rows in the table. + typedef std::vector< table_row > rows_vector; + + /// The rows of the table. + /// + /// This is actually the matrix representing the table. Every element of + /// this vector (which are vectors themselves) must have _ncolumns items. + rows_vector _rows; + +public: + table(const table_row::size_type); + + widths_vector::size_type ncolumns(void) const; + widths_vector::value_type column_width(const widths_vector::size_type) + const; + const widths_vector& column_widths(void) const; + + void add_row(const table_row&); + + bool empty(void) const; + + /// Constant iterator on the rows of the table. + typedef rows_vector::const_iterator const_iterator; + + const_iterator begin(void) const; + const_iterator end(void) const; +}; + + +/// Settings to format a table. +/// +/// This class implements a builder pattern to construct an object that contains +/// all the knowledge to format a table. Once all the settings have been set, +/// the format() method provides the algorithm to apply such formatting settings +/// to any input table. +class table_formatter { + /// Text to use as the separator between cells. + std::string _separator; + + /// Colletion of widths of the columns of a table. + std::size_t _table_width; + + /// Widths of the table columns. + /// + /// Note that this only includes widths for the column widths explicitly + /// overriden by the caller. In other words, this vector can be shorter + /// than the table passed to the format() method, which is just fine. Any + /// non-specified column widths are assumed to be width_auto. + widths_vector _column_widths; + +public: + table_formatter(void); + + static const std::size_t width_auto; + static const std::size_t width_refill; + table_formatter& set_column_width(const table_row::size_type, + const std::size_t); + table_formatter& set_separator(const char*); + table_formatter& set_table_width(const std::size_t); + + std::vector< std::string > format(const table&) const; +}; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TABLE_HPP) diff --git a/utils/text/table_fwd.hpp b/utils/text/table_fwd.hpp new file mode 100644 index 000000000000..77c6b1fa8c78 --- /dev/null +++ b/utils/text/table_fwd.hpp @@ -0,0 +1,58 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/table_fwd.hpp +/// Forward declarations for utils/text/table.hpp + +#if !defined(UTILS_TEXT_TABLE_FWD_HPP) +#define UTILS_TEXT_TABLE_FWD_HPP + +#include <cstddef> +#include <string> +#include <vector> + +namespace utils { +namespace text { + + +/// Values of the cells of a particular table row. +typedef std::vector< std::string > table_row; + + +/// Vector of column widths. +typedef std::vector< std::size_t > widths_vector; + + +class table; +class table_formatter; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TABLE_FWD_HPP) diff --git a/utils/text/table_test.cpp b/utils/text/table_test.cpp new file mode 100644 index 000000000000..45928dae89c4 --- /dev/null +++ b/utils/text/table_test.cpp @@ -0,0 +1,413 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/table.hpp" + +#include <algorithm> + +#include <atf-c++.hpp> + +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +/// Performs a check on text::table_formatter. +/// +/// This is provided for test simplicity's sake. Having to match the result of +/// the formatting on a line by line basis would result in too verbose tests +/// (maybe not with C++11, but not using this yet). +/// +/// Because of the flattening of the formatted table into a string, we risk +/// misdetecting problems when the algorithm bundles newlines into the lines of +/// a table. This should not happen, and not accounting for this little detail +/// makes testing so much easier. +/// +/// \param expected Textual representation of the table, as a collection of +/// lines separated by newline characters. +/// \param formatter The formatter to use. +/// \param table The table to format. +static void +table_formatter_check(const std::string& expected, + const text::table_formatter& formatter, + const text::table& table) +{ + ATF_REQUIRE_EQ(expected, text::join(formatter.format(table), "\n") + "\n"); +} + + + +ATF_TEST_CASE_WITHOUT_HEAD(table__ncolumns); +ATF_TEST_CASE_BODY(table__ncolumns) +{ + ATF_REQUIRE_EQ(5, text::table(5).ncolumns()); + ATF_REQUIRE_EQ(10, text::table(10).ncolumns()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__column_width); +ATF_TEST_CASE_BODY(table__column_width) +{ + text::table_row row1; + row1.push_back("1234"); + row1.push_back("123456"); + text::table_row row2; + row2.push_back("12"); + row2.push_back("12345678"); + + text::table table(2); + table.add_row(row1); + table.add_row(row2); + + ATF_REQUIRE_EQ(4, table.column_width(0)); + ATF_REQUIRE_EQ(8, table.column_width(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__column_widths); +ATF_TEST_CASE_BODY(table__column_widths) +{ + text::table_row row1; + row1.push_back("1234"); + row1.push_back("123456"); + text::table_row row2; + row2.push_back("12"); + row2.push_back("12345678"); + + text::table table(2); + table.add_row(row1); + table.add_row(row2); + + ATF_REQUIRE_EQ(4, table.column_widths()[0]); + ATF_REQUIRE_EQ(8, table.column_widths()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__empty); +ATF_TEST_CASE_BODY(table__empty) +{ + text::table table(2); + ATF_REQUIRE(table.empty()); + table.add_row(text::table_row(2)); + ATF_REQUIRE(!table.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__iterate); +ATF_TEST_CASE_BODY(table__iterate) +{ + text::table_row row1; + row1.push_back("foo"); + text::table_row row2; + row2.push_back("bar"); + + text::table table(1); + table.add_row(row1); + table.add_row(row2); + + text::table::const_iterator iter = table.begin(); + ATF_REQUIRE(iter != table.end()); + ATF_REQUIRE(row1 == *iter); + ++iter; + ATF_REQUIRE(iter != table.end()); + ATF_REQUIRE(row2 == *iter); + ++iter; + ATF_REQUIRE(iter == table.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__empty); +ATF_TEST_CASE_BODY(table_formatter__empty) +{ + ATF_REQUIRE(text::table_formatter().set_separator(" ") + .format(text::table(1)).empty()); + ATF_REQUIRE(text::table_formatter().set_separator(" ") + .format(text::table(10)).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__defaults); +ATF_TEST_CASE_BODY(table_formatter__defaults) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First Second Third\n" + "Fourth with some textFifth with some more textSixth foo\n", + text::table_formatter(), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__one_column__no_max_width); +ATF_TEST_CASE_BODY(table_formatter__one_column__no_max_width) +{ + text::table table(1); + { + text::table_row row; + row.push_back("First row with some words"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Second row with some words"); + table.add_row(row); + } + + table_formatter_check( + "First row with some words\n" + "Second row with some words\n", + text::table_formatter().set_separator(" | "), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__one_column__explicit_width); +ATF_TEST_CASE_BODY(table_formatter__one_column__explicit_width) +{ + text::table table(1); + { + text::table_row row; + row.push_back("First row with some words"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Second row with some words"); + table.add_row(row); + } + + table_formatter_check( + "First row with some words\n" + "Second row with some words\n", + text::table_formatter().set_separator(" | ").set_column_width(0, 1024), + table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__one_column__max_width); +ATF_TEST_CASE_BODY(table_formatter__one_column__max_width) +{ + text::table table(1); + { + text::table_row row; + row.push_back("First row with some words"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Second row with some words"); + table.add_row(row); + } + + table_formatter_check( + "First row\nwith some\nwords\n" + "Second row\nwith some\nwords\n", + text::table_formatter().set_separator(" | ").set_table_width(11), + table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__many_columns__no_max_width); +ATF_TEST_CASE_BODY(table_formatter__many_columns__no_max_width) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with some more text | Sixth foo\n", + text::table_formatter().set_separator(" | "), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__many_columns__explicit_width); +ATF_TEST_CASE_BODY(table_formatter__many_columns__explicit_width) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with some more text | Sixth foo\n", + text::table_formatter().set_separator(" | ").set_column_width(0, 23) + .set_column_width(1, 28), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__many_columns__max_width); +ATF_TEST_CASE_BODY(table_formatter__many_columns__max_width) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with | Sixth foo\n" + " | some more | \n" + " | text | \n", + text::table_formatter().set_separator(" | ").set_table_width(46) + .set_column_width(1, text::table_formatter::width_refill) + .set_column_width(0, text::table_formatter::width_auto), table); + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with | Sixth foo\n" + " | some more | \n" + " | text | \n", + text::table_formatter().set_separator(" | ").set_table_width(48) + .set_column_width(1, text::table_formatter::width_refill) + .set_column_width(0, 23), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__use_case__cli_help); +ATF_TEST_CASE_BODY(table_formatter__use_case__cli_help) +{ + text::table options_table(2); + { + text::table_row row; + row.push_back("-a a_value"); + row.push_back("This is the description of the first flag"); + options_table.add_row(row); + } + { + text::table_row row; + row.push_back("-b"); + row.push_back("And this is the text for the second flag"); + options_table.add_row(row); + } + + text::table commands_table(2); + { + text::table_row row; + row.push_back("first"); + row.push_back("This is the first command"); + commands_table.add_row(row); + } + { + text::table_row row; + row.push_back("second"); + row.push_back("And this is the second command"); + commands_table.add_row(row); + } + + const text::widths_vector::value_type first_width = + std::max(options_table.column_width(0), commands_table.column_width(0)); + + table_formatter_check( + "-a a_value This is the description\n" + " of the first flag\n" + "-b And this is the text for\n" + " the second flag\n", + text::table_formatter().set_separator(" ").set_table_width(36) + .set_column_width(0, first_width) + .set_column_width(1, text::table_formatter::width_refill), + options_table); + + table_formatter_check( + "first This is the first\n" + " command\n" + "second And this is the second\n" + " command\n", + text::table_formatter().set_separator(" ").set_table_width(36) + .set_column_width(0, first_width) + .set_column_width(1, text::table_formatter::width_refill), + commands_table); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, table__ncolumns); + ATF_ADD_TEST_CASE(tcs, table__column_width); + ATF_ADD_TEST_CASE(tcs, table__column_widths); + ATF_ADD_TEST_CASE(tcs, table__empty); + ATF_ADD_TEST_CASE(tcs, table__iterate); + + ATF_ADD_TEST_CASE(tcs, table_formatter__empty); + ATF_ADD_TEST_CASE(tcs, table_formatter__defaults); + ATF_ADD_TEST_CASE(tcs, table_formatter__one_column__no_max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__one_column__explicit_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__one_column__max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__many_columns__no_max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__many_columns__explicit_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__many_columns__max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__use_case__cli_help); +} diff --git a/utils/text/templates.cpp b/utils/text/templates.cpp new file mode 100644 index 000000000000..13cb27b1cce2 --- /dev/null +++ b/utils/text/templates.cpp @@ -0,0 +1,764 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/templates.hpp" + +#include <algorithm> +#include <fstream> +#include <sstream> +#include <stack> + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +namespace { + + +/// Definition of a template statement. +/// +/// A template statement is a particular line in the input file that is +/// preceeded by a template marker. This class provides a high-level +/// representation of the contents of such statement and a mechanism to parse +/// the textual line into this high-level representation. +class statement_def { +public: + /// Types of the known statements. + enum statement_type { + /// Alternative clause of a conditional. + /// + /// Takes no arguments. + type_else, + + /// End of conditional marker. + /// + /// Takes no arguments. + type_endif, + + /// End of loop marker. + /// + /// Takes no arguments. + type_endloop, + + /// Beginning of a conditional. + /// + /// Takes a single argument, which denotes the name of the variable or + /// vector to check for existence. This is the only expression + /// supported. + type_if, + + /// Beginning of a loop over all the elements of a vector. + /// + /// Takes two arguments: the name of the vector over which to iterate + /// and the name of the iterator to later index this vector. + type_loop, + }; + +private: + /// Internal data describing the structure of a particular statement type. + struct type_descriptor { + /// The native type of the statement. + statement_type type; + + /// The expected number of arguments. + unsigned int n_arguments; + + /// Constructs a new type descriptor. + /// + /// \param type_ The native type of the statement. + /// \param n_arguments_ The expected number of arguments. + type_descriptor(const statement_type type_, + const unsigned int n_arguments_) + : type(type_), n_arguments(n_arguments_) + { + } + }; + + /// Mapping of statement type names to their definitions. + typedef std::map< std::string, type_descriptor > types_map; + + /// Description of the different statement types. + /// + /// This static map is initialized once and reused later for any statement + /// lookup. Unfortunately, we cannot perform this initialization in a + /// static manner without C++11. + static types_map _types; + + /// Generates a new types definition map. + /// + /// \return A new types definition map, to be assigned to _types. + static types_map + generate_types_map(void) + { + // If you change this, please edit the comments in the enum above. + types_map types; + types.insert(types_map::value_type( + "else", type_descriptor(type_else, 0))); + types.insert(types_map::value_type( + "endif", type_descriptor(type_endif, 0))); + types.insert(types_map::value_type( + "endloop", type_descriptor(type_endloop, 0))); + types.insert(types_map::value_type( + "if", type_descriptor(type_if, 1))); + types.insert(types_map::value_type( + "loop", type_descriptor(type_loop, 2))); + return types; + } + +public: + /// The type of the statement. + statement_type type; + + /// The arguments to the statement, in textual form. + const std::vector< std::string > arguments; + + /// Creates a new statement. + /// + /// \param type_ The type of the statement. + /// \param arguments_ The arguments to the statement. + statement_def(const statement_type& type_, + const std::vector< std::string >& arguments_) : + type(type_), arguments(arguments_) + { +#if !defined(NDEBUG) + for (types_map::const_iterator iter = _types.begin(); + iter != _types.end(); ++iter) { + const type_descriptor& descriptor = (*iter).second; + if (descriptor.type == type_) { + PRE(descriptor.n_arguments == arguments_.size()); + return; + } + } + UNREACHABLE; +#endif + } + + /// Parses a statement. + /// + /// \param line The textual representation of the statement without any + /// prefix. + /// + /// \return The parsed statement. + /// + /// \throw text::syntax_error If the statement is not correctly defined. + static statement_def + parse(const std::string& line) + { + if (_types.empty()) + _types = generate_types_map(); + + const std::vector< std::string > words = text::split(line, ' '); + if (words.empty()) + throw text::syntax_error("Empty statement"); + + const types_map::const_iterator iter = _types.find(words[0]); + if (iter == _types.end()) + throw text::syntax_error(F("Unknown statement '%s'") % words[0]); + const type_descriptor& descriptor = (*iter).second; + + if (words.size() - 1 != descriptor.n_arguments) + throw text::syntax_error(F("Invalid number of arguments for " + "statement '%s'") % words[0]); + + std::vector< std::string > new_arguments; + new_arguments.resize(words.size() - 1); + std::copy(words.begin() + 1, words.end(), new_arguments.begin()); + + return statement_def(descriptor.type, new_arguments); + } +}; + + +statement_def::types_map statement_def::_types; + + +/// Definition of a loop. +/// +/// This simple structure is used to keep track of the parameters of a loop. +struct loop_def { + /// The name of the vector over which this loop is iterating. + std::string vector; + + /// The name of the iterator defined by this loop. + std::string iterator; + + /// Position in the input to which to rewind to on looping. + /// + /// This position points to the line after the loop statement, not the loop + /// itself. This is one of the reasons why we have this structure, so that + /// we can maintain the data about the loop without having to re-process it. + std::istream::pos_type position; + + /// Constructs a new loop definition. + /// + /// \param vector_ The name of the vector (first argument). + /// \param iterator_ The name of the iterator (second argumnet). + /// \param position_ Position of the next line after the loop statement. + loop_def(const std::string& vector_, const std::string& iterator_, + const std::istream::pos_type position_) : + vector(vector_), iterator(iterator_), position(position_) + { + } +}; + + +/// Stateful class to instantiate the templates in an input stream. +/// +/// The goal of this parser is to scan the input once and not buffer anything in +/// memory. The only exception are loops: loops are reinterpreted on every +/// iteration from the same input file by rewidining the stream to the +/// appropriate position. +class templates_parser : utils::noncopyable { + /// The templates to apply. + /// + /// Note that this is not const because the parser has to have write access + /// to the templates. In particular, it needs to be able to define the + /// iterators as regular variables. + text::templates_def _templates; + + /// Prefix that marks a line as a statement. + const std::string _prefix; + + /// Delimiter to surround an expression instantiation. + const std::string _delimiter; + + /// Whether to skip incoming lines or not. + /// + /// The top of the stack is true whenever we encounter a conditional that + /// evaluates to false or a loop that does not have any iterations left. + /// Under these circumstances, we need to continue scanning the input stream + /// until we find the matching closing endif or endloop construct. + /// + /// This is a stack rather than a plain boolean to allow us deal with + /// if-else clauses. + std::stack< bool > _skip; + + /// Current count of nested conditionals. + unsigned int _if_level; + + /// Level of the top-most conditional that evaluated to false. + unsigned int _exit_if_level; + + /// Current count of nested loops. + unsigned int _loop_level; + + /// Level of the top-most loop that does not have any iterations left. + unsigned int _exit_loop_level; + + /// Information about all the nested loops up to the current point. + std::stack< loop_def > _loops; + + /// Checks if a line is a statement or not. + /// + /// \param line The line to validate. + /// + /// \return True if the line looks like a statement, which is determined by + /// checking if the line starts by the predefined prefix. + bool + is_statement(const std::string& line) + { + return ((line.length() >= _prefix.length() && + line.substr(0, _prefix.length()) == _prefix) && + (line.length() < _delimiter.length() || + line.substr(0, _delimiter.length()) != _delimiter)); + } + + /// Parses a given statement line into a statement definition. + /// + /// \param line The line to validate; it must be a valid statement. + /// + /// \return The parsed statement. + /// + /// \throw text::syntax_error If the input is not a valid statement. + statement_def + parse_statement(const std::string& line) + { + PRE(is_statement(line)); + return statement_def::parse(line.substr(_prefix.length())); + } + + /// Processes a line from the input when not in skip mode. + /// + /// \param line The line to be processed. + /// \param input The input stream from which the line was read. The current + /// position in the stream must be after the line being processed. + /// \param output The output stream into which to write the results. + /// + /// \throw text::syntax_error If the input is not valid. + void + handle_normal(const std::string& line, std::istream& input, + std::ostream& output) + { + if (!is_statement(line)) { + // Fast path. Mostly to avoid an indentation level for the big + // chunk of code below. + output << line << '\n'; + return; + } + + const statement_def statement = parse_statement(line); + + switch (statement.type) { + case statement_def::type_else: + _skip.top() = !_skip.top(); + break; + + case statement_def::type_endif: + _if_level--; + break; + + case statement_def::type_endloop: { + PRE(_loops.size() == _loop_level); + loop_def& loop = _loops.top(); + + const std::size_t next_index = 1 + text::to_type< std::size_t >( + _templates.get_variable(loop.iterator)); + + if (next_index < _templates.get_vector(loop.vector).size()) { + _templates.add_variable(loop.iterator, F("%s") % next_index); + input.seekg(loop.position); + } else { + _loop_level--; + _loops.pop(); + _templates.remove_variable(loop.iterator); + } + } break; + + case statement_def::type_if: { + _if_level++; + const std::string value = _templates.evaluate( + statement.arguments[0]); + if (value.empty() || value == "0" || value == "false") { + _exit_if_level = _if_level; + _skip.push(true); + } else { + _skip.push(false); + } + } break; + + case statement_def::type_loop: { + _loop_level++; + + const loop_def loop(statement.arguments[0], statement.arguments[1], + input.tellg()); + if (_templates.get_vector(loop.vector).empty()) { + _exit_loop_level = _loop_level; + _skip.push(true); + } else { + _templates.add_variable(loop.iterator, "0"); + _loops.push(loop); + _skip.push(false); + } + } break; + } + } + + /// Processes a line from the input when in skip mode. + /// + /// \param line The line to be processed. + /// + /// \throw text::syntax_error If the input is not valid. + void + handle_skip(const std::string& line) + { + PRE(_skip.top()); + + if (!is_statement(line)) + return; + + const statement_def statement = parse_statement(line); + switch (statement.type) { + case statement_def::type_else: + if (_exit_if_level == _if_level) + _skip.top() = !_skip.top(); + break; + + case statement_def::type_endif: + INV(_if_level >= _exit_if_level); + if (_if_level == _exit_if_level) + _skip.top() = false; + _if_level--; + _skip.pop(); + break; + + case statement_def::type_endloop: + INV(_loop_level >= _exit_loop_level); + if (_loop_level == _exit_loop_level) + _skip.top() = false; + _loop_level--; + _skip.pop(); + break; + + case statement_def::type_if: + _if_level++; + _skip.push(true); + break; + + case statement_def::type_loop: + _loop_level++; + _skip.push(true); + break; + + default: + break; + } + } + + /// Evaluates expressions on a given input line. + /// + /// An expression is surrounded by _delimiter on both sides. We scan the + /// string from left to right finding any expressions that may appear, yank + /// them out and call templates_def::evaluate() to get their value. + /// + /// Lonely or unbalanced appearances of _delimiter on the input line are + /// not considered an error, given that the user may actually want to supply + /// that character sequence without being interpreted as a template. + /// + /// \param in_line The input line from which to evaluate expressions. + /// + /// \return The evaluated line. + /// + /// \throw text::syntax_error If the expressions in the line are malformed. + std::string + evaluate(const std::string& in_line) + { + std::string out_line; + + std::string::size_type last_pos = 0; + while (last_pos != std::string::npos) { + const std::string::size_type open_pos = in_line.find( + _delimiter, last_pos); + if (open_pos == std::string::npos) { + out_line += in_line.substr(last_pos); + last_pos = std::string::npos; + } else { + const std::string::size_type close_pos = in_line.find( + _delimiter, open_pos + _delimiter.length()); + if (close_pos == std::string::npos) { + out_line += in_line.substr(last_pos); + last_pos = std::string::npos; + } else { + out_line += in_line.substr(last_pos, open_pos - last_pos); + out_line += _templates.evaluate(in_line.substr( + open_pos + _delimiter.length(), + close_pos - open_pos - _delimiter.length())); + last_pos = close_pos + _delimiter.length(); + } + } + } + + return out_line; + } + +public: + /// Constructs a new template parser. + /// + /// \param templates_ The templates to apply to the processed file. + /// \param prefix_ The prefix that identifies lines as statements. + /// \param delimiter_ Delimiter to surround a variable instantiation. + templates_parser(const text::templates_def& templates_, + const std::string& prefix_, + const std::string& delimiter_) : + _templates(templates_), + _prefix(prefix_), + _delimiter(delimiter_), + _if_level(0), + _exit_if_level(0), + _loop_level(0), + _exit_loop_level(0) + { + } + + /// Applies the templates to a given input. + /// + /// \param input The stream to which to apply the templates. + /// \param output The stream into which to write the results. + /// + /// \throw text::syntax_error If the input is not valid. Note that the + /// is not guaranteed to be unmodified on exit if an error is + /// encountered. + void + instantiate(std::istream& input, std::ostream& output) + { + std::string line; + while (std::getline(input, line).good()) { + if (!_skip.empty() && _skip.top()) + handle_skip(line); + else + handle_normal(evaluate(line), input, output); + } + } +}; + + +} // anonymous namespace + + +/// Constructs an empty templates definition. +text::templates_def::templates_def(void) +{ +} + + +/// Sets a string variable in the templates. +/// +/// If the variable already exists, its value is replaced. This behavior is +/// required to implement iterators, but client code should really not be +/// redefining variables. +/// +/// \pre The variable must not already exist as a vector. +/// +/// \param name The name of the variable to set. +/// \param value The value to set the given variable to. +void +text::templates_def::add_variable(const std::string& name, + const std::string& value) +{ + PRE(_vectors.find(name) == _vectors.end()); + _variables[name] = value; +} + + +/// Unsets a string variable from the templates. +/// +/// Client code has no reason to use this. This is only required to implement +/// proper scoping of loop iterators. +/// +/// \pre The variable must exist. +/// +/// \param name The name of the variable to remove from the templates. +void +text::templates_def::remove_variable(const std::string& name) +{ + PRE(_variables.find(name) != _variables.end()); + _variables.erase(_variables.find(name)); +} + + +/// Creates a new vector in the templates. +/// +/// If the vector already exists, it is cleared. Client code should really not +/// be redefining variables. +/// +/// \pre The vector must not already exist as a variable. +/// +/// \param name The name of the vector to set. +void +text::templates_def::add_vector(const std::string& name) +{ + PRE(_variables.find(name) == _variables.end()); + _vectors[name] = strings_vector(); +} + + +/// Adds a value to an existing vector in the templates. +/// +/// \pre name The vector must exist. +/// +/// \param name The name of the vector to append the value to. +/// \param value The textual value to append to the vector. +void +text::templates_def::add_to_vector(const std::string& name, + const std::string& value) +{ + PRE(_variables.find(name) == _variables.end()); + PRE(_vectors.find(name) != _vectors.end()); + _vectors[name].push_back(value); +} + + +/// Checks whether a given identifier exists as a variable or a vector. +/// +/// This is used to implement the evaluation of conditions in if clauses. +/// +/// \param name The name of the variable or vector. +/// +/// \return True if the given name exists as a variable or a vector; false +/// otherwise. +bool +text::templates_def::exists(const std::string& name) const +{ + return (_variables.find(name) != _variables.end() || + _vectors.find(name) != _vectors.end()); +} + + +/// Gets the value of a variable. +/// +/// \param name The name of the variable. +/// +/// \return The value of the requested variable. +/// +/// \throw text::syntax_error If the variable does not exist. +const std::string& +text::templates_def::get_variable(const std::string& name) const +{ + const variables_map::const_iterator iter = _variables.find(name); + if (iter == _variables.end()) + throw text::syntax_error(F("Unknown variable '%s'") % name); + return (*iter).second; +} + + +/// Gets a vector. +/// +/// \param name The name of the vector. +/// +/// \return A reference to the requested vector. +/// +/// \throw text::syntax_error If the vector does not exist. +const text::templates_def::strings_vector& +text::templates_def::get_vector(const std::string& name) const +{ + const vectors_map::const_iterator iter = _vectors.find(name); + if (iter == _vectors.end()) + throw text::syntax_error(F("Unknown vector '%s'") % name); + return (*iter).second; +} + + +/// Indexes a vector and gets the value. +/// +/// \param name The name of the vector to index. +/// \param index_name The name of a variable representing the index to use. +/// This must be convertible to a natural. +/// +/// \return The value of the vector at the given index. +/// +/// \throw text::syntax_error If the vector does not existor if the index is out +/// of range. +const std::string& +text::templates_def::get_vector(const std::string& name, + const std::string& index_name) const +{ + const strings_vector& vector = get_vector(name); + const std::string& index_str = get_variable(index_name); + + std::size_t index; + try { + index = text::to_type< std::size_t >(index_str); + } catch (const text::syntax_error& e) { + throw text::syntax_error(F("Index '%s' not an integer, value '%s'") % + index_name % index_str); + } + if (index >= vector.size()) + throw text::syntax_error(F("Index '%s' out of range at position '%s'") % + index_name % index); + + return vector[index]; +} + + +/// Evaluates a expression using these templates. +/// +/// An expression is a query on the current templates to fetch a particular +/// value. The value is always returned as a string, as this is how templates +/// are internally stored. +/// +/// \param expression The expression to evaluate. This should not include any +/// of the delimiters used in the user input, as otherwise the expression +/// will not be evaluated properly. +/// +/// \return The result of the expression evaluation as a string. +/// +/// \throw text::syntax_error If there is any problem while evaluating the +/// expression. +std::string +text::templates_def::evaluate(const std::string& expression) const +{ + const std::string::size_type paren_open = expression.find('('); + if (paren_open == std::string::npos) { + return get_variable(expression); + } else { + const std::string::size_type paren_close = expression.find( + ')', paren_open); + if (paren_close == std::string::npos) + throw text::syntax_error(F("Expected ')' in expression '%s')") % + expression); + if (paren_close != expression.length() - 1) + throw text::syntax_error(F("Unexpected text found after ')' in " + "expression '%s'") % expression); + + const std::string arg0 = expression.substr(0, paren_open); + const std::string arg1 = expression.substr( + paren_open + 1, paren_close - paren_open - 1); + if (arg0 == "defined") { + return exists(arg1) ? "true" : "false"; + } else if (arg0 == "length") { + return F("%s") % get_vector(arg1).size(); + } else { + return get_vector(arg0, arg1); + } + } +} + + +/// Applies a set of templates to an input stream. +/// +/// \param templates The templates to use. +/// \param input The input to process. +/// \param output The stream to which to write the processed text. +/// +/// \throw text::syntax_error If there is any problem processing the input. +void +text::instantiate(const templates_def& templates, + std::istream& input, std::ostream& output) +{ + templates_parser parser(templates, "%", "%%"); + parser.instantiate(input, output); +} + + +/// Applies a set of templates to an input file and writes an output file. +/// +/// \param templates The templates to use. +/// \param input_file The path to the input to process. +/// \param output_file The path to the file into which to write the output. +/// +/// \throw text::error If the input or output files cannot be opened. +/// \throw text::syntax_error If there is any problem processing the input. +void +text::instantiate(const templates_def& templates, + const fs::path& input_file, const fs::path& output_file) +{ + std::ifstream input(input_file.c_str()); + if (!input) + throw text::error(F("Failed to open %s for read") % input_file); + + std::ofstream output(output_file.c_str()); + if (!output) + throw text::error(F("Failed to open %s for write") % output_file); + + instantiate(templates, input, output); +} diff --git a/utils/text/templates.hpp b/utils/text/templates.hpp new file mode 100644 index 000000000000..ffbf28512d0d --- /dev/null +++ b/utils/text/templates.hpp @@ -0,0 +1,122 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/templates.hpp +/// Custom templating engine for text documents. +/// +/// This module provides a simple mechanism to generate text documents based on +/// templates. The templates are just text files that contain template +/// statements that instruct this processor to perform transformations on the +/// input. +/// +/// While this was originally written to handle HTML templates, it is actually +/// generic enough to handle any kind of text document, hence why it lives +/// within the utils::text library. +/// +/// An example of how the templates look like: +/// +/// %if names +/// List of names +/// ------------- +/// Amount of names: %%length(names)%% +/// Most preferred name: %%preferred_name%% +/// Full list: +/// %loop names iter +/// * %%last_names(iter)%%, %%names(iter)%% +/// %endloop +/// %endif names + +#if !defined(UTILS_TEXT_TEMPLATES_HPP) +#define UTILS_TEXT_TEMPLATES_HPP + +#include "utils/text/templates_fwd.hpp" + +#include <istream> +#include <map> +#include <ostream> +#include <string> +#include <vector> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace text { + + +/// Definitions of the templates to apply to a file. +/// +/// This class provides the environment (e.g. the list of variables) that the +/// templating system has to use when generating the output files. This +/// definition is static in the sense that this is what the caller program +/// specifies. +class templates_def { + /// Mapping of variable names to their values. + typedef std::map< std::string, std::string > variables_map; + + /// Collection of global variables available to the templates. + variables_map _variables; + + /// Convenience name for a vector of strings. + typedef std::vector< std::string > strings_vector; + + /// Mapping of vector names to their contents. + /// + /// Ideally, these would be represented as part of the _variables, but we + /// would need a complex mechanism to identify whether a variable is a + /// string or a vector. + typedef std::map< std::string, strings_vector > vectors_map; + + /// Collection of vectors available to the templates. + vectors_map _vectors; + + const std::string& get_vector(const std::string&, const std::string&) const; + +public: + templates_def(void); + + void add_variable(const std::string&, const std::string&); + void remove_variable(const std::string&); + void add_vector(const std::string&); + void add_to_vector(const std::string&, const std::string&); + + bool exists(const std::string&) const; + const std::string& get_variable(const std::string&) const; + const strings_vector& get_vector(const std::string&) const; + + std::string evaluate(const std::string&) const; +}; + + +void instantiate(const templates_def&, std::istream&, std::ostream&); +void instantiate(const templates_def&, const fs::path&, const fs::path&); + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TEMPLATES_HPP) diff --git a/utils/text/templates_fwd.hpp b/utils/text/templates_fwd.hpp new file mode 100644 index 000000000000..c806be0cf497 --- /dev/null +++ b/utils/text/templates_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/text/templates_fwd.hpp +/// Forward declarations for utils/text/templates.hpp + +#if !defined(UTILS_TEXT_TEMPLATES_FWD_HPP) +#define UTILS_TEXT_TEMPLATES_FWD_HPP + +namespace utils { +namespace text { + + +class templates_def; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TEMPLATES_FWD_HPP) diff --git a/utils/text/templates_test.cpp b/utils/text/templates_test.cpp new file mode 100644 index 000000000000..4524dc61a416 --- /dev/null +++ b/utils/text/templates_test.cpp @@ -0,0 +1,1001 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/text/templates.hpp" + +#include <fstream> +#include <sstream> + +#include <atf-c++.hpp> + +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/text/exceptions.hpp" + +namespace fs = utils::fs; +namespace text = utils::text; + + +namespace { + + +/// Applies a set of templates to an input string and validates the output. +/// +/// This fails the test case if exp_output does not match the document generated +/// by the application of the templates. +/// +/// \param templates The templates to apply. +/// \param input_str The input document to which to apply the templates. +/// \param exp_output The expected output document. +static void +do_test_ok(const text::templates_def& templates, const std::string& input_str, + const std::string& exp_output) +{ + std::istringstream input(input_str); + std::ostringstream output; + + text::instantiate(templates, input, output); + ATF_REQUIRE_EQ(exp_output, output.str()); +} + + +/// Applies a set of templates to an input string and checks for an error. +/// +/// This fails the test case if the exception raised by the template processing +/// does not match the expected message. +/// +/// \param templates The templates to apply. +/// \param input_str The input document to which to apply the templates. +/// \param exp_message The expected error message in the raised exception. +static void +do_test_fail(const text::templates_def& templates, const std::string& input_str, + const std::string& exp_message) +{ + std::istringstream input(input_str); + std::ostringstream output; + + ATF_REQUIRE_THROW_RE(text::syntax_error, exp_message, + text::instantiate(templates, input, output)); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_variable__first); +ATF_TEST_CASE_BODY(templates_def__add_variable__first) +{ + text::templates_def templates; + templates.add_variable("the-name", "first-value"); + ATF_REQUIRE_EQ("first-value", templates.get_variable("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_variable__replace); +ATF_TEST_CASE_BODY(templates_def__add_variable__replace) +{ + text::templates_def templates; + templates.add_variable("the-name", "first-value"); + templates.add_variable("the-name", "second-value"); + ATF_REQUIRE_EQ("second-value", templates.get_variable("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__remove_variable); +ATF_TEST_CASE_BODY(templates_def__remove_variable) +{ + text::templates_def templates; + templates.add_variable("the-name", "the-value"); + templates.get_variable("the-name"); // Should not throw. + templates.remove_variable("the-name"); + ATF_REQUIRE_THROW(text::syntax_error, templates.get_variable("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_vector__first); +ATF_TEST_CASE_BODY(templates_def__add_vector__first) +{ + text::templates_def templates; + templates.add_vector("the-name"); + ATF_REQUIRE(templates.get_vector("the-name").empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_vector__replace); +ATF_TEST_CASE_BODY(templates_def__add_vector__replace) +{ + text::templates_def templates; + templates.add_vector("the-name"); + templates.add_to_vector("the-name", "foo"); + ATF_REQUIRE(!templates.get_vector("the-name").empty()); + templates.add_vector("the-name"); + ATF_REQUIRE(templates.get_vector("the-name").empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_to_vector); +ATF_TEST_CASE_BODY(templates_def__add_to_vector) +{ + text::templates_def templates; + templates.add_vector("the-name"); + ATF_REQUIRE_EQ(0, templates.get_vector("the-name").size()); + templates.add_to_vector("the-name", "first"); + ATF_REQUIRE_EQ(1, templates.get_vector("the-name").size()); + templates.add_to_vector("the-name", "second"); + ATF_REQUIRE_EQ(2, templates.get_vector("the-name").size()); + templates.add_to_vector("the-name", "third"); + ATF_REQUIRE_EQ(3, templates.get_vector("the-name").size()); + + std::vector< std::string > expected; + expected.push_back("first"); + expected.push_back("second"); + expected.push_back("third"); + ATF_REQUIRE(expected == templates.get_vector("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__exists__variable); +ATF_TEST_CASE_BODY(templates_def__exists__variable) +{ + text::templates_def templates; + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_variable("some-name ", "foo"); + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_variable("some-name", "foo"); + ATF_REQUIRE(templates.exists("some-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__exists__vector); +ATF_TEST_CASE_BODY(templates_def__exists__vector) +{ + text::templates_def templates; + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_vector("some-name "); + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_vector("some-name"); + ATF_REQUIRE(templates.exists("some-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_variable__ok); +ATF_TEST_CASE_BODY(templates_def__get_variable__ok) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + templates.add_variable("bar", " baz "); + ATF_REQUIRE_EQ("", templates.get_variable("foo")); + ATF_REQUIRE_EQ(" baz ", templates.get_variable("bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_variable__unknown); +ATF_TEST_CASE_BODY(templates_def__get_variable__unknown) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown variable 'foo '", + templates.get_variable("foo ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_vector__ok); +ATF_TEST_CASE_BODY(templates_def__get_vector__ok) +{ + text::templates_def templates; + templates.add_vector("foo"); + templates.add_vector("bar"); + templates.add_to_vector("bar", "baz"); + ATF_REQUIRE_EQ(0, templates.get_vector("foo").size()); + ATF_REQUIRE_EQ(1, templates.get_vector("bar").size()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_vector__unknown); +ATF_TEST_CASE_BODY(templates_def__get_vector__unknown) +{ + text::templates_def templates; + templates.add_vector("foo"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown vector 'foo '", + templates.get_vector("foo ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__variable__ok); +ATF_TEST_CASE_BODY(templates_def__evaluate__variable__ok) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + templates.add_variable("bar", " baz "); + ATF_REQUIRE_EQ("", templates.evaluate("foo")); + ATF_REQUIRE_EQ(" baz ", templates.evaluate("bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__variable__unknown); +ATF_TEST_CASE_BODY(templates_def__evaluate__variable__unknown) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown variable 'foo1'", + templates.evaluate("foo1")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__ok); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__ok) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_to_vector("v", "bar"); + templates.add_to_vector("v", "baz"); + + templates.add_variable("index", "0"); + ATF_REQUIRE_EQ("foo", templates.evaluate("v(index)")); + templates.add_variable("index", "1"); + ATF_REQUIRE_EQ("bar", templates.evaluate("v(index)")); + templates.add_variable("index", "2"); + ATF_REQUIRE_EQ("baz", templates.evaluate("v(index)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__unknown_vector); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__unknown_vector) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_variable("index", "0"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown vector 'fooz'", + templates.evaluate("fooz(index)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__unknown_index); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__unknown_index) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_variable("index", "0"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown variable 'indexz'", + templates.evaluate("v(indexz)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__out_of_range); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__out_of_range) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_variable("index", "1"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Index 'index' out of range " + "at position '1'", templates.evaluate("v(index)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__defined); +ATF_TEST_CASE_BODY(templates_def__evaluate__defined) +{ + text::templates_def templates; + templates.add_vector("the-variable"); + templates.add_vector("the-vector"); + ATF_REQUIRE_EQ("false", templates.evaluate("defined(the-variabl)")); + ATF_REQUIRE_EQ("false", templates.evaluate("defined(the-vecto)")); + ATF_REQUIRE_EQ("true", templates.evaluate("defined(the-variable)")); + ATF_REQUIRE_EQ("true", templates.evaluate("defined(the-vector)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__length__ok); +ATF_TEST_CASE_BODY(templates_def__evaluate__length__ok) +{ + text::templates_def templates; + templates.add_vector("v"); + ATF_REQUIRE_EQ("0", templates.evaluate("length(v)")); + templates.add_to_vector("v", "foo"); + ATF_REQUIRE_EQ("1", templates.evaluate("length(v)")); + templates.add_to_vector("v", "bar"); + ATF_REQUIRE_EQ("2", templates.evaluate("length(v)")); + templates.add_to_vector("v", "baz"); + ATF_REQUIRE_EQ("3", templates.evaluate("length(v)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__length__unknown_vector); +ATF_TEST_CASE_BODY(templates_def__evaluate__length__unknown_vector) +{ + text::templates_def templates; + templates.add_vector("foo1"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown vector 'foo'", + templates.evaluate("length(foo)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__parenthesis_error); +ATF_TEST_CASE_BODY(templates_def__evaluate__parenthesis_error) +{ + text::templates_def templates; + ATF_REQUIRE_THROW_RE(text::syntax_error, + "Expected '\\)' in.*'foo\\(abc'", + templates.evaluate("foo(abc")); + ATF_REQUIRE_THROW_RE(text::syntax_error, + "Unexpected text.*'\\)' in.*'a\\(b\\)c'", + templates.evaluate("a(b)c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__empty_input); +ATF_TEST_CASE_BODY(instantiate__empty_input) +{ + const text::templates_def templates; + do_test_ok(templates, "", ""); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__value__ok); +ATF_TEST_CASE_BODY(instantiate__value__ok) +{ + const std::string input = + "first line\n" + "%%testvar1%%\n" + "third line\n" + "%%testvar2%% %%testvar3%%%%testvar4%%\n" + "fifth line\n"; + + const std::string exp_output = + "first line\n" + "second line\n" + "third line\n" + "fourth line.\n" + "fifth line\n"; + + text::templates_def templates; + templates.add_variable("testvar1", "second line"); + templates.add_variable("testvar2", "fourth"); + templates.add_variable("testvar3", "line"); + templates.add_variable("testvar4", "."); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__value__unknown_variable); +ATF_TEST_CASE_BODY(instantiate__value__unknown_variable) +{ + const std::string input = + "%%testvar1%%\n"; + + text::templates_def templates; + templates.add_variable("testvar2", "fourth line"); + + do_test_fail(templates, input, "Unknown variable 'testvar1'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_length__ok); +ATF_TEST_CASE_BODY(instantiate__vector_length__ok) +{ + const std::string input = + "%%length(testvector1)%%\n" + "%%length(testvector2)%% - %%length(testvector3)%%\n"; + + const std::string exp_output = + "4\n" + "0 - 1\n"; + + text::templates_def templates; + templates.add_vector("testvector1"); + templates.add_to_vector("testvector1", "000"); + templates.add_to_vector("testvector1", "111"); + templates.add_to_vector("testvector1", "543"); + templates.add_to_vector("testvector1", "999"); + templates.add_vector("testvector2"); + templates.add_vector("testvector3"); + templates.add_to_vector("testvector3", "123"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_length__unknown_vector); +ATF_TEST_CASE_BODY(instantiate__vector_length__unknown_vector) +{ + const std::string input = + "%%length(testvector)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector2"); + + do_test_fail(templates, input, "Unknown vector 'testvector'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__ok); +ATF_TEST_CASE_BODY(instantiate__vector_value__ok) +{ + const std::string input = + "first line\n" + "%%testvector1(i)%%\n" + "third line\n" + "%%testvector2(j)%%\n" + "fifth line\n"; + + const std::string exp_output = + "first line\n" + "543\n" + "third line\n" + "123\n" + "fifth line\n"; + + text::templates_def templates; + templates.add_variable("i", "2"); + templates.add_variable("j", "0"); + templates.add_vector("testvector1"); + templates.add_to_vector("testvector1", "000"); + templates.add_to_vector("testvector1", "111"); + templates.add_to_vector("testvector1", "543"); + templates.add_to_vector("testvector1", "999"); + templates.add_vector("testvector2"); + templates.add_to_vector("testvector2", "123"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__unknown_vector); +ATF_TEST_CASE_BODY(instantiate__vector_value__unknown_vector) +{ + const std::string input = + "%%testvector(j)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector2"); + + do_test_fail(templates, input, "Unknown vector 'testvector'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__out_of_range__empty); +ATF_TEST_CASE_BODY(instantiate__vector_value__out_of_range__empty) +{ + const std::string input = + "%%testvector(j)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector"); + templates.add_variable("j", "0"); + + do_test_fail(templates, input, "Index 'j' out of range at position '0'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__out_of_range__not_empty); +ATF_TEST_CASE_BODY(instantiate__vector_value__out_of_range__not_empty) +{ + const std::string input = + "%%testvector(j)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector"); + templates.add_to_vector("testvector", "a"); + templates.add_to_vector("testvector", "b"); + templates.add_variable("j", "2"); + + do_test_fail(templates, input, "Index 'j' out of range at position '2'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__one_level__taken); +ATF_TEST_CASE_BODY(instantiate__if__one_level__taken) +{ + const std::string input = + "first line\n" + "%if defined(some_var)\n" + "hello from within the variable conditional\n" + "%endif\n" + "%if defined(some_vector)\n" + "hello from within the vector conditional\n" + "%else\n" + "bye from within the vector conditional\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "hello from within the variable conditional\n" + "hello from within the vector conditional\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("some_var", "zzz"); + templates.add_vector("some_vector"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__one_level__not_taken); +ATF_TEST_CASE_BODY(instantiate__if__one_level__not_taken) +{ + const std::string input = + "first line\n" + "%if defined(some_var)\n" + "hello from within the variable conditional\n" + "%endif\n" + "%if defined(some_vector)\n" + "hello from within the vector conditional\n" + "%else\n" + "bye from within the vector conditional\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "bye from within the vector conditional\n" + "some more\n"; + + text::templates_def templates; + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__multiple_levels__taken); +ATF_TEST_CASE_BODY(instantiate__if__multiple_levels__taken) +{ + const std::string input = + "first line\n" + "%if defined(var1)\n" + "first before\n" + "%if length(var2)\n" + "second before\n" + "%if defined(var3)\n" + "third before\n" + "hello from within the conditional\n" + "third after\n" + "%endif\n" + "second after\n" + "%else\n" + "second after not shown\n" + "%endif\n" + "first after\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "first before\n" + "second before\n" + "third before\n" + "hello from within the conditional\n" + "third after\n" + "second after\n" + "first after\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("var1", "false"); + templates.add_vector("var2"); + templates.add_to_vector("var2", "not-empty"); + templates.add_variable("var3", "foobar"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__multiple_levels__not_taken); +ATF_TEST_CASE_BODY(instantiate__if__multiple_levels__not_taken) +{ + const std::string input = + "first line\n" + "%if defined(var1)\n" + "first before\n" + "%if length(var2)\n" + "second before\n" + "%if defined(var3)\n" + "third before\n" + "hello from within the conditional\n" + "third after\n" + "%else\n" + "will not be shown either\n" + "%endif\n" + "second after\n" + "%else\n" + "second after shown\n" + "%endif\n" + "first after\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "first before\n" + "second after shown\n" + "first after\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("var1", "false"); + templates.add_vector("var2"); + templates.add_vector("var3"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__no_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__no_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "hello\n" + "value in vector: %%table1(i)%%\n" + "%if defined(var1)\n" "some other text\n" "%endif\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("var1", "defined"); + templates.add_vector("table1"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__multiple_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__multiple_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "hello %%table1(i)%% %%table2(i)%%\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "hello foo1 foo2\n" + "hello bar1 bar2\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "foo1"); + templates.add_to_vector("table1", "bar1"); + templates.add_vector("table2"); + templates.add_to_vector("table2", "foo2"); + templates.add_to_vector("table2", "bar2"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__nested__no_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__nested__no_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "before: %%table1(i)%%\n" + "%loop table2 j\n" + "before: %%table2(j)%%\n" + "%loop table3 k\n" + "%%table3(k)%%\n" + "%endloop\n" + "after: %%table2(i)%%\n" + "%endloop\n" + "after: %%table1(i)%%\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "before: a\n" + "after: a\n" + "before: b\n" + "after: b\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "a"); + templates.add_to_vector("table1", "b"); + templates.add_vector("table2"); + templates.add_vector("table3"); + templates.add_to_vector("table3", "1"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__nested__multiple_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__nested__multiple_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "%loop table2 j\n" + "%%table1(i)%% %%table2(j)%%\n" + "%endloop\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "a 1\n" + "a 2\n" + "a 3\n" + "b 1\n" + "b 2\n" + "b 3\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "a"); + templates.add_to_vector("table1", "b"); + templates.add_vector("table2"); + templates.add_to_vector("table2", "1"); + templates.add_to_vector("table2", "2"); + templates.add_to_vector("table2", "3"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__sequential); +ATF_TEST_CASE_BODY(instantiate__loop__sequential) +{ + const std::string input = + "first line\n" + "%loop table1 iter\n" + "1: %%table1(iter)%%\n" + "%endloop\n" + "divider\n" + "%loop table2 iter\n" + "2: %%table2(iter)%%\n" + "%endloop\n" + "divider\n" + "%loop table3 iter\n" + "3: %%table3(iter)%%\n" + "%endloop\n" + "divider\n" + "%loop table4 iter\n" + "4: %%table4(iter)%%\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "1: a\n" + "1: b\n" + "divider\n" + "divider\n" + "divider\n" + "4: 1\n" + "4: 2\n" + "4: 3\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "a"); + templates.add_to_vector("table1", "b"); + templates.add_vector("table2"); + templates.add_vector("table3"); + templates.add_vector("table4"); + templates.add_to_vector("table4", "1"); + templates.add_to_vector("table4", "2"); + templates.add_to_vector("table4", "3"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__scoping); +ATF_TEST_CASE_BODY(instantiate__loop__scoping) +{ + const std::string input = + "%loop table1 i\n" + "%if defined(i)\n" "i defined inside scope 1\n" "%endif\n" + "%loop table2 j\n" + "%if defined(i)\n" "i defined inside scope 2\n" "%endif\n" + "%if defined(j)\n" "j defined inside scope 2\n" "%endif\n" + "%endloop\n" + "%if defined(j)\n" "j defined inside scope 1\n" "%endif\n" + "%endloop\n" + "%if defined(i)\n" "i defined outside\n" "%endif\n" + "%if defined(j)\n" "j defined outside\n" "%endif\n"; + + const std::string exp_output = + "i defined inside scope 1\n" + "i defined inside scope 2\n" + "j defined inside scope 2\n" + "i defined inside scope 1\n" + "i defined inside scope 2\n" + "j defined inside scope 2\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "first"); + templates.add_to_vector("table1", "second"); + templates.add_vector("table2"); + templates.add_to_vector("table2", "first"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__mismatched_delimiters); +ATF_TEST_CASE_BODY(instantiate__mismatched_delimiters) +{ + const std::string input = + "this is some %% text\n" + "and this is %%var%% text%%\n"; + + const std::string exp_output = + "this is some %% text\n" + "and this is some more text%%\n"; + + text::templates_def templates; + templates.add_variable("var", "some more"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__empty_statement); +ATF_TEST_CASE_BODY(instantiate__empty_statement) +{ + do_test_fail(text::templates_def(), "%\n", "Empty statement"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__unknown_statement); +ATF_TEST_CASE_BODY(instantiate__unknown_statement) +{ + do_test_fail(text::templates_def(), "%if2\n", "Unknown statement 'if2'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__invalid_narguments); +ATF_TEST_CASE_BODY(instantiate__invalid_narguments) +{ + do_test_fail(text::templates_def(), "%if a b\n", + "Invalid number of arguments for statement 'if'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__files__ok); +ATF_TEST_CASE_BODY(instantiate__files__ok) +{ + text::templates_def templates; + templates.add_variable("string", "Hello, world!"); + + atf::utils::create_file("input.txt", "The string is: %%string%%\n"); + + text::instantiate(templates, fs::path("input.txt"), fs::path("output.txt")); + + std::ifstream output("output.txt"); + std::string line; + ATF_REQUIRE(std::getline(output, line).good()); + ATF_REQUIRE_EQ(line, "The string is: Hello, world!"); + ATF_REQUIRE(std::getline(output, line).eof()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__files__input_error); +ATF_TEST_CASE_BODY(instantiate__files__input_error) +{ + text::templates_def templates; + ATF_REQUIRE_THROW_RE(text::error, "Failed to open input.txt for read", + text::instantiate(templates, fs::path("input.txt"), + fs::path("output.txt"))); +} + + +ATF_TEST_CASE(instantiate__files__output_error); +ATF_TEST_CASE_HEAD(instantiate__files__output_error) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(instantiate__files__output_error) +{ + text::templates_def templates; + + atf::utils::create_file("input.txt", ""); + + fs::mkdir(fs::path("dir"), 0444); + + ATF_REQUIRE_THROW_RE(text::error, "Failed to open dir/output.txt for write", + text::instantiate(templates, fs::path("input.txt"), + fs::path("dir/output.txt"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, templates_def__add_variable__first); + ATF_ADD_TEST_CASE(tcs, templates_def__add_variable__replace); + ATF_ADD_TEST_CASE(tcs, templates_def__remove_variable); + ATF_ADD_TEST_CASE(tcs, templates_def__add_vector__first); + ATF_ADD_TEST_CASE(tcs, templates_def__add_vector__replace); + ATF_ADD_TEST_CASE(tcs, templates_def__add_to_vector); + ATF_ADD_TEST_CASE(tcs, templates_def__exists__variable); + ATF_ADD_TEST_CASE(tcs, templates_def__exists__vector); + ATF_ADD_TEST_CASE(tcs, templates_def__get_variable__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__get_variable__unknown); + ATF_ADD_TEST_CASE(tcs, templates_def__get_vector__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__get_vector__unknown); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__variable__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__variable__unknown); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__unknown_vector); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__unknown_index); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__out_of_range); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__defined); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__length__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__length__unknown_vector); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__parenthesis_error); + + ATF_ADD_TEST_CASE(tcs, instantiate__empty_input); + ATF_ADD_TEST_CASE(tcs, instantiate__value__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__value__unknown_variable); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_length__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_length__unknown_vector); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__unknown_vector); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__out_of_range__empty); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__out_of_range__not_empty); + ATF_ADD_TEST_CASE(tcs, instantiate__if__one_level__taken); + ATF_ADD_TEST_CASE(tcs, instantiate__if__one_level__not_taken); + ATF_ADD_TEST_CASE(tcs, instantiate__if__multiple_levels__taken); + ATF_ADD_TEST_CASE(tcs, instantiate__if__multiple_levels__not_taken); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__no_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__multiple_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__nested__no_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__nested__multiple_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__sequential); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__scoping); + ATF_ADD_TEST_CASE(tcs, instantiate__mismatched_delimiters); + ATF_ADD_TEST_CASE(tcs, instantiate__empty_statement); + ATF_ADD_TEST_CASE(tcs, instantiate__unknown_statement); + ATF_ADD_TEST_CASE(tcs, instantiate__invalid_narguments); + + ATF_ADD_TEST_CASE(tcs, instantiate__files__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__files__input_error); + ATF_ADD_TEST_CASE(tcs, instantiate__files__output_error); +} diff --git a/utils/units.cpp b/utils/units.cpp new file mode 100644 index 000000000000..bfb488fa2ed6 --- /dev/null +++ b/utils/units.cpp @@ -0,0 +1,172 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/units.hpp" + +extern "C" { +#include <stdint.h> +} + +#include <stdexcept> + +#include "utils/format/macros.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace units = utils::units; + + +/// Constructs a zero bytes quantity. +units::bytes::bytes(void) : + _count(0) +{ +} + + +/// Constructs an arbitrary bytes quantity. +/// +/// \param count_ The amount of bytes in the quantity. +units::bytes::bytes(const uint64_t count_) : + _count(count_) +{ +} + + +/// Parses a string into a bytes quantity. +/// +/// \param in_str The user-provided string to be converted. +/// +/// \return The converted bytes quantity. +/// +/// \throw std::runtime_error If the input string is empty or invalid. +units::bytes +units::bytes::parse(const std::string& in_str) +{ + if (in_str.empty()) + throw std::runtime_error("Bytes quantity cannot be empty"); + + uint64_t multiplier; + std::string str = in_str; + { + const char unit = str[str.length() - 1]; + switch (unit) { + case 'T': case 't': multiplier = TB; break; + case 'G': case 'g': multiplier = GB; break; + case 'M': case 'm': multiplier = MB; break; + case 'K': case 'k': multiplier = KB; break; + default: multiplier = 1; + } + if (multiplier != 1) + str.erase(str.length() - 1); + } + + if (str.empty()) + throw std::runtime_error("Bytes quantity cannot be empty"); + if (str[0] == '.' || str[str.length() - 1] == '.') { + // The standard parser for float values accepts things like ".3" and + // "3.", which means that we would interpret ".3K" and "3.K" as valid + // quantities. I think this is ugly and should not be allowed, so + // special-case this condition and just error out. + throw std::runtime_error(F("Invalid bytes quantity '%s'") % in_str); + } + + double count; + try { + count = text::to_type< double >(str); + } catch (const text::value_error& e) { + throw std::runtime_error(F("Invalid bytes quantity '%s'") % in_str); + } + + return bytes(uint64_t(count * multiplier)); +} + + +/// Formats a bytes quantity for user consumption. +/// +/// \return A textual representation of the bytes quantiy. +std::string +units::bytes::format(void) const +{ + if (_count >= TB) { + return F("%.2sT") % (static_cast< float >(_count) / TB); + } else if (_count >= GB) { + return F("%.2sG") % (static_cast< float >(_count) / GB); + } else if (_count >= MB) { + return F("%.2sM") % (static_cast< float >(_count) / MB); + } else if (_count >= KB) { + return F("%.2sK") % (static_cast< float >(_count) / KB); + } else { + return F("%s") % _count; + } +} + + +/// Implicit conversion to an integral representation. +units::bytes::operator uint64_t(void) const +{ + return _count; +} + + +/// Extracts a bytes quantity from a stream. +/// +/// \param input The stream from which to read a single word representing the +/// bytes quantity. +/// \param rhs The variable into which to store the parsed value. +/// +/// \return The input stream. +/// +/// \post The bad bit of input is set to 1 if the parsing failed. +std::istream& +units::operator>>(std::istream& input, bytes& rhs) +{ + std::string word; + input >> word; + if (input.good() || input.eof()) { + try { + rhs = bytes::parse(word); + } catch (const std::runtime_error& e) { + input.setstate(std::ios::badbit); + } + } + return input; +} + + +/// Injects a bytes quantity into a stream. +/// +/// \param output The stream into which to inject the bytes quantity as a +/// user-readable string. +/// \param rhs The bytes quantity to format. +/// +/// \return The output stream. +std::ostream& +units::operator<<(std::ostream& output, const bytes& rhs) +{ + return (output << rhs.format()); +} diff --git a/utils/units.hpp b/utils/units.hpp new file mode 100644 index 000000000000..281788c3199f --- /dev/null +++ b/utils/units.hpp @@ -0,0 +1,96 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/units.hpp +/// Formatters and parsers of user-friendly units. + +#if !defined(UTILS_UNITS_HPP) +#define UTILS_UNITS_HPP + +#include "utils/units_fwd.hpp" + +extern "C" { +#include <stdint.h> +} + +#include <istream> +#include <ostream> +#include <string> + +namespace utils { +namespace units { + + +namespace { + +/// Constant representing 1 kilobyte. +const uint64_t KB = int64_t(1) << 10; + +/// Constant representing 1 megabyte. +const uint64_t MB = int64_t(1) << 20; + +/// Constant representing 1 gigabyte. +const uint64_t GB = int64_t(1) << 30; + +/// Constant representing 1 terabyte. +const uint64_t TB = int64_t(1) << 40; + +} // anonymous namespace + + +/// Representation of a bytes quantity. +/// +/// The purpose of this class is to represent an amount of bytes in a semantic +/// manner, and to provide functions to format such numbers for nice user +/// presentation and to parse back such numbers. +/// +/// The input follows this regular expression: [0-9]+(|\.[0-9]+)[GgKkMmTt]? +/// The output follows this regular expression: [0-9]+\.[0-9]{3}[GKMT]? +class bytes { + /// Raw representation, in bytes, of the quantity. + uint64_t _count; + +public: + bytes(void); + explicit bytes(const uint64_t); + + static bytes parse(const std::string&); + std::string format(void) const; + + operator uint64_t(void) const; +}; + + +std::istream& operator>>(std::istream&, bytes&); +std::ostream& operator<<(std::ostream&, const bytes&); + + +} // namespace units +} // namespace utils + +#endif // !defined(UTILS_UNITS_HPP) diff --git a/utils/units_fwd.hpp b/utils/units_fwd.hpp new file mode 100644 index 000000000000..3653d9727a2d --- /dev/null +++ b/utils/units_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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. + +/// \file utils/units_fwd.hpp +/// Forward declarations for utils/units.hpp + +#if !defined(UTILS_UNITS_FWD_HPP) +#define UTILS_UNITS_FWD_HPP + +namespace utils { +namespace units { + + +class bytes; + + +} // namespace units +} // namespace utils + +#endif // !defined(UTILS_UNITS_FWD_HPP) diff --git a/utils/units_test.cpp b/utils/units_test.cpp new file mode 100644 index 000000000000..601265c95b49 --- /dev/null +++ b/utils/units_test.cpp @@ -0,0 +1,248 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// 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 "utils/units.hpp" + +#include <sstream> +#include <stdexcept> + +#include <atf-c++.hpp> + +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__tb); +ATF_TEST_CASE_BODY(bytes__format__tb) +{ + using units::TB; + using units::GB; + + ATF_REQUIRE_EQ("2.00T", units::bytes(2 * TB).format()); + ATF_REQUIRE_EQ("45.12T", units::bytes(45 * TB + 120 * GB).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__gb); +ATF_TEST_CASE_BODY(bytes__format__gb) +{ + using units::GB; + using units::MB; + + ATF_REQUIRE_EQ("5.00G", units::bytes(5 * GB).format()); + ATF_REQUIRE_EQ("745.96G", units::bytes(745 * GB + 980 * MB).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__mb); +ATF_TEST_CASE_BODY(bytes__format__mb) +{ + using units::MB; + using units::KB; + + ATF_REQUIRE_EQ("1.00M", units::bytes(1 * MB).format()); + ATF_REQUIRE_EQ("1023.50M", units::bytes(1023 * MB + 512 * KB).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__kb); +ATF_TEST_CASE_BODY(bytes__format__kb) +{ + using units::KB; + + ATF_REQUIRE_EQ("3.00K", units::bytes(3 * KB).format()); + ATF_REQUIRE_EQ("1.33K", units::bytes(1 * KB + 340).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__b); +ATF_TEST_CASE_BODY(bytes__format__b) +{ + ATF_REQUIRE_EQ("0", units::bytes().format()); + ATF_REQUIRE_EQ("0", units::bytes(0).format()); + ATF_REQUIRE_EQ("1023", units::bytes(1023).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__tb); +ATF_TEST_CASE_BODY(bytes__parse__tb) +{ + using units::TB; + using units::GB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0T")); + ATF_REQUIRE_EQ(units::bytes(TB), units::bytes::parse("1T")); + ATF_REQUIRE_EQ(units::bytes(TB), units::bytes::parse("1t")); + ATF_REQUIRE_EQ(13567973486755LL, units::bytes::parse("12.340000T")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__gb); +ATF_TEST_CASE_BODY(bytes__parse__gb) +{ + using units::GB; + using units::MB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0G")); + ATF_REQUIRE_EQ(units::bytes(GB), units::bytes::parse("1G")); + ATF_REQUIRE_EQ(units::bytes(GB), units::bytes::parse("1g")); + ATF_REQUIRE_EQ(13249974108LL, units::bytes::parse("12.340G")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__mb); +ATF_TEST_CASE_BODY(bytes__parse__mb) +{ + using units::MB; + using units::KB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0M")); + ATF_REQUIRE_EQ(units::bytes(MB), units::bytes::parse("1M")); + ATF_REQUIRE_EQ(units::bytes(MB), units::bytes::parse("1m")); + ATF_REQUIRE_EQ(12939427, units::bytes::parse("12.34000M")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__kb); +ATF_TEST_CASE_BODY(bytes__parse__kb) +{ + using units::KB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0K")); + ATF_REQUIRE_EQ(units::bytes(KB), units::bytes::parse("1K")); + ATF_REQUIRE_EQ(units::bytes(KB), units::bytes::parse("1k")); + ATF_REQUIRE_EQ(12636, units::bytes::parse("12.34K")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__b); +ATF_TEST_CASE_BODY(bytes__parse__b) +{ + ATF_REQUIRE_EQ(0, units::bytes::parse("0")); + ATF_REQUIRE_EQ(89, units::bytes::parse("89")); + ATF_REQUIRE_EQ(1234, units::bytes::parse("1234")); + ATF_REQUIRE_EQ(1234567890, units::bytes::parse("1234567890")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__error); +ATF_TEST_CASE_BODY(bytes__parse__error) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "empty", units::bytes::parse("")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "empty", units::bytes::parse("k")); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.'", + units::bytes::parse(".")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'3.'", + units::bytes::parse("3.")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.3'", + units::bytes::parse(".3")); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*' t'", + units::bytes::parse(" t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.t'", + units::bytes::parse(".t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'12 t'", + units::bytes::parse("12 t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'12.t'", + units::bytes::parse("12.t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.12t'", + units::bytes::parse(".12t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'abt'", + units::bytes::parse("abt")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__istream__one_word); +ATF_TEST_CASE_BODY(bytes__istream__one_word) +{ + std::istringstream input("12M"); + + units::bytes bytes; + input >> bytes; + ATF_REQUIRE(input.eof()); + ATF_REQUIRE_EQ(units::bytes(12 * units::MB), bytes); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__istream__many_words); +ATF_TEST_CASE_BODY(bytes__istream__many_words) +{ + std::istringstream input("12M more"); + + units::bytes bytes; + input >> bytes; + ATF_REQUIRE(input.good()); + ATF_REQUIRE_EQ(units::bytes(12 * units::MB), bytes); + + std::string word; + input >> word; + ATF_REQUIRE_EQ("more", word); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__istream__error); +ATF_TEST_CASE_BODY(bytes__istream__error) +{ + std::istringstream input("12.M more"); + + units::bytes bytes(123456789); + input >> bytes; + ATF_REQUIRE(input.bad()); + ATF_REQUIRE_EQ(units::bytes(123456789), bytes); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__ostream); +ATF_TEST_CASE_BODY(bytes__ostream) +{ + std::ostringstream output; + output << "foo " << units::bytes(5 * units::KB) << " bar"; + ATF_REQUIRE_EQ("foo 5.00K bar", output.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, bytes__format__tb); + ATF_ADD_TEST_CASE(tcs, bytes__format__gb); + ATF_ADD_TEST_CASE(tcs, bytes__format__mb); + ATF_ADD_TEST_CASE(tcs, bytes__format__kb); + ATF_ADD_TEST_CASE(tcs, bytes__format__b); + + ATF_ADD_TEST_CASE(tcs, bytes__parse__tb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__gb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__mb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__kb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__b); + ATF_ADD_TEST_CASE(tcs, bytes__parse__error); + + ATF_ADD_TEST_CASE(tcs, bytes__istream__one_word); + ATF_ADD_TEST_CASE(tcs, bytes__istream__many_words); + ATF_ADD_TEST_CASE(tcs, bytes__istream__error); + ATF_ADD_TEST_CASE(tcs, bytes__ostream); +} |