diff --git a/contrib/kyua/.gitignore b/contrib/kyua/.gitignore new file mode 100644 index 000000000000..d7f1a180d6fb --- /dev/null +++ b/contrib/kyua/.gitignore @@ -0,0 +1,23 @@ +*.a +*.o +*_helpers +*_inttest +*_test +*~ + +.deps +.dirstamp +Doxyfile +Makefile +Makefile.in +aclocal.m4 +api-docs +autom4te.cache +config.h +config.h.in +config.log +config.status +configure +kyua +local-kyua +stamp-h1 diff --git a/contrib/kyua/.travis.yml b/contrib/kyua/.travis.yml new file mode 100644 index 000000000000..619dceaf3d2d --- /dev/null +++ b/contrib/kyua/.travis.yml @@ -0,0 +1,49 @@ +language: cpp +sudo: required + +before_install: + - ./admin/travis-install-deps.sh + +matrix: + include: + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=distcheck AS_ROOT=no + - os: linux + dist: xenial + compiler: gcc + env: ARCH=amd64 DO=distcheck AS_ROOT=no + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=apidocs + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=style + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=distcheck AS_ROOT=yes UNPRIVILEGED_USER=no + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=distcheck AS_ROOT=yes UNPRIVILEGED_USER=yes + # TODO(ngie): reenable i386; the libraries were not available in the + # Ubuntu Xenial x86_64 docker image. + #- os: linux + # dist: xenial + # compiler: clang + # env: ARCH=i386 DO=distcheck AS_ROOT=no + #- os: linux + # dist: xenial + # compiler: gcc + # env: ARCH=i386 DO=distcheck AS_ROOT=no + +script: + - ./admin/travis-build.sh + +notifications: + email: + - kyua-log@googlegroups.com diff --git a/contrib/kyua/AUTHORS b/contrib/kyua/AUTHORS new file mode 100644 index 000000000000..ac0998fb937c --- /dev/null +++ b/contrib/kyua/AUTHORS @@ -0,0 +1,11 @@ +# This is the official list of Kyua authors for copyright purposes. +# +# This file is distinct from the CONTRIBUTORS files; see the latter for +# an explanation. +# +# Names are sorted alphabetically and should be added to this file as: +# +# * Name +# * Organization + +* Google Inc. diff --git a/contrib/kyua/CONTRIBUTING.md b/contrib/kyua/CONTRIBUTING.md new file mode 100644 index 000000000000..daa55c308e97 --- /dev/null +++ b/contrib/kyua/CONTRIBUTING.md @@ -0,0 +1,173 @@ +Contributing code to Kyua +========================= + +Want to contribute? Great! But first, please take a few minutes to read this +document in full. Doing so upfront will minimize the turnaround time required +to get your changes incorporated. + + +Legal notes +----------- + +* Before we can use your code, you must sign the + [Google Individual Contributor License + Agreement](https://developers.google.com/open-source/cla/individual), + also known as the CLA, which you can easily do online. The CLA is necessary + mainly because you own the copyright to your changes, even after your + contribution becomes part of our codebase, so we need your permission to use + and distribute your code. We also need to be sure of various other + things--for instance that you will tell us if you know that your code + infringes on other people's patents. You do not have to sign the CLA until + after you have submitted your code for review and a member has approved it, + but you must do it before we can put your code into our codebase. + +* Contributions made by corporations are covered by a different agreement than + the one above: the + [Google Software Grant and Corporate Contributor License + Agreement](https://developers.google.com/open-source/cla/corporate). + Please get your company to sign this agreement instead if your contribution is + on their behalf. + +* Unless you have a strong reason not to, please assign copyright of your + changes to Google Inc. and use the 3-clause BSD license text included + throughout the codebase (see [LICENSE](LICENSE)). Keeping the whole project + owned by a single entity is important, particularly to avoid the problem of + having to replicate potentially hundreds of different copyright notes in + documentation materials, etc. + + +Communication +------------- + +* Before you start working on a larger contribution, you should get in touch + with us first through the + [kyua-discuss mailing + list](https://groups.google.com/forum/#!forum/kyua-discuss) + with your idea so that we can help out and possibly guide you. Coordinating + upfront makes it much easier to avoid frustration later on. + +* Subscribe to the + [kyua-log mailing list](https://groups.google.com/forum/#!forum/kyua-log) to + get notifications on new commits, Travis CI results, or changes to bugs. + + +Git workflow +------------ + +* Always work on a non-master branch. + +* Make sure the history of your branch is clean. (Ab)use `git rebase -i master` + to ensure the sequence of commits you want pulled is easy to follow and that + every commit does one (and only one) thing. In particular, commits of the + form `Fix previous` or `Fix build` should never ever exist; merge those fixes + into the relevant commits so that the history is clean at pull time. + +* Always trigger Travis CI builds for your changes (hence why working on a + branch is important). Push your branch to GitHub so that Travis CI picks it + up and performs a build. If you have forked the repository, you may need to + enable Travis CI builds on your end. Wait for a green result. + +* It is OK and expected for you to `git push --force` on **non-master** + branches. This is required if you need to go through the commit/test cycle + more than once for any given branch after you have "fixed-up" commits to + correct problems spotted in earlier builds. + +* Do not send pull requests that subsume other/older pull requests. Each major + change being submitted belongs in a different pull request, which is trivial + to achieve if you use one branch per change as requested in this workflow. + + +Code reviews +------------ + +* All changes will be subject to code reviews pre-merge time. In other words: + all pull requests will be carefully inspected before being accepted and they + will be returned to you with comments if there are issues to be fixed. + +* Be careful of stylistic errors in your code (see below for style guidelines). + Style violations hinder the review process and distract from the actual code. + By keeping your code clean of style issues upfront, you will speed up the + review process and avoid frustration along the way. + +* Whenever you are ready to submit a pull request, review the *combined diff* + you are requesting to be pulled and look for issues. This is the diff that + will be subject to review, not necessarily the individual commits. You can + view this diff in GitHub at the bottom of the `Open a pull request` form that + appears when you click the button to file a pull request, or you can see the + diff by typing `git diff master`. + + +Commit messages +--------------- + +* Follow standard Git commit message guidelines. The first line has a maximum + length of 50 characters, does not terminate in a period, and has to summarize + the whole commit. Then a blank line comes, and then multiple plain-text + paragraphs provide details on the commit if necessary with a maximum length of + 72-75 characters per line. Vim has syntax highlighting for Git commit + messages and will let you know when you go above the maximum line lengths. + +* Use the imperative tense. Say `Add foo-bar` or `Fix baz` instead of `Adding + blah`, `Adds bleh`, or `Added bloh`. + + +Handling bug tracker issues +--------------------------- + +* All changes pushed to `master` should cross-reference one or more issues in + the bug tracker. This is particularly important for bug fixes, but also + applies to major feature improvements. + +* Unless you have a good reason to do otherwise, name your branch `issue-N` + where `N` is the number of the issue being fixed. + +* If the fix to the issue can be done *in a single commit*, terminate the commit + message with `Fixes #N.` where `N` is the number of the issue being fixed and + include a note in `NEWS` about the issue in the same commit. Such fixes can + be merged onto master using fast-forward (the default behavior of `git + merge`). + +* If the fix to the issue requires *more than one commit*, do **not** include + `Fixes #N.` in any of the individual commit messages of the branch nor include + any changes to the `NEWS` file in those commits. These "announcement" changes + belong in the merge commit onto `master`, which is done by `git merge --no-ff + --no-commit your-branch`, followed by an edit of `NEWS`, and terminated with a + `git commit -a` with the proper note on the bug being fixed. + + +Style guide +----------- + +These notes are generic and certainly *non-exhaustive*: + +* Respect formatting of existing files. Note where braces are placed, number of + blank lines between code chunks, how continuation lines are indented, how + docstrings are typed, etc. + +* Indentation is *always* done using spaces, not tabs. The only exception is in + `Makefile`s, where any continuation line within a target must be prefixed by a + *single tab*. + +* [Be mindful of spelling and + grammar.](http://julipedia.meroh.net/2013/06/readability-mind-your-typos-and-grammar.html) + Mistakes of this kind are enough of a reason to return a pull request. + +* Use proper punctuation for all sentences. Always start with a capital letter + and terminate with a period. + +* Respect lexicographical sorting wherever possible. + +* Lines must not be over 80 characters. + +* No trailing whitespace. + +* Two spaces after end-of-sentence periods. + +* Two blank lines between functions. If there are two blank lines among code + blocks, they usually exist for a reason: keep them. + +* In C++ code, prefix all C identifiers (those coming from `extern "C"` + includes) with `::`. + +* Getter functions/methods only need to be documented via `\return`. A + redundant summary is not necessary. diff --git a/contrib/kyua/CONTRIBUTORS b/contrib/kyua/CONTRIBUTORS new file mode 100644 index 000000000000..faf726a4fefd --- /dev/null +++ b/contrib/kyua/CONTRIBUTORS @@ -0,0 +1,20 @@ +# This is the list of people who have agreed to one of the CLAs and can +# contribute patches to the Kyua project. +# +# The AUTHORS file lists the copyright holders; this file lists people. +# For example: Google employees are listed here but not in AUTHORS +# because Google holds the copyright. +# +# See the following links for details on the CLA: +# +# https://developers.google.com/open-source/cla/individual +# https://developers.google.com/open-source/cla/corporate +# +# Names are sorted by last name and should be added as: +# +# * Name + +* Sergey Bronnikov +* Enji Cooper +* Julio Merino +* Craig Rodrigues diff --git a/contrib/kyua/Doxyfile.in b/contrib/kyua/Doxyfile.in new file mode 100644 index 000000000000..e28d82f8999a --- /dev/null +++ b/contrib/kyua/Doxyfile.in @@ -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. + +BUILTIN_STL_SUPPORT = YES +ENABLE_PREPROCESSING = YES +EXCLUDE_SYMBOLS = "ATF_TC*" +EXTRACT_ANON_NSPACES = YES +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +EXPAND_ONLY_PREDEF = YES +EXTENSION_MAPPING = ipp = C++ +FILE_PATTERNS = *.c *.h *.cpp *.hpp *.ipp +GENERATE_LATEX = NO +GENERATE_TAGFILE = @top_builddir@/api-docs/api-docs.tag +HIDE_FRIEND_COMPOUNDS = YES +INPUT = @top_srcdir@ +INPUT_ENCODING = ISO-8859-1 +JAVADOC_AUTOBRIEF = YES +MACRO_EXPANSION = YES +OUTPUT_DIRECTORY = @top_builddir@/api-docs +OUTPUT_LANGUAGE = English +PREDEFINED += "KYUA_DEFS_NORETURN=" +PREDEFINED += "KYUA_DEFS_FORMAT_PRINTF(x, y)=" +PROJECT_NAME = "@PACKAGE_NAME@" +PROJECT_NUMBER = @VERSION@ +QUIET = YES +RECURSIVE = YES +SORT_BY_SCOPE_NAME = YES +SORT_MEMBERS_CTORS_1ST = YES +WARN_IF_DOC_ERROR = YES +WARN_IF_UNDOCUMENTED = YES +WARN_NO_PARAMDOC = YES +WARNINGS = YES diff --git a/contrib/kyua/INSTALL.md b/contrib/kyua/INSTALL.md new file mode 100644 index 000000000000..d3dcab49cb74 --- /dev/null +++ b/contrib/kyua/INSTALL.md @@ -0,0 +1,268 @@ +Installation instructions +========================= + +Kyua uses the GNU Automake, GNU Autoconf and GNU Libtool utilities as +its build system. These are used only when compiling the application +from the source code package. If you want to install Kyua from a binary +package, you do not need to read this document. + +For the impatient: + + $ ./configure + $ make + $ make check + Gain root privileges + # make install + Drop root privileges + $ make installcheck + +Or alternatively, install as a regular user into your home directory: + + $ ./configure --prefix ~/local + $ make + $ make check + $ make install + $ make installcheck + + +Dependencies +------------ + +To build and use Kyua successfully you need: + +* A standards-compliant C and C++ complier. +* Lutok 0.4. +* pkg-config. +* SQLite 3.6.22. + +To build the Kyua tests, you optionally need: + +* The Automated Testing Framework (ATF), version 0.15 or greater. This + is required if you want to create a distribution file. + +If you are building Kyua from the code on the repository, you will also +need the following tools: + +* GNU Autoconf. +* GNU Automake. +* GNU Libtool. + + +Regenerating the build system +----------------------------- + +This is not necessary if you are building from a formal release +distribution file. + +On the other hand, if you are building Kyua from code extracted from the +repository, you must first regenerate the files used by the build +system. You will also need to do this if you modify `configure.ac`, +`Makefile.am` or any of the other build system files. To do this, simply +run: + + $ autoreconf -i -s + +If ATF is installed in a different prefix than Autoconf, you will also +need to tell autoreconf where the ATF M4 macros are located. Otherwise, +the configure script will be incomplete and will show confusing syntax +errors mentioning, for example, `ATF_CHECK_SH`. To fix this, you have +to run autoreconf in the following manner, replacing `` with +the appropriate path: + + $ autoreconf -i -s -I /share/aclocal + + +General build procedure +----------------------- + +To build and install the source package, you must follow these steps: + +1. Configure the sources to adapt to your operating system. This is + done using the `configure` script located on the sources' top + directory, and it is usually invoked without arguments unless you + want to change the installation prefix. More details on this + procedure are given on a later section. + +2. Build the sources to generate the binaries and scripts. Simply run + `make` on the sources' top directory after configuring them. No + problems should arise. + +3. Check that the built programs work by running `make check`. You do + not need to be root to do this, but if you are not, some checks will + be skipped. + +4. Install the program by running `make install`. You may need to + become root to issue this step. + +5. Issue any manual installation steps that may be required. These are + described later in their own section. + +6. Check that the installed programs work by running `make + installcheck`. You do not need to be root to do this, but if you are + not, some checks will be skipped. + + +Configuration flags +------------------- + +The most common, standard flags given to `configure` are: + +* `--prefix=directory`: + **Possible values:** Any path. + **Default:** `/usr/local`. + + Specifies where the program (binaries and all associated files) will + be installed. + +* `--sysconfdir=directory`: + **Possible values:** Any path. + **Default:** `/usr/local/etc`. + + Specifies where the installed programs will look for configuration + files. `/kyua` will be appended to the given path unless + `KYUA_CONFSUBDIR` is redefined as explained later on. + +* `--help`: + + Shows information about all available flags and exits immediately, + without running any configuration tasks. + +The following environment variables are specific to Kyua's `configure` +script: + +* `GDB`: + **Possible values:** empty, absolute path to GNU GDB. + **Default:** empty. + + Specifies the path to the GNU GDB binary that Kyua will use to gather a + stack trace of a crashing test program. If empty, the configure script + will try to find a suitable binary for you and, if not found, Kyua will + attempt to do the search at run time. + +* `KYUA_ARCHITECTURE`: + **Possible values:** name of a CPU architecture (e.g. `x86_64`, `powerpc`). + **Default:** autodetected; typically the output of `uname -p`. + + Specifies the name of the CPU architecture on which Kyua will run. + This value is used at run-time to determine tests that are not + applicable to the host system. + +* `KYUA_CONFSUBDIR`: + **Possible values:** empty, a relative path. + **Default:** `kyua`. + + Specifies the subdirectory of the configuration directory (given by + the `--sysconfdir` argument) under which Kyua will search for its + configuration files. + +* `KYUA_CONFIG_FILE_FOR_CHECK`: + **Possible values:** none, an absolute path to an existing file. + **Default:** none. + + Specifies the `kyua.conf` configuration file to use when running any + of the `check`, `installcheck` or `distcheck` targets on this source + tree. This setting is exclusively used to customize the test runs of + Kyua itself and has no effect whatsoever on the built product. + +* `KYUA_MACHINE`: + **Possible values:** name of a machine type (e.g. `amd64`, `macppc`). + **Default:** autodetected; typically the output of `uname -m`. + + Specifies the name of the machine type on which Kyua will run. This + value is used at run-time to determine tests that are not applicable + to the host system. + +* `KYUA_TMPDIR`: + **Possible values:** an absolute path to a temporary directory. + **Default:** `/tmp`. + + Specifies the path that Kyua will use to create temporary directories + in by default. + +The following flags are specific to Kyua's `configure` script: + +* `--enable-developer`: + **Possible values:** `yes`, `no`. + **Default:** `yes` in Git `HEAD` builds; `no` in formal releases. + + Enables several features useful for development, such as the inclusion + of debugging symbols in all objects or the enforcement of compilation + warnings. + + The compiler will be executed with an exhaustive collection of warning + detection features regardless of the value of this flag. However, such + warnings are only fatal when `--enable-developer` is `yes`. + +* `--with-atf`: + **Possible values:** `yes`, `no`, `auto`. + **Default:** `auto`. + + Enables usage of ATF to build (and later install) the tests. + + Setting this to `yes` causes the configure script to look for ATF + unconditionally and abort if not found. Setting this to `auto` lets + configure perform the best decision based on availability of ATF. + Setting this to `no` explicitly disables ATF usage. + + When support for tests is enabled, the build process will generate the + test programs and will later install them into the tests tree. + Running `make check` or `make installcheck` from within the source + directory will cause these tests to be run with Kyua. + +* `--with-doxygen`: + **Possible values:** `yes`, `no`, `auto` or a path. + **Default:** `auto`. + + Enables usage of Doxygen to generate documentation for internal APIs. + This documentation is *not* installed and is only provided to help the + developer of this package. Therefore, enabling or disabling Doxygen + causes absolutely no differences on the files installed by this + package. + + Setting this to `yes` causes the configure script to look for Doxygen + unconditionally and abort if not found. Setting this to `auto` lets + configure perform the best decision based on availability of Doxygen. + Setting this to `no` explicitly disables Doxygen usage. And, lastly, + setting this to a path forces configure to use a specific Doxygen + binary, which must exist. + + +Post-installation steps +----------------------- + +Copy the `Kyuafile.top` file installed in the examples directory to the +root of your tests hierarchy and name it `Kyuafile`. For example: + + # cp /usr/local/share/kyua/examples/Kyuafile.top \ + /usr/local/tests/Kyuafile + +This will allow you to simply go into `/usr/tests` and run the tests +from there. + + +Run the tests! +-------------- + +Lastly, after a successful installation, you should periodically run the +tests from the final location to ensure things remain stable. Do so as +follows: + + $ cd /usr/local/kyua && kyua test + +The following configuration variables are specific to the 'kyua' test +suite and can be given to Kyua with arguments of the form +`-v test_suites.kyua.=`: + +* `run_coredump_tests`: + **Possible values:** `true` or `false`. + **Default:** `true`. + + Avoids running tests that crash subprocesses on purpose to make them + dump core. Such tests are particularly slow on macOS, and it is + sometimes handy to disable them for quicker development iteration. + +If you see any tests fail, do not hesitate to report them in: + + https://github.com/jmmv/kyua/issues/ + +Thank you! diff --git a/contrib/kyua/Kyuafile b/contrib/kyua/Kyuafile new file mode 100644 index 000000000000..e986218a45f4 --- /dev/null +++ b/contrib/kyua/Kyuafile @@ -0,0 +1,18 @@ +syntax(2) + +test_suite("kyua") + +include("bootstrap/Kyuafile") +include("cli/Kyuafile") +if fs.exists("doc/Kyuafile") then + -- The tests for the docs are not installed because they only cover the + -- build-time process of the manual pages. + include("doc/Kyuafile") +end +include("drivers/Kyuafile") +include("engine/Kyuafile") +include("examples/Kyuafile") +include("integration/Kyuafile") +include("model/Kyuafile") +include("store/Kyuafile") +include("utils/Kyuafile") diff --git a/contrib/kyua/LICENSE b/contrib/kyua/LICENSE new file mode 100644 index 000000000000..ffb8e3da7d86 --- /dev/null +++ b/contrib/kyua/LICENSE @@ -0,0 +1,27 @@ +Copyright 2010-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. diff --git a/contrib/kyua/Makefile.am b/contrib/kyua/Makefile.am new file mode 100644 index 000000000000..d7f3cd27e73b --- /dev/null +++ b/contrib/kyua/Makefile.am @@ -0,0 +1,186 @@ +# 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. + +ACLOCAL_AMFLAGS = -I m4 + +CHECK_BOOTSTRAP_DEPS = +CHECK_KYUA_DEPS = +CHECK_LOCAL = +CLEAN_TARGETS = +DIST_HOOKS = +PHONY_TARGETS = +CLEANFILES = + +EXTRA_DIST = +noinst_DATA = +noinst_LIBRARIES = +noinst_SCRIPTS = + +doc_DATA = AUTHORS CONTRIBUTING.md CONTRIBUTORS LICENSE NEWS.md +noinst_DATA += INSTALL.md README.md +EXTRA_DIST += $(doc_DATA) INSTALL.md README.md + +if WITH_ATF +tests_topdir = $(pkgtestsdir) + +tests_top_DATA = Kyuafile +EXTRA_DIST += $(tests_top_DATA) +endif + +include admin/Makefile.am.inc +include bootstrap/Makefile.am.inc +include cli/Makefile.am.inc +include doc/Makefile.am.inc +include drivers/Makefile.am.inc +include engine/Makefile.am.inc +include examples/Makefile.am.inc +include integration/Makefile.am.inc +include misc/Makefile.am.inc +include model/Makefile.am.inc +include store/Makefile.am.inc +include utils/Makefile.am.inc + +bin_PROGRAMS = kyua +kyua_SOURCES = main.cpp +kyua_CXXFLAGS = $(CLI_CFLAGS) $(ENGINE_CFLAGS) $(UTILS_CFLAGS) +kyua_LDADD = $(CLI_LIBS) $(ENGINE_LIBS) $(UTILS_LIBS) + +CHECK_ENVIRONMENT = KYUA_CONFDIR="/non-existent" \ + KYUA_DOCDIR="$(abs_top_srcdir)" \ + KYUA_EXAMPLESDIR="$(abs_top_srcdir)/examples" \ + KYUA_MISCDIR="$(abs_top_srcdir)/misc" \ + KYUA_STOREDIR="$(abs_top_srcdir)/store" \ + KYUA_STORETESTDATADIR="$(abs_top_srcdir)/store" \ + PATH="$(abs_top_builddir):$${PATH}" +INSTALLCHECK_ENVIRONMENT = KYUA_CONFDIR="/non-existent" \ + PATH="$(prefix)/bin:$${PATH}" + +# Generate local-kyua, a wrapper shell script to run the just-built 'kyua' +# binary by pointing it to the possibly not-yet-installed data files in the +# build tree. +noinst_SCRIPTS += local-kyua +CLEANFILES += local-kyua local-kyua.tmp +local-kyua: Makefile + $(AM_V_GEN)echo '#!/bin/sh' >local-kyua.tmp; \ + echo 'env $(CHECK_ENVIRONMENT) $(TESTS_ENVIRONMENT)' \ + '"$(abs_top_builddir)/kyua" \ + --config='$(KYUA_CONFIG_FILE_FOR_CHECK)' \ + "$${@}"' >>local-kyua.tmp; \ + chmod +x local-kyua.tmp; \ + mv -f local-kyua.tmp local-kyua + +if WITH_ATF +CHECK_LOCAL += dump-ulimits check-kyua +PHONY_TARGETS += check-kyua +check-kyua: $(CHECK_KYUA_DEPS) + @failed=no; \ + ./local-kyua test \ + --kyuafile='$(top_srcdir)/Kyuafile' --build-root='$(top_builddir)' \ + || failed=yes; \ + if [ "$${failed}" = yes ]; then \ + ./local-kyua report --results-file='$(abs_top_srcdir)' \ + --verbose --results-filter=broken,failed; \ + exit 1; \ + fi + +installcheck-local: dump-ulimits installcheck-kyua +PHONY_TARGETS += installcheck-kyua +installcheck-kyua: + @failed=no; \ + cd $(pkgtestsdir) && $(INSTALLCHECK_ENVIRONMENT) $(TESTS_ENVIRONMENT) \ + kyua --config='$(KYUA_CONFIG_FILE_FOR_CHECK)' test \ + || failed=yes; \ + if [ "$${failed}" = yes ]; then \ + cd $(pkgtestsdir) && $(INSTALLCHECK_ENVIRONMENT) \ + $(TESTS_ENVIRONMENT) \ + kyua --config='$(KYUA_CONFIG_FILE_FOR_CHECK)' report \ + --verbose --results-filter=broken,failed; \ + exit 1; \ + fi + +# TODO(jmmv): kyua should probably be recording this information itself as part +# of the execution context, just as we record environment variables. +PHONY_TARGETS += dump-ulimits +dump-ulimits: + @echo "Resource limits:" + @{ \ + ulimit -a | sed -e 's,$$, (soft),'; \ + ulimit -a -H | sed -e 's,$$, (hard),'; \ + } | sort | sed -e 's,^, ,' + @echo +else +DIST_HOOKS += forbid-dist +PHONY_TARGETS += forbid-dist +forbid-dist: + @echo "Sorry; cannot make dist without atf." + @false +endif +check-local: $(CHECK_LOCAL) + +if WITH_DOXYGEN +# Runs doxygen on the source tree and validates the contents of the docstrings. +# We do not do this by default, even if doxygen has been enabled, because this +# step takes a long time. Instead, we just rely on a Travis CI build to catch +# inconsistencies. +PHONY_TARGETS += check-api-docs +check-api-docs: api-docs/api-docs.tag + @$(AWK) -f $(srcdir)/admin/check-api-docs.awk api-docs/doxygen.out + +api-docs/api-docs.tag: $(builddir)/Doxyfile $(SOURCES) + @$(MKDIR_P) api-docs + @rm -f api-docs/doxygen.out api-docs/doxygen.out.tmp + $(AM_V_GEN)$(DOXYGEN) $(builddir)/Doxyfile \ + >api-docs/doxygen.out.tmp 2>&1 && \ + mv api-docs/doxygen.out.tmp api-docs/doxygen.out + +CLEAN_TARGETS += clean-api-docs +clean-api-docs: + rm -rf api-docs +endif + +# Replace Automake's builtin check-news functionality so that we can validate +# the NEWS.md file instead of NEWS. +DIST_HOOKS += check-news +PHONY_TARGETS += check-news +check-news: + @case "$$(sed 15q "$(srcdir)/NEWS.md")" in \ + *"$(VERSION)"*) : ;; \ + *) \ + echo "NEWS.md not updated; not releasing" 1>&2; \ + exit 1 \ + ;; \ + esac + +clean-local: $(CLEAN_TARGETS) +dist-hook: $(DIST_HOOKS) + +PHONY_TARGETS += clean-all +clean-all: + GIT="$(GIT)" $(SH) $(srcdir)/admin/clean-all.sh + +.PHONY: $(PHONY_TARGETS) diff --git a/contrib/kyua/NEWS.md b/contrib/kyua/NEWS.md new file mode 100644 index 000000000000..304cfe94695a --- /dev/null +++ b/contrib/kyua/NEWS.md @@ -0,0 +1,622 @@ +Major changes between releases +============================== + + +Changes in version 0.14 +----------------------- + +**NOT RELEASED YET; STILL UNDER DEVELOPMENT.** + +* Explicitly require C++11 language features when compiling Kyua. + + +Changes in version 0.13 +----------------------- + +**Released on August 26th, 2016.** + +* Fixed execution of test cases as an unprivileged user, at least under + NetBSD 7.0. Kyua-level failures were probably a regression introduced + in Kyua 0.12, but the underlying may have existed for much longer: + test cases might have previously failed for mysterious reasons when + running under an unprivileged user. + +* Issue #134: Fixed metadata test broken on 32-bit platforms. + +* Issue #139: Added per-test case start/end timestamps to all reports. + +* Issue #156: Fixed crashes due to the invalid handling of cleanup + routine data and triggered by the reuse of PIDs in long-running Kyua + instances. + +* Issue #159: Fixed TAP parser to ignore case while matching `TODO` and + `SKIP` directives, and to also recognize `Skipped`. + +* Fixed potential crash due to a race condition in the unprogramming of + timers to control test deadlines. + + +Changes in version 0.12 +----------------------- + +**Released on November 22nd, 2015.** + +This is a huge release and marks a major milestone for Kyua as it finally +implements a long-standing feature request: the ability to execute test +cases in parallel. This is a big deal because test cases are rarely +CPU-bound: running them in parallel yields much faster execution times for +large test suites, allowing faster iteration of changes during development. + +As an example: the FreeBSD test suite as of this date contains 3285 test +cases. With sequential execution, a full test suite run takes around 12 +minutes to complete, whereas on a 4-core machine with a high level of +parallelism it takes a little over 1 minute. + +Implementing parallel execution required rewriting most of Kyua's core and +partly explains explains why there has not been a new release for over a +year. The current implementation is purely subprocess-based, which works +but has some limitations and has resulted in a core that is really complex +and difficult to understand. Future versions will investigate the use of +threads instead for a simplified programming model and additional +parallelization possibilities. + +* Issue #2: Implemented support to execute test cases in parallel when + invoking `kyua test`. Parallel execution is *only* enabled when the new + `parallelism` configuration variable is set to a value greater than `1`. + The default behavior is still to run tests sequentially because some test + suites contain test cases with side-effects that might fail when run in + parallel. To resolve this, the new metadata property `is_exclusive` can + be set to `true` on a test basis to indicate that the test must be run on + its own. + +* Known regression: Running `kyua debug` on a TAP-based test program does + not currently report the output in real time. The output will only be + displayed once the test program completes. This is a shortcoming of + the new parallel execution engine and will be resolved. + +* Removed the external C-based testers code in favor of the new built-in + implementations. The new approach feels significantly faster than the + previous one. + +* Fixed the handling of relative paths in the `fs.*` functions available + in `Kyuafile`s. All paths are now resolved relative to the location of + the caller `Kyuafile`. `Kyuafile.top` has been updated with these + changes and you should update custom copies of this file with the new + version. + +* Changed temporary directory creation to always grant search + permissions on temporary directories. This is to prevent potential + problems when running Kyua as root and executing test cases that require + dropping privileges (as they may later be unable to use absolute paths + that point inside their work directory). + +* The cleanup of work directories does not longer attempt to deal with + mount points. If a test case mounts a file system and forgets to unmount + it, the mount point will be left behind. It is now the responsibility of + the test case to clean after itself. The reasons for this change are + simplicity and clarity: there are many more things that a test case can + do that have side-effects on the system and Kyua cannot protect against + them all, so it is better to just have the test undo anything it might + have done. + +* Improved `kyua report --verbose` to properly handle environment + variables with continuation lines in them, and fixed the integration + tests for this command to avoid false negatives. + +* Changed the configuration file format to accept the definition of + unknown variables without declaring them local. The syntax version + number remains at 2. This is to allow configuration files for newer Kyua + versions to work on older Kyua versions, as there is no reason to forbid + this. + +* Fixed stacktrace gathering with FreeBSD's ancient version of GDB. + GDB 6.1.1 (circa 2004) does not have the `-ex` flag so we need to + generate a temporary GDB script and feed it to GDB with `-x` instead. + +* Issue #136: Fixed the XML escaping in the JUnit output so that + non-printable characters are properly handled when they appear in the + process's stdout or stderr. + +* Issue #141: Improved reporting of errors triggered by sqlite3. In + particular, all error messages are now tagged with their corresponding + database filename and, if they are API-level errors, the name of the + sqlite3 function that caused them. + +* Issue #144: Improved documentation on the support for custom properties + in the test metadata. + +* Converted the `INSTALL`, `NEWS`, and `README` distribution documents to + Markdown for better formatting online. + + +Changes in version 0.11 +----------------------- + +**Released on October 23rd, 2014.** + +* Added support to print the details of all test cases (metadata and + their output) to `report`. This is via a new `--verbose` flag which + replaces the previous `--show-context`. + +* Added support to specify the amount of physical disk space required + by a test case. This is in the form of a new `required_disk_space` + metadata property, which can also be provided by ATF test cases as + `require.diskspace`. + +* Assimilated the contents of all the `kyua-*-tester(1)` and + `kyua-*-interface(7)` manual pages into more relevant places. In + particular, added more details on test program registration and their + metadata to `kyuafile(5)`, and added `kyua-test-isolation(7)` + describing the isolation features of the test execution. + +* Assimilated the contents of all auxiliary manual pages, including + `kyua-build-root(7)`, `kyua-results-files(7)`, `kyua-test-filters(7)` + and `kyua-test-isolation(7)`, into the relevant command-specific + manual pages. This is for easier discoverability of relevant + information when reading how specific Kyua commands work. + +* Issue #30: Plumbed through support to query configuration variables + from ATF's test case heads. This resolves the confusing situation + where test cases could only do this from their body and cleanup + routines. + +* Issue #49: Extended `report` to support test case filters as + command-line arguments. Combined with `--verbose`, this allows + inspecting the details of a test case failure after execution. + +* Issue #55: Deprecated support for specifying `test_suite` overrides on + a test program basis. This idiom should not be used but support for + it remains in place. + +* Issue #72: Added caching support to the `getcwd(3)` test in configure + so that the result can be overriden for cross-compilation purposes. + +* Issue #83: Changed manual page headings to include a `kyua` prefix in + their name. This prevents some possible confusion when displaying, + for example, the `kyua-test` manual page with a plain name of `test`. + +* Issue #84: Started passing test-suite configuration variables to plain + and TAP test programs via the environment. The name of the + environment variables set this way is prefixed by `TEST_ENV_`, so a + configuration variable of the form + `test_suites.some_name.allow_unsafe_ops=yes` in `kyua.conf` becomes + `TEST_ENV_allow_unsafe_ops=YES` in the environment. + +* Issues #97 and #116: Fixed the build on Illumos. + +* Issue #102: Set `TMPDIR` to the test case's work directory when running + the test case. If the test case happens to use the `mktemp(3)` family + of functions (due to misunderstandings on how Kyua works or due to + the reuse of legacy test code), we don't want it to easily escape the + automanaged work directory. + +* Issue #103: Started being more liberal in the parsing of TAP test + results by treating the number in `ok` and `not ok` lines as optional. + +* Issue #105: Started using tmpfs instead of md as a temporary file + system for tests in FreeBSD so that we do not leak `md(4)` devices. + +* Issue #109: Changed the privilege dropping code to start properly + dropping group privileges when `unprivileged_user` is set. Also fixes + `testers/run_test:fork_wait__unprivileged_group`. + +* Issue #110: Changed `help` to display version information and clarified + the purpose of the `about` command in its documentation. + +* Issue #111: Fixed crash when defining a test program in a `Kyuafile` + that has not yet specified the test suite name. + +* Issue #114: Improved the `kyuafile(5)` manual page by clarifying the + restrictions of the `include()` directive and by adding abundant + examples. + + +Changes in version 0.10 +----------------------- + +**Experimental version released on August 14th, 2014.** + +* Merged `kyua-cli` and `kyua-testers` into a single `kyua` package. + +* Dropped the `kyua-atf-compat` package. + +* Issue #100: Do not try to drop privileges to `unprivileged_user` when we + are already running as an unprivileged user. Doing so is not possible + and thus causes spurious test failures when the current user is not + root and the current user and `unprivileged_user` do not match. + +* Issue #79: Mention `kyua.conf(5)` in the *See also* section of `kyua(1)`. + +* Issue #75: Change the `rewrite__expected_signal__bad_arg` test in + `testers/atf_result_test` to use a different signal value. This is to + prevent triggering a core dump that made the test fail in some platforms. + + +Changes in kyua-cli version 0.9 +------------------------------- + +**Experimental version released on August 8th, 2014.** + +Major changes: + +The internal architecture of Kyua to record the results of test suite +runs has completely changed in this release. Kyua no longer stores all +the different test suite run results as different "actions" within the +single `store.db` database. Instead, Kyua now generates a separate +results file inside `~/.kyua/store/` for every test suite run. + +Due to the complexity involved in the migration process and the little +need for it, this is probably going to be the only release where the +`db-migrate` command is able to convert an old `store.db` file to the +new scheme. + +Changes in more detail: + +* Added the `report-junit` command to generate JUnit XML result files. + The output has been verified to work within Jenkins. + +* Switched to results files specific to their corresponding test suite + run. The unified `store.db` file is now gone: `kyua test` creates a + new results file for every invocation under `~/.kyua/store/` and the + `kyua report*` commands are able to locate the latest file for a + corresponding test suite automatically. + +* The `db-migrate` command takes an old `store.db` file and generates + one results file for every previously-recorded action, later deleting + the `store.db` file. + +* The `--action` flag has been removed from all commands that accepted + it. This has been superseded by the tests results files. + +* The `--store` flag that many commands took has been renamed to + `--results-file` in line with the semantical changes. + +* The `db-exec` command no longer creates an empty database when none + is found. This command is now intended to run only over existing + files. + + +Changes in kyua-testers version 0.3 +----------------------------------- + +**Experimental version released on August 8th, 2014.** + +* Made the testers set a "sanitized" value for the `HOME` environment + variable where, for example, consecutive and trailing slashes have + been cleared. Mac OS X has a tendency to append a trailing slash to + the value of `TMPDIR`, which can cause third-party tests to fail if + they compare `${HOME}` with `$(pwd)`. + +* Issues #85, #86, #90 and #92: Made the TAP parser more complete: mark + test cases reported as `TODO` or `SKIP` as passed; handle skip plans; + ignore lines that look like `ok` and `not ok` but aren't results; and + handle test programs that report a pass but exit with a non-zero code. + + +Changes in kyua-cli version 0.8 +------------------------------- + +**Experimental version released on December 7th, 2013.** + +* Added support for Lutok 0.4. + +* Issue #24: Plug the bootstrap tests back into the test suite. Fixes + in `kyua-testers` 0.2 to isolate test cases into their own sessions + should allow these to run fine. + +* Issue #74: Changed the `kyuafile(5)` parser to automatically discover + existing tester interfaces. The various `*_test_program()` functions + will now exist (or not) based on tester availability, which simplifies + the addition of new testers or the selective installation of them. + + +Changes in kyua-testers version 0.2 +----------------------------------- + +**Experimental version released on December 7th, 2013.** + +* Issue #74: Added the `kyua-tap-tester`, a new backend to interact with + test programs that comply with the Test Anything Protocol. + +* Issue #69: Cope with the lack of `AM_PROG_AR` in `configure.ac`, which + first appeared in Automake 1.11.2. Fixes a problem in Ubuntu 10.04 + LTS, which appears stuck in 1.11.1. + +* Issue #24: Improve test case isolation by confining the tests to their + own session instead of just to their own process group. + + +Changes in kyua-cli version 0.7 +------------------------------- + +**Experimental version released on October 18th, 2013.** + +* Made failures from testers more resilent. If a tester fails, the + corresponding test case will be marked as broken instead of causing + kyua to exit. + +* Added the `--results-filter` option to the `report-html` command and + set its default value to skip passed results from HTML reports. This + is to keep these reports more succint and to avoid generating tons of + detail files that will be, in general, useless. + +* Switched to use Lutok 0.3 to gain compatibility with Lua 5.2. + +* Issue #69: Cope with the lack of `AM_PROG_AR` in `configure.ac`, which + first appeared in Automake 1.11.2. Fixes a problem in Ubuntu 10.04 + LTS, which appears stuck in 1.11.1. + + +Changes in kyua-cli version 0.6 +------------------------------- + +**Experimental version released on February 22nd, 2013.** + +* Issue #36: Changed `kyua help` to not fail when the configuration file + is bogus. Help should always work. + +* Issue #37: Simplified the `syntax()` calls in configuration and + `Kyuafile` files to only specify the requested version instead of also + the format name. The format name is implied by the file being loaded, so + there is no use in the caller having to specify it. The version number + of these file formats has been bumped to 2. + +* Issue #39: Added per-test-case metadata values to the HTML reports. + +* Issue #40: Rewrote the documentation as manual pages and removed the + previous GNU Info document. + +* Issue #47: Started using the independent testers in the `kyua-testers` + package to run the test cases. Kyua does not implement the logic to + invoke test cases any more, which provides for better modularity, + extensibility and robustness. + +* Issue #57: Added support to specify arbitrary metadata properties for + test programs right from the `Kyuafile`. This is to make plain test + programs more versatile, by allowing them to specify any of the + requirements (allowed architectures, required files, etc.) supported + by Kyua. + +* Reduced automatic screen line wrapping of messages to the `help` + command and the output of tables by `db-exec`. Wrapping any other + messages (specially anything going to stderr) was very annoying + because it prevented natural copy/pasting of text. + +* Increased the granularity of the error codes returned by `kyua(1)` to + denote different error conditions. This avoids the overload of `1` to + indicate both "expected" errors from specific subcommands and + unexpected errors caused by the internals of the code. The manual now + correctly explain how the exit codes behave on a command basis. + +* Optimized the database schema to make report generation almost + instantaneous. + +* Bumped the database schema to 2. The database now records the + metadata of both test programs and test cases generically, without + knowledge of their interface. + +* Added the `db-migrate` command to provide a mechanism to upgrade a + database with an old schema to the current schema. + +* Removed the GDB build-time configuration variable. This is now part + of the `kyua-testers` package. + +* Issue #31: Rewrote the `Kyuafile` parsing code in C++, which results in + a much simpler implementation. As a side-effect, this gets rid of the + external Lua files required by `kyua`, which in turn make the tool + self-contained. + +* Added caching of various configure test results (particularly in those + tests that need to execute a test program) so that cross-compilers can + predefine the results of the tests without having to run the + executables. + + +Changes in kyua-testers version 0.1 +----------------------------------- + +**Experimental version released on February 19th, 2013.** + +This is the first public release of the `kyua-testers` package. + +The goal of this first release is to adopt all the test case execution +code of `kyua-cli` 0.5 and ship it as a collection of independent tester +binaries. The `kyua-cli` package will rely on these binaries to run the +tests, which provides better modularity and simplicity to the +architecture of Kyua. + +The code in this package is all C as opposed to the current C++ codebase +of `kyua-cli`, which means that the overall build times of Kyua are now +reduced. + + +Changes in kyua-cli version 0.5 +------------------------------- + +**Experimental version released on July 10th, 2012.** + +* Issue #15: Added automatic stacktrace gathering of crashing test cases. + This relies on GDB and is a best-effort operation. + +* Issue #32: Added the `--build-root` option to the debug, list and test + commands. This allows executing test programs from a different + directory than where the `Kyuafile` scripts live. See the *Build roots* + section in the manual for more details. + +* Issue #33: Removed the `kyuaify.sh` script. This has been renamed to + atf2kyua and moved to the `kyua-atf-compat` module, where it ships as a + first-class utility (with a manual page and tests). + +* Issue #34: Changed the HTML reports to include the stdout and stderr of + every test case. + +* Fixed the build when using a "build directory" and a clean source tree + from the repository. + + +Changes in kyua-cli version 0.4 +------------------------------- + +**Experimental version released on June 6th, 2012.** + +* Added the `report-html` command to generate HTML reports of the + execution of any recorded action. + +* Changed the `--output` flag of the `report` command to only take a + path to the target file, not its format. Different formats are better + supported by implementing different subcommands, as the options they + may receive will vary from format to format. + +* Added a `--with-atf` flag to the configure script to control whether + the ATF tests get built or not. May be useful for packaging systems + that do not have ATF in them yet. Disabling ATF also cuts down the + build time of Kyua significantly, but with the obvious drawbacks. + +* Grouped `kyua` subcommands by topic both in the output of `help` and + in the documentation. In general, the user needs to be aware of + commands that rely on a current project and those commands that rely + purely on the database to generate reports. + +* Made `help` print the descriptions of options and commands properly + tabulated. + +* Changed most informational messages to automatically wrap on screen + boundaries. + +* Rewrote the configuration file parsing module for extensibility. This + will allow future versions of Kyua to provide additional user-facing + options in the configuration file. + + No syntax changes have been made, so existing configuration files + (version 1) will continue to be parsed without problems. There is one + little exception though: all variables under the top-level + `test_suites` tree must be declared as strings. + + Similarly, the `-v` and `--variable` flags to the command line must + now carry a `test_suites.` prefix when referencing any variables under + such tree. + + +Changes in kyua-cli version 0.3 +------------------------------- + +**Experimental version released on February 24th, 2012.** + +* Made the `test` command record the results of the executed test + cases into a SQLite database. As a side effect, `test` now supports a + `--store` option to indicate where the database lives. + +* Added the `report` command to generate plain-text reports of the + test results stored in the database. The interface of this command is + certainly subject to change at this point. + +* Added the `db-exec` command to directly interact with the store + database. + +* Issue #28: Added support for the `require.memory` test case property + introduced in ATF 0.15. + +* Renamed the user-specific configuration file from `~/.kyuarc` to + `~/.kyua/kyua.conf` for consistency with other files stored in the + `~/.kyua/` subdirectory. + +* Switched to use Lutok instead of our own wrappers over the Lua C + library. Lutok is just what used to be our own utils::lua module, but + is now distributed separately. + +* Removed the `Atffile`s from the source tree. Kyua is stable enough + to generate trustworthy reports, and we do not want to give the + impression that atf-run / atf-report are still supported. + +* Enabled logging to stderr for our own test programs. This makes it + slightly easier to debug problems in our own code when we get a + failing test. + + +Changes in kyua-cli version 0.2 +------------------------------- + +**Experimental version released on August 24th, 2011.** + +The biggest change in this release is the ability for Kyua to run test +programs implemented using different frameworks. What this means is +that, now, a Kyua test suite can include not only ATF-based test +programs, but also "legacy" (aka plain) test programs that do not use +any framework. I.e. if you have tests that are simple programs that +exit with 0 on success and 1 on failure, you can plug them in into a +Kyua test suite. + +Other than this, there have been several user-visible changes. The most +important are the addition of the new `config` and `debug` subcommands +to the `kyua` binary. The former can be used to inspect the runtime +configuration of Kyua after parsing, and the latter is useful to +interact with failing tests cases in order to get more data about the +failure itself. + +Without further ado, here comes the itemized list of changes: + +* Generalized the run-time engine to support executing test programs + that implement different interfaces. Test programs that use the ATF + libraries are just a special case of this. (Issue #18.) + +* Added support to the engine to run `plain` test programs: i.e. test + programs that do not use any framework and report their pass/fail + status as an exit code. This is to simplify the integration of legacy + test programs into a test suite, and also to demonstrate that the + run-time engine is generic enough to support different test + interfaces. (Issue #18.) + +* Added the `debug` subcommand. This command allows end users to tweak + the execution of a specific test case and to poke into the behavior of + its execution. At the moment, all this command allows is to view the + stdout and stderr of the command in real time (which the `test` + command currently completely hides). + +* Added the `config` subcommand. This command allows the end user to + inspect the current configuration variables after evaluation, without + having to read through configuration files. (Issue #11.) + +* Removed the `test_suites_var` function from configuration files. This + was used to set the value of test-suite-sepecific variables, but it + was ugly-looking. It is now possible to use the more natural syntax + `test_suites.. = `. (Issue #11.) + +* Added a mechanism to disable the loading of configuration files + altogether. Needed for testing purposes and for scriptability. + Available by passing the `--config=none` flag. + +* Enabled detection of unused parameters and variables in the code and + fixed all warnings. (Issue #23.) + +* Changed the behavior of "developer mode". Compiler warnings are now + enabled unconditionally regardless of whether we are in developer mode + or not; developer mode is now only used to perform strict warning + checks and to enable assertions. Additionally, developer mode is now + only automatically enabled when building from the repository, not for + formal releases. (Issue #22.) + +* Fixed many build and portability problems to Debian sid with GCC 4.6.3 + and Ubuntu 10.04.1 LTS. (Issues #20, #21, #26.) + + +Changes in kyua-cli version 0.1 +------------------------------- + +**Experimental version released on June 23rd, 2011.** + +This is the first public release of the `kyua-cli` package. + +The scope of this release is to provide functional replacement for the +`atf-run` utility included in the atf package. At this point, `kyua` +can reliably run the NetBSD 5.99.53 test suite delivering the same +results as `atf-run`. + +The reporting facilities of this release are quite limited. There is +no replacement for `atf-report` yet, and there is no easy way of +debugging failing test programs other than running them by hand. These +features will mark future milestones and therefore be part of other +releases. + +Be aware that this release has suffered very limited field testing. +The test suite for `kyua-cli` is quite comprehensive, but some bugs may +be left in any place. diff --git a/contrib/kyua/README.md b/contrib/kyua/README.md new file mode 100644 index 000000000000..eb34c0fd4550 --- /dev/null +++ b/contrib/kyua/README.md @@ -0,0 +1,84 @@ +Welcome to the Kyua project! +============================ + +Kyua is a **testing framework** for infrastructure software, originally +designed to equip BSD-based operating systems with a test suite. This +means that Kyua is lightweight and simple, and that Kyua integrates well +with various build systems and continuous integration frameworks. + +Kyua features an **expressive test suite definition language**, a **safe +runtime engine** for test suites and a **powerful report generation +engine**. + +Kyua is for **both developers *and* users**, from the developer applying a +simple fix to a library to the system administrator deploying a new release +on a production machine. + +Kyua is **able to execute test programs written with a plethora of testing +libraries and languages**. The library of choice is +[ATF](https://github.com/jmmv/atf/), for which Kyua was originally +designed, but simple, framework-less test programs and TAP-compliant test +programs can also be executed through Kyua. + +Kyua is licensed under a **[liberal BSD 3-clause license](LICENSE)**. +This is not an official Google product. + +[Read more about Kyua in the About wiki page.](../../wiki/About) + + +Download +-------- + +The latest version of Kyua is 0.13 and was released on August 26th, 2016. + +Download: [kyua-0.13](../../releases/tag/kyua-0.13). + +See the [release notes](NEWS.md) for information about the changes in this +and all previous releases. + + +Installation +------------ + +You are encouraged to install binary packages for your operating system +wherever available: + +* Fedora 20 and above: install the `kyua-cli` package with `yum install + kyua-cli`. + +* FreeBSD 10.0 and above: install the `kyua` package with `pkg install kyua`. + +* NetBSD with pkgsrc: install the `pkgsrc/devel/kyua` package. + +* OpenBSD with packages: install the `kyua` package with `pkg_add kyua`. + +* OS X (with Homebrew): install the `kyua` package with `brew install kyua`. + +Should you want to build and install Kyua from the source tree provided +here, follow the instructions in the +[INSTALL.md file](INSTALL.md). + +You should also install the ATF libraries to assist in the development of +test programs. To that end, see the +[ATF project page](https://github.com/jmmv/atf/). + + +Contributing +------------ + +Want to contribute? Great! But please first read the guidelines provided +in [CONTRIBUTING.md](CONTRIBUTING.md). + +If you are curious about who made this project possible, you can check out +the [list of copyright holders](AUTHORS) and the [list of +individuals](CONTRIBUTORS). + + +Support +------- + +Please use the [kyua-discuss mailing +list](https://groups.google.com/forum/#!forum/kyua-discuss) for any support +inquiries. + +*Homepage:* https://github.com/jmmv/kyua/ diff --git a/contrib/kyua/admin/.gitignore b/contrib/kyua/admin/.gitignore new file mode 100644 index 000000000000..1b34cbb4e096 --- /dev/null +++ b/contrib/kyua/admin/.gitignore @@ -0,0 +1,6 @@ +ar-lib +compile +depcomp +install-sh +mdate-sh +missing diff --git a/contrib/kyua/admin/Makefile.am.inc b/contrib/kyua/admin/Makefile.am.inc new file mode 100644 index 000000000000..7d02b0e611c3 --- /dev/null +++ b/contrib/kyua/admin/Makefile.am.inc @@ -0,0 +1,41 @@ +# 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. + +PHONY_TARGETS += check-style +check-style: + @$(srcdir)/admin/check-style.sh \ + -b "$(abs_top_builddir)" \ + -s "$(abs_top_srcdir)" \ + -t "$(PACKAGE_TARNAME)" + +EXTRA_DIST += admin/check-style-common.awk \ + admin/check-style-cpp.awk \ + admin/check-style-make.awk \ + admin/check-style-man.awk \ + admin/check-style-shell.awk \ + admin/check-style.sh diff --git a/contrib/kyua/admin/build-bintray-dist.sh b/contrib/kyua/admin/build-bintray-dist.sh new file mode 100755 index 000000000000..99cd439892c5 --- /dev/null +++ b/contrib/kyua/admin/build-bintray-dist.sh @@ -0,0 +1,131 @@ +#! /bin/sh +# Copyright 2017 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 admin/build-bintray-dist.sh +# Builds a full Kyua installation under /usr/local for Ubuntu. +# +# This script is used to create the bintray distribution packages in lieu +# of real Debian packages for Kyua. The result of this script is a +# tarball that provides the contents of /usr/local for Kyua. + +set -e -x + +err() { + echo "${@}" 1>&2 + exit 1 +} + +install_deps() { + sudo apt-get update -qq + + local pkgsuffix= + local packages= + packages="${packages} autoconf" + packages="${packages} automake" + packages="${packages} clang" + packages="${packages} g++" + packages="${packages} gdb" + packages="${packages} git" + packages="${packages} libtool" + packages="${packages} make" + if [ "${ARCH?}" = i386 ]; then + pkgsuffix=:i386 + packages="${packages} gcc-multilib" + packages="${packages} g++-multilib" + fi + packages="${packages} liblua5.2-0${pkgsuffix}" + packages="${packages} liblua5.2-dev${pkgsuffix}" + packages="${packages} libsqlite3-0${pkgsuffix}" + packages="${packages} libsqlite3-dev${pkgsuffix}" + packages="${packages} pkg-config${pkgsuffix}" + packages="${packages} sqlite3" + sudo apt-get install -y ${packages} +} + +install_from_github() { + local name="${1}"; shift + local release="${1}"; shift + + local distname="${name}-${release}" + + local baseurl="https://github.com/jmmv/${name}" + wget --no-check-certificate \ + "${baseurl}/releases/download/${distname}/${distname}.tar.gz" + tar -xzvf "${distname}.tar.gz" + + local archflags= + [ "${ARCH?}" != i386 ] || archflags=-m32 + + cd "${distname}" + ./configure \ + --disable-developer \ + --without-atf \ + --without-doxygen \ + CC="${CC?}" \ + CFLAGS="${archflags}" \ + CPPFLAGS="-I/usr/local/include" \ + CXX="${CXX?}" \ + CXXFLAGS="${archflags}" \ + LDFLAGS="-L/usr/local/lib -Wl,-R/usr/local/lib" \ + PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" + make + sudo make install + cd - + + rm -rf "${distname}" "${distname}.tar.gz" +} + +main() { + [ "${ARCH+set}" = set ] || err "ARCH must be set in the environment" + [ "${CC+set}" = set ] || err "CC must be set in the environment" + [ "${CXX+set}" = set ] || err "CXX must be set in the environment" + + [ ! -f /root/local.tgz ] || err "/root/local.tgz already exists" + tar -czf /root/local.tgz /usr/local + restore() { + rm -rf /usr/local + tar -xz -C / -f /root/local.tgz + rm /root/local.tgz + } + trap restore EXIT + rm -rf /usr/local + mkdir /usr/local + + install_deps + install_from_github atf 0.21 + install_from_github lutok 0.4 + install_from_github kyua 0.13 + + local version="$(lsb_release -rs | cut -d . -f 1-2 | tr . -)" + local name="$(date +%Y%m%d)-usr-local-kyua" + name="${name}-ubuntu-${version}-${ARCH?}-${CC?}.tar.gz" + tar -czf "${name}" /usr/local +} + +main "${@}" diff --git a/contrib/kyua/admin/check-api-docs.awk b/contrib/kyua/admin/check-api-docs.awk new file mode 100644 index 000000000000..358e3d54c177 --- /dev/null +++ b/contrib/kyua/admin/check-api-docs.awk @@ -0,0 +1,72 @@ +#! /bin/sh +# 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. + +BEGIN { + failed = 0 +} + +# Skip empty lines. +/^$/ {next} + +# Skip lines that do not directly reference a file. +/^[^\/]/ {next} + +# Ignore known problems. As far as I can tell, all the cases listed here are +# well-documented in the code but Doxygen fails, for some reason or another, to +# properly locate the docstrings. +/engine\/kyuafile\.cpp.*no matching class member/ {next} +/engine\/scheduler\.hpp.*Member setup\(void\).*friend/ {next} +/engine\/scheduler\.hpp.*Member wait_any\(void\)/ {next} +/utils\/optional\.ipp.*no matching file member/ {next} +/utils\/optional\.hpp.*Member make_optional\(const T &\)/ {next} +/utils\/config\/nodes\.hpp.*Member set_lua\(lutok::state &, const int\)/ {next} +/utils\/config\/nodes\.hpp.*Member push_lua\(lutok::state &\)/ {next} +/utils\/config\/nodes\.hpp.*Member set_string\(const std::string &\)/ {next} +/utils\/config\/nodes\.hpp.*Member to_string\(void\)/ {next} +/utils\/config\/nodes\.hpp.*Member is_set\(void\)/ {next} +/utils\/process\/executor\.hpp.*Member spawn\(Hook.*\)/ {next} +/utils\/process\/executor\.hpp.*Member spawn_followup\(Hook.*\)/ {next} +/utils\/process\/executor\.hpp.*Member setup\(void\).*friend/ {next} +/utils\/signals\/timer\.hpp.*Member detail::invoke_do_fired.*friend/ {next} +/utils\/stacktrace_test\.cpp.*no matching class member/ {next} + +# Dump any other problems and account for the failure. +{ + failed = 1 + print +} + +END { + if (failed) { + print "ERROR: Unexpected docstring problems encountered" + exit 1 + } else { + exit 0 + } +} diff --git a/contrib/kyua/admin/check-style-common.awk b/contrib/kyua/admin/check-style-common.awk new file mode 100644 index 000000000000..39516d00d4e5 --- /dev/null +++ b/contrib/kyua/admin/check-style-common.awk @@ -0,0 +1,79 @@ +# 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. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next + + if (length > 80 && NF > 1) + warn("Line too long to fit on screen") +} + +/^ *\t+/ { + if (! match(FILENAME, "Makefile")) + warn("Tab character used for indentation"); +} + +/[ \t]+$/ { + warn("Trailing spaces or tabs"); +} + +/^#![^ ]/ { + warn("Missing space after #!"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/contrib/kyua/admin/check-style-cpp.awk b/contrib/kyua/admin/check-style-cpp.awk new file mode 100644 index 000000000000..126789ca9262 --- /dev/null +++ b/contrib/kyua/admin/check-style-cpp.awk @@ -0,0 +1,87 @@ +# 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. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next +} + +/#ifdef/ { + warn("Undesired usage of #ifdef; use #if defined()") +} + +/#ifndef/ { + warn("Undesired usage of #ifndef; use #if !defined()") +} + +/assert[ \t]*\(/ { + warn("Use the macros in sanity.hpp instead of assert"); +} + +/#.*include.*assert/ { + warn("Do not include assert.h nor cassert"); +} + +/std::endl/ { + warn("Use \\n instead of std::endl"); +} + +/\/\*/ && ! /\*\// { + warn("Do not use multi-line C-style comments"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/contrib/kyua/admin/check-style-make.awk b/contrib/kyua/admin/check-style-make.awk new file mode 100644 index 000000000000..9a6c532e7131 --- /dev/null +++ b/contrib/kyua/admin/check-style-make.awk @@ -0,0 +1,71 @@ +# 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. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next +} + +/^\t *\t/ { + warn("Continuation lines must use a single tab"); +} + +/mkdir.*-p/ { + warn("Use $(MKDIR_P) instead of mkdir -p"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/contrib/kyua/admin/check-style-man.awk b/contrib/kyua/admin/check-style-man.awk new file mode 100644 index 000000000000..5c4a2c261b96 --- /dev/null +++ b/contrib/kyua/admin/check-style-man.awk @@ -0,0 +1,71 @@ +# 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. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE|^\.Bd/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE|^\.Ed/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +/^\.\\"/ { + next +} + +{ + if (skip) + next +} + +/\.\.|e\.g\.|i\.e\./ { + next +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/contrib/kyua/admin/check-style-shell.awk b/contrib/kyua/admin/check-style-shell.awk new file mode 100644 index 000000000000..43d3472cb45b --- /dev/null +++ b/contrib/kyua/admin/check-style-shell.awk @@ -0,0 +1,95 @@ +# 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. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next +} + +/^[ \t]*#/ { + next +} + +/[$ \t]+_[a-zA-Z0-9]+=/ { + warn("Variable should not start with an underline") +} + +/[^\\]\$[^0-9!'"$?@#*{}(|\/,]+/ { + warn("Missing braces around variable name") +} + +/=(""|'')/ { + warn("Assignment to the empty string does not need quotes"); +} + +/basename[ \t]+/ { + warn("Use parameter expansion instead of basename"); +} + +/if[ \t]+(test|![ \t]+test)/ { + warn("Use [ instead of test"); +} + +/[ \t]+(test|\[).*==/ { + warn("test(1)'s == operator is not portable"); +} + +/if.*;[ \t]*fi$/ { + warn("Avoid using a single-line if conditional"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/contrib/kyua/admin/check-style.sh b/contrib/kyua/admin/check-style.sh new file mode 100755 index 000000000000..696f9247a74a --- /dev/null +++ b/contrib/kyua/admin/check-style.sh @@ -0,0 +1,170 @@ +#! /bin/sh +# 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 admin/check-style.sh +# +# Sanity checks the coding style of all source files in the project tree. + +ProgName="${0##*/}" + + +# Prints an error message and exits. +# +# \param ... Parts of the error message; concatenated using a space as the +# separator. +err() { + echo "${ProgName}:" "${@}" 1>&2 + exit 1 +} + + +# Locates all source files within the project directory. +# +# We require the project to have been configured in a directory that is separate +# from the source tree. This is to allow us to easily filter out build +# artifacts from our search. +# +# \param srcdir Absolute path to the source directory. +# \param builddir Absolute path to the build directory. +# \param tarname Basename of the project's tar file, to skip possible distfile +# directories. +find_sources() { + local srcdir="${1}"; shift + local builddir="${1}"; shift + local tarname="${1}"; shift + + ( + cd "${srcdir}" + find . -type f -a \ + \! -path "*/.git/*" \ + \! -path "*/.deps/*" \ + \! -path "*/autom4te.cache/*" \ + \! -path "*/${tarname}-[0-9]*/*" \ + \! -path "*/${builddir##*/}/*" \ + \! -name "Makefile.in" \ + \! -name "aclocal.m4" \ + \! -name "config.h.in" \ + \! -name "configure" \ + \! -name "testsuite" + ) +} + + +# Prints the style rules applicable to a given file. +# +# \param file Path to the source file. +guess_rules() { + local file="${1}"; shift + + case "${file}" in + */ax_cxx_compile_stdcxx.m4) ;; + */ltmain.sh) ;; + *Makefile*) echo common make ;; + *.[0-9]) echo common man ;; + *.cpp|*.hpp) echo common cpp ;; + *.sh) echo common shell ;; + *) echo common ;; + esac +} + + +# Validates a given file against the rules that apply to it. +# +# \param srcdir Absolute path to the source directory. +# \param file Name of the file to validate relative to srcdir. +# +# \return 0 if the file is valid; 1 otherwise, in which case the style +# violations are printed to the output. +check_file() { + local srcdir="${1}"; shift + local file="${1}"; shift + + local err=0 + for rule in $(guess_rules "${file}"); do + awk -f "${srcdir}/admin/check-style-${rule}.awk" \ + "${srcdir}/${file}" || err=1 + done + + return ${err} +} + + +# Entry point. +main() { + local builddir=. + local srcdir=. + local tarname=UNKNOWN + + local arg + while getopts :b:s:t: arg; do + case "${arg}" in + b) + builddir="${OPTARG}" + ;; + + s) + srcdir="${OPTARG}" + ;; + + t) + tarname="${OPTARG}" + ;; + + \?) + err "Unknown option -${OPTARG}" + ;; + esac + done + shift $(expr ${OPTIND} - 1) + + srcdir="$(cd "${srcdir}" && pwd -P)" + builddir="$(cd "${builddir}" && pwd -P)" + [ "${srcdir}" != "${builddir}" ] || \ + err "srcdir and builddir cannot match; reconfigure the package" \ + "in a separate directory" + + local sources + if [ ${#} -gt 0 ]; then + sources="${@}" + else + sources="$(find_sources "${srcdir}" "${builddir}" "${tarname}")" + fi + + local ok=0 + for file in ${sources}; do + local file="$(echo ${file} | sed -e "s,\\./,,")" + + check_file "${srcdir}" "${file}" || ok=1 + done + + return "${ok}" +} + + +main "${@}" diff --git a/contrib/kyua/admin/clean-all.sh b/contrib/kyua/admin/clean-all.sh new file mode 100755 index 000000000000..bc02f1e811f4 --- /dev/null +++ b/contrib/kyua/admin/clean-all.sh @@ -0,0 +1,90 @@ +#! /bin/sh +# 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. + +Prog_Name=${0##*/} + +if [ ! -f ./main.cpp ]; then + echo "${Prog_Name}: must be run from the source top directory" 1>&2 + exit 1 +fi + +if [ ! -f configure ]; then + echo "${Prog_Name}: configure not found; nothing to clean?" 1>&2 + exit 1 +fi + +[ -f Makefile ] || ./configure +make distclean + +# Top-level directory. +rm -f Makefile.in +rm -f aclocal.m4 +rm -rf autom4te.cache +rm -f config.h.in +rm -f configure +rm -f mkinstalldirs +rm -f kyua-*.tar.gz + +# admin directory. +rm -f admin/ar-lib +rm -f admin/compile +rm -f admin/config.guess +rm -f admin/config.sub +rm -f admin/depcomp +rm -f admin/install-sh +rm -f admin/ltmain.sh +rm -f admin/mdate-sh +rm -f admin/missing + +# bootstrap directory. +rm -f bootstrap/package.m4 +rm -f bootstrap/testsuite + +# doc directory. +rm -f doc/*.info +rm -f doc/stamp-vti +rm -f doc/version.texi + +# m4 directory. +rm -f m4/libtool.m4 +rm -f m4/lt*.m4 + +# Files and directories spread all around the tree. +find . -name '#*' | xargs rm -rf +find . -name '*~' | xargs rm -rf +find . -name .deps | xargs rm -rf +find . -name .gdb_history | xargs rm -rf +find . -name .libs | xargs rm -rf +find . -name .tmp | xargs rm -rf + +# Show remaining files. +if [ -n "${GIT}" ]; then + echo ">>> untracked and ignored files" + "${GIT}" status --porcelain --ignored | grep -E '^(\?\?|!!)' || true +fi diff --git a/contrib/kyua/admin/travis-build.sh b/contrib/kyua/admin/travis-build.sh new file mode 100755 index 000000000000..e69f271c13f1 --- /dev/null +++ b/contrib/kyua/admin/travis-build.sh @@ -0,0 +1,98 @@ +#! /bin/sh +# 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. + +set -e -x + +run_autoreconf() { + if [ -d /usr/local/share/aclocal ]; then + autoreconf -isv -I/usr/local/share/aclocal + else + autoreconf -isv + fi +} + +do_apidocs() { + run_autoreconf || return 1 + ./configure --with-doxygen || return 1 + make check-api-docs +} + +do_distcheck() { + run_autoreconf || return 1 + ./configure || return 1 + + sudo sysctl -w "kernel.core_pattern=core.%p" + + local archflags= + [ "${ARCH?}" != i386 ] || archflags=-m32 + + cat >kyua.conf <>kyua.conf + + local f= + f="${f} CFLAGS='${archflags}'" + f="${f} CPPFLAGS='-I/usr/local/include'" + f="${f} CXXFLAGS='${archflags}'" + f="${f} LDFLAGS='-L/usr/local/lib -Wl,-R/usr/local/lib'" + f="${f} PKG_CONFIG_PATH='/usr/local/lib/pkgconfig'" + f="${f} KYUA_CONFIG_FILE_FOR_CHECK=$(pwd)/kyua.conf" + if [ "${AS_ROOT:-no}" = yes ]; then + sudo -H PATH="${PATH}" make distcheck DISTCHECK_CONFIGURE_FLAGS="${f}" + else + make distcheck DISTCHECK_CONFIGURE_FLAGS="${f}" + fi +} + +do_style() { + run_autoreconf || return 1 + mkdir build + cd build + ../configure || return 1 + make check-style +} + +main() { + if [ -z "${DO}" ]; then + echo "DO must be defined" 1>&2 + exit 1 + fi + for step in ${DO}; do + "do_${DO}" || exit 1 + done +} + +main "${@}" diff --git a/contrib/kyua/admin/travis-install-deps.sh b/contrib/kyua/admin/travis-install-deps.sh new file mode 100755 index 000000000000..9341c43895b1 --- /dev/null +++ b/contrib/kyua/admin/travis-install-deps.sh @@ -0,0 +1,83 @@ +#! /bin/sh +# 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. + +set -e -x + +install_deps() { + local pkgsuffix= + local packages= + if [ "${ARCH?}" = i386 ]; then + pkgsuffix=:i386 + packages="${packages} gcc-multilib" + packages="${packages} g++-multilib" + sudo dpkg --add-architecture i386 + fi + packages="${packages} gdb" + packages="${packages} liblua5.2-0${pkgsuffix}" + packages="${packages} liblua5.2-dev${pkgsuffix}" + packages="${packages} libsqlite3-0${pkgsuffix}" + packages="${packages} libsqlite3-dev${pkgsuffix}" + packages="${packages} pkg-config${pkgsuffix}" + packages="${packages} sqlite3" + sudo apt-get update -qq + sudo apt-get install -y ${packages} +} + +install_kyua() { + local name="20190321-usr-local-kyua-ubuntu-16-04-${ARCH?}-${CC?}.tar.gz" + wget -O "${name}" "http://dl.bintray.com/ngie-eign/kyua/${name}" || return 1 + sudo tar -xzvp -C / -f "${name}" + rm -f "${name}" +} + +do_apidocs() { + sudo apt-get install -y doxygen +} + +do_distcheck() { + : +} + +do_style() { + : +} + +main() { + if [ -z "${DO}" ]; then + echo "DO must be defined" 1>&2 + exit 1 + fi + install_deps + install_kyua + for step in ${DO}; do + "do_${DO}" || exit 1 + done +} + +main "${@}" diff --git a/contrib/kyua/bootstrap/.gitignore b/contrib/kyua/bootstrap/.gitignore new file mode 100644 index 000000000000..effaef8e6b4a --- /dev/null +++ b/contrib/kyua/bootstrap/.gitignore @@ -0,0 +1,4 @@ +atconfig +package.m4 +testsuite +testsuite.log diff --git a/contrib/kyua/bootstrap/Kyuafile b/contrib/kyua/bootstrap/Kyuafile new file mode 100644 index 000000000000..0f161b2d66eb --- /dev/null +++ b/contrib/kyua/bootstrap/Kyuafile @@ -0,0 +1,5 @@ +syntax(2) + +test_suite("kyua") + +plain_test_program{name="testsuite"} diff --git a/contrib/kyua/bootstrap/Makefile.am.inc b/contrib/kyua/bootstrap/Makefile.am.inc new file mode 100644 index 000000000000..0dbf26002ce9 --- /dev/null +++ b/contrib/kyua/bootstrap/Makefile.am.inc @@ -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. + +if WITH_ATF +tests_bootstrapdir = $(pkgtestsdir)/bootstrap + +tests_bootstrap_DATA = bootstrap/Kyuafile +EXTRA_DIST += $(tests_bootstrap_DATA) + +DISTCLEANFILES = bootstrap/atconfig \ + bootstrap/testsuite.lineno \ + bootstrap/testsuite.log + +distclean-local: distclean-testsuite +distclean-testsuite: + -rm -rf bootstrap/testsuite.dir + +EXTRA_DIST += bootstrap/Kyuafile \ + bootstrap/testsuite \ + bootstrap/package.m4 \ + bootstrap/testsuite.at + +tests_bootstrap_PROGRAMS = bootstrap/atf_helpers +bootstrap_atf_helpers_SOURCES = bootstrap/atf_helpers.cpp +bootstrap_atf_helpers_CXXFLAGS = $(ATF_CXX_CFLAGS) +bootstrap_atf_helpers_LDADD = $(ATF_CXX_LIBS) + +tests_bootstrap_PROGRAMS += bootstrap/plain_helpers +bootstrap_plain_helpers_SOURCES = bootstrap/plain_helpers.cpp +bootstrap_plain_helpers_CXXFLAGS = $(UTILS_CFLAGS) + +tests_bootstrap_SCRIPTS = bootstrap/testsuite +@target_srcdir@bootstrap/package.m4: $(top_srcdir)/configure.ac + $(AM_V_GEN){ \ + echo '# Signature of the current package.'; \ + echo 'm4_define(AT_PACKAGE_NAME, @PACKAGE_NAME@)'; \ + echo 'm4_define(AT_PACKAGE_TARNAME, @PACKAGE_TARNAME@)'; \ + echo 'm4_define(AT_PACKAGE_VERSION, @PACKAGE_VERSION@)'; \ + echo 'm4_define(AT_PACKAGE_STRING, @PACKAGE_STRING@)'; \ + echo 'm4_define(AT_PACKAGE_BUGREPORT, @PACKAGE_BUGREPORT@)'; \ + } >$(srcdir)/bootstrap/package.m4 + +@target_srcdir@bootstrap/testsuite: $(srcdir)/bootstrap/testsuite.at \ + @target_srcdir@bootstrap/package.m4 + $(AM_V_GEN)autom4te --language=Autotest -I $(srcdir) \ + -I $(srcdir)/bootstrap \ + $(srcdir)/bootstrap/testsuite.at -o $@.tmp; \ + mv $@.tmp $@ + +CHECK_LOCAL += check-bootstrap +PHONY_TARGETS += check-bootstrap +check-bootstrap: @target_srcdir@bootstrap/testsuite $(check_PROGRAMS) \ + $(CHECK_BOOTSTRAP_DEPS) + cd bootstrap && $(CHECK_ENVIRONMENT) $(TESTS_ENVIRONMENT) \ + ./testsuite + +if !TARGET_SRCDIR_EMPTY +CHECK_BOOTSTRAP_DEPS += copy-bootstrap-testsuite +CHECK_KYUA_DEPS += copy-bootstrap-testsuite +PHONY_TARGETS += copy-bootstrap-testsuite +copy-bootstrap-testsuite: + cp -f @target_srcdir@bootstrap/testsuite bootstrap/testsuite +CLEANFILES += bootstrap/testsuite +endif +endif diff --git a/contrib/kyua/bootstrap/atf_helpers.cpp b/contrib/kyua/bootstrap/atf_helpers.cpp new file mode 100644 index 000000000000..6a31b4ced994 --- /dev/null +++ b/contrib/kyua/bootstrap/atf_helpers.cpp @@ -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. + +#include +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(fails); +ATF_TEST_CASE_BODY(fails) +{ + fail("Failed on purpose"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(passes); +ATF_TEST_CASE_BODY(passes) +{ +} + + +ATF_TEST_CASE_WITHOUT_HEAD(skips); +ATF_TEST_CASE_BODY(skips) +{ + skip("Skipped on purpose"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + std::string enabled; + + const char* tests = std::getenv("TESTS"); + if (tests == NULL) + enabled = "fails passes skips"; + else + enabled = tests; + + if (enabled.find("fails") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, fails); + if (enabled.find("passes") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, passes); + if (enabled.find("skips") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, skips); +} diff --git a/contrib/kyua/bootstrap/plain_helpers.cpp b/contrib/kyua/bootstrap/plain_helpers.cpp new file mode 100644 index 000000000000..7de629a99d4d --- /dev/null +++ b/contrib/kyua/bootstrap/plain_helpers.cpp @@ -0,0 +1,141 @@ +// 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 +#include +#include + +#include "utils/defs.hpp" +#include "utils/test_utils.ipp" + + +namespace { + + +/// Prints a fake but valid test case list and then aborts. +/// +/// \param argv The original arguments of the program. +/// +/// \return Nothing because this dies before returning. +static int +helper_abort_test_cases_list(int /* argc */, char** argv) +{ + for (const char* const* arg = argv; *arg != NULL; arg++) { + if (std::strcmp(*arg, "-l") == 0) { + std::cout << "Content-Type: application/X-atf-tp; " + "version=\"1\"\n\n"; + std::cout << "ident: foo\n"; + } + } + utils::abort_without_coredump(); +} + + +/// Just returns without printing anything as the test case list. +/// +/// \return Always 0, as required for test programs. +static int +helper_empty_test_cases_list(int /* argc */, char** /* argv */) +{ + return EXIT_SUCCESS; +} + + +/// Prints a correctly-formatted test case list but empty. +/// +/// \param argv The original arguments of the program. +/// +/// \return Always 0, as required for test programs. +static int +helper_zero_test_cases(int /* argc */, char** argv) +{ + for (const char* const* arg = argv; *arg != NULL; arg++) { + if (std::strcmp(*arg, "-l") == 0) + std::cout << "Content-Type: application/X-atf-tp; " + "version=\"1\"\n\n"; + } + return EXIT_SUCCESS; +} + + +/// Mapping of the name of a helper to its implementation. +struct helper { + /// The name of the helper, as will be provided by the user on the CLI. + const char* name; + + /// A pointer to the function implementing the helper. + int (*hook)(int, char**); +}; + + +/// NULL-terminated table mapping helper names to their implementations. +static const helper helpers[] = { + { "abort_test_cases_list", helper_abort_test_cases_list, }, + { "empty_test_cases_list", helper_empty_test_cases_list, }, + { "zero_test_cases", helper_zero_test_cases, }, + { NULL, NULL, }, +}; + + +} // anonymous namespace + + +/// Entry point to the ATF-less helpers. +/// +/// The caller must select a helper to execute by defining the HELPER +/// environment variable to the name of the desired helper. Think of this main +/// method as a subprogram dispatcher, to avoid having many individual helper +/// binaries. +/// +/// \todo Maybe we should really have individual helper binaries. It would +/// avoid a significant amount of complexity here and in the tests, at the +/// expense of some extra files and extra build logic. +/// +/// \param argc The user argument count; delegated to the helper. +/// \param argv The user arguments; delegated to the helper. +/// +/// \return The exit code of the helper, which depends on the requested helper. +int +main(int argc, char** argv) +{ + const char* command = std::getenv("HELPER"); + if (command == NULL) { + std::cerr << "Usage error: HELPER must be set to a helper name\n"; + std::exit(EXIT_FAILURE); + } + + const struct helper* iter = helpers; + for (; iter->name != NULL && std::strcmp(iter->name, command) != 0; iter++) + ; + if (iter->name == NULL) { + std::cerr << "Usage error: unknown command " << command << "\n"; + std::exit(EXIT_FAILURE); + } + + return iter->hook(argc, argv); +} diff --git a/contrib/kyua/bootstrap/testsuite.at b/contrib/kyua/bootstrap/testsuite.at new file mode 100644 index 000000000000..10200a67a5ca --- /dev/null +++ b/contrib/kyua/bootstrap/testsuite.at @@ -0,0 +1,200 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +AT_INIT([bootstrapping tests]) + + +m4_define([GUESS_TOPDIR], { + old=$(pwd) + cd "$(dirname ${as_myself})" + # We need to locate a build product, not a source file, because the + # test suite may be run outside of the source tree (think distcheck). + while test $(pwd) != '/' -a ! -e bootstrap/plain_helpers; do + cd .. + done + topdir=$(pwd) + cd ${old} + echo ${topdir} +}) + + +m4_define([CREATE_ATF_HELPERS], [ + AT_DATA([Kyuafile], [ +syntax(2) +test_suite("bootstrap") +atf_test_program{name="atf_helpers"} +]) + ln -s $(GUESS_TOPDIR)/bootstrap/atf_helpers atf_helpers +]) +m4_define([RUN_ATF_HELPERS], + [HOME=$(pwd) TESTS="$1" kyua --config=none \ + test --results-file=bootstrap.db $2]) + + +m4_define([CREATE_PLAIN_HELPERS], [ + AT_DATA([Kyuafile], [ +syntax(2) +test_suite("bootstrap") +atf_test_program{name="plain_helpers"} +]) + ln -s $(GUESS_TOPDIR)/bootstrap/plain_helpers plain_helpers +]) +m4_define([RUN_PLAIN_HELPER], + [HOME=$(pwd) HELPER="$1" kyua --config=none \ + test --results-file=bootstrap.db]) + + +AT_SETUP([test program crashes in test list]) +AT_TESTED([kyua]) + +CREATE_PLAIN_HELPERS +AT_CHECK([RUN_PLAIN_HELPER([abort_test_cases_list])], [1], [stdout], []) +re='plain_helpers:__test_cases_list__.*broken.*Test program received signal' +AT_CHECK([grep "${re}" stdout], [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([test program prints an empty test list]) +AT_TESTED([kyua]) + +CREATE_PLAIN_HELPERS +AT_CHECK([RUN_PLAIN_HELPER([empty_test_cases_list])], [1], [stdout], []) +re="plain_helpers:__test_cases_list__.*broken.*Invalid header.*got ''" +AT_CHECK([grep "${re}" stdout], [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([test program with zero test cases]) +AT_TESTED([kyua]) + +CREATE_PLAIN_HELPERS +AT_CHECK([RUN_PLAIN_HELPER([zero_test_cases])], [1], [stdout], []) +re='plain_helpers:__test_cases_list__.*broken.*No test cases' +AT_CHECK([grep "${re}" stdout], [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run test case that passes]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([passes])], [0], [stdout], []) +AT_CHECK([grep "atf_helpers:fails" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips" stdout], [1], [], []) + +AT_CLEANUP + + +AT_SETUP([run test case that fails]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([fails])], [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failed.*Failed on purpose" stdout], + [0], [ignore], []) +AT_CHECK([grep "atf_helpers:passes" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:skips" stdout], [1], [], []) + +AT_CLEANUP + + +AT_SETUP([run test case that skips]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([skips])], [0], [stdout], []) +AT_CHECK([grep "atf_helpers:fails" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:passes" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run two test cases, success]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([passes skips])], [0], [stdout], []) +AT_CHECK([grep "atf_helpers:fails" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run two test cases, failure]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([fails passes])], [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failure.*Failed on purpose" stdout], + [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips" stdout], [1], [], []) + +AT_CLEANUP + + +AT_SETUP([run mixed test cases]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([fails passes skips])], [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failure.*Failed on purpose" stdout], + [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run tests from build directories]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([mkdir src], [0], [], []) +AT_CHECK([mv Kyuafile src], [0], [], []) +AT_CHECK([mkdir obj], [0], [], []) +AT_CHECK([mv atf_helpers obj], [0], [], []) +AT_CHECK([RUN_ATF_HELPERS([fails passes skips], + [--kyuafile=src/Kyuafile --build-root=obj])], + [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failure.*Failed on purpose" stdout], + [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP diff --git a/contrib/kyua/cli/Kyuafile b/contrib/kyua/cli/Kyuafile new file mode 100644 index 000000000000..f5b797d760c3 --- /dev/null +++ b/contrib/kyua/cli/Kyuafile @@ -0,0 +1,14 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="cmd_about_test"} +atf_test_program{name="cmd_config_test"} +atf_test_program{name="cmd_db_exec_test"} +atf_test_program{name="cmd_debug_test"} +atf_test_program{name="cmd_help_test"} +atf_test_program{name="cmd_list_test"} +atf_test_program{name="cmd_test_test"} +atf_test_program{name="common_test"} +atf_test_program{name="config_test"} +atf_test_program{name="main_test"} diff --git a/contrib/kyua/cli/Makefile.am.inc b/contrib/kyua/cli/Makefile.am.inc new file mode 100644 index 000000000000..27872088a1b7 --- /dev/null +++ b/contrib/kyua/cli/Makefile.am.inc @@ -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. + +CLI_CFLAGS = $(DRIVERS_CFLAGS) +CLI_LIBS = libcli.a $(DRIVERS_LIBS) + +noinst_LIBRARIES += libcli.a +libcli_a_SOURCES = cli/cmd_about.cpp +libcli_a_SOURCES += cli/cmd_about.hpp +libcli_a_SOURCES += cli/cmd_config.cpp +libcli_a_SOURCES += cli/cmd_config.hpp +libcli_a_SOURCES += cli/cmd_db_exec.cpp +libcli_a_SOURCES += cli/cmd_db_exec.hpp +libcli_a_SOURCES += cli/cmd_db_migrate.cpp +libcli_a_SOURCES += cli/cmd_db_migrate.hpp +libcli_a_SOURCES += cli/cmd_debug.cpp +libcli_a_SOURCES += cli/cmd_debug.hpp +libcli_a_SOURCES += cli/cmd_help.cpp +libcli_a_SOURCES += cli/cmd_help.hpp +libcli_a_SOURCES += cli/cmd_list.cpp +libcli_a_SOURCES += cli/cmd_list.hpp +libcli_a_SOURCES += cli/cmd_report.cpp +libcli_a_SOURCES += cli/cmd_report.hpp +libcli_a_SOURCES += cli/cmd_report_html.cpp +libcli_a_SOURCES += cli/cmd_report_html.hpp +libcli_a_SOURCES += cli/cmd_report_junit.cpp +libcli_a_SOURCES += cli/cmd_report_junit.hpp +libcli_a_SOURCES += cli/cmd_test.cpp +libcli_a_SOURCES += cli/cmd_test.hpp +libcli_a_SOURCES += cli/common.cpp +libcli_a_SOURCES += cli/common.hpp +libcli_a_SOURCES += cli/common.ipp +libcli_a_SOURCES += cli/config.cpp +libcli_a_SOURCES += cli/config.hpp +libcli_a_SOURCES += cli/main.cpp +libcli_a_SOURCES += cli/main.hpp +libcli_a_CPPFLAGS = -DKYUA_CONFDIR="\"$(kyua_confdir)\"" +libcli_a_CPPFLAGS += -DKYUA_DOCDIR="\"$(docdir)\"" +libcli_a_CPPFLAGS += -DKYUA_MISCDIR="\"$(miscdir)\"" +libcli_a_CPPFLAGS += $(DRIVERS_CFLAGS) +libcli_a_LIBADD = libutils.a + +if WITH_ATF +tests_clidir = $(pkgtestsdir)/cli + +tests_cli_DATA = cli/Kyuafile +EXTRA_DIST += $(tests_cli_DATA) + +tests_cli_PROGRAMS = cli/cmd_about_test +cli_cmd_about_test_SOURCES = cli/cmd_about_test.cpp +cli_cmd_about_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_about_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_config_test +cli_cmd_config_test_SOURCES = cli/cmd_config_test.cpp +cli_cmd_config_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_config_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_db_exec_test +cli_cmd_db_exec_test_SOURCES = cli/cmd_db_exec_test.cpp +cli_cmd_db_exec_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_db_exec_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_debug_test +cli_cmd_debug_test_SOURCES = cli/cmd_debug_test.cpp +cli_cmd_debug_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_debug_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_help_test +cli_cmd_help_test_SOURCES = cli/cmd_help_test.cpp +cli_cmd_help_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_help_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_list_test +cli_cmd_list_test_SOURCES = cli/cmd_list_test.cpp +cli_cmd_list_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_list_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_test_test +cli_cmd_test_test_SOURCES = cli/cmd_test_test.cpp +cli_cmd_test_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_test_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/common_test +cli_common_test_SOURCES = cli/common_test.cpp +cli_common_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_common_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/config_test +cli_config_test_SOURCES = cli/config_test.cpp +cli_config_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_config_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/main_test +cli_main_test_SOURCES = cli/main_test.cpp +cli_main_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_main_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/contrib/kyua/cli/cmd_about.cpp b/contrib/kyua/cli/cmd_about.cpp new file mode 100644 index 000000000000..f2b3f99e0ada --- /dev/null +++ b/contrib/kyua/cli/cmd_about.cpp @@ -0,0 +1,160 @@ +// 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 "cli/cmd_about.hpp" + +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/text/regex.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace text = utils::text; + +using cli::cmd_about; + + +namespace { + + +/// Print the contents of a document. +/// +/// If the file cannot be opened for whatever reason, an error message is +/// printed to the output of the program instead of the contents of the file. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param file The file to print. +/// \param filter_re Regular expression to match the lines to print. If empty, +/// no filtering is applied. +/// +/// \return True if the file was printed, false otherwise. +static bool +cat_file(cmdline::ui* ui, const fs::path& file, + const std::string& filter_re = "") +{ + std::ifstream input(file.c_str()); + if (!input) { + ui->err(F("Failed to open %s") % file); + return false; + } + + std::string line; + if (filter_re.empty()) { + while (std::getline(input, line).good()) { + ui->out(line); + } + } else { + const text::regex filter = text::regex::compile(filter_re, 0); + while (std::getline(input, line).good()) { + if (filter.match(line)) { + ui->out(line); + } + } + } + input.close(); + return true; +} + + +} // anonymous namespace + + +/// Default constructor for cmd_about. +cmd_about::cmd_about(void) : cli_command( + "about", "[authors|license|version]", 0, 1, + "Shows detailed authors and contributors; license; and version information") +{ +} + + +/// Entry point for the "about" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_about::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const fs::path docdir(utils::getenv_with_default( + "KYUA_DOCDIR", KYUA_DOCDIR)); + + bool success = true; + + static const char* list_re = "^\\* "; + + if (cmdline.arguments().empty()) { + ui->out(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); + ui->out(""); + ui->out("License terms:"); + ui->out(""); + success &= cat_file(ui, docdir / "LICENSE"); + ui->out(""); + ui->out("Brought to you by:"); + ui->out(""); + success &= cat_file(ui, docdir / "AUTHORS", list_re); + ui->out(""); + success &= cat_file(ui, docdir / "CONTRIBUTORS", list_re); + ui->out(""); + ui->out(F("Homepage: %s") % PACKAGE_URL); + } else { + const std::string& topic = cmdline.arguments()[0]; + + if (topic == "authors") { + success &= cat_file(ui, docdir / "AUTHORS", list_re); + success &= cat_file(ui, docdir / "CONTRIBUTORS", list_re); + } else if (topic == "license") { + success &= cat_file(ui, docdir / "LICENSE"); + } else if (topic == "version") { + write_version_header(ui); + } else { + throw cmdline::usage_error(F("Invalid about topic '%s'") % topic); + } + } + + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/contrib/kyua/cli/cmd_about.hpp b/contrib/kyua/cli/cmd_about.hpp new file mode 100644 index 000000000000..2d1ed57a498b --- /dev/null +++ b/contrib/kyua/cli/cmd_about.hpp @@ -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 cli/cmd_about.hpp +/// Provides the cmd_about class. + +#if !defined(CLI_CMD_ABOUT_HPP) +#define CLI_CMD_ABOUT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "about" subcommand. +class cmd_about : public cli_command +{ + /// Path to the directory containing the distribution documents. + const std::string _docdir; + +public: + cmd_about(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_ABOUT_HPP) diff --git a/contrib/kyua/cli/cmd_about_test.cpp b/contrib/kyua/cli/cmd_about_test.cpp new file mode 100644 index 000000000000..da75db3b3871 --- /dev/null +++ b/contrib/kyua/cli/cmd_about_test.cpp @@ -0,0 +1,306 @@ +// 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 "cli/cmd_about.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace fs = utils::fs; + +using cli::cmd_about; + + +ATF_TEST_CASE_WITHOUT_HEAD(all_topics__ok); +ATF_TEST_CASE_BODY(all_topics__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/AUTHORS", + "Content of AUTHORS\n" + "* First author\n" + " * garbage\n" + "* Second author\n"); + atf::utils::create_file("fake-docs/CONTRIBUTORS", + "Content of CONTRIBUTORS\n" + "* First contributor\n" + " * garbage\n" + "* Second contributor\n"); + atf::utils::create_file("fake-docs/LICENSE", "Content of LICENSE\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of AUTHORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First author", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second author", ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of CONTRIBUTORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First contributor", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second contributor", + ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Content of LICENSE", + ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_topics__missing_docs); +ATF_TEST_CASE_BODY(all_topics__missing_docs) +{ + cmdline::args_vector args; + args.push_back("about"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + + ATF_REQUIRE(atf::utils::grep_collection("Homepage", ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_authors__ok); +ATF_TEST_CASE_BODY(topic_authors__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("authors"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/AUTHORS", + "Content of AUTHORS\n" + "* First author\n" + " * garbage\n" + "* Second author\n"); + atf::utils::create_file("fake-docs/CONTRIBUTORS", + "Content of CONTRIBUTORS\n" + "* First contributor\n" + " * garbage\n" + "* Second contributor\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(!atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of AUTHORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First author", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second author", ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of CONTRIBUTORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First contributor", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second contributor", + ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("LICENSE", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_authors__missing_doc); +ATF_TEST_CASE_BODY(topic_authors__missing_doc) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("authors"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE_EQ(0, ui.out_log().size()); + + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_license__ok); +ATF_TEST_CASE_BODY(topic_license__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("license"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/LICENSE", "Content of LICENSE\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(!atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(!atf::utils::grep_collection("AUTHORS", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("CONTRIBUTORS", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Content of LICENSE", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_license__missing_doc); +ATF_TEST_CASE_BODY(topic_license__missing_doc) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("license"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE_EQ(0, ui.out_log().size()); + + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_version__ok); +ATF_TEST_CASE_BODY(topic_version__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("version"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_args); +ATF_TEST_CASE_BODY(invalid_args) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("first"); + args.push_back("second"); + + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_topic); +ATF_TEST_CASE_BODY(invalid_topic) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("foo"); + + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Invalid about topic 'foo'", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, all_topics__ok); + ATF_ADD_TEST_CASE(tcs, all_topics__missing_docs); + ATF_ADD_TEST_CASE(tcs, topic_authors__ok); + ATF_ADD_TEST_CASE(tcs, topic_authors__missing_doc); + ATF_ADD_TEST_CASE(tcs, topic_license__ok); + ATF_ADD_TEST_CASE(tcs, topic_license__missing_doc); + ATF_ADD_TEST_CASE(tcs, topic_version__ok); + ATF_ADD_TEST_CASE(tcs, invalid_args); + ATF_ADD_TEST_CASE(tcs, invalid_topic); +} diff --git a/contrib/kyua/cli/cmd_config.cpp b/contrib/kyua/cli/cmd_config.cpp new file mode 100644 index 000000000000..947449aacc2d --- /dev/null +++ b/contrib/kyua/cli/cmd_config.cpp @@ -0,0 +1,122 @@ +// 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 "cli/cmd_config.hpp" + +#include + +#include "cli/common.ipp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_config; + + +namespace { + + +/// Prints all configuration variables. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param properties The key/value map representing all the configuration +/// variables. +/// +/// \return 0 for success. +static int +print_all(cmdline::ui* ui, const config::properties_map& properties) +{ + for (config::properties_map::const_iterator iter = properties.begin(); + iter != properties.end(); iter++) + ui->out(F("%s = %s") % (*iter).first % (*iter).second); + return EXIT_SUCCESS; +} + + +/// Prints the configuration variables that the user requests. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param properties The key/value map representing all the configuration +/// variables. +/// \param filters The names of the configuration variables to print. +/// +/// \return 0 if all specified filters are valid; 1 otherwise. +static int +print_some(cmdline::ui* ui, const config::properties_map& properties, + const cmdline::args_vector& filters) +{ + bool ok = true; + + for (cmdline::args_vector::const_iterator iter = filters.begin(); + iter != filters.end(); iter++) { + const config::properties_map::const_iterator match = + properties.find(*iter); + if (match == properties.end()) { + cmdline::print_warning(ui, F("'%s' is not defined.") % *iter); + ok = false; + } else + ui->out(F("%s = %s") % (*match).first % (*match).second); + } + + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +} + + +} // anonymous namespace + + +/// Default constructor for cmd_config. +cmd_config::cmd_config(void) : cli_command( + "config", "[variable1 .. variableN]", 0, -1, + "Inspects the values of configuration variables") +{ +} + + +/// Entry point for the "config" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_config::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const config::properties_map properties = user_config.all_properties(); + if (cmdline.arguments().empty()) + return print_all(ui, properties); + else + return print_some(ui, properties, cmdline.arguments()); +} diff --git a/contrib/kyua/cli/cmd_config.hpp b/contrib/kyua/cli/cmd_config.hpp new file mode 100644 index 000000000000..42f5abd90c28 --- /dev/null +++ b/contrib/kyua/cli/cmd_config.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 cli/cmd_config.hpp +/// Provides the cmd_config class. + +#if !defined(CLI_CMD_CONFIG_HPP) +#define CLI_CMD_CONFIG_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "config" subcommand. +class cmd_config : public cli_command +{ +public: + cmd_config(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_CONFIG_HPP) diff --git a/contrib/kyua/cli/cmd_config_test.cpp b/contrib/kyua/cli/cmd_config_test.cpp new file mode 100644 index 000000000000..f084f99bb90a --- /dev/null +++ b/contrib/kyua/cli/cmd_config_test.cpp @@ -0,0 +1,144 @@ +// 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 "cli/cmd_config.hpp" + +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/optional.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_config; +using utils::none; + + +namespace { + + +/// Instantiates a fake user configuration for testing purposes. +/// +/// The user configuration is populated with a collection of test-suite +/// properties and some hardcoded values for the generic configuration options. +/// +/// \return A new user configuration object. +static config::tree +fake_config(void) +{ + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "the-architecture"); + user_config.set_string("parallelism", "128"); + user_config.set_string("platform", "the-platform"); + //user_config.set_string("unprivileged_user", ""); + user_config.set_string("test_suites.foo.bar", "first"); + user_config.set_string("test_suites.foo.baz", "second"); + return user_config; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(all); +ATF_TEST_CASE_BODY(all) +{ + cmdline::args_vector args; + args.push_back("config"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(5, ui.out_log().size()); + ATF_REQUIRE_EQ("architecture = the-architecture", ui.out_log()[0]); + ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[1]); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[2]); + ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[3]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[4]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some__ok); +ATF_TEST_CASE_BODY(some__ok) +{ + cmdline::args_vector args; + args.push_back("config"); + args.push_back("platform"); + args.push_back("test_suites.foo.baz"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some__fail); +ATF_TEST_CASE_BODY(some__fail) +{ + cmdline::args_vector args; + args.push_back("config"); + args.push_back("platform"); + args.push_back("unknown"); + args.push_back("test_suites.foo.baz"); + + cmdline::init("progname"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE(atf::utils::grep_string("unknown.*not defined", + ui.err_log()[0])); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, all); + ATF_ADD_TEST_CASE(tcs, some__ok); + ATF_ADD_TEST_CASE(tcs, some__fail); +} diff --git a/contrib/kyua/cli/cmd_db_exec.cpp b/contrib/kyua/cli/cmd_db_exec.cpp new file mode 100644 index 000000000000..54304e6643de --- /dev/null +++ b/contrib/kyua/cli/cmd_db_exec.cpp @@ -0,0 +1,200 @@ +// 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 "cli/cmd_db_exec.hpp" + +#include +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/read_backend.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace sqlite = utils::sqlite; + +using cli::cmd_db_exec; + + +namespace { + + +/// Concatenates a vector into a string using ' ' as a separator. +/// +/// \param args The objects to join. This cannot be empty. +/// +/// \return The concatenation of all the objects in the set. +static std::string +flatten_args(const cmdline::args_vector& args) +{ + std::ostringstream output; + std::copy(args.begin(), args.end(), + std::ostream_iterator< std::string >(output, " ")); + + std::string result = output.str(); + result.erase(result.end() - 1); + return result; +} + + +} // anonymous namespace + + +/// Formats a particular cell of a statement result. +/// +/// \param stmt The statement whose cell to format. +/// \param index The index of the cell to format. +/// +/// \return A textual representation of the cell. +std::string +cli::format_cell(sqlite::statement& stmt, const int index) +{ + switch (stmt.column_type(index)) { + case sqlite::type_blob: { + const sqlite::blob blob = stmt.column_blob(index); + return F("BLOB of %s bytes") % blob.size; + } + + case sqlite::type_float: + return F("%s") % stmt.column_double(index); + + case sqlite::type_integer: + return F("%s") % stmt.column_int64(index); + + case sqlite::type_null: + return "NULL"; + + case sqlite::type_text: + return stmt.column_text(index); + } + + UNREACHABLE; +} + + +/// Formats the column names of a statement for output as CSV. +/// +/// \param stmt The statement whose columns to format. +/// +/// \return A comma-separated list of column names. +std::string +cli::format_headers(sqlite::statement& stmt) +{ + std::string output; + int i = 0; + for (; i < stmt.column_count() - 1; ++i) + output += stmt.column_name(i) + ','; + output += stmt.column_name(i); + return output; +} + + +/// Formats a row of a statement for output as CSV. +/// +/// \param stmt The statement whose current row to format. +/// +/// \return A comma-separated list of values. +std::string +cli::format_row(sqlite::statement& stmt) +{ + std::string output; + int i = 0; + for (; i < stmt.column_count() - 1; ++i) + output += cli::format_cell(stmt, i) + ','; + output += cli::format_cell(stmt, i); + return output; +} + + +/// Default constructor for cmd_db_exec. +cmd_db_exec::cmd_db_exec(void) : cli_command( + "db-exec", "sql_statement", 1, -1, + "Executes an arbitrary SQL statement in a results file and prints " + "the resulting table") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option("no-headers", "Do not show headers in the " + "output table")); +} + + +/// Entry point for the "db-exec" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_db_exec::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + try { + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + // TODO(jmmv): Shouldn't be using store::detail here... + sqlite::database db = store::detail::open_and_setup( + results_file, sqlite::open_readwrite); + sqlite::statement stmt = db.create_statement( + flatten_args(cmdline.arguments())); + + if (stmt.step()) { + if (!cmdline.has_option("no-headers")) + ui->out(cli::format_headers(stmt)); + do + ui->out(cli::format_row(stmt)); + while (stmt.step()); + } + + return EXIT_SUCCESS; + } catch (const sqlite::error& e) { + cmdline::print_error(ui, F("SQLite error: %s.") % e.what()); + return EXIT_FAILURE; + } catch (const store::error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + return EXIT_FAILURE; + } +} diff --git a/contrib/kyua/cli/cmd_db_exec.hpp b/contrib/kyua/cli/cmd_db_exec.hpp new file mode 100644 index 000000000000..18aa16108553 --- /dev/null +++ b/contrib/kyua/cli/cmd_db_exec.hpp @@ -0,0 +1,61 @@ +// 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 cli/cmd_db_exec.hpp +/// Provides the cmd_db_exec class. + +#if !defined(CLI_CMD_DB_EXEC_HPP) +#define CLI_CMD_DB_EXEC_HPP + +#include + +#include "cli/common.hpp" +#include "utils/sqlite/statement_fwd.hpp" + +namespace cli { + + +std::string format_cell(utils::sqlite::statement&, const int); +std::string format_headers(utils::sqlite::statement&); +std::string format_row(utils::sqlite::statement&); + + +/// Implementation of the "db-exec" subcommand. +class cmd_db_exec : public cli_command +{ +public: + cmd_db_exec(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + +#endif // !defined(CLI_CMD_DB_EXEC_HPP) diff --git a/contrib/kyua/cli/cmd_db_exec_test.cpp b/contrib/kyua/cli/cmd_db_exec_test.cpp new file mode 100644 index 000000000000..1bf6b2e074a9 --- /dev/null +++ b/contrib/kyua/cli/cmd_db_exec_test.cpp @@ -0,0 +1,165 @@ +// 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 "cli/cmd_db_exec.hpp" + +#include + +#include + +#include "utils/format/macros.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Performs a test for the cli::format_cell() function. +/// +/// \tparam Cell The type of the value to insert into the test column. +/// \param column_type The SQL type of the test column. +/// \param value The value to insert into the test column. +/// \param exp_value The expected return value of cli::format_cell(). +template< class Cell > +static void +do_format_cell_test(const std::string column_type, + const Cell& value, const std::string& exp_value) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + F("CREATE TABLE test (column %s)") % column_type); + create.step_without_results(); + + sqlite::statement insert = db.create_statement( + "INSERT INTO test (column) VALUES (:column)"); + insert.bind(":column", value); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + ATF_REQUIRE(query.step()); + ATF_REQUIRE_EQ(exp_value, cli::format_cell(query, 0)); + ATF_REQUIRE(!query.step()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__blob); +ATF_TEST_CASE_BODY(format_cell__blob) +{ + const char* contents = "Some random contents"; + do_format_cell_test( + "BLOB", sqlite::blob(contents, std::strlen(contents)), + F("BLOB of %s bytes") % strlen(contents)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__float); +ATF_TEST_CASE_BODY(format_cell__float) +{ + do_format_cell_test("FLOAT", 3.5, "3.5"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__integer); +ATF_TEST_CASE_BODY(format_cell__integer) +{ + do_format_cell_test("INTEGER", 123456, "123456"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__null); +ATF_TEST_CASE_BODY(format_cell__null) +{ + do_format_cell_test("TEXT", sqlite::null(), "NULL"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__text); +ATF_TEST_CASE_BODY(format_cell__text) +{ + do_format_cell_test("TEXT", "Hello, world", "Hello, world"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_headers); +ATF_TEST_CASE_BODY(format_headers) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + "CREATE TABLE test (c1 TEXT, c2 TEXT, c3 TEXT)"); + create.step_without_results(); + + sqlite::statement query = db.create_statement( + "SELECT c1, c2, c3 AS c3bis FROM test"); + ATF_REQUIRE_EQ("c1,c2,c3bis", cli::format_headers(query)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_row); +ATF_TEST_CASE_BODY(format_row) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + "CREATE TABLE test (c1 TEXT, c2 BLOB)"); + create.step_without_results(); + + const char* memory = "BLOB contents"; + sqlite::statement insert = db.create_statement( + "INSERT INTO test VALUES (:v1, :v2)"); + insert.bind(":v1", "A string"); + insert.bind(":v2", sqlite::blob(memory, std::strlen(memory))); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + query.step(); + ATF_REQUIRE_EQ( + (F("A string,BLOB of %s bytes") % std::strlen(memory)).str(), + cli::format_row(query)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, format_cell__blob); + ATF_ADD_TEST_CASE(tcs, format_cell__float); + ATF_ADD_TEST_CASE(tcs, format_cell__integer); + ATF_ADD_TEST_CASE(tcs, format_cell__null); + ATF_ADD_TEST_CASE(tcs, format_cell__text); + + ATF_ADD_TEST_CASE(tcs, format_headers); + + ATF_ADD_TEST_CASE(tcs, format_row); +} diff --git a/contrib/kyua/cli/cmd_db_migrate.cpp b/contrib/kyua/cli/cmd_db_migrate.cpp new file mode 100644 index 000000000000..c6076c6afa4d --- /dev/null +++ b/contrib/kyua/cli/cmd_db_migrate.cpp @@ -0,0 +1,82 @@ +// Copyright 2013 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 "cli/cmd_db_migrate.hpp" + +#include + +#include "cli/common.ipp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/migrate.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_db_migrate; + + +/// Default constructor for cmd_db_migrate. +cmd_db_migrate::cmd_db_migrate(void) : cli_command( + "db-migrate", "", 0, 0, + "Upgrades the schema of an existing results file to the currently " + "implemented version. A backup of the results file is created, but " + "this operation is not reversible") +{ + add_option(results_file_open_option); +} + + +/// Entry point for the "db-migrate" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_db_migrate::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + try { + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + store::migrate_schema(results_file); + return EXIT_SUCCESS; + } catch (const store::error& e) { + cmdline::print_error(ui, F("Migration failed: %s.") % e.what()); + return EXIT_FAILURE; + } +} diff --git a/contrib/kyua/cli/cmd_db_migrate.hpp b/contrib/kyua/cli/cmd_db_migrate.hpp new file mode 100644 index 000000000000..ebbe2b8a4ba4 --- /dev/null +++ b/contrib/kyua/cli/cmd_db_migrate.hpp @@ -0,0 +1,54 @@ +// Copyright 2013 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 cli/cmd_db_migrate.hpp +/// Provides the cmd_db_migrate class. + +#if !defined(CLI_CMD_DB_MIGRATE_HPP) +#define CLI_CMD_DB_MIGRATE_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "db-migrate" subcommand. +class cmd_db_migrate : public cli_command +{ +public: + cmd_db_migrate(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_DB_MIGRATE_HPP) diff --git a/contrib/kyua/cli/cmd_debug.cpp b/contrib/kyua/cli/cmd_debug.cpp new file mode 100644 index 000000000000..b7a29b7ab804 --- /dev/null +++ b/contrib/kyua/cli/cmd_debug.cpp @@ -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. + +#include "cli/cmd_debug.hpp" + +#include + +#include "cli/common.ipp" +#include "drivers/debug_test.hpp" +#include "engine/filters.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/format/macros.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_debug; + + +/// Default constructor for cmd_debug. +cmd_debug::cmd_debug(void) : cli_command( + "debug", "test_case", 1, 1, + "Executes a single test case providing facilities for debugging") +{ + add_option(build_root_option); + add_option(kyuafile_option); + + add_option(cmdline::path_option( + "stdout", "Where to direct the standard output of the test case", + "path", "/dev/stdout")); + + add_option(cmdline::path_option( + "stderr", "Where to direct the standard error of the test case", + "path", "/dev/stderr")); +} + + +/// Entry point for the "debug" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime debuguration of the program. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_debug::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const std::string& test_case_name = cmdline.arguments()[0]; + if (test_case_name.find(':') == std::string::npos) + throw cmdline::usage_error(F("'%s' is not a test case identifier " + "(missing ':'?)") % test_case_name); + const engine::test_filter filter = engine::test_filter::parse( + test_case_name); + + const drivers::debug_test::result result = drivers::debug_test::drive( + kyuafile_path(cmdline), build_root_path(cmdline), filter, user_config, + cmdline.get_option< cmdline::path_option >("stdout"), + cmdline.get_option< cmdline::path_option >("stderr")); + + ui->out(F("%s -> %s") % cli::format_test_case_id(result.test_case) % + cli::format_result(result.test_result)); + + return result.test_result.good() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/contrib/kyua/cli/cmd_debug.hpp b/contrib/kyua/cli/cmd_debug.hpp new file mode 100644 index 000000000000..2d9e8dee1797 --- /dev/null +++ b/contrib/kyua/cli/cmd_debug.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 cli/cmd_debug.hpp +/// Provides the cmd_debug class. + +#if !defined(CLI_CMD_DEBUG_HPP) +#define CLI_CMD_DEBUG_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "debug" subcommand. +class cmd_debug : public cli_command +{ +public: + cmd_debug(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_DEBUG_HPP) diff --git a/contrib/kyua/cli/cmd_debug_test.cpp b/contrib/kyua/cli/cmd_debug_test.cpp new file mode 100644 index 000000000000..28137e028962 --- /dev/null +++ b/contrib/kyua/cli/cmd_debug_test.cpp @@ -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. + +#include "cli/cmd_debug.hpp" + +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_filter); +ATF_TEST_CASE_BODY(invalid_filter) +{ + cmdline::args_vector args; + args.push_back("debug"); + args.push_back("incorrect:"); + + cli::cmd_debug cmd; + cmdline::ui_mock ui; + // TODO(jmmv): This error should really be cmdline::usage_error. + ATF_REQUIRE_THROW_RE(std::runtime_error, "Test case.*'incorrect:'.*empty", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filter_without_test_case); +ATF_TEST_CASE_BODY(filter_without_test_case) +{ + cmdline::args_vector args; + args.push_back("debug"); + args.push_back("program"); + + cli::cmd_debug cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::error, "'program'.*not a test case", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, invalid_filter); + ATF_ADD_TEST_CASE(tcs, filter_without_test_case); +} diff --git a/contrib/kyua/cli/cmd_help.cpp b/contrib/kyua/cli/cmd_help.cpp new file mode 100644 index 000000000000..9ebe6f50c852 --- /dev/null +++ b/contrib/kyua/cli/cmd_help.cpp @@ -0,0 +1,250 @@ +// 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 "cli/cmd_help.hpp" + +#include +#include + +#include "cli/common.ipp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/text/table.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace text = utils::text; + +using cli::cmd_help; + + +namespace { + + +/// Creates a table with the help of a set of options. +/// +/// \param options The set of options to describe. May be empty. +/// +/// \return A 2-column wide table with the description of the options. +static text::table +options_help(const cmdline::options_vector& options) +{ + text::table table(2); + + for (cmdline::options_vector::const_iterator iter = options.begin(); + iter != options.end(); iter++) { + const cmdline::base_option* option = *iter; + + std::string description = option->description(); + if (option->needs_arg() && option->has_default_value()) + description += F(" (default: %s)") % option->default_value(); + + text::table_row row; + + if (option->has_short_name()) + row.push_back(F("%s, %s") % option->format_short_name() % + option->format_long_name()); + else + row.push_back(F("%s") % option->format_long_name()); + row.push_back(F("%s.") % description); + + table.add_row(row); + } + + return table; +} + + +/// Prints the summary of commands and generic options. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param options The set of program-wide options for which to print help. +/// \param commands The set of commands for which to print help. +static void +general_help(cmdline::ui* ui, const cmdline::options_vector* options, + const cmdline::commands_map< cli::cli_command >* commands) +{ + PRE(!commands->empty()); + + cli::write_version_header(ui); + ui->out(""); + ui->out_tag_wrap( + "Usage: ", + F("%s [general_options] command [command_options] [args]") % + cmdline::progname(), false); + + const text::table options_table = options_help(*options); + text::widths_vector::value_type first_width = + options_table.column_width(0); + + std::map< std::string, text::table > command_tables; + + for (cmdline::commands_map< cli::cli_command >::const_iterator + iter = commands->begin(); iter != commands->end(); iter++) { + const std::string& category = (*iter).first; + const std::set< std::string >& command_names = (*iter).second; + + command_tables.insert(std::map< std::string, text::table >::value_type( + category, text::table(2))); + text::table& table = command_tables.find(category)->second; + + for (std::set< std::string >::const_iterator i2 = command_names.begin(); + i2 != command_names.end(); i2++) { + const cli::cli_command* command = commands->find(*i2); + text::table_row row; + row.push_back(command->name()); + row.push_back(F("%s.") % command->short_description()); + table.add_row(row); + } + + if (table.column_width(0) > first_width) + first_width = table.column_width(0); + } + + text::table_formatter formatter; + formatter.set_column_width(0, first_width); + formatter.set_column_width(1, text::table_formatter::width_refill); + formatter.set_separator(" "); + + if (!options_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available general options:"); + ui->out_table(options_table, formatter, " "); + } + + // Iterate using the same loop as above to preserve ordering. + for (cmdline::commands_map< cli::cli_command >::const_iterator + iter = commands->begin(); iter != commands->end(); iter++) { + const std::string& category = (*iter).first; + ui->out_wrap(""); + ui->out_wrap(F("%s commands:") % + (category.empty() ? "Generic" : category)); + ui->out_table(command_tables.find(category)->second, formatter, " "); + } + + ui->out_wrap(""); + ui->out_wrap("See kyua(1) for more details."); +} + + +/// Prints help for a particular subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param general_options The options that apply to all commands. +/// \param command Pointer to the command to describe. +static void +subcommand_help(cmdline::ui* ui, + const utils::cmdline::options_vector* general_options, + const cli::cli_command* command) +{ + cli::write_version_header(ui); + ui->out(""); + ui->out_tag_wrap( + "Usage: ", F("%s [general_options] %s%s%s") % + cmdline::progname() % command->name() % + (command->options().empty() ? "" : " [command_options]") % + (command->arg_list().empty() ? "" : (" " + command->arg_list())), + false); + ui->out_wrap(""); + ui->out_wrap(F("%s.") % command->short_description()); + + const text::table general_table = options_help(*general_options); + const text::table command_table = options_help(command->options()); + + const text::widths_vector::value_type first_width = + std::max(general_table.column_width(0), command_table.column_width(0)); + text::table_formatter formatter; + formatter.set_column_width(0, first_width); + formatter.set_column_width(1, text::table_formatter::width_refill); + formatter.set_separator(" "); + + if (!general_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available general options:"); + ui->out_table(general_table, formatter, " "); + } + + if (!command_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available command options:"); + ui->out_table(command_table, formatter, " "); + } + + ui->out_wrap(""); + ui->out_wrap(F("See kyua-%s(1) for more details.") % command->name()); +} + + +} // anonymous namespace + + +/// Default constructor for cmd_help. +/// +/// \param options_ The set of program-wide options for which to provide help. +/// \param commands_ The set of commands for which to provide help. +cmd_help::cmd_help(const cmdline::options_vector* options_, + const cmdline::commands_map< cli_command >* commands_) : + cli_command("help", "[subcommand]", 0, 1, "Shows usage information"), + _options(options_), + _commands(commands_) +{ +} + + +/// Entry point for the "help" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 to indicate success. +int +cmd_help::run(utils::cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + if (cmdline.arguments().empty()) { + general_help(ui, _options, _commands); + } else { + INV(cmdline.arguments().size() == 1); + const std::string& cmdname = cmdline.arguments()[0]; + const cli::cli_command* command = _commands->find(cmdname); + if (command == NULL) + throw cmdline::usage_error(F("The command %s does not exist") % + cmdname); + else + subcommand_help(ui, _options, command); + } + + return EXIT_SUCCESS; +} diff --git a/contrib/kyua/cli/cmd_help.hpp b/contrib/kyua/cli/cmd_help.hpp new file mode 100644 index 000000000000..5f3b19db901d --- /dev/null +++ b/contrib/kyua/cli/cmd_help.hpp @@ -0,0 +1,62 @@ +// 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 cli/cmd_help.hpp +/// Provides the cmd_help class. + +#if !defined(CLI_CMD_HELP_HPP) +#define CLI_CMD_HELP_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/commands_map_fwd.hpp" + +namespace cli { + + +/// Implementation of the "help" subcommand. +class cmd_help : public cli_command +{ + /// The set of program-wide options for which to provide help. + const utils::cmdline::options_vector* _options; + + /// The set of commands for which to provide help. + const utils::cmdline::commands_map< cli_command >* _commands; + +public: + cmd_help(const utils::cmdline::options_vector*, + const utils::cmdline::commands_map< cli_command >*); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_HELP_HPP) diff --git a/contrib/kyua/cli/cmd_help_test.cpp b/contrib/kyua/cli/cmd_help_test.cpp new file mode 100644 index 000000000000..d292090be451 --- /dev/null +++ b/contrib/kyua/cli/cmd_help_test.cpp @@ -0,0 +1,347 @@ +// 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 "cli/cmd_help.hpp" + +#include +#include +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/defs.hpp" +#include "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_help; + + +namespace { + + +/// Mock command with a simple definition (no options, no arguments). +/// +/// Attempting to run this command will result in a crash. It is only provided +/// to validate the generation of interactive help. +class cmd_mock_simple : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// \param name_ The name of the command to create. + cmd_mock_simple(const char* name_) : cli::cli_command( + name_, "", 0, 0, "Simple command") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + UNREACHABLE; + } +}; + + +/// Mock command with a complex definition (some options, some arguments). +/// +/// Attempting to run this command will result in a crash. It is only provided +/// to validate the generation of interactive help. +class cmd_mock_complex : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// \param name_ The name of the command to create. + cmd_mock_complex(const char* name_) : cli::cli_command( + name_, "[arg1 .. argN]", 0, 2, "Complex command") + { + add_option(cmdline::bool_option("flag_a", "Flag A")); + add_option(cmdline::bool_option('b', "flag_b", "Flag B")); + add_option(cmdline::string_option('c', "flag_c", "Flag C", "c_arg")); + add_option(cmdline::string_option("flag_d", "Flag D", "d_arg", "foo")); + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + UNREACHABLE; + } +}; + + +/// Initializes the cmdline library and generates the set of test commands. +/// +/// \param [out] commands A mapping that is updated to contain the commands to +/// use for testing. +static void +setup(cmdline::commands_map< cli::cli_command >& commands) +{ + cmdline::init("progname"); + + commands.insert(new cmd_mock_simple("mock_simple")); + commands.insert(new cmd_mock_complex("mock_complex")); + + commands.insert(new cmd_mock_simple("mock_simple_2"), "First"); + commands.insert(new cmd_mock_complex("mock_complex_2"), "First"); + + commands.insert(new cmd_mock_simple("mock_simple_3"), "Second"); +} + + +/// Performs a test on the global help (not that of a subcommand). +/// +/// \param general_options The genral options supported by the tool, if any. +/// \param expected_options Expected lines of help output documenting the +/// options in general_options. +/// \param ui The cmdline::mock_ui object to which to write the output. +static void +global_test(const cmdline::options_vector& general_options, + const std::vector< std::string >& expected_options, + cmdline::ui_mock& ui) +{ + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + + cmd_help cmd(&general_options, &mock_commands); + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + + std::vector< std::string > expected; + + expected.push_back(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); + expected.push_back(""); + expected.push_back("Usage: progname [general_options] command " + "[command_options] [args]"); + if (!general_options.empty()) { + expected.push_back(""); + expected.push_back("Available general options:"); + std::copy(expected_options.begin(), expected_options.end(), + std::back_inserter(expected)); + } + expected.push_back(""); + expected.push_back("Generic commands:"); + expected.push_back(" mock_complex Complex command."); + expected.push_back(" mock_simple Simple command."); + expected.push_back(""); + expected.push_back("First commands:"); + expected.push_back(" mock_complex_2 Complex command."); + expected.push_back(" mock_simple_2 Simple command."); + expected.push_back(""); + expected.push_back("Second commands:"); + expected.push_back(" mock_simple_3 Simple command."); + expected.push_back(""); + expected.push_back("See kyua(1) for more details."); + + ATF_REQUIRE(expected == ui.out_log()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(global__no_options); +ATF_TEST_CASE_BODY(global__no_options) +{ + cmdline::ui_mock ui; + + cmdline::options_vector general_options; + + global_test(general_options, std::vector< std::string >(), ui); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(global__some_options); +ATF_TEST_CASE_BODY(global__some_options) +{ + cmdline::ui_mock ui; + + cmdline::options_vector general_options; + const cmdline::bool_option flag_a("flag_a", "Flag A"); + general_options.push_back(&flag_a); + const cmdline::string_option flag_c('c', "lc", "Flag C", "X"); + general_options.push_back(&flag_c); + + std::vector< std::string > expected; + expected.push_back(" --flag_a Flag A."); + expected.push_back(" -c X, --lc=X Flag C."); + + global_test(general_options, expected, ui); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__simple); +ATF_TEST_CASE_BODY(subcommand__simple) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_simple"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(atf::utils::grep_collection( + "^kyua.*" PACKAGE_VERSION, ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^Usage: progname \\[general_options\\] mock_simple$", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection( + "Available.*options", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^See kyua-mock_simple\\(1\\) for more details.", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__complex); +ATF_TEST_CASE_BODY(subcommand__complex) +{ + cmdline::options_vector general_options; + const cmdline::bool_option global_a("global_a", "Global A"); + general_options.push_back(&global_a); + const cmdline::string_option global_c('c', "global_c", "Global C", + "c_global"); + general_options.push_back(&global_c); + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_complex"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(atf::utils::grep_collection( + "^kyua.*" PACKAGE_VERSION, ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^Usage: progname \\[general_options\\] mock_complex " + "\\[command_options\\] \\[arg1 .. argN\\]$", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Available general options", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--global_a", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--global_c=c_global", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Available command options", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--flag_a *Flag A", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("-b.*--flag_b *Flag B", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "-c c_arg.*--flag_c=c_arg *Flag C", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "--flag_d=d_arg *Flag D.*default.*foo", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^See kyua-mock_complex\\(1\\) for more details.", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__unknown); +ATF_TEST_CASE_BODY(subcommand__unknown) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("foobar"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "command foobar.*not exist", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_args); +ATF_TEST_CASE_BODY(invalid_args) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_simple"); + args.push_back("mock_complex"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, global__no_options); + ATF_ADD_TEST_CASE(tcs, global__some_options); + ATF_ADD_TEST_CASE(tcs, subcommand__simple); + ATF_ADD_TEST_CASE(tcs, subcommand__complex); + ATF_ADD_TEST_CASE(tcs, subcommand__unknown); + ATF_ADD_TEST_CASE(tcs, invalid_args); +} diff --git a/contrib/kyua/cli/cmd_list.cpp b/contrib/kyua/cli/cmd_list.cpp new file mode 100644 index 000000000000..ed0e4980fc47 --- /dev/null +++ b/contrib/kyua/cli/cmd_list.cpp @@ -0,0 +1,161 @@ +// 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 "cli/cmd_list.hpp" + +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/list_tests.hpp" +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/types.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Hooks for list_tests to print test cases as they come. +class progress_hooks : public drivers::list_tests::base_hooks { + /// The ui object to which to print the test cases. + cmdline::ui* _ui; + + /// Whether to print test case details or just their names. + bool _verbose; + +public: + /// Initializes the hooks. + /// + /// \param ui_ The ui object to which to print the test cases. + /// \param verbose_ Whether to print test case details or just their names. + progress_hooks(cmdline::ui* ui_, const bool verbose_) : + _ui(ui_), + _verbose(verbose_) + { + } + + /// Reports a test case as soon as it is found. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the located test case. + void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + cli::detail::list_test_case(_ui, _verbose, test_program, + test_case_name); + } +}; + + +} // anonymous namespace + + +/// Lists a single test case. +/// +/// \param [out] ui Object to interact with the I/O of the program. +/// \param verbose Whether to be verbose or not. +/// \param test_program The test program containing the test case to print. +/// \param test_case_name The name of the test case to print. +void +cli::detail::list_test_case(cmdline::ui* ui, const bool verbose, + const model::test_program& test_program, + const std::string& test_case_name) +{ + const model::test_case& test_case = test_program.find(test_case_name); + + const std::string id = format_test_case_id(test_program, test_case_name); + if (!verbose) { + ui->out(id); + } else { + ui->out(F("%s (%s)") % id % test_program.test_suite_name()); + + // TODO(jmmv): Running these for every test case is probably not the + // fastest thing to do. + const model::metadata default_md = model::metadata_builder().build(); + const model::properties_map default_props = default_md.to_properties(); + + const model::metadata& test_md = test_case.get_metadata(); + const model::properties_map test_props = test_md.to_properties(); + + for (model::properties_map::const_iterator iter = test_props.begin(); + iter != test_props.end(); iter++) { + const model::properties_map::const_iterator default_iter = + default_props.find((*iter).first); + if (default_iter == default_props.end() || + (*iter).second != (*default_iter).second) + ui->out(F(" %s = %s") % (*iter).first % (*iter).second); + } + } +} + + +/// Default constructor for cmd_list. +cli::cmd_list::cmd_list(void) : + cli_command("list", "[test-program ...]", 0, -1, + "Lists test cases and their meta-data") +{ + add_option(build_root_option); + add_option(kyuafile_option); + add_option(cmdline::bool_option('v', "verbose", "Show properties")); +} + + +/// Entry point for the "list" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 to indicate success. +int +cli::cmd_list::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + progress_hooks hooks(ui, cmdline.has_option("verbose")); + const drivers::list_tests::result result = drivers::list_tests::drive( + kyuafile_path(cmdline), build_root_path(cmdline), + parse_filters(cmdline.arguments()), user_config, hooks); + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/contrib/kyua/cli/cmd_list.hpp b/contrib/kyua/cli/cmd_list.hpp new file mode 100644 index 000000000000..cbdc084a6e16 --- /dev/null +++ b/contrib/kyua/cli/cmd_list.hpp @@ -0,0 +1,65 @@ +// 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 cli/cmd_list.hpp +/// Provides the cmd_list class. + +#if !defined(CLI_CMD_LIST_HPP) +#define CLI_CMD_LIST_HPP + +#include + +#include "cli/common.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace cli { + + +namespace detail { + +void list_test_case(utils::cmdline::ui*, const bool, const model::test_program&, + const std::string&); + +} // namespace detail + + +/// Implementation of the "list" subcommand. +class cmd_list : public cli_command +{ +public: + cmd_list(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + +#endif // !defined(CLI_CMD_LIST_HPP) diff --git a/contrib/kyua/cli/cmd_list_test.cpp b/contrib/kyua/cli/cmd_list_test.cpp new file mode 100644 index 000000000000..19078abd7d48 --- /dev/null +++ b/contrib/kyua/cli/cmd_list_test.cpp @@ -0,0 +1,112 @@ +// 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 "cli/cmd_list.hpp" + +#include + +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__no_verbose); +ATF_TEST_CASE_BODY(list_test_case__no_verbose) +{ + const model::metadata md = model::metadata_builder() + .set_description("This should not be shown") + .build(); + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("the/test-program"), fs::path("root"), "suite") + .add_test_case("abc", md) + .set_metadata(md) + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, false, test_program, "abc"); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("the/test-program:abc", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__verbose__no_properties); +ATF_TEST_CASE_BODY(list_test_case__verbose__no_properties) +{ + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("hello/world"), fs::path("root"), "the-suite") + .add_test_case("my_name") + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, true, test_program, "my_name"); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("hello/world:my_name (the-suite)", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__verbose__some_properties); +ATF_TEST_CASE_BODY(list_test_case__verbose__some_properties) +{ + const model::metadata md = model::metadata_builder() + .add_custom("my-property", "value") + .set_description("Some description") + .set_has_cleanup(true) + .build(); + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("hello/world"), fs::path("root"), "the-suite") + .add_test_case("my_name", md) + .set_metadata(md) + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, true, test_program, "my_name"); + ATF_REQUIRE_EQ(4, ui.out_log().size()); + ATF_REQUIRE_EQ("hello/world:my_name (the-suite)", ui.out_log()[0]); + ATF_REQUIRE_EQ(" custom.my-property = value", ui.out_log()[1]); + ATF_REQUIRE_EQ(" description = Some description", ui.out_log()[2]); + ATF_REQUIRE_EQ(" has_cleanup = true", ui.out_log()[3]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, list_test_case__no_verbose); + ATF_ADD_TEST_CASE(tcs, list_test_case__verbose__no_properties); + ATF_ADD_TEST_CASE(tcs, list_test_case__verbose__some_properties); + + // Tests for cmd_list::run are located in integration/cmd_list_test. +} diff --git a/contrib/kyua/cli/cmd_report.cpp b/contrib/kyua/cli/cmd_report.cpp new file mode 100644 index 000000000000..27827e893de7 --- /dev/null +++ b/contrib/kyua/cli/cmd_report.cpp @@ -0,0 +1,421 @@ +// 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 "cli/cmd_report.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/scan_results.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "model/types.hpp" +#include "store/layout.hpp" +#include "store/read_transaction.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/text/operations.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using cli::cmd_report; +using utils::optional; + + +namespace { + + +/// Generates a plain-text report intended to be printed to the console. +class report_console_hooks : public drivers::scan_results::base_hooks { + /// Stream to which to write the report. + std::ostream& _output; + + /// Whether to include details in the report or not. + const bool _verbose; + + /// Collection of result types to include in the report. + const cli::result_types& _results_filters; + + /// Path to the results file being read. + const fs::path& _results_file; + + /// The start time of the first test. + optional< utils::datetime::timestamp > _start_time; + + /// The end time of the last test. + optional< utils::datetime::timestamp > _end_time; + + /// The total run time of the tests. Note that we cannot subtract _end_time + /// from _start_time to compute this due to parallel execution. + utils::datetime::delta _runtime; + + /// Representation of a single result. + struct result_data { + /// The relative path to the test program. + utils::fs::path binary_path; + + /// The name of the test case. + std::string test_case_name; + + /// The result of the test case. + model::test_result result; + + /// The duration of the test case execution. + utils::datetime::delta duration; + + /// Constructs a new results data. + /// + /// \param binary_path_ The relative path to the test program. + /// \param test_case_name_ The name of the test case. + /// \param result_ The result of the test case. + /// \param duration_ The duration of the test case execution. + result_data(const utils::fs::path& binary_path_, + const std::string& test_case_name_, + const model::test_result& result_, + const utils::datetime::delta& duration_) : + binary_path(binary_path_), test_case_name(test_case_name_), + result(result_), duration(duration_) + { + } + }; + + /// Results received, broken down by their type. + /// + /// Note that this may not include all results, as keeping the whole list in + /// memory may be too much. + std::map< model::test_result_type, std::vector< result_data > > _results; + + /// Pretty-prints the value of an environment variable. + /// + /// \param indent Prefix for the lines to print. Continuation lines + /// use this indentation twice. + /// \param name Name of the variable. + /// \param value Value of the variable. Can have newlines. + void + print_env_var(const char* indent, const std::string& name, + const std::string& value) + { + const std::vector< std::string > lines = text::split(value, '\n'); + if (lines.size() == 0) { + _output << F("%s%s=\n") % indent % name;; + } else { + _output << F("%s%s=%s\n") % indent % name % lines[0]; + for (std::vector< std::string >::size_type i = 1; + i < lines.size(); ++i) { + _output << F("%s%s%s\n") % indent % indent % lines[i]; + } + } + } + + /// Prints the execution context to the output. + /// + /// \param context The context to dump. + void + print_context(const model::context& context) + { + _output << "===> Execution context\n"; + + _output << F("Current directory: %s\n") % context.cwd(); + const std::map< std::string, std::string >& env = context.env(); + if (env.empty()) + _output << "No environment variables recorded\n"; + else { + _output << "Environment variables:\n"; + for (std::map< std::string, std::string >::const_iterator + iter = env.begin(); iter != env.end(); iter++) { + print_env_var(" ", (*iter).first, (*iter).second); + } + } + } + + /// Dumps a detailed view of the test case. + /// + /// \param result_iter Results iterator pointing at the test case to be + /// dumped. + void + print_test_case_and_result(const store::results_iterator& result_iter) + { + const model::test_case& test_case = + result_iter.test_program()->find(result_iter.test_case_name()); + const model::properties_map props = + test_case.get_metadata().to_properties(); + + _output << F("===> %s:%s\n") % + result_iter.test_program()->relative_path() % + result_iter.test_case_name(); + _output << F("Result: %s\n") % + cli::format_result(result_iter.result()); + _output << F("Start time: %s\n") % + result_iter.start_time().to_iso8601_in_utc(); + _output << F("End time: %s\n") % + result_iter.end_time().to_iso8601_in_utc(); + _output << F("Duration: %s\n") % + cli::format_delta(result_iter.end_time() - + result_iter.start_time()); + + _output << "\n"; + _output << "Metadata:\n"; + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + if ((*iter).second.empty()) { + _output << F(" %s is empty\n") % (*iter).first; + } else { + _output << F(" %s = %s\n") % (*iter).first % (*iter).second; + } + } + + const std::string stdout_contents = result_iter.stdout_contents(); + if (!stdout_contents.empty()) { + _output << "\n" + << "Standard output:\n" + << stdout_contents; + } + + const std::string stderr_contents = result_iter.stderr_contents(); + if (!stderr_contents.empty()) { + _output << "\n" + << "Standard error:\n" + << stderr_contents; + } + } + + /// Counts how many results of a given type have been received. + /// + /// \param type Test result type to count results for. + /// + /// \return The number of test results with \p type. + std::size_t + count_results(const model::test_result_type type) + { + const std::map< model::test_result_type, + std::vector< result_data > >::const_iterator iter = + _results.find(type); + if (iter == _results.end()) + return 0; + else + return (*iter).second.size(); + } + + /// Prints a set of results. + /// + /// \param type Test result type to print results for. + /// \param title Title used when printing results. + void + print_results(const model::test_result_type type, + const char* title) + { + const std::map< model::test_result_type, + std::vector< result_data > >::const_iterator iter2 = + _results.find(type); + if (iter2 == _results.end()) + return; + const std::vector< result_data >& all = (*iter2).second; + + _output << F("===> %s\n") % title; + for (std::vector< result_data >::const_iterator iter = all.begin(); + iter != all.end(); iter++) { + _output << F("%s:%s -> %s [%s]\n") % (*iter).binary_path % + (*iter).test_case_name % + cli::format_result((*iter).result) % + cli::format_delta((*iter).duration); + } + } + +public: + /// Constructor for the hooks. + /// + /// \param [out] output_ Stream to which to write the report. + /// \param verbose_ Whether to include details in the output or not. + /// \param results_filters_ The result types to include in the report. + /// Cannot be empty. + /// \param results_file_ Path to the results file being read. + report_console_hooks(std::ostream& output_, const bool verbose_, + const cli::result_types& results_filters_, + const fs::path& results_file_) : + _output(output_), + _verbose(verbose_), + _results_filters(results_filters_), + _results_file(results_file_) + { + PRE(!results_filters_.empty()); + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void + got_context(const model::context& context) + { + if (_verbose) + print_context(context); + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void + got_result(store::results_iterator& iter) + { + if (!_start_time || _start_time.get() > iter.start_time()) + _start_time = iter.start_time(); + if (!_end_time || _end_time.get() < iter.end_time()) + _end_time = iter.end_time(); + + const datetime::delta duration = iter.end_time() - iter.start_time(); + + _runtime += duration; + const model::test_result result = iter.result(); + _results[result.type()].push_back( + result_data(iter.test_program()->relative_path(), + iter.test_case_name(), iter.result(), duration)); + + if (_verbose) { + // TODO(jmmv): _results_filters is a list and is small enough for + // std::find to not be an expensive operation here (probably). But + // we should be using a std::set instead. + if (std::find(_results_filters.begin(), _results_filters.end(), + iter.result().type()) != _results_filters.end()) { + print_test_case_and_result(iter); + } + } + } + + /// Prints the tests summary. + void + end(const drivers::scan_results::result& /* r */) + { + typedef std::map< model::test_result_type, const char* > types_map; + + types_map titles; + titles[model::test_result_broken] = "Broken tests"; + titles[model::test_result_expected_failure] = "Expected failures"; + titles[model::test_result_failed] = "Failed tests"; + titles[model::test_result_passed] = "Passed tests"; + titles[model::test_result_skipped] = "Skipped tests"; + + for (cli::result_types::const_iterator iter = _results_filters.begin(); + iter != _results_filters.end(); ++iter) { + const types_map::const_iterator match = titles.find(*iter); + INV_MSG(match != titles.end(), "Conditional does not match user " + "input validation in parse_types()"); + print_results((*match).first, (*match).second); + } + + const std::size_t broken = count_results(model::test_result_broken); + const std::size_t failed = count_results(model::test_result_failed); + const std::size_t passed = count_results(model::test_result_passed); + const std::size_t skipped = count_results(model::test_result_skipped); + const std::size_t xfail = count_results( + model::test_result_expected_failure); + const std::size_t total = broken + failed + passed + skipped + xfail; + + _output << "===> Summary\n"; + _output << F("Results read from %s\n") % _results_file; + _output << F("Test cases: %s total, %s skipped, %s expected failures, " + "%s broken, %s failed\n") % + total % skipped % xfail % broken % failed; + if (_verbose && _start_time) { + INV(_end_time); + _output << F("Start time: %s\n") % + _start_time.get().to_iso8601_in_utc(); + _output << F("End time: %s\n") % + _end_time.get().to_iso8601_in_utc(); + } + _output << F("Total time: %s\n") % cli::format_delta(_runtime); + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_report. +cmd_report::cmd_report(void) : cli_command( + "report", "", 0, -1, + "Generates a report with the results of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option( + "verbose", "Include the execution context and the details of each test " + "case in the report")); + add_option(cmdline::path_option("output", "Path to the output file", "path", + "/dev/stdout")); + add_option(results_filter_option); +} + + +/// Entry point for the "report" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_report::run(cmdline::ui* ui, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + std::auto_ptr< std::ostream > output = utils::open_ostream( + cmdline.get_option< cmdline::path_option >("output")); + + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + const result_types types = get_result_types(cmdline); + report_console_hooks hooks(*output.get(), cmdline.has_option("verbose"), + types, results_file); + const drivers::scan_results::result result = drivers::scan_results::drive( + results_file, parse_filters(cmdline.arguments()), hooks); + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/contrib/kyua/cli/cmd_report.hpp b/contrib/kyua/cli/cmd_report.hpp new file mode 100644 index 000000000000..3d73c592ed9b --- /dev/null +++ b/contrib/kyua/cli/cmd_report.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 cli/cmd_report.hpp +/// Provides the cmd_report class. + +#if !defined(CLI_CMD_REPORT_HPP) +#define CLI_CMD_REPORT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "report" subcommand. +class cmd_report : public cli_command +{ +public: + cmd_report(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_HPP) diff --git a/contrib/kyua/cli/cmd_report_html.cpp b/contrib/kyua/cli/cmd_report_html.cpp new file mode 100644 index 000000000000..b2133a8de047 --- /dev/null +++ b/contrib/kyua/cli/cmd_report_html.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 "cli/cmd_report_html.hpp" + +#include +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "store/read_transaction.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/text/templates.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using utils::optional; + + +namespace { + + +/// Creates the report's top directory and fails if it exists. +/// +/// \param directory The directory to create. +/// \param force Whether to wipe an existing directory or not. +/// +/// \throw std::runtime_error If the directory already exists; this is a user +/// error that the user must correct. +/// \throw fs::error If the directory creation fails for any other reason. +static void +create_top_directory(const fs::path& directory, const bool force) +{ + if (force) { + if (fs::exists(directory)) + fs::rm_r(directory); + } + + try { + fs::mkdir(directory, 0755); + } catch (const fs::system_error& e) { + if (e.original_errno() == EEXIST) + throw std::runtime_error(F("Output directory '%s' already exists; " + "maybe use --force?") % + directory); + else + throw e; + } +} + + +/// Generates a flat unique filename for a given test case. +/// +/// \param test_program The test program for which to genereate the name. +/// \param test_case_name The test case name. +/// +/// \return A filename unique within a directory with a trailing HTML extension. +static std::string +test_case_filename(const model::test_program& test_program, + const std::string& test_case_name) +{ + static const char* special_characters = "/:"; + + std::string name = cli::format_test_case_id(test_program, test_case_name); + std::string::size_type pos = name.find_first_of(special_characters); + while (pos != std::string::npos) { + name.replace(pos, 1, "_"); + pos = name.find_first_of(special_characters, pos + 1); + } + return name + ".html"; +} + + +/// Adds a string to string map to the templates. +/// +/// \param [in,out] templates The templates to add the map to. +/// \param props The map to add to the templates. +/// \param key_vector Name of the template vector that holds the keys. +/// \param value_vector Name of the template vector that holds the values. +static void +add_map(text::templates_def& templates, const config::properties_map& props, + const std::string& key_vector, const std::string& value_vector) +{ + templates.add_vector(key_vector); + templates.add_vector(value_vector); + + for (config::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + templates.add_to_vector(key_vector, (*iter).first); + templates.add_to_vector(value_vector, (*iter).second); + } +} + + +/// Generates an HTML report. +class html_hooks : public drivers::scan_results::base_hooks { + /// User interface object where to report progress. + cmdline::ui* _ui; + + /// The top directory in which to create the HTML files. + fs::path _directory; + + /// Collection of result types to include in the report. + const cli::result_types& _results_filters; + + /// The start time of the first test. + optional< utils::datetime::timestamp > _start_time; + + /// The end time of the last test. + optional< utils::datetime::timestamp > _end_time; + + /// The total run time of the tests. Note that we cannot subtract _end_time + /// from _start_time to compute this due to parallel execution. + utils::datetime::delta _runtime; + + /// Templates accumulator to generate the index.html file. + text::templates_def _summary_templates; + + /// Mapping of result types to the amount of tests with such result. + std::map< model::test_result_type, std::size_t > _types_count; + + /// Generates a common set of templates for all of our files. + /// + /// \return A new templates object with common parameters. + static text::templates_def + common_templates(void) + { + text::templates_def templates; + templates.add_variable("css", "report.css"); + return templates; + } + + /// Adds a test case result to the summary. + /// + /// \param test_program The test program with the test case to be added. + /// \param test_case_name Name of the test case. + /// \param result The result of the test case. + /// \param has_detail If true, the result of the test case has not been + /// filtered and therefore there exists a separate file for the test + /// with all of its information. + void + add_to_summary(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const bool has_detail) + { + ++_types_count[result.type()]; + + if (!has_detail) + return; + + std::string test_cases_vector; + std::string test_cases_file_vector; + switch (result.type()) { + case model::test_result_broken: + test_cases_vector = "broken_test_cases"; + test_cases_file_vector = "broken_test_cases_file"; + break; + + case model::test_result_expected_failure: + test_cases_vector = "xfail_test_cases"; + test_cases_file_vector = "xfail_test_cases_file"; + break; + + case model::test_result_failed: + test_cases_vector = "failed_test_cases"; + test_cases_file_vector = "failed_test_cases_file"; + break; + + case model::test_result_passed: + test_cases_vector = "passed_test_cases"; + test_cases_file_vector = "passed_test_cases_file"; + break; + + case model::test_result_skipped: + test_cases_vector = "skipped_test_cases"; + test_cases_file_vector = "skipped_test_cases_file"; + break; + } + INV(!test_cases_vector.empty()); + INV(!test_cases_file_vector.empty()); + + _summary_templates.add_to_vector( + test_cases_vector, + cli::format_test_case_id(test_program, test_case_name)); + _summary_templates.add_to_vector( + test_cases_file_vector, + test_case_filename(test_program, test_case_name)); + } + + /// Instantiate a template to generate an HTML file in the output directory. + /// + /// \param templates The templates to use. + /// \param template_name The name of the template. This is automatically + /// searched for in the installed directory, so do not provide a path. + /// \param output_name The name of the output file. This is a basename to + /// be created within the output directory. + /// + /// \throw text::error If there is any problem applying the templates. + void + generate(const text::templates_def& templates, + const std::string& template_name, + const std::string& output_name) const + { + const fs::path miscdir(utils::getenv_with_default( + "KYUA_MISCDIR", KYUA_MISCDIR)); + const fs::path template_file = miscdir / template_name; + const fs::path output_path(_directory / output_name); + + _ui->out(F("Generating %s") % output_path); + text::instantiate(templates, template_file, output_path); + } + + /// Gets the number of tests with a given result type. + /// + /// \param type The type to be queried. + /// + /// \return The number of tests of the given type, or 0 if none have yet + /// been registered by add_to_summary(). + std::size_t + get_count(const model::test_result_type type) const + { + const std::map< model::test_result_type, std::size_t >::const_iterator + iter = _types_count.find(type); + if (iter == _types_count.end()) + return 0; + else + return (*iter).second; + } + +public: + /// Constructor for the hooks. + /// + /// \param ui_ User interface object where to report progress. + /// \param directory_ The directory in which to create the HTML files. + /// \param results_filters_ The result types to include in the report. + /// Cannot be empty. + html_hooks(cmdline::ui* ui_, const fs::path& directory_, + const cli::result_types& results_filters_) : + _ui(ui_), + _directory(directory_), + _results_filters(results_filters_), + _summary_templates(common_templates()) + { + PRE(!results_filters_.empty()); + + // Keep in sync with add_to_summary(). + _summary_templates.add_vector("broken_test_cases"); + _summary_templates.add_vector("broken_test_cases_file"); + _summary_templates.add_vector("xfail_test_cases"); + _summary_templates.add_vector("xfail_test_cases_file"); + _summary_templates.add_vector("failed_test_cases"); + _summary_templates.add_vector("failed_test_cases_file"); + _summary_templates.add_vector("passed_test_cases"); + _summary_templates.add_vector("passed_test_cases_file"); + _summary_templates.add_vector("skipped_test_cases"); + _summary_templates.add_vector("skipped_test_cases_file"); + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void + got_context(const model::context& context) + { + text::templates_def templates = common_templates(); + templates.add_variable("cwd", context.cwd().str()); + add_map(templates, context.env(), "env_var", "env_var_value"); + generate(templates, "context.html", "context.html"); + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void + got_result(store::results_iterator& iter) + { + const model::test_program_ptr test_program = iter.test_program(); + const std::string& test_case_name = iter.test_case_name(); + const model::test_result result = iter.result(); + + if (std::find(_results_filters.begin(), _results_filters.end(), + result.type()) == _results_filters.end()) { + add_to_summary(*test_program, test_case_name, result, false); + return; + } + + add_to_summary(*test_program, test_case_name, result, true); + + if (!_start_time || _start_time.get() > iter.start_time()) + _start_time = iter.start_time(); + if (!_end_time || _end_time.get() < iter.end_time()) + _end_time = iter.end_time(); + + const datetime::delta duration = iter.end_time() - iter.start_time(); + + _runtime += duration; + + text::templates_def templates = common_templates(); + templates.add_variable("test_case", + cli::format_test_case_id(*test_program, + test_case_name)); + templates.add_variable("test_program", + test_program->absolute_path().str()); + templates.add_variable("result", cli::format_result(result)); + templates.add_variable("start_time", + iter.start_time().to_iso8601_in_utc()); + templates.add_variable("end_time", + iter.end_time().to_iso8601_in_utc()); + templates.add_variable("duration", cli::format_delta(duration)); + + const model::test_case& test_case = test_program->find(test_case_name); + add_map(templates, test_case.get_metadata().to_properties(), + "metadata_var", "metadata_value"); + + { + const std::string stdout_text = iter.stdout_contents(); + if (!stdout_text.empty()) + templates.add_variable("stdout", stdout_text); + } + { + const std::string stderr_text = iter.stderr_contents(); + if (!stderr_text.empty()) + templates.add_variable("stderr", stderr_text); + } + + generate(templates, "test_result.html", + test_case_filename(*test_program, test_case_name)); + } + + /// Writes the index.html file in the output directory. + /// + /// This should only be called once all the processing has been done; + /// i.e. when the scan_results driver returns. + void + write_summary(void) + { + const std::size_t n_passed = get_count(model::test_result_passed); + const std::size_t n_failed = get_count(model::test_result_failed); + const std::size_t n_skipped = get_count(model::test_result_skipped); + const std::size_t n_xfail = get_count( + model::test_result_expected_failure); + const std::size_t n_broken = get_count(model::test_result_broken); + + const std::size_t n_bad = n_broken + n_failed; + + if (_start_time) { + INV(_end_time); + _summary_templates.add_variable( + "start_time", _start_time.get().to_iso8601_in_utc()); + _summary_templates.add_variable( + "end_time", _end_time.get().to_iso8601_in_utc()); + } else { + _summary_templates.add_variable("start_time", "No tests run"); + _summary_templates.add_variable("end_time", "No tests run"); + } + _summary_templates.add_variable("duration", + cli::format_delta(_runtime)); + _summary_templates.add_variable("passed_tests_count", + F("%s") % n_passed); + _summary_templates.add_variable("failed_tests_count", + F("%s") % n_failed); + _summary_templates.add_variable("skipped_tests_count", + F("%s") % n_skipped); + _summary_templates.add_variable("xfail_tests_count", + F("%s") % n_xfail); + _summary_templates.add_variable("broken_tests_count", + F("%s") % n_broken); + _summary_templates.add_variable("bad_tests_count", F("%s") % n_bad); + + generate(text::templates_def(), "report.css", "report.css"); + generate(_summary_templates, "index.html", "index.html"); + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_report_html. +cli::cmd_report_html::cmd_report_html(void) : cli_command( + "report-html", "", 0, 0, + "Generates an HTML report with the result of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option( + "force", "Wipe the output directory before generating the new report; " + "use care")); + add_option(cmdline::path_option( + "output", "The directory in which to store the HTML files", + "path", "html")); + add_option(cmdline::list_option( + "results-filter", "Comma-separated list of result types to include in " + "the report", "types", "skipped,xfail,broken,failed")); +} + + +/// Entry point for the "report-html" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cli::cmd_report_html::run(cmdline::ui* ui, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const result_types types = get_result_types(cmdline); + + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + const fs::path directory = + cmdline.get_option< cmdline::path_option >("output"); + create_top_directory(directory, cmdline.has_option("force")); + html_hooks hooks(ui, directory, types); + drivers::scan_results::drive(results_file, + std::set< engine::test_filter >(), + hooks); + hooks.write_summary(); + + return EXIT_SUCCESS; +} diff --git a/contrib/kyua/cli/cmd_report_html.hpp b/contrib/kyua/cli/cmd_report_html.hpp new file mode 100644 index 000000000000..fadc138293ad --- /dev/null +++ b/contrib/kyua/cli/cmd_report_html.hpp @@ -0,0 +1,55 @@ +// 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 cli/cmd_report_html.hpp +/// Provides the cmd_report_html class. + +#if !defined(CLI_CMD_REPORT_HTML_HPP) +#define CLI_CMD_REPORT_HTML_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/ui_fwd.hpp" + +namespace cli { + + +/// Implementation of the "report-html" subcommand. +class cmd_report_html : public cli_command +{ +public: + cmd_report_html(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_HTML_HPP) diff --git a/contrib/kyua/cli/cmd_report_junit.cpp b/contrib/kyua/cli/cmd_report_junit.cpp new file mode 100644 index 000000000000..c4846c8795f2 --- /dev/null +++ b/contrib/kyua/cli/cmd_report_junit.cpp @@ -0,0 +1,89 @@ +// 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 "cli/cmd_report_junit.hpp" + +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/report_junit.hpp" +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/defs.hpp" +#include "utils/optional.ipp" +#include "utils/stream.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_report_junit; +using utils::optional; + + +/// Default constructor for cmd_report. +cmd_report_junit::cmd_report_junit(void) : cli_command( + "report-junit", "", 0, 0, + "Generates a JUnit report with the result of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::path_option("output", "Path to the output file", "path", + "/dev/stdout")); +} + + +/// Entry point for the "report" subcommand. +/// +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_report_junit::run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + std::auto_ptr< std::ostream > output = utils::open_ostream( + cmdline.get_option< cmdline::path_option >("output")); + + drivers::report_junit_hooks hooks(*output.get()); + drivers::scan_results::drive(results_file, + std::set< engine::test_filter >(), + hooks); + + return EXIT_SUCCESS; +} diff --git a/contrib/kyua/cli/cmd_report_junit.hpp b/contrib/kyua/cli/cmd_report_junit.hpp new file mode 100644 index 000000000000..1dc0bb731645 --- /dev/null +++ b/contrib/kyua/cli/cmd_report_junit.hpp @@ -0,0 +1,54 @@ +// 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 cli/cmd_report_junit.hpp +/// Provides the cmd_report_junit class. + +#if !defined(CLI_CMD_REPORT_JUNIT_HPP) +#define CLI_CMD_REPORT_JUNIT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "report-junit" subcommand. +class cmd_report_junit : public cli_command +{ +public: + cmd_report_junit(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_JUNIT_HPP) diff --git a/contrib/kyua/cli/cmd_test.cpp b/contrib/kyua/cli/cmd_test.cpp new file mode 100644 index 000000000000..cfaeec9b74cc --- /dev/null +++ b/contrib/kyua/cli/cmd_test.cpp @@ -0,0 +1,186 @@ +// 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 "cli/cmd_test.hpp" + +#include + +#include "cli/common.ipp" +#include "drivers/run_tests.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_test; + + +namespace { + + +/// Hooks to print a progress report of the execution of the tests. +class print_hooks : public drivers::run_tests::base_hooks { + /// Object to interact with the I/O of the program. + cmdline::ui* _ui; + + /// Whether the tests are executed in parallel or not. + bool _parallel; + +public: + /// The amount of positive test results found so far. + unsigned long good_count; + + /// The amount of negative test results found so far. + unsigned long bad_count; + + /// Constructor for the hooks. + /// + /// \param ui_ Object to interact with the I/O of the program. + /// \param parallel_ True if we are executing more than one test at once. + print_hooks(cmdline::ui* ui_, const bool parallel_) : + _ui(ui_), + _parallel(parallel_), + good_count(0), + bad_count(0) + { + } + + /// Called when the processing of a test case begins. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + virtual void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + if (!_parallel) { + _ui->out(F("%s -> ") % + cli::format_test_case_id(test_program, test_case_name), + false); + } + } + + /// Called when a result of a test case becomes available. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + /// \param result The result of the execution of the test case. + /// \param duration The time it took to run the test. + virtual void + got_result(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const datetime::delta& duration) + { + if (_parallel) { + _ui->out(F("%s -> ") % + cli::format_test_case_id(test_program, test_case_name), + false); + } + _ui->out(F("%s [%s]") % cli::format_result(result) % + cli::format_delta(duration)); + if (result.good()) + good_count++; + else + bad_count++; + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_test. +cmd_test::cmd_test(void) : cli_command( + "test", "[test-program ...]", 0, -1, "Run tests") +{ + add_option(build_root_option); + add_option(kyuafile_option); + add_option(results_file_create_option); +} + + +/// Entry point for the "test" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 if all tests passed, 1 otherwise. +int +cmd_test::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const layout::results_id_file_pair results = layout::new_db( + results_file_create(cmdline), kyuafile_path(cmdline).branch_path()); + + const bool parallel = (user_config.lookup< config::positive_int_node >( + "parallelism") > 1); + + print_hooks hooks(ui, parallel); + const drivers::run_tests::result result = drivers::run_tests::drive( + kyuafile_path(cmdline), build_root_path(cmdline), results.second, + parse_filters(cmdline.arguments()), user_config, hooks); + + int exit_code; + if (hooks.good_count > 0 || hooks.bad_count > 0) { + ui->out(""); + if (!results.first.empty()) { + ui->out(F("Results file id is %s") % results.first); + } + ui->out(F("Results saved to %s") % results.second); + ui->out(""); + + ui->out(F("%s/%s passed (%s failed)") % hooks.good_count % + (hooks.good_count + hooks.bad_count) % hooks.bad_count); + + exit_code = (hooks.bad_count == 0 ? EXIT_SUCCESS : EXIT_FAILURE); + } else { + // TODO(jmmv): Delete created empty file; it's useless! + if (!results.first.empty()) { + ui->out(F("Results file id is %s") % results.first); + } + ui->out(F("Results saved to %s") % results.second); + exit_code = EXIT_SUCCESS; + } + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : exit_code; +} diff --git a/contrib/kyua/cli/cmd_test.hpp b/contrib/kyua/cli/cmd_test.hpp new file mode 100644 index 000000000000..22d8422cb293 --- /dev/null +++ b/contrib/kyua/cli/cmd_test.hpp @@ -0,0 +1,54 @@ +// 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 cli/cmd_test.hpp +/// Provides the cmd_test class. + +#if !defined(CLI_CMD_TEST_HPP) +#define CLI_CMD_TEST_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "test" subcommand. +class cmd_test : public cli_command +{ +public: + cmd_test(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_TEST_HPP) diff --git a/contrib/kyua/cli/cmd_test_test.cpp b/contrib/kyua/cli/cmd_test_test.cpp new file mode 100644 index 000000000000..fb623323dd86 --- /dev/null +++ b/contrib/kyua/cli/cmd_test_test.cpp @@ -0,0 +1,63 @@ +// 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 "cli/cmd_test.hpp" + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_filter); +ATF_TEST_CASE_BODY(invalid_filter) +{ + cmdline::args_vector args; + args.push_back("test"); + args.push_back("correct"); + args.push_back("incorrect:"); + + cli::cmd_test cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::error, "Test case.*'incorrect:'.*empty", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, invalid_filter); +} diff --git a/contrib/kyua/cli/common.cpp b/contrib/kyua/cli/common.cpp new file mode 100644 index 000000000000..dbb7f12f18e0 --- /dev/null +++ b/contrib/kyua/cli/common.cpp @@ -0,0 +1,411 @@ +// 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 "cli/common.hpp" + +#include +#include +#include +#include + +#include "engine/filters.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using utils::none; +using utils::optional; + + +/// Standard definition of the option to specify the build root. +const cmdline::path_option cli::build_root_option( + "build-root", + "Path to the built test programs, if different from the location of the " + "Kyuafile scripts", + "path"); + + +/// Standard definition of the option to specify a Kyuafile. +const cmdline::path_option cli::kyuafile_option( + 'k', "kyuafile", + "Path to the test suite definition", + "file", "Kyuafile"); + + +/// Standard definition of the option to specify filters on test results. +const cmdline::list_option cli::results_filter_option( + "results-filter", "Comma-separated list of result types to include in " + "the report", "types", "skipped,xfail,broken,failed"); + + +/// Standard definition of the option to specify the results file. +/// +/// TODO(jmmv): Should support a git-like syntax to go back in time, like +/// --results-file=LATEST^N where N indicates how many runs to go back to. +const cmdline::string_option cli::results_file_create_option( + 'r', "results-file", + "Path to the results file to create; if left to the default value, the " + "name of the file is automatically computed for the current test suite", + "file", layout::results_auto_create_name); + + +/// Standard definition of the option to specify the results file. +/// +/// TODO(jmmv): Should support a git-like syntax to go back in time, like +/// --results-file=LATEST^N where N indicates how many runs to go back to. +const cmdline::string_option cli::results_file_open_option( + 'r', "results-file", + "Path to the results file to open or the identifier of the current test " + "suite or a previous results file for automatic lookup; if left to the " + "default value, uses the current directory as the test suite name", + "file", layout::results_auto_open_name); + + +namespace { + + +/// Gets the path to the historical database if it exists. +/// +/// TODO(jmmv): This function should go away. It only exists as a temporary +/// transitional path to force the use of the stale ~/.kyua/store.db if it +/// exists. +/// +/// \return A path if the file is found; none otherwise. +static optional< fs::path > +get_historical_db(void) +{ + optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path old_db = home.get() / ".kyua/store.db"; + if (fs::exists(old_db)) { + if (old_db.is_absolute()) + return utils::make_optional(old_db); + else + return utils::make_optional(old_db.to_absolute()); + } else { + return none; + } + } else { + return none; + } +} + + +/// Converts a set of result type names to identifiers. +/// +/// \param names The collection of names to process; may be empty. +/// +/// \return The result type identifiers corresponding to the input names. +/// +/// \throw std::runtime_error If any name in the input names is invalid. +static cli::result_types +parse_types(const std::vector< std::string >& names) +{ + typedef std::map< std::string, model::test_result_type > types_map; + types_map valid_types; + valid_types["broken"] = model::test_result_broken; + valid_types["failed"] = model::test_result_failed; + valid_types["passed"] = model::test_result_passed; + valid_types["skipped"] = model::test_result_skipped; + valid_types["xfail"] = model::test_result_expected_failure; + + cli::result_types types; + for (std::vector< std::string >::const_iterator iter = names.begin(); + iter != names.end(); ++iter) { + const types_map::const_iterator match = valid_types.find(*iter); + if (match == valid_types.end()) + throw std::runtime_error(F("Unknown result type '%s'") % *iter); + else + types.push_back((*match).second); + } + return types; +} + + +} // anonymous namespace + + +/// Gets the path to the build root, if any. +/// +/// This is just syntactic sugar to simplify quierying the 'build_root_option'. +/// +/// \param cmdline The parsed command line. +/// +/// \return The path to the build root, if specified; none otherwise. +optional< fs::path > +cli::build_root_path(const cmdline::parsed_cmdline& cmdline) +{ + optional< fs::path > build_root; + if (cmdline.has_option(build_root_option.long_name())) + build_root = cmdline.get_option< cmdline::path_option >( + build_root_option.long_name()); + return build_root; +} + + +/// Gets the path to the Kyuafile to be loaded. +/// +/// This is just syntactic sugar to simplify quierying the 'kyuafile_option'. +/// +/// \param cmdline The parsed command line. +/// +/// \return The path to the Kyuafile to be loaded. +fs::path +cli::kyuafile_path(const cmdline::parsed_cmdline& cmdline) +{ + return cmdline.get_option< cmdline::path_option >( + kyuafile_option.long_name()); +} + + +/// Gets the value of the results-file flag for the creation of a new file. +/// +/// \param cmdline The parsed command line from which to extract any possible +/// override for the location of the database via the --results-file flag. +/// +/// \return The path to the database to be used. +/// +/// \throw cmdline::error If the value passed to the flag is invalid. +std::string +cli::results_file_create(const cmdline::parsed_cmdline& cmdline) +{ + std::string results_file = cmdline.get_option< cmdline::string_option >( + results_file_create_option.long_name()); + if (results_file == results_file_create_option.default_value()) { + const optional< fs::path > historical_db = get_historical_db(); + if (historical_db) + results_file = historical_db.get().str(); + } else { + try { + (void)fs::path(results_file); + } catch (const fs::error& e) { + throw cmdline::usage_error(F("Invalid value passed to --%s") % + results_file_create_option.long_name()); + } + } + return results_file; +} + + +/// Gets the value of the results-file flag for the lookup of the file. +/// +/// \param cmdline The parsed command line from which to extract any possible +/// override for the location of the database via the --results-file flag. +/// +/// \return The path to the database to be used. +/// +/// \throw cmdline::error If the value passed to the flag is invalid. +std::string +cli::results_file_open(const cmdline::parsed_cmdline& cmdline) +{ + std::string results_file = cmdline.get_option< cmdline::string_option >( + results_file_open_option.long_name()); + if (results_file == results_file_open_option.default_value()) { + const optional< fs::path > historical_db = get_historical_db(); + if (historical_db) + results_file = historical_db.get().str(); + } else { + try { + (void)fs::path(results_file); + } catch (const fs::error& e) { + throw cmdline::usage_error(F("Invalid value passed to --%s") % + results_file_open_option.long_name()); + } + } + return results_file; +} + + +/// Gets the filters for the result types. +/// +/// \param cmdline The parsed command line. +/// +/// \return A collection of result types to be used for filtering. +/// +/// \throw std::runtime_error If any of the user-provided filters is invalid. +cli::result_types +cli::get_result_types(const utils::cmdline::parsed_cmdline& cmdline) +{ + result_types types = parse_types( + cmdline.get_option< cmdline::list_option >("results-filter")); + if (types.empty()) { + types.push_back(model::test_result_passed); + types.push_back(model::test_result_skipped); + types.push_back(model::test_result_expected_failure); + types.push_back(model::test_result_broken); + types.push_back(model::test_result_failed); + } + return types; +} + + +/// Parses a set of command-line arguments to construct test filters. +/// +/// \param args The command-line arguments representing test filters. +/// +/// \return A set of test filters. +/// +/// \throw cmdline:error If any of the arguments is invalid, or if they +/// represent a non-disjoint collection of filters. +std::set< engine::test_filter > +cli::parse_filters(const cmdline::args_vector& args) +{ + std::set< engine::test_filter > filters; + + try { + for (cmdline::args_vector::const_iterator iter = args.begin(); + iter != args.end(); iter++) { + const engine::test_filter filter(engine::test_filter::parse(*iter)); + if (filters.find(filter) != filters.end()) + throw cmdline::error(F("Duplicate filter '%s'") % filter.str()); + filters.insert(filter); + } + check_disjoint_filters(filters); + } catch (const std::runtime_error& e) { + throw cmdline::error(e.what()); + } + + return filters; +} + + +/// Reports the filters that have not matched any tests as errors. +/// +/// \param unused The collection of unused filters to report. +/// \param ui The user interface object through which errors are to be reported. +/// +/// \return True if there are any unused filters. The caller should report this +/// as an error to the user by means of a non-successful exit code. +bool +cli::report_unused_filters(const std::set< engine::test_filter >& unused, + cmdline::ui* ui) +{ + for (std::set< engine::test_filter >::const_iterator iter = unused.begin(); + iter != unused.end(); iter++) { + cmdline::print_warning(ui, F("No test cases matched by the filter " + "'%s'.") % (*iter).str()); + } + + return !unused.empty(); +} + + +/// Formats a time delta for user presentation. +/// +/// \param delta The time delta to format. +/// +/// \return A user-friendly representation of the time delta. +std::string +cli::format_delta(const datetime::delta& delta) +{ + return F("%.3ss") % (delta.seconds + (delta.useconds / 1000000.0)); +} + + +/// Formats a test case result for user presentation. +/// +/// \param result The result to format. +/// +/// \return A user-friendly representation of the result. +std::string +cli::format_result(const model::test_result& result) +{ + std::string text; + + switch (result.type()) { + case model::test_result_broken: text = "broken"; break; + case model::test_result_expected_failure: text = "expected_failure"; break; + case model::test_result_failed: text = "failed"; break; + case model::test_result_passed: text = "passed"; break; + case model::test_result_skipped: text = "skipped"; break; + } + INV(!text.empty()); + + if (!result.reason().empty()) + text += ": " + result.reason(); + + return text; +} + + +/// Formats the identifier of a test case for user presentation. +/// +/// \param test_program The test program containing the test case. +/// \param test_case_name The name of the test case. +/// +/// \return A string representing the test case uniquely within a test suite. +std::string +cli::format_test_case_id(const model::test_program& test_program, + const std::string& test_case_name) +{ + return F("%s:%s") % test_program.relative_path() % test_case_name; +} + + +/// Formats a filter using the same syntax of a test case. +/// +/// \param test_filter The filter to format. +/// +/// \return A string representing the test filter. +std::string +cli::format_test_case_id(const engine::test_filter& test_filter) +{ + return F("%s:%s") % test_filter.test_program % test_filter.test_case; +} + + +/// Prints the version header information to the interface output. +/// +/// \param ui Interface to which to write the version details. +void +cli::write_version_header(utils::cmdline::ui* ui) +{ + ui->out(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); +} diff --git a/contrib/kyua/cli/common.hpp b/contrib/kyua/cli/common.hpp new file mode 100644 index 000000000000..15a7e9fa3344 --- /dev/null +++ b/contrib/kyua/cli/common.hpp @@ -0,0 +1,104 @@ +// 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 cli/common.hpp +/// Utility functions to implement CLI subcommands. + +#if !defined(CLI_COMMON_HPP) +#define CLI_COMMON_HPP + +#include +#include +#include + +#include "engine/filters_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result.hpp" +#include "utils/cmdline/base_command.hpp" +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace cli { + + +extern const utils::cmdline::path_option build_root_option; +extern const utils::cmdline::path_option kyuafile_option; +extern const utils::cmdline::string_option results_file_create_option; +extern const utils::cmdline::string_option results_file_open_option; +extern const utils::cmdline::list_option results_filter_option; +extern const utils::cmdline::property_option variable_option; + + +/// Base type for commands defined in the cli module. +/// +/// All commands in Kyua receive a configuration object as their runtime +/// data parameter because the configuration file applies to all the +/// commands. +typedef utils::cmdline::base_command< utils::config::tree > cli_command; + + +/// Scoped, strictly owned pointer to a cli_command. +typedef std::auto_ptr< cli_command > cli_command_ptr; + + +/// Collection of result types. +/// +/// This is a vector rather than a set because we want to respect the order in +/// which the user provided the types. +typedef std::vector< model::test_result_type > result_types; + + +utils::optional< utils::fs::path > build_root_path( + const utils::cmdline::parsed_cmdline&); +utils::fs::path kyuafile_path(const utils::cmdline::parsed_cmdline&); +std::string results_file_create(const utils::cmdline::parsed_cmdline&); +std::string results_file_open(const utils::cmdline::parsed_cmdline&); +result_types get_result_types(const utils::cmdline::parsed_cmdline&); + +std::set< engine::test_filter > parse_filters( + const utils::cmdline::args_vector&); +bool report_unused_filters(const std::set< engine::test_filter >&, + utils::cmdline::ui*); + +std::string format_delta(const utils::datetime::delta&); +std::string format_result(const model::test_result&); +std::string format_test_case_id(const model::test_program&, const std::string&); +std::string format_test_case_id(const engine::test_filter&); + + +void write_version_header(utils::cmdline::ui*); + + +} // namespace cli + +#endif // !defined(CLI_COMMON_HPP) diff --git a/contrib/kyua/cli/common.ipp b/contrib/kyua/cli/common.ipp new file mode 100644 index 000000000000..c0de4e44ccc1 --- /dev/null +++ b/contrib/kyua/cli/common.ipp @@ -0,0 +1,30 @@ +// 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 "cli/common.hpp" +#include "utils/cmdline/base_command.ipp" diff --git a/contrib/kyua/cli/common_test.cpp b/contrib/kyua/cli/common_test.cpp new file mode 100644 index 000000000000..05bb187ace22 --- /dev/null +++ b/contrib/kyua/cli/common_test.cpp @@ -0,0 +1,488 @@ +// 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 "cli/common.hpp" + +#include + +#include + +#include "engine/exceptions.hpp" +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using utils::optional; + + +namespace { + + +/// Syntactic sugar to instantiate engine::test_filter objects. +/// +/// \param test_program Test program. +/// \param test_case Test case. +/// +/// \return A \code test_filter \endcode object, based on \p test_program and +/// \p test_case. +inline engine::test_filter +mkfilter(const char* test_program, const char* test_case) +{ + return engine::test_filter(fs::path(test_program), test_case); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root_path__default); +ATF_TEST_CASE_BODY(build_root_path__default) +{ + std::map< std::string, std::vector< std::string > > options; + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE(!cli::build_root_path(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root_path__explicit); +ATF_TEST_CASE_BODY(build_root_path__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["build-root"].push_back("/my//path"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE(cli::build_root_path(mock_cmdline)); + ATF_REQUIRE_EQ("/my/path", cli::build_root_path(mock_cmdline).get().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile_path__default); +ATF_TEST_CASE_BODY(kyuafile_path__default) +{ + std::map< std::string, std::vector< std::string > > options; + options["kyuafile"].push_back(cli::kyuafile_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ(cli::kyuafile_option.default_value(), + cli::kyuafile_path(mock_cmdline).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile_path__explicit); +ATF_TEST_CASE_BODY(kyuafile_path__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["kyuafile"].push_back("/my//path"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my/path", cli::kyuafile_path(mock_cmdline).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__default); +ATF_TEST_CASE_BODY(result_types__default) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back( + cli::results_filter_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__empty); +ATF_TEST_CASE_BODY(result_types__empty) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back(""); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_passed); + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__all); +ATF_TEST_CASE_BODY(result_types__explicit__all) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("passed,skipped,xfail,broken,failed"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_passed); + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__some); +ATF_TEST_CASE_BODY(result_types__explicit__some) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("skipped,broken"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_broken); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__invalid); +ATF_TEST_CASE_BODY(result_types__explicit__invalid) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("skipped,foo,broken"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Unknown result type 'foo'", + cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__default__new); +ATF_TEST_CASE_BODY(results_file_create__default__new) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_create_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + + ATF_REQUIRE_EQ(cli::results_file_create_option.default_value(), + cli::results_file_create(mock_cmdline)); + ATF_REQUIRE(!fs::exists(home / ".kyua")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__default__historical); +ATF_TEST_CASE_BODY(results_file_create__default__historical) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_create_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + fs::mkdir_p(fs::path("homedir/.kyua"), 0755); + atf::utils::create_file("homedir/.kyua/store.db", "fake store"); + + ATF_REQUIRE_EQ(fs::path("homedir/.kyua/store.db").to_absolute(), + fs::path(cli::results_file_create(mock_cmdline))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__explicit); +ATF_TEST_CASE_BODY(results_file_create__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back("/my//path/f.db"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my//path/f.db", + cli::results_file_create(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__default__latest); +ATF_TEST_CASE_BODY(results_file_open__default__latest) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_open_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + + ATF_REQUIRE_EQ(cli::results_file_open_option.default_value(), + cli::results_file_open(mock_cmdline)); + ATF_REQUIRE(!fs::exists(home / ".kyua")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__default__historical); +ATF_TEST_CASE_BODY(results_file_open__default__historical) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_open_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + fs::mkdir_p(fs::path("homedir/.kyua"), 0755); + atf::utils::create_file("homedir/.kyua/store.db", "fake store"); + + ATF_REQUIRE_EQ(fs::path("homedir/.kyua/store.db").to_absolute(), + fs::path(cli::results_file_open(mock_cmdline))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__explicit); +ATF_TEST_CASE_BODY(results_file_open__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back("/my//path/f.db"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my//path/f.db", cli::results_file_open(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__none); +ATF_TEST_CASE_BODY(parse_filters__none) +{ + const cmdline::args_vector args; + const std::set< engine::test_filter > filters = cli::parse_filters(args); + ATF_REQUIRE(filters.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__ok); +ATF_TEST_CASE_BODY(parse_filters__ok) +{ + cmdline::args_vector args; + args.push_back("foo"); + args.push_back("bar/baz"); + args.push_back("other:abc"); + args.push_back("other:bcd"); + const std::set< engine::test_filter > filters = cli::parse_filters(args); + + std::set< engine::test_filter > exp_filters; + exp_filters.insert(mkfilter("foo", "")); + exp_filters.insert(mkfilter("bar/baz", "")); + exp_filters.insert(mkfilter("other", "abc")); + exp_filters.insert(mkfilter("other", "bcd")); + + ATF_REQUIRE(exp_filters == filters); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__duplicate); +ATF_TEST_CASE_BODY(parse_filters__duplicate) +{ + cmdline::args_vector args; + args.push_back("foo/bar//baz"); + args.push_back("hello/world:yes"); + args.push_back("foo//bar/baz"); + ATF_REQUIRE_THROW_RE(cmdline::error, "Duplicate.*'foo/bar/baz'", + cli::parse_filters(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__nondisjoint); +ATF_TEST_CASE_BODY(parse_filters__nondisjoint) +{ + cmdline::args_vector args; + args.push_back("foo/bar"); + args.push_back("hello/world:yes"); + args.push_back("foo/bar:baz"); + ATF_REQUIRE_THROW_RE(cmdline::error, "'foo/bar'.*'foo/bar:baz'.*disjoint", + cli::parse_filters(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_unused_filters__none); +ATF_TEST_CASE_BODY(report_unused_filters__none) +{ + std::set< engine::test_filter > unused; + + cmdline::ui_mock ui; + ATF_REQUIRE(!cli::report_unused_filters(unused, &ui)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_unused_filters__some); +ATF_TEST_CASE_BODY(report_unused_filters__some) +{ + std::set< engine::test_filter > unused; + unused.insert(mkfilter("a/b", "")); + unused.insert(mkfilter("hey/d", "yes")); + + cmdline::ui_mock ui; + cmdline::init("progname"); + ATF_REQUIRE(cli::report_unused_filters(unused, &ui)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(2, ui.err_log().size()); + ATF_REQUIRE( atf::utils::grep_collection("No.*matched.*'a/b'", + ui.err_log())); + ATF_REQUIRE( atf::utils::grep_collection("No.*matched.*'hey/d:yes'", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_delta); +ATF_TEST_CASE_BODY(format_delta) +{ + ATF_REQUIRE_EQ("0.000s", cli::format_delta(datetime::delta())); + ATF_REQUIRE_EQ("0.012s", cli::format_delta(datetime::delta(0, 12300))); + ATF_REQUIRE_EQ("0.999s", cli::format_delta(datetime::delta(0, 999000))); + ATF_REQUIRE_EQ("51.321s", cli::format_delta(datetime::delta(51, 321000))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_result__no_reason); +ATF_TEST_CASE_BODY(format_result__no_reason) +{ + ATF_REQUIRE_EQ("passed", cli::format_result( + model::test_result(model::test_result_passed))); + ATF_REQUIRE_EQ("failed", cli::format_result( + model::test_result(model::test_result_failed))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_result__with_reason); +ATF_TEST_CASE_BODY(format_result__with_reason) +{ + ATF_REQUIRE_EQ("broken: Something", cli::format_result( + model::test_result(model::test_result_broken, "Something"))); + ATF_REQUIRE_EQ("expected_failure: A B C", cli::format_result( + model::test_result(model::test_result_expected_failure, "A B C"))); + ATF_REQUIRE_EQ("failed: More text", cli::format_result( + model::test_result(model::test_result_failed, "More text"))); + ATF_REQUIRE_EQ("skipped: Bye", cli::format_result( + model::test_result(model::test_result_skipped, "Bye"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_test_case_id__test_case); +ATF_TEST_CASE_BODY(format_test_case_id__test_case) +{ + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("foo/bar/baz"), fs::path("unused-root"), + "unused-suite-name") + .add_test_case("abc") + .build(); + ATF_REQUIRE_EQ("foo/bar/baz:abc", + cli::format_test_case_id(test_program, "abc")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_test_case_id__test_filter); +ATF_TEST_CASE_BODY(format_test_case_id__test_filter) +{ + const engine::test_filter filter(fs::path("foo/bar"), "baz"); + ATF_REQUIRE_EQ("foo/bar:baz", cli::format_test_case_id(filter)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(write_version_header); +ATF_TEST_CASE_BODY(write_version_header) +{ + cmdline::ui_mock ui; + cli::write_version_header(&ui); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_MATCH("^kyua .*[0-9]+\\.[0-9]+$", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, build_root_path__default); + ATF_ADD_TEST_CASE(tcs, build_root_path__explicit); + + ATF_ADD_TEST_CASE(tcs, kyuafile_path__default); + ATF_ADD_TEST_CASE(tcs, kyuafile_path__explicit); + + ATF_ADD_TEST_CASE(tcs, result_types__default); + ATF_ADD_TEST_CASE(tcs, result_types__empty); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__all); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__some); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__invalid); + + ATF_ADD_TEST_CASE(tcs, results_file_create__default__new); + ATF_ADD_TEST_CASE(tcs, results_file_create__default__historical); + ATF_ADD_TEST_CASE(tcs, results_file_create__explicit); + + ATF_ADD_TEST_CASE(tcs, results_file_open__default__latest); + ATF_ADD_TEST_CASE(tcs, results_file_open__default__historical); + ATF_ADD_TEST_CASE(tcs, results_file_open__explicit); + + ATF_ADD_TEST_CASE(tcs, parse_filters__none); + ATF_ADD_TEST_CASE(tcs, parse_filters__ok); + ATF_ADD_TEST_CASE(tcs, parse_filters__duplicate); + ATF_ADD_TEST_CASE(tcs, parse_filters__nondisjoint); + + ATF_ADD_TEST_CASE(tcs, report_unused_filters__none); + ATF_ADD_TEST_CASE(tcs, report_unused_filters__some); + + ATF_ADD_TEST_CASE(tcs, format_delta); + + ATF_ADD_TEST_CASE(tcs, format_result__no_reason); + ATF_ADD_TEST_CASE(tcs, format_result__with_reason); + + ATF_ADD_TEST_CASE(tcs, format_test_case_id__test_case); + ATF_ADD_TEST_CASE(tcs, format_test_case_id__test_filter); + + ATF_ADD_TEST_CASE(tcs, write_version_header); +} diff --git a/contrib/kyua/cli/config.cpp b/contrib/kyua/cli/config.cpp new file mode 100644 index 000000000000..0049103706bf --- /dev/null +++ b/contrib/kyua/cli/config.cpp @@ -0,0 +1,223 @@ +// 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 "cli/config.hpp" + +#include "cli/common.hpp" +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/env.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + +using utils::optional; + + +namespace { + + +/// Basename of the configuration file. +static const char* config_basename = "kyua.conf"; + + +/// Magic string to disable loading of configuration files. +static const char* none_config = "none"; + + +/// Textual description of the default configuration files. +/// +/// This is just an auxiliary string required to define the option below, which +/// requires a pointer to a static C string. +/// +/// \todo If the user overrides the KYUA_CONFDIR environment variable, we don't +/// reflect this fact here. We don't want to query the variable during program +/// initialization due to the side-effects it may have. Therefore, fixing this +/// is tricky as it may require a whole rethink of this module. +static const std::string config_lookup_names = + (fs::path("~/.kyua") / config_basename).str() + " or " + + (fs::path(KYUA_CONFDIR) / config_basename).str(); + + +/// Loads the configuration file for this session, if any. +/// +/// This is a helper function that does not apply user-specified overrides. See +/// the documentation for cli::load_config() for more details. +/// +/// \param cmdline The parsed command line. +/// +/// \return The loaded configuration file, or the configuration defaults if the +/// loading is disabled. +/// +/// \throw engine::error If the parsing of the configuration file fails. +/// TODO(jmmv): I'm not sure if this is the raised exception. And even if +/// it is, we should make it more accurate. +config::tree +load_config_file(const cmdline::parsed_cmdline& cmdline) +{ + // TODO(jmmv): We should really be able to use cmdline.has_option here to + // detect whether the option was provided or not instead of checking against + // the default value. + const fs::path filename = cmdline.get_option< cmdline::path_option >( + cli::config_option.long_name()); + if (filename.str() == none_config) { + LD("Configuration loading disabled; using defaults"); + return engine::default_config(); + } else if (filename.str() != cli::config_option.default_value()) + return engine::load_config(filename); + + const optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path path = home.get() / ".kyua" / config_basename; + try { + if (fs::exists(path)) + return engine::load_config(path); + } catch (const fs::error& e) { + // Fall through. If we fail to load the user-specific configuration + // file because it cannot be openend, we try to load the system-wide + // one. + LW(F("Failed to load user-specific configuration file '%s': %s") % + path % e.what()); + } + } + + const fs::path confdir(utils::getenv_with_default( + "KYUA_CONFDIR", KYUA_CONFDIR)); + + const fs::path path = confdir / config_basename; + if (fs::exists(path)) { + return engine::load_config(path); + } else { + return engine::default_config(); + } +} + + +/// Loads the configuration file for this session, if any. +/// +/// This is a helper function for cli::load_config() that attempts to load the +/// configuration unconditionally. +/// +/// \param cmdline The parsed command line. +/// +/// \return The loaded configuration file data. +/// +/// \throw engine::error If the parsing of the configuration file fails. +static config::tree +load_required_config(const cmdline::parsed_cmdline& cmdline) +{ + config::tree user_config = load_config_file(cmdline); + + if (cmdline.has_option(cli::variable_option.long_name())) { + typedef std::pair< std::string, std::string > override_pair; + + const std::vector< override_pair >& overrides = + cmdline.get_multi_option< cmdline::property_option >( + cli::variable_option.long_name()); + + for (std::vector< override_pair >::const_iterator + iter = overrides.begin(); iter != overrides.end(); iter++) { + try { + user_config.set_string((*iter).first, (*iter).second); + } catch (const config::error& e) { + // TODO(jmmv): Raising this type from here is obviously the + // wrong thing to do. + throw engine::error(e.what()); + } + } + } + + return user_config; +} + + +} // anonymous namespace + + +/// Standard definition of the option to specify a configuration file. +/// +/// You must use load_config() to load a configuration file while honoring the +/// value of this flag. +const cmdline::path_option cli::config_option( + 'c', "config", + (std::string("Path to the configuration file; '") + none_config + + "' to disable loading").c_str(), + "file", config_lookup_names.c_str()); + + +/// Standard definition of the option to specify a configuration variable. +const cmdline::property_option cli::variable_option( + 'v', "variable", + "Overrides a particular configuration variable", + "K=V"); + + +/// Loads the configuration file for this session, if any. +/// +/// The algorithm implemented here is as follows: +/// 1) If ~/.kyua/kyua.conf exists, load it. +/// 2) Otherwise, if sysconfdir/kyua.conf exists, load it. +/// 3) Otherwise, use the built-in settings. +/// 4) Lastly, apply any user-provided overrides. +/// +/// \param cmdline The parsed command line. +/// \param required Whether the loading of the configuration file must succeed. +/// Some commands should run regardless, and therefore we need to set this +/// to false for those commands. +/// +/// \return The loaded configuration file data. If required was set to false, +/// this might be the default configuration data if the requested file could not +/// be properly loaded. +/// +/// \throw engine::error If the parsing of the configuration file fails. +config::tree +cli::load_config(const cmdline::parsed_cmdline& cmdline, + const bool required) +{ + try { + return load_required_config(cmdline); + } catch (const engine::error& e) { + if (required) { + throw; + } else { + LW(F("Ignoring failure to load configuration because the requested " + "command should not fail: %s") % e.what()); + return engine::default_config(); + } + } +} diff --git a/contrib/kyua/cli/config.hpp b/contrib/kyua/cli/config.hpp new file mode 100644 index 000000000000..d948208ee5d0 --- /dev/null +++ b/contrib/kyua/cli/config.hpp @@ -0,0 +1,55 @@ +// 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 cli/config.hpp +/// Utility functions to load configuration files. +/// +/// \todo All this should probably just be merged into the main module +/// as nothing else should have access to this. + +#if !defined(CLI_CONFIG_HPP) +#define CLI_CONFIG_HPP + +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/config/tree_fwd.hpp" + +namespace cli { + + +extern const utils::cmdline::path_option config_option; +extern const utils::cmdline::property_option variable_option; + + +utils::config::tree load_config(const utils::cmdline::parsed_cmdline&, + const bool); + + +} // namespace cli + +#endif // !defined(CLI_CONFIG_HPP) diff --git a/contrib/kyua/cli/config_test.cpp b/contrib/kyua/cli/config_test.cpp new file mode 100644 index 000000000000..7a20c2941d8c --- /dev/null +++ b/contrib/kyua/cli/config_test.cpp @@ -0,0 +1,351 @@ +// 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 "cli/config.hpp" + +#include + +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Creates a configuration file for testing purposes. +/// +/// To ensure that the loaded file is the one created by this function, use +/// validate_mock_config(). +/// +/// \param name The name of the configuration file to create. +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +create_mock_config(const char* name, const char* cookie) +{ + if (cookie != NULL) { + atf::utils::create_file( + name, + F("syntax(2)\n" + "test_suites.suite.magic_value = '%s'\n") % cookie); + } else { + atf::utils::create_file(name, "syntax(200)\n"); + } +} + + +/// Creates an invalid system configuration. +/// +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +mock_system_config(const char* cookie) +{ + fs::mkdir(fs::path("system-dir"), 0755); + utils::setenv("KYUA_CONFDIR", (fs::current_path() / "system-dir").str()); + create_mock_config("system-dir/kyua.conf", cookie); +} + + +/// Creates an invalid user configuration. +/// +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +mock_user_config(const char* cookie) +{ + fs::mkdir(fs::path("user-dir"), 0755); + fs::mkdir(fs::path("user-dir/.kyua"), 0755); + utils::setenv("HOME", (fs::current_path() / "user-dir").str()); + create_mock_config("user-dir/.kyua/kyua.conf", cookie); +} + + +/// Ensures that a loaded configuration was created with create_mock_config(). +/// +/// \param user_config The configuration to validate. +/// \param cookie The magic value to expect in the configuration file. +static void +validate_mock_config(const config::tree& user_config, const char* cookie) +{ + const config::properties_map& properties = user_config.all_properties( + "test_suites.suite", true); + const config::properties_map::const_iterator iter = + properties.find("magic_value"); + ATF_REQUIRE(iter != properties.end()); + ATF_REQUIRE_EQ(cookie, (*iter).second); +} + + +/// Ensures that two configuration trees are equal. +/// +/// \param exp_tree The expected configuration tree. +/// \param actual_tree The configuration tree being validated against exp_tree. +static void +require_eq(const config::tree& exp_tree, const config::tree& actual_tree) +{ + ATF_REQUIRE(exp_tree.all_properties() == actual_tree.all_properties()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__none); +ATF_TEST_CASE_BODY(load_config__none) +{ + utils::setenv("KYUA_CONFDIR", "/the/system/does/not/exist"); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + require_eq(engine::default_config(), + cli::load_config(mock_cmdline, true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__ok); +ATF_TEST_CASE_BODY(load_config__explicit__ok) +{ + mock_system_config(NULL); + mock_user_config(NULL); + + create_mock_config("test-file", "hello"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("test-file"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "hello"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__disable); +ATF_TEST_CASE_BODY(load_config__explicit__disable) +{ + mock_system_config(NULL); + mock_user_config(NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("none"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + require_eq(engine::default_config(), + cli::load_config(mock_cmdline, true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__fail); +ATF_TEST_CASE_BODY(load_config__explicit__fail) +{ + mock_system_config("ok1"); + mock_user_config("ok2"); + + create_mock_config("test-file", NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("test-file"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__ok); +ATF_TEST_CASE_BODY(load_config__user__ok) +{ + mock_system_config(NULL); + mock_user_config("I am the user config"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "I am the user config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__fail); +ATF_TEST_CASE_BODY(load_config__user__fail) +{ + mock_system_config("valid"); + mock_user_config(NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__bad_home); +ATF_TEST_CASE_BODY(load_config__user__bad_home) +{ + mock_system_config("Fallback system config"); + utils::setenv("HOME", ""); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "Fallback system config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__system__ok); +ATF_TEST_CASE_BODY(load_config__system__ok) +{ + mock_system_config("I am the system config"); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "I am the system config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__system__fail); +ATF_TEST_CASE_BODY(load_config__system__fail) +{ + mock_system_config(NULL); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__no); +ATF_TEST_CASE_BODY(load_config__overrides__no) +{ + utils::setenv("KYUA_CONFDIR", fs::current_path().str()); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + options["variable"].push_back("architecture=1"); + options["variable"].push_back("platform=2"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + ATF_REQUIRE_EQ("1", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ("2", + user_config.lookup< config::string_node >("platform")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__yes); +ATF_TEST_CASE_BODY(load_config__overrides__yes) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "architecture = 'do not see me'\n" + "platform = 'see me'\n"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("config"); + options["variable"].push_back("architecture=overriden"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + ATF_REQUIRE_EQ("overriden", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ("see me", + user_config.lookup< config::string_node >("platform")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__fail); +ATF_TEST_CASE_BODY(load_config__overrides__fail) +{ + utils::setenv("KYUA_CONFDIR", fs::current_path().str()); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + options["variable"].push_back(".a=d"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "Empty component in key.*'\\.a'", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, load_config__none); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__ok); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__disable); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__fail); + ATF_ADD_TEST_CASE(tcs, load_config__user__ok); + ATF_ADD_TEST_CASE(tcs, load_config__user__fail); + ATF_ADD_TEST_CASE(tcs, load_config__user__bad_home); + ATF_ADD_TEST_CASE(tcs, load_config__system__ok); + ATF_ADD_TEST_CASE(tcs, load_config__system__fail); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__no); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__yes); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__fail); +} diff --git a/contrib/kyua/cli/main.cpp b/contrib/kyua/cli/main.cpp new file mode 100644 index 000000000000..531c252b0a75 --- /dev/null +++ b/contrib/kyua/cli/main.cpp @@ -0,0 +1,356 @@ +// 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 "cli/main.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include +#include +} + +#include +#include +#include +#include + +#include "cli/cmd_about.hpp" +#include "cli/cmd_config.hpp" +#include "cli/cmd_db_exec.hpp" +#include "cli/cmd_db_migrate.hpp" +#include "cli/cmd_debug.hpp" +#include "cli/cmd_help.hpp" +#include "cli/cmd_list.hpp" +#include "cli/cmd_report.hpp" +#include "cli/cmd_report_html.hpp" +#include "cli/cmd_report_junit.hpp" +#include "cli/cmd_test.hpp" +#include "cli/common.ipp" +#include "cli/config.hpp" +#include "engine/atf.hpp" +#include "engine/plain.hpp" +#include "engine/scheduler.hpp" +#include "engine/tap.hpp" +#include "store/exceptions.hpp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#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/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace signals = utils::signals; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Registers all valid scheduler interfaces. +/// +/// This is part of Kyua's setup but it is a bit strange to find it here. I am +/// not sure what a better location would be though, so for now this is good +/// enough. +static void +register_scheduler_interfaces(void) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + scheduler::register_interface( + "tap", std::shared_ptr< scheduler::interface >( + new engine::tap_interface())); +} + + +/// Executes the given subcommand with proper usage_error reporting. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param command The subcommand to execute. +/// \param args The part of the command line passed to the subcommand. The +/// first item of this collection must match the command name. +/// \param user_config The runtime configuration to pass to the subcommand. +/// +/// \return The exit code of the command. Typically 0 on success, some other +/// integer otherwise. +/// +/// \throw cmdline::usage_error If the user input to the subcommand is invalid. +/// This error does not encode the command name within it, so this function +/// extends the message in the error to specify which subcommand was +/// affected. +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +static int +run_subcommand(cmdline::ui* ui, cli::cli_command* command, + const cmdline::args_vector& args, + const config::tree& user_config) +{ + try { + PRE(command->name() == args[0]); + return command->main(ui, args, user_config); + } catch (const cmdline::usage_error& e) { + throw std::pair< std::string, cmdline::usage_error >( + command->name(), e); + } +} + + +/// Exception-safe version of main. +/// +/// This function provides the real meat of the entry point of the program. It +/// is allowed to throw some known exceptions which are parsed by the caller. +/// Doing so keeps this function simpler and allow tests to actually validate +/// that the errors reported are accurate. +/// +/// \return The exit code of the program. Should be EXIT_SUCCESS on success and +/// EXIT_FAILURE on failure. The caller extends this to additional integers for +/// errors reported through exceptions. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// \param mock_command An extra command provided for testing purposes; should +/// just be NULL other than for tests. +/// +/// \throw cmdline::usage_error If the user ran the program with invalid +/// arguments. +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +static int +safe_main(cmdline::ui* ui, int argc, const char* const argv[], + cli::cli_command_ptr mock_command) +{ + cmdline::options_vector options; + options.push_back(&cli::config_option); + options.push_back(&cli::variable_option); + const cmdline::string_option loglevel_option( + "loglevel", "Level of the messages to log", "level", "info"); + options.push_back(&loglevel_option); + const cmdline::path_option logfile_option( + "logfile", "Path to the log file", "file", + cli::detail::default_log_name().c_str()); + options.push_back(&logfile_option); + + cmdline::commands_map< cli::cli_command > commands; + + commands.insert(new cli::cmd_about()); + commands.insert(new cli::cmd_config()); + commands.insert(new cli::cmd_db_exec()); + commands.insert(new cli::cmd_db_migrate()); + commands.insert(new cli::cmd_help(&options, &commands)); + + commands.insert(new cli::cmd_debug(), "Workspace"); + commands.insert(new cli::cmd_list(), "Workspace"); + commands.insert(new cli::cmd_test(), "Workspace"); + + commands.insert(new cli::cmd_report(), "Reporting"); + commands.insert(new cli::cmd_report_html(), "Reporting"); + commands.insert(new cli::cmd_report_junit(), "Reporting"); + + if (mock_command.get() != NULL) + commands.insert(mock_command); + + const cmdline::parsed_cmdline cmdline = cmdline::parse(argc, argv, options); + + const fs::path logfile(cmdline.get_option< cmdline::path_option >( + "logfile")); + fs::mkdir_p(logfile.branch_path(), 0755); + LD(F("Log file is %s") % logfile); + utils::install_crash_handlers(logfile.str()); + try { + logging::set_persistency(cmdline.get_option< cmdline::string_option >( + "loglevel"), logfile); + } catch (const std::range_error& e) { + throw cmdline::usage_error(e.what()); + } + + if (cmdline.arguments().empty()) + throw cmdline::usage_error("No command provided"); + const std::string cmdname = cmdline.arguments()[0]; + + const config::tree user_config = cli::load_config(cmdline, + cmdname != "help"); + + cli::cli_command* command = commands.find(cmdname); + if (command == NULL) + throw cmdline::usage_error(F("Unknown command '%s'") % cmdname); + register_scheduler_interfaces(); + return run_subcommand(ui, command, cmdline.arguments(), user_config); +} + + +} // anonymous namespace + + +/// Gets the name of the default log file. +/// +/// \return The path to the log file. +fs::path +cli::detail::default_log_name(void) +{ + // Update doc/troubleshooting.texi if you change this algorithm. + const optional< std::string > home(utils::getenv("HOME")); + if (home) { + return logging::generate_log_name(fs::path(home.get()) / ".kyua" / + "logs", cmdline::progname()); + } else { + const optional< std::string > tmpdir(utils::getenv("TMPDIR")); + if (tmpdir) { + return logging::generate_log_name(fs::path(tmpdir.get()), + cmdline::progname()); + } else { + return logging::generate_log_name(fs::path("/tmp"), + cmdline::progname()); + } + } +} + + +/// Testable entry point, with catch-all exception handlers. +/// +/// This entry point does not perform any initialization of global state; it is +/// provided to allow unit-testing of the utility's entry point. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// \param mock_command An extra command provided for testing purposes; should +/// just be NULL other than for tests. +/// +/// \return 0 on success, some other integer on error. +/// +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +int +cli::main(cmdline::ui* ui, const int argc, const char* const* const argv, + cli_command_ptr mock_command) +{ + try { + const int exit_code = safe_main(ui, argc, argv, mock_command); + + // Codes above 1 are reserved to report conditions captured as + // exceptions below. + INV(exit_code == EXIT_SUCCESS || exit_code == EXIT_FAILURE); + + return exit_code; + } catch (const signals::interrupted_error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + // Re-deliver the interruption signal to self so that we terminate with + // the right status. At this point we should NOT have any custom signal + // handlers in place. + ::kill(getpid(), e.signo()); + LD("Interrupt signal re-delivery did not terminate program"); + // If we reach this, something went wrong because we did not exit as + // intended. Return an internal error instead. (Would be nicer to + // abort in principle, but it wouldn't be a nice experience if it ever + // happened.) + return 2; + } catch (const std::pair< std::string, cmdline::usage_error >& e) { + const std::string message = F("Usage error for command %s: %s.") % + e.first % e.second.what(); + LE(message); + ui->err(message); + ui->err(F("Type '%s help %s' for usage information.") % + cmdline::progname() % e.first); + return 3; + } catch (const cmdline::usage_error& e) { + const std::string message = F("Usage error: %s.") % e.what(); + LE(message); + ui->err(message); + ui->err(F("Type '%s help' for usage information.") % + cmdline::progname()); + return 3; + } catch (const store::old_schema_error& e) { + const std::string message = F("The database has schema version %s, " + "which is too old; please use db-migrate " + "to upgrade it.") % e.old_version(); + cmdline::print_error(ui, message); + return 2; + } catch (const std::runtime_error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + return 2; + } +} + + +/// Delegate for ::main(). +/// +/// This function is supposed to be called directly from the top-level ::main() +/// function. It takes care of initializing internal libraries and then calls +/// main(ui, argc, argv). +/// +/// \pre This function can only be called once. +/// +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +int +cli::main(const int argc, const char* const* const argv) +{ + logging::set_inmemory(); + + LI(F("%s %s") % PACKAGE % VERSION); + + std::string plain_args; + for (const char* const* arg = argv; *arg != NULL; arg++) + plain_args += F(" %s") % *arg; + LI(F("Command line:%s") % plain_args); + + cmdline::init(argv[0]); + cmdline::ui ui; + + const int exit_code = main(&ui, argc, argv); + LI(F("Clean exit with code %s") % exit_code); + return exit_code; +} diff --git a/contrib/kyua/cli/main.hpp b/contrib/kyua/cli/main.hpp new file mode 100644 index 000000000000..00e53c5a4ab2 --- /dev/null +++ b/contrib/kyua/cli/main.hpp @@ -0,0 +1,61 @@ +// 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 cli/main.hpp +/// Entry point for the program. +/// +/// These entry points are separate from the top-level ::main() function to +/// allow unit-testing of the main code. + +#if !defined(CLI_MAIN_HPP) +#define CLI_MAIN_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace cli { + + +namespace detail { + + +utils::fs::path default_log_name(void); + + +} // namespace detail + + +int main(utils::cmdline::ui*, const int, const char* const* const, + cli_command_ptr = cli_command_ptr()); +int main(const int, const char* const* const); + + +} // namespace cli + +#endif // !defined(CLI_MAIN_HPP) diff --git a/contrib/kyua/cli/main_test.cpp b/contrib/kyua/cli/main_test.cpp new file mode 100644 index 000000000000..70d167ff6963 --- /dev/null +++ b/contrib/kyua/cli/main_test.cpp @@ -0,0 +1,489 @@ +// 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 "cli/main.hpp" + +extern "C" { +#include +} + +#include + +#include + +#include "utils/cmdline/base_command.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/logging/operations.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/test_utils.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace process = utils::process; + + +namespace { + + +/// Fake command implementation that crashes during its execution. +class cmd_mock_crash : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// All command parameters are set to irrelevant values. + cmd_mock_crash(void) : + cli::cli_command("mock_error", "", 0, 0, "Mock command that crashes") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function always aborts. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + utils::abort_without_coredump(); + } +}; + + +/// Fake command implementation that throws an exception during its execution. +class cmd_mock_error : public cli::cli_command { + /// Whether the command raises an exception captured by the parent or not. + /// + /// If this is true, the command will raise a std::runtime_error exception + /// or a subclass of it. The main program is in charge of capturing these + /// and reporting them appropriately. If false, this raises another + /// exception that does not inherit from std::runtime_error. + bool _unhandled; + +public: + /// Constructs a new mock command. + /// + /// \param unhandled If true, make run raise an exception not catched by the + /// main program. + cmd_mock_error(const bool unhandled) : + cli::cli_command("mock_error", "", 0, 0, + "Mock command that raises an error"), + _unhandled(unhandled) + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function always aborts. + /// + /// \throw std::logic_error If _unhandled is true. + /// \throw std::runtime_error If _unhandled is false. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + if (_unhandled) + throw std::logic_error("This is unhandled"); + else + throw std::runtime_error("Runtime error"); + } +}; + + +/// Fake command implementation that prints messages during its execution. +class cmd_mock_write : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// All command parameters are set to irrelevant values. + cmd_mock_write(void) : cli::cli_command( + "mock_write", "", 0, 0, "Mock command that prints output") + { + } + + /// Runs the mock command. + /// + /// \param ui Object to interact with the I/O of the program. + /// + /// \return Nothing because this function always aborts. + int + run(cmdline::ui* ui, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + ui->out("stdout message from subcommand"); + ui->err("stderr message from subcommand"); + return EXIT_FAILURE; + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__home); +ATF_TEST_CASE_BODY(detail__default_log_name__home) +{ + datetime::set_mock_now(2011, 2, 21, 21, 10, 30, 0); + cmdline::init("progname1"); + + utils::setenv("HOME", "/home//fake"); + utils::setenv("TMPDIR", "/do/not/use/this"); + ATF_REQUIRE_EQ( + fs::path("/home/fake/.kyua/logs/progname1.20110221-211030.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__tmpdir); +ATF_TEST_CASE_BODY(detail__default_log_name__tmpdir) +{ + datetime::set_mock_now(2011, 2, 21, 21, 10, 50, 987); + cmdline::init("progname2"); + + utils::unsetenv("HOME"); + utils::setenv("TMPDIR", "/a/b//c"); + ATF_REQUIRE_EQ(fs::path("/a/b/c/progname2.20110221-211050.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__hardcoded); +ATF_TEST_CASE_BODY(detail__default_log_name__hardcoded) +{ + datetime::set_mock_now(2011, 2, 21, 21, 15, 00, 123456); + cmdline::init("progname3"); + + utils::unsetenv("HOME"); + utils::unsetenv("TMPDIR"); + ATF_REQUIRE_EQ(fs::path("/tmp/progname3.20110221-211500.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__no_args); +ATF_TEST_CASE_BODY(main__no_args) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("Usage error: No command provided", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__unknown_command); +ATF_TEST_CASE_BODY(main__unknown_command) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "foo", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("Usage error: Unknown command.*foo", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__logfile__default); +ATF_TEST_CASE_BODY(main__logfile__default) +{ + logging::set_inmemory(); + datetime::set_mock_now(2011, 2, 21, 21, 30, 00, 0); + cmdline::init("progname"); + + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE(!fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__logfile__override); +ATF_TEST_CASE_BODY(main__logfile__override) +{ + logging::set_inmemory(); + datetime::set_mock_now(2011, 2, 21, 21, 30, 00, 321); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "--logfile=test.log", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE(!fs::exists(fs::path("test.log"))); + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); + ATF_REQUIRE(fs::exists(fs::path("test.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__default); +ATF_TEST_CASE_BODY(main__loglevel__default) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "--logfile=test.log", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__higher); +ATF_TEST_CASE_BODY(main__loglevel__higher) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=debug", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__lower); +ATF_TEST_CASE_BODY(main__loglevel__lower) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=warning", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(!atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__error); +ATF_TEST_CASE_BODY(main__loglevel__error) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=i-am-invalid", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(atf::utils::grep_collection("Usage error.*i-am-invalid", + ui.err_log())); + ATF_REQUIRE(!fs::exists(fs::path("test.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__ok); +ATF_TEST_CASE_BODY(main__subcommand__ok) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_write", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_write()))); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("stdout message from subcommand", ui.out_log()[0]); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("stderr message from subcommand", ui.err_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__invalid_args); +ATF_TEST_CASE_BODY(main__subcommand__invalid_args) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "mock_write", "bar", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_write()))); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection( + "Usage error for command mock_write: Too many arguments.", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__runtime_error); +ATF_TEST_CASE_BODY(main__subcommand__runtime_error) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(2, cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_error(false)))); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("progname: E: Runtime error.", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__unhandled_exception); +ATF_TEST_CASE_BODY(main__subcommand__unhandled_exception) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_THROW(std::logic_error, cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_error(true)))); +} + + +static void +do_subcommand_crash(void) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_crash())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__crash); +ATF_TEST_CASE_BODY(main__subcommand__crash) +{ + const process::status status = process::child::fork_files( + do_subcommand_crash, fs::path("stdout.txt"), + fs::path("stderr.txt"))->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file("Fatal signal", "stderr.txt")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__home); + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__tmpdir); + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__hardcoded); + + ATF_ADD_TEST_CASE(tcs, main__no_args); + ATF_ADD_TEST_CASE(tcs, main__unknown_command); + ATF_ADD_TEST_CASE(tcs, main__logfile__default); + ATF_ADD_TEST_CASE(tcs, main__logfile__override); + ATF_ADD_TEST_CASE(tcs, main__loglevel__default); + ATF_ADD_TEST_CASE(tcs, main__loglevel__higher); + ATF_ADD_TEST_CASE(tcs, main__loglevel__lower); + ATF_ADD_TEST_CASE(tcs, main__loglevel__error); + ATF_ADD_TEST_CASE(tcs, main__subcommand__ok); + ATF_ADD_TEST_CASE(tcs, main__subcommand__invalid_args); + ATF_ADD_TEST_CASE(tcs, main__subcommand__runtime_error); + ATF_ADD_TEST_CASE(tcs, main__subcommand__unhandled_exception); + ATF_ADD_TEST_CASE(tcs, main__subcommand__crash); +} diff --git a/contrib/kyua/configure.ac b/contrib/kyua/configure.ac new file mode 100644 index 000000000000..a0df977c5226 --- /dev/null +++ b/contrib/kyua/configure.ac @@ -0,0 +1,173 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +AC_INIT([Kyua], [0.14], [kyua-discuss@googlegroups.com], [kyua], + [https://github.com/jmmv/kyua/]) +AC_PREREQ([2.65]) + + +AC_COPYRIGHT([Copyright 2010 The Kyua Authors.]) +AC_CONFIG_AUX_DIR([admin]) +AC_CONFIG_FILES([Doxyfile Makefile utils/defs.hpp]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_MACRO_DIR([m4]) +AC_CONFIG_SRCDIR([main.cpp]) +AC_CONFIG_TESTDIR([bootstrap]) + + +AM_INIT_AUTOMAKE([1.9 foreign subdir-objects -Wall]) + + +AC_LANG([C++]) +AC_PROG_CXX +AX_CXX_COMPILE_STDCXX([11], [noext], [mandatory]) +m4_ifdef([AM_PROG_AR], [AM_PROG_AR]) +KYUA_DEVELOPER_MODE([C++]) +KYUA_ATTRIBUTE_NORETURN +KYUA_ATTRIBUTE_PURE +KYUA_ATTRIBUTE_UNUSED +KYUA_FS_MODULE +KYUA_GETOPT +KYUA_LAST_SIGNO +KYUA_MEMORY +AC_CHECK_FUNCS([putenv setenv unsetenv]) +AC_CHECK_HEADERS([termios.h]) + + +AC_PROG_RANLIB + + +m4_ifndef([PKG_CHECK_MODULES], + [m4_fatal([Cannot find pkg.m4; see the INSTALL document for help])]) + +m4_ifndef([ATF_CHECK_CXX], + [m4_fatal([Cannot find atf-c++.m4; see the INSTALL document for help])]) +ATF_CHECK_CXX([>= 0.17]) +m4_ifndef([ATF_CHECK_SH], + [m4_fatal([Cannot find atf-sh.m4; see the INSTALL document for help])]) +ATF_CHECK_SH([>= 0.15]) +m4_ifndef([ATF_ARG_WITH], + [m4_fatal([Cannot find atf-common.m4; see the INSTALL document for help])]) +ATF_ARG_WITH + +PKG_CHECK_MODULES([LUTOK], [lutok >= 0.4], + [], + AC_MSG_ERROR([lutok (0.4 or newer) is required])) +PKG_CHECK_MODULES([SQLITE3], [sqlite3 >= 3.6.22], + [], + AC_MSG_ERROR([sqlite3 (3.6.22 or newer) is required])) +KYUA_DOXYGEN +AC_PATH_PROG([GDB], [gdb]) +test -n "${GDB}" || GDB=gdb +AC_PATH_PROG([GIT], [git]) + + +KYUA_UNAME_ARCHITECTURE +KYUA_UNAME_PLATFORM + + +AC_ARG_VAR([KYUA_CONFSUBDIR], + [Subdirectory of sysconfdir under which to look for files]) +if test x"${KYUA_CONFSUBDIR-unset}" = x"unset"; then + KYUA_CONFSUBDIR=kyua +else + case ${KYUA_CONFSUBDIR} in + /*) + AC_MSG_ERROR([KYUA_CONFSUBDIR must hold a relative path]) + ;; + *) + ;; + esac +fi +if test x"${KYUA_CONFSUBDIR}" = x""; then + AC_SUBST(kyua_confdir, \${sysconfdir}) +else + AC_SUBST(kyua_confdir, \${sysconfdir}/${KYUA_CONFSUBDIR}) +fi + + +dnl Allow the caller of 'make check', 'make installcheck' and 'make distcheck' +dnl on the Kyua source tree to override the configuration file passed to our +dnl own test runs. This is for the development of Kyua only and the value of +dnl this setting has no effect on the built product in any way. If we go +dnl through great extents in validating the value of this setting, it is to +dnl minimize the chance of false test run negatives later on. +AC_ARG_VAR([KYUA_CONFIG_FILE_FOR_CHECK], + [kyua.conf file to use at 'make (|dist|install)check' time]) +case "${KYUA_CONFIG_FILE_FOR_CHECK-none}" in +none) + KYUA_CONFIG_FILE_FOR_CHECK=none + ;; +/*) + if test -f "${KYUA_CONFIG_FILE_FOR_CHECK}"; then + : # All good! + else + AC_MSG_ERROR([KYUA_CONFIG_FILE_FOR_CHECK file does not exist]) + fi + ;; +*) + AC_MSG_ERROR([KYUA_CONFIG_FILE_FOR_CHECK must hold an absolute path]) + ;; +esac + + +AC_ARG_VAR([KYUA_TMPDIR], + [Path to the directory in which to place work directories]) +case "${KYUA_TMPDIR:-unset}" in + unset) + KYUA_TMPDIR=/tmp + ;; + /*) + ;; + *) + AC_MSG_ERROR([KYUA_TMPDIR must be an absolute path]) + ;; +esac + + +AC_SUBST(examplesdir, \${pkgdatadir}/examples) +AC_SUBST(luadir, \${pkgdatadir}/lua) +AC_SUBST(miscdir, \${pkgdatadir}/misc) +AC_SUBST(pkgtestsdir, \${testsdir}/kyua) +AC_SUBST(storedir, \${pkgdatadir}/store) +AC_SUBST(testsdir, \${exec_prefix}/tests) + + +dnl BSD make(1) doesn't deal with targets specified as './foo' well: they +dnl need to be specified as 'foo'. The following hack is to workaround this +dnl issue. +if test "${srcdir}" = .; then + target_srcdir= +else + target_srcdir="${srcdir}/" +fi +AM_CONDITIONAL(TARGET_SRCDIR_EMPTY, [test -z "${target_srcdir}"]) +AC_SUBST([target_srcdir]) + + +AC_OUTPUT diff --git a/contrib/kyua/doc/.gitignore b/contrib/kyua/doc/.gitignore new file mode 100644 index 000000000000..ecaaf27b9262 --- /dev/null +++ b/contrib/kyua/doc/.gitignore @@ -0,0 +1,14 @@ +kyua-about.1 +kyua-config.1 +kyua-db-exec.1 +kyua-db-migrate.1 +kyua-debug.1 +kyua-help.1 +kyua-list.1 +kyua-report-html.1 +kyua-report-junit.1 +kyua-report.1 +kyua-test.1 +kyua.1 +kyua.conf.5 +kyuafile.5 diff --git a/contrib/kyua/doc/Kyuafile b/contrib/kyua/doc/Kyuafile new file mode 100644 index 000000000000..c538f5b2a531 --- /dev/null +++ b/contrib/kyua/doc/Kyuafile @@ -0,0 +1,5 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="manbuild_test"} diff --git a/contrib/kyua/doc/Makefile.am.inc b/contrib/kyua/doc/Makefile.am.inc new file mode 100644 index 000000000000..638191218bcc --- /dev/null +++ b/contrib/kyua/doc/Makefile.am.inc @@ -0,0 +1,152 @@ +# 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. + +BUILD_MANPAGE = \ + $(MKDIR_P) doc; \ + $(SHELL) $(srcdir)/doc/manbuild.sh \ + -v "CONFDIR=$(kyua_confdir)" \ + -v "DOCDIR=$(docdir)" \ + -v "EGDIR=$(examplesdir)" \ + -v "MISCDIR=$(miscdir)" \ + -v "PACKAGE=$(PACKAGE_TARNAME)" \ + -v "STOREDIR=$(storedir)" \ + -v "TESTSDIR=$(testsdir)" \ + -v "VERSION=$(PACKAGE_VERSION)" \ + "$(srcdir)/doc/$${name}.in" "doc/$${name}" + +DIST_MAN_DEPS = doc/manbuild.sh \ + doc/build-root.mdoc \ + doc/results-file-flag-read.mdoc \ + doc/results-file-flag-write.mdoc \ + doc/results-files.mdoc \ + doc/results-files-report-example.mdoc \ + doc/test-filters.mdoc \ + doc/test-isolation.mdoc +MAN_DEPS = $(DIST_MAN_DEPS) Makefile +EXTRA_DIST += $(DIST_MAN_DEPS) + +man_MANS = doc/kyua-about.1 +CLEANFILES += doc/kyua-about.1 +EXTRA_DIST += doc/kyua-about.1.in +doc/kyua-about.1: $(srcdir)/doc/kyua-about.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-about.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-config.1 +CLEANFILES += doc/kyua-config.1 +EXTRA_DIST += doc/kyua-config.1.in +doc/kyua-config.1: $(srcdir)/doc/kyua-config.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-config.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-db-exec.1 +CLEANFILES += doc/kyua-db-exec.1 +EXTRA_DIST += doc/kyua-db-exec.1.in +doc/kyua-db-exec.1: $(srcdir)/doc/kyua-db-exec.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-db-exec.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-db-migrate.1 +CLEANFILES += doc/kyua-db-migrate.1 +EXTRA_DIST += doc/kyua-db-migrate.1.in +doc/kyua-db-migrate.1: $(srcdir)/doc/kyua-db-migrate.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-db-migrate.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-debug.1 +CLEANFILES += doc/kyua-debug.1 +EXTRA_DIST += doc/kyua-debug.1.in +doc/kyua-debug.1: $(srcdir)/doc/kyua-debug.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-debug.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-help.1 +CLEANFILES += doc/kyua-help.1 +EXTRA_DIST += doc/kyua-help.1.in +doc/kyua-help.1: $(srcdir)/doc/kyua-help.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-help.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-list.1 +CLEANFILES += doc/kyua-list.1 +EXTRA_DIST += doc/kyua-list.1.in +doc/kyua-list.1: $(srcdir)/doc/kyua-list.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-list.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-report-html.1 +CLEANFILES += doc/kyua-report-html.1 +EXTRA_DIST += doc/kyua-report-html.1.in +doc/kyua-report-html.1: $(srcdir)/doc/kyua-report-html.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-report-html.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-report-junit.1 +CLEANFILES += doc/kyua-report-junit.1 +EXTRA_DIST += doc/kyua-report-junit.1.in +doc/kyua-report-junit.1: $(srcdir)/doc/kyua-report-junit.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-report-junit.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-report.1 +CLEANFILES += doc/kyua-report.1 +EXTRA_DIST += doc/kyua-report.1.in +doc/kyua-report.1: $(srcdir)/doc/kyua-report.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-report.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-test.1 +CLEANFILES += doc/kyua-test.1 +EXTRA_DIST += doc/kyua-test.1.in +doc/kyua-test.1: $(srcdir)/doc/kyua-test.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-test.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua.1 +CLEANFILES += doc/kyua.1 +EXTRA_DIST += doc/kyua.1.in +doc/kyua.1: $(srcdir)/doc/kyua.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua.conf.5 +CLEANFILES += doc/kyua.conf.5 +EXTRA_DIST += doc/kyua.conf.5.in +doc/kyua.conf.5: $(srcdir)/doc/kyua.conf.5.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua.conf.5; $(BUILD_MANPAGE) + +man_MANS += doc/kyuafile.5 +CLEANFILES += doc/kyuafile.5 +EXTRA_DIST += doc/kyuafile.5.in +doc/kyuafile.5: $(srcdir)/doc/kyuafile.5.in $(MAN_DEPS) + $(AM_V_GEN)name=kyuafile.5; $(BUILD_MANPAGE) + +if WITH_ATF +EXTRA_DIST += doc/Kyuafile + +noinst_SCRIPTS += doc/manbuild_test +CLEANFILES += doc/manbuild_test +EXTRA_DIST += doc/manbuild_test.sh +doc/manbuild_test: $(srcdir)/doc/manbuild_test.sh Makefile + $(AM_V_GEN)$(MKDIR_P) doc; \ + echo "#! $(ATF_SH)" >doc/manbuild_test.tmp; \ + echo "# AUTOMATICALLY GENERATED FROM Makefile" \ + >>doc/manbuild_test.tmp; \ + sed -e 's,__MANBUILD__,$(abs_srcdir)/doc/manbuild.sh,g' \ + <$(srcdir)/doc/manbuild_test.sh >>doc/manbuild_test.tmp; \ + mv doc/manbuild_test.tmp doc/manbuild_test; \ + chmod +x doc/manbuild_test +endif diff --git a/contrib/kyua/doc/build-root.mdoc b/contrib/kyua/doc/build-root.mdoc new file mode 100644 index 000000000000..2fb008246f41 --- /dev/null +++ b/contrib/kyua/doc/build-root.mdoc @@ -0,0 +1,104 @@ +.\" 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. +.Em Build directories +(or object directories, target directories, product directories, etc.) is +the concept that allows a developer to keep the source tree clean from +build products by asking the build system to place such build products +under a separate subtree. +.Pp +Most build systems today support build directories. +For example, the GNU Automake/Autoconf build system exposes such concept when +invoked as follows: +.Bd -literal -offset indent +$ cd my-project-1.0 +$ mkdir build +$ cd build +$ ../configure +$ make +.Ed +.Pp +Under such invocation, all the results of the build are left in the +.Pa my-project-1.0/build/ +subdirectory while maintaining the contents of +.Pa my-project-1.0/ +intact. +.Pp +Because build directories are an integral part of most build systems, and +because they are a tool that developers use frequently, +.Nm +supports build directories too. +This manifests in the form of +.Nm +being able to run tests from build directories while reading the (often +immutable) test suite definition from the source tree. +.Pp +One important property of build directories is that they follow (or need to +follow) the exact same layout as the source tree. +For example, consider the following directory listings: +.Bd -literal -offset indent +src/Kyuafile +src/bin/ls/ +src/bin/ls/Kyuafile +src/bin/ls/ls.c +src/bin/ls/ls_test.c +src/sbin/su/ +src/sbin/su/Kyuafile +src/sbin/su/su.c +src/sbin/su/su_test.c + +obj/bin/ls/ +obj/bin/ls/ls* +obj/bin/ls/ls_test* +obj/sbin/su/ +obj/sbin/su/su* +obj/sbin/su/su_test* +.Ed +.Pp +Note how the directory layout within +.Pa src/ +matches that of +.Pa obj/ . +The +.Pa src/ +directory contains only source files and the definition of the test suite +(the Kyuafiles), while the +.Pa obj/ +directory contains only the binaries generated during a build. +.Pp +All commands that deal with the workspace support the +.Fl -build-root Ar path +option. +When this option is provided, the directory specified by the +option is considered to be the root of the build directory. +For example, considering our previous fake tree layout, we could invoke +.Nm +as any of the following: +.Bd -literal -offset indent +$ kyua __COMMAND__ --kyuafile=src/Kyuafile --build-root=obj +$ cd src && kyua __COMMAND__ --build-root=../obj +.Ed diff --git a/contrib/kyua/doc/kyua-about.1.in b/contrib/kyua/doc/kyua-about.1.in new file mode 100644 index 000000000000..1ea134810e65 --- /dev/null +++ b/contrib/kyua/doc/kyua-about.1.in @@ -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. +.Dd May 20, 2015 +.Dt KYUA-ABOUT 1 +.Os +.Sh NAME +.Nm "kyua about" +.Nd Shows detailed authors, license, and version information +.Sh SYNOPSIS +.Nm +.Op Ar authors | license | version +.Sh DESCRIPTION +The +.Sq about +command provides generic information about the +.Xr kyua 1 +tool. +In the default synopsis form (no arguments), the information printed +includes: +.Bl -enum +.It +The name of the package, which is +.Sq __PACKAGE__ . +.It +The version number, which is +.Sq __VERSION__ . +.It +License information. +.It +Authors information. +.It +A link to the project web site. +.El +.Pp +You can customize the information printed by this command by specifying +the desired topic as the single argument to the command. +This can be one of: +.Bl -tag -width authorsXX +.It Ar authors +Displays the list of authors and contributors only. +.It Ar license +Displays the license information and the list of copyrights. +.It Ar version +Displays the package name and the version number in a format that is +compatible with the output of GNU tools that support a +.Fl -version +flag. +Use this whenever you have to query the version number of the package. +.El +.Sh FILES +The following files are read by the +.Nm +command: +.Bl -tag -width XX +.It Pa __DOCDIR__/AUTHORS +List of authors (aka copyright holders). +.It Pa __DOCDIR__/CONTRIBUTORS +List of contributors (aka individuals that have contributed to the project). +.It Pa __DOCDIR__/LICENSE +License information. +.El +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/contrib/kyua/doc/kyua-config.1.in b/contrib/kyua/doc/kyua-config.1.in new file mode 100644 index 000000000000..9c13ce06505e --- /dev/null +++ b/contrib/kyua/doc/kyua-config.1.in @@ -0,0 +1,59 @@ +.\" 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. +.Dd September 9, 2012 +.Dt KYUA-CONFIG 1 +.Os +.Sh NAME +.Nm "kyua config" +.Nd Inspects the values of the loaded configuration +.Sh SYNOPSIS +.Nm +.Op Ar variable1 .. variableN +.Sh DESCRIPTION +The +.Nm +command provides a way to list all defined configuration variables and +their current values. +.Pp +This command is intended to help you in resolving the values of the +configuration variables without having to scan over configuration files. +.Pp +In the default synopsis form (no arguments), the command prints all +configuration variables. +If any arguments are provided, the command will only print the +requested variables. +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if any of the specified configuration +variables does not exist. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/contrib/kyua/doc/kyua-db-exec.1.in b/contrib/kyua/doc/kyua-db-exec.1.in new file mode 100644 index 000000000000..04f34c7b54e7 --- /dev/null +++ b/contrib/kyua/doc/kyua-db-exec.1.in @@ -0,0 +1,80 @@ +.\" 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. +.Dd October 13, 2014 +.Dt KYUA-DB-EXEC 1 +.Os +.Sh NAME +.Nm "kyua db-exec" +.Nd Executes a SQL statement in a results file +.Sh SYNOPSIS +.Nm +.Op Fl -no-headers +.Op Fl -results-file Ar file +.Ar statement +.Sh DESCRIPTION +The +.Nm +command provides a way to execute an arbitrary SQL statement within the +database. +This command is mostly intended to aid in debugging, but can also be used to +extract information from the database when the current interfaces do not +provide the desired functionality. +.Pp +The input database must exist. +It makes no sense to use +.Nm +on a nonexistent or empty database. +.Pp +The +.Nm +command takes one or more arguments, all of which are concatenated to form +a single SQL statement. +Once the statement is executed, +.Nm +prints the resulting table on the screen, if any. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -no-headers +Avoids printing the headers of the table in the output of the command. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if the SQL statement is invalid or fails +to run. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-test 1 diff --git a/contrib/kyua/doc/kyua-db-migrate.1.in b/contrib/kyua/doc/kyua-db-migrate.1.in new file mode 100644 index 000000000000..67e46de46fec --- /dev/null +++ b/contrib/kyua/doc/kyua-db-migrate.1.in @@ -0,0 +1,63 @@ +.\" Copyright 2013 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. +.Dd October 13, 2014 +.Dt KYUA-DB-MIGRATE 1 +.Os +.Sh NAME +.Nm "kyua db-migrate" +.Nd Upgrades the schema of an existing results file +.Sh SYNOPSIS +.Nm +.Op Fl -results-file Ar file +.Sh DESCRIPTION +The +.Nm +command migrates the schema of an existing database to the latest +version implemented in +.Xr kyua 1 . +.Pp +This operation is not reversible. +However, a backup of the database is created in the same directory where the +database lives. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if the migration fails. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/contrib/kyua/doc/kyua-debug.1.in b/contrib/kyua/doc/kyua-debug.1.in new file mode 100644 index 000000000000..9e962a465421 --- /dev/null +++ b/contrib/kyua/doc/kyua-debug.1.in @@ -0,0 +1,145 @@ +.\" 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. +.Dd October 13, 2014 +.Dt KYUA-DEBUG 1 +.Os +.Sh NAME +.Nm "kyua debug" +.Nd Executes a single test case with facilities for debugging +.Sh SYNOPSIS +.Nm +.Op Fl -build-root Ar path +.Op Fl -kyuafile Ar file +.Op Fl -stdout Ar path +.Op Fl -stderr Ar path +.Ar test_case +.Sh DESCRIPTION +The +.Nm +command provides a mechanism to execute a single test case bypassing some +of the Kyua infrastructure and allowing the user to poke into the execution +behavior of the test. +.Pp +The test case to run is selected by providing a test filter, described below in +.Sx Test filters , +that matches a single test case. +The test case is executed and its result is printed as the last line of the +output of the tool. +.Pp +The test executed by +.Nm +is run under a controlled environment as described in +.Sx Test isolation . +.Pp +At the moment, the +.Nm +command allows the following aspects of a test case execution to be +tweaked: +.Bl -bullet +.It +Redirection of the test case's stdout and stderr to the console (the +default) or to arbitrary files. +See the +.Fl -stdout +and +.Fl -stderr +options below. +.El +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -build-root Ar path +Specifies the build root in which to find the test programs referenced +by the Kyuafile, if different from the Kyuafile's directory. +See +.Sx Build directories +below for more information. +.It Fl -kyuafile Ar file , Fl k Ar file +Specifies the Kyuafile to process. +Defaults to +.Pa Kyuafile +file in the current directory. +.It Fl -stderr Ar path +Specifies the file to which to send the standard error of the test +program's body. +The default is +.Pa /dev/stderr , +which is a special character device that redirects the output to +standard error on the console. +.It Fl -stdout Ar path +Specifies the file to which to send the standard output of the test +program's body. +The default is +.Pa /dev/stdout , +which is a special character device that redirects the output to +standard output on the console. +.El +.Pp +For example, consider the following Kyua session: +.Bd -literal -offset indent +$ kyua test +kernel/fs:mkdir -> passed +kernel/fs:rmdir -> failed: Invalid argument + +1/2 passed (1 failed) +.Ed +.Pp +At this point, we do not have a lot of information regarding the +failure of the +.Sq kernel/fs:rmdir +test. +We can run this test through the +.Nm +command to inspect its output a bit closer, hoping that the test case is +kind enough to log its progress: +.Bd -literal -offset indent +$ kyua debug kernel/fs:rmdir +Trying rmdir('foo') +Trying rmdir(NULL) +kernel/fs:rmdir -> failed: Invalid argument +.Ed +.Pp +Luckily, the offending test case was printing status lines as it +progressed, so we could see the last attempted call and we can know match +the failure message to the problem. +.Ss Build directories +__include__ build-root.mdoc COMMAND=debug +.Ss Test filters +__include__ test-filters.mdoc +.Ss Test isolation +__include__ test-isolation.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 if the test case passes or 1 if the test case fails. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyuafile 5 diff --git a/contrib/kyua/doc/kyua-help.1.in b/contrib/kyua/doc/kyua-help.1.in new file mode 100644 index 000000000000..2c4f2bc3859e --- /dev/null +++ b/contrib/kyua/doc/kyua-help.1.in @@ -0,0 +1,64 @@ +.\" 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. +.Dd September 9, 2012 +.Dt KYUA-HELP 1 +.Os +.Sh NAME +.Nm "kyua help" +.Nd Shows usage information +.Sh SYNOPSIS +.Nm +.Op Ar command +.Sh DESCRIPTION +The +.Nm +command provides interactive help on all supported commands and options. +If, for some reason, you happen to spot a discrepancy in the output of this +command and this document, the command is the authoritative source of +information. +.Pp +If no arguments are provided, the command prints the list of common options +and the list of supported subcommands. +.Pp +If the +.Ar command +argument is provided to, this single argument is the name of a valid +subcommand. +In that case, +.Nm +prints a textual description of the command, the list of common options and +the list of subcommand-specific options. +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/contrib/kyua/doc/kyua-list.1.in b/contrib/kyua/doc/kyua-list.1.in new file mode 100644 index 000000000000..5774354d9236 --- /dev/null +++ b/contrib/kyua/doc/kyua-list.1.in @@ -0,0 +1,90 @@ +.\" 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. +.Dd October 13, 2014 +.Dt KYUA-LIST 1 +.Os +.Sh NAME +.Nm "kyua list" +.Nd Lists test cases and their metadata +.Sh SYNOPSIS +.Nm +.Op Fl -build-root Ar path +.Op Fl -kyuafile Ar file +.Op Fl -verbose +.Ar test_case1 Op Ar .. test_caseN +.Sh DESCRIPTION +The +.Nm +command scans all the test programs and test cases in a test suite (as +defined by a +.Xr kyuafile 5 ) +and prints a list of all their names, optionally accompanied by any metadata +properties they have. +.Pp +The optional arguments to +.Nm +are used to select which test programs or test cases to run. +These are filters and are described below in +.Sx Test filters . +.Pp +This command must be run within a test suite or a test suite must be +provided with the +.Fl -kyuafile +flag. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -build-root Ar path +Specifies the build root in which to find the test programs referenced +by the Kyuafile, if different from the Kyuafile's directory. +See +.Sx Build directories +below for more information. +.It Fl -kyuafile Ar path , Fl k Ar path +Specifies the Kyuafile to process. +Defaults to a +.Pa Kyuafile +file in the current directory. +.It Fl -verbose , Fl v +Prints metadata properties for every test case. +.El +.Ss Build directories +__include__ build-root.mdoc COMMAND=list +.Ss Test filters +__include__ test-filters.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if any of the given test case filters +does not match any test case. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyuafile 5 diff --git a/contrib/kyua/doc/kyua-report-html.1.in b/contrib/kyua/doc/kyua-report-html.1.in new file mode 100644 index 000000000000..1f9f55b69a3f --- /dev/null +++ b/contrib/kyua/doc/kyua-report-html.1.in @@ -0,0 +1,103 @@ +.\" 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. +.Dd October 13, 2014 +.Dt KYUA-REPORT-HTML 1 +.Os +.Sh NAME +.Nm "kyua report-html" +.Nd Generates an HTML report with the results of a test suite run +.Sh SYNOPSIS +.Nm +.Op Fl -force +.Op Fl -output Ar path +.Op Fl -results-file Ar file +.Op Fl -results-filter Ar types +.Sh DESCRIPTION +The +.Nm +command provides a simple mechanism to generate HTML reports of the +execution of a test suite. +The command processes a results file and then populates a directory with +multiple HTML and supporting files to describe the results recorded in that +results file. +.Pp +The HTML output is static and self-contained, so it can easily be served by +any simple web server. +The command expects the target directory to not exist, because it would +overwrite any contents if not careful. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -force +Forces the deletion of the output directory if it exists. +Use care, as this effectively means a +.Sq rm -rf . +.It Fl -output Ar directory +Specifies the target directory into which to generate the HTML files. +The directory must not exist unless the +.Fl -force +option is provided. +The default is +.Pa ./html . +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.It Fl -results-filter Ar types +Comma-separated list of the test result types to include in the report. +The ordering of the values is respected so that you can determine how you +want the list of tests to be shown. +.Pp +The valid values are: +.Sq broken , +.Sq failed , +.Sq passed , +.Sq skipped +and +.Sq xfail . +If the parameter supplied to the option is empty, filtering is suppressed +and all result types are shown in the report. +.Pp +The default value for this flag includes all the test results except the +passed tests. +Showing the passed tests by default clutters the report with too much +information, so only abnormal conditions are included. +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report-html +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report 1 , +.Xr kyua-report-junit 1 diff --git a/contrib/kyua/doc/kyua-report-junit.1.in b/contrib/kyua/doc/kyua-report-junit.1.in new file mode 100644 index 000000000000..f1ad3a2e7f29 --- /dev/null +++ b/contrib/kyua/doc/kyua-report-junit.1.in @@ -0,0 +1,87 @@ +.\" 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. +.Dd October 13, 2014 +.Dt KYUA-REPORT-JUNIT 1 +.Os +.Sh NAME +.Nm "kyua report-junit" +.Nd Generates a JUnit report with the results of a test suite run +.Sh SYNOPSIS +.Nm +.Op Fl -output Ar path +.Op Fl -results-file Ar file +.Sh DESCRIPTION +The +.Nm +command provides a simple mechanism to generate JUnit reports of the +execution of a test suite. +The command processes a results file and then generates a single XML file +that complies with the JUnit XSchema. +.Pp +The JUnit output is static and self-contained, so it can easily be plugged +into any continuous integration system, like Jenkins. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -output Ar directory +Specifies the file into which to store the JUnit report. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.El +.Ss Caveats +Because of limitations in the JUnit XML schema, not all the data collected by +Kyua can be properly represented in JUnit reports. +However, because test data are extremely useful for debugging purposes, the +.Nm +command shovels these data into the JUnit output. +In particular: +.Bl -bullet +.It +The test case metadata values are prepended to the test case's standard error +output. +.It +Test cases that report expected failures as their results are recorded as +passed. +The fact that they failed as expected is recorded in the test case's standard +error output along with the corresponding reason. +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report-junit +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report 1 , +.Xr kyua-report-html 1 diff --git a/contrib/kyua/doc/kyua-report.1.in b/contrib/kyua/doc/kyua-report.1.in new file mode 100644 index 000000000000..8e2485f9c4ac --- /dev/null +++ b/contrib/kyua/doc/kyua-report.1.in @@ -0,0 +1,118 @@ +.\" 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. +.Dd October 13, 2014 +.Dt KYUA-REPORT 1 +.Os +.Sh NAME +.Nm "kyua report" +.Nd Generates reports with the results of a test suite run +.Sh SYNOPSIS +.Nm +.Op Fl -output Ar path +.Op Fl -results-file Ar file +.Op Fl -results-filter Ar types +.Op Fl -verbose +.Op Ar test_filter1 .. test_filterN +.Sh DESCRIPTION +The +.Nm +command parses a results file and generates a user-friendly, plaintext +report for user consumption on the terminal. +By default, these reports only display a summary of the execution of the full +test suite to highlight where problems may lie. +.Pp +The output of +.Nm +can be customized to display full details on all executed test cases. +Additionally, the optional arguments to +.Nm +are used to select which test programs or test cases to display. +These are filters and are described below in +.Sx Test filters . +.Pp +Reports generated by +.Nm +are +.Em not intended to be machine-parseable . +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -output Ar path +Specifies the path to which the report should be written to. +The special values +.Pa /dev/stdout +and +.Pa /dev/stderr +can be used to specify the standard output and the standard error, +respectively. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.It Fl -results-filter Ar types +Comma-separated list of the test result types to include in the report. +The ordering of the values is respected so that you can determine how you +want the list of tests to be shown. +.Pp +The valid values are: +.Sq broken , +.Sq failed , +.Sq passed , +.Sq skipped +and +.Sq xfail . +If the parameter supplied to the option is empty, filtering is suppressed +and all result types are shown in the report. +.Pp +The default value for this flag includes all the test results except the +passed tests. +Showing the passed tests by default clutters the report with too much +information, so only abnormal conditions are included. +.It Fl -verbose +Prints a detailed report of the execution. +In addition to all the information printed by default, verbose reports +include the runtime context of the test suite run, the metadata of each +test case, and the verbatim output of the test cases. +.El +.Ss Results files +__include__ results-files.mdoc +.Ss Test filters +__include__ test-filters.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 if no filters were specified or if all filters match one +or more test cases. +If any filter fails to match any test case, the command returns 1. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report-html 1 , +.Xr kyua-report-junit 1 diff --git a/contrib/kyua/doc/kyua-test.1.in b/contrib/kyua/doc/kyua-test.1.in new file mode 100644 index 000000000000..8cd5f34ae6af --- /dev/null +++ b/contrib/kyua/doc/kyua-test.1.in @@ -0,0 +1,102 @@ +.\" 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. +.Dd October 13, 2014 +.Dt KYUA-TEST 1 +.Os +.Sh NAME +.Nm "kyua test" +.Nd Runs tests +.Sh SYNOPSIS +.Nm +.Op Fl -build-root Ar path +.Op Fl -kyuafile Ar file +.Op Fl -results-file Ar file +.Op Ar test_filter1 .. test_filterN +.Sh DESCRIPTION +The +.Nm +command loads a test suite definition from a +.Xr kyuafile 5 , +runs the tests defined in it, and records the results into a new results +file. +By default, all tests in the test suite are executed but the optional +arguments to +.Nm +can be used to select which test programs or test cases to run. +These are filters and are described below in +.Sx Test filters . +.Pp +Every test executed by +.Nm +is run under a controlled environment as described in +.Sx Test isolation . +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -build-root Ar path +Specifies the build root in which to find the test programs referenced by +the Kyuafile, if different from the Kyuafile's directory. +See +.Sx Build directories +below for more information. +.It Fl -kyuafile Ar path , Fl k Ar path +Specifies the Kyuafile to process. +Defaults to a +.Pa Kyuafile +file in the current directory. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-write.mdoc +.El +.Pp +You can later inspect the results of the test run in more detail by using +.Xr kyua-report 1 +or you can execute a single test case with debugging functionality by using +.Xr kyua-debug 1 . +.Ss Build directories +__include__ build-root.mdoc COMMAND=test +.Ss Results files +__include__ results-files.mdoc +.Ss Test filters +__include__ test-filters.mdoc +.Ss Test isolation +__include__ test-isolation.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 if all executed test cases pass or 1 if any of the +executed test cases fails or if any of the given test case filters does not +match any test case. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report 1 , +.Xr kyuafile 5 diff --git a/contrib/kyua/doc/kyua.1.in b/contrib/kyua/doc/kyua.1.in new file mode 100644 index 000000000000..2fca5eb09f9f --- /dev/null +++ b/contrib/kyua/doc/kyua.1.in @@ -0,0 +1,400 @@ +.\" 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. +.Dd May 12, 2015 +.Dt KYUA 1 +.Os +.Sh NAME +.Nm kyua +.Nd Testing framework for infrastructure software +.Sh SYNOPSIS +.Nm +.Op Fl -config Ar file +.Op Fl -logfile Ar file +.Op Fl -loglevel Ar level +.Op Fl -variable Ar name=value +.Ar command +.Op Ar command_options +.Op Ar command_arguments +.Sh DESCRIPTION +.Em If you are here looking for details on how to run the test suite in +.Pa /usr/tests +.Em ( or +.Pa __TESTSDIR__ ) , +.Em please start by reading the +.Xr tests 7 +.Em manual page that should be supplied by your system . +.Pp +Kyua is a testing framework for infrastructure software, originally +designed to equip BSD-based operating systems with a test suite. +This means that Kyua is lightweight and simple, and that Kyua integrates well +with various build systems and continuous integration frameworks. +.Pp +Kyua features an expressive test suite definition language, a safe +runtime engine for test suites and a powerful report generation engine. +.Pp +Kyua is for both developers and users, from the developer applying a +simple fix to a library to the system administrator deploying a new +release on a production machine. +.Pp +Kyua is able to execute test programs written with a plethora of testing +libraries and languages. +The test program library of choice is ATF, which +.Nm Ns 's +design originated from. +However, framework-less test programs and TAP-compliant test programs can also +be executed through +.Nm +.Ss Overview +As can be observed in the synopsis, the interface of +.Nm +implements a common subcommand-based interface. +The arguments to the tool specify, in this order: a set of common options +that all the commands accept, a required +.Ar command +name that specifies what +.Nm +should do, and +a set of possibly-optional +.Ar command_options +and +.Ar command_arguments +that are specific to the chosen command. +.Pp +The following options are recognized by all the commands. +Keep in mind that these must always be specified before the command name. +.Bl -tag -width XX +.It Fl -config Ar path , Fl c Ar path +Specifies the configuration file to process, which must be in the format +described in +.Xr kyua.conf 5 . +The special value +.Sq none +explicitly disables the loading of any configuration file. +.Pp +Defaults to +.Pa ~/.kyua/kyua.conf +if it exists, otherwise to +.Pa __CONFDIR__/kyua.conf +if it exists, +or else to +.Sq none . +.It Fl -logfile Ar path +Specifies the location of the file to which +.Nm +will log run time events useful for postmortem debugging. +.Pp +The default depends on different environment variables as described in +.Sx Logging , +but typically the file will be stored within the user's home directory. +.It Fl -loglevel Ar level +Specifies the maximum logging level to record in the log file. +See +.Sx Logging +for more details. +.Pp +The default is +.Sq info . +.It Fl -variable Ar name=value , Fl v Ar name=value +Sets the +.Ar name +configuration variable to +.Ar value . +The values set through this option have preference over the values set in the +configuration file. +.Pp +The specified variable can either be a builtin variable or a test-suite +specific variable. +See +.Xr kyua.conf 5 +for more details. +.El +.Pp +The following commands are generic and do not have any relation to the execution +of tests or the inspection of their results: +.Bl -tag -width reportXjunitXX -offset indent +.It Ar about +Shows general program information. +See +.Xr kyua-about 1 . +.It Ar config +Inspects the values of the configuration variables. +See +.Xr kyua-config 1 . +.It Ar db-exec +Executes an arbitrary SQL statement on a results file and prints the +resulting table. +See +.Xr kyua-db-exec 1 . +.It Ar help +Shows usage information. +See +.Xr kyua-help 1 . +.El +.Pp +The following commands are used to generate reports based on the data previously +recorded in a results file: +.Bl -tag -width reportXjunitXX -offset indent +.It Ar report +Generates a plaintext report. +Combined with its +.Fl -verbose +flag and the ability to only display specific test cases, this command can also +be used to debug test failures post-facto on the console. +See +.Xr kyua-report 1 . +.It Ar report-html +Generates an HTML report. +See +.Xr kyua-report-html 1 . +.It Ar report-junit +Generates a JUnit report. +See +.Xr kyua-report-junit 1 . +.El +.Pp +The following commands are used to interact with a test suite: +.Bl -tag -width reportXjunitXX -offset indent +.It Ar debug +Executes a single test case in a controlled environment for debugging purposes. +See +.Xr kyua-debug 1 . +.It Ar list +Lists test cases defined in a test suite by a +.Xr kyuafile 5 +and, optionally, displays their metadata. +See +.Xr kyua-list 1 . +.It Ar test +Runs tests defined in a test suite by a +.Xr kyuafile 5 . +See +.Xr kyua-test 1 . +.El +.Ss Logging +.Nm +has a logging facility that collects all kinds of events at run time. +These events are always logged to a file so that the log is available when +it is most needed: right after a non-reproducible problem happens. +The only way to disable logging is by sending the log to +.Pa /dev/null . +.Pp +The location of the log file can be manually specified with the +.Fl -logfile +option, which applies to all commands. +If no file is explicitly specified, the location of the log files is chosen in +this order: +.Bl -enum -offset indent +.It +.Pa ${HOME}/.kyua/logs/ +if +.Va HOME +is defined. +.It +.Pa ${TMPDIR}/ +if +.Va TMPDIR +is defined. +.It +.Pa /tmp/ . +.El +.Pp +And the default naming scheme of the log files is: +.Sq ..log . +.Pp +The messages stored in the log file have a level (or severity) attached to +them. +These are: +.Bl -tag -width warningXX -offset indent +.It error +Fatal error messages. +The program generally terminates after these, either in a clean manner or by +crashing. +.It warning +Non-fatal error messages. +These generally report a condition that must be addressed but the application +can continue to run. +.It info +Informational messages. +These tell the user what the program was doing at a general level of +operation. +.It debug +Detailed informational messages. +These are often useful when debugging problems in the application, as they +contain lots of internal details. +.El +.Pp +The default log level is +.Sq info +unless explicitly overridden with +.Fl -loglevel . +.Pp +The log file is a plain text file containing one line per log record. +The format of each line is as follows: +.Bd -literal -offset indent +timestamp entry_type pid file:line: message +.Ed +.Pp +.Ar entry_type +can be one of: +.Sq E +for an error, +.Sq W +for a warning, +.Sq I +for an informational message and +.Sq D +for a debug message. +.Ss Bug reporting +If you think you have encountered a bug in +.Nm , +please take the time to let the developers know about it. +This will ensure that the bug is addressed and potentially fixed in the next +Kyua release. +.Pp +The first step in reporting a bug is to check if there already is a similar +bug in the database. +You can check what issues are currently in the database by going to: +.Bd -literal -offset indent +https://github.com/jmmv/kyua/issues/ +.Ed +.Pp +If there is no existing issue that describes an issue similar to the +one you are experiencing, you can open a new one by visiting: +.Bd -literal -offset indent +https://github.com/jmmv/kyua/issues/new/ +.Ed +.Pp +When doing so, please include as much detail as possible. +Among other things, explain what operating system and platform you are running +.Nm +on, what were you trying to do, what exact messages you saw on the screen, +how did you expect the program to behave, and any other details that you +may find relevant. +.Pp +Also, please include a copy of the log file corresponding to the problem +you are experiencing. +Unless you have changed the location of the log files, you can most likely +find them in +.Pa ~/.kyua/logs/ . +If the problem is reproducible, it is good idea to regenerate the log file +with an increased log level so as to provide more information. +For example: +.Bd -literal -offset indent +$ kyua --logfile=problem.log --loglevel=debug \\ + [rest of the command line] +.Ed +.Sh ENVIRONMENT +The following variables are recognized and can be freely tuned by the end user: +.Bl -tag -width COLUMNSXX +.It Va COLUMNS +The width of the screen, in number of characters. +.Nm +uses this to wrap long lines. +If not present, the width of the screen is determined from the terminal +stdout is connected to, and, if the guessing fails, this defaults to infinity. +.It Va HOME +Path to the user's home directory. +.Nm +uses this location to determine paths to configuration files and default log +files. +.It Va TMPDIR +Path to the system-wide temporary directory. +.Nm +uses this location to place the work directory of test cases, among other +things. +.Pp +The default value of this variable depends on the operating system. +In general, it is +.Pa /tmp . +.El +.Pp +The following variables are also recognized, but you should not need to set them +during normal operation. +They are only provided to override the value of built-in values, which is useful +when testing +.Nm +itself: +.Bl -tag -width KYUAXCONFDIRXX +.It Va KYUA_CONFDIR +Path to the system-wide configuration files for +.Nm . +.Pp +Defaults to +.Pa __CONFDIR__ . +.It Va KYUA_DOCDIR +Path to the location of installed documentation. +.Pp +Defaults to +.Pa __DOCDIR__ . +.It Va KYUA_MISCDIR +Path to the location of the installed miscellaneous scripts and data +files provided by +.Nm . +.Pp +Defaults to +.Pa __MISCDIR__ . +.It Va KYUA_STOREDIR +Path to the location of the installed store support files; e.g., the +directory containing the SQL database schema. +.Pp +Defaults to +.Pa __STOREDIR__ . +.El +.Sh FILES +.Bl -tag -width XXXX +.It Pa ~/.kyua/store/ +Default location for the results files. +.It Pa ~/.kyua/kyua.conf +User-specific configuration file. +.It Pa ~/.kyua/logs/ +Default location for the collected log files. +.It Pa __CONFDIR__/kyua.conf +System-wide configuration file. +.El +.Sh EXIT STATUS +.Nm +returns 0 on success, 1 on a controlled error condition in the given +subcommand, 2 on a general unexpected error and 3 on a usage error. +.Pp +The documentation of the subcommands in the corresponding manual pages only +details the difference between a successful exit (0) and the detection of a +controlled error (1). +Even though when those manual pages do not describe any other exit statuses, +codes above 1 can be returned. +.Sh SEE ALSO +.Xr kyua.conf 5 , +.Xr kyuafile 5 , +.Xr atf 7 , +.Xr tests 7 +.Sh AUTHORS +For more details on the people that made +.Nm +possible and the license terms, run: +.Bd -literal -offset indent +$ kyua about +.Ed diff --git a/contrib/kyua/doc/kyua.conf.5.in b/contrib/kyua/doc/kyua.conf.5.in new file mode 100644 index 000000000000..05a9499b48c4 --- /dev/null +++ b/contrib/kyua/doc/kyua.conf.5.in @@ -0,0 +1,141 @@ +.\" 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. +.Dd February 20, 2015 +.Dt KYUA.CONF 5 +.Os +.Sh NAME +.Nm kyua.conf +.Nd Configuration file for the kyua tool +.Sh SYNOPSIS +.Fn syntax "int version" +.Pp +Variables: +.Va architecture , +.Va platform , +.Va test_suites , +.Va unprivileged_user . +.Sh DESCRIPTION +The configuration of Kyua is a simple collection of key/value pairs called +configuration variables. +There are configuration variables that have a special meaning to the runtime +engine implemented by +.Xr kyua 1 , +and there are variables that only have meaning in the context of particular +test suites. +.Pp +Configuration files are Lua scripts. +In their most basic form, their whole purpose is to assign values to +variables, but the user has the freedom to implement any logic he desires +to compute such values. +.Ss File versioning +Every +.Nm +file starts with a call to +.Fn syntax "int version" . +This call determines the specific schema used by the file so that future +backwards-incompatible modifications to the file can be introduced. +.Pp +Any new +.Nm +file should set +.Fa version +to +.Sq 2 . +.Ss Runtime configuration variables +The following variables are internally recognized by +.Xr kyua 1 : +.Bl -tag -width XX -offset indent +.It Va architecture +Name of the system architecture (aka processor type). +.It Va parallelism +Maximum number of test cases to execute concurrently. +.It Va platform +Name of the system platform (aka machine type). +.It Va unprivileged_user +Name or UID of the unprivileged user. +.Pp +If set, the given user must exist in the system and his privileges will be +used to run test cases that need regular privileges when +.Xr kyua 1 +is executed as root. +.El +.Ss Test-suite configuration variables +Each test suite is able to recognize arbitrary configuration variables, and +their type and meaning is specific to the test suite. +Because the existence and naming of these variables depends on every test +suite, this manual page cannot detail them; please refer to the documentation +of the test suite you are working with for more details on this topic. +.Pp +Test-suite specific configuration variables are defined inside the +.Va test_suites +dictionary. +The general syntax is: +.Bd -literal -offset indent +test_suites.. = +.Ed +.Pp +where +.Va test_suite_name +is the name of the test suite, +.Va variable_name +is the name of the variable to set, and +.Va value +is a value. +The value can be a string, an integer or a boolean. +.Sh FILES +.Bl -tag -width XX +.It __EGDIR__/kyua.conf +Sample configuration file. +.El +.Sh EXAMPLES +The following +.Nm +shows a simple configuration file that overrides a bunch of the built-in +.Xr kyua 1 +configuration variables: +.Bd -literal -offset indent +syntax(2) + +architecture = 'x86_64' +platform = 'amd64' +.Ed +.Pp +The following is a more complex example that introduces the definition of +per-test suite configuration variables: +.Bd -literal -offset indent +syntax(2) + +-- Assign built-in variables. +unprivileged_user = '_tests' + +-- Assign test-suite variables. All of these must be strings. +test_suites.NetBSD.file_systems = 'ffs ext2fs' +test_suites.X11.graphics_driver = 'vesa' +.Ed +.Sh SEE ALSO +.Xr kyua 1 diff --git a/contrib/kyua/doc/kyuafile.5.in b/contrib/kyua/doc/kyuafile.5.in new file mode 100644 index 000000000000..06cb2dbc42a8 --- /dev/null +++ b/contrib/kyua/doc/kyuafile.5.in @@ -0,0 +1,407 @@ +.\" 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. +.Dd July 3, 2015 +.Dt KYUAFILE 5 +.Os +.Sh NAME +.Nm Kyuafile +.Nd Test suite description files +.Sh SYNOPSIS +.Fn atf_test_program "string name" "[string metadata]" +.Fn current_kyuafile +.Fn fs.basename "string path" +.Fn fs.dirname "string path" +.Fn fs.exists "string path" +.Fn fs.files "string path" +.Fn fs.is_absolute "string path" +.Fn fs.join "string path" "string path" +.Fn include "string path" +.Fn plain_test_program "string name" "[string metadata]" +.Fn syntax "int version" +.Fn tap_test_program "string name" "[string metadata]" +.Fn test_suite "string name" +.Sh DESCRIPTION +A test suite is a collection of test programs and is represented by a +hierarchical layout of test binaries on the file system. +Any subtree of the file system can represent a test suite, provided that it +includes one or more +.Nm Ns s , +which are the test suite definition files. +.Pp +A +.Nm +is a Lua script whose purpose is to describe the structure of the test +suite it belongs to. +To do so, the script has access to a collection of special functions provided +by +.Xr kyua 1 +as described in +.Sx Helper functions . +.Ss File versioning +Every +.Nm +file starts with a call to +.Fn syntax "int version" . +This call determines the specific schema used by the file so that future +backwards-incompatible modifications to the file can be introduced. +.Pp +Any new +.Nm +file should set +.Fa version +to +.Sq 2 . +.Ss Test suite definition +If the +.Nm +registers any test programs, +the +.Nm +must define the name of the test suite the test programs belong to by using the +.Fn test_suite +function at the very beginning of the file. +.Pp +The test suite name provided in the +.Fn test_suite +call tells +.Xr kyua 1 +which set of configuration variables from +.Xr kyua.conf 5 +to pass to the test programs at run time. +.Ss Test program registration +A +.Nm +can register test programs by means of a variety of +.Fn *_test_program +functions, all of which take the name of a test program and a set of +optional metadata properties that describe such test program. +.Pp +The test programs to be registered must live in the current directory; in +other words, the various +.Fn *_test_program +calls cannot reference test programs in other directories. +The rationale for this is to force all +.Nm +files to be self-contained, and to simplify their internal representation. +.Pp +.Em ATF test programs +are those that use the +.Xr atf 7 +libraries. +They can be registered with the +.Fn atf_test_program +table constructor. +This function takes the +.Fa name +of the test program and a collection of optional metadata settings for all +the test cases in the test program. +Any metadata properties defined by the test cases themselves override the +metadata values defined here. +.Pp +.Em Plain test programs +are those that return 0 on success and non-0 on failure; in general, most test +programs (even those that use fancy unit-testing libraries) behave this way and +thus also qualify as plain test programs. +They can be registered with the +.Fn plain_test_program +table constructor. +This function takes the +.Fa name +of the test program, an optional +.Fa test_suite +name that overrides the global test suite name, and a collection of optional +metadata settings for the test program. +.Pp +.Em TAP test programs +are those that implement the Test Anything Protocol. +They can be registered with the +.Fn tap_test_program +table constructor. +This function takes the +.Fa name +of the test program and a collection of optional metadata settings for the +test program. +.Pp +The following metadata properties can be passed to any test program definition: +.Bl -tag -width XX -offset indent +.It Va allowed_architectures +Whitespace-separated list of machine architecture names allowed by the test. +If empty or not defined, the test is allowed to run on any machine +architecture. +.It Va allowed_platforms +Whitespace-separated list of machine platform names allowed by the test. +If empty or not defined, the test is allowed to run on any machine +platform. +.It Va custom.NAME +Custom variable defined by the test where +.Sq NAME +denotes the name of the variable. +These variables are useful to tag your tests with information specific to +your project. +The values of such variables are propagated all the way from the tests to the +results files and later to any generated reports. +.Pp +Note that if the name happens to have dashes or any other special characters +in it, you will have to use a special Lua syntax to define the property. +Refer to the +.Sx EXAMPLES +section below for clarification. +.It Va description +Textual description of the test. +.It Va is_exclusive +If true, indicates that this test program cannot be executed along any other +programs at the same time. +Test programs that affect global system state, such as those that modify the +value of a +.Xr sysctl 8 +setting, must set themselves as exclusive to prevent failures due to race +conditions. +Defaults to false. +.It Va required_configs +Whitespace-separated list of configuration variables that the test requires +to be defined before it can run. +.It Va required_disk_space +Amount of available disk space that the test needs to run successfully. +.It Va required_files +Whitespace-separated list of paths that the test requires to exist before +it can run. +.It Va required_memory +Amount of physical memory that the test needs to run successfully. +.It Va required_programs +Whitespace-separated list of basenames or absolute paths pointing to executable +binaries that the test requires to exist before it can run. +.It Va required_user +If empty, the test has no restrictions on the calling user for it to run. +If set to +.Sq unprivileged , +the test needs to not run as root. +If set to +.Sq root , +the test must run as root. +.It Va timeout +Amount of seconds that the test is allowed to execute before being killed. +.El +.Ss Recursion +To reference test programs in another subdirectory, a different +.Nm +must be created in that directory and it must be included into the original +.Nm +by means of the +.Fn include +function. +.Pp +.Fn include +may only be called with a relative path and with at most one directory +component. +This is by design: Kyua uses the file system structure as the layout of the +test suite definition. +Therefore, each subdirectory in a test suite must include its own +.Nm +and each +.Nm +can only descend into the +.Nm Ns s +of immediate subdirectories. +.Pp +If you need to source a +.Nm +located in disjoint parts of your file system namespace, you will have to +create a +.Sq shadow tree +using symbolic links and possibly helper +.Nm Ns s +to plug the various subdirectories together. +See the +.Sx EXAMPLES +section below for details. +.Pp +Note that each file is processed in its own Lua environment: there is no +mechanism to pass state from one file to the other. +The reason for this is that there is no such thing as a +.Dq top-level +.Nm +in a test suite: the user has to be able to run the test suite from any +directory in a given hierarchy, and this execution must not depend on files +that live in parent directories. +.Ss Top-level Kyuafile +Every system has a top directory into which test suites get installed. +The default is +.Pa __TESTSDIR__ . +Within this directory live test suites, each of which is in an independent +subdirectory. +Each subdirectory can be provided separately by independent third-party +packages. +.Pp +Kyua allows running all the installed test suites at once in order to +provide comprehensive cross-component reports. +In order to do this, there is a special file in the top directory that knows +how to inspect the subdirectories in search for other Kyuafiles and include +them. +.Pp +The +.Sx FILES +section includes more details on where this file lives. +.Ss Helper functions +The +.Sq base , +.Sq string , +and +.Sq table +Lua modules are fully available in the context of a +.Nm . +.Pp +The following extra functions are provided by Kyua: +.Bl -tag -width XX -offset indent +.It Ft string Fn current_kyuafile +Returns the absolute path to the current +.Nm . +.It Ft string Fn fs.basename "string path" +Returns the last component of the given path. +.It Ft string Fn fs.dirname "string path" +Returns the given path without its last component or a dot if the path has +a single component. +.It Ft bool Fn fs.exists "string path" +Checks if the given path exists. +If the path is not absolute, it is relative to the directory containing the +.Nm +in which the call to this function occurs. +.It Ft iterator Fn fs.files "string path" +Opens a directory for scanning of its entries. +The returned iterator yields an entry on each call, and the entry is simply +the filename. +If the path is not absolute, it is relative to the directory containing the +.Nm +in which the call to this function occurs. +.It Ft is_absolute Fn fs.is_absolute "string path" +Returns true if the given path is absolute; false otherwise. +.It Ft join Fn fs.join "string path" "string path" +Concatenates the two paths. +The second path cannot be absolute. +.El +.Sh FILES +.Bl -tag -width XX +.It Pa __TESTSDIR__/Kyuafile . +Top-level +.Nm +for the current system. +.It Pa __EGDIR__/Kyuafile.top . +Sample file to serve as a top-level +.Nm . +.El +.Sh EXAMPLES +The following +.Nm +is the simplest you can define. +It provides a test suite definition and registers a couple of different test +programs using different interfaces: +.Bd -literal -offset indent +syntax(2) + +test_suite('first') + +atf_test_program{name='integration_test'} +plain_test_program{name='legacy_test'} +.Ed +.Pp +The following example is a bit more elaborate. +It introduces some metadata properties to the test program definitions and +recurses into a couple of subdirectories: +.Bd -literal -offset indent +syntax(2) + +test_suite('second') + +plain_test_program{name='legacy_test', + allowed_architectures='amd64 i386', + required_files='/bin/ls', + timeout=30} + +tap_test_program{name='privileged_test', + required_user='root'} + +include('module-1/Kyuafile') +include('module-2/Kyuafile') +.Ed +.Pp +The syntax to define custom properties may be not obvious if their names +have any characters that make the property name not be a valid Lua identifier. +Dashes are just one example. +To set such properties, do something like this: +.Bd -literal -offset indent +syntax(2) + +test_suite('FreeBSD') + +plain_test_program{name='the_test', + ['custom.FreeBSD-Bug-Id']='category/12345'} +.Ed +.Ss Connecting disjoint test suites +Now suppose you had various test suites on your file system and you would +like to connect them together so that they could be executed and treated as +a single unit. +The test suites we would like to connect live under +.Pa /usr/tests , +.Pa /usr/local/tests +and +.Pa ~/local/tests . +.Pp +We cannot create a +.Nm +that references these because the +.Fn include +directive does not support absolute paths. +Instead, what we can do is create a shadow tree using symbolic links: +.Bd -literal -offset indent +$ mkdir ~/everything +$ ln -s /usr/tests ~/everything/system-tests +$ ln -s /usr/local/tests ~/everything/local-tests +$ ln -s ~/local/tests ~/everything/home-tests +.Ed +.Pp +And then we create an +.Pa ~/everything/Kyuafile +file to drive the execution of the integrated test suite: +.Bd -literal -offset indent +syntax(2) + +test_suite('test-all-the-things') + +include('system-tests/Kyuafile') +include('local-tests/Kyuafile') +include('home-tests/Kyuafile') +.Ed +.Pp +Or, simply, you could reuse the sample top-level +.Nm +to avoid having to manually craft the list of directories into which to +recurse: +.Bd -literal -offset indent +$ cp __EGDIR__/Kyuafile.top ~/everything/Kyuafile +.Ed +.Sh SEE ALSO +.Xr kyua 1 diff --git a/contrib/kyua/doc/manbuild.sh b/contrib/kyua/doc/manbuild.sh new file mode 100755 index 000000000000..e01239909183 --- /dev/null +++ b/contrib/kyua/doc/manbuild.sh @@ -0,0 +1,171 @@ +#! /bin/sh +# 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 doc/manbuild.sh +# Generates a manual page from a source file. +# +# Input files can have __VAR__-style patterns in them that are replaced +# with the values provided by the caller via the -v VAR=VALUE flag. +# +# Input files can also include other files using the __include__ directive, +# which takes a relative path to the file to include plus an optional +# collection of additional variables to replace in the included file. + + +# Name of the running program for error reporting purposes. +Prog_Name="${0##*/}" + + +# Prints an error message and exits. +# +# Args: +# ...: The error message to print. Multiple arguments are joined with a +# single space separator. +err() { + echo "${Prog_Name}: ${*}" 1>&2 + exit 1 +} + + +# Invokes sed(1) translating input variables to expressions. +# +# Args: +# ...: List of var=value pairs to replace. +# +# Returns: +# True if the operation succeeds; false otherwise. +sed_with_vars() { + local vars="${*}" + + set -- + for pair in ${vars}; do + local var="$(echo "${pair}" | cut -d = -f 1)" + local value="$(echo "${pair}" | cut -d = -f 2-)" + set -- "${@}" -e"s&__${var}__&${value}&g" + done + + if [ "${#}" -gt 0 ]; then + sed "${@}" + else + cat + fi +} + + +# Generates the manual page reading from stdin and dumping to stdout. +# +# Args: +# include_dir: Path to the directory containing the include files. +# ...: List of var=value pairs to replace in the manpage. +# +# Returns: +# True if the generation succeeds; false otherwise. +generate() { + local include_dir="${1}"; shift + + while :; do + local read_ok=yes + local oldifs="${IFS}" + IFS= + read -r line || read_ok=no + IFS="${oldifs}" + [ "${read_ok}" = yes ] || break + + case "${line}" in + __include__*) + local file="$(echo "${line}" | cut -d ' ' -f 2)" + local extra_vars="$(echo "${line}" | cut -d ' ' -f 3-)" + # If we fail to output the included file, just leave the line as + # is. validate_file() will later error out. + [ -f "${include_dir}/${file}" ] || echo "${line}" + generate <"${include_dir}/${file}" "${include_dir}" \ + "${@}" ${extra_vars} || echo "${line}" + ;; + + *) + echo "${line}" + ;; + esac + done | sed_with_vars "${@}" +} + + +# Validates that the manual page has been properly generated. +# +# In particular, this checks if any directives or common replacement patterns +# have been left in place. +# +# Returns: +# True if the manual page is valid; false otherwise. +validate_file() { + local filename="${1}" + + if grep '__[A-Za-z0-9]*__' "${filename}" >/dev/null; then + return 1 + else + return 0 + fi +} + + +# Program entry point. +main() { + local vars= + + while getopts :v: arg; do + case "${arg}" in + v) + vars="${vars} ${OPTARG}" + ;; + + \?) + err "Unknown option -${OPTARG}" + ;; + esac + done + shift $((${OPTIND} - 1)) + + [ ${#} -eq 2 ] || err "Must provide input and output names as arguments" + local input="${1}"; shift + local output="${1}"; shift + + trap "rm -f '${output}.tmp'" EXIT HUP INT TERM + generate "$(dirname "${input}")" ${vars} \ + <"${input}" >"${output}.tmp" \ + || err "Failed to generate ${output}" + if validate_file "${output}.tmp"; then + : + else + err "Failed to generate ${output}; some patterns were left unreplaced" + fi + mv "${output}.tmp" "${output}" +} + + +main "${@}" diff --git a/contrib/kyua/doc/manbuild_test.sh b/contrib/kyua/doc/manbuild_test.sh new file mode 100755 index 000000000000..87234324e829 --- /dev/null +++ b/contrib/kyua/doc/manbuild_test.sh @@ -0,0 +1,235 @@ +# 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. + + +# Absolute path to the uninstalled script. +MANBUILD="__MANBUILD__" + + +atf_test_case empty +empty_body() { + touch input + atf_check "${MANBUILD}" input output + atf_check cat output +} + + +atf_test_case no_replacements +no_replacements_body() { + cat >input <input <expout <input <expout <input <expout <doc/input <doc/subdir/chunk <doc/chunk <expout <input <chunk <expout <input <input < +#include + +#include "engine/filters.hpp" +#include "engine/kyuafile.hpp" +#include "engine/scanner.hpp" +#include "engine/scheduler.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/auto_cleaners.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::optional; + + +/// Executes the operation. +/// +/// \param kyuafile_path The path to the Kyuafile to be loaded. +/// \param build_root If not none, path to the built test programs. +/// \param filter The test case filter to locate the test to debug. +/// \param user_config The end-user configuration properties. +/// \param stdout_path The name of the file into which to store the test case +/// stdout. +/// \param stderr_path The name of the file into which to store the test case +/// stderr. +/// +/// \returns A structure with all results computed by this driver. +drivers::debug_test::result +drivers::debug_test::drive(const fs::path& kyuafile_path, + const optional< fs::path > build_root, + const engine::test_filter& filter, + const config::tree& user_config, + const fs::path& stdout_path, + const fs::path& stderr_path) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + kyuafile_path, build_root, user_config, handle); + std::set< engine::test_filter > filters; + filters.insert(filter); + + engine::scanner scanner(kyuafile.test_programs(), filters); + optional< engine::scan_result > match; + while (!match && !scanner.done()) { + match = scanner.yield(); + } + if (!match) { + throw std::runtime_error(F("Unknown test case '%s'") % filter.str()); + } else if (!scanner.done()) { + throw std::runtime_error(F("The filter '%s' matches more than one test " + "case") % filter.str()); + } + INV(match && scanner.done()); + const model::test_program_ptr test_program = match.get().first; + const std::string& test_case_name = match.get().second; + + scheduler::result_handle_ptr result_handle = handle.debug_test( + test_program, test_case_name, user_config, + stdout_path, stderr_path); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + const model::test_result test_result = test_result_handle->test_result(); + result_handle->cleanup(); + + handle.check_interrupt(); + handle.cleanup(); + + return result(engine::test_filter( + test_program->relative_path(), test_case_name), test_result); +} diff --git a/contrib/kyua/drivers/debug_test.hpp b/contrib/kyua/drivers/debug_test.hpp new file mode 100644 index 000000000000..cbaa2f6acea0 --- /dev/null +++ b/contrib/kyua/drivers/debug_test.hpp @@ -0,0 +1,79 @@ +// 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 drivers/debug_test.hpp +/// Driver to run a single test in a controlled manner. +/// +/// This driver module implements the logic to execute a particular test +/// with hooks into the runtime procedure. This is to permit debugging the +/// behavior of the test. + +#if !defined(DRIVERS_DEBUG_TEST_HPP) +#define DRIVERS_DEBUG_TEST_HPP + +#include "engine/filters.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace drivers { +namespace debug_test { + + +/// Tuple containing the results of this driver. +class result { +public: + /// A filter matching the executed test case only. + engine::test_filter test_case; + + /// The result of the test case. + model::test_result test_result; + + /// Initializer for the tuple's fields. + /// + /// \param test_case_ The matched test case. + /// \param test_result_ The result of the test case. + result(const engine::test_filter& test_case_, + const model::test_result& test_result_) : + test_case(test_case_), + test_result(test_result_) + { + } +}; + + +result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, + const engine::test_filter&, const utils::config::tree&, + const utils::fs::path&, const utils::fs::path&); + + +} // namespace debug_test +} // namespace drivers + +#endif // !defined(DRIVERS_DEBUG_TEST_HPP) diff --git a/contrib/kyua/drivers/list_tests.cpp b/contrib/kyua/drivers/list_tests.cpp new file mode 100644 index 000000000000..b56706d30b93 --- /dev/null +++ b/contrib/kyua/drivers/list_tests.cpp @@ -0,0 +1,84 @@ +// 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 "drivers/list_tests.hpp" + +#include "engine/exceptions.hpp" +#include "engine/filters.hpp" +#include "engine/kyuafile.hpp" +#include "engine/scanner.hpp" +#include "engine/scheduler.hpp" +#include "model/test_program.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::optional; + + +/// Pure abstract destructor. +drivers::list_tests::base_hooks::~base_hooks(void) +{ +} + + +/// Executes the operation. +/// +/// \param kyuafile_path The path to the Kyuafile to be loaded. +/// \param build_root If not none, path to the built test programs. +/// \param filters The test case filters as provided by the user. +/// \param user_config The end-user configuration properties. +/// \param hooks The hooks for this execution. +/// +/// \returns A structure with all results computed by this driver. +drivers::list_tests::result +drivers::list_tests::drive(const fs::path& kyuafile_path, + const optional< fs::path > build_root, + const std::set< engine::test_filter >& filters, + const config::tree& user_config, + base_hooks& hooks) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + kyuafile_path, build_root, user_config, handle); + + engine::scanner scanner(kyuafile.test_programs(), filters); + + while (!scanner.done()) { + const optional< engine::scan_result > result = scanner.yield(); + INV(result); + hooks.got_test_case(*result.get().first, result.get().second); + } + + handle.cleanup(); + + return result(scanner.unused_filters()); +} diff --git a/contrib/kyua/drivers/list_tests.hpp b/contrib/kyua/drivers/list_tests.hpp new file mode 100644 index 000000000000..6b1257e41b22 --- /dev/null +++ b/contrib/kyua/drivers/list_tests.hpp @@ -0,0 +1,92 @@ +// 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 drivers/list_tests.hpp +/// Driver to obtain a list of test cases out of a test suite. +/// +/// This driver module implements the logic to extract a list of test cases out +/// of a particular test suite. + +#if !defined(DRIVERS_LIST_TESTS_HPP) +#define DRIVERS_LIST_TESTS_HPP + +#include +#include + +#include "engine/filters_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace drivers { +namespace list_tests { + + +/// Abstract definition of the hooks for this driver. +class base_hooks { +public: + virtual ~base_hooks(void) = 0; + + /// Called when a test case is identified in a test suite. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the located test case. + virtual void got_test_case(const model::test_program& test_program, + const std::string& test_case_name) = 0; +}; + + +/// Tuple containing the results of this driver. +class result { +public: + /// Filters that did not match any available test case. + /// + /// The presence of any filters here probably indicates a usage error. If a + /// test filter does not match any test case, it is probably a typo. + std::set< engine::test_filter > unused_filters; + + /// Initializer for the tuple's fields. + /// + /// \param unused_filters_ The filters that did not match any test case. + result(const std::set< engine::test_filter >& unused_filters_) : + unused_filters(unused_filters_) + { + } +}; + + +result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, + const std::set< engine::test_filter >&, + const utils::config::tree&, base_hooks&); + + +} // namespace list_tests +} // namespace drivers + +#endif // !defined(DRIVERS_LIST_TESTS_HPP) diff --git a/contrib/kyua/drivers/list_tests_helpers.cpp b/contrib/kyua/drivers/list_tests_helpers.cpp new file mode 100644 index 000000000000..2b40b1e11db1 --- /dev/null +++ b/contrib/kyua/drivers/list_tests_helpers.cpp @@ -0,0 +1,98 @@ +// 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 + +#include + +#include "utils/test_utils.ipp" + + +ATF_TEST_CASE(config_in_head); +ATF_TEST_CASE_HEAD(config_in_head) +{ + if (has_config_var("the-variable")) { + set_md_var("descr", "the-variable is " + + get_config_var("the-variable")); + } +} +ATF_TEST_CASE_BODY(config_in_head) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(crash_list); +ATF_TEST_CASE_HEAD(crash_list) +{ + utils::abort_without_coredump(); +} +ATF_TEST_CASE_BODY(crash_list) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(no_properties); +ATF_TEST_CASE_BODY(no_properties) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(some_properties); +ATF_TEST_CASE_HEAD(some_properties) +{ + set_md_var("descr", "This is a description"); + set_md_var("require.progs", "non-existent /bin/ls"); +} +ATF_TEST_CASE_BODY(some_properties) +{ + utils::abort_without_coredump(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + std::string enabled; + + const char* tests = std::getenv("TESTS"); + if (tests == NULL) + enabled = "config_in_head crash_list no_properties some_properties"; + else + enabled = tests; + + if (enabled.find("config_in_head") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, config_in_head); + if (enabled.find("crash_list") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, crash_list); + if (enabled.find("no_properties") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, no_properties); + if (enabled.find("some_properties") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, some_properties); +} diff --git a/contrib/kyua/drivers/list_tests_test.cpp b/contrib/kyua/drivers/list_tests_test.cpp new file mode 100644 index 000000000000..752b251052ad --- /dev/null +++ b/contrib/kyua/drivers/list_tests_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 "drivers/list_tests.hpp" + +extern "C" { +#include + +#include +} + +#include +#include +#include + +#include + +#include "cli/cmd_list.hpp" +#include "cli/common.ipp" +#include "engine/atf.hpp" +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "engine/filters.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/test_utils.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Gets the path to the helpers for this test program. +/// +/// \param test_case A pointer to the currently running test case. +/// +/// \return The path to the helpers binary. +static fs::path +helpers(const atf::tests::tc* test_case) +{ + return fs::path(test_case->get_config_var("srcdir")) / + "list_tests_helpers"; +} + + +/// Hooks to capture the incremental listing of test cases. +class capture_hooks : public drivers::list_tests::base_hooks { +public: + /// Set of the listed test cases in a program:test_case form. + std::set< std::string > test_cases; + + /// Set of the listed test cases in a program:test_case form. + std::map< std::string, model::metadata > metadatas; + + /// Called when a test case is identified in a test suite. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the located test case. + virtual void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + const std::string ident = F("%s:%s") % + test_program.relative_path() % test_case_name; + test_cases.insert(ident); + + metadatas.insert(std::map< std::string, model::metadata >::value_type( + ident, test_program.find(test_case_name).get_metadata())); + } +}; + + +/// Creates a mock test suite. +/// +/// \param tc Pointer to the caller test case; needed to obtain the srcdir +/// variable of the caller. +/// \param source_root Basename of the directory that will contain the +/// Kyuafiles. +/// \param build_root Basename of the directory that will contain the test +/// programs. May or may not be the same as source_root. +static void +create_helpers(const atf::tests::tc* tc, const fs::path& source_root, + const fs::path& build_root) +{ + ATF_REQUIRE(::mkdir(source_root.c_str(), 0755) != -1); + ATF_REQUIRE(::mkdir((source_root / "dir").c_str(), 0755) != -1); + if (source_root != build_root) { + ATF_REQUIRE(::mkdir(build_root.c_str(), 0755) != -1); + ATF_REQUIRE(::mkdir((build_root / "dir").c_str(), 0755) != -1); + } + ATF_REQUIRE(::symlink(helpers(tc).c_str(), + (build_root / "dir/program").c_str()) != -1); + + atf::utils::create_file( + (source_root / "Kyuafile").str(), + "syntax(2)\n" + "include('dir/Kyuafile')\n"); + + atf::utils::create_file( + (source_root / "dir/Kyuafile").str(), + "syntax(2)\n" + "atf_test_program{name='program', test_suite='suite-name'}\n"); +} + + +/// Runs the mock test suite. +/// +/// \param source_root Path to the directory that contains the Kyuafiles. +/// \param build_root If not none, path to the directory that contains the test +/// programs. +/// \param hooks The hooks to use during the listing. +/// \param filter_program If not null, the filter on the test program name. +/// \param filter_test_case If not null, the filter on the test case name. +/// \param the_variable If not null, the value to pass to the test program as +/// its "the-variable" configuration property. +/// +/// \return The result data of the driver. +static drivers::list_tests::result +run_helpers(const fs::path& source_root, + const optional< fs::path > build_root, + drivers::list_tests::base_hooks& hooks, + const char* filter_program = NULL, + const char* filter_test_case = NULL, + const char* the_variable = NULL) +{ + std::set< engine::test_filter > filters; + if (filter_program != NULL && filter_test_case != NULL) + filters.insert(engine::test_filter(fs::path(filter_program), + filter_test_case)); + + config::tree user_config = engine::empty_config(); + if (the_variable != NULL) { + user_config.set_string("test_suites.suite-name.the-variable", + the_variable); + } + + return drivers::list_tests::drive(source_root / "Kyuafile", build_root, + filters, user_config, hooks); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(one_test_case); +ATF_TEST_CASE_BODY(one_test_case) +{ + utils::setenv("TESTS", "some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(many_test_cases); +ATF_TEST_CASE_BODY(many_test_cases) +{ + utils::setenv("TESTS", "no_properties some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:no_properties"); + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filter_match); +ATF_TEST_CASE_BODY(filter_match) +{ + utils::setenv("TESTS", "no_properties some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks, "dir/program", + "some_properties"); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root); +ATF_TEST_CASE_BODY(build_root) +{ + utils::setenv("TESTS", "no_properties some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("source"), fs::path("build")); + run_helpers(fs::path("source"), utils::make_optional(fs::path("build")), + hooks); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:no_properties"); + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config_in_head); +ATF_TEST_CASE_BODY(config_in_head) +{ + utils::setenv("TESTS", "config_in_head"); + capture_hooks hooks; + create_helpers(this, fs::path("source"), fs::path("build")); + run_helpers(fs::path("source"), utils::make_optional(fs::path("build")), + hooks, NULL, NULL, "magic value"); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:config_in_head"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); + + const model::metadata& metadata = hooks.metadatas.find( + "dir/program:config_in_head")->second; + ATF_REQUIRE_EQ("the-variable is magic value", metadata.description()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(crash); +ATF_TEST_CASE_BODY(crash) +{ + utils::setenv("TESTS", "crash_list some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks, "dir/program"); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:__test_cases_list__"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + + ATF_ADD_TEST_CASE(tcs, one_test_case); + ATF_ADD_TEST_CASE(tcs, many_test_cases); + ATF_ADD_TEST_CASE(tcs, filter_match); + ATF_ADD_TEST_CASE(tcs, build_root); + ATF_ADD_TEST_CASE(tcs, config_in_head); + ATF_ADD_TEST_CASE(tcs, crash); +} diff --git a/contrib/kyua/drivers/report_junit.cpp b/contrib/kyua/drivers/report_junit.cpp new file mode 100644 index 000000000000..4c14d535675f --- /dev/null +++ b/contrib/kyua/drivers/report_junit.cpp @@ -0,0 +1,258 @@ +// 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 "drivers/report_junit.hpp" + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "model/types.hpp" +#include "store/read_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/text/operations.hpp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace text = utils::text; + + +/// Converts a test program name into a class-like name. +/// +/// \param test_program Test program from which to extract the name. +/// +/// \return A class-like representation of the test program's identifier. +std::string +drivers::junit_classname(const model::test_program& test_program) +{ + std::string classname = test_program.relative_path().str(); + std::replace(classname.begin(), classname.end(), '/', '.'); + return classname; +} + + +/// Converts a test case's duration to a second-based representation. +/// +/// \param delta The duration to convert. +/// +/// \return A second-based with millisecond-precision representation of the +/// input duration. +std::string +drivers::junit_duration(const datetime::delta& delta) +{ + return F("%.3s") % (delta.seconds + (delta.useconds / 1000000.0)); +} + + +/// String to prepend to the formatted test case metadata. +const char* const drivers::junit_metadata_header = + "Test case metadata\n" + "------------------\n" + "\n"; + + +/// String to prepend to the formatted test case timing details. +const char* const drivers::junit_timing_header = + "\n" + "Timing information\n" + "------------------\n" + "\n"; + + +/// String to append to the formatted test case metadata. +const char* const drivers::junit_stderr_header = + "\n" + "Original stderr\n" + "---------------\n" + "\n"; + + +/// Formats a test's metadata for recording in stderr. +/// +/// \param metadata The metadata to format. +/// +/// \return A string with the metadata contents that can be prepended to the +/// original test's stderr. +std::string +drivers::junit_metadata(const model::metadata& metadata) +{ + const model::properties_map props = metadata.to_properties(); + if (props.empty()) + return ""; + + std::ostringstream output; + output << junit_metadata_header; + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + if ((*iter).second.empty()) { + output << F("%s is empty\n") % (*iter).first; + } else { + output << F("%s = %s\n") % (*iter).first % (*iter).second; + } + } + return output.str(); +} + + +/// Formats a test's timing information for recording in stderr. +/// +/// \param start_time The start time of the test. +/// \param end_time The end time of the test. +/// +/// \return A string with the timing information that can be prepended to the +/// original test's stderr. +std::string +drivers::junit_timing(const datetime::timestamp& start_time, + const datetime::timestamp& end_time) +{ + std::ostringstream output; + output << junit_timing_header; + output << F("Start time: %s\n") % start_time.to_iso8601_in_utc(); + output << F("End time: %s\n") % end_time.to_iso8601_in_utc(); + output << F("Duration: %ss\n") % junit_duration(end_time - start_time); + return output.str(); +} + + +/// Constructor for the hooks. +/// +/// \param [out] output_ Stream to which to write the report. +drivers::report_junit_hooks::report_junit_hooks(std::ostream& output_) : + _output(output_) +{ +} + + +/// Callback executed when the context is loaded. +/// +/// \param context The context loaded from the database. +void +drivers::report_junit_hooks::got_context(const model::context& context) +{ + _output << "\n"; + _output << "\n"; + + _output << "\n"; + _output << F("\n") + % text::escape_xml(context.cwd().str()); + for (model::properties_map::const_iterator iter = + context.env().begin(); iter != context.env().end(); ++iter) { + _output << F("\n") + % text::escape_xml((*iter).first) + % text::escape_xml((*iter).second); + } + _output << "\n"; +} + + +/// Callback executed when a test results is found. +/// +/// \param iter Container for the test result's data. +void +drivers::report_junit_hooks::got_result(store::results_iterator& iter) +{ + const model::test_result result = iter.result(); + + _output << F("\n") + % text::escape_xml(junit_classname(*iter.test_program())) + % text::escape_xml(iter.test_case_name()) + % junit_duration(iter.end_time() - iter.start_time()); + + std::string stderr_contents; + + switch (result.type()) { + case model::test_result_failed: + _output << F("\n") + % text::escape_xml(result.reason()); + break; + + case model::test_result_expected_failure: + stderr_contents += ("Expected failure result details\n" + "-------------------------------\n" + "\n" + + result.reason() + "\n" + "\n"); + break; + + case model::test_result_passed: + // Passed results have no status nodes. + break; + + case model::test_result_skipped: + _output << "\n"; + stderr_contents += ("Skipped result details\n" + "----------------------\n" + "\n" + + result.reason() + "\n" + "\n"); + break; + + default: + _output << F("\n") + % text::escape_xml(result.reason()); + } + + const std::string stdout_contents = iter.stdout_contents(); + if (!stdout_contents.empty()) { + _output << F("%s\n") + % text::escape_xml(stdout_contents); + } + + { + const model::test_case& test_case = iter.test_program()->find( + iter.test_case_name()); + stderr_contents += junit_metadata(test_case.get_metadata()); + } + stderr_contents += junit_timing(iter.start_time(), iter.end_time()); + { + stderr_contents += junit_stderr_header; + const std::string real_stderr_contents = iter.stderr_contents(); + if (real_stderr_contents.empty()) { + stderr_contents += "\n"; + } else { + stderr_contents += real_stderr_contents; + } + } + _output << "" << text::escape_xml(stderr_contents) + << "\n"; + + _output << "\n"; +} + + +/// Finalizes the report. +void +drivers::report_junit_hooks::end(const drivers::scan_results::result& /* r */) +{ + _output << "\n"; +} diff --git a/contrib/kyua/drivers/report_junit.hpp b/contrib/kyua/drivers/report_junit.hpp new file mode 100644 index 000000000000..adb0aa12757e --- /dev/null +++ b/contrib/kyua/drivers/report_junit.hpp @@ -0,0 +1,75 @@ +// 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 drivers/report_junit.hpp +/// Generates a JUnit report out of a test suite execution. + +#if !defined(ENGINE_REPORT_JUNIT_HPP) +#define ENGINE_REPORT_JUNIT_HPP + +#include +#include + +#include "drivers/scan_results.hpp" +#include "model/metadata_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/datetime_fwd.hpp" + +namespace drivers { + + +extern const char* const junit_metadata_header; +extern const char* const junit_timing_header; +extern const char* const junit_stderr_header; + + +std::string junit_classname(const model::test_program&); +std::string junit_duration(const utils::datetime::delta&); +std::string junit_metadata(const model::metadata&); +std::string junit_timing(const utils::datetime::timestamp&, + const utils::datetime::timestamp&); + + +/// Hooks for the scan_results driver to generate a JUnit report. +class report_junit_hooks : public drivers::scan_results::base_hooks { + /// Stream to which to write the report. + std::ostream& _output; + +public: + report_junit_hooks(std::ostream&); + + void got_context(const model::context&); + void got_result(store::results_iterator&); + + void end(const drivers::scan_results::result&); +}; + + +} // namespace drivers + +#endif // !defined(ENGINE_REPORT_JUNIT_HPP) diff --git a/contrib/kyua/drivers/report_junit_test.cpp b/contrib/kyua/drivers/report_junit_test.cpp new file mode 100644 index 000000000000..462dca72f9be --- /dev/null +++ b/contrib/kyua/drivers/report_junit_test.cpp @@ -0,0 +1,415 @@ +// 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 "drivers/report_junit.hpp" + +#include +#include + +#include + +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace units = utils::units; + +using utils::none; + + +namespace { + + +/// Formatted metadata for a test case with defaults. +static const char* const default_metadata = + "allowed_architectures is empty\n" + "allowed_platforms is empty\n" + "description is empty\n" + "has_cleanup = false\n" + "is_exclusive = false\n" + "required_configs is empty\n" + "required_disk_space = 0\n" + "required_files is empty\n" + "required_memory = 0\n" + "required_programs is empty\n" + "required_user is empty\n" + "timeout = 300\n"; + + +/// Formatted metadata for a test case constructed with the "with_metadata" flag +/// set to true in add_tests. +static const char* const overriden_metadata = + "allowed_architectures is empty\n" + "allowed_platforms is empty\n" + "description = Textual description\n" + "has_cleanup = false\n" + "is_exclusive = false\n" + "required_configs is empty\n" + "required_disk_space = 0\n" + "required_files is empty\n" + "required_memory = 0\n" + "required_programs is empty\n" + "required_user is empty\n" + "timeout = 5678\n"; + + +/// Populates the context of the given database. +/// +/// \param tx Transaction to use for the writes to the database. +/// \param env_vars Number of environment variables to add to the context. +static void +add_context(store::write_transaction& tx, const std::size_t env_vars) +{ + std::map< std::string, std::string > env; + for (std::size_t i = 0; i < env_vars; i++) + env[F("VAR%s") % i] = F("Value %s") % i; + const model::context context(fs::path("/root"), env); + (void)tx.put_context(context); +} + + +/// Adds a new test program with various test cases to the given database. +/// +/// \param tx Transaction to use for the writes to the database. +/// \param prog Test program name. +/// \param results Collection of results for the added test cases. The size of +/// this vector indicates the number of tests in the test program. +/// \param with_metadata Whether to add metadata overrides to the test cases. +/// \param with_output Whether to add stdout/stderr messages to the test cases. +static void +add_tests(store::write_transaction& tx, + const char* prog, + const std::vector< model::test_result >& results, + const bool with_metadata, const bool with_output) +{ + model::test_program_builder test_program_builder( + "plain", fs::path(prog), fs::path("/root"), "suite"); + + for (std::size_t j = 0; j < results.size(); j++) { + model::metadata_builder builder; + if (with_metadata) { + builder.set_description("Textual description"); + builder.set_timeout(datetime::delta(5678, 0)); + } + test_program_builder.add_test_case(F("t%s") % j, builder.build()); + } + + const model::test_program test_program = test_program_builder.build(); + const int64_t tp_id = tx.put_test_program(test_program); + + for (std::size_t j = 0; j < results.size(); j++) { + const int64_t tc_id = tx.put_test_case(test_program, F("t%s") % j, + tp_id); + const datetime::timestamp start = + datetime::timestamp::from_microseconds(0); + const datetime::timestamp end = + datetime::timestamp::from_microseconds(j * 1000000 + 500000); + tx.put_result(results[j], tc_id, start, end); + + if (with_output) { + atf::utils::create_file("fake-out", F("stdout file %s") % j); + tx.put_test_case_file("__STDOUT__", fs::path("fake-out"), tc_id); + atf::utils::create_file("fake-err", F("stderr file %s") % j); + tx.put_test_case_file("__STDERR__", fs::path("fake-err"), tc_id); + } + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_classname); +ATF_TEST_CASE_BODY(junit_classname) +{ + const model::test_program test_program = model::test_program_builder( + "plain", fs::path("dir1/dir2/program"), fs::path("/root"), "suite") + .build(); + + ATF_REQUIRE_EQ("dir1.dir2.program", drivers::junit_classname(test_program)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_duration); +ATF_TEST_CASE_BODY(junit_duration) +{ + ATF_REQUIRE_EQ("0.457", + drivers::junit_duration(datetime::delta(0, 456700))); + ATF_REQUIRE_EQ("3.120", + drivers::junit_duration(datetime::delta(3, 120000))); + ATF_REQUIRE_EQ("5.000", drivers::junit_duration(datetime::delta(5, 0))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_metadata__defaults); +ATF_TEST_CASE_BODY(junit_metadata__defaults) +{ + const model::metadata metadata = model::metadata_builder().build(); + + const std::string expected = std::string() + + drivers::junit_metadata_header + + default_metadata; + + ATF_REQUIRE_EQ(expected, drivers::junit_metadata(metadata)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_metadata__overrides); +ATF_TEST_CASE_BODY(junit_metadata__overrides) +{ + const model::metadata metadata = model::metadata_builder() + .add_allowed_architecture("arch1") + .add_allowed_platform("platform1") + .set_description("This is a test") + .set_has_cleanup(true) + .set_is_exclusive(true) + .add_required_config("config1") + .set_required_disk_space(units::bytes(456)) + .add_required_file(fs::path("file1")) + .set_required_memory(units::bytes(123)) + .add_required_program(fs::path("prog1")) + .set_required_user("root") + .set_timeout(datetime::delta(10, 0)) + .build(); + + const std::string expected = std::string() + + drivers::junit_metadata_header + + "allowed_architectures = arch1\n" + + "allowed_platforms = platform1\n" + + "description = This is a test\n" + + "has_cleanup = true\n" + + "is_exclusive = true\n" + + "required_configs = config1\n" + + "required_disk_space = 456\n" + + "required_files = file1\n" + + "required_memory = 123\n" + + "required_programs = prog1\n" + + "required_user = root\n" + + "timeout = 10\n"; + + ATF_REQUIRE_EQ(expected, drivers::junit_metadata(metadata)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_timing); +ATF_TEST_CASE_BODY(junit_timing) +{ + const std::string expected = std::string() + + drivers::junit_timing_header + + "Start time: 2015-06-12T01:02:35.123456Z\n" + "End time: 2016-07-13T18:47:10.000001Z\n" + "Duration: 34364674.877s\n"; + + const datetime::timestamp start_time = + datetime::timestamp::from_values(2015, 6, 12, 1, 2, 35, 123456); + const datetime::timestamp end_time = + datetime::timestamp::from_values(2016, 7, 13, 18, 47, 10, 1); + + ATF_REQUIRE_EQ(expected, drivers::junit_timing(start_time, end_time)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_junit_hooks__minimal); +ATF_TEST_CASE_BODY(report_junit_hooks__minimal) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + add_context(tx, 0); + tx.commit(); + backend.close(); + + std::ostringstream output; + + drivers::report_junit_hooks hooks(output); + drivers::scan_results::drive(fs::path("test.db"), + std::set< engine::test_filter >(), + hooks); + + const char* expected = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n"; + ATF_REQUIRE_EQ(expected, output.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_junit_hooks__some_tests); +ATF_TEST_CASE_BODY(report_junit_hooks__some_tests) +{ + std::vector< model::test_result > results1; + results1.push_back(model::test_result( + model::test_result_broken, "Broken")); + results1.push_back(model::test_result( + model::test_result_expected_failure, "XFail")); + results1.push_back(model::test_result( + model::test_result_failed, "Failed")); + std::vector< model::test_result > results2; + results2.push_back(model::test_result( + model::test_result_passed)); + results2.push_back(model::test_result( + model::test_result_skipped, "Skipped")); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + add_context(tx, 2); + add_tests(tx, "dir/prog-1", results1, false, false); + add_tests(tx, "dir/sub/prog-2", results2, true, true); + tx.commit(); + backend.close(); + + std::ostringstream output; + + drivers::report_junit_hooks hooks(output); + drivers::scan_results::drive(fs::path("test.db"), + std::set< engine::test_filter >(), + hooks); + + const std::string expected = std::string() + + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + + "\n" + "\n" + "" + + drivers::junit_metadata_header + + default_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:00.500000Z\n" + "Duration: 0.500s\n" + + drivers::junit_stderr_header + + "<EMPTY>\n" + "\n" + "\n" + + "\n" + "" + "Expected failure result details\n" + "-------------------------------\n" + "\n" + "XFail\n" + "\n" + + drivers::junit_metadata_header + + default_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:01.500000Z\n" + "Duration: 1.500s\n" + + drivers::junit_stderr_header + + "<EMPTY>\n" + "\n" + "\n" + + "\n" + "\n" + "" + + drivers::junit_metadata_header + + default_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:02.500000Z\n" + "Duration: 2.500s\n" + + drivers::junit_stderr_header + + "<EMPTY>\n" + "\n" + "\n" + + "\n" + "stdout file 0\n" + "" + + drivers::junit_metadata_header + + overriden_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:00.500000Z\n" + "Duration: 0.500s\n" + + drivers::junit_stderr_header + + "stderr file 0\n" + "\n" + + "\n" + "\n" + "stdout file 1\n" + "" + "Skipped result details\n" + "----------------------\n" + "\n" + "Skipped\n" + "\n" + + drivers::junit_metadata_header + + overriden_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:01.500000Z\n" + "Duration: 1.500s\n" + + drivers::junit_stderr_header + + "stderr file 1\n" + "\n" + + "\n"; + ATF_REQUIRE_EQ(expected, output.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, junit_classname); + + ATF_ADD_TEST_CASE(tcs, junit_duration); + + ATF_ADD_TEST_CASE(tcs, junit_metadata__defaults); + ATF_ADD_TEST_CASE(tcs, junit_metadata__overrides); + + ATF_ADD_TEST_CASE(tcs, junit_timing); + + ATF_ADD_TEST_CASE(tcs, report_junit_hooks__minimal); + ATF_ADD_TEST_CASE(tcs, report_junit_hooks__some_tests); +} diff --git a/contrib/kyua/drivers/run_tests.cpp b/contrib/kyua/drivers/run_tests.cpp new file mode 100644 index 000000000000..d92940005242 --- /dev/null +++ b/contrib/kyua/drivers/run_tests.cpp @@ -0,0 +1,344 @@ +// 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 "drivers/run_tests.hpp" + +#include + +#include "engine/config.hpp" +#include "engine/filters.hpp" +#include "engine/kyuafile.hpp" +#include "engine/scanner.hpp" +#include "engine/scheduler.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace scheduler = engine::scheduler; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Map of test program identifiers (relative paths) to their identifiers in the +/// database. We need to keep this in memory because test programs can be +/// returned by the scanner in any order, and we only want to put each test +/// program once. +typedef std::map< fs::path, int64_t > path_to_id_map; + + +/// Map of in-flight PIDs to their corresponding test case IDs. +typedef std::map< int, int64_t > pid_to_id_map; + + +/// Pair of PID to a test case ID. +typedef pid_to_id_map::value_type pid_and_id_pair; + + +/// Puts a test program in the store and returns its identifier. +/// +/// This function is idempotent: we maintain a side cache of already-put test +/// programs so that we can return their identifiers without having to put them +/// again. +/// TODO(jmmv): It's possible that the store module should offer this +/// functionality and not have to do this ourselves here. +/// +/// \param test_program The test program being put. +/// \param [in,out] tx Writable transaction on the store. +/// \param [in,out] ids_cache Cache of already-put test programs. +/// +/// \return A test program identifier. +static int64_t +find_test_program_id(const model::test_program_ptr test_program, + store::write_transaction& tx, + path_to_id_map& ids_cache) +{ + const fs::path& key = test_program->relative_path(); + std::map< fs::path, int64_t >::const_iterator iter = ids_cache.find(key); + if (iter == ids_cache.end()) { + const int64_t id = tx.put_test_program(*test_program); + ids_cache.insert(std::make_pair(key, id)); + return id; + } else { + return (*iter).second; + } +} + + +/// Stores the result of an execution in the database. +/// +/// \param test_case_id Identifier of the test case in the database. +/// \param result The result of the execution. +/// \param [in,out] tx Writable transaction where to store the result data. +static void +put_test_result(const int64_t test_case_id, + const scheduler::test_result_handle& result, + store::write_transaction& tx) +{ + tx.put_result(result.test_result(), test_case_id, + result.start_time(), result.end_time()); + tx.put_test_case_file("__STDOUT__", result.stdout_file(), test_case_id); + tx.put_test_case_file("__STDERR__", result.stderr_file(), test_case_id); + +} + + +/// Cleans up a test case and folds any errors into the test result. +/// +/// \param handle The result handle for the test. +/// +/// \return The test result if the cleanup succeeds; a broken test result +/// otherwise. +model::test_result +safe_cleanup(scheduler::test_result_handle handle) throw() +{ + try { + handle.cleanup(); + return handle.test_result(); + } catch (const std::exception& e) { + return model::test_result( + model::test_result_broken, + F("Failed to clean up test case's work directory %s: %s") % + handle.work_directory() % e.what()); + } +} + + +/// Starts a test asynchronously. +/// +/// \param handle Scheduler handle. +/// \param match Test program and test case to start. +/// \param [in,out] tx Writable transaction to obtain test IDs. +/// \param [in,out] ids_cache Cache of already-put test cases. +/// \param user_config The end-user configuration properties. +/// \param hooks The hooks for this execution. +/// +/// \returns The PID for the started test and the test case's identifier in the +/// store. +pid_and_id_pair +start_test(scheduler::scheduler_handle& handle, + const engine::scan_result& match, + store::write_transaction& tx, + path_to_id_map& ids_cache, + const config::tree& user_config, + drivers::run_tests::base_hooks& hooks) +{ + const model::test_program_ptr test_program = match.first; + const std::string& test_case_name = match.second; + + hooks.got_test_case(*test_program, test_case_name); + + const int64_t test_program_id = find_test_program_id( + test_program, tx, ids_cache); + const int64_t test_case_id = tx.put_test_case( + *test_program, test_case_name, test_program_id); + + const scheduler::exec_handle exec_handle = handle.spawn_test( + test_program, test_case_name, user_config); + return std::make_pair(exec_handle, test_case_id); +} + + +/// Processes the completion of a test. +/// +/// \param [in,out] result_handle The completion handle of the test subprocess. +/// \param test_case_id Identifier of the test case as returned by start_test(). +/// \param [in,out] tx Writable transaction to put the test results. +/// \param hooks The hooks for this execution. +/// +/// \post result_handle is cleaned up. The caller cannot clean it up again. +void +finish_test(scheduler::result_handle_ptr result_handle, + const int64_t test_case_id, + store::write_transaction& tx, + drivers::run_tests::base_hooks& hooks) +{ + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + put_test_result(test_case_id, *test_result_handle, tx); + + const model::test_result test_result = safe_cleanup(*test_result_handle); + hooks.got_result( + *test_result_handle->test_program(), + test_result_handle->test_case_name(), + test_result_handle->test_result(), + result_handle->end_time() - result_handle->start_time()); +} + + +/// Extracts the keys of a pid_to_id_map and returns them as a string. +/// +/// \param map The PID to test ID map from which to get the PIDs. +/// +/// \return A user-facing string with the collection of PIDs. +static std::string +format_pids(const pid_to_id_map& map) +{ + std::set< pid_to_id_map::key_type > pids; + for (pid_to_id_map::const_iterator iter = map.begin(); iter != map.end(); + ++iter) { + pids.insert(iter->first); + } + return text::join(pids, ","); +} + + +} // anonymous namespace + + +/// Pure abstract destructor. +drivers::run_tests::base_hooks::~base_hooks(void) +{ +} + + +/// Executes the operation. +/// +/// \param kyuafile_path The path to the Kyuafile to be loaded. +/// \param build_root If not none, path to the built test programs. +/// \param store_path The path to the store to be used. +/// \param filters The test case filters as provided by the user. +/// \param user_config The end-user configuration properties. +/// \param hooks The hooks for this execution. +/// +/// \returns A structure with all results computed by this driver. +drivers::run_tests::result +drivers::run_tests::drive(const fs::path& kyuafile_path, + const optional< fs::path > build_root, + const fs::path& store_path, + const std::set< engine::test_filter >& filters, + const config::tree& user_config, + base_hooks& hooks) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + kyuafile_path, build_root, user_config, handle); + store::write_backend db = store::write_backend::open_rw(store_path); + store::write_transaction tx = db.start_write(); + + { + const model::context context = scheduler::current_context(); + (void)tx.put_context(context); + } + + engine::scanner scanner(kyuafile.test_programs(), filters); + + path_to_id_map ids_cache; + pid_to_id_map in_flight; + std::vector< engine::scan_result > exclusive_tests; + + const std::size_t slots = user_config.lookup< config::positive_int_node >( + "parallelism"); + INV(slots >= 1); + do { + INV(in_flight.size() <= slots); + + // Spawn as many jobs as needed to fill our execution slots. We do this + // first with the assumption that the spawning is faster than any single + // job, so we want to keep as many jobs in the background as possible. + while (in_flight.size() < slots) { + optional< engine::scan_result > match = scanner.yield(); + if (!match) + break; + const model::test_program_ptr test_program = match.get().first; + const std::string& test_case_name = match.get().second; + + const model::test_case& test_case = test_program->find( + test_case_name); + if (test_case.get_metadata().is_exclusive()) { + // Exclusive tests get processed later, separately. + exclusive_tests.push_back(match.get()); + continue; + } + + const pid_and_id_pair pid_id = start_test( + handle, match.get(), tx, ids_cache, user_config, hooks); + INV_MSG(in_flight.find(pid_id.first) == in_flight.end(), + F("Spawned test has PID of still-tracked process %s") % + pid_id.first); + in_flight.insert(pid_id); + } + + // If there are any used slots, consume any at random and return the + // result. We consume slots one at a time to give preference to the + // spawning of new tests as detailed above. + if (!in_flight.empty()) { + scheduler::result_handle_ptr result_handle = handle.wait_any(); + + const pid_to_id_map::iterator iter = in_flight.find( + result_handle->original_pid()); + INV_MSG(iter != in_flight.end(), + F("Lost track of in-flight PID %s; tracking %s") % + result_handle->original_pid() % format_pids(in_flight)); + const int64_t test_case_id = (*iter).second; + in_flight.erase(iter); + + finish_test(result_handle, test_case_id, tx, hooks); + } + } while (!in_flight.empty() || !scanner.done()); + + // Run any exclusive tests that we spotted earlier sequentially. + for (std::vector< engine::scan_result >::const_iterator + iter = exclusive_tests.begin(); iter != exclusive_tests.end(); + ++iter) { + const pid_and_id_pair data = start_test( + handle, *iter, tx, ids_cache, user_config, hooks); + scheduler::result_handle_ptr result_handle = handle.wait_any(); + finish_test(result_handle, data.second, tx, hooks); + } + + tx.commit(); + + handle.cleanup(); + + return result(scanner.unused_filters()); +} diff --git a/contrib/kyua/drivers/run_tests.hpp b/contrib/kyua/drivers/run_tests.hpp new file mode 100644 index 000000000000..7f09953d4e03 --- /dev/null +++ b/contrib/kyua/drivers/run_tests.hpp @@ -0,0 +1,106 @@ +// 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 drivers/run_tests.hpp +/// Driver to run a collection of tests. +/// +/// This driver module implements the logic to execute a collection of tests. +/// The presentation layer is able to monitor progress by hooking into +/// particular points of the driver. + +#if !defined(DRIVERS_RUN_TESTS_HPP) +#define DRIVERS_RUN_TESTS_HPP + +#include +#include + +#include "engine/filters.hpp" +#include "model/test_program.hpp" +#include "model/test_result_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace drivers { +namespace run_tests { + + +/// Abstract definition of the hooks for this driver. +class base_hooks { +public: + virtual ~base_hooks(void) = 0; + + /// Called when the processing of a test case begins. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + virtual void got_test_case(const model::test_program& test_program, + const std::string& test_case_name) = 0; + + /// Called when a result of a test case becomes available. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the executed test case. + /// \param result The result of the execution of the test case. + /// \param duration The time it took to run the test. + virtual void got_result(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const utils::datetime::delta& duration) = 0; +}; + + +/// Tuple containing the results of this driver. +class result { +public: + /// Filters that did not match any available test case. + /// + /// The presence of any filters here probably indicates a usage error. If a + /// test filter does not match any test case, it is probably a typo. + std::set< engine::test_filter > unused_filters; + + /// Initializer for the tuple's fields. + /// + /// \param unused_filters_ The filters that did not match any test case. + result(const std::set< engine::test_filter >& unused_filters_) : + unused_filters(unused_filters_) + { + } +}; + + +result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, + const utils::fs::path&, const std::set< engine::test_filter >&, + const utils::config::tree&, base_hooks&); + + +} // namespace run_tests +} // namespace drivers + +#endif // !defined(DRIVERS_RUN_TESTS_HPP) diff --git a/contrib/kyua/drivers/scan_results.cpp b/contrib/kyua/drivers/scan_results.cpp new file mode 100644 index 000000000000..e013cd1fb314 --- /dev/null +++ b/contrib/kyua/drivers/scan_results.cpp @@ -0,0 +1,107 @@ +// 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 "drivers/scan_results.hpp" + +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "store/read_backend.hpp" +#include "store/read_transaction.hpp" +#include "utils/defs.hpp" + +namespace fs = utils::fs; + + +/// Pure abstract destructor. +drivers::scan_results::base_hooks::~base_hooks(void) +{ +} + + +/// Callback executed before any operation is performed. +void +drivers::scan_results::base_hooks::begin(void) +{ +} + + +/// Callback executed after all operations are performed. +void +drivers::scan_results::base_hooks::end(const result& /* r */) +{ +} + + +/// Executes the operation. +/// +/// \param store_path The path to the database store. +/// \param raw_filters The test case filters as provided by the user. +/// \param hooks The hooks for this execution. +/// +/// \returns A structure with all results computed by this driver. +drivers::scan_results::result +drivers::scan_results::drive(const fs::path& store_path, + const std::set< engine::test_filter >& raw_filters, + base_hooks& hooks) +{ + engine::filters_state filters(raw_filters); + + store::read_backend db = store::read_backend::open_ro(store_path); + store::read_transaction tx = db.start_read(); + + hooks.begin(); + + const model::context context = tx.get_context(); + hooks.got_context(context); + + store::results_iterator iter = tx.get_results(); + while (iter) { + // TODO(jmmv): We should be filtering at the test case level for + // efficiency, but that means we would need to execute more than one + // query on the database and our current interfaces don't support that. + // + // Reuse engine::filters_state for the time being because it is simpler + // and we get tracking of unmatched filters "for free". + const model::test_program_ptr test_program = iter.test_program(); + if (filters.match_test_program(test_program->relative_path())) { + const model::test_case& test_case = test_program->find( + iter.test_case_name()); + if (filters.match_test_case(test_program->relative_path(), + test_case.name())) { + hooks.got_result(iter); + } + } + ++iter; + } + + result r(filters.unused()); + hooks.end(r); + return r; +} diff --git a/contrib/kyua/drivers/scan_results.hpp b/contrib/kyua/drivers/scan_results.hpp new file mode 100644 index 000000000000..ddf099ae3565 --- /dev/null +++ b/contrib/kyua/drivers/scan_results.hpp @@ -0,0 +1,105 @@ +// 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 drivers/scan_results.hpp +/// Driver to scan the contents of a results file. +/// +/// This driver module implements the logic to scan the contents of a results +/// file and to notify the presentation layer as soon as data becomes +/// available. This is to prevent reading all the data from the file at once, +/// which could take too much memory. + +#if !defined(DRIVERS_SCAN_RESULTS_HPP) +#define DRIVERS_SCAN_RESULTS_HPP + +extern "C" { +#include +} + +#include + +#include "engine/filters.hpp" +#include "model/context_fwd.hpp" +#include "store/read_transaction_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace drivers { +namespace scan_results { + + +/// Tuple containing the results of this driver. +class result { +public: + /// Filters that did not match any available test case. + /// + /// The presence of any filters here probably indicates a usage error. If a + /// test filter does not match any test case, it is probably a typo. + std::set< engine::test_filter > unused_filters; + + /// Initializer for the tuple's fields. + /// + /// \param unused_filters_ The filters that did not match any test case. + result(const std::set< engine::test_filter >& unused_filters_) : + unused_filters(unused_filters_) + { + } +}; + + +/// Abstract definition of the hooks for this driver. +class base_hooks { +public: + virtual ~base_hooks(void) = 0; + + virtual void begin(void); + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + virtual void got_context(const model::context& context) = 0; + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. Some of the data are + /// lazily fetched, hence why we receive the object instead of the + /// individual elements. + virtual void got_result(store::results_iterator& iter) = 0; + + virtual void end(const result& r); +}; + + +result drive(const utils::fs::path&, const std::set< engine::test_filter >&, + base_hooks&); + + +} // namespace scan_results +} // namespace drivers + +#endif // !defined(DRIVERS_SCAN_RESULTS_HPP) diff --git a/contrib/kyua/drivers/scan_results_test.cpp b/contrib/kyua/drivers/scan_results_test.cpp new file mode 100644 index 000000000000..5519cb670d85 --- /dev/null +++ b/contrib/kyua/drivers/scan_results_test.cpp @@ -0,0 +1,258 @@ +// 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 "drivers/scan_results.hpp" + +#include + +#include + +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Records the callback values for futher investigation. +class capture_hooks : public drivers::scan_results::base_hooks { +public: + /// Whether begin() was called or not. + bool _begin_called; + + /// The captured driver result, if any. + optional< drivers::scan_results::result > _end_result; + + /// The captured context, if any. + optional< model::context > _context; + + /// The captured results, flattened as "program:test_case:result". + std::set< std::string > _results; + + /// Constructor. + capture_hooks(void) : + _begin_called(false) + { + } + + /// Callback executed before any operation is performed. + void + begin(void) + { + _begin_called = true; + } + + /// Callback executed after all operations are performed. + /// + /// \param r A structure with all results computed by this driver. Note + /// that this is also returned by the drive operation. + void + end(const drivers::scan_results::result& r) + { + PRE(!_end_result); + _end_result = r; + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void got_context(const model::context& context) + { + PRE(!_context); + _context = context; + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void got_result(store::results_iterator& iter) + { + const char* type; + switch (iter.result().type()) { + case model::test_result_passed: type = "passed"; break; + case model::test_result_skipped: type = "skipped"; break; + default: + UNREACHABLE_MSG("Formatting unimplemented"); + } + const datetime::delta duration = iter.end_time() - iter.start_time(); + _results.insert(F("%s:%s:%s:%s:%s:%s") % + iter.test_program()->absolute_path() % + iter.test_case_name() % type % iter.result().reason() % + duration.seconds % duration.useconds); + } +}; + + +/// Populates a results file. +/// +/// It is not OK to call this function multiple times on the same file. +/// +/// \param db_name The database to update. +/// \param count The number of "elements" to insert into the results file. +/// Determines the number of test programs and the number of test cases +/// each has. Can be used to determine from the caller which particular +/// results file has been loaded. +static void +populate_results_file(const char* db_name, const int count) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path(db_name)); + + store::write_transaction tx = backend.start_write(); + + std::map< std::string, std::string > env; + for (int i = 0; i < count; i++) + env[F("VAR%s") % i] = F("Value %s") % i; + const model::context context(fs::path("/root"), env); + tx.put_context(context); + + for (int i = 0; i < count; i++) { + model::test_program_builder test_program_builder( + "fake", fs::path(F("dir/prog_%s") % i), fs::path("/root"), + F("suite_%s") % i); + for (int j = 0; j < count; j++) { + test_program_builder.add_test_case(F("case_%s") % j); + } + const model::test_program test_program = test_program_builder.build(); + const int64_t tp_id = tx.put_test_program(test_program); + + for (int j = 0; j < count; j++) { + const model::test_result result(model::test_result_skipped, + F("Count %s") % j); + const int64_t tc_id = tx.put_test_case(test_program, + F("case_%s") % j, tp_id); + const datetime::timestamp start = + datetime::timestamp::from_microseconds(1000010); + const datetime::timestamp end = + datetime::timestamp::from_microseconds(5000020 + i + j); + tx.put_result(result, tc_id, start, end); + } + } + + tx.commit(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(ok__all); +ATF_TEST_CASE_BODY(ok__all) +{ + populate_results_file("test.db", 2); + + capture_hooks hooks; + const drivers::scan_results::result result = drivers::scan_results::drive( + fs::path("test.db"), std::set< engine::test_filter >(), hooks); + ATF_REQUIRE(result.unused_filters.empty()); + ATF_REQUIRE(hooks._begin_called); + ATF_REQUIRE(hooks._end_result); + + std::map< std::string, std::string > env; + env["VAR0"] = "Value 0"; + env["VAR1"] = "Value 1"; + const model::context context(fs::path("/root"), env); + ATF_REQUIRE(context == hooks._context.get()); + + std::set< std::string > results; + results.insert("/root/dir/prog_0:case_0:skipped:Count 0:4:10"); + results.insert("/root/dir/prog_0:case_1:skipped:Count 1:4:11"); + results.insert("/root/dir/prog_1:case_0:skipped:Count 0:4:11"); + results.insert("/root/dir/prog_1:case_1:skipped:Count 1:4:12"); + ATF_REQUIRE_EQ(results, hooks._results); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ok__filters); +ATF_TEST_CASE_BODY(ok__filters) +{ + populate_results_file("test.db", 3); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("dir/prog_1"), "")); + filters.insert(engine::test_filter(fs::path("dir/prog_1"), "no")); + filters.insert(engine::test_filter(fs::path("dir/prog_2"), "case_1")); + filters.insert(engine::test_filter(fs::path("dir/prog_3"), "")); + + capture_hooks hooks; + const drivers::scan_results::result result = drivers::scan_results::drive( + fs::path("test.db"), filters, hooks); + ATF_REQUIRE(hooks._begin_called); + ATF_REQUIRE(hooks._end_result); + + std::set< engine::test_filter > unused_filters; + unused_filters.insert(engine::test_filter(fs::path("dir/prog_1"), "no")); + unused_filters.insert(engine::test_filter(fs::path("dir/prog_3"), "")); + ATF_REQUIRE_EQ(unused_filters, result.unused_filters); + + std::set< std::string > results; + results.insert("/root/dir/prog_1:case_0:skipped:Count 0:4:11"); + results.insert("/root/dir/prog_1:case_1:skipped:Count 1:4:12"); + results.insert("/root/dir/prog_1:case_2:skipped:Count 2:4:13"); + results.insert("/root/dir/prog_2:case_1:skipped:Count 1:4:13"); + ATF_REQUIRE_EQ(results, hooks._results); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_db); +ATF_TEST_CASE_BODY(missing_db) +{ + capture_hooks hooks; + ATF_REQUIRE_THROW( + store::error, + drivers::scan_results::drive(fs::path("test.db"), + std::set< engine::test_filter >(), + hooks)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ok__all); + ATF_ADD_TEST_CASE(tcs, ok__filters); + ATF_ADD_TEST_CASE(tcs, missing_db); +} diff --git a/contrib/kyua/engine/Kyuafile b/contrib/kyua/engine/Kyuafile new file mode 100644 index 000000000000..1baa63bc9118 --- /dev/null +++ b/contrib/kyua/engine/Kyuafile @@ -0,0 +1,17 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="atf_test"} +atf_test_program{name="atf_list_test"} +atf_test_program{name="atf_result_test"} +atf_test_program{name="config_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="filters_test"} +atf_test_program{name="kyuafile_test"} +atf_test_program{name="plain_test"} +atf_test_program{name="requirements_test"} +atf_test_program{name="scanner_test"} +atf_test_program{name="tap_test"} +atf_test_program{name="tap_parser_test"} +atf_test_program{name="scheduler_test"} diff --git a/contrib/kyua/engine/Makefile.am.inc b/contrib/kyua/engine/Makefile.am.inc new file mode 100644 index 000000000000..baa7fe0bb8a0 --- /dev/null +++ b/contrib/kyua/engine/Makefile.am.inc @@ -0,0 +1,155 @@ +# 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. + +ENGINE_CFLAGS = $(STORE_CFLAGS) $(MODEL_CFLAGS) $(UTILS_CFLAGS) +ENGINE_LIBS = libengine.a $(STORE_LIBS) $(MODEL_LIBS) $(UTILS_LIBS) + +noinst_LIBRARIES += libengine.a +libengine_a_CPPFLAGS = $(STORE_CFLAGS) $(UTILS_CFLAGS) +libengine_a_SOURCES = engine/atf.cpp +libengine_a_SOURCES += engine/atf.hpp +libengine_a_SOURCES += engine/atf_list.cpp +libengine_a_SOURCES += engine/atf_list.hpp +libengine_a_SOURCES += engine/atf_result.cpp +libengine_a_SOURCES += engine/atf_result.hpp +libengine_a_SOURCES += engine/atf_result_fwd.hpp +libengine_a_SOURCES += engine/config.cpp +libengine_a_SOURCES += engine/config.hpp +libengine_a_SOURCES += engine/config_fwd.hpp +libengine_a_SOURCES += engine/exceptions.cpp +libengine_a_SOURCES += engine/exceptions.hpp +libengine_a_SOURCES += engine/filters.cpp +libengine_a_SOURCES += engine/filters.hpp +libengine_a_SOURCES += engine/filters_fwd.hpp +libengine_a_SOURCES += engine/kyuafile.cpp +libengine_a_SOURCES += engine/kyuafile.hpp +libengine_a_SOURCES += engine/kyuafile_fwd.hpp +libengine_a_SOURCES += engine/plain.cpp +libengine_a_SOURCES += engine/plain.hpp +libengine_a_SOURCES += engine/requirements.cpp +libengine_a_SOURCES += engine/requirements.hpp +libengine_a_SOURCES += engine/scanner.cpp +libengine_a_SOURCES += engine/scanner.hpp +libengine_a_SOURCES += engine/scanner_fwd.hpp +libengine_a_SOURCES += engine/tap.cpp +libengine_a_SOURCES += engine/tap.hpp +libengine_a_SOURCES += engine/tap_parser.cpp +libengine_a_SOURCES += engine/tap_parser.hpp +libengine_a_SOURCES += engine/tap_parser_fwd.hpp +libengine_a_SOURCES += engine/scheduler.cpp +libengine_a_SOURCES += engine/scheduler.hpp +libengine_a_SOURCES += engine/scheduler_fwd.hpp + +if WITH_ATF +tests_enginedir = $(pkgtestsdir)/engine + +tests_engine_DATA = engine/Kyuafile +EXTRA_DIST += $(tests_engine_DATA) + +tests_engine_PROGRAMS = engine/atf_helpers +engine_atf_helpers_SOURCES = engine/atf_helpers.cpp +engine_atf_helpers_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_helpers_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/atf_test +engine_atf_test_SOURCES = engine/atf_test.cpp +engine_atf_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/atf_list_test +engine_atf_list_test_SOURCES = engine/atf_list_test.cpp +engine_atf_list_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_list_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/atf_result_test +engine_atf_result_test_SOURCES = engine/atf_result_test.cpp +engine_atf_result_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_result_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/config_test +engine_config_test_SOURCES = engine/config_test.cpp +engine_config_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_config_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/exceptions_test +engine_exceptions_test_SOURCES = engine/exceptions_test.cpp +engine_exceptions_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_exceptions_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/filters_test +engine_filters_test_SOURCES = engine/filters_test.cpp +engine_filters_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_filters_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/kyuafile_test +engine_kyuafile_test_SOURCES = engine/kyuafile_test.cpp +engine_kyuafile_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_kyuafile_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/plain_helpers +engine_plain_helpers_SOURCES = engine/plain_helpers.cpp +engine_plain_helpers_CXXFLAGS = $(UTILS_CFLAGS) +engine_plain_helpers_LDADD = $(UTILS_LIBS) + +tests_engine_PROGRAMS += engine/plain_test +engine_plain_test_SOURCES = engine/plain_test.cpp +engine_plain_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_plain_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/requirements_test +engine_requirements_test_SOURCES = engine/requirements_test.cpp +engine_requirements_test_CXXFLAGS = $(ENGINE_CFLAGS) $(UTILS_TEST_CFLAGS) \ + $(ATF_CXX_CFLAGS) +engine_requirements_test_LDADD = $(ENGINE_LIBS) $(UTILS_TEST_LIBS) \ + $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/scanner_test +engine_scanner_test_SOURCES = engine/scanner_test.cpp +engine_scanner_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_scanner_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/tap_helpers +engine_tap_helpers_SOURCES = engine/tap_helpers.cpp +engine_tap_helpers_CXXFLAGS = $(UTILS_CFLAGS) +engine_tap_helpers_LDADD = $(UTILS_LIBS) + +tests_engine_PROGRAMS += engine/tap_test +engine_tap_test_SOURCES = engine/tap_test.cpp +engine_tap_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_tap_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/tap_parser_test +engine_tap_parser_test_SOURCES = engine/tap_parser_test.cpp +engine_tap_parser_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_tap_parser_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/scheduler_test +engine_scheduler_test_SOURCES = engine/scheduler_test.cpp +engine_scheduler_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_scheduler_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/contrib/kyua/engine/atf.cpp b/contrib/kyua/engine/atf.cpp new file mode 100644 index 000000000000..eb63be20b0e7 --- /dev/null +++ b/contrib/kyua/engine/atf.cpp @@ -0,0 +1,242 @@ +// 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 "engine/atf.hpp" + +extern "C" { +#include +} + +#include +#include +#include + +#include "engine/atf_list.hpp" +#include "engine/atf_result.hpp" +#include "engine/exceptions.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.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/process/exceptions.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/stream.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::optional; + + +namespace { + + +/// Basename of the file containing the result written by the ATF test case. +static const char* result_name = "result.atf"; + + +/// Magic numbers returned by exec_list when exec(2) fails. +enum list_exit_code { + exit_eacces = 90, + exit_enoent, + exit_enoexec, +}; + + +} // anonymous namespace + + +/// Executes a test program's list operation. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param vars User-provided variables to pass to the test program. +void +engine::atf_interface::exec_list(const model::test_program& test_program, + const config::properties_map& vars) const +{ + utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); + + process::args_vector args; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); + } + + args.push_back("-l"); + try { + process::exec_unsafe(test_program.absolute_path(), args); + } catch (const process::system_error& e) { + if (e.original_errno() == EACCES) + ::_exit(exit_eacces); + else if (e.original_errno() == ENOENT) + ::_exit(exit_enoent); + else if (e.original_errno() == ENOEXEC) + ::_exit(exit_enoexec); + throw; + } +} + + +/// Computes the test cases list of a test program. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// \param stdout_path Path to the file containing the stdout of the test. +/// \param stderr_path Path to the file containing the stderr of the test. +/// +/// \return A list of test cases. +/// +/// \throw error If there is a problem parsing the test case list. +model::test_cases_map +engine::atf_interface::parse_list(const optional< process::status >& status, + const fs::path& stdout_path, + const fs::path& stderr_path) const +{ + const std::string stderr_contents = utils::read_file(stderr_path); + if (!stderr_contents.empty()) + LW("Test case list wrote to stderr: " + stderr_contents); + + if (!status) + throw engine::error("Test case list timed out"); + if (status.get().exited()) { + const int exitstatus = status.get().exitstatus(); + if (exitstatus == EXIT_SUCCESS) { + // Nothing to do; fall through. + } else if (exitstatus == exit_eacces) { + throw engine::error("Permission denied to run test program"); + } else if (exitstatus == exit_enoent) { + throw engine::error("Cannot find test program"); + } else if (exitstatus == exit_enoexec) { + throw engine::error("Invalid test program format"); + } else { + throw engine::error("Test program did not exit cleanly"); + } + } else { + throw engine::error("Test program received signal"); + } + + std::ifstream input(stdout_path.c_str()); + if (!input) + throw engine::load_error(stdout_path, "Cannot open file for read"); + const model::test_cases_map test_cases = parse_atf_list(input); + + if (!stderr_contents.empty()) + throw engine::error("Test case list wrote to stderr"); + + return test_cases; +} + + +/// Executes a test case of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +/// \param control_directory Directory where the interface may place control +/// files. +void +engine::atf_interface::exec_test(const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& control_directory) const +{ + utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); + + process::args_vector args; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); + } + + args.push_back(F("-r%s") % (control_directory / result_name)); + args.push_back(test_case_name); + process::exec(test_program.absolute_path(), args); +} + + +/// Executes a test cleanup routine of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +void +engine::atf_interface::exec_cleanup( + const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& /* control_directory */) const +{ + utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); + + process::args_vector args; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); + } + + args.push_back(F("%s:cleanup") % test_case_name); + process::exec(test_program.absolute_path(), args); +} + + +/// Computes the result of a test case based on its termination status. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// \param control_directory Directory where the interface may have placed +/// control files. +/// +/// \return A test result. +model::test_result +engine::atf_interface::compute_result( + const optional< process::status >& status, + const fs::path& control_directory, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + return calculate_atf_result(status, control_directory / result_name); +} diff --git a/contrib/kyua/engine/atf.hpp b/contrib/kyua/engine/atf.hpp new file mode 100644 index 000000000000..34ddc2413235 --- /dev/null +++ b/contrib/kyua/engine/atf.hpp @@ -0,0 +1,72 @@ +// 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 engine/atf.hpp +/// Execution engine for test programs that implement the atf interface. + +#if !defined(ENGINE_ATF_HPP) +#define ENGINE_ATF_HPP + +#include "engine/scheduler.hpp" + +namespace engine { + + +/// Implementation of the scheduler interface for atf test programs. +class atf_interface : public engine::scheduler::interface { +public: + void exec_list(const model::test_program&, + const utils::config::properties_map&) const UTILS_NORETURN; + + model::test_cases_map parse_list( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&) const; + + void exec_test(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + void exec_cleanup(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + model::test_result compute_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_ATF_HPP) diff --git a/contrib/kyua/engine/atf_helpers.cpp b/contrib/kyua/engine/atf_helpers.cpp new file mode 100644 index 000000000000..c45654f10e58 --- /dev/null +++ b/contrib/kyua/engine/atf_helpers.cpp @@ -0,0 +1,414 @@ +// 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. + +extern "C" { +#include + +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/test_utils.ipp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace text = utils::text; + +using utils::optional; + + +namespace { + + +/// Creates an empty file in the given directory. +/// +/// \param test_case The test case currently running. +/// \param directory The name of the configuration variable that holds the path +/// to the directory in which to create the cookie file. +/// \param name The name of the cookie file to create. +static void +create_cookie(const atf::tests::tc* test_case, const char* directory, + const char* name) +{ + if (!test_case->has_config_var(directory)) + test_case->fail(std::string(name) + " not provided"); + + const fs::path control_dir(test_case->get_config_var(directory)); + std::ofstream file((control_dir / name).c_str()); + if (!file) + test_case->fail("Failed to create the control cookie"); + file.close(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITH_CLEANUP(check_cleanup_workdir); +ATF_TEST_CASE_HEAD(check_cleanup_workdir) +{ + set_md_var("require.config", "control_dir"); +} +ATF_TEST_CASE_BODY(check_cleanup_workdir) +{ + std::ofstream cookie("workdir_cookie"); + cookie << "1234\n"; + cookie.close(); + skip("cookie created"); +} +ATF_TEST_CASE_CLEANUP(check_cleanup_workdir) +{ + const fs::path control_dir(get_config_var("control_dir")); + + std::ifstream cookie("workdir_cookie"); + if (!cookie) { + std::ofstream((control_dir / "missing_cookie").c_str()).close(); + std::exit(EXIT_FAILURE); + } + + std::string value; + cookie >> value; + if (value != "1234") { + std::ofstream((control_dir / "invalid_cookie").c_str()).close(); + std::exit(EXIT_FAILURE); + } + + std::ofstream((control_dir / "cookie_ok").c_str()).close(); + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_configuration_variables); +ATF_TEST_CASE_BODY(check_configuration_variables) +{ + ATF_REQUIRE(has_config_var("first")); + ATF_REQUIRE_EQ("some value", get_config_var("first")); + + ATF_REQUIRE(has_config_var("second")); + ATF_REQUIRE_EQ("some other value", get_config_var("second")); +} + + +ATF_TEST_CASE(check_list_config); +ATF_TEST_CASE_HEAD(check_list_config) +{ + std::string description = "Found:"; + + if (has_config_var("var1")) + description += " var1=" + get_config_var("var1"); + if (has_config_var("var2")) + description += " var2=" + get_config_var("var2"); + + set_md_var("descr", description); +} +ATF_TEST_CASE_BODY(check_list_config) +{ +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_unprivileged); +ATF_TEST_CASE_BODY(check_unprivileged) +{ + if (::getuid() == 0) + fail("Running as root, but I shouldn't be"); + + std::ofstream file("cookie"); + if (!file) + fail("Failed to create the cookie; work directory probably owned by " + "root"); + file.close(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(crash); +ATF_TEST_CASE_BODY(crash) +{ + std::abort(); +} + + +ATF_TEST_CASE(crash_head); +ATF_TEST_CASE_HEAD(crash_head) +{ + utils::abort_without_coredump(); +} +ATF_TEST_CASE_BODY(crash_head) +{ +} + + +ATF_TEST_CASE_WITH_CLEANUP(crash_cleanup); +ATF_TEST_CASE_HEAD(crash_cleanup) +{ +} +ATF_TEST_CASE_BODY(crash_cleanup) +{ +} +ATF_TEST_CASE_CLEANUP(crash_cleanup) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_cookie_in_control_dir); +ATF_TEST_CASE_BODY(create_cookie_in_control_dir) +{ + create_cookie(this, "control_dir", "cookie"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_cookie_in_workdir); +ATF_TEST_CASE_BODY(create_cookie_in_workdir) +{ + std::ofstream file("cookie"); + if (!file) + fail("Failed to create the cookie"); + file.close(); +} + + +ATF_TEST_CASE_WITH_CLEANUP(create_cookie_from_cleanup); +ATF_TEST_CASE_HEAD(create_cookie_from_cleanup) +{ +} +ATF_TEST_CASE_BODY(create_cookie_from_cleanup) +{ +} +ATF_TEST_CASE_CLEANUP(create_cookie_from_cleanup) +{ + create_cookie(this, "control_dir", "cookie"); +} + + +ATF_TEST_CASE_WITH_CLEANUP(expect_timeout); +ATF_TEST_CASE_HEAD(expect_timeout) +{ + if (has_config_var("timeout")) + set_md_var("timeout", get_config_var("timeout")); +} +ATF_TEST_CASE_BODY(expect_timeout) +{ + expect_timeout("Times out on purpose"); + ::sleep(10); + create_cookie(this, "control_dir", "cookie"); +} +ATF_TEST_CASE_CLEANUP(expect_timeout) +{ + create_cookie(this, "control_dir", "cookie.cleanup"); +} + + +ATF_TEST_CASE_WITH_CLEANUP(output); +ATF_TEST_CASE_HEAD(output) +{ +} +ATF_TEST_CASE_BODY(output) +{ + std::cout << "Body message to stdout\n"; + std::cerr << "Body message to stderr\n"; +} +ATF_TEST_CASE_CLEANUP(output) +{ + std::cout << "Cleanup message to stdout\n"; + std::cerr << "Cleanup message to stderr\n"; +} + + +ATF_TEST_CASE(output_in_list); +ATF_TEST_CASE_HEAD(output_in_list) +{ + std::cerr << "Should not write anything!\n"; +} +ATF_TEST_CASE_BODY(output_in_list) +{ +} + + +ATF_TEST_CASE(pass); +ATF_TEST_CASE_HEAD(pass) +{ + set_md_var("descr", "Always-passing test case"); +} +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_TEST_CASE_WITH_CLEANUP(shared_workdir); +ATF_TEST_CASE_HEAD(shared_workdir) +{ +} +ATF_TEST_CASE_BODY(shared_workdir) +{ + atf::utils::create_file("shared_cookie", ""); +} +ATF_TEST_CASE_CLEANUP(shared_workdir) +{ + if (!atf::utils::file_exists("shared_cookie")) + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(spawn_blocking_child); +ATF_TEST_CASE_HEAD(spawn_blocking_child) +{ + set_md_var("require.config", "control_dir"); +} +ATF_TEST_CASE_BODY(spawn_blocking_child) +{ + pid_t pid = ::fork(); + if (pid == -1) + fail("Cannot fork subprocess"); + else if (pid == 0) { + for (;;) + ::pause(); + } else { + const fs::path name = fs::path(get_config_var("control_dir")) / "pid"; + std::ofstream pidfile(name.c_str()); + ATF_REQUIRE(pidfile); + pidfile << pid; + pidfile.close(); + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(timeout_body); +ATF_TEST_CASE_HEAD(timeout_body) +{ + if (has_config_var("timeout")) + set_md_var("timeout", get_config_var("timeout")); +} +ATF_TEST_CASE_BODY(timeout_body) +{ + ::sleep(10); + create_cookie(this, "control_dir", "cookie"); +} +ATF_TEST_CASE_CLEANUP(timeout_body) +{ + create_cookie(this, "control_dir", "cookie.cleanup"); +} + + +ATF_TEST_CASE_WITH_CLEANUP(timeout_cleanup); +ATF_TEST_CASE_HEAD(timeout_cleanup) +{ +} +ATF_TEST_CASE_BODY(timeout_cleanup) +{ +} +ATF_TEST_CASE_CLEANUP(timeout_cleanup) +{ + ::sleep(10); + create_cookie(this, "control_dir", "cookie"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(validate_isolation); +ATF_TEST_CASE_BODY(validate_isolation) +{ + ATF_REQUIRE(utils::getenv("HOME").get() != "fake-value"); + ATF_REQUIRE(!utils::getenv("LANG")); +} + + +/// Wrapper around ATF_ADD_TEST_CASE to only add a test when requested. +/// +/// The caller can set the TEST_CASES environment variable to a +/// whitespace-separated list of test case names to enable. If not empty, the +/// list acts as a filter for the tests to add. +/// +/// \param tcs List of test cases into which to register the test. +/// \param filters List of filters to determine whether the test applies or not. +/// \param name Name of the test case being added. +#define ADD_TEST_CASE(tcs, filters, name) \ + do { \ + if (filters.empty() || filters.find(#name) != filters.end()) \ + ATF_ADD_TEST_CASE(tcs, name); \ + } while (false) + + +ATF_INIT_TEST_CASES(tcs) +{ + logging::set_inmemory(); + + // TODO(jmmv): Instead of using "filters", we should make TEST_CASES + // explicitly list all the test cases to enable. This would let us get rid + // of some of the hacks below... + std::set< std::string > filters; + + const optional< std::string > names_raw = utils::getenv("TEST_CASES"); + if (names_raw) { + if (names_raw.get().empty()) + return; // See TODO above. + + const std::vector< std::string > names = text::split( + names_raw.get(), ' '); + std::copy(names.begin(), names.end(), + std::inserter(filters, filters.begin())); + } + + if (filters.find("crash_head") != filters.end()) // See TODO above. + ATF_ADD_TEST_CASE(tcs, crash_head); + if (filters.find("output_in_list") != filters.end()) // See TODO above. + ATF_ADD_TEST_CASE(tcs, output_in_list); + + ADD_TEST_CASE(tcs, filters, check_cleanup_workdir); + ADD_TEST_CASE(tcs, filters, check_configuration_variables); + ADD_TEST_CASE(tcs, filters, check_list_config); + ADD_TEST_CASE(tcs, filters, check_unprivileged); + ADD_TEST_CASE(tcs, filters, crash); + ADD_TEST_CASE(tcs, filters, crash_cleanup); + ADD_TEST_CASE(tcs, filters, create_cookie_in_control_dir); + ADD_TEST_CASE(tcs, filters, create_cookie_in_workdir); + ADD_TEST_CASE(tcs, filters, create_cookie_from_cleanup); + ADD_TEST_CASE(tcs, filters, expect_timeout); + ADD_TEST_CASE(tcs, filters, output); + ADD_TEST_CASE(tcs, filters, pass); + ADD_TEST_CASE(tcs, filters, shared_workdir); + ADD_TEST_CASE(tcs, filters, spawn_blocking_child); + ADD_TEST_CASE(tcs, filters, timeout_body); + ADD_TEST_CASE(tcs, filters, timeout_cleanup); + ADD_TEST_CASE(tcs, filters, validate_isolation); +} diff --git a/contrib/kyua/engine/atf_list.cpp b/contrib/kyua/engine/atf_list.cpp new file mode 100644 index 000000000000..a16b889c74f0 --- /dev/null +++ b/contrib/kyua/engine/atf_list.cpp @@ -0,0 +1,196 @@ +// 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 "engine/atf_list.hpp" + +#include +#include +#include + +#include "engine/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Splits a property line of the form "name: word1 [... wordN]". +/// +/// \param line The line to parse. +/// +/// \return A (property_name, property_value) pair. +/// +/// \throw format_error If the value of line is invalid. +static std::pair< std::string, std::string > +split_prop_line(const std::string& line) +{ + const std::string::size_type pos = line.find(": "); + if (pos == std::string::npos) + throw engine::format_error("Invalid property line; expecting line of " + "the form 'name: value'"); + return std::make_pair(line.substr(0, pos), line.substr(pos + 2)); +} + + +/// Parses a set of consecutive property lines. +/// +/// Processing stops when an empty line or the end of file is reached. None of +/// these conditions indicate errors. +/// +/// \param input The stream to read the lines from. +/// +/// \return The parsed property lines. +/// +/// throw format_error If the input stream has an invalid format. +static model::properties_map +parse_properties(std::istream& input) +{ + model::properties_map properties; + + std::string line; + while (std::getline(input, line).good() && !line.empty()) { + const std::pair< std::string, std::string > property = split_prop_line( + line); + if (properties.find(property.first) != properties.end()) + throw engine::format_error("Duplicate value for property " + + property.first); + properties.insert(property); + } + + return properties; +} + + +} // anonymous namespace + + +/// Parses the metadata of an ATF test case. +/// +/// \param props The properties (name/value string pairs) as provided by the +/// ATF test program. +/// +/// \return A parsed metadata object. +/// +/// \throw engine::format_error If the syntax of any of the properties is +/// invalid. +model::metadata +engine::parse_atf_metadata(const model::properties_map& props) +{ + model::metadata_builder mdbuilder; + + try { + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); iter++) { + const std::string& name = (*iter).first; + const std::string& value = (*iter).second; + + if (name == "descr") { + mdbuilder.set_string("description", value); + } else if (name == "has.cleanup") { + mdbuilder.set_string("has_cleanup", value); + } else if (name == "require.arch") { + mdbuilder.set_string("allowed_architectures", value); + } else if (name == "require.config") { + mdbuilder.set_string("required_configs", value); + } else if (name == "require.files") { + mdbuilder.set_string("required_files", value); + } else if (name == "require.machine") { + mdbuilder.set_string("allowed_platforms", value); + } else if (name == "require.memory") { + mdbuilder.set_string("required_memory", value); + } else if (name == "require.progs") { + mdbuilder.set_string("required_programs", value); + } else if (name == "require.user") { + mdbuilder.set_string("required_user", value); + } else if (name == "timeout") { + mdbuilder.set_string("timeout", value); + } else if (name.length() > 2 && name.substr(0, 2) == "X-") { + mdbuilder.add_custom(name.substr(2), value); + } else { + throw engine::format_error(F("Unknown test case metadata " + "property '%s'") % name); + } + } + } catch (const config::error& e) { + throw engine::format_error(e.what()); + } + + return mdbuilder.build(); +} + + +/// Parses the ATF list of test cases from an open stream. +/// +/// \param input The stream to read from. +/// +/// \return The collection of parsed test cases. +/// +/// \throw format_error If there is any problem in the input data. +model::test_cases_map +engine::parse_atf_list(std::istream& input) +{ + std::string line; + + std::getline(input, line); + if (line != "Content-Type: application/X-atf-tp; version=\"1\"" + || !input.good()) + throw format_error(F("Invalid header for test case list; expecting " + "Content-Type for application/X-atf-tp version 1, " + "got '%s'") % line); + + std::getline(input, line); + if (!line.empty() || !input.good()) + throw format_error(F("Invalid header for test case list; expecting " + "a blank line, got '%s'") % line); + + model::test_cases_map_builder test_cases_builder; + while (std::getline(input, line).good()) { + const std::pair< std::string, std::string > ident = split_prop_line( + line); + if (ident.first != "ident" or ident.second.empty()) + throw format_error("Invalid test case definition; must be " + "preceeded by the identifier"); + + const model::properties_map props = parse_properties(input); + test_cases_builder.add(ident.second, parse_atf_metadata(props)); + } + const model::test_cases_map test_cases = test_cases_builder.build(); + if (test_cases.empty()) { + // The scheduler interface also checks for the presence of at least one + // test case. However, because the atf format itself requires one test + // case to be always present, we check for this condition here as well. + throw format_error("No test cases"); + } + return test_cases; +} diff --git a/contrib/kyua/engine/atf_list.hpp b/contrib/kyua/engine/atf_list.hpp new file mode 100644 index 000000000000..3d81d03e3bcf --- /dev/null +++ b/contrib/kyua/engine/atf_list.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 engine/atf_list.hpp +/// Parser of ATF test case lists. + +#if !defined(ENGINE_ATF_LIST_HPP) +#define ENGINE_ATF_LIST_HPP + +#include + +#include "model/metadata_fwd.hpp" +#include "model/test_case_fwd.hpp" +#include "model/types.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace engine { + + +model::metadata parse_atf_metadata(const model::properties_map&); +model::test_cases_map parse_atf_list(std::istream&); + + +} // namespace engine + +#endif // !defined(ENGINE_ATF_LIST_HPP) diff --git a/contrib/kyua/engine/atf_list_test.cpp b/contrib/kyua/engine/atf_list_test.cpp new file mode 100644 index 000000000000..7f19ca8fbec5 --- /dev/null +++ b/contrib/kyua/engine/atf_list_test.cpp @@ -0,0 +1,278 @@ +// 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 "engine/atf_list.hpp" + +#include +#include + +#include + +#include "engine/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/types.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_metadata__defaults) +ATF_TEST_CASE_BODY(parse_atf_metadata__defaults) +{ + const model::properties_map properties; + const model::metadata md = engine::parse_atf_metadata(properties); + + const model::metadata exp_md = model::metadata_builder().build(); + ATF_REQUIRE_EQ(exp_md, md); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_metadata__override_all) +ATF_TEST_CASE_BODY(parse_atf_metadata__override_all) +{ + model::properties_map properties; + properties["descr"] = "Some text"; + properties["has.cleanup"] = "true"; + properties["require.arch"] = "i386 x86_64"; + properties["require.config"] = "var1 var2 var3"; + properties["require.files"] = "/file1 /dir/file2"; + properties["require.machine"] = "amd64"; + properties["require.memory"] = "1m"; + properties["require.progs"] = "/bin/ls svn"; + properties["require.user"] = "root"; + properties["timeout"] = "123"; + properties["X-foo"] = "value1"; + properties["X-bar"] = "value2"; + properties["X-baz-www"] = "value3"; + const model::metadata md = engine::parse_atf_metadata(properties); + + const model::metadata exp_md = model::metadata_builder() + .add_allowed_architecture("i386") + .add_allowed_architecture("x86_64") + .add_allowed_platform("amd64") + .add_custom("foo", "value1") + .add_custom("bar", "value2") + .add_custom("baz-www", "value3") + .add_required_config("var1") + .add_required_config("var2") + .add_required_config("var3") + .add_required_file(fs::path("/file1")) + .add_required_file(fs::path("/dir/file2")) + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("svn")) + .set_description("Some text") + .set_has_cleanup(true) + .set_required_memory(units::bytes::parse("1m")) + .set_required_user("root") + .set_timeout(datetime::delta(123, 0)) + .build(); + ATF_REQUIRE_EQ(exp_md, md); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_metadata__unknown) +ATF_TEST_CASE_BODY(parse_atf_metadata__unknown) +{ + model::properties_map properties; + properties["foobar"] = "Some text"; + + ATF_REQUIRE_THROW_RE(engine::format_error, "Unknown.*property.*'foobar'", + engine::parse_atf_metadata(properties)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__empty); +ATF_TEST_CASE_BODY(parse_atf_list__empty) +{ + const std::string text = ""; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting Content-Type", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__invalid_header); +ATF_TEST_CASE_BODY(parse_atf_list__invalid_header) +{ + { + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting.*blank line", + engine::parse_atf_list(input)); + } + + { + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\nfoo\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting.*blank line", + engine::parse_atf_list(input)); + } + + { + const std::string text = + "Content-Type: application/X-atf-tp; version=\"2\"\n\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting Content-Type", + engine::parse_atf_list(input)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__no_test_cases); +ATF_TEST_CASE_BODY(parse_atf_list__no_test_cases) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "No test cases", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_simple); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_simple) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n" + "\n" + "ident: test-case\n"; + std::istringstream input(text); + const model::test_cases_map tests = engine::parse_atf_list(input); + + const model::test_cases_map exp_tests = model::test_cases_map_builder() + .add("test-case").build(); + ATF_REQUIRE_EQ(exp_tests, tests); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_complex); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_complex) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n" + "\n" + "ident: first\n" + "descr: This is the description\n" + "timeout: 500\n"; + std::istringstream input(text); + const model::test_cases_map tests = engine::parse_atf_list(input); + + const model::test_cases_map exp_tests = model::test_cases_map_builder() + .add("first", model::metadata_builder() + .set_description("This is the description") + .set_timeout(datetime::delta(500, 0)) + .build()) + .build(); + ATF_REQUIRE_EQ(exp_tests, tests); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_invalid_syntax); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_invalid_syntax) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n\n" + "descr: This is the description\n" + "ident: first\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "preceeded.*identifier", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_invalid_properties); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_invalid_properties) +{ + // Inject a single invalid property that makes test_case::from_properties() + // raise a particular error message so that we can validate that such + // function was called. We do intensive testing separately, so it is not + // necessary to redo it here. + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n\n" + "ident: first\n" + "require.progs: bin/ls\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "Relative path 'bin/ls'", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__many_test_cases); +ATF_TEST_CASE_BODY(parse_atf_list__many_test_cases) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n" + "\n" + "ident: first\n" + "descr: This is the description\n" + "\n" + "ident: second\n" + "timeout: 500\n" + "descr: Some text\n" + "\n" + "ident: third\n"; + std::istringstream input(text); + const model::test_cases_map tests = engine::parse_atf_list(input); + + const model::test_cases_map exp_tests = model::test_cases_map_builder() + .add("first", model::metadata_builder() + .set_description("This is the description") + .build()) + .add("second", model::metadata_builder() + .set_description("Some text") + .set_timeout(datetime::delta(500, 0)) + .build()) + .add("third") + .build(); + ATF_REQUIRE_EQ(exp_tests, tests); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, parse_atf_metadata__defaults); + ATF_ADD_TEST_CASE(tcs, parse_atf_metadata__override_all); + ATF_ADD_TEST_CASE(tcs, parse_atf_metadata__unknown); + + ATF_ADD_TEST_CASE(tcs, parse_atf_list__empty); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__invalid_header); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__no_test_cases); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_simple); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_complex); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_invalid_syntax); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_invalid_properties); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__many_test_cases); +} diff --git a/contrib/kyua/engine/atf_result.cpp b/contrib/kyua/engine/atf_result.cpp new file mode 100644 index 000000000000..f99b28f9e96e --- /dev/null +++ b/contrib/kyua/engine/atf_result.cpp @@ -0,0 +1,642 @@ +// 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 "engine/atf_result.hpp" + +#include +#include +#include + +#include "engine/exceptions.hpp" +#include "model/test_result.hpp" +#include "utils/fs/path.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Reads a file and flattens its lines. +/// +/// The main purpose of this function is to simplify the parsing of a file +/// containing the result of a test. Therefore, the return value carries +/// several assumptions. +/// +/// \param input The stream to read from. +/// +/// \return A pair (line count, contents) detailing how many lines where read +/// and their contents. If the file contains a single line with no newline +/// character, the line count is 0. If the file includes more than one line, +/// the lines are merged together and separated by the magic string +/// '<<NEWLINE>>'. +static std::pair< size_t, std::string > +read_lines(std::istream& input) +{ + std::pair< size_t, std::string > ret = std::make_pair(0, ""); + + do { + std::string line; + std::getline(input, line); + if (input.eof() && !line.empty()) { + if (ret.first == 0) + ret.second = line; + else { + ret.second += "<>" + line; + ret.first++; + } + } else if (input.good()) { + if (ret.first == 0) + ret.second = line; + else + ret.second += "<>" + line; + ret.first++; + } + } while (input.good()); + + return ret; +} + + +/// Parses a test result that does not accept a reason. +/// +/// \param status The result status name. +/// \param rest The rest of the line after the status name. +/// +/// \return An object representing the test result. +/// +/// \throw format_error If the result is invalid (i.e. rest is invalid). +/// +/// \pre status must be "passed". +static engine::atf_result +parse_without_reason(const std::string& status, const std::string& rest) +{ + if (!rest.empty()) + throw engine::format_error(F("%s cannot have a reason") % status); + PRE(status == "passed"); + return engine::atf_result(engine::atf_result::passed); +} + + +/// Parses a test result that needs a reason. +/// +/// \param status The result status name. +/// \param rest The rest of the line after the status name. +/// +/// \return An object representing the test result. +/// +/// \throw format_error If the result is invalid (i.e. rest is invalid). +/// +/// \pre status must be one of "broken", "expected_death", "expected_failure", +/// "expected_timeout", "failed" or "skipped". +static engine::atf_result +parse_with_reason(const std::string& status, const std::string& rest) +{ + using engine::atf_result; + + if (rest.length() < 3 || rest.substr(0, 2) != ": ") + throw engine::format_error(F("%s must be followed by ': '") % + status); + const std::string reason = rest.substr(2); + INV(!reason.empty()); + + if (status == "broken") + return atf_result(atf_result::broken, reason); + else if (status == "expected_death") + return atf_result(atf_result::expected_death, reason); + else if (status == "expected_failure") + return atf_result(atf_result::expected_failure, reason); + else if (status == "expected_timeout") + return atf_result(atf_result::expected_timeout, reason); + else if (status == "failed") + return atf_result(atf_result::failed, reason); + else if (status == "skipped") + return atf_result(atf_result::skipped, reason); + else + PRE_MSG(false, "Unexpected status"); +} + + +/// Converts a string to an integer. +/// +/// \param str The string containing the integer to convert. +/// +/// \return The converted integer; none if the parsing fails. +static optional< int > +parse_int(const std::string& str) +{ + try { + return utils::make_optional(text::to_type< int >(str)); + } catch (const text::value_error& e) { + return none; + } +} + + +/// Parses a test result that needs a reason and accepts an optional integer. +/// +/// \param status The result status name. +/// \param rest The rest of the line after the status name. +/// +/// \return The parsed test result if the data is valid, or a broken result if +/// the parsing failed. +/// +/// \pre status must be one of "expected_exit" or "expected_signal". +static engine::atf_result +parse_with_reason_and_arg(const std::string& status, const std::string& rest) +{ + using engine::atf_result; + + std::string::size_type delim = rest.find_first_of(":("); + if (delim == std::string::npos) + throw engine::format_error(F("Invalid format for '%s' test case " + "result; must be followed by '[(num)]: " + "' but found '%s'") % + status % rest); + + optional< int > arg; + if (rest[delim] == '(') { + const std::string::size_type delim2 = rest.find("):", delim); + if (delim == std::string::npos) + throw engine::format_error(F("Mismatched '(' in %s") % rest); + + const std::string argstr = rest.substr(delim + 1, delim2 - delim - 1); + arg = parse_int(argstr); + if (!arg) + throw engine::format_error(F("Invalid integer argument '%s' to " + "'%s' test case result") % + argstr % status); + delim = delim2 + 1; + } + + const std::string reason = rest.substr(delim + 2); + + if (status == "expected_exit") + return atf_result(atf_result::expected_exit, arg, reason); + else if (status == "expected_signal") + return atf_result(atf_result::expected_signal, arg, reason); + else + PRE_MSG(false, "Unexpected status"); +} + + +/// Formats the termination status of a process to be used with validate_result. +/// +/// \param status The status to format. +/// +/// \return A string describing the status. +static std::string +format_status(const process::status& status) +{ + if (status.exited()) + return F("exited with code %s") % status.exitstatus(); + else if (status.signaled()) + return F("received signal %s%s") % status.termsig() % + (status.coredump() ? " (core dumped)" : ""); + else + return F("terminated in an unknown manner"); +} + + +} // anonymous namespace + + +/// Constructs a raw result with a type. +/// +/// The reason and the argument are left uninitialized. +/// +/// \param type_ The type of the result. +engine::atf_result::atf_result(const types type_) : + _type(type_) +{ +} + + +/// Constructs a raw result with a type and a reason. +/// +/// The argument is left uninitialized. +/// +/// \param type_ The type of the result. +/// \param reason_ The reason for the result. +engine::atf_result::atf_result(const types type_, const std::string& reason_) : + _type(type_), _reason(reason_) +{ +} + + +/// Constructs a raw result with a type, an optional argument and a reason. +/// +/// \param type_ The type of the result. +/// \param argument_ The optional argument for the result. +/// \param reason_ The reason for the result. +engine::atf_result::atf_result(const types type_, + const utils::optional< int >& argument_, + const std::string& reason_) : + _type(type_), _argument(argument_), _reason(reason_) +{ +} + + +/// Parses an input stream to extract a test result. +/// +/// If the parsing fails for any reason, the test result is 'broken' and it +/// contains the reason for the parsing failure. Test cases that report results +/// in an inconsistent state cannot be trusted (e.g. the test program code may +/// have a bug), and thus why they are reported as broken instead of just failed +/// (which is a legitimate result for a test case). +/// +/// \param input The stream to read from. +/// +/// \return A generic representation of the result of the test case. +/// +/// \throw format_error If the input is invalid. +engine::atf_result +engine::atf_result::parse(std::istream& input) +{ + const std::pair< size_t, std::string > data = read_lines(input); + if (data.first == 0) + throw format_error("Empty test result or no new line"); + else if (data.first > 1) + throw format_error("Test result contains multiple lines: " + + data.second); + else { + const std::string::size_type delim = data.second.find_first_not_of( + "abcdefghijklmnopqrstuvwxyz_"); + const std::string status = data.second.substr(0, delim); + const std::string rest = data.second.substr(status.length()); + + if (status == "broken") + return parse_with_reason(status, rest); + else if (status == "expected_death") + return parse_with_reason(status, rest); + else if (status == "expected_exit") + return parse_with_reason_and_arg(status, rest); + else if (status == "expected_failure") + return parse_with_reason(status, rest); + else if (status == "expected_signal") + return parse_with_reason_and_arg(status, rest); + else if (status == "expected_timeout") + return parse_with_reason(status, rest); + else if (status == "failed") + return parse_with_reason(status, rest); + else if (status == "passed") + return parse_without_reason(status, rest); + else if (status == "skipped") + return parse_with_reason(status, rest); + else + throw format_error(F("Unknown test result '%s'") % status); + } +} + + +/// Loads a test case result from a file. +/// +/// \param file The file to parse. +/// +/// \return The parsed test case result if all goes well. +/// +/// \throw std::runtime_error If the file does not exist. +/// \throw engine::format_error If the contents of the file are bogus. +engine::atf_result +engine::atf_result::load(const fs::path& file) +{ + std::ifstream input(file.c_str()); + if (!input) + throw std::runtime_error("Cannot open results file"); + else + return parse(input); +} + + +/// Gets the type of the result. +/// +/// \return A result type. +engine::atf_result::types +engine::atf_result::type(void) const +{ + return _type; +} + + +/// Gets the optional argument of the result. +/// +/// \return The argument of the result if present; none otherwise. +const optional< int >& +engine::atf_result::argument(void) const +{ + return _argument; +} + + +/// Gets the optional reason of the result. +/// +/// \return The reason of the result if present; none otherwise. +const optional< std::string >& +engine::atf_result::reason(void) const +{ + return _reason; +} + + +/// Checks whether the result should be reported as good or not. +/// +/// \return True if the result can be considered "good", false otherwise. +bool +engine::atf_result::good(void) const +{ + switch (_type) { + case atf_result::expected_death: + case atf_result::expected_exit: + case atf_result::expected_failure: + case atf_result::expected_signal: + case atf_result::expected_timeout: + case atf_result::passed: + case atf_result::skipped: + return true; + + case atf_result::broken: + case atf_result::failed: + return false; + + default: + UNREACHABLE; + } +} + + +/// Reinterprets a raw result based on the termination status of the test case. +/// +/// This reinterpretation ensures that the termination conditions of the program +/// match what is expected of the paticular result reported by the test program. +/// If such conditions do not match, the test program is considered bogus and is +/// thus reported as broken. +/// +/// This is just a helper function for calculate_result(); the real result of +/// the test case cannot be inferred from apply() only. +/// +/// \param status The exit status of the test program, or none if the test +/// program timed out. +/// +/// \result The adjusted result. The original result is transformed into broken +/// if the exit status of the program does not match our expectations. +engine::atf_result +engine::atf_result::apply(const optional< process::status >& status) + const +{ + if (!status) { + if (_type != atf_result::expected_timeout) + return atf_result(atf_result::broken, "Test case body timed out"); + else + return *this; + } + + INV(status); + switch (_type) { + case atf_result::broken: + return *this; + + case atf_result::expected_death: + return *this; + + case atf_result::expected_exit: + if (status.get().exited()) { + if (_argument) { + if (_argument.get() == status.get().exitstatus()) + return *this; + else + return atf_result( + atf_result::failed, + F("Test case expected to exit with code %s but got " + "code %s") % + _argument.get() % status.get().exitstatus()); + } else + return *this; + } else + return atf_result(atf_result::broken, "Expected clean exit but " + + format_status(status.get())); + + case atf_result::expected_failure: + if (status.get().exited() && status.get().exitstatus() == EXIT_SUCCESS) + return *this; + else + return atf_result(atf_result::broken, "Expected failure should " + "have reported success but " + + format_status(status.get())); + + case atf_result::expected_signal: + if (status.get().signaled()) { + if (_argument) { + if (_argument.get() == status.get().termsig()) + return *this; + else + return atf_result( + atf_result::failed, + F("Test case expected to receive signal %s but " + "got %s") % + _argument.get() % status.get().termsig()); + } else + return *this; + } else + return atf_result(atf_result::broken, "Expected signal but " + + format_status(status.get())); + + case atf_result::expected_timeout: + return atf_result(atf_result::broken, "Expected timeout but " + + format_status(status.get())); + + case atf_result::failed: + if (status.get().exited() && status.get().exitstatus() == EXIT_FAILURE) + return *this; + else + return atf_result(atf_result::broken, "Failed test case should " + "have reported failure but " + + format_status(status.get())); + + case atf_result::passed: + if (status.get().exited() && status.get().exitstatus() == EXIT_SUCCESS) + return *this; + else + return atf_result(atf_result::broken, "Passed test case should " + "have reported success but " + + format_status(status.get())); + + case atf_result::skipped: + if (status.get().exited() && status.get().exitstatus() == EXIT_SUCCESS) + return *this; + else + return atf_result(atf_result::broken, "Skipped test case should " + "have reported success but " + + format_status(status.get())); + } + + UNREACHABLE; +} + + +/// Converts an internal result to the interface-agnostic representation. +/// +/// \return A generic result instance representing this result. +model::test_result +engine::atf_result::externalize(void) const +{ + switch (_type) { + case atf_result::broken: + return model::test_result(model::test_result_broken, _reason.get()); + + case atf_result::expected_death: + case atf_result::expected_exit: + case atf_result::expected_failure: + case atf_result::expected_signal: + case atf_result::expected_timeout: + return model::test_result(model::test_result_expected_failure, + _reason.get()); + + case atf_result::failed: + return model::test_result(model::test_result_failed, _reason.get()); + + case atf_result::passed: + return model::test_result(model::test_result_passed); + + case atf_result::skipped: + return model::test_result(model::test_result_skipped, _reason.get()); + + default: + UNREACHABLE; + } +} + + +/// Compares two raw results for equality. +/// +/// \param other The result to compare to. +/// +/// \return True if the two raw results are equal; false otherwise. +bool +engine::atf_result::operator==(const atf_result& other) const +{ + return _type == other._type && _argument == other._argument && + _reason == other._reason; +} + + +/// Compares two raw results for inequality. +/// +/// \param other The result to compare to. +/// +/// \return True if the two raw results are different; false otherwise. +bool +engine::atf_result::operator!=(const atf_result& other) const +{ + return !(*this == other); +} + + +/// 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& +engine::operator<<(std::ostream& output, const atf_result& object) +{ + std::string result_name; + switch (object.type()) { + case atf_result::broken: result_name = "broken"; break; + case atf_result::expected_death: result_name = "expected_death"; break; + case atf_result::expected_exit: result_name = "expected_exit"; break; + case atf_result::expected_failure: result_name = "expected_failure"; break; + case atf_result::expected_signal: result_name = "expected_signal"; break; + case atf_result::expected_timeout: result_name = "expected_timeout"; break; + case atf_result::failed: result_name = "failed"; break; + case atf_result::passed: result_name = "passed"; break; + case atf_result::skipped: result_name = "skipped"; break; + } + + const optional< int >& argument = object.argument(); + + const optional< std::string >& reason = object.reason(); + + output << F("model::test_result{type=%s, argument=%s, reason=%s}") + % text::quote(result_name, '\'') + % (argument ? (F("%s") % argument.get()).str() : "none") + % (reason ? text::quote(reason.get(), '\'') : "none"); + + return output; +} + + +/// Calculates the user-visible result of a test case. +/// +/// This function needs to perform magic to ensure that what the test case +/// reports as its result is what the user should really see: i.e. it adjusts +/// the reported status of the test to the exit conditions of its body and +/// cleanup parts. +/// +/// \param body_status The termination status of the process that executed +/// the body of the test. None if the body timed out. +/// \param results_file The path to the results file that the test case body is +/// supposed to have created. +/// +/// \return The calculated test case result. +model::test_result +engine::calculate_atf_result(const optional< process::status >& body_status, + const fs::path& results_file) +{ + using engine::atf_result; + + atf_result result(atf_result::broken, "Unknown result"); + try { + result = atf_result::load(results_file); + } catch (const engine::format_error& error) { + result = atf_result(atf_result::broken, error.what()); + } catch (const std::runtime_error& error) { + if (body_status) + result = atf_result( + atf_result::broken, F("Premature exit; test case %s") % + format_status(body_status.get())); + else { + // The test case timed out. apply() handles this case later. + } + } + + result = result.apply(body_status); + + return result.externalize(); +} diff --git a/contrib/kyua/engine/atf_result.hpp b/contrib/kyua/engine/atf_result.hpp new file mode 100644 index 000000000000..55f8a117a237 --- /dev/null +++ b/contrib/kyua/engine/atf_result.hpp @@ -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. + +/// \file engine/atf_result.hpp +/// Functions and types to process the results of ATF-based test cases. + +#if !defined(ENGINE_ATF_RESULT_HPP) +#define ENGINE_ATF_RESULT_HPP + +#include "engine/atf_result_fwd.hpp" + +#include +#include + +#include "model/test_result_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace engine { + + +/// Internal representation of the raw result files of ATF-based tests. +/// +/// This class is used exclusively to represent the transient result files read +/// from test cases before generating the "public" version of the result. This +/// class should actually not be exposed in the header files, but it is for +/// testing purposes only. +class atf_result { +public: + /// List of possible types for the test case result. + enum types { + broken, + expected_death, + expected_exit, + expected_failure, + expected_signal, + expected_timeout, + failed, + passed, + skipped, + }; + +private: + /// The test case result. + types _type; + + /// The optional integral argument that may accompany the result. + /// + /// Should only be present if the type is expected_exit or expected_signal. + utils::optional< int > _argument; + + /// A description of the test case result. + /// + /// Should always be present except for the passed type. + utils::optional< std::string > _reason; + +public: + atf_result(const types); + atf_result(const types, const std::string&); + atf_result(const types, const utils::optional< int >&, const std::string&); + + static atf_result parse(std::istream&); + static atf_result load(const utils::fs::path&); + + types type(void) const; + const utils::optional< int >& argument(void) const; + const utils::optional< std::string >& reason(void) const; + + bool good(void) const; + atf_result apply(const utils::optional< utils::process::status >&) const; + model::test_result externalize(void) const; + + bool operator==(const atf_result&) const; + bool operator!=(const atf_result&) const; +}; + + +std::ostream& operator<<(std::ostream&, const atf_result&); + + +model::test_result calculate_atf_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&); + + +} // namespace engine + +#endif // !defined(ENGINE_ATF_IFACE_RESULTS_HPP) diff --git a/contrib/kyua/engine/atf_result_fwd.hpp b/contrib/kyua/engine/atf_result_fwd.hpp new file mode 100644 index 000000000000..2a1440e4929c --- /dev/null +++ b/contrib/kyua/engine/atf_result_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 engine/atf_result_fwd.hpp +/// Forward declarations for engine/atf_result.hpp + +#if !defined(ENGINE_ATF_RESULT_FWD_HPP) +#define ENGINE_ATF_RESULT_FWD_HPP + +namespace engine { + + +class atf_result; + + +} // namespace engine + +#endif // !defined(ENGINE_ATF_RESULT_FWD_HPP) diff --git a/contrib/kyua/engine/atf_result_test.cpp b/contrib/kyua/engine/atf_result_test.cpp new file mode 100644 index 000000000000..8ec61dc3c07e --- /dev/null +++ b/contrib/kyua/engine/atf_result_test.cpp @@ -0,0 +1,788 @@ +// 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 "engine/atf_result.hpp" + +extern "C" { +#include +} + +#include +#include +#include +#include + +#include + +#include "engine/exceptions.hpp" +#include "model/test_result.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/status.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Performs a test for results::parse() that should succeed. +/// +/// \param exp_type The expected type of the result. +/// \param exp_argument The expected argument in the result, if any. +/// \param exp_reason The expected reason describing the result, if any. +/// \param text The literal input to parse; can include multiple lines. +static void +parse_ok_test(const engine::atf_result::types& exp_type, + const optional< int >& exp_argument, + const char* exp_reason, const char* text) +{ + std::istringstream input(text); + const engine::atf_result actual = engine::atf_result::parse(input); + ATF_REQUIRE(exp_type == actual.type()); + ATF_REQUIRE_EQ(exp_argument, actual.argument()); + if (exp_reason != NULL) { + ATF_REQUIRE(actual.reason()); + ATF_REQUIRE_EQ(exp_reason, actual.reason().get()); + } else { + ATF_REQUIRE(!actual.reason()); + } +} + + +/// Wrapper around parse_ok_test to define a test case. +/// +/// \param name The name of the test case; will be prefixed with +/// "atf_result__parse__". +/// \param exp_type The expected type of the result. +/// \param exp_argument The expected argument in the result, if any. +/// \param exp_reason The expected reason describing the result, if any. +/// \param input The literal input to parse. +#define PARSE_OK(name, exp_type, exp_argument, exp_reason, input) \ + ATF_TEST_CASE_WITHOUT_HEAD(atf_result__parse__ ## name); \ + ATF_TEST_CASE_BODY(atf_result__parse__ ## name) \ + { \ + parse_ok_test(exp_type, exp_argument, exp_reason, input); \ + } + + +/// Performs a test for results::parse() that should fail. +/// +/// \param reason_regexp The reason to match against the broken reason. +/// \param text The literal input to parse; can include multiple lines. +static void +parse_broken_test(const char* reason_regexp, const char* text) +{ + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, reason_regexp, + engine::atf_result::parse(input)); +} + + +/// Wrapper around parse_broken_test to define a test case. +/// +/// \param name The name of the test case; will be prefixed with +/// "atf_result__parse__". +/// \param reason_regexp The reason to match against the broken reason. +/// \param input The literal input to parse. +#define PARSE_BROKEN(name, reason_regexp, input) \ + ATF_TEST_CASE_WITHOUT_HEAD(atf_result__parse__ ## name); \ + ATF_TEST_CASE_BODY(atf_result__parse__ ## name) \ + { \ + parse_broken_test(reason_regexp, input); \ + } + + +} // anonymous namespace + + +PARSE_BROKEN(empty, + "Empty.*no new line", + ""); +PARSE_BROKEN(no_newline__unknown, + "Empty.*no new line", + "foo"); +PARSE_BROKEN(no_newline__known, + "Empty.*no new line", + "passed"); +PARSE_BROKEN(multiline__no_newline, + "multiple lines.*foo<>bar", + "failed: foo\nbar"); +PARSE_BROKEN(multiline__with_newline, + "multiple lines.*foo<>bar", + "failed: foo\nbar\n"); +PARSE_BROKEN(unknown_status__no_reason, + "Unknown.*result.*'cba'", + "cba\n"); +PARSE_BROKEN(unknown_status__with_reason, + "Unknown.*result.*'hgf'", + "hgf: foo\n"); +PARSE_BROKEN(missing_reason__no_delim, + "failed.*followed by.*reason", + "failed\n"); +PARSE_BROKEN(missing_reason__bad_delim, + "failed.*followed by.*reason", + "failed:\n"); +PARSE_BROKEN(missing_reason__empty, + "failed.*followed by.*reason", + "failed: \n"); + + +PARSE_OK(broken__ok, + engine::atf_result::broken, none, "a b c", + "broken: a b c\n"); +PARSE_OK(broken__blanks, + engine::atf_result::broken, none, " ", + "broken: \n"); + + +PARSE_OK(expected_death__ok, + engine::atf_result::expected_death, none, "a b c", + "expected_death: a b c\n"); +PARSE_OK(expected_death__blanks, + engine::atf_result::expected_death, none, " ", + "expected_death: \n"); + + +PARSE_OK(expected_exit__ok__any, + engine::atf_result::expected_exit, none, "any exit code", + "expected_exit: any exit code\n"); +PARSE_OK(expected_exit__ok__specific, + engine::atf_result::expected_exit, optional< int >(712), + "some known exit code", + "expected_exit(712): some known exit code\n"); +PARSE_BROKEN(expected_exit__bad_int, + "Invalid integer.*45a3", + "expected_exit(45a3): this is broken\n"); + + +PARSE_OK(expected_failure__ok, + engine::atf_result::expected_failure, none, "a b c", + "expected_failure: a b c\n"); +PARSE_OK(expected_failure__blanks, + engine::atf_result::expected_failure, none, " ", + "expected_failure: \n"); + + +PARSE_OK(expected_signal__ok__any, + engine::atf_result::expected_signal, none, "any signal code", + "expected_signal: any signal code\n"); +PARSE_OK(expected_signal__ok__specific, + engine::atf_result::expected_signal, optional< int >(712), + "some known signal code", + "expected_signal(712): some known signal code\n"); +PARSE_BROKEN(expected_signal__bad_int, + "Invalid integer.*45a3", + "expected_signal(45a3): this is broken\n"); + + +PARSE_OK(expected_timeout__ok, + engine::atf_result::expected_timeout, none, "a b c", + "expected_timeout: a b c\n"); +PARSE_OK(expected_timeout__blanks, + engine::atf_result::expected_timeout, none, " ", + "expected_timeout: \n"); + + +PARSE_OK(failed__ok, + engine::atf_result::failed, none, "a b c", + "failed: a b c\n"); +PARSE_OK(failed__blanks, + engine::atf_result::failed, none, " ", + "failed: \n"); + + +PARSE_OK(passed__ok, + engine::atf_result::passed, none, NULL, + "passed\n"); +PARSE_BROKEN(passed__reason, + "cannot have a reason", + "passed a b c\n"); + + +PARSE_OK(skipped__ok, + engine::atf_result::skipped, none, "a b c", + "skipped: a b c\n"); +PARSE_OK(skipped__blanks, + engine::atf_result::skipped, none, " ", + "skipped: \n"); + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__load__ok); +ATF_TEST_CASE_BODY(atf_result__load__ok) +{ + std::ofstream output("result.txt"); + ATF_REQUIRE(output); + output << "skipped: a b c\n"; + output.close(); + + const engine::atf_result result = engine::atf_result::load( + utils::fs::path("result.txt")); + ATF_REQUIRE(engine::atf_result::skipped == result.type()); + ATF_REQUIRE(!result.argument()); + ATF_REQUIRE(result.reason()); + ATF_REQUIRE_EQ("a b c", result.reason().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__load__missing_file); +ATF_TEST_CASE_BODY(atf_result__load__missing_file) +{ + ATF_REQUIRE_THROW_RE( + std::runtime_error, "Cannot open", + engine::atf_result::load(utils::fs::path("result.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__load__format_error); +ATF_TEST_CASE_BODY(atf_result__load__format_error) +{ + std::ofstream output("abc.txt"); + ATF_REQUIRE(output); + output << "passed: foo\n"; + output.close(); + + ATF_REQUIRE_THROW_RE(engine::format_error, "cannot have a reason", + engine::atf_result::load(utils::fs::path("abc.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__broken__ok); +ATF_TEST_CASE_BODY(atf_result__apply__broken__ok) +{ + const engine::atf_result in_result(engine::atf_result::broken, + "Passthrough"); + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + ATF_REQUIRE_EQ(in_result, in_result.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__timed_out); +ATF_TEST_CASE_BODY(atf_result__apply__timed_out) +{ + const engine::atf_result timed_out(engine::atf_result::broken, + "Some arbitrary error"); + ATF_REQUIRE_EQ(engine::atf_result(engine::atf_result::broken, + "Test case body timed out"), + timed_out.apply(none)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_death__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_death__ok) +{ + const engine::atf_result in_result(engine::atf_result::expected_death, + "Passthrough"); + const process::status status = process::status::fake_signaled(SIGINT, true); + ATF_REQUIRE_EQ(in_result, in_result.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_exit__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_exit__ok) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + + const engine::atf_result any_code(engine::atf_result::expected_exit, none, + "The reason"); + ATF_REQUIRE_EQ(any_code, any_code.apply(utils::make_optional(success))); + ATF_REQUIRE_EQ(any_code, any_code.apply(utils::make_optional(failure))); + + const engine::atf_result a_code(engine::atf_result::expected_exit, + utils::make_optional(EXIT_FAILURE), "The reason"); + ATF_REQUIRE_EQ(a_code, a_code.apply(utils::make_optional(failure))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_exit__failed); +ATF_TEST_CASE_BODY(atf_result__apply__expected_exit__failed) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + + const engine::atf_result a_code(engine::atf_result::expected_exit, + utils::make_optional(EXIT_FAILURE), "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::failed, + "Test case expected to exit with code 1 but got " + "code 0"), + a_code.apply(utils::make_optional(success))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_exit__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_exit__broken) +{ + const process::status sig3 = process::status::fake_signaled(3, false); + + const engine::atf_result any_code(engine::atf_result::expected_exit, none, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected clean exit but received signal 3"), + any_code.apply(utils::make_optional(sig3))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_failure__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_failure__ok) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result xfailure(engine::atf_result::expected_failure, + "The reason"); + ATF_REQUIRE_EQ(xfailure, xfailure.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_failure__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_failure__broken) +{ + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result xfailure(engine::atf_result::expected_failure, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected failure should have reported success but " + "exited with code 1"), + xfailure.apply(utils::make_optional(failure))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected failure should have reported success but " + "received signal 3 (core dumped)"), + xfailure.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected failure should have reported success but " + "received signal 4"), + xfailure.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_signal__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_signal__ok) +{ + const process::status sig1 = process::status::fake_signaled(1, false); + const process::status sig3 = process::status::fake_signaled(3, true); + + const engine::atf_result any_sig(engine::atf_result::expected_signal, none, + "The reason"); + ATF_REQUIRE_EQ(any_sig, any_sig.apply(utils::make_optional(sig1))); + ATF_REQUIRE_EQ(any_sig, any_sig.apply(utils::make_optional(sig3))); + + const engine::atf_result a_sig(engine::atf_result::expected_signal, + utils::make_optional(3), "The reason"); + ATF_REQUIRE_EQ(a_sig, a_sig.apply(utils::make_optional(sig3))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_signal__failed); +ATF_TEST_CASE_BODY(atf_result__apply__expected_signal__failed) +{ + const process::status sig5 = process::status::fake_signaled(5, false); + + const engine::atf_result a_sig(engine::atf_result::expected_signal, + utils::make_optional(4), "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::failed, + "Test case expected to receive signal 4 but got 5"), + a_sig.apply(utils::make_optional(sig5))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_signal__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_signal__broken) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + + const engine::atf_result any_sig(engine::atf_result::expected_signal, none, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected signal but exited with code 0"), + any_sig.apply(utils::make_optional(success))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_timeout__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_timeout__ok) +{ + const engine::atf_result timeout(engine::atf_result::expected_timeout, + "The reason"); + ATF_REQUIRE_EQ(timeout, timeout.apply(none)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_timeout__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_timeout__broken) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result timeout(engine::atf_result::expected_timeout, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected timeout but exited with code 0"), + timeout.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__failed__ok); +ATF_TEST_CASE_BODY(atf_result__apply__failed__ok) +{ + const process::status status = process::status::fake_exited(EXIT_FAILURE); + const engine::atf_result failed(engine::atf_result::failed, "The reason"); + ATF_REQUIRE_EQ(failed, failed.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__failed__broken); +ATF_TEST_CASE_BODY(atf_result__apply__failed__broken) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result failed(engine::atf_result::failed, "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Failed test case should have reported failure but " + "exited with code 0"), + failed.apply(utils::make_optional(success))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Failed test case should have reported failure but " + "received signal 3 (core dumped)"), + failed.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Failed test case should have reported failure but " + "received signal 4"), + failed.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__passed__ok); +ATF_TEST_CASE_BODY(atf_result__apply__passed__ok) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result passed(engine::atf_result::passed); + ATF_REQUIRE_EQ(passed, passed.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__passed__broken); +ATF_TEST_CASE_BODY(atf_result__apply__passed__broken) +{ + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result passed(engine::atf_result::passed); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Passed test case should have reported success but " + "exited with code 1"), + passed.apply(utils::make_optional(failure))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Passed test case should have reported success but " + "received signal 3 (core dumped)"), + passed.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Passed test case should have reported success but " + "received signal 4"), + passed.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__skipped__ok); +ATF_TEST_CASE_BODY(atf_result__apply__skipped__ok) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result skipped(engine::atf_result::skipped, "The reason"); + ATF_REQUIRE_EQ(skipped, skipped.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__skipped__broken); +ATF_TEST_CASE_BODY(atf_result__apply__skipped__broken) +{ + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result skipped(engine::atf_result::skipped, "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Skipped test case should have reported success but " + "exited with code 1"), + skipped.apply(utils::make_optional(failure))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Skipped test case should have reported success but " + "received signal 3 (core dumped)"), + skipped.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Skipped test case should have reported success but " + "received signal 4"), + skipped.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__broken); +ATF_TEST_CASE_BODY(atf_result__externalize__broken) +{ + const engine::atf_result raw(engine::atf_result::broken, "The reason"); + const model::test_result expected(model::test_result_broken, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_death); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_death) +{ + const engine::atf_result raw(engine::atf_result::expected_death, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_exit); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_exit) +{ + const engine::atf_result raw(engine::atf_result::expected_exit, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_failure); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_failure) +{ + const engine::atf_result raw(engine::atf_result::expected_failure, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_signal); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_signal) +{ + const engine::atf_result raw(engine::atf_result::expected_signal, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_timeout); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_timeout) +{ + const engine::atf_result raw(engine::atf_result::expected_timeout, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__failed); +ATF_TEST_CASE_BODY(atf_result__externalize__failed) +{ + const engine::atf_result raw(engine::atf_result::failed, "The reason"); + const model::test_result expected(model::test_result_failed, + "The reason"); + ATF_REQUIRE(expected == raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__passed); +ATF_TEST_CASE_BODY(atf_result__externalize__passed) +{ + const engine::atf_result raw(engine::atf_result::passed); + const model::test_result expected(model::test_result_passed); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__skipped); +ATF_TEST_CASE_BODY(atf_result__externalize__skipped) +{ + const engine::atf_result raw(engine::atf_result::skipped, "The reason"); + const model::test_result expected(model::test_result_skipped, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__missing_file); +ATF_TEST_CASE_BODY(calculate_atf_result__missing_file) +{ + using process::status; + + const status body_status = status::fake_exited(EXIT_SUCCESS); + const model::test_result expected( + model::test_result_broken, + "Premature exit; test case exited with code 0"); + ATF_REQUIRE_EQ(expected, engine::calculate_atf_result( + utils::make_optional(body_status), fs::path("foo"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__bad_file); +ATF_TEST_CASE_BODY(calculate_atf_result__bad_file) +{ + using process::status; + + const status body_status = status::fake_exited(EXIT_SUCCESS); + atf::utils::create_file("foo", "invalid\n"); + const model::test_result expected(model::test_result_broken, + "Unknown test result 'invalid'"); + ATF_REQUIRE_EQ(expected, engine::calculate_atf_result( + utils::make_optional(body_status), fs::path("foo"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__body_ok); +ATF_TEST_CASE_BODY(calculate_atf_result__body_ok) +{ + using process::status; + + atf::utils::create_file("result.txt", "skipped: Something\n"); + const status body_status = status::fake_exited(EXIT_SUCCESS); + ATF_REQUIRE_EQ( + model::test_result(model::test_result_skipped, "Something"), + engine::calculate_atf_result(utils::make_optional(body_status), + fs::path("result.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__body_bad); +ATF_TEST_CASE_BODY(calculate_atf_result__body_bad) +{ + using process::status; + + atf::utils::create_file("result.txt", "skipped: Something\n"); + const status body_status = status::fake_exited(EXIT_FAILURE); + ATF_REQUIRE_EQ( + model::test_result(model::test_result_broken, "Skipped test case " + "should have reported success but exited with " + "code 1"), + engine::calculate_atf_result(utils::make_optional(body_status), + fs::path("result.txt"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, atf_result__parse__empty); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__no_newline__unknown); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__no_newline__known); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__multiline__no_newline); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__multiline__with_newline); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__unknown_status__no_reason); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__unknown_status__with_reason); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__missing_reason__no_delim); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__missing_reason__bad_delim); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__missing_reason__empty); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__broken__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__broken__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_death__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_death__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_exit__ok__any); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_exit__ok__specific); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_exit__bad_int); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_failure__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_failure__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_signal__ok__any); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_signal__ok__specific); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_signal__bad_int); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_timeout__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_timeout__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__failed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__failed__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__passed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__passed__reason); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__skipped__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__skipped__blanks); + + ATF_ADD_TEST_CASE(tcs, atf_result__load__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__load__missing_file); + ATF_ADD_TEST_CASE(tcs, atf_result__load__format_error); + + ATF_ADD_TEST_CASE(tcs, atf_result__apply__broken__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__timed_out); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_death__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_exit__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_exit__failed); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_exit__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_failure__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_failure__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_signal__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_signal__failed); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_signal__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_timeout__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_timeout__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__failed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__failed__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__passed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__passed__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__skipped__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__skipped__broken); + + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_death); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_exit); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_failure); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_signal); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_timeout); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__failed); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__passed); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__skipped); + + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__missing_file); + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__bad_file); + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__body_ok); + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__body_bad); +} diff --git a/contrib/kyua/engine/atf_test.cpp b/contrib/kyua/engine/atf_test.cpp new file mode 100644 index 000000000000..9fe7797f4362 --- /dev/null +++ b/contrib/kyua/engine/atf_test.cpp @@ -0,0 +1,450 @@ +// 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 "engine/atf.hpp" + +extern "C" { +#include + +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.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/stacktrace.hpp" +#include "utils/test_utils.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Lists the test cases associated with an ATF test program. +/// +/// \param program_name Basename of the test program to run. +/// \param root Path to the base of the test suite. +/// \param names_filter Whitespace-separated list of test cases that the helper +/// test program is allowed to expose. +/// \param user_config User-provided configuration. +/// +/// \return The list of loaded test cases. +static model::test_cases_map +list_one(const char* program_name, + const fs::path& root, + const char* names_filter = NULL, + config::tree user_config = engine::empty_config()) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const scheduler::lazy_test_program program( + "atf", fs::path(program_name), root, "the-suite", + model::metadata_builder().build(), user_config, handle); + + if (names_filter != NULL) + utils::setenv("TEST_CASES", names_filter); + const model::test_cases_map test_cases = handle.list_tests( + &program, user_config); + + handle.cleanup(); + + return test_cases; +} + + +/// Runs a bogus test program and checks the error result. +/// +/// \param exp_error Expected error string to find. +/// \param program_name Basename of the test program to run. +/// \param root Path to the base of the test suite. +/// \param names_filter Whitespace-separated list of test cases that the helper +/// test program is allowed to expose. +static void +check_list_one_fail(const char* exp_error, + const char* program_name, + const fs::path& root, + const char* names_filter = NULL) +{ + const model::test_cases_map test_cases = list_one( + program_name, root, names_filter); + + ATF_REQUIRE_EQ(1, test_cases.size()); + const model::test_case& test_case = test_cases.begin()->second; + ATF_REQUIRE_EQ("__test_cases_list__", test_case.name()); + ATF_REQUIRE(test_case.fake_result()); + ATF_REQUIRE_MATCH(exp_error, + test_case.fake_result().get().reason()); +} + + +/// Runs one ATF test program and checks its result. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param test_case_name Name of the "test case" to select from the helper +/// program. +/// \param exp_result The expected result. +/// \param user_config User-provided configuration. +/// \param check_empty_output If true, verify that the output of the test is +/// silent. This is just a hack to implement one of the test cases; we'd +/// easily have a nicer abstraction here... +static void +run_one(const atf::tests::tc* tc, const char* test_case_name, + const model::test_result& exp_result, + config::tree user_config = engine::empty_config(), + const bool check_empty_output = false) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const model::test_program_ptr program(new scheduler::lazy_test_program( + "atf", fs::path("atf_helpers"), fs::path(tc->get_config_var("srcdir")), + "the-suite", model::metadata_builder().build(), + user_config, handle)); + + (void)handle.spawn_test(program, test_case_name, user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + atf::utils::cat_file(result_handle->stdout_file().str(), "stdout: "); + atf::utils::cat_file(result_handle->stderr_file().str(), "stderr: "); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + if (check_empty_output) { + ATF_REQUIRE(atf::utils::compare_file(result_handle->stdout_file().str(), + "")); + ATF_REQUIRE(atf::utils::compare_file(result_handle->stderr_file().str(), + "")); + } + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(list__ok); +ATF_TEST_CASE_BODY(list__ok) +{ + const model::test_cases_map test_cases = list_one( + "atf_helpers", fs::path(get_config_var("srcdir")), "pass crash"); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("crash") + .add("pass", model::metadata_builder() + .set_description("Always-passing test case") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__configuration_variables); +ATF_TEST_CASE_BODY(list__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.var1", "value1"); + user_config.set_string("test_suites.the-suite.var2", "value2"); + + const model::test_cases_map test_cases = list_one( + "atf_helpers", fs::path(get_config_var("srcdir")), "check_list_config", + user_config); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("check_list_config", model::metadata_builder() + .set_description("Found: var1=value1 var2=value2") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__current_directory); +ATF_TEST_CASE_BODY(list__current_directory) +{ + const fs::path helpers = fs::path(get_config_var("srcdir")) / "atf_helpers"; + ATF_REQUIRE(::symlink(helpers.c_str(), "atf_helpers") != -1); + const model::test_cases_map test_cases = list_one( + "atf_helpers", fs::path("."), "pass"); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("pass", model::metadata_builder() + .set_description("Always-passing test case") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__relative_path); +ATF_TEST_CASE_BODY(list__relative_path) +{ + const fs::path helpers = fs::path(get_config_var("srcdir")) / "atf_helpers"; + ATF_REQUIRE(::mkdir("dir1", 0755) != -1); + ATF_REQUIRE(::mkdir("dir1/dir2", 0755) != -1); + ATF_REQUIRE(::symlink(helpers.c_str(), "dir1/dir2/atf_helpers") != -1); + const model::test_cases_map test_cases = list_one( + "dir2/atf_helpers", fs::path("dir1"), "pass"); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("pass", model::metadata_builder() + .set_description("Always-passing test case") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__missing_test_program); +ATF_TEST_CASE_BODY(list__missing_test_program) +{ + check_list_one_fail("Cannot find test program", "non-existent", + fs::current_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__not_a_test_program); +ATF_TEST_CASE_BODY(list__not_a_test_program) +{ + atf::utils::create_file("not-valid", "garbage\n"); + ATF_REQUIRE(::chmod("not-valid", 0755) != -1); + check_list_one_fail("Invalid test program format", "not-valid", + fs::current_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__no_permissions); +ATF_TEST_CASE_BODY(list__no_permissions) +{ + atf::utils::create_file("not-executable", "garbage\n"); + check_list_one_fail("Permission denied to run test program", + "not-executable", fs::current_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__abort); +ATF_TEST_CASE_BODY(list__abort) +{ + check_list_one_fail("Test program received signal", "atf_helpers", + fs::path(get_config_var("srcdir")), + "crash_head"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__empty); +ATF_TEST_CASE_BODY(list__empty) +{ + check_list_one_fail("No test cases", "atf_helpers", + fs::path(get_config_var("srcdir")), + ""); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__stderr_not_quiet); +ATF_TEST_CASE_BODY(list__stderr_not_quiet) +{ + check_list_one_fail("Test case list wrote to stderr", "atf_helpers", + fs::path(get_config_var("srcdir")), + "output_in_list"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__passes); +ATF_TEST_CASE_BODY(test__body_only__passes) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__crashes); +ATF_TEST_CASE_BODY(test__body_only__crashes) +{ + utils::prepare_coredump_test(this); + + const model::test_result exp_result( + model::test_result_broken, + F("Premature exit; test case received signal %s (core dumped)") % + SIGABRT); + run_one(this, "crash", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__times_out); +ATF_TEST_CASE_BODY(test__body_only__times_out) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + user_config.set_string("test_suites.the-suite.timeout", "1"); + + const model::test_result exp_result( + model::test_result_broken, "Test case body timed out"); + run_one(this, "timeout_body", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__configuration_variables); +ATF_TEST_CASE_BODY(test__body_only__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.first", "some value"); + user_config.set_string("test_suites.the-suite.second", "some other value"); + + const model::test_result exp_result(model::test_result_passed); + run_one(this, "check_configuration_variables", exp_result, user_config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__no_atf_run_warning); +ATF_TEST_CASE_BODY(test__body_only__no_atf_run_warning) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result, engine::empty_config(), true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__body_times_out); +ATF_TEST_CASE_BODY(test__body_and_cleanup__body_times_out) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + user_config.set_string("test_suites.the-suite.timeout", "1"); + + const model::test_result exp_result( + model::test_result_broken, "Test case body timed out"); + run_one(this, "timeout_body", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); + ATF_REQUIRE(atf::utils::file_exists("cookie.cleanup")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__cleanup_crashes); +ATF_TEST_CASE_BODY(test__body_and_cleanup__cleanup_crashes) +{ + const model::test_result exp_result( + model::test_result_broken, + "Test case cleanup did not terminate successfully"); + run_one(this, "crash_cleanup", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__cleanup_times_out); +ATF_TEST_CASE_BODY(test__body_and_cleanup__cleanup_times_out) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + + scheduler::cleanup_timeout = datetime::delta(1, 0); + const model::test_result exp_result( + model::test_result_broken, "Test case cleanup timed out"); + run_one(this, "timeout_cleanup", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__expect_timeout); +ATF_TEST_CASE_BODY(test__body_and_cleanup__expect_timeout) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + user_config.set_string("test_suites.the-suite.timeout", "1"); + + const model::test_result exp_result( + model::test_result_expected_failure, "Times out on purpose"); + run_one(this, "expect_timeout", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); + ATF_REQUIRE(atf::utils::file_exists("cookie.cleanup")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__shared_workdir); +ATF_TEST_CASE_BODY(test__body_and_cleanup__shared_workdir) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "shared_workdir", exp_result); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + + ATF_ADD_TEST_CASE(tcs, list__ok); + ATF_ADD_TEST_CASE(tcs, list__configuration_variables); + ATF_ADD_TEST_CASE(tcs, list__current_directory); + ATF_ADD_TEST_CASE(tcs, list__relative_path); + ATF_ADD_TEST_CASE(tcs, list__missing_test_program); + ATF_ADD_TEST_CASE(tcs, list__not_a_test_program); + ATF_ADD_TEST_CASE(tcs, list__no_permissions); + ATF_ADD_TEST_CASE(tcs, list__abort); + ATF_ADD_TEST_CASE(tcs, list__empty); + ATF_ADD_TEST_CASE(tcs, list__stderr_not_quiet); + + ATF_ADD_TEST_CASE(tcs, test__body_only__passes); + ATF_ADD_TEST_CASE(tcs, test__body_only__crashes); + ATF_ADD_TEST_CASE(tcs, test__body_only__times_out); + ATF_ADD_TEST_CASE(tcs, test__body_only__configuration_variables); + ATF_ADD_TEST_CASE(tcs, test__body_only__no_atf_run_warning); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__body_times_out); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__cleanup_crashes); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__cleanup_times_out); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__expect_timeout); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__shared_workdir); +} diff --git a/contrib/kyua/engine/config.cpp b/contrib/kyua/engine/config.cpp new file mode 100644 index 000000000000..3f162a94fbb5 --- /dev/null +++ b/contrib/kyua/engine/config.cpp @@ -0,0 +1,254 @@ +// 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 "engine/config.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include + +#include "engine/exceptions.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/config/parser.hpp" +#include "utils/config/tree.ipp" +#include "utils/passwd.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace text = utils::text; + + +namespace { + + +/// Defines the schema of a configuration tree. +/// +/// \param [in,out] tree The tree to populate. The tree should be empty on +/// entry to prevent collisions with the keys defined in here. +static void +init_tree(config::tree& tree) +{ + tree.define< config::string_node >("architecture"); + tree.define< config::positive_int_node >("parallelism"); + tree.define< config::string_node >("platform"); + tree.define< engine::user_node >("unprivileged_user"); + tree.define_dynamic("test_suites"); +} + + +/// Fills in a configuration tree with default values. +/// +/// \param [in,out] tree The tree to populate. init_tree() must have been +/// called on it beforehand. +static void +set_defaults(config::tree& tree) +{ + tree.set< config::string_node >("architecture", KYUA_ARCHITECTURE); + // TODO(jmmv): Automatically derive this from the number of CPUs in the + // machine and forcibly set to a value greater than 1. Still testing + // the new parallel implementation as of 2015-02-27 though. + tree.set< config::positive_int_node >("parallelism", 1); + tree.set< config::string_node >("platform", KYUA_PLATFORM); +} + + +/// Configuration parser specialization for Kyua configuration files. +class config_parser : public config::parser { + /// Initializes the configuration tree. + /// + /// This is a callback executed when the configuration script invokes the + /// syntax() method. We populate the configuration tree from here with the + /// schema version requested by the file. + /// + /// \param [in,out] tree The tree to populate. + /// \param syntax_version The version of the file format as specified in the + /// configuration file. + /// + /// \throw config::syntax_error If the syntax_format/syntax_version + /// combination is not supported. + void + setup(config::tree& tree, const int syntax_version) + { + if (syntax_version < 1 || syntax_version > 2) + throw config::syntax_error(F("Unsupported config version %s") % + syntax_version); + + init_tree(tree); + set_defaults(tree); + } + +public: + /// Initializes the parser. + /// + /// \param [out] tree_ The tree in which the results of the parsing will be + /// stored when parse() is called. Should be empty on entry. Because + /// we grab a reference to this object, the tree must remain valid for + /// the existence of the parser object. + explicit config_parser(config::tree& tree_) : + config::parser(tree_) + { + } +}; + + +} // anonymous namespace + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +engine::user_node::deep_copy(void) const +{ + std::auto_ptr< user_node > new_node(new user_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 +engine::user_node::push_lua(lutok::state& state) const +{ + state.push_string(value().name); +} + + +/// 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 +engine::user_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_number(value_index)) { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_uid(state.to_integer(-1))); + } else if (state.is_string(value_index)) { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_name(state.to_string(-1))); + } else + throw config::value_error("Invalid user identifier"); +} + + +/// 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. +void +engine::user_node::set_string(const std::string& raw_value) +{ + try { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_name(raw_value)); + } catch (const std::runtime_error& e) { + int uid; + try { + uid = text::to_type< int >(raw_value); + } catch (const text::value_error& e2) { + throw error(F("Cannot find user with name '%s'") % raw_value); + } + + try { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_uid(uid)); + } catch (const std::runtime_error& e2) { + throw error(F("Cannot find user with UID %s") % uid); + } + } +} + + +/// 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. +std::string +engine::user_node::to_string(void) const +{ + return config::typed_leaf_node< passwd::user >::value().name; +} + + +/// Constructs a config with the built-in settings. +/// +/// \return A default test suite configuration. +config::tree +engine::default_config(void) +{ + config::tree tree(false); + init_tree(tree); + set_defaults(tree); + return tree; +} + + +/// Constructs a config with the built-in settings. +/// +/// \return An empty test suite configuration. +config::tree +engine::empty_config(void) +{ + config::tree tree(false); + init_tree(tree); + return tree; +} + + +/// Parses a test suite configuration file. +/// +/// \param file The file to parse. +/// +/// \return High-level representation of the configuration file. +/// +/// \throw load_error If there is any problem loading the file. This includes +/// file access errors and syntax errors. +config::tree +engine::load_config(const utils::fs::path& file) +{ + config::tree tree(false); + try { + config_parser(tree).parse(file); + } catch (const config::error& e) { + throw load_error(file, e.what()); + } + return tree; +} diff --git a/contrib/kyua/engine/config.hpp b/contrib/kyua/engine/config.hpp new file mode 100644 index 000000000000..2c1b83481862 --- /dev/null +++ b/contrib/kyua/engine/config.hpp @@ -0,0 +1,65 @@ +// 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 engine/config.hpp +/// Test suite configuration parsing and representation. + +#if !defined(ENGINE_CONFIG_HPP) +#define ENGINE_CONFIG_HPP + +#include "engine/config_fwd.hpp" + +#include "utils/config/nodes.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/passwd_fwd.hpp" + +namespace engine { + + +/// Tree node to hold a system user identifier. +class user_node : public utils::config::typed_leaf_node< utils::passwd::user > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); + + void set_string(const std::string&); + std::string to_string(void) const; +}; + + +utils::config::tree default_config(void); +utils::config::tree empty_config(void); +utils::config::tree load_config(const utils::fs::path&); + + +} // namespace engine + +#endif // !defined(ENGINE_CONFIG_HPP) diff --git a/contrib/kyua/engine/config_fwd.hpp b/contrib/kyua/engine/config_fwd.hpp new file mode 100644 index 000000000000..82da9b1382bd --- /dev/null +++ b/contrib/kyua/engine/config_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 engine/config_fwd.hpp +/// Forward declarations for engine/config.hpp + +#if !defined(ENGINE_CONFIG_FWD_HPP) +#define ENGINE_CONFIG_FWD_HPP + +namespace engine { + + +class user_node; + + +} // namespace engine + +#endif // !defined(ENGINE_CONFIG_FWD_HPP) diff --git a/contrib/kyua/engine/config_test.cpp b/contrib/kyua/engine/config_test.cpp new file mode 100644 index 000000000000..e4eb27421078 --- /dev/null +++ b/contrib/kyua/engine/config_test.cpp @@ -0,0 +1,203 @@ +// 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 "engine/config.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include +#include + +#include + +#include "engine/exceptions.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/config/tree.ipp" +#include "utils/passwd.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Replaces the system user database with a fake one for testing purposes. +static void +set_mock_users(void) +{ + std::vector< passwd::user > users; + users.push_back(passwd::user("user1", 100, 150)); + users.push_back(passwd::user("user2", 200, 250)); + passwd::set_mock_users_for_testing(users); +} + + +/// Checks that the default values of a config object match our expectations. +/// +/// This fails the test case if any field of the input config object is not +/// what we expect. +/// +/// \param config The configuration to validate. +static void +validate_defaults(const config::tree& config) +{ + ATF_REQUIRE_EQ( + KYUA_ARCHITECTURE, + config.lookup< config::string_node >("architecture")); + + ATF_REQUIRE_EQ( + 1, + config.lookup< config::positive_int_node >("parallelism")); + + ATF_REQUIRE_EQ( + KYUA_PLATFORM, + config.lookup< config::string_node >("platform")); + + ATF_REQUIRE(!config.is_set("unprivileged_user")); + + ATF_REQUIRE(config.all_properties("test_suites").empty()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(config__defaults); +ATF_TEST_CASE_BODY(config__defaults) +{ + const config::tree user_config = engine::default_config(); + validate_defaults(user_config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__set__parallelism); +ATF_TEST_CASE_BODY(config__set__parallelism) +{ + config::tree user_config = engine::default_config(); + user_config.set_string("parallelism", "8"); + ATF_REQUIRE_THROW_RE( + config::error, "parallelism.*Must be a positive integer", + user_config.set_string("parallelism", "0")); + ATF_REQUIRE_THROW_RE( + config::error, "parallelism.*Must be a positive integer", + user_config.set_string("parallelism", "-1")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__defaults); +ATF_TEST_CASE_BODY(config__load__defaults) +{ + atf::utils::create_file("config", "syntax(2)\n"); + + const config::tree user_config = engine::load_config(fs::path("config")); + validate_defaults(user_config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__overrides); +ATF_TEST_CASE_BODY(config__load__overrides) +{ + set_mock_users(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "architecture = 'test-architecture'\n" + "parallelism = 16\n" + "platform = 'test-platform'\n" + "unprivileged_user = 'user2'\n" + "test_suites.mysuite.myvar = 'myvalue'\n"); + + const config::tree user_config = engine::load_config(fs::path("config")); + + ATF_REQUIRE_EQ("test-architecture", + user_config.lookup_string("architecture")); + ATF_REQUIRE_EQ("16", + user_config.lookup_string("parallelism")); + ATF_REQUIRE_EQ("test-platform", + user_config.lookup_string("platform")); + + const passwd::user& user = user_config.lookup< engine::user_node >( + "unprivileged_user"); + ATF_REQUIRE_EQ("user2", user.name); + ATF_REQUIRE_EQ(200, user.uid); + + config::properties_map exp_test_suites; + exp_test_suites["test_suites.mysuite.myvar"] = "myvalue"; + + ATF_REQUIRE(exp_test_suites == user_config.all_properties("test_suites")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__lua_error); +ATF_TEST_CASE_BODY(config__load__lua_error) +{ + atf::utils::create_file("config", "this syntax is invalid\n"); + + ATF_REQUIRE_THROW(engine::load_error, engine::load_config( + fs::path("config"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__bad_syntax__version); +ATF_TEST_CASE_BODY(config__load__bad_syntax__version) +{ + atf::utils::create_file("config", "syntax(123)\n"); + + ATF_REQUIRE_THROW_RE(engine::load_error, + "Unsupported config version 123", + engine::load_config(fs::path("config"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__missing_file); +ATF_TEST_CASE_BODY(config__load__missing_file) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, "Load of 'missing' failed", + engine::load_config(fs::path("missing"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, config__defaults); + ATF_ADD_TEST_CASE(tcs, config__set__parallelism); + ATF_ADD_TEST_CASE(tcs, config__load__defaults); + ATF_ADD_TEST_CASE(tcs, config__load__overrides); + ATF_ADD_TEST_CASE(tcs, config__load__lua_error); + ATF_ADD_TEST_CASE(tcs, config__load__bad_syntax__version); + ATF_ADD_TEST_CASE(tcs, config__load__missing_file); +} diff --git a/contrib/kyua/engine/exceptions.cpp b/contrib/kyua/engine/exceptions.cpp new file mode 100644 index 000000000000..98a7b43a7de3 --- /dev/null +++ b/contrib/kyua/engine/exceptions.cpp @@ -0,0 +1,81 @@ +// 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 "engine/exceptions.hpp" + +#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. +engine::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +engine::error::~error(void) throw() +{ +} + + +/// Constructs a new format_error. +/// +/// \param reason_ Description of the format problem. +engine::format_error::format_error(const std::string& reason_) : + error(reason_) +{ +} + + +/// Destructor for the error. +engine::format_error::~format_error(void) throw() +{ +} + + +/// Constructs a new load_error. +/// +/// \param file_ The file in which the error was encountered. +/// \param reason_ Description of the load problem. +engine::load_error::load_error(const fs::path& file_, + const std::string& reason_) : + error(F("Load of '%s' failed: %s") % file_ % reason_), + file(file_), + reason(reason_) +{ +} + + +/// Destructor for the error. +engine::load_error::~load_error(void) throw() +{ +} diff --git a/contrib/kyua/engine/exceptions.hpp b/contrib/kyua/engine/exceptions.hpp new file mode 100644 index 000000000000..fccb04f1aff2 --- /dev/null +++ b/contrib/kyua/engine/exceptions.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 engine/exceptions.hpp +/// Exception types raised by the engine module. + +#if !defined(ENGINE_EXCEPTIONS_HPP) +#define ENGINE_EXCEPTIONS_HPP + +#include + +#include "utils/fs/path.hpp" + +namespace engine { + + +/// Base exception for engine errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error while processing data. +class format_error : public error { +public: + explicit format_error(const std::string&); + virtual ~format_error(void) throw(); +}; + + +/// Error while parsing external data. +class load_error : public error { +public: + /// The path to the file that caused the load error. + utils::fs::path file; + + /// The reason for the error; may not include the file name. + std::string reason; + + explicit load_error(const utils::fs::path&, const std::string&); + virtual ~load_error(void) throw(); +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_EXCEPTIONS_HPP) diff --git a/contrib/kyua/engine/exceptions_test.cpp b/contrib/kyua/engine/exceptions_test.cpp new file mode 100644 index 000000000000..16e7c9f33d16 --- /dev/null +++ b/contrib/kyua/engine/exceptions_test.cpp @@ -0,0 +1,69 @@ +// 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 "engine/exceptions.hpp" + +#include + +#include + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const engine::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_error); +ATF_TEST_CASE_BODY(format_error) +{ + const engine::format_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_error); +ATF_TEST_CASE_BODY(load_error) +{ + const engine::load_error e(fs::path("/my/file"), "foo"); + ATF_REQUIRE_EQ(fs::path("/my/file"), e.file); + ATF_REQUIRE_EQ("foo", e.reason); + ATF_REQUIRE(std::strcmp("Load of '/my/file' failed: foo", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, format_error); + ATF_ADD_TEST_CASE(tcs, load_error); +} diff --git a/contrib/kyua/engine/filters.cpp b/contrib/kyua/engine/filters.cpp new file mode 100644 index 000000000000..753e64ae05f8 --- /dev/null +++ b/contrib/kyua/engine/filters.cpp @@ -0,0 +1,389 @@ +// 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 "engine/filters.hpp" + +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + +using utils::none; +using utils::optional; + + +/// Constructs a filter. +/// +/// \param test_program_ The name of the test program or of the subdirectory to +/// match. +/// \param test_case_ The name of the test case to match. +engine::test_filter::test_filter(const fs::path& test_program_, + const std::string& test_case_) : + test_program(test_program_), + test_case(test_case_) +{ +} + + +/// Parses a user-provided test filter. +/// +/// \param str The user-provided string representing a filter for tests. Must +/// be of the form <test_program%gt;[:<test_case%gt;]. +/// +/// \return The parsed filter. +/// +/// \throw std::runtime_error If the provided filter is invalid. +engine::test_filter +engine::test_filter::parse(const std::string& str) +{ + if (str.empty()) + throw std::runtime_error("Test filter cannot be empty"); + + const std::string::size_type pos = str.find(':'); + if (pos == 0) + throw std::runtime_error(F("Program name component in '%s' is empty") + % str); + if (pos == str.length() - 1) + throw std::runtime_error(F("Test case component in '%s' is empty") + % str); + + try { + const fs::path test_program_(str.substr(0, pos)); + if (test_program_.is_absolute()) + throw std::runtime_error(F("Program name '%s' must be relative " + "to the test suite, not absolute") % + test_program_.str()); + if (pos == std::string::npos) { + LD(F("Parsed user filter '%s': test program '%s', no test case") % + str % test_program_.str()); + return test_filter(test_program_, ""); + } else { + const std::string test_case_(str.substr(pos + 1)); + LD(F("Parsed user filter '%s': test program '%s', test case '%s'") % + str % test_program_.str() % test_case_); + return test_filter(test_program_, test_case_); + } + } catch (const fs::error& e) { + throw std::runtime_error(F("Invalid path in filter '%s': %s") % str % + e.what()); + } +} + + +/// Formats a filter for user presentation. +/// +/// \return A user-friendly string representing the filter. Note that this does +/// not necessarily match the string the user provided: in particular, the path +/// may have been internally normalized. +std::string +engine::test_filter::str(void) const +{ + if (!test_case.empty()) + return F("%s:%s") % test_program % test_case; + else + return test_program.str(); +} + + +/// Checks if this filter contains another. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter contains the other filter or if they are equal. +bool +engine::test_filter::contains(const test_filter& other) const +{ + if (*this == other) + return true; + else + return test_case.empty() && test_program.is_parent_of( + other.test_program); +} + + +/// Checks if this filter matches a given test program name or subdirectory. +/// +/// \param test_program_ The test program to compare to. +/// +/// \return Whether the filter matches the test program. This is a superset of +/// matches_test_case. +bool +engine::test_filter::matches_test_program(const fs::path& test_program_) const +{ + if (test_program == test_program_) + return true; + else { + // Check if the filter matches a subdirectory of the test program. + // The test case must be empty because we don't want foo:bar to match + // foo/baz. + return (test_case.empty() && test_program.is_parent_of(test_program_)); + } +} + + +/// Checks if this filter matches a given test case identifier. +/// +/// \param test_program_ The test program to compare to. +/// \param test_case_ The test case to compare to. +/// +/// \return Whether the filter matches the test case. +bool +engine::test_filter::matches_test_case(const fs::path& test_program_, + const std::string& test_case_) const +{ + if (matches_test_program(test_program_)) { + return test_case.empty() || test_case == test_case_; + } else + return false; +} + + +/// Less-than comparison for sorting purposes. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter sorts before the other filter. +bool +engine::test_filter::operator<(const test_filter& other) const +{ + return ( + test_program < other.test_program || + (test_program == other.test_program && test_case < other.test_case)); +} + + +/// Equality comparison. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter is equal to the other filter. +bool +engine::test_filter::operator==(const test_filter& other) const +{ + return test_program == other.test_program && test_case == other.test_case; +} + + +/// Non-equality comparison. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter is different than the other filter. +bool +engine::test_filter::operator!=(const test_filter& other) const +{ + return !(*this == other); +} + + +/// 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& +engine::operator<<(std::ostream& output, const test_filter& object) +{ + if (object.test_case.empty()) { + output << F("test_filter{test_program=%s}") % object.test_program; + } else { + output << F("test_filter{test_program=%s, test_case=%s}") + % object.test_program % object.test_case; + } + return output; +} + + +/// Constructs a new set of filters. +/// +/// \param filters_ The filters themselves; if empty, no filters are applied. +engine::test_filters::test_filters(const std::set< test_filter >& filters_) : + _filters(filters_) +{ +} + + +/// Checks if a given test program matches the set of filters. +/// +/// This is provided as an optimization only, and the results of this function +/// are less specific than those of match_test_case. Checking for the matching +/// of a test program should be done before loading the list of test cases from +/// a program, so as to avoid the delay in executing the test program, but +/// match_test_case must still be called afterwards. +/// +/// \param name The test program to check against the filters. +/// +/// \return True if the provided identifier matches any filter. +bool +engine::test_filters::match_test_program(const fs::path& name) const +{ + if (_filters.empty()) + return true; + + bool matches = false; + for (std::set< test_filter >::const_iterator iter = _filters.begin(); + !matches && iter != _filters.end(); iter++) { + matches = (*iter).matches_test_program(name); + } + return matches; +} + + +/// Checks if a given test case identifier matches the set of filters. +/// +/// \param test_program The test program to check against the filters. +/// \param test_case The test case to check against the filters. +/// +/// \return A boolean indicating if the test case is matched by any filter and, +/// if true, a string containing the filter name. The string is empty when +/// there are no filters defined. +engine::test_filters::match +engine::test_filters::match_test_case(const fs::path& test_program, + const std::string& test_case) const +{ + if (_filters.empty()) { + INV(match_test_program(test_program)); + return match(true, none); + } + + optional< test_filter > found = none; + for (std::set< test_filter >::const_iterator iter = _filters.begin(); + !found && iter != _filters.end(); iter++) { + if ((*iter).matches_test_case(test_program, test_case)) + found = *iter; + } + INV(!found || match_test_program(test_program)); + return match(static_cast< bool >(found), found); +} + + +/// Calculates the filters that have not matched any tests. +/// +/// \param matched The filters that did match some tests. This must be a subset +/// of the filters held by this object. +/// +/// \return The set of filters that have not been used. +std::set< engine::test_filter > +engine::test_filters::difference(const std::set< test_filter >& matched) const +{ + PRE(std::includes(_filters.begin(), _filters.end(), + matched.begin(), matched.end())); + + std::set< test_filter > filters; + std::set_difference(_filters.begin(), _filters.end(), + matched.begin(), matched.end(), + std::inserter(filters, filters.begin())); + return filters; +} + + +/// Checks if a collection of filters is disjoint. +/// +/// \param filters The filters to check. +/// +/// \throw std::runtime_error If the filters are not disjoint. +void +engine::check_disjoint_filters(const std::set< engine::test_filter >& filters) +{ + // Yes, this is an O(n^2) algorithm. However, we can assume that the number + // of test filters (which are provided by the user on the command line) on a + // particular run is in the order of tens, and thus this should not cause + // any serious performance trouble. + for (std::set< test_filter >::const_iterator i1 = filters.begin(); + i1 != filters.end(); i1++) { + for (std::set< test_filter >::const_iterator i2 = filters.begin(); + i2 != filters.end(); i2++) { + const test_filter& filter1 = *i1; + const test_filter& filter2 = *i2; + + if (i1 != i2 && filter1.contains(filter2)) { + throw std::runtime_error( + F("Filters '%s' and '%s' are not disjoint") % + filter1.str() % filter2.str()); + } + } + } +} + + +/// Constructs a filters_state instance. +/// +/// \param filters_ The set of filters to track. +engine::filters_state::filters_state( + const std::set< engine::test_filter >& filters_) : + _filters(test_filters(filters_)) +{ +} + + +/// Checks whether these filters match the given test program. +/// +/// \param test_program The test program to match against. +/// +/// \return True if these filters match the given test program name. +bool +engine::filters_state::match_test_program(const fs::path& test_program) const +{ + return _filters.match_test_program(test_program); +} + + +/// Checks whether these filters match the given test case. +/// +/// \param test_program The test program to match against. +/// \param test_case The test case to match against. +/// +/// \return True if these filters match the given test case identifier. +bool +engine::filters_state::match_test_case(const fs::path& test_program, + const std::string& test_case) +{ + engine::test_filters::match match = _filters.match_test_case( + test_program, test_case); + if (match.first && match.second) + _used_filters.insert(match.second.get()); + return match.first; +} + + +/// Calculates the unused filters in this set. +/// +/// \return Returns the set of filters that have not matched any tests. This +/// information is useful to report usage errors to the user. +std::set< engine::test_filter > +engine::filters_state::unused(void) const +{ + return _filters.difference(_used_filters); +} diff --git a/contrib/kyua/engine/filters.hpp b/contrib/kyua/engine/filters.hpp new file mode 100644 index 000000000000..91a667c3b46b --- /dev/null +++ b/contrib/kyua/engine/filters.hpp @@ -0,0 +1,134 @@ +// 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 engine/filters.hpp +/// Representation and manipulation of filters for test cases. +/// +/// All the filter classes in this module are supposed to be purely functional: +/// they are mere filters that decide whether they match or not the input data +/// fed to them. User-interface filter manipulation must go somewhere else. + +#if !defined(ENGINE_FILTERS_HPP) +#define ENGINE_FILTERS_HPP + +#include "engine/filters_fwd.hpp" + +#include +#include +#include +#include + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + + +namespace engine { + + +/// Filter for test cases. +/// +/// A filter is one of: the name of a directory containing test cases, the name +/// of a test program, or the name of a test program plus the name of a test +/// case. +class test_filter { +public: + /// The name of the test program or subdirectory to match. + utils::fs::path test_program; + + /// The name of the test case to match; if empty, represents any test case. + std::string test_case; + + test_filter(const utils::fs::path&, const std::string&); + static test_filter parse(const std::string&); + + std::string str(void) const; + + bool contains(const test_filter&) const; + bool matches_test_program(const utils::fs::path&) const; + bool matches_test_case(const utils::fs::path&, const std::string&) const; + + bool operator<(const test_filter&) const; + bool operator==(const test_filter&) const; + bool operator!=(const test_filter&) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_filter&); + + +/// Collection of user-provided filters to select test cases. +/// +/// An empty collection of filters is considered to match any test case. +/// +/// In general, the filters maintained by this class should be disjoint. If +/// they are not, some filters may never have a chance to do a match, which is +/// most likely the fault of the user. To check for non-disjoint filters before +/// constructing this object, use check_disjoint_filters. +class test_filters { + /// The user-provided filters. + std::set< test_filter > _filters; + +public: + explicit test_filters(const std::set< test_filter >&); + + /// Return type of match_test_case. Indicates whether the filters have + /// matched a particular test case and, if they have, which filter did the + /// match (if any). + typedef std::pair< bool, utils::optional< test_filter > > match; + + bool match_test_program(const utils::fs::path&) const; + match match_test_case(const utils::fs::path&, const std::string&) const; + + std::set< test_filter > difference(const std::set< test_filter >&) const; +}; + + +void check_disjoint_filters(const std::set< test_filter >&); + + +/// Tracks state of the filters that have matched tests during execution. +class filters_state { + /// The user-provided filters. + test_filters _filters; + + /// Collection of filters that have matched test cases so far. + std::set< test_filter > _used_filters; + +public: + explicit filters_state(const std::set< test_filter >&); + + bool match_test_program(const utils::fs::path&) const; + bool match_test_case(const utils::fs::path&, const std::string&); + + std::set< test_filter > unused(void) const; +}; + + +} // namespace engine + +#endif // !defined(ENGINE_FILTERS_HPP) diff --git a/contrib/kyua/engine/filters_fwd.hpp b/contrib/kyua/engine/filters_fwd.hpp new file mode 100644 index 000000000000..ee5d0c692ff5 --- /dev/null +++ b/contrib/kyua/engine/filters_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 engine/filters_fwd.hpp +/// Forward declarations for engine/filters.hpp + +#if !defined(ENGINE_FILTERS_FWD_HPP) +#define ENGINE_FILTERS_FWD_HPP + +namespace engine { + + +class filters_state; +class test_filter; +class test_filters; + + +} // namespace engine + +#endif // !defined(ENGINE_FILTERS_FWD_HPP) diff --git a/contrib/kyua/engine/filters_test.cpp b/contrib/kyua/engine/filters_test.cpp new file mode 100644 index 000000000000..081755b2553f --- /dev/null +++ b/contrib/kyua/engine/filters_test.cpp @@ -0,0 +1,594 @@ +// 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 "engine/filters.hpp" + +#include + +#include + +namespace fs = utils::fs; + + +namespace { + + +/// Syntactic sugar to instantiate engine::test_filter objects. +/// +/// \param test_program Test program. +/// \param test_case Test case. +/// +/// \return A \p test_filter object, based on \p test_program and \p test_case. +inline engine::test_filter +mkfilter(const char* test_program, const char* test_case) +{ + return engine::test_filter(fs::path(test_program), test_case); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__public_fields); +ATF_TEST_CASE_BODY(test_filter__public_fields) +{ + const engine::test_filter filter(fs::path("foo/bar"), "baz"); + ATF_REQUIRE_EQ(fs::path("foo/bar"), filter.test_program); + ATF_REQUIRE_EQ("baz", filter.test_case); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__ok); +ATF_TEST_CASE_BODY(test_filter__parse__ok) +{ + const engine::test_filter filter(engine::test_filter::parse("foo")); + ATF_REQUIRE_EQ(fs::path("foo"), filter.test_program); + ATF_REQUIRE(filter.test_case.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__empty); +ATF_TEST_CASE_BODY(test_filter__parse__empty) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "empty", + engine::test_filter::parse("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__absolute); +ATF_TEST_CASE_BODY(test_filter__parse__absolute) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "'/foo/bar'.*relative", + engine::test_filter::parse("/foo//bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__bad_program_name); +ATF_TEST_CASE_BODY(test_filter__parse__bad_program_name) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "Program name.*':foo'", + engine::test_filter::parse(":foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__bad_test_case); +ATF_TEST_CASE_BODY(test_filter__parse__bad_test_case) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "Test case.*'bar/baz:'", + engine::test_filter::parse("bar/baz:")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__bad_path); +ATF_TEST_CASE_BODY(test_filter__parse__bad_path) +{ + // TODO(jmmv): Not implemented. At the moment, the only reason for a path + // to be invalid is if it is empty... but we are checking this exact + // condition ourselves as part of the input validation. So we can't mock in + // an argument with an invalid non-empty path... +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__str); +ATF_TEST_CASE_BODY(test_filter__str) +{ + const engine::test_filter filter(fs::path("foo/bar"), "baz"); + ATF_REQUIRE_EQ("foo/bar:baz", filter.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__contains__same); +ATF_TEST_CASE_BODY(test_filter__contains__same) +{ + { + const engine::test_filter f(fs::path("foo/bar"), "baz"); + ATF_REQUIRE(f.contains(f)); + } + { + const engine::test_filter f(fs::path("foo/bar"), ""); + ATF_REQUIRE(f.contains(f)); + } + { + const engine::test_filter f(fs::path("foo"), ""); + ATF_REQUIRE(f.contains(f)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__contains__different); +ATF_TEST_CASE_BODY(test_filter__contains__different) +{ + { + const engine::test_filter f1(fs::path("foo"), ""); + const engine::test_filter f2(fs::path("foo"), "bar"); + ATF_REQUIRE( f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo/bar"), ""); + const engine::test_filter f2(fs::path("foo/bar"), "baz"); + ATF_REQUIRE( f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo/bar"), ""); + const engine::test_filter f2(fs::path("foo/baz"), ""); + ATF_REQUIRE(!f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo"), ""); + const engine::test_filter f2(fs::path("foo/bar"), ""); + ATF_REQUIRE( f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo"), "bar"); + const engine::test_filter f2(fs::path("foo/bar"), ""); + ATF_REQUIRE(!f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__matches_test_program) +ATF_TEST_CASE_BODY(test_filter__matches_test_program) +{ + { + const engine::test_filter f(fs::path("top"), "unused"); + ATF_REQUIRE( f.matches_test_program(fs::path("top"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("top2"))); + } + + { + const engine::test_filter f(fs::path("dir1/dir2"), ""); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/foo"))); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/bar"))); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir2/bar/baz"))); + } + + { + const engine::test_filter f(fs::path("dir1/dir2"), "unused"); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/foo"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/bar"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir2/bar/baz"))); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__matches_test_case) +ATF_TEST_CASE_BODY(test_filter__matches_test_case) +{ + { + const engine::test_filter f(fs::path("top"), "foo"); + ATF_REQUIRE( f.matches_test_case(fs::path("top"), "foo")); + ATF_REQUIRE(!f.matches_test_case(fs::path("top"), "bar")); + } + + { + const engine::test_filter f(fs::path("top"), ""); + ATF_REQUIRE( f.matches_test_case(fs::path("top"), "foo")); + ATF_REQUIRE( f.matches_test_case(fs::path("top"), "bar")); + ATF_REQUIRE(!f.matches_test_case(fs::path("top2"), "foo")); + } + + { + const engine::test_filter f(fs::path("d1/d2/prog"), "t1"); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog"), "t1")); + ATF_REQUIRE(!f.matches_test_case(fs::path("d1/d2/prog"), "t2")); + } + + { + const engine::test_filter f(fs::path("d1/d2"), ""); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog"), "t1")); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog"), "t2")); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog2"), "t2")); + ATF_REQUIRE(!f.matches_test_case(fs::path("d1/d3"), "foo")); + ATF_REQUIRE(!f.matches_test_case(fs::path("d2"), "foo")); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__operator_lt) +ATF_TEST_CASE_BODY(test_filter__operator_lt) +{ + { + const engine::test_filter f1(fs::path("d1/d2"), ""); + ATF_REQUIRE(!(f1 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d3"), ""); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d2"), "foo"); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), "bar"); + const engine::test_filter f2(fs::path("d1/d2"), "foo"); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), "bar"); + const engine::test_filter f2(fs::path("d1/d3"), ""); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__operator_eq) +ATF_TEST_CASE_BODY(test_filter__operator_eq) +{ + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d2"), "bar"); + ATF_REQUIRE( (f1 == f1)); + ATF_REQUIRE(!(f1 == f2)); + ATF_REQUIRE(!(f2 == f1)); + ATF_REQUIRE( (f2 == f2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__operator_ne) +ATF_TEST_CASE_BODY(test_filter__operator_ne) +{ + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d2"), "bar"); + ATF_REQUIRE(!(f1 != f1)); + ATF_REQUIRE( (f1 != f2)); + ATF_REQUIRE( (f2 != f1)); + ATF_REQUIRE(!(f2 != f2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__output); +ATF_TEST_CASE_BODY(test_filter__output) +{ + { + std::ostringstream str; + str << engine::test_filter(fs::path("d1/d2"), ""); + ATF_REQUIRE_EQ( + "test_filter{test_program=d1/d2}", + str.str()); + } + { + std::ostringstream str; + str << engine::test_filter(fs::path("d1/d2"), "bar"); + ATF_REQUIRE_EQ( + "test_filter{test_program=d1/d2, test_case=bar}", + str.str()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_case__no_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_case__no_filters) +{ + const std::set< engine::test_filter > raw_filters; + + const engine::test_filters filters(raw_filters); + engine::test_filters::match match; + + match = filters.match_test_case(fs::path("foo"), "baz"); + ATF_REQUIRE(match.first); + ATF_REQUIRE(!match.second); + + match = filters.match_test_case(fs::path("foo/bar"), "baz"); + ATF_REQUIRE(match.first); + ATF_REQUIRE(!match.second); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_case__some_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_case__some_filters) +{ + std::set< engine::test_filter > raw_filters; + raw_filters.insert(mkfilter("top_test", "")); + raw_filters.insert(mkfilter("subdir_1", "")); + raw_filters.insert(mkfilter("subdir_2/a_test", "")); + raw_filters.insert(mkfilter("subdir_2/b_test", "foo")); + + const engine::test_filters filters(raw_filters); + engine::test_filters::match match; + + match = filters.match_test_case(fs::path("top_test"), "a"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("top_test", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_1/foo"), "a"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_1", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_1/bar"), "z"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_1", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_2/a_test"), "bar"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_2/a_test", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_2/b_test"), "foo"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_2/b_test:foo", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_2/b_test"), "bar"); + ATF_REQUIRE(!match.first); + + match = filters.match_test_case(fs::path("subdir_2/c_test"), "foo"); + ATF_REQUIRE(!match.first); + + match = filters.match_test_case(fs::path("subdir_3"), "hello"); + ATF_REQUIRE(!match.first); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_program__no_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_program__no_filters) +{ + const std::set< engine::test_filter > raw_filters; + + const engine::test_filters filters(raw_filters); + ATF_REQUIRE(filters.match_test_program(fs::path("foo"))); + ATF_REQUIRE(filters.match_test_program(fs::path("foo/bar"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_program__some_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_program__some_filters) +{ + std::set< engine::test_filter > raw_filters; + raw_filters.insert(mkfilter("top_test", "")); + raw_filters.insert(mkfilter("subdir_1", "")); + raw_filters.insert(mkfilter("subdir_2/a_test", "")); + raw_filters.insert(mkfilter("subdir_2/b_test", "foo")); + + const engine::test_filters filters(raw_filters); + ATF_REQUIRE( filters.match_test_program(fs::path("top_test"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_1/foo"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_1/bar"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_2/a_test"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_2/b_test"))); + ATF_REQUIRE(!filters.match_test_program(fs::path("subdir_2/c_test"))); + ATF_REQUIRE(!filters.match_test_program(fs::path("subdir_3"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__difference__no_filters); +ATF_TEST_CASE_BODY(test_filters__difference__no_filters) +{ + const std::set< engine::test_filter > in_filters; + const std::set< engine::test_filter > used; + const std::set< engine::test_filter > diff = engine::test_filters( + in_filters).difference(used); + ATF_REQUIRE(diff.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__difference__some_filters__all_used); +ATF_TEST_CASE_BODY(test_filters__difference__some_filters__all_used) +{ + std::set< engine::test_filter > in_filters; + in_filters.insert(mkfilter("a", "")); + in_filters.insert(mkfilter("b", "c")); + + const std::set< engine::test_filter > used = in_filters; + + const std::set< engine::test_filter > diff = engine::test_filters( + in_filters).difference(used); + ATF_REQUIRE(diff.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__difference__some_filters__some_unused); +ATF_TEST_CASE_BODY(test_filters__difference__some_filters__some_unused) +{ + std::set< engine::test_filter > in_filters; + in_filters.insert(mkfilter("a", "")); + in_filters.insert(mkfilter("b", "c")); + in_filters.insert(mkfilter("d", "")); + in_filters.insert(mkfilter("e", "f")); + + std::set< engine::test_filter > used; + used.insert(mkfilter("b", "c")); + used.insert(mkfilter("d", "")); + + const std::set< engine::test_filter > diff = engine::test_filters( + in_filters).difference(used); + ATF_REQUIRE_EQ(2, diff.size()); + ATF_REQUIRE(diff.find(mkfilter("a", "")) != diff.end()); + ATF_REQUIRE(diff.find(mkfilter("e", "f")) != diff.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_disjoint_filters__ok); +ATF_TEST_CASE_BODY(check_disjoint_filters__ok) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a", "")); + filters.insert(mkfilter("b", "")); + filters.insert(mkfilter("c", "a")); + filters.insert(mkfilter("c", "b")); + + engine::check_disjoint_filters(filters); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_disjoint_filters__fail); +ATF_TEST_CASE_BODY(check_disjoint_filters__fail) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a", "")); + filters.insert(mkfilter("b", "")); + filters.insert(mkfilter("c", "a")); + filters.insert(mkfilter("d", "b")); + filters.insert(mkfilter("c", "")); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "'c'.*'c:a'.*not disjoint", + engine::check_disjoint_filters(filters)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__match_test_program); +ATF_TEST_CASE_BODY(filters_state__match_test_program) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("foo/bar", "")); + filters.insert(mkfilter("baz", "tc")); + engine::filters_state state(filters); + + ATF_REQUIRE(state.match_test_program(fs::path("foo/bar/something"))); + ATF_REQUIRE(state.match_test_program(fs::path("baz"))); + + ATF_REQUIRE(!state.match_test_program(fs::path("foo/baz"))); + ATF_REQUIRE(!state.match_test_program(fs::path("hello"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__match_test_case); +ATF_TEST_CASE_BODY(filters_state__match_test_case) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("foo/bar", "")); + filters.insert(mkfilter("baz", "tc")); + engine::filters_state state(filters); + + ATF_REQUIRE(state.match_test_case(fs::path("foo/bar/something"), "any")); + ATF_REQUIRE(state.match_test_case(fs::path("baz"), "tc")); + + ATF_REQUIRE(!state.match_test_case(fs::path("foo/baz/something"), "tc")); + ATF_REQUIRE(!state.match_test_case(fs::path("baz"), "tc2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__unused__none); +ATF_TEST_CASE_BODY(filters_state__unused__none) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a/b", "")); + filters.insert(mkfilter("baz", "tc")); + filters.insert(mkfilter("hey/d", "yes")); + engine::filters_state state(filters); + + state.match_test_case(fs::path("a/b/c"), "any"); + state.match_test_case(fs::path("baz"), "tc"); + state.match_test_case(fs::path("hey/d"), "yes"); + + ATF_REQUIRE(state.unused().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__unused__some); +ATF_TEST_CASE_BODY(filters_state__unused__some) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a/b", "")); + filters.insert(mkfilter("baz", "tc")); + filters.insert(mkfilter("hey/d", "yes")); + engine::filters_state state(filters); + + state.match_test_program(fs::path("a/b/c")); + state.match_test_case(fs::path("baz"), "tc"); + + std::set< engine::test_filter > exp_unused; + exp_unused.insert(mkfilter("a/b", "")); + exp_unused.insert(mkfilter("hey/d", "yes")); + + ATF_REQUIRE(exp_unused == state.unused()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, test_filter__public_fields); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__ok); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__empty); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__absolute); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__bad_program_name); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__bad_test_case); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__bad_path); + ATF_ADD_TEST_CASE(tcs, test_filter__str); + ATF_ADD_TEST_CASE(tcs, test_filter__contains__same); + ATF_ADD_TEST_CASE(tcs, test_filter__contains__different); + ATF_ADD_TEST_CASE(tcs, test_filter__matches_test_program); + ATF_ADD_TEST_CASE(tcs, test_filter__matches_test_case); + ATF_ADD_TEST_CASE(tcs, test_filter__operator_lt); + ATF_ADD_TEST_CASE(tcs, test_filter__operator_eq); + ATF_ADD_TEST_CASE(tcs, test_filter__operator_ne); + ATF_ADD_TEST_CASE(tcs, test_filter__output); + + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_case__no_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_case__some_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_program__no_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_program__some_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__difference__no_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__difference__some_filters__all_used); + ATF_ADD_TEST_CASE(tcs, test_filters__difference__some_filters__some_unused); + + ATF_ADD_TEST_CASE(tcs, check_disjoint_filters__ok); + ATF_ADD_TEST_CASE(tcs, check_disjoint_filters__fail); + + ATF_ADD_TEST_CASE(tcs, filters_state__match_test_program); + ATF_ADD_TEST_CASE(tcs, filters_state__match_test_case); + ATF_ADD_TEST_CASE(tcs, filters_state__unused__none); + ATF_ADD_TEST_CASE(tcs, filters_state__unused__some); +} diff --git a/contrib/kyua/engine/kyuafile.cpp b/contrib/kyua/engine/kyuafile.cpp new file mode 100644 index 000000000000..4dca3193832b --- /dev/null +++ b/contrib/kyua/engine/kyuafile.cpp @@ -0,0 +1,694 @@ +// 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 "engine/kyuafile.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#include "engine/exceptions.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/lua_module.hpp" +#include "utils/fs/operations.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +// History of Kyuafile file versions: +// +// 3 - DOES NOT YET EXIST. Pending changes for when this is introduced: +// +// * Revisit what to do about the test_suite definition. Support for +// per-test program overrides is deprecated and should be removed. +// But, maybe, the whole test_suite definition idea is wrong and we +// should instead be explicitly telling which configuration variables +// to "inject" into each test program. +// +// 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('kyuafile', 1). +// +// 1 - Initial version. + + +namespace { + + +static int lua_current_kyuafile(lutok::state&); +static int lua_generic_test_program(lutok::state&); +static int lua_include(lutok::state&); +static int lua_syntax(lutok::state&); +static int lua_test_suite(lutok::state&); + + +/// Concatenates two paths while avoiding paths to start with './'. +/// +/// \param root Path to the directory containing the file. +/// \param file Path to concatenate to root. Cannot be absolute. +/// +/// \return The concatenated path. +static fs::path +relativize(const fs::path& root, const fs::path& file) +{ + PRE(!file.is_absolute()); + + if (root == fs::path(".")) + return file; + else + return root / file; +} + + +/// Implementation of a parser for Kyuafiles. +/// +/// The main purpose of having this as a class is to keep track of global state +/// within the Lua files and allowing the Lua callbacks to easily access such +/// data. +class parser : utils::noncopyable { + /// Lua state to parse a single Kyuafile file. + lutok::state _state; + + /// Root directory of the test suite represented by the Kyuafile. + const fs::path _source_root; + + /// Root directory of the test programs. + const fs::path _build_root; + + /// Name of the Kyuafile to load relative to _source_root. + const fs::path _relative_filename; + + /// Version of the Kyuafile file format requested by the parsed file. + /// + /// This is set once the Kyuafile invokes the syntax() call. + optional< int > _version; + + /// Name of the test suite defined by the Kyuafile. + /// + /// This is set once the Kyuafile invokes the test_suite() call. + optional< std::string > _test_suite; + + /// Collection of test programs defined by the Kyuafile. + /// + /// This acts as an accumulator for all the *_test_program() calls within + /// the Kyuafile. + model::test_programs_vector _test_programs; + + /// Safely gets _test_suite and respects any test program overrides. + /// + /// \param program_override The test program-specific test suite name. May + /// be empty to indicate no override. + /// + /// \return The name of the test suite. + /// + /// \throw std::runtime_error If program_override is empty and the Kyuafile + /// did not yet define the global name of the test suite. + std::string + get_test_suite(const std::string& program_override) + { + std::string test_suite; + + if (program_override.empty()) { + if (!_test_suite) { + throw std::runtime_error("No test suite defined in the " + "Kyuafile and no override provided in " + "the test_program definition"); + } + test_suite = _test_suite.get(); + } else { + test_suite = program_override; + } + + return test_suite; + } + +public: + /// Initializes the parser and the Lua state. + /// + /// \param source_root_ The root directory of the test suite represented by + /// the Kyuafile. + /// \param build_root_ The root directory of the test programs. + /// \param relative_filename_ Name of the Kyuafile to load relative to + /// source_root_. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle The scheduler context to use for loading the + /// test case lists. + parser(const fs::path& source_root_, const fs::path& build_root_, + const fs::path& relative_filename_, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) : + _source_root(source_root_), _build_root(build_root_), + _relative_filename(relative_filename_) + { + lutok::stack_cleaner cleaner(_state); + + _state.push_cxx_function(lua_syntax); + _state.set_global("syntax"); + + *_state.new_userdata< parser* >() = this; + _state.set_global("_parser"); + + _state.push_cxx_function(lua_current_kyuafile); + _state.set_global("current_kyuafile"); + + *_state.new_userdata< const config::tree* >() = &user_config; + *_state.new_userdata< scheduler::scheduler_handle* >() = + &scheduler_handle; + _state.push_cxx_closure(lua_include, 2); + _state.set_global("include"); + + _state.push_cxx_function(lua_test_suite); + _state.set_global("test_suite"); + + const std::set< std::string > interfaces = + scheduler::registered_interface_names(); + for (std::set< std::string >::const_iterator iter = interfaces.begin(); + iter != interfaces.end(); ++iter) { + const std::string& interface = *iter; + + _state.push_string(interface); + *_state.new_userdata< const config::tree* >() = &user_config; + *_state.new_userdata< scheduler::scheduler_handle* >() = + &scheduler_handle; + _state.push_cxx_closure(lua_generic_test_program, 3); + _state.set_global(interface + "_test_program"); + } + + _state.open_base(); + _state.open_string(); + _state.open_table(); + fs::open_fs(_state, callback_current_kyuafile().branch_path()); + } + + /// Destructor. + ~parser(void) + { + } + + /// Gets the parser object associated to a Lua state. + /// + /// \param state The Lua state from which to obtain the parser object. + /// + /// \return A pointer to the parser. + static parser* + get_from_state(lutok::state& state) + { + lutok::stack_cleaner cleaner(state); + state.get_global("_parser"); + return *state.to_userdata< parser* >(-1); + } + + /// Callback for the Kyuafile current_kyuafile() function. + /// + /// \return Returns the absolute path to the current Kyuafile. + fs::path + callback_current_kyuafile(void) const + { + const fs::path file = relativize(_source_root, _relative_filename); + if (file.is_absolute()) + return file; + else + return file.to_absolute(); + } + + /// Callback for the Kyuafile include() function. + /// + /// \post _test_programs is extended with the the test programs defined by + /// the included file. + /// + /// \param raw_file Path to the file to include. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle Scheduler context to run test programs in. + void + callback_include(const fs::path& raw_file, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) + { + const fs::path file = relativize(_relative_filename.branch_path(), + raw_file); + const model::test_programs_vector subtps = + parser(_source_root, _build_root, file, user_config, + scheduler_handle).parse(); + + std::copy(subtps.begin(), subtps.end(), + std::back_inserter(_test_programs)); + } + + /// Callback for the Kyuafile syntax() function. + /// + /// \post _version is set to the requested version. + /// + /// \param version Version of the Kyuafile syntax requested by the file. + /// + /// \throw std::runtime_error If the format or the version are invalid, or + /// if syntax() has already been called. + void + callback_syntax(const int version) + { + if (_version) + throw std::runtime_error("Can only call syntax() once"); + + if (version < 1 || version > 2) + throw std::runtime_error(F("Unsupported file version %s") % + version); + + _version = utils::make_optional(version); + } + + /// Callback for the various Kyuafile *_test_program() functions. + /// + /// \post _test_programs is extended to include the newly defined test + /// program. + /// + /// \param interface Name of the test program interface. + /// \param raw_path Path to the test program, relative to the Kyuafile. + /// This has to be adjusted according to the relative location of this + /// Kyuafile to _source_root. + /// \param test_suite_override Name of the test suite this test program + /// belongs to, if explicitly defined at the test program level. + /// \param metadata Metadata variables passed to the test program. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle Scheduler context to run test programs in. + /// + /// \throw std::runtime_error If the test program definition is invalid or + /// if the test program does not exist. + void + callback_test_program(const std::string& interface, + const fs::path& raw_path, + const std::string& test_suite_override, + const model::metadata& metadata, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) + { + if (raw_path.is_absolute()) + throw std::runtime_error(F("Got unexpected absolute path for test " + "program '%s'") % raw_path); + else if (raw_path.str() != raw_path.leaf_name()) + throw std::runtime_error(F("Test program '%s' cannot contain path " + "components") % raw_path); + + const fs::path path = relativize(_relative_filename.branch_path(), + raw_path); + + if (!fs::exists(_build_root / path)) + throw std::runtime_error(F("Non-existent test program '%s'") % + path); + + const std::string test_suite = get_test_suite(test_suite_override); + + _test_programs.push_back(model::test_program_ptr( + new scheduler::lazy_test_program(interface, path, _build_root, + test_suite, metadata, user_config, + scheduler_handle))); + } + + /// Callback for the Kyuafile test_suite() function. + /// + /// \post _version is set to the requested version. + /// + /// \param name Name of the test suite. + /// + /// \throw std::runtime_error If test_suite() has already been called. + void + callback_test_suite(const std::string& name) + { + if (_test_suite) + throw std::runtime_error("Can only call test_suite() once"); + _test_suite = utils::make_optional(name); + } + + /// Parses the Kyuafile. + /// + /// \pre Can only be invoked once. + /// + /// \return The collection of test programs defined by the Kyuafile. + /// + /// \throw load_error If there is any problem parsing the file. + const model::test_programs_vector& + parse(void) + { + PRE(_test_programs.empty()); + + const fs::path load_path = relativize(_source_root, _relative_filename); + try { + lutok::do_file(_state, load_path.str(), 0, 0, 0); + } catch (const std::runtime_error& e) { + // It is tempting to think that all of our various auxiliary + // functions above could raise load_error by themselves thus making + // this exception rewriting here unnecessary. Howver, that would + // not work because the helper functions above are executed within a + // Lua context, and we lose their type when they are propagated out + // of it. + throw engine::load_error(load_path, e.what()); + } + + if (!_version) + throw engine::load_error(load_path, "syntax() never called"); + + return _test_programs; + } +}; + + +/// Glue to invoke parser::callback_test_program() from Lua. +/// +/// This is a helper function for the various *_test_program() calls, as they +/// only differ in the interface of the defined test program. +/// +/// \pre state(-1) A table with the arguments that define the test program. The +/// special argument 'test_suite' provides an override to the global test suite +/// name. The rest of the arguments are part of the test program metadata. +/// \pre state(upvalue 1) String with the name of the interface. +/// \pre state(upvalue 2) User configuration with the per-test suite settings. +/// \pre state(upvalue 3) Scheduler context to run test programs in. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +/// +/// \throw std::runtime_error If the arguments to the function are invalid. +static int +lua_generic_test_program(lutok::state& state) +{ + if (!state.is_string(state.upvalue_index(1))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const std::string interface = state.to_string(state.upvalue_index(1)); + + if (!state.is_userdata(state.upvalue_index(2))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const config::tree* user_config = *state.to_userdata< const config::tree* >( + state.upvalue_index(2)); + + if (!state.is_userdata(state.upvalue_index(3))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + scheduler::scheduler_handle* scheduler_handle = + *state.to_userdata< scheduler::scheduler_handle* >( + state.upvalue_index(3)); + + if (!state.is_table(-1)) + throw std::runtime_error( + F("%s_test_program expects a table of properties as its single " + "argument") % interface); + + scheduler::ensure_valid_interface(interface); + + lutok::stack_cleaner cleaner(state); + + state.push_string("name"); + state.get_table(-2); + if (!state.is_string(-1)) + throw std::runtime_error("Test program name not defined or not a " + "string"); + const fs::path path(state.to_string(-1)); + state.pop(1); + + state.push_string("test_suite"); + state.get_table(-2); + std::string test_suite; + if (state.is_nil(-1)) { + // Leave empty to use the global test-suite value. + } else if (state.is_string(-1)) { + test_suite = state.to_string(-1); + } else { + throw std::runtime_error(F("Found non-string value in the test_suite " + "property of test program '%s'") % path); + } + state.pop(1); + + model::metadata_builder mdbuilder; + state.push_nil(); + while (state.next(-2)) { + if (!state.is_string(-2)) + throw std::runtime_error(F("Found non-string metadata property " + "name in test program '%s'") % + path); + const std::string property = state.to_string(-2); + + if (property != "name" && property != "test_suite") { + std::string value; + if (state.is_boolean(-1)) { + value = F("%s") % state.to_boolean(-1); + } else if (state.is_number(-1)) { + value = F("%s") % state.to_integer(-1); + } else if (state.is_string(-1)) { + value = state.to_string(-1); + } else { + throw std::runtime_error( + F("Metadata property '%s' in test program '%s' cannot be " + "converted to a string") % property % path); + } + + mdbuilder.set_string(property, value); + } + + state.pop(1); + } + + parser::get_from_state(state)->callback_test_program( + interface, path, test_suite, mdbuilder.build(), *user_config, + *scheduler_handle); + return 0; +} + + +/// Glue to invoke parser::callback_current_kyuafile() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_current_kyuafile(lutok::state& state) +{ + state.push_string(parser::get_from_state(state)-> + callback_current_kyuafile().str()); + return 1; +} + + +/// Glue to invoke parser::callback_include() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \pre state(upvalue 1) User configuration with the per-test suite settings. +/// \pre state(upvalue 2) Scheduler context to run test programs in. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_include(lutok::state& state) +{ + if (!state.is_userdata(state.upvalue_index(1))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const config::tree* user_config = *state.to_userdata< const config::tree* >( + state.upvalue_index(1)); + + if (!state.is_userdata(state.upvalue_index(2))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + scheduler::scheduler_handle* scheduler_handle = + *state.to_userdata< scheduler::scheduler_handle* >( + state.upvalue_index(2)); + + parser::get_from_state(state)->callback_include( + fs::path(state.to_string(-1)), *user_config, *scheduler_handle); + return 0; +} + + +/// Glue to invoke parser::callback_syntax() from Lua. +/// +/// \pre state(-2) The syntax format name, if a v1 file. +/// \pre state(-1) The syntax format version. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_syntax(lutok::state& state) +{ + if (!state.is_number(-1)) + throw std::runtime_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 std::runtime_error("Version 1 files need two arguments to " + "syntax()"); + if (!state.is_string(-2) || state.to_string(-2) != "kyuafile") + throw std::runtime_error("First argument to syntax must be " + "'kyuafile' for version 1 files"); + } else { + if (state.get_top() != 1) + throw std::runtime_error("syntax() only takes one argument"); + } + + parser::get_from_state(state)->callback_syntax(syntax_version); + return 0; +} + + +/// Glue to invoke parser::callback_test_suite() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_test_suite(lutok::state& state) +{ + parser::get_from_state(state)->callback_test_suite(state.to_string(-1)); + return 0; +} + + +} // anonymous namespace + + +/// Constructs a kyuafile form initialized data. +/// +/// Use load() to parse a test suite configuration file and construct a +/// kyuafile object. +/// +/// \param source_root_ The root directory for the test suite represented by the +/// Kyuafile. In other words, the directory containing the first Kyuafile +/// processed. +/// \param build_root_ The root directory for the test programs themselves. In +/// general, this will be the same as source_root_. If different, the +/// specified directory must follow the exact same layout of source_root_. +/// \param tps_ Collection of test programs that belong to this test suite. +engine::kyuafile::kyuafile(const fs::path& source_root_, + const fs::path& build_root_, + const model::test_programs_vector& tps_) : + _source_root(source_root_), + _build_root(build_root_), + _test_programs(tps_) +{ +} + + +/// Destructor. +engine::kyuafile::~kyuafile(void) +{ +} + + +/// Parses a test suite configuration file. +/// +/// \param file The file to parse. +/// \param user_build_root If not none, specifies a path to a directory +/// containing the test programs themselves. The layout of the build root +/// must match the layout of the source root (which is just the directory +/// from which the Kyuafile is being read). +/// \param user_config User configuration holding any test suite properties +/// to be passed to the list operation. +/// \param scheduler_handle The scheduler context to use for loading the test +/// case lists. +/// +/// \return High-level representation of the configuration file. +/// +/// \throw load_error If there is any problem loading the file. This includes +/// file access errors and syntax errors. +engine::kyuafile +engine::kyuafile::load(const fs::path& file, + const optional< fs::path > user_build_root, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) +{ + const fs::path source_root_ = file.branch_path(); + const fs::path build_root_ = user_build_root ? + user_build_root.get() : source_root_; + + // test_program.absolute_path() uses the current work directory and that + // fails to resolve the correct path once we have used chdir to enter the + // test work directory. To prevent this causing issues down the road, + // force the build root to be absolute so that absolute_path() does not + // need to rely on the current work directory. + const fs::path abs_build_root = build_root_.is_absolute() ? + build_root_ : build_root_.to_absolute(); + + return kyuafile(source_root_, build_root_, + parser(source_root_, abs_build_root, + fs::path(file.leaf_name()), user_config, + scheduler_handle).parse()); +} + + +/// Gets the root directory of the test suite. +/// +/// \return A path. +const fs::path& +engine::kyuafile::source_root(void) const +{ + return _source_root; +} + + +/// Gets the root directory of the test programs. +/// +/// \return A path. +const fs::path& +engine::kyuafile::build_root(void) const +{ + return _build_root; +} + + +/// Gets the collection of test programs that belong to this test suite. +/// +/// \return Collection of test program executable names. +const model::test_programs_vector& +engine::kyuafile::test_programs(void) const +{ + return _test_programs; +} diff --git a/contrib/kyua/engine/kyuafile.hpp b/contrib/kyua/engine/kyuafile.hpp new file mode 100644 index 000000000000..161f4305f4d1 --- /dev/null +++ b/contrib/kyua/engine/kyuafile.hpp @@ -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. + +/// \file engine/kyuafile.hpp +/// Test suite configuration parsing and representation. + +#if !defined(ENGINE_KYUAFILE_HPP) +#define ENGINE_KYUAFILE_HPP + +#include "engine/kyuafile_fwd.hpp" + +#include +#include + +#include + +#include "engine/scheduler_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional_fwd.hpp" + +namespace engine { + + +/// Representation of the configuration of a test suite. +/// +/// Test suites are collections of related test programs. They are described by +/// a configuration file. +/// +/// Test suites have two path references: one to the "source root" and another +/// one to the "build root". The source root points to the directory from which +/// the Kyuafile is being read, and all recursive inclusions are resolved +/// relative to that directory. The build root points to the directory +/// containing the generated test programs and is prepended to the absolute path +/// of the test programs referenced by the Kyuafiles. In general, the build +/// root will be the same as the source root; however, when using a build system +/// that supports "build directories", providing this option comes in handy to +/// allow running the tests without much hassle. +/// +/// This class provides the parser for test suite configuration files and +/// methods to access the parsed data. +class kyuafile { + /// Path to the directory containing the top-level Kyuafile loaded. + utils::fs::path _source_root; + + /// Path to the directory containing the test programs. + utils::fs::path _build_root; + + /// Collection of the test programs defined in the Kyuafile. + model::test_programs_vector _test_programs; + +public: + explicit kyuafile(const utils::fs::path&, const utils::fs::path&, + const model::test_programs_vector&); + ~kyuafile(void); + + static kyuafile load(const utils::fs::path&, + const utils::optional< utils::fs::path >, + const utils::config::tree&, + scheduler::scheduler_handle&); + + const utils::fs::path& source_root(void) const; + const utils::fs::path& build_root(void) const; + const model::test_programs_vector& test_programs(void) const; +}; + + +} // namespace engine + +#endif // !defined(ENGINE_KYUAFILE_HPP) diff --git a/contrib/kyua/engine/kyuafile_fwd.hpp b/contrib/kyua/engine/kyuafile_fwd.hpp new file mode 100644 index 000000000000..60a98f65e3ab --- /dev/null +++ b/contrib/kyua/engine/kyuafile_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 engine/kyuafile_fwd.hpp +/// Forward declarations for engine/kyuafile.hpp + +#if !defined(ENGINE_KYUAFILE_FWD_HPP) +#define ENGINE_KYUAFILE_FWD_HPP + +namespace engine { + + +class kyuafile; + + +} // namespace engine + +#endif // !defined(ENGINE_KYUAFILE_FWD_HPP) diff --git a/contrib/kyua/engine/kyuafile_test.cpp b/contrib/kyua/engine/kyuafile_test.cpp new file mode 100644 index 000000000000..d95f28c71acb --- /dev/null +++ b/contrib/kyua/engine/kyuafile_test.cpp @@ -0,0 +1,606 @@ +// 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 "engine/kyuafile.hpp" + +extern "C" { +#include +} + +#include +#include + +#include +#include +#include +#include + +#include "engine/atf.hpp" +#include "engine/exceptions.hpp" +#include "engine/plain.hpp" +#include "engine/scheduler.hpp" +#include "engine/tap.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__empty); +ATF_TEST_CASE_BODY(kyuafile__load__empty) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file("config", "syntax(2)\n"); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(0, suite.test_programs().size()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__real_interfaces); +ATF_TEST_CASE_BODY(kyuafile__load__real_interfaces) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('one-suite')\n" + "atf_test_program{name='1st'}\n" + "atf_test_program{name='2nd', test_suite='first'}\n" + "plain_test_program{name='3rd'}\n" + "tap_test_program{name='4th', test_suite='second'}\n" + "include('dir/config')\n"); + + fs::mkdir(fs::path("dir"), 0755); + atf::utils::create_file( + "dir/config", + "syntax(2)\n" + "atf_test_program{name='1st', test_suite='other-suite'}\n" + "include('subdir/config')\n"); + + fs::mkdir(fs::path("dir/subdir"), 0755); + atf::utils::create_file( + "dir/subdir/config", + "syntax(2)\n" + "atf_test_program{name='5th', test_suite='last-suite'}\n"); + + atf::utils::create_file("1st", ""); + atf::utils::create_file("2nd", ""); + atf::utils::create_file("3rd", ""); + atf::utils::create_file("4th", ""); + atf::utils::create_file("dir/1st", ""); + atf::utils::create_file("dir/subdir/5th", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(6, suite.test_programs().size()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[0]->interface_name()); + ATF_REQUIRE_EQ(fs::path("1st"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("one-suite", suite.test_programs()[0]->test_suite_name()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[1]->interface_name()); + ATF_REQUIRE_EQ(fs::path("2nd"), suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[1]->test_suite_name()); + + ATF_REQUIRE_EQ("plain", suite.test_programs()[2]->interface_name()); + ATF_REQUIRE_EQ(fs::path("3rd"), suite.test_programs()[2]->relative_path()); + ATF_REQUIRE_EQ("one-suite", suite.test_programs()[2]->test_suite_name()); + + ATF_REQUIRE_EQ("tap", suite.test_programs()[3]->interface_name()); + ATF_REQUIRE_EQ(fs::path("4th"), suite.test_programs()[3]->relative_path()); + ATF_REQUIRE_EQ("second", suite.test_programs()[3]->test_suite_name()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[4]->interface_name()); + ATF_REQUIRE_EQ(fs::path("dir/1st"), + suite.test_programs()[4]->relative_path()); + ATF_REQUIRE_EQ("other-suite", suite.test_programs()[4]->test_suite_name()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[5]->interface_name()); + ATF_REQUIRE_EQ(fs::path("dir/subdir/5th"), + suite.test_programs()[5]->relative_path()); + ATF_REQUIRE_EQ("last-suite", suite.test_programs()[5]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__mock_interfaces); +ATF_TEST_CASE_BODY(kyuafile__load__mock_interfaces) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + std::shared_ptr< scheduler::interface > mock_interface( + new engine::plain_interface()); + + scheduler::register_interface("some", mock_interface); + scheduler::register_interface("random", mock_interface); + scheduler::register_interface("names", mock_interface); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('one-suite')\n" + "some_test_program{name='1st'}\n" + "random_test_program{name='2nd'}\n" + "names_test_program{name='3rd'}\n"); + + atf::utils::create_file("1st", ""); + atf::utils::create_file("2nd", ""); + atf::utils::create_file("3rd", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(3, suite.test_programs().size()); + + ATF_REQUIRE_EQ("some", suite.test_programs()[0]->interface_name()); + ATF_REQUIRE_EQ(fs::path("1st"), suite.test_programs()[0]->relative_path()); + + ATF_REQUIRE_EQ("random", suite.test_programs()[1]->interface_name()); + ATF_REQUIRE_EQ(fs::path("2nd"), suite.test_programs()[1]->relative_path()); + + ATF_REQUIRE_EQ("names", suite.test_programs()[2]->interface_name()); + ATF_REQUIRE_EQ(fs::path("3rd"), suite.test_programs()[2]->relative_path()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__metadata); +ATF_TEST_CASE_BODY(kyuafile__load__metadata) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='1st', test_suite='first'," + " allowed_architectures='amd64 i386', timeout=15}\n" + "plain_test_program{name='2nd', test_suite='second'," + " required_files='foo /bar//baz', required_user='root'," + " ['custom.a-number']=123, ['custom.a-bool']=true}\n"); + atf::utils::create_file("1st", ""); + atf::utils::create_file("2nd", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(2, suite.test_programs().size()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[0]->interface_name()); + ATF_REQUIRE_EQ(fs::path("1st"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[0]->test_suite_name()); + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("amd64") + .add_allowed_architecture("i386") + .set_timeout(datetime::delta(15, 0)) + .build(); + ATF_REQUIRE_EQ(md1, suite.test_programs()[0]->get_metadata()); + + ATF_REQUIRE_EQ("plain", suite.test_programs()[1]->interface_name()); + ATF_REQUIRE_EQ(fs::path("2nd"), suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("second", suite.test_programs()[1]->test_suite_name()); + const model::metadata md2 = model::metadata_builder() + .add_required_file(fs::path("foo")) + .add_required_file(fs::path("/bar/baz")) + .add_custom("a-bool", "true") + .add_custom("a-number", "123") + .set_required_user("root") + .build(); + ATF_REQUIRE_EQ(md2, suite.test_programs()[1]->get_metadata()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__current_directory); +ATF_TEST_CASE_BODY(kyuafile__load__current_directory) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='one', test_suite='first'}\n" + "include('config2')\n"); + + atf::utils::create_file( + "config2", + "syntax(2)\n" + "test_suite('second')\n" + "atf_test_program{name='two'}\n"); + + atf::utils::create_file("one", ""); + atf::utils::create_file("two", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(2, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[0]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("two"), + suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("second", suite.test_programs()[1]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__other_directory); +ATF_TEST_CASE_BODY(kyuafile__load__other_directory) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file( + "root/config", + "syntax(2)\n" + "test_suite('abc')\n" + "atf_test_program{name='one'}\n" + "include('dir/config')\n"); + + fs::mkdir(fs::path("root/dir"), 0755); + atf::utils::create_file( + "root/dir/config", + "syntax(2)\n" + "test_suite('foo')\n" + "atf_test_program{name='two', test_suite='def'}\n" + "atf_test_program{name='three'}\n"); + + atf::utils::create_file("root/one", ""); + atf::utils::create_file("root/dir/two", ""); + atf::utils::create_file("root/dir/three", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("root/config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("root"), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("root"), suite.build_root()); + ATF_REQUIRE_EQ(3, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("abc", suite.test_programs()[0]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("dir/two"), + suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("def", suite.test_programs()[1]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("dir/three"), + suite.test_programs()[2]->relative_path()); + ATF_REQUIRE_EQ("foo", suite.test_programs()[2]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__build_directory); +ATF_TEST_CASE_BODY(kyuafile__load__build_directory) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("srcdir"), 0755); + atf::utils::create_file( + "srcdir/config", + "syntax(2)\n" + "test_suite('abc')\n" + "atf_test_program{name='one'}\n" + "include('dir/config')\n"); + + fs::mkdir(fs::path("srcdir/dir"), 0755); + atf::utils::create_file( + "srcdir/dir/config", + "syntax(2)\n" + "test_suite('foo')\n" + "atf_test_program{name='two', test_suite='def'}\n" + "atf_test_program{name='three'}\n"); + + fs::mkdir(fs::path("builddir"), 0755); + atf::utils::create_file("builddir/one", ""); + fs::mkdir(fs::path("builddir/dir"), 0755); + atf::utils::create_file("builddir/dir/two", ""); + atf::utils::create_file("builddir/dir/three", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("srcdir/config"), utils::make_optional(fs::path("builddir")), + config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("srcdir"), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("builddir"), suite.build_root()); + ATF_REQUIRE_EQ(3, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::path("builddir/one").to_absolute(), + suite.test_programs()[0]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("abc", suite.test_programs()[0]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("builddir/dir/two").to_absolute(), + suite.test_programs()[1]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("dir/two"), + suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("def", suite.test_programs()[1]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("builddir/dir/three").to_absolute(), + suite.test_programs()[2]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("dir/three"), + suite.test_programs()[2]->relative_path()); + ATF_REQUIRE_EQ("foo", suite.test_programs()[2]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__absolute_paths_are_stable); +ATF_TEST_CASE_BODY(kyuafile__load__absolute_paths_are_stable) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='one', test_suite='first'}\n"); + atf::utils::create_file("one", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + + const fs::path previous_dir = fs::current_path(); + fs::mkdir(fs::path("other"), 0755); + // Change the directory. We want later calls to absolute_path() on the test + // programs to return references to previous_dir instead. + ATF_REQUIRE(::chdir("other") != -1); + + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(1, suite.test_programs().size()); + ATF_REQUIRE_EQ(previous_dir / "one", + suite.test_programs()[0]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[0]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__fs_calls_are_relative); +ATF_TEST_CASE_BODY(kyuafile__load__fs_calls_are_relative) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "Kyuafile", + "syntax(2)\n" + "if fs.exists('one') then\n" + " plain_test_program{name='one', test_suite='first'}\n" + "end\n" + "if fs.exists('two') then\n" + " plain_test_program{name='two', test_suite='first'}\n" + "end\n" + "include('dir/Kyuafile')\n"); + atf::utils::create_file("one", ""); + fs::mkdir(fs::path("dir"), 0755); + atf::utils::create_file( + "dir/Kyuafile", + "syntax(2)\n" + "if fs.exists('one') then\n" + " plain_test_program{name='one', test_suite='first'}\n" + "end\n" + "if fs.exists('two') then\n" + " plain_test_program{name='two', test_suite='first'}\n" + "end\n"); + atf::utils::create_file("dir/two", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("Kyuafile"), none, config::tree(), handle); + + ATF_REQUIRE_EQ(2, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::current_path() / "one", + suite.test_programs()[0]->absolute_path()); + ATF_REQUIRE_EQ(fs::current_path() / "dir/two", + suite.test_programs()[1]->absolute_path()); + + handle.cleanup(); +} + + +/// Verifies that load raises a load_error on a given input. +/// +/// \param file Name of the file to load. +/// \param regex Expression to match on load_error's contents. +static void +do_load_error_test(const char* file, const char* regex) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + ATF_REQUIRE_THROW_RE(engine::load_error, regex, + engine::kyuafile::load(fs::path(file), none, + config::tree(), handle)); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__test_program_not_basename); +ATF_TEST_CASE_BODY(kyuafile__load__test_program_not_basename) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('abc')\n" + "atf_test_program{name='one'}\n" + "atf_test_program{name='./ls'}\n"); + + atf::utils::create_file("one", ""); + do_load_error_test("config", "./ls.*path components"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__lua_error); +ATF_TEST_CASE_BODY(kyuafile__load__lua_error) +{ + atf::utils::create_file("config", "this syntax is invalid\n"); + + do_load_error_test("config", ".*"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__not_called); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__not_called) +{ + atf::utils::create_file("config", ""); + + do_load_error_test("config", "syntax.* never called"); +} + + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__deprecated_format); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__deprecated_format) +{ + atf::utils::create_file("config", "syntax('foo', 1)\n"); + do_load_error_test("config", "must be 'kyuafile'"); + + atf::utils::create_file("config", "syntax('config', 2)\n"); + do_load_error_test("config", "only takes one argument"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__twice); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__twice) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "syntax(2)\n"); + + do_load_error_test("config", "Can only call syntax.* once"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__bad_version); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__bad_version) +{ + atf::utils::create_file("config", "syntax(12)\n"); + + do_load_error_test("config", "Unsupported file version 12"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__test_suite__missing); +ATF_TEST_CASE_BODY(kyuafile__load__test_suite__missing) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "plain_test_program{name='one'}"); + + atf::utils::create_file("one", ""); + + do_load_error_test("config", "No test suite defined"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__test_suite__twice); +ATF_TEST_CASE_BODY(kyuafile__load__test_suite__twice) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('foo')\n" + "test_suite('bar')\n"); + + do_load_error_test("config", "Can only call test_suite.* once"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__missing_file); +ATF_TEST_CASE_BODY(kyuafile__load__missing_file) +{ + do_load_error_test("missing", "Load of 'missing' failed"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__missing_test_program); +ATF_TEST_CASE_BODY(kyuafile__load__missing_test_program) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='one', test_suite='first'}\n" + "atf_test_program{name='two', test_suite='first'}\n"); + + atf::utils::create_file("one", ""); + + do_load_error_test("config", "Non-existent.*'two'"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + scheduler::register_interface( + "tap", std::shared_ptr< scheduler::interface >( + new engine::tap_interface())); + + ATF_ADD_TEST_CASE(tcs, kyuafile__load__empty); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__real_interfaces); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__mock_interfaces); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__metadata); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__current_directory); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__other_directory); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__build_directory); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__absolute_paths_are_stable); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__fs_calls_are_relative); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__test_program_not_basename); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__lua_error); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__not_called); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__deprecated_format); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__twice); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__bad_version); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__test_suite__missing); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__test_suite__twice); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__missing_file); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__missing_test_program); +} diff --git a/contrib/kyua/engine/plain.cpp b/contrib/kyua/engine/plain.cpp new file mode 100644 index 000000000000..8346e50bbecf --- /dev/null +++ b/contrib/kyua/engine/plain.cpp @@ -0,0 +1,143 @@ +// 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 "engine/plain.hpp" + +extern "C" { +#include +} + +#include + +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::optional; + + +/// Executes a test program's list operation. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +void +engine::plain_interface::exec_list( + const model::test_program& /* test_program */, + const config::properties_map& /* vars */) const +{ + ::_exit(EXIT_SUCCESS); +} + + +/// Computes the test cases list of a test program. +/// +/// \return A list of test cases. +model::test_cases_map +engine::plain_interface::parse_list( + const optional< process::status >& /* status */, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + return model::test_cases_map_builder().add("main").build(); +} + + +/// Executes a test case of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +void +engine::plain_interface::exec_test( + const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& /* control_directory */) const +{ + PRE(test_case_name == "main"); + + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + utils::setenv(F("TEST_ENV_%s") % (*iter).first, (*iter).second); + } + + process::args_vector args; + process::exec(test_program.absolute_path(), args); +} + + +/// Computes the result of a test case based on its termination status. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// +/// \return A test result. +model::test_result +engine::plain_interface::compute_result( + const optional< process::status >& status, + const fs::path& /* control_directory */, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + if (!status) { + return model::test_result(model::test_result_broken, + "Test case timed out"); + } + + if (status.get().exited()) { + const int exitstatus = status.get().exitstatus(); + if (exitstatus == EXIT_SUCCESS) { + return model::test_result(model::test_result_passed); + } else { + return model::test_result( + model::test_result_failed, + F("Returned non-success exit status %s") % exitstatus); + } + } else { + return model::test_result( + model::test_result_broken, + F("Received signal %s") % status.get().termsig()); + } +} diff --git a/contrib/kyua/engine/plain.hpp b/contrib/kyua/engine/plain.hpp new file mode 100644 index 000000000000..ee5f3e746781 --- /dev/null +++ b/contrib/kyua/engine/plain.hpp @@ -0,0 +1,67 @@ +// 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 engine/plain.hpp +/// Execution engine for test programs that implement the plain interface. + +#if !defined(ENGINE_PLAIN_HPP) +#define ENGINE_PLAIN_HPP + +#include "engine/scheduler.hpp" + +namespace engine { + + +/// Implementation of the scheduler interface for plain test programs. +class plain_interface : public engine::scheduler::interface { +public: + void exec_list(const model::test_program&, + const utils::config::properties_map&) const UTILS_NORETURN; + + model::test_cases_map parse_list( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&) const; + + void exec_test(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + model::test_result compute_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_PLAIN_HPP) diff --git a/contrib/kyua/engine/plain_helpers.cpp b/contrib/kyua/engine/plain_helpers.cpp new file mode 100644 index 000000000000..52b1bc74fe10 --- /dev/null +++ b/contrib/kyua/engine/plain_helpers.cpp @@ -0,0 +1,238 @@ +// 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. + +extern "C" { +#include + +#include + +extern char** environ; +} + +#include +#include +#include +#include +#include + +#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/test_utils.ipp" + +namespace fs = utils::fs; + +using utils::optional; + + +namespace { + + +/// Gets the name of the test case to run. +/// +/// We use the value of the TEST_CASE environment variable if present, or +/// else the basename of the test program. +/// +/// \param arg0 Value of argv[0] as passed to main(). +/// +/// \return A test case name. The name may not be valid. +static std::string +guess_test_case_name(const char* arg0) +{ + const optional< std::string > test_case_env = utils::getenv("TEST_CASE"); + if (test_case_env) { + return test_case_env.get(); + } else { + return fs::path(arg0).leaf_name(); + } +} + + +/// Logs an error message and exits the test with an error code. +/// +/// \param str The error message to log. +static void +fail(const std::string& str) +{ + std::cerr << str << '\n'; + std::exit(EXIT_FAILURE); +} + + +/// A test case that validates the TEST_ENV_* variables. +static void +test_check_configuration_variables(void) +{ + std::set< std::string > vars; + char** iter; + for (iter = environ; *iter != NULL; ++iter) { + if (std::strstr(*iter, "TEST_ENV_") == *iter) { + vars.insert(*iter); + } + } + + std::set< std::string > exp_vars; + exp_vars.insert("TEST_ENV_first=some value"); + exp_vars.insert("TEST_ENV_second=some other value"); + if (vars != exp_vars) { + fail(F("Expected: %s\nFound: %s\n") % exp_vars % vars); + } +} + + +/// A test case that crashes. +static void +test_crash(void) +{ + utils::abort_without_coredump(); +} + + +/// A test case that exits with a non-zero exit code, and not 1. +static void +test_fail(void) +{ + std::exit(8); +} + + +/// A test case that passes. +static void +test_pass(void) +{ +} + + +/// A test case that spawns a subchild that gets stuck. +/// +/// This test case is used by the caller to validate that the whole process tree +/// is terminated when the test case is killed. +static void +test_spawn_blocking_child(void) +{ + pid_t pid = ::fork(); + if (pid == -1) + fail("Cannot fork subprocess"); + 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) + fail("Failed to create the pidfile"); + pidfile << pid; + pidfile.close(); + } +} + + +/// A test case that times out. +/// +/// Note that the timeout is defined in the Kyuafile, as the plain interface has +/// no means for test programs to specify this by themselves. +static void +test_timeout(void) +{ + ::sleep(10); + const fs::path control_dir = fs::path(utils::getenv("CONTROL_DIR").get()); + std::ofstream file((control_dir / "cookie").c_str()); + if (!file) + fail("Failed to create the control cookie"); + file.close(); +} + + +/// A test case that performs basic checks on the runtime environment. +/// +/// If the runtime environment does not look clean (according to the rules in +/// the Kyua runtime properties), the test fails. +static void +test_validate_isolation(void) +{ + if (utils::getenv("HOME").get() == "fake-value") + fail("HOME not reset"); + if (utils::getenv("LANG")) + fail("LANG not unset"); +} + + +} // anonymous namespace + + +/// Entry point to the test program. +/// +/// The caller can select which test case to run by defining the TEST_CASE +/// environment variable. This is not "standard", in the sense this is not a +/// generic property of the plain test case interface. +/// +/// \todo It may be worth to split this binary into separate, smaller binaries, +/// one for every "test case". We use this program as a dispatcher for +/// different "main"s, the only reason being to keep the amount of helper test +/// programs to a minimum. However, putting this each function in its own +/// binary could simplify many other things. +/// +/// \param argc The number of CLI arguments. +/// \param argv The CLI arguments themselves. These are not used because +/// Kyua will not pass any arguments to the plain test program. +int +main(int argc, char** argv) +{ + if (argc != 1) { + std::cerr << "No arguments allowed; select the test case with the " + "TEST_CASE variable"; + return EXIT_FAILURE; + } + + const std::string& test_case = guess_test_case_name(argv[0]); + + if (test_case == "check_configuration_variables") + test_check_configuration_variables(); + else if (test_case == "crash") + test_crash(); + else if (test_case == "fail") + test_fail(); + else if (test_case == "pass") + test_pass(); + else if (test_case == "spawn_blocking_child") + test_spawn_blocking_child(); + else if (test_case == "timeout") + test_timeout(); + else if (test_case == "validate_isolation") + test_validate_isolation(); + else { + std::cerr << "Unknown test case"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/contrib/kyua/engine/plain_test.cpp b/contrib/kyua/engine/plain_test.cpp new file mode 100644 index 000000000000..cc3326e4c581 --- /dev/null +++ b/contrib/kyua/engine/plain_test.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 "engine/plain.hpp" + +extern "C" { +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.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" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Copies the plain helper to the work directory, selecting a specific helper. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param name Name of the new binary to create. Must match the name of a +/// valid helper, as the binary name is used to select it. +static void +copy_plain_helper(const atf::tests::tc* tc, const char* name) +{ + const fs::path srcdir(tc->get_config_var("srcdir")); + atf::utils::copy_file((srcdir / "plain_helpers").str(), name); +} + + +/// Runs one plain test program and checks its result. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param test_case_name Name of the "test case" to select from the helper +/// program. +/// \param exp_result The expected result. +/// \param metadata The test case metadata. +/// \param user_config User-provided configuration variables. +static void +run_one(const atf::tests::tc* tc, const char* test_case_name, + const model::test_result& exp_result, + const model::metadata& metadata = model::metadata_builder().build(), + const config::tree& user_config = engine::empty_config()) +{ + copy_plain_helper(tc, test_case_name); + const model::test_program_ptr program = model::test_program_builder( + "plain", fs::path(test_case_name), fs::current_path(), "the-suite") + .add_test_case("main", metadata).build_ptr(); + + scheduler::scheduler_handle handle = scheduler::setup(); + (void)handle.spawn_test(program, "main", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + atf::utils::cat_file(result_handle->stdout_file().str(), "stdout: "); + atf::utils::cat_file(result_handle->stderr_file().str(), "stderr: "); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(list); +ATF_TEST_CASE_BODY(list) +{ + const model::test_program program = model::test_program_builder( + "plain", fs::path("non-existent"), fs::path("."), "unused-suite") + .build(); + + scheduler::scheduler_handle handle = scheduler::setup(); + const model::test_cases_map test_cases = handle.list_tests( + &program, engine::empty_config()); + handle.cleanup(); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("main").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__exit_success_is_pass); +ATF_TEST_CASE_BODY(test__exit_success_is_pass) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__exit_non_zero_is_fail); +ATF_TEST_CASE_BODY(test__exit_non_zero_is_fail) +{ + const model::test_result exp_result( + model::test_result_failed, + "Returned non-success exit status 8"); + run_one(this, "fail", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__signal_is_broken); +ATF_TEST_CASE_BODY(test__signal_is_broken) +{ + const model::test_result exp_result(model::test_result_broken, + F("Received signal %s") % SIGABRT); + run_one(this, "crash", exp_result); +} + + +ATF_TEST_CASE(test__timeout_is_broken); +ATF_TEST_CASE_HEAD(test__timeout_is_broken) +{ + set_md_var("timeout", "60"); +} +ATF_TEST_CASE_BODY(test__timeout_is_broken) +{ + utils::setenv("CONTROL_DIR", fs::current_path().str()); + + const model::metadata metadata = model::metadata_builder() + .set_timeout(datetime::delta(1, 0)).build(); + const model::test_result exp_result(model::test_result_broken, + "Test case timed out"); + run_one(this, "timeout", exp_result, metadata); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__configuration_variables); +ATF_TEST_CASE_BODY(test__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.a-suite.first", "unused"); + user_config.set_string("test_suites.the-suite.first", "some value"); + user_config.set_string("test_suites.the-suite.second", "some other value"); + user_config.set_string("test_suites.other-suite.first", "unused"); + + const model::test_result exp_result(model::test_result_passed); + run_one(this, "check_configuration_variables", exp_result, + model::metadata_builder().build(), user_config); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + + ATF_ADD_TEST_CASE(tcs, list); + + ATF_ADD_TEST_CASE(tcs, test__exit_success_is_pass); + ATF_ADD_TEST_CASE(tcs, test__exit_non_zero_is_fail); + ATF_ADD_TEST_CASE(tcs, test__signal_is_broken); + ATF_ADD_TEST_CASE(tcs, test__timeout_is_broken); + ATF_ADD_TEST_CASE(tcs, test__configuration_variables); +} diff --git a/contrib/kyua/engine/requirements.cpp b/contrib/kyua/engine/requirements.cpp new file mode 100644 index 000000000000..a7b0a90d97db --- /dev/null +++ b/contrib/kyua/engine/requirements.cpp @@ -0,0 +1,293 @@ +// 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 "engine/requirements.hpp" + +#include "model/metadata.hpp" +#include "model/types.hpp" +#include "utils/config/nodes.ipp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/memory.hpp" +#include "utils/passwd.hpp" +#include "utils/sanity.hpp" +#include "utils/units.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace units = utils::units; + + +namespace { + + +/// Checks if all required configuration variables are present. +/// +/// \param required_configs Set of required variable names. +/// \param user_config Runtime user configuration. +/// \param test_suite_name Name of the test suite the test belongs to. +/// +/// \return Empty if all variables are present or an error message otherwise. +static std::string +check_required_configs(const model::strings_set& required_configs, + const config::tree& user_config, + const std::string& test_suite_name) +{ + for (model::strings_set::const_iterator iter = required_configs.begin(); + iter != required_configs.end(); iter++) { + std::string property; + // TODO(jmmv): All this rewrite logic belongs in the ATF interface. + if ((*iter) == "unprivileged-user" || (*iter) == "unprivileged_user") + property = "unprivileged_user"; + else + property = F("test_suites.%s.%s") % test_suite_name % (*iter); + + if (!user_config.is_set(property)) + return F("Required configuration property '%s' not defined") % + (*iter); + } + return ""; +} + + +/// Checks if the allowed architectures match the current architecture. +/// +/// \param allowed_architectures Set of allowed architectures. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the current architecture is in the list or an error +/// message otherwise. +static std::string +check_allowed_architectures(const model::strings_set& allowed_architectures, + const config::tree& user_config) +{ + if (!allowed_architectures.empty()) { + const std::string architecture = + user_config.lookup< config::string_node >("architecture"); + if (allowed_architectures.find(architecture) == + allowed_architectures.end()) + return F("Current architecture '%s' not supported") % architecture; + } + return ""; +} + + +/// Checks if the allowed platforms match the current architecture. +/// +/// \param allowed_platforms Set of allowed platforms. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the current platform is in the list or an error message +/// otherwise. +static std::string +check_allowed_platforms(const model::strings_set& allowed_platforms, + const config::tree& user_config) +{ + if (!allowed_platforms.empty()) { + const std::string platform = + user_config.lookup< config::string_node >("platform"); + if (allowed_platforms.find(platform) == allowed_platforms.end()) + return F("Current platform '%s' not supported") % platform; + } + return ""; +} + + +/// Checks if the current user matches the required user. +/// +/// \param required_user Name of the required user category. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the current user fits the required user characteristics or +/// an error message otherwise. +static std::string +check_required_user(const std::string& required_user, + const config::tree& user_config) +{ + if (!required_user.empty()) { + const passwd::user user = passwd::current_user(); + if (required_user == "root") { + if (!user.is_root()) + return "Requires root privileges"; + } else if (required_user == "unprivileged") { + if (user.is_root()) + if (!user_config.is_set("unprivileged_user")) + return "Requires an unprivileged user but the " + "unprivileged-user configuration variable is not " + "defined"; + } else + UNREACHABLE_MSG("Value of require.user not properly validated"); + } + return ""; +} + + +/// Checks if all required files exist. +/// +/// \param required_files Set of paths. +/// +/// \return Empty if the required files all exist or an error message otherwise. +static std::string +check_required_files(const model::paths_set& required_files) +{ + for (model::paths_set::const_iterator iter = required_files.begin(); + iter != required_files.end(); iter++) { + INV((*iter).is_absolute()); + if (!fs::exists(*iter)) + return F("Required file '%s' not found") % *iter; + } + return ""; +} + + +/// Checks if all required programs exist. +/// +/// \param required_programs Set of paths. +/// +/// \return Empty if the required programs all exist or an error message +/// otherwise. +static std::string +check_required_programs(const model::paths_set& required_programs) +{ + for (model::paths_set::const_iterator iter = required_programs.begin(); + iter != required_programs.end(); iter++) { + if ((*iter).is_absolute()) { + if (!fs::exists(*iter)) + return F("Required program '%s' not found") % *iter; + } else { + if (!fs::find_in_path((*iter).c_str())) + return F("Required program '%s' not found in PATH") % *iter; + } + } + return ""; +} + + +/// Checks if the current system has the specified amount of memory. +/// +/// \param required_memory Amount of required physical memory, or zero if not +/// applicable. +/// +/// \return Empty if the current system has the required amount of memory or an +/// error message otherwise. +static std::string +check_required_memory(const units::bytes& required_memory) +{ + if (required_memory > 0) { + const units::bytes physical_memory = utils::physical_memory(); + if (physical_memory > 0 && physical_memory < required_memory) + return F("Requires %s bytes of physical memory but only %s " + "available") % + required_memory.format() % physical_memory.format(); + } + return ""; +} + + +/// Checks if the work directory's file system has enough free disk space. +/// +/// \param required_disk_space Amount of required free disk space, or zero if +/// not applicable. +/// \param work_directory Path to where the test case will be run. +/// +/// \return Empty if the file system where the work directory is hosted has +/// enough free disk space or an error message otherwise. +static std::string +check_required_disk_space(const units::bytes& required_disk_space, + const fs::path& work_directory) +{ + if (required_disk_space > 0) { + const units::bytes free_disk_space = fs::free_disk_space( + work_directory); + if (free_disk_space < required_disk_space) + return F("Requires %s bytes of free disk space but only %s " + "available") % + required_disk_space.format() % free_disk_space.format(); + } + return ""; +} + + +} // anonymous namespace + + +/// Checks if all the requirements specified by the test case are met. +/// +/// \param md The test metadata. +/// \param cfg The engine configuration. +/// \param test_suite Name of the test suite the test belongs to. +/// \param work_directory Path to where the test case will be run. +/// +/// \return A string describing the reason for skipping the test, or empty if +/// the test should be executed. +std::string +engine::check_reqs(const model::metadata& md, const config::tree& cfg, + const std::string& test_suite, + const fs::path& work_directory) +{ + std::string reason; + + reason = check_required_configs(md.required_configs(), cfg, test_suite); + if (!reason.empty()) + return reason; + + reason = check_allowed_architectures(md.allowed_architectures(), cfg); + if (!reason.empty()) + return reason; + + reason = check_allowed_platforms(md.allowed_platforms(), cfg); + if (!reason.empty()) + return reason; + + reason = check_required_user(md.required_user(), cfg); + if (!reason.empty()) + return reason; + + reason = check_required_files(md.required_files()); + if (!reason.empty()) + return reason; + + reason = check_required_programs(md.required_programs()); + if (!reason.empty()) + return reason; + + reason = check_required_memory(md.required_memory()); + if (!reason.empty()) + return reason; + + reason = check_required_disk_space(md.required_disk_space(), + work_directory); + if (!reason.empty()) + return reason; + + INV(reason.empty()); + return reason; +} diff --git a/contrib/kyua/engine/requirements.hpp b/contrib/kyua/engine/requirements.hpp new file mode 100644 index 000000000000..a36a938b3034 --- /dev/null +++ b/contrib/kyua/engine/requirements.hpp @@ -0,0 +1,51 @@ +// 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 engine/requirements.hpp +/// Handling of test case requirements. + +#if !defined(ENGINE_REQUIREMENTS_HPP) +#define ENGINE_REQUIREMENTS_HPP + +#include + +#include "model/metadata_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace engine { + + +std::string check_reqs(const model::metadata&, const utils::config::tree&, + const std::string&, const utils::fs::path&); + + +} // namespace engine + + +#endif // !defined(ENGINE_REQUIREMENTS_HPP) diff --git a/contrib/kyua/engine/requirements_test.cpp b/contrib/kyua/engine/requirements_test.cpp new file mode 100644 index 000000000000..5052da932cb6 --- /dev/null +++ b/contrib/kyua/engine/requirements_test.cpp @@ -0,0 +1,511 @@ +// 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 "model/metadata.hpp" + +#include + +#include "engine/config.hpp" +#include "engine/requirements.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/memory.hpp" +#include "utils/passwd.hpp" +#include "utils/units.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__none); +ATF_TEST_CASE_BODY(check_reqs__none) +{ + const model::metadata md = model::metadata_builder().build(); + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__one_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__one_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "x86_64"); + user_config.set_string("platform", ""); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__one_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__one_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "i386"); + user_config.set_string("platform", ""); + ATF_REQUIRE_MATCH("Current architecture 'i386' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__many_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__many_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .add_allowed_architecture("i386") + .add_allowed_architecture("powerpc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "i386"); + user_config.set_string("platform", ""); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__many_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__many_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .add_allowed_architecture("i386") + .add_allowed_architecture("powerpc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "arm"); + user_config.set_string("platform", ""); + ATF_REQUIRE_MATCH("Current architecture 'arm' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__one_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__one_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "amd64"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__one_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__one_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "i386"); + ATF_REQUIRE_MATCH("Current platform 'i386' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__many_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__many_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .add_allowed_platform("i386") + .add_allowed_platform("macppc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "i386"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__many_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__many_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .add_allowed_platform("i386") + .add_allowed_platform("macppc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "shark"); + ATF_REQUIRE_MATCH("Current platform 'shark' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__one_ok); +ATF_TEST_CASE_BODY(check_reqs__required_configs__one_ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("my-var") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.my-var", "value2"); + user_config.set_string("test_suites.suite.zzz", "value3"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "suite", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__one_fail); +ATF_TEST_CASE_BODY(check_reqs__required_configs__one_fail) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("unprivileged_user") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.my-var", "value2"); + user_config.set_string("test_suites.suite.zzz", "value3"); + ATF_REQUIRE_MATCH("Required configuration property 'unprivileged_user' not " + "defined", + engine::check_reqs(md, user_config, "suite", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__many_ok); +ATF_TEST_CASE_BODY(check_reqs__required_configs__many_ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("foo") + .add_required_config("bar") + .add_required_config("baz") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.foo", "value2"); + user_config.set_string("test_suites.suite.bar", "value3"); + user_config.set_string("test_suites.suite.baz", "value4"); + user_config.set_string("test_suites.suite.zzz", "value5"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "suite", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__many_fail); +ATF_TEST_CASE_BODY(check_reqs__required_configs__many_fail) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("foo") + .add_required_config("bar") + .add_required_config("baz") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.foo", "value2"); + user_config.set_string("test_suites.suite.zzz", "value3"); + ATF_REQUIRE_MATCH("Required configuration property 'bar' not defined", + engine::check_reqs(md, user_config, "suite", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__special); +ATF_TEST_CASE_BODY(check_reqs__required_configs__special) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("unprivileged-user") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE_MATCH("Required configuration property 'unprivileged-user' " + "not defined", + engine::check_reqs(md, user_config, "", fs::path("."))); + user_config.set< engine::user_node >( + "unprivileged_user", passwd::user("foo", 1, 2)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "foo", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__root__ok); +ATF_TEST_CASE_BODY(check_reqs__required_user__root__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("root") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE(!user_config.is_set("unprivileged_user")); + + passwd::set_current_user_for_testing(passwd::user("", 0, 1)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__root__fail); +ATF_TEST_CASE_BODY(check_reqs__required_user__root__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("root") + .build(); + + passwd::set_current_user_for_testing(passwd::user("", 123, 1)); + ATF_REQUIRE_MATCH("Requires root privileges", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__unprivileged__same); +ATF_TEST_CASE_BODY(check_reqs__required_user__unprivileged__same) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("unprivileged") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE(!user_config.is_set("unprivileged_user")); + + passwd::set_current_user_for_testing(passwd::user("", 123, 1)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__unprivileged__ok); +ATF_TEST_CASE_BODY(check_reqs__required_user__unprivileged__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("unprivileged") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set< engine::user_node >( + "unprivileged_user", passwd::user("", 123, 1)); + + passwd::set_current_user_for_testing(passwd::user("", 0, 1)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__unprivileged__fail); +ATF_TEST_CASE_BODY(check_reqs__required_user__unprivileged__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("unprivileged") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE(!user_config.is_set("unprivileged_user")); + + passwd::set_current_user_for_testing(passwd::user("", 0, 1)); + ATF_REQUIRE_MATCH("Requires.*unprivileged.*unprivileged-user", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_disk_space__ok); +ATF_TEST_CASE_BODY(check_reqs__required_disk_space__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_disk_space(units::bytes::parse("1m")) + .build(); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_disk_space__fail); +ATF_TEST_CASE_BODY(check_reqs__required_disk_space__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_disk_space(units::bytes::parse("1000t")) + .build(); + + ATF_REQUIRE_MATCH("Requires 1000.00T .*disk space", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_files__ok); +ATF_TEST_CASE_BODY(check_reqs__required_files__ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_file(fs::current_path() / "test-file") + .build(); + + atf::utils::create_file("test-file", ""); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_files__fail); +ATF_TEST_CASE_BODY(check_reqs__required_files__fail) +{ + const model::metadata md = model::metadata_builder() + .add_required_file(fs::path("/non-existent/file")) + .build(); + + ATF_REQUIRE_MATCH("'/non-existent/file' not found$", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_memory__ok); +ATF_TEST_CASE_BODY(check_reqs__required_memory__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_memory(units::bytes::parse("1m")) + .build(); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_memory__fail); +ATF_TEST_CASE_BODY(check_reqs__required_memory__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_memory(units::bytes::parse("100t")) + .build(); + + if (utils::physical_memory() == 0) + skip("Don't know how to query the amount of physical memory"); + ATF_REQUIRE_MATCH("Requires 100.00T .*memory", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE(check_reqs__required_programs__ok); +ATF_TEST_CASE_HEAD(check_reqs__required_programs__ok) +{ + set_md_var("require.progs", "/bin/ls /bin/mv"); +} +ATF_TEST_CASE_BODY(check_reqs__required_programs__ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("foo")) + .add_required_program(fs::path("/bin/mv")) + .build(); + + fs::mkdir(fs::path("bin"), 0755); + atf::utils::create_file("bin/foo", ""); + utils::setenv("PATH", (fs::current_path() / "bin").str()); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_programs__fail_absolute); +ATF_TEST_CASE_BODY(check_reqs__required_programs__fail_absolute) +{ + const model::metadata md = model::metadata_builder() + .add_required_program(fs::path("/non-existent/program")) + .build(); + + ATF_REQUIRE_MATCH("'/non-existent/program' not found$", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_programs__fail_relative); +ATF_TEST_CASE_BODY(check_reqs__required_programs__fail_relative) +{ + const model::metadata md = model::metadata_builder() + .add_required_program(fs::path("foo")) + .add_required_program(fs::path("bar")) + .build(); + + fs::mkdir(fs::path("bin"), 0755); + atf::utils::create_file("bin/foo", ""); + utils::setenv("PATH", (fs::current_path() / "bin").str()); + + ATF_REQUIRE_MATCH("'bar' not found in PATH$", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, check_reqs__none); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__one_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__one_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__many_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__many_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__one_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__one_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__many_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__many_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__one_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__one_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__many_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__many_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__special); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__root__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__root__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__unprivileged__same); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__unprivileged__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__unprivileged__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_disk_space__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_disk_space__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_files__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_files__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_memory__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_memory__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_programs__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_programs__fail_absolute); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_programs__fail_relative); +} diff --git a/contrib/kyua/engine/scanner.cpp b/contrib/kyua/engine/scanner.cpp new file mode 100644 index 000000000000..b42b089c3c3c --- /dev/null +++ b/contrib/kyua/engine/scanner.cpp @@ -0,0 +1,216 @@ +// 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 "engine/scanner.hpp" + +#include +#include + +#include "engine/filters.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +using utils::none; +using utils::optional; + + +namespace { + + +/// Extracts the keys of a map as a deque. +/// +/// \tparam KeyType The type of the map keys. +/// \tparam ValueType The type of the map values. +/// \param map The input map. +/// +/// \return A deque with the keys of the map. +template< typename KeyType, typename ValueType > +static std::deque< KeyType > +map_keys(const std::map< KeyType, ValueType >& map) +{ + std::deque< KeyType > keys; + for (typename std::map< KeyType, ValueType >::const_iterator iter = + map.begin(); iter != map.end(); ++iter) { + keys.push_back((*iter).first); + } + return keys; +} + + +} // anonymous namespace + + +/// Internal implementation for the scanner class. +struct engine::scanner::impl : utils::noncopyable { + /// Collection of test programs not yet scanned. + /// + /// The first element in this deque is the "active" test program when + /// first_test_cases is defined. + std::deque< model::test_program_ptr > pending_test_programs; + + /// Current state of the provided filters. + engine::filters_state filters; + + /// Collection of test cases not yet scanned. + /// + /// These are the test cases for the first test program in + /// pending_test_programs when such test program is active. + optional< std::deque< std::string > > first_test_cases; + + /// Constructor. + /// + /// \param test_programs_ Collection of test programs to scan through. + /// \param filters_ List of scan filters as provided by the user. + impl(const model::test_programs_vector& test_programs_, + const std::set< engine::test_filter >& filters_) : + pending_test_programs(test_programs_.begin(), test_programs_.end()), + filters(filters_) + { + } + + /// Positions the internal state to return the next element if any. + /// + /// \post If there are more elements to read, returns true and + /// pending_test_programs[0] points to the active test program and + /// first_test_cases[0] has the test case to be returned. + /// + /// \return True if there is one more result available. + bool + advance(void) + { + for (;;) { + if (first_test_cases) { + if (first_test_cases.get().empty()) { + pending_test_programs.pop_front(); + first_test_cases = none; + } + } + if (pending_test_programs.empty()) { + break; + } + + model::test_program_ptr test_program = pending_test_programs[0]; + if (!first_test_cases) { + if (!filters.match_test_program( + test_program->relative_path())) { + pending_test_programs.pop_front(); + continue; + } + + first_test_cases = utils::make_optional( + map_keys(test_program->test_cases())); + } + + if (!first_test_cases.get().empty()) { + std::deque< std::string >::iterator iter = + first_test_cases.get().begin(); + const std::string test_case_name = *iter; + if (!filters.match_test_case(test_program->relative_path(), + test_case_name)) { + first_test_cases.get().erase(iter); + continue; + } + return true; + } else { + pending_test_programs.pop_front(); + first_test_cases = none; + } + } + return false; + } + + /// Extracts the current element. + /// + /// \pre Must be called only if advance() returns true, and immediately + /// afterwards. + /// + /// \return The current scan result. + engine::scan_result + consume(void) + { + const std::string test_case_name = first_test_cases.get()[0]; + first_test_cases.get().pop_front(); + return scan_result(pending_test_programs[0], test_case_name); + } +}; + + +/// Constructor. +/// +/// \param test_programs Collection of test programs to scan through. +/// \param filters List of scan filters as provided by the user. +engine::scanner::scanner(const model::test_programs_vector& test_programs, + const std::set< engine::test_filter >& filters) : + _pimpl(new impl(test_programs, filters)) +{ +} + + +/// Destructor. +engine::scanner::~scanner(void) +{ +} + + +/// Returns the next scan result. +/// +/// \return A scan result if there are still pending test cases to be processed, +/// or none otherwise. +optional< engine::scan_result > +engine::scanner::yield(void) +{ + if (_pimpl->advance()) { + return utils::make_optional(_pimpl->consume()); + } else { + return none; + } +} + + +/// Checks whether the scan is finished. +/// +/// \return True if the scan is finished, in which case yield() will return +/// none; false otherwise. +bool +engine::scanner::done(void) +{ + return !_pimpl->advance(); +} + + +/// Returns the list of test filters that did not match any test case. +/// +/// \return The collection of unmatched test filters. +std::set< engine::test_filter > +engine::scanner::unused_filters(void) const +{ + return _pimpl->filters.unused(); +} diff --git a/contrib/kyua/engine/scanner.hpp b/contrib/kyua/engine/scanner.hpp new file mode 100644 index 000000000000..722bc9be5f4c --- /dev/null +++ b/contrib/kyua/engine/scanner.hpp @@ -0,0 +1,76 @@ +// 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 engine/scanner.hpp +/// Utilities to scan through list of tests in a test suite. + +#if !defined(ENGINE_SCANNER_HPP) +#define ENGINE_SCANNER_HPP + +#include "engine/scanner_fwd.hpp" + +#include +#include + +#include "engine/filters_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace engine { + + +/// Scans a list of test programs, yielding one test case at a time. +/// +/// This class contains the state necessary to process a collection of test +/// programs (possibly as provided by the Kyuafile) and to extract an arbitrary +/// (test program, test_case) pair out of them one at a time. +/// +/// The scanning algorithm guarantees that test programs are initialized +/// dynamically, should they need to load their list of test cases from disk. +/// +/// The order of the extraction is not guaranteed. +class scanner { + struct impl; + /// Pointer to the internal implementation data. + std::shared_ptr< impl > _pimpl; + +public: + scanner(const model::test_programs_vector&, const std::set< test_filter >&); + ~scanner(void); + + bool done(void); + utils::optional< scan_result > yield(void); + + std::set< test_filter > unused_filters(void) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_SCANNER_HPP) diff --git a/contrib/kyua/engine/scanner_fwd.hpp b/contrib/kyua/engine/scanner_fwd.hpp new file mode 100644 index 000000000000..5c91888fa266 --- /dev/null +++ b/contrib/kyua/engine/scanner_fwd.hpp @@ -0,0 +1,59 @@ +// 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 engine/scanner_fwd.hpp +/// Forward declarations for engine/scanner.hpp + +#if !defined(ENGINE_SCANNER_FWD_HPP) +#define ENGINE_SCANNER_FWD_HPP + +#include +#include + +#include "model/test_program_fwd.hpp" + +namespace engine { + + +/// Result type yielded by the scanner: a (test program, test case name) pair. +/// +/// We must use model::test_program_ptr here instead of model::test_program +/// because we must keep the polimorphic properties of the test program. In +/// particular, if the test program comes from the Kyuafile and is of the type +/// model::lazy_test_program, we must keep access to the loaded list of test +/// cases (which, for obscure reasons, is kept in the subclass). +/// TODO(jmmv): This is ugly, very ugly. There has to be a better way. +typedef std::pair< model::test_program_ptr, std::string > scan_result; + + +class scanner; + + +} // namespace engine + +#endif // !defined(ENGINE_SCANNER_FWD_HPP) diff --git a/contrib/kyua/engine/scanner_test.cpp b/contrib/kyua/engine/scanner_test.cpp new file mode 100644 index 000000000000..f79717eca49e --- /dev/null +++ b/contrib/kyua/engine/scanner_test.cpp @@ -0,0 +1,476 @@ +// 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 "engine/scanner.hpp" + +#include +#include +#include + +#include + +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace fs = utils::fs; + +using utils::optional; + + +namespace { + + +/// Test program that implements a mock test_cases() lazy call. +class mock_test_program : public model::test_program { + /// Number of times test_cases has been called. + mutable std::size_t _num_calls; + + /// Collection of test cases; lazily initialized. + mutable model::test_cases_map _test_cases; + +public: + /// Constructs a new test program. + /// + /// \param binary_ The name of the test program binary relative to root_. + mock_test_program(const fs::path& binary_) : + test_program("unused-interface", binary_, fs::path("unused-root"), + "unused-suite", model::metadata_builder().build(), + model::test_cases_map()), + _num_calls(0) + { + } + + /// Gets or loads the list of test cases from the test program. + /// + /// \return The list of test cases provided by the test program. + const model::test_cases_map& + test_cases(void) const + { + if (_num_calls == 0) { + const model::metadata metadata = model::metadata_builder().build(); + const model::test_case tc1("one", metadata); + const model::test_case tc2("two", metadata); + _test_cases.insert(model::test_cases_map::value_type("one", tc1)); + _test_cases.insert(model::test_cases_map::value_type("two", tc2)); + } + _num_calls++; + return _test_cases; + } + + /// Returns the number of times test_cases() has been called. + /// + /// \return A counter. + std::size_t + num_calls(void) const + { + return _num_calls; + } +}; + + +/// Syntactic sugar to instantiate a test program with various test cases. +/// +/// The scanner only cares about the relative path of the test program object +/// and the names of the test cases. This function helps in instantiating a +/// test program that has the minimum set of details only. +/// +/// \param relative_path Relative path to the test program. +/// \param ... List of test case names to add to the test program. Must be +/// NULL-terminated. +/// +/// \return A constructed test program. +static model::test_program_ptr +new_test_program(const char* relative_path, ...) +{ + model::test_program_builder builder( + "unused-interface", fs::path(relative_path), fs::path("unused-root"), + "unused-suite"); + + va_list ap; + va_start(ap, relative_path); + const char* test_case_name; + while ((test_case_name = va_arg(ap, const char*)) != NULL) { + builder.add_test_case(test_case_name); + } + va_end(ap); + + return builder.build_ptr(); +} + + +/// Yields all test cases in the scanner for simplicity of testing. +/// +/// In most of the tests below, we just care about the scanner returning the +/// full set of matching test cases, not the specific behavior of every single +/// yield() call. This function just returns the whole set, which helps in +/// writing functional tests. +/// +/// \param scanner The scanner on which to iterate. +/// +/// \return The full collection of results yielded by the scanner. +static std::set< engine::scan_result > +yield_all(engine::scanner& scanner) +{ + std::set< engine::scan_result > results; + while (!scanner.done()) { + const optional< engine::scan_result > result = scanner.yield(); + ATF_REQUIRE(result); + results.insert(result.get()); + } + ATF_REQUIRE(!scanner.yield()); + ATF_REQUIRE(scanner.done()); + return results; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__no_tests); +ATF_TEST_CASE_BODY(scanner__no_filters__no_tests) +{ + const model::test_programs_vector test_programs; + const std::set< engine::test_filter > filters; + + engine::scanner scanner(test_programs, filters); + ATF_REQUIRE(scanner.done()); + ATF_REQUIRE(!scanner.yield()); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__one_test_in_one_program); +ATF_TEST_CASE_BODY(scanner__no_filters__one_test_in_one_program) +{ + const model::test_program_ptr test_program = new_test_program( + "dir/program", "lone_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program, "lone_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__one_test_per_many_programs); +ATF_TEST_CASE_BODY(scanner__no_filters__one_test_per_many_programs) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "program2", "bar_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "a/b/c/d/e/program3", "baz_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "foo_test")); + exp_results.insert(engine::scan_result(test_program2, "bar_test")); + exp_results.insert(engine::scan_result(test_program3, "baz_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__many_tests_in_one_program); +ATF_TEST_CASE_BODY(scanner__no_filters__many_tests_in_one_program) +{ + const model::test_program_ptr test_program = new_test_program( + "dir/program", "first_test", "second_test", "third_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program, "first_test")); + exp_results.insert(engine::scan_result(test_program, "second_test")); + exp_results.insert(engine::scan_result(test_program, "third_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__many_tests_per_many_programs); +ATF_TEST_CASE_BODY(scanner__no_filters__many_tests_per_many_programs) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", "bar_test", "baz_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "program2", "lone_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "a/b/c/d/e/program3", "another_test", "last_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "foo_test")); + exp_results.insert(engine::scan_result(test_program1, "bar_test")); + exp_results.insert(engine::scan_result(test_program1, "baz_test")); + exp_results.insert(engine::scan_result(test_program2, "lone_test")); + exp_results.insert(engine::scan_result(test_program3, "another_test")); + exp_results.insert(engine::scan_result(test_program3, "last_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__verify_lazy_loads); +ATF_TEST_CASE_BODY(scanner__no_filters__verify_lazy_loads) +{ + const model::test_program_ptr test_program1(new mock_test_program( + fs::path("first"))); + const mock_test_program* mock_program1 = + dynamic_cast< const mock_test_program* >(test_program1.get()); + const model::test_program_ptr test_program2(new mock_test_program( + fs::path("second"))); + const mock_test_program* mock_program2 = + dynamic_cast< const mock_test_program* >(test_program2.get()); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "one")); + exp_results.insert(engine::scan_result(test_program1, "two")); + exp_results.insert(engine::scan_result(test_program2, "one")); + exp_results.insert(engine::scan_result(test_program2, "two")); + + engine::scanner scanner(test_programs, filters); + std::set< engine::scan_result > results; + ATF_REQUIRE_EQ(0, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + + // This abuses the internal implementation of the scanner by making + // assumptions on the order of the results. + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(1, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(1, mock_program2->num_calls()); + ATF_REQUIRE(scanner.done()); + + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); + + // Make sure we are still talking to the original objects. + for (std::set< engine::scan_result >::const_iterator iter = results.begin(); + iter != results.end(); ++iter) { + const mock_test_program* mock_program = + dynamic_cast< const mock_test_program* >((*iter).first.get()); + ATF_REQUIRE_EQ(1, mock_program->num_calls()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__no_tests); +ATF_TEST_CASE_BODY(scanner__with_filters__no_tests) +{ + const model::test_programs_vector test_programs; + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("foo"), "bar")); + + engine::scanner scanner(test_programs, filters); + ATF_REQUIRE(scanner.done()); + ATF_REQUIRE(!scanner.yield()); + ATF_REQUIRE_EQ(filters, scanner.unused_filters()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__no_matches); +ATF_TEST_CASE_BODY(scanner__with_filters__no_matches) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", "bar_test", "baz_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "dir/program2", "bar_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "program3", "another_test", "last_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("dir/program2"), "baz_test")); + filters.insert(engine::test_filter(fs::path("program4"), "another_test")); + filters.insert(engine::test_filter(fs::path("dir/program3"), "")); + + const std::set< engine::scan_result > exp_results; + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE_EQ(filters, scanner.unused_filters()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__some_matches); +ATF_TEST_CASE_BODY(scanner__with_filters__some_matches) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", "bar_test", "baz_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "dir/program2", "bar_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "program3", "another_test", "last_test", NULL); + const model::test_program_ptr test_program4 = new_test_program( + "program4", "more_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + test_programs.push_back(test_program4); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("dir/program1"), "baz_test")); + filters.insert(engine::test_filter(fs::path("dir/program2"), "foo_test")); + filters.insert(engine::test_filter(fs::path("program3"), "")); + + std::set< engine::test_filter > exp_filters; + exp_filters.insert(engine::test_filter(fs::path("dir/program2"), + "foo_test")); + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "baz_test")); + exp_results.insert(engine::scan_result(test_program3, "another_test")); + exp_results.insert(engine::scan_result(test_program3, "last_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + + ATF_REQUIRE_EQ(exp_filters, scanner.unused_filters()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__verify_lazy_loads); +ATF_TEST_CASE_BODY(scanner__with_filters__verify_lazy_loads) +{ + const model::test_program_ptr test_program1(new mock_test_program( + fs::path("first"))); + const mock_test_program* mock_program1 = + dynamic_cast< const mock_test_program* >(test_program1.get()); + const model::test_program_ptr test_program2(new mock_test_program( + fs::path("second"))); + const mock_test_program* mock_program2 = + dynamic_cast< const mock_test_program* >(test_program2.get()); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("first"), "")); + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "one")); + exp_results.insert(engine::scan_result(test_program1, "two")); + + engine::scanner scanner(test_programs, filters); + std::set< engine::scan_result > results; + ATF_REQUIRE_EQ(0, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + ATF_REQUIRE(scanner.done()); + + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); + + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__no_tests); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__one_test_in_one_program); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__one_test_per_many_programs); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__many_tests_in_one_program); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__many_tests_per_many_programs); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__verify_lazy_loads); + + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__no_tests); + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__no_matches); + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__some_matches); + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__verify_lazy_loads); +} diff --git a/contrib/kyua/engine/scheduler.cpp b/contrib/kyua/engine/scheduler.cpp new file mode 100644 index 000000000000..e7b51d23acca --- /dev/null +++ b/contrib/kyua/engine/scheduler.cpp @@ -0,0 +1,1373 @@ +// 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 "engine/scheduler.hpp" + +extern "C" { +#include +} + +#include +#include +#include +#include +#include + +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "engine/requirements.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#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/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/executor.ipp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/stacktrace.hpp" +#include "utils/stream.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +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 scheduler = engine::scheduler; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Timeout for the test case cleanup operation. +/// +/// TODO(jmmv): This is here only for testing purposes. Maybe we should expose +/// this setting as part of the user_config. +datetime::delta scheduler::cleanup_timeout(60, 0); + + +/// Timeout for the test case listing operation. +/// +/// TODO(jmmv): This is here only for testing purposes. Maybe we should expose +/// this setting as part of the user_config. +datetime::delta scheduler::list_timeout(300, 0); + + +namespace { + + +/// Magic exit status to indicate that the test case was probably skipped. +/// +/// The test case was only skipped if and only if we return this exit code and +/// we find the skipped_cookie file on disk. +static const int exit_skipped = 84; + + +/// Text file containing the skip reason for the test case. +/// +/// This will only be present within unique_work_directory if the test case +/// exited with the exit_skipped code. However, there is no guarantee that the +/// file is there (say if the test really decided to exit with code exit_skipped +/// on its own). +static const char* skipped_cookie = "skipped.txt"; + + +/// Mapping of interface names to interface definitions. +typedef std::map< std::string, std::shared_ptr< scheduler::interface > > + interfaces_map; + + +/// Mapping of interface names to interface definitions. +/// +/// Use register_interface() to add an entry to this global table. +static interfaces_map interfaces; + + +/// Scans the contents of a directory and appends the file listing to a file. +/// +/// \param dir_path The directory to scan. +/// \param output_file The file to which to append the listing. +/// +/// \throw engine::error If there are problems listing the files. +static void +append_files_listing(const fs::path& dir_path, const fs::path& output_file) +{ + std::ofstream output(output_file.c_str(), std::ios::app); + if (!output) + throw engine::error(F("Failed to open output file %s for append") + % output_file); + try { + std::set < std::string > names; + + const fs::directory dir(dir_path); + for (fs::directory::const_iterator iter = dir.begin(); + iter != dir.end(); ++iter) { + if (iter->name != "." && iter->name != "..") + names.insert(iter->name); + } + + if (!names.empty()) { + output << "Files left in work directory after failure: " + << text::join(names, ", ") << '\n'; + } + } catch (const fs::error& e) { + throw engine::error(F("Cannot append files listing to %s: %s") + % output_file % e.what()); + } +} + + +/// Maintenance data held while a test is being executed. +/// +/// This data structure exists from the moment when a test is executed via +/// scheduler::spawn_test() or scheduler::impl::spawn_cleanup() to when it is +/// cleaned up with result_handle::cleanup(). +/// +/// This is a base data type intended to be extended for the test and cleanup +/// cases so that each contains only the relevant data. +struct exec_data : utils::noncopyable { + /// Test program data for this test case. + const model::test_program_ptr test_program; + + /// Name of the test case. + const std::string test_case_name; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_) : + test_program(test_program_), test_case_name(test_case_name_) + { + } + + /// Destructor. + virtual ~exec_data(void) + { + } +}; + + +/// Maintenance data held while a test is being executed. +struct test_exec_data : public exec_data { + /// Test program-specific execution interface. + const std::shared_ptr< scheduler::interface > interface; + + /// User configuration passed to the execution of the test. We need this + /// here to recover it later when chaining the execution of a cleanup + /// routine (if any). + const config::tree user_config; + + /// Whether this test case still needs to have its cleanup routine executed. + /// + /// This is set externally when the cleanup routine is actually invoked to + /// denote that no further attempts shall be made at cleaning this up. + bool needs_cleanup; + + /// The exit_handle for this test once it has completed. + /// + /// This is set externally when the test case has finished, as we need this + /// information to invoke the followup cleanup routine in the right context, + /// as indicated by needs_cleanup. + optional< executor::exit_handle > exit_handle; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param interface_ Test program-specific execution interface. + /// \param user_config_ User configuration passed to the test. + test_exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const std::shared_ptr< scheduler::interface > interface_, + const config::tree& user_config_) : + exec_data(test_program_, test_case_name_), + interface(interface_), user_config(user_config_) + { + const model::test_case& test_case = test_program->find(test_case_name); + needs_cleanup = test_case.get_metadata().has_cleanup(); + } +}; + + +/// Maintenance data held while a test cleanup routine is being executed. +/// +/// Instances of this object are related to a previous test_exec_data, as +/// cleanup routines can only exist once the test has been run. +struct cleanup_exec_data : public exec_data { + /// The exit handle of the test. This is necessary so that we can return + /// the correct exit_handle to the user of the scheduler. + executor::exit_handle body_exit_handle; + + /// The final result of the test's body. This is necessary to compute the + /// right return value for a test with a cleanup routine: the body result is + /// respected if it is a "bad" result; else the result of the cleanup + /// routine is used if it has failed. + model::test_result body_result; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param body_exit_handle_ If not none, exit handle of the body + /// corresponding to the cleanup routine represented by this exec_data. + /// \param body_result_ If not none, result of the body corresponding to the + /// cleanup routine represented by this exec_data. + cleanup_exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const executor::exit_handle& body_exit_handle_, + const model::test_result& body_result_) : + exec_data(test_program_, test_case_name_), + body_exit_handle(body_exit_handle_), body_result(body_result_) + { + } +}; + + +/// Shared pointer to exec_data. +/// +/// We require this because we want exec_data to not be copyable, and thus we +/// cannot just store it in the map without move constructors. +typedef std::shared_ptr< exec_data > exec_data_ptr; + + +/// Mapping of active PIDs to their maintenance data. +typedef std::map< int, exec_data_ptr > exec_data_map; + + +/// Enforces a test program to hold an absolute path. +/// +/// TODO(jmmv): This function (which is a pretty ugly hack) exists because we +/// want the interface hooks to receive a test_program as their argument. +/// However, those hooks run after the test program has been isolated, which +/// means that the current directory has changed since when the test_program +/// objects were created. This causes the absolute_path() method of +/// test_program to return bogus values if the internal representation of their +/// path is relative. We should fix somehow: maybe making the fs module grab +/// its "current_path" view at program startup time; or maybe by grabbing the +/// current path at test_program creation time; or maybe something else. +/// +/// \param program The test program to modify. +/// +/// \return A new test program whose internal paths are absolute. +static model::test_program +force_absolute_paths(const model::test_program program) +{ + const std::string& relative = program.relative_path().str(); + const std::string absolute = program.absolute_path().str(); + + const std::string root = absolute.substr( + 0, absolute.length() - relative.length()); + + return model::test_program( + program.interface_name(), + program.relative_path(), fs::path(root), + program.test_suite_name(), + program.get_metadata(), program.test_cases()); +} + + +/// Functor to list the test cases of a test program. +class list_test_cases { + /// Interface of the test program to execute. + std::shared_ptr< scheduler::interface > _interface; + + /// Test program to execute. + const model::test_program _test_program; + + /// User-provided configuration variables. + const config::tree& _user_config; + +public: + /// Constructor. + /// + /// \param interface Interface of the test program to execute. + /// \param test_program Test program to execute. + /// \param user_config User-provided configuration variables. + list_test_cases( + const std::shared_ptr< scheduler::interface > interface, + const model::test_program* test_program, + const config::tree& user_config) : + _interface(interface), + _test_program(force_absolute_paths(*test_program)), + _user_config(user_config) + { + } + + /// Body of the subprocess. + void + operator()(const fs::path& /* control_directory */) + { + const config::properties_map vars = scheduler::generate_config( + _user_config, _test_program.test_suite_name()); + _interface->exec_list(_test_program, vars); + } +}; + + +/// Functor to execute a test program in a child process. +class run_test_program { + /// Interface of the test program to execute. + std::shared_ptr< scheduler::interface > _interface; + + /// Test program to execute. + const model::test_program _test_program; + + /// Name of the test case to execute. + const std::string& _test_case_name; + + /// User-provided configuration variables. + const config::tree& _user_config; + + /// Verifies if the test case needs to be skipped or not. + /// + /// We could very well run this on the scheduler parent process before + /// issuing the fork. However, doing this here in the child process is + /// better for two reasons: first, it allows us to continue using the simple + /// spawn/wait abstraction of the scheduler; and, second, we parallelize the + /// requirements checks among tests. + /// + /// \post If the test's preconditions are not met, the caller process is + /// terminated with a special exit code and a "skipped cookie" is written to + /// the disk with the reason for the failure. + /// + /// \param skipped_cookie_path File to create with the skip reason details + /// if this test is skipped. + void + do_requirements_check(const fs::path& skipped_cookie_path) + { + const model::test_case& test_case = _test_program.find( + _test_case_name); + + const std::string skip_reason = engine::check_reqs( + test_case.get_metadata(), _user_config, + _test_program.test_suite_name(), + fs::current_path()); + if (skip_reason.empty()) + return; + + std::ofstream output(skipped_cookie_path.c_str()); + if (!output) { + std::perror((F("Failed to open %s for write") % + skipped_cookie_path).str().c_str()); + std::abort(); + } + output << skip_reason; + output.close(); + + // Abruptly terminate the process. We don't want to run any destructors + // inherited from the parent process by mistake, which could, for + // example, delete our own control files! + ::_exit(exit_skipped); + } + +public: + /// Constructor. + /// + /// \param interface Interface of the test program to execute. + /// \param test_program Test program to execute. + /// \param test_case_name Name of the test case to execute. + /// \param user_config User-provided configuration variables. + run_test_program( + const std::shared_ptr< scheduler::interface > interface, + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config) : + _interface(interface), + _test_program(force_absolute_paths(*test_program)), + _test_case_name(test_case_name), + _user_config(user_config) + { + } + + /// Body of the subprocess. + /// + /// \param control_directory The testcase directory where files will be + /// read from. + void + operator()(const fs::path& control_directory) + { + const model::test_case& test_case = _test_program.find( + _test_case_name); + if (test_case.fake_result()) + ::_exit(EXIT_SUCCESS); + + do_requirements_check(control_directory / skipped_cookie); + + const config::properties_map vars = scheduler::generate_config( + _user_config, _test_program.test_suite_name()); + _interface->exec_test(_test_program, _test_case_name, vars, + control_directory); + } +}; + + +/// Functor to execute a test program in a child process. +class run_test_cleanup { + /// Interface of the test program to execute. + std::shared_ptr< scheduler::interface > _interface; + + /// Test program to execute. + const model::test_program _test_program; + + /// Name of the test case to execute. + const std::string& _test_case_name; + + /// User-provided configuration variables. + const config::tree& _user_config; + +public: + /// Constructor. + /// + /// \param interface Interface of the test program to execute. + /// \param test_program Test program to execute. + /// \param test_case_name Name of the test case to execute. + /// \param user_config User-provided configuration variables. + run_test_cleanup( + const std::shared_ptr< scheduler::interface > interface, + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config) : + _interface(interface), + _test_program(force_absolute_paths(*test_program)), + _test_case_name(test_case_name), + _user_config(user_config) + { + } + + /// Body of the subprocess. + /// + /// \param control_directory The testcase directory where cleanup will be + /// run from. + void + operator()(const fs::path& control_directory) + { + const config::properties_map vars = scheduler::generate_config( + _user_config, _test_program.test_suite_name()); + _interface->exec_cleanup(_test_program, _test_case_name, vars, + control_directory); + } +}; + + +/// Obtains the right scheduler interface for a given test program. +/// +/// \param name The name of the interface of the test program. +/// +/// \return An scheduler interface. +std::shared_ptr< scheduler::interface > +find_interface(const std::string& name) +{ + const interfaces_map::const_iterator iter = interfaces.find(name); + PRE(interfaces.find(name) != interfaces.end()); + return (*iter).second; +} + + +} // anonymous namespace + + +void +scheduler::interface::exec_cleanup( + const model::test_program& /* test_program */, + const std::string& /* test_case_name */, + const config::properties_map& /* vars */, + const utils::fs::path& /* control_directory */) const +{ + // Most test interfaces do not support standalone cleanup routines so + // provide a default implementation that does nothing. + UNREACHABLE_MSG("exec_cleanup not implemented for an interface that " + "supports standalone cleanup routines"); +} + + +/// Internal implementation of a lazy_test_program. +struct engine::scheduler::lazy_test_program::impl : utils::noncopyable { + /// Whether the test cases list has been yet loaded or not. + bool _loaded; + + /// User configuration to pass to the test program list operation. + config::tree _user_config; + + /// Scheduler context to use to load test cases. + scheduler::scheduler_handle& _scheduler_handle; + + /// Constructor. + /// + /// \param user_config_ User configuration to pass to the test program list + /// operation. + /// \param scheduler_handle_ Scheduler context to use when loading test + /// cases. + impl(const config::tree& user_config_, + scheduler::scheduler_handle& scheduler_handle_) : + _loaded(false), _user_config(user_config_), + _scheduler_handle(scheduler_handle_) + { + } +}; + + +/// Constructs a new test program. +/// +/// \param interface_name_ Name of the test program interface. +/// \param binary_ The name of the test program binary relative to root_. +/// \param root_ The root of the test suite containing the test program. +/// \param test_suite_name_ The name of the test suite this program belongs to. +/// \param md_ Metadata of the test program. +/// \param user_config_ User configuration to pass to the scheduler. +/// \param scheduler_handle_ Scheduler context to use to load test cases. +scheduler::lazy_test_program::lazy_test_program( + const std::string& interface_name_, + const fs::path& binary_, + const fs::path& root_, + const std::string& test_suite_name_, + const model::metadata& md_, + const config::tree& user_config_, + scheduler::scheduler_handle& scheduler_handle_) : + test_program(interface_name_, binary_, root_, test_suite_name_, md_, + model::test_cases_map()), + _pimpl(new impl(user_config_, scheduler_handle_)) +{ +} + + +/// Gets or loads the list of test cases from the test program. +/// +/// \return The list of test cases provided by the test program. +const model::test_cases_map& +scheduler::lazy_test_program::test_cases(void) const +{ + _pimpl->_scheduler_handle.check_interrupt(); + + if (!_pimpl->_loaded) { + const model::test_cases_map tcs = _pimpl->_scheduler_handle.list_tests( + this, _pimpl->_user_config); + + // Due to the restrictions on when set_test_cases() may be called (as a + // way to lazily initialize the test cases list before it is ever + // returned), this cast is valid. + const_cast< scheduler::lazy_test_program* >(this)->set_test_cases(tcs); + + _pimpl->_loaded = true; + + _pimpl->_scheduler_handle.check_interrupt(); + } + + INV(_pimpl->_loaded); + return test_program::test_cases(); +} + + +/// Internal implementation for the result_handle class. +struct engine::scheduler::result_handle::bimpl : utils::noncopyable { + /// Generic executor exit handle for this result handle. + executor::exit_handle generic; + + /// Mutable pointer to the corresponding scheduler state. + /// + /// This object references a member of the scheduler_handle that yielded + /// this result_handle instance. We need this direct access to clean up + /// after ourselves when the result is destroyed. + exec_data_map& all_exec_data; + + /// Constructor. + /// + /// \param generic_ Generic executor exit handle for this result handle. + /// \param [in,out] all_exec_data_ Global object keeping track of all active + /// executions for an scheduler. This is a pointer to a member of the + /// scheduler_handle object. + bimpl(const executor::exit_handle generic_, exec_data_map& all_exec_data_) : + generic(generic_), all_exec_data(all_exec_data_) + { + } + + /// Destructor. + ~bimpl(void) + { + LD(F("Removing %s from all_exec_data") % generic.original_pid()); + all_exec_data.erase(generic.original_pid()); + } +}; + + +/// Constructor. +/// +/// \param pbimpl Constructed internal implementation. +scheduler::result_handle::result_handle(std::shared_ptr< bimpl > pbimpl) : + _pbimpl(pbimpl) +{ +} + + +/// Destructor. +scheduler::result_handle::~result_handle(void) +{ +} + + +/// Cleans up the test case results. +/// +/// 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 +scheduler::result_handle::cleanup(void) +{ + _pbimpl->generic.cleanup(); +} + + +/// Returns the original PID corresponding to this result. +/// +/// \return An exec_handle. +int +scheduler::result_handle::original_pid(void) const +{ + return _pbimpl->generic.original_pid(); +} + + +/// Returns the timestamp of when spawn_test was called. +/// +/// \return A timestamp. +const datetime::timestamp& +scheduler::result_handle::start_time(void) const +{ + return _pbimpl->generic.start_time(); +} + + +/// Returns the timestamp of when wait_any_test returned this object. +/// +/// \return A timestamp. +const datetime::timestamp& +scheduler::result_handle::end_time(void) const +{ + return _pbimpl->generic.end_time(); +} + + +/// Returns the path to the test-specific work directory. +/// +/// This is guaranteed to be clear of files created by the scheduler. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +scheduler::result_handle::work_directory(void) const +{ + return _pbimpl->generic.work_directory(); +} + + +/// Returns the path to the test's stdout file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +scheduler::result_handle::stdout_file(void) const +{ + return _pbimpl->generic.stdout_file(); +} + + +/// Returns the path to the test's stderr file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +scheduler::result_handle::stderr_file(void) const +{ + return _pbimpl->generic.stderr_file(); +} + + +/// Internal implementation for the test_result_handle class. +struct engine::scheduler::test_result_handle::impl : utils::noncopyable { + /// Test program data for this test case. + model::test_program_ptr test_program; + + /// Name of the test case. + std::string test_case_name; + + /// The actual result of the test execution. + const model::test_result test_result; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param test_result_ The actual result of the test execution. + impl(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const model::test_result& test_result_) : + test_program(test_program_), + test_case_name(test_case_name_), + test_result(test_result_) + { + } +}; + + +/// Constructor. +/// +/// \param pbimpl Constructed internal implementation for the base object. +/// \param pimpl Constructed internal implementation. +scheduler::test_result_handle::test_result_handle( + std::shared_ptr< bimpl > pbimpl, std::shared_ptr< impl > pimpl) : + result_handle(pbimpl), _pimpl(pimpl) +{ +} + + +/// Destructor. +scheduler::test_result_handle::~test_result_handle(void) +{ +} + + +/// Returns the test program that yielded this result. +/// +/// \return A test program. +const model::test_program_ptr +scheduler::test_result_handle::test_program(void) const +{ + return _pimpl->test_program; +} + + +/// Returns the name of the test case that yielded this result. +/// +/// \return A test case name +const std::string& +scheduler::test_result_handle::test_case_name(void) const +{ + return _pimpl->test_case_name; +} + + +/// Returns the actual result of the test execution. +/// +/// \return A test result. +const model::test_result& +scheduler::test_result_handle::test_result(void) const +{ + return _pimpl->test_result; +} + + +/// Internal implementation for the scheduler_handle. +struct engine::scheduler::scheduler_handle::impl : utils::noncopyable { + /// Generic executor instance encapsulated by this one. + executor::executor_handle generic; + + /// Mapping of exec handles to the data required at run time. + exec_data_map all_exec_data; + + /// Collection of test_exec_data objects. + typedef std::vector< const test_exec_data* > test_exec_data_vector; + + /// Constructor. + impl(void) : generic(executor::setup()) + { + } + + /// Destructor. + /// + /// This runs any pending cleanup routines, which should only happen if the + /// scheduler is abruptly terminated (aka if a signal is received). + ~impl(void) + { + const test_exec_data_vector tests_data = tests_needing_cleanup(); + + for (test_exec_data_vector::const_iterator iter = tests_data.begin(); + iter != tests_data.end(); ++iter) { + const test_exec_data* test_data = *iter; + + try { + sync_cleanup(test_data); + } catch (const std::runtime_error& e) { + LW(F("Failed to run cleanup routine for %s:%s on abrupt " + "termination") + % test_data->test_program->relative_path() + % test_data->test_case_name); + } + } + } + + /// Finds any pending exec_datas that correspond to tests needing cleanup. + /// + /// \return The collection of test_exec_data objects that have their + /// needs_cleanup property set to true. + test_exec_data_vector + tests_needing_cleanup(void) + { + test_exec_data_vector tests_data; + + for (exec_data_map::const_iterator iter = all_exec_data.begin(); + iter != all_exec_data.end(); ++iter) { + const exec_data_ptr data = (*iter).second; + + try { + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *data.get()); + if (test_data->needs_cleanup) { + tests_data.push_back(test_data); + test_data->needs_cleanup = false; + } + } catch (const std::bad_cast& e) { + // Do nothing for cleanup_exec_data objects. + } + } + + return tests_data; + } + + /// Cleans up a single test case synchronously. + /// + /// \param test_data The data of the previously executed test case to be + /// cleaned up. + void + sync_cleanup(const test_exec_data* test_data) + { + // The message in this result should never be seen by the user, but use + // something reasonable just in case it leaks and we need to pinpoint + // the call site. + model::test_result result(model::test_result_broken, + "Test case died abruptly"); + + const executor::exec_handle cleanup_handle = spawn_cleanup( + test_data->test_program, test_data->test_case_name, + test_data->user_config, test_data->exit_handle.get(), + result); + generic.wait(cleanup_handle); + } + + /// Forks and executes a test case cleanup routine asynchronously. + /// + /// \param test_program The container test program. + /// \param test_case_name The name of the test case to run. + /// \param user_config User-provided configuration variables. + /// \param body_handle The exit handle of the test case's corresponding + /// body. The cleanup will be executed in the same context. + /// \param body_result The result of the test case's corresponding body. + /// + /// \return A handle for the background operation. Used to match the result + /// of the execution returned by wait_any() with this invocation. + executor::exec_handle + spawn_cleanup(const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config, + const executor::exit_handle& body_handle, + const model::test_result& body_result) + { + generic.check_interrupt(); + + const std::shared_ptr< scheduler::interface > interface = + find_interface(test_program->interface_name()); + + LI(F("Spawning %s:%s (cleanup)") % test_program->absolute_path() % + test_case_name); + + const executor::exec_handle handle = generic.spawn_followup( + run_test_cleanup(interface, test_program, test_case_name, + user_config), + body_handle, cleanup_timeout); + + const exec_data_ptr data(new cleanup_exec_data( + test_program, test_case_name, body_handle, body_result)); + LD(F("Inserting %s into all_exec_data (cleanup)") % handle.pid()); + INV_MSG(all_exec_data.find(handle.pid()) == all_exec_data.end(), + F("PID %s already in all_exec_data; not properly cleaned " + "up or reused too fast") % handle.pid());; + all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); + + return handle; + } +}; + + +/// Constructor. +scheduler::scheduler_handle::scheduler_handle(void) : _pimpl(new impl()) +{ +} + + +/// Destructor. +scheduler::scheduler_handle::~scheduler_handle(void) +{ +} + + +/// Queries the path to the root of the work directory for all tests. +/// +/// \return A path. +const fs::path& +scheduler::scheduler_handle::root_work_directory(void) const +{ + return _pimpl->generic.root_work_directory(); +} + + +/// Cleans up the scheduler 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 scheduler. +void +scheduler::scheduler_handle::cleanup(void) +{ + _pimpl->generic.cleanup(); +} + + +/// Checks if the given interface name is valid. +/// +/// \param name The name of the interface to validate. +/// +/// \throw engine::error If the given interface is not supported. +void +scheduler::ensure_valid_interface(const std::string& name) +{ + if (interfaces.find(name) == interfaces.end()) + throw engine::error(F("Unsupported test interface '%s'") % name); +} + + +/// Registers a new interface. +/// +/// \param name The name of the interface. Must not have yet been registered. +/// \param spec Interface specification. +void +scheduler::register_interface(const std::string& name, + const std::shared_ptr< interface > spec) +{ + PRE(interfaces.find(name) == interfaces.end()); + interfaces.insert(interfaces_map::value_type(name, spec)); +} + + +/// Returns the names of all registered interfaces. +/// +/// \return A collection of interface names. +std::set< std::string > +scheduler::registered_interface_names(void) +{ + std::set< std::string > names; + for (interfaces_map::const_iterator iter = interfaces.begin(); + iter != interfaces.end(); ++iter) { + names.insert((*iter).first); + } + return names; +} + + +/// Initializes the scheduler. +/// +/// \pre This function can only be called if there is no other scheduler_handle +/// object alive. +/// +/// \return A handle to the operations of the scheduler. +scheduler::scheduler_handle +scheduler::setup(void) +{ + return scheduler_handle(); +} + + +/// Retrieves the list of test cases from a test program. +/// +/// This operation is currently synchronous. +/// +/// This operation should never throw. Any errors during the processing of the +/// test case list are subsumed into a single test case in the return value that +/// represents the failed retrieval. +/// +/// \param test_program The test program from which to obtain the list of test +/// cases. +/// \param user_config User-provided configuration variables. +/// +/// \return The list of test cases. +model::test_cases_map +scheduler::scheduler_handle::list_tests( + const model::test_program* test_program, + const config::tree& user_config) +{ + _pimpl->generic.check_interrupt(); + + const std::shared_ptr< scheduler::interface > interface = find_interface( + test_program->interface_name()); + + try { + const executor::exec_handle exec_handle = _pimpl->generic.spawn( + list_test_cases(interface, test_program, user_config), + list_timeout, none); + executor::exit_handle exit_handle = _pimpl->generic.wait(exec_handle); + + const model::test_cases_map test_cases = interface->parse_list( + exit_handle.status(), + exit_handle.stdout_file(), + exit_handle.stderr_file()); + + exit_handle.cleanup(); + + if (test_cases.empty()) + throw std::runtime_error("Empty test cases list"); + + return test_cases; + } catch (const std::runtime_error& e) { + // TODO(jmmv): This is a very ugly workaround for the fact that we + // cannot report failures at the test-program level. + LW(F("Failed to load test cases list: %s") % e.what()); + model::test_cases_map fake_test_cases; + fake_test_cases.insert(model::test_cases_map::value_type( + "__test_cases_list__", + model::test_case( + "__test_cases_list__", + "Represents the correct processing of the test cases list", + model::test_result(model::test_result_broken, e.what())))); + return fake_test_cases; + } +} + + +/// Forks and executes a test case asynchronously. +/// +/// Note that the caller needn't know if the test has a cleanup routine or not. +/// If there indeed is a cleanup routine, we trigger it at wait_any() time. +/// +/// \param test_program The container test program. +/// \param test_case_name The name of the test case to run. +/// \param user_config User-provided configuration variables. +/// +/// \return A handle for the background operation. Used to match the result of +/// the execution returned by wait_any() with this invocation. +scheduler::exec_handle +scheduler::scheduler_handle::spawn_test( + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config) +{ + _pimpl->generic.check_interrupt(); + + const std::shared_ptr< scheduler::interface > interface = find_interface( + test_program->interface_name()); + + LI(F("Spawning %s:%s") % test_program->absolute_path() % test_case_name); + + const model::test_case& test_case = test_program->find(test_case_name); + + optional< passwd::user > unprivileged_user; + if (user_config.is_set("unprivileged_user") && + test_case.get_metadata().required_user() == "unprivileged") { + unprivileged_user = user_config.lookup< engine::user_node >( + "unprivileged_user"); + } + + const executor::exec_handle handle = _pimpl->generic.spawn( + run_test_program(interface, test_program, test_case_name, + user_config), + test_case.get_metadata().timeout(), + unprivileged_user); + + const exec_data_ptr data(new test_exec_data( + test_program, test_case_name, interface, user_config)); + LD(F("Inserting %s into all_exec_data") % handle.pid()); + INV_MSG( + _pimpl->all_exec_data.find(handle.pid()) == _pimpl->all_exec_data.end(), + F("PID %s already in all_exec_data; not cleaned up or reused too fast") + % handle.pid());; + _pimpl->all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); + + return handle.pid(); +} + + +/// Waits for completion of any forked test case. +/// +/// Note that if the terminated test case has a cleanup routine, this function +/// is the one in charge of spawning the cleanup routine asynchronously. +/// +/// \return The result of the execution of a subprocess. This is a dynamically +/// allocated object because the scheduler can spawn subprocesses of various +/// types and, at wait time, we don't know upfront what we are going to get. +scheduler::result_handle_ptr +scheduler::scheduler_handle::wait_any(void) +{ + _pimpl->generic.check_interrupt(); + + executor::exit_handle handle = _pimpl->generic.wait_any(); + + const exec_data_map::iterator iter = _pimpl->all_exec_data.find( + handle.original_pid()); + exec_data_ptr data = (*iter).second; + + utils::dump_stacktrace_if_available(data->test_program->absolute_path(), + _pimpl->generic, handle); + + optional< model::test_result > result; + try { + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *data.get()); + LD(F("Got %s from all_exec_data") % handle.original_pid()); + + test_data->exit_handle = handle; + + const model::test_case& test_case = test_data->test_program->find( + test_data->test_case_name); + + result = test_case.fake_result(); + + if (!result && handle.status() && handle.status().get().exited() && + handle.status().get().exitstatus() == exit_skipped) { + // If the test's process terminated with our magic "exit_skipped" + // status, there are two cases to handle. The first is the case + // where the "skipped cookie" exists, in which case we never got to + // actually invoke the test program; if that's the case, handle it + // here. The second case is where the test case actually decided to + // exit with the "exit_skipped" status; in that case, just fall back + // to the regular status handling. + const fs::path skipped_cookie_path = handle.control_directory() / + skipped_cookie; + std::ifstream input(skipped_cookie_path.c_str()); + if (input) { + result = model::test_result(model::test_result_skipped, + utils::read_stream(input)); + input.close(); + + // If we determined that the test needs to be skipped, we do not + // want to run the cleanup routine because doing so could result + // in errors. However, we still want to run the cleanup routine + // if the test's body reports a skip (because actions could have + // already been taken). + test_data->needs_cleanup = false; + } + } + if (!result) { + result = test_data->interface->compute_result( + handle.status(), + handle.control_directory(), + handle.stdout_file(), + handle.stderr_file()); + } + INV(result); + + if (!result.get().good()) { + append_files_listing(handle.work_directory(), + handle.stderr_file()); + } + + if (test_data->needs_cleanup) { + INV(test_case.get_metadata().has_cleanup()); + // The test body has completed and we have processed it. If there + // is a cleanup routine, trigger it now and wait for any other test + // completion. The caller never knows about cleanup routines. + _pimpl->spawn_cleanup(test_data->test_program, + test_data->test_case_name, + test_data->user_config, handle, result.get()); + test_data->needs_cleanup = false; + + // TODO(jmmv): Chaining this call is ugly. We'd be better off by + // looping over terminated processes until we got a result suitable + // for user consumption. For the time being this is good enough and + // not a problem because the call chain won't get big: the majority + // of test cases do not have cleanup routines. + return wait_any(); + } + } catch (const std::bad_cast& e) { + const cleanup_exec_data* cleanup_data = + &dynamic_cast< const cleanup_exec_data& >(*data.get()); + LD(F("Got %s from all_exec_data (cleanup)") % handle.original_pid()); + + // Handle the completion of cleanup subprocesses internally: the caller + // is not aware that these exist so, when we return, we must return the + // data for the original test that triggered this routine. For example, + // because the caller wants to see the exact same exec_handle that was + // returned by spawn_test. + + const model::test_result& body_result = cleanup_data->body_result; + if (body_result.good()) { + if (!handle.status()) { + result = model::test_result(model::test_result_broken, + "Test case cleanup timed out"); + } else { + if (!handle.status().get().exited() || + handle.status().get().exitstatus() != EXIT_SUCCESS) { + result = model::test_result( + model::test_result_broken, + "Test case cleanup did not terminate successfully"); + } else { + result = body_result; + } + } + } else { + result = body_result; + } + + // Untrack the cleanup process. This must be done explicitly because we + // do not create a result_handle object for the cleanup, and that is the + // one in charge of doing so in the regular (non-cleanup) case. + LD(F("Removing %s from all_exec_data (cleanup) in favor of %s") + % handle.original_pid() + % cleanup_data->body_exit_handle.original_pid()); + _pimpl->all_exec_data.erase(handle.original_pid()); + + handle = cleanup_data->body_exit_handle; + } + INV(result); + + std::shared_ptr< result_handle::bimpl > result_handle_bimpl( + new result_handle::bimpl(handle, _pimpl->all_exec_data)); + std::shared_ptr< test_result_handle::impl > test_result_handle_impl( + new test_result_handle::impl( + data->test_program, data->test_case_name, result.get())); + return result_handle_ptr(new test_result_handle(result_handle_bimpl, + test_result_handle_impl)); +} + + +/// Forks and executes a test case synchronously for debugging. +/// +/// \pre No other processes should be in execution by the scheduler. +/// +/// \param test_program The container test program. +/// \param test_case_name The name of the test case to run. +/// \param user_config User-provided configuration variables. +/// \param stdout_target File to which to write the stdout of the test case. +/// \param stderr_target File to which to write the stderr of the test case. +/// +/// \return The result of the execution of the test. +scheduler::result_handle_ptr +scheduler::scheduler_handle::debug_test( + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config, + const fs::path& stdout_target, + const fs::path& stderr_target) +{ + const exec_handle exec_handle = spawn_test( + test_program, test_case_name, user_config); + result_handle_ptr result_handle = wait_any(); + + // TODO(jmmv): We need to do this while the subprocess is alive. This is + // important for debugging purposes, as we should see the contents of stdout + // or stderr as they come in. + // + // Unfortunately, we cannot do so. We cannot just read and block from a + // file, waiting for further output to appear... as this only works on pipes + // or sockets. We need a better interface for this whole thing. + { + std::auto_ptr< std::ostream > output = utils::open_ostream( + stdout_target); + *output << utils::read_file(result_handle->stdout_file()); + } + { + std::auto_ptr< std::ostream > output = utils::open_ostream( + stderr_target); + *output << utils::read_file(result_handle->stderr_file()); + } + + INV(result_handle->original_pid() == exec_handle); + return result_handle; +} + + +/// 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 +scheduler::scheduler_handle::check_interrupt(void) const +{ + _pimpl->generic.check_interrupt(); +} + + +/// Queries the current execution context. +/// +/// \return The queried context. +model::context +scheduler::current_context(void) +{ + return model::context(fs::current_path(), utils::getallenv()); +} + + +/// Generates the set of configuration variables for a test program. +/// +/// \param user_config The configuration variables provided by the user. +/// \param test_suite The name of the test suite. +/// +/// \return The mapping of configuration variables for the test program. +config::properties_map +scheduler::generate_config(const config::tree& user_config, + const std::string& test_suite) +{ + config::properties_map props; + + try { + props = user_config.all_properties(F("test_suites.%s") % test_suite, + true); + } catch (const config::unknown_key_error& unused_error) { + // Ignore: not all test suites have entries in the configuration. + } + + // TODO(jmmv): This is a hack that exists for the ATF interface only, so it + // should be moved there. + if (user_config.is_set("unprivileged_user")) { + const passwd::user& user = + user_config.lookup< engine::user_node >("unprivileged_user"); + props["unprivileged-user"] = user.name; + } + + return props; +} diff --git a/contrib/kyua/engine/scheduler.hpp b/contrib/kyua/engine/scheduler.hpp new file mode 100644 index 000000000000..24ff0b5a26fc --- /dev/null +++ b/contrib/kyua/engine/scheduler.hpp @@ -0,0 +1,282 @@ +// 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 engine/scheduler.hpp +/// Multiprogrammed executor of test related operations. +/// +/// The scheduler's public interface exposes test cases as "black boxes". The +/// handling of cleanup routines is completely hidden from the caller and +/// happens in two cases: first, once a test case completes; and, second, in the +/// case of abrupt termination due to the reception of a signal. +/// +/// Hiding cleanup routines from the caller is an attempt to keep the logic of +/// execution and results handling in a single place. Otherwise, the various +/// drivers (say run_tests and debug_test) would need to replicate the handling +/// of this logic, which is tricky in itself (particularly due to signal +/// handling) and would lead to inconsistencies. +/// +/// Handling cleanup routines in the manner described above is *incredibly +/// complicated* (insane, actually) as you will see from the code. The +/// complexity will bite us in the future (today is 2015-06-26). Switching to a +/// threads-based implementation would probably simplify the code flow +/// significantly and allow parallelization of the test case listings in a +/// reasonable manner, though it depends on whether we can get clean handling of +/// signals and on whether we could use C++11's std::thread. (Is this a to-do? +/// Maybe. Maybe not.) +/// +/// See the documentation in utils/process/executor.hpp for details on +/// the expected workflow of these classes. + +#if !defined(ENGINE_SCHEDULER_HPP) +#define ENGINE_SCHEDULER_HPP + +#include "engine/scheduler_fwd.hpp" + +#include +#include +#include + +#include "model/context_fwd.hpp" +#include "model/metadata_fwd.hpp" +#include "model/test_case_fwd.hpp" +#include "model/test_program.hpp" +#include "model/test_result_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/process/executor_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace engine { +namespace scheduler { + + +/// Abstract interface of a test program scheduler interface. +/// +/// This interface defines the test program-specific operations that need to be +/// invoked at different points during the execution of a given test case. The +/// scheduler internally instantiates one of these for every test case. +class interface { +public: + /// Destructor. + virtual ~interface() {} + + /// Executes a test program's list operation. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param vars User-provided variables to pass to the test program. + virtual void exec_list(const model::test_program& test_program, + const utils::config::properties_map& vars) + const UTILS_NORETURN = 0; + + /// Computes the test cases list of a test program. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A list of test cases. + virtual model::test_cases_map parse_list( + const utils::optional< utils::process::status >& status, + const utils::fs::path& stdout_path, + const utils::fs::path& stderr_path) const = 0; + + /// Executes a test case of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke. + /// \param vars User-provided variables to pass to the test program. + /// \param control_directory Directory where the interface may place control + /// files. + virtual void exec_test(const model::test_program& test_program, + const std::string& test_case_name, + const utils::config::properties_map& vars, + const utils::fs::path& control_directory) + const UTILS_NORETURN = 0; + + /// Executes a test cleanup routine of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke. + /// \param vars User-provided variables to pass to the test program. + /// \param control_directory Directory where the interface may place control + /// files. + virtual void exec_cleanup(const model::test_program& test_program, + const std::string& test_case_name, + const utils::config::properties_map& vars, + const utils::fs::path& control_directory) + const UTILS_NORETURN; + + /// Computes the result of a test case based on its termination status. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param control_directory Directory where the interface may have placed + /// control files. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A test result. + virtual model::test_result compute_result( + const utils::optional< utils::process::status >& status, + const utils::fs::path& control_directory, + const utils::fs::path& stdout_path, + const utils::fs::path& stderr_path) const = 0; +}; + + +/// Implementation of a test program with lazy loading of test cases. +class lazy_test_program : public model::test_program { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + +public: + lazy_test_program(const std::string&, const utils::fs::path&, + const utils::fs::path&, const std::string&, + const model::metadata&, + const utils::config::tree&, + scheduler_handle&); + + const model::test_cases_map& test_cases(void) const; +}; + + +/// Base type containing the results of the execution of a subprocess. +class result_handle { +protected: + struct bimpl; + +private: + /// Pointer to internal implementation of the base type. + std::shared_ptr< bimpl > _pbimpl; + +protected: + friend class scheduler_handle; + result_handle(std::shared_ptr< bimpl >); + +public: + virtual ~result_handle(void) = 0; + + void cleanup(void); + + int original_pid(void) const; + const utils::datetime::timestamp& start_time() const; + const utils::datetime::timestamp& end_time() 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 all test termination data and accessor to cleanup operations. +class test_result_handle : public result_handle { + struct impl; + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class scheduler_handle; + test_result_handle(std::shared_ptr< bimpl >, std::shared_ptr< impl >); + +public: + ~test_result_handle(void); + + const model::test_program_ptr test_program(void) const; + const std::string& test_case_name(void) const; + const model::test_result& test_result(void) const; +}; + + +/// Stateful interface to the multiprogrammed execution of tests. +class scheduler_handle { + struct impl; + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend scheduler_handle setup(void); + scheduler_handle(void); + +public: + ~scheduler_handle(void); + + const utils::fs::path& root_work_directory(void) const; + + void cleanup(void); + + model::test_cases_map list_tests(const model::test_program*, + const utils::config::tree&); + exec_handle spawn_test(const model::test_program_ptr, + const std::string&, + const utils::config::tree&); + result_handle_ptr wait_any(void); + + result_handle_ptr debug_test(const model::test_program_ptr, + const std::string&, + const utils::config::tree&, + const utils::fs::path&, + const utils::fs::path&); + + void check_interrupt(void) const; +}; + + +extern utils::datetime::delta cleanup_timeout; +extern utils::datetime::delta list_timeout; + + +void ensure_valid_interface(const std::string&); +void register_interface(const std::string&, const std::shared_ptr< interface >); +std::set< std::string > registered_interface_names(void); +scheduler_handle setup(void); + +model::context current_context(void); +utils::config::properties_map generate_config(const utils::config::tree&, + const std::string&); + + +} // namespace scheduler +} // namespace engine + + +#endif // !defined(ENGINE_SCHEDULER_HPP) diff --git a/contrib/kyua/engine/scheduler_fwd.hpp b/contrib/kyua/engine/scheduler_fwd.hpp new file mode 100644 index 000000000000..f61b084e5a8d --- /dev/null +++ b/contrib/kyua/engine/scheduler_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 engine/scheduler_fwd.hpp +/// Forward declarations for engine/scheduler.hpp + +#if !defined(ENGINE_SCHEDULER_FWD_HPP) +#define ENGINE_SCHEDULER_FWD_HPP + +#include + +namespace engine { +namespace scheduler { + + +/// Unique identifier for in-flight execution operations. +/// +/// TODO(jmmv): Might be worth to drop altogether and just use "int". The type +/// difference with executor::exec_handle is confusing. +typedef int exec_handle; + + +class scheduler_handle; +class interface; +class result_handle; +class test_result_handle; + + +/// Pointer to a dynamically-allocated result_handle. +typedef std::shared_ptr< result_handle > result_handle_ptr; + + +} // namespace scheduler +} // namespace engine + +#endif // !defined(ENGINE_SCHEDULER_FWD_HPP) diff --git a/contrib/kyua/engine/scheduler_test.cpp b/contrib/kyua/engine/scheduler_test.cpp new file mode 100644 index 000000000000..e144761d8f01 --- /dev/null +++ b/contrib/kyua/engine/scheduler_test.cpp @@ -0,0 +1,1242 @@ +// 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 "engine/scheduler.hpp" + +extern "C" { +#include + +#include +#include +} + +#include +#include +#include +#include + +#include + +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#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/stacktrace.hpp" +#include "utils/stream.hpp" +#include "utils/test_utils.ipp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace scheduler = engine::scheduler; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Checks if a string starts with a prefix. +/// +/// \param str The string to be tested. +/// \param prefix The prefix to look for. +/// +/// \return True if the string is prefixed as specified. +static bool +starts_with(const std::string& str, const std::string& prefix) +{ + return (str.length() >= prefix.length() && + str.substr(0, prefix.length()) == prefix); +} + + +/// Strips a prefix from a string and converts the rest to an integer. +/// +/// \param str The string to be tested. +/// \param prefix The prefix to strip from the string. +/// +/// \return The part of the string after the prefix converted to an integer. +static int +suffix_to_int(const std::string& str, const std::string& prefix) +{ + PRE(starts_with(str, prefix)); + try { + return text::to_type< int >(str.substr(prefix.length())); + } catch (const text::value_error& error) { + std::cerr << F("Failed: %s\n") % error.what(); + std::abort(); + } +} + + +/// Mock interface definition for testing. +/// +/// This scheduler interface does not execute external binaries. It is designed +/// to simulate the scheduler of various programs with different exit statuses. +class mock_interface : public scheduler::interface { + /// Executes the subprocess simulating an exec. + /// + /// This is just a simple wrapper over _exit(2) because we cannot use + /// std::exit on exit from this mock interface. The reason is that we do + /// not want to invoke any destructors as otherwise we'd clear up the global + /// scheduler 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 Exit code. + void + do_exit(const int exit_code) const UTILS_NORETURN + { + std::cout.flush(); + std::cerr.flush(); + ::_exit(exit_code); + } + + /// Executes a test case that creates various files and then fails. + void + exec_create_files_and_fail(void) const UTILS_NORETURN + { + std::cerr << "This should not be clobbered\n"; + atf::utils::create_file("first file", ""); + atf::utils::create_file("second-file", ""); + fs::mkdir_p(fs::path("dir1/dir2"), 0755); + ::kill(::getpid(), SIGTERM); + std::abort(); + } + + /// Executes a test case 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 scheduler may have created. + void + exec_delete_all(void) const UTILS_NORETURN + { + const int exit_code = ::system("rm *") == -1 + ? EXIT_FAILURE : EXIT_SUCCESS; + + // Recreate our own cookie. + atf::utils::create_file("exec_test_was_called", ""); + + do_exit(exit_code); + } + + /// Executes a test case that returns a specific exit code. + /// + /// \param exit_code Exit status to terminate the program with. + void + exec_exit(const int exit_code) const UTILS_NORETURN + { + do_exit(exit_code); + } + + /// Executes a test case that just fails. + void + exec_fail(void) const UTILS_NORETURN + { + std::cerr << "This should not be clobbered\n"; + ::kill(::getpid(), SIGTERM); + std::abort(); + } + + /// Executes a test case that prints all input parameters to the functor. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke, which must be a + /// number. + /// \param vars User-provided variables to pass to the test program. + void + exec_print_params(const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars) const + UTILS_NORETURN + { + std::cout << F("Test program: %s\n") % test_program.relative_path(); + std::cout << F("Test case: %s\n") % test_case_name; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + std::cout << F("%s=%s\n") % (*iter).first % (*iter).second; + } + + std::cerr << F("stderr: %s\n") % test_case_name; + + do_exit(EXIT_SUCCESS); + } + +public: + /// Executes a test program's list operation. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param vars User-provided variables to pass to the test program. + void + exec_list(const model::test_program& test_program, + const config::properties_map& vars) + const UTILS_NORETURN + { + const std::string name = test_program.absolute_path().leaf_name(); + + std::cerr << name; + std::cerr.flush(); + if (name == "check_i_exist") { + if (fs::exists(test_program.absolute_path())) { + std::cout << "found\n"; + do_exit(EXIT_SUCCESS); + } else { + std::cout << "not_found\n"; + do_exit(EXIT_FAILURE); + } + } else if (name == "empty") { + do_exit(EXIT_SUCCESS); + } else if (name == "misbehave") { + utils::abort_without_coredump(); + } else if (name == "timeout") { + std::cout << "sleeping\n"; + std::cout.flush(); + ::sleep(100); + utils::abort_without_coredump(); + } else if (name == "vars") { + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + std::cout << F("%s_%s\n") % (*iter).first % (*iter).second; + } + do_exit(15); + } else { + std::abort(); + } + } + + /// Computes the test cases list of a test program. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A list of test cases. + model::test_cases_map + parse_list(const optional< process::status >& status, + const fs::path& stdout_path, + const fs::path& stderr_path) const + { + const std::string name = utils::read_file(stderr_path); + if (name == "check_i_exist") { + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.get().exitstatus()); + } else if (name == "empty") { + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.get().exitstatus()); + } else if (name == "misbehave") { + throw std::runtime_error("misbehaved in parse_list"); + } else if (name == "timeout") { + ATF_REQUIRE(!status); + } else if (name == "vars") { + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(15, status.get().exitstatus()); + } else { + ATF_FAIL("Invalid stderr contents; got " + name); + } + + model::test_cases_map_builder test_cases_builder; + + std::ifstream input(stdout_path.c_str()); + ATF_REQUIRE(input); + std::string line; + while (std::getline(input, line).good()) { + test_cases_builder.add(line); + } + + return test_cases_builder.build(); + } + + /// Executes a test case of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke. + /// \param vars User-provided variables to pass to the test program. + /// \param control_directory Directory where the interface may place control + /// files. + void + exec_test(const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& control_directory) const + { + const fs::path cookie = control_directory / "exec_test_was_called"; + std::ofstream control_file(cookie.c_str()); + if (!control_file) { + std::cerr << "Failed to create " << cookie << '\n'; + std::abort(); + } + control_file << test_case_name; + control_file.close(); + + if (test_case_name == "check_i_exist") { + do_exit(fs::exists(test_program.absolute_path()) ? 0 : 1); + } else if (starts_with(test_case_name, "cleanup_timeout")) { + exec_exit(EXIT_SUCCESS); + } else if (starts_with(test_case_name, "create_files_and_fail")) { + exec_create_files_and_fail(); + } else if (test_case_name == "delete_all") { + exec_delete_all(); + } else if (starts_with(test_case_name, "exit ")) { + exec_exit(suffix_to_int(test_case_name, "exit ")); + } else if (starts_with(test_case_name, "fail")) { + exec_fail(); + } else if (starts_with(test_case_name, "fail_body_fail_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "fail_body_pass_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "pass_body_fail_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else if (starts_with(test_case_name, "print_params")) { + exec_print_params(test_program, test_case_name, vars); + } else if (starts_with(test_case_name, "skip_body_pass_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else { + std::cerr << "Unknown test case " << test_case_name << '\n'; + std::abort(); + } + } + + /// Executes a test cleanup routine of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_case_name Name of the test case to invoke. + void + exec_cleanup(const model::test_program& /* test_program */, + const std::string& test_case_name, + const config::properties_map& /* vars */, + const fs::path& /* control_directory */) const + { + std::cout << "exec_cleanup was called\n"; + std::cout.flush(); + + if (starts_with(test_case_name, "cleanup_timeout")) { + ::sleep(100); + std::abort(); + } else if (starts_with(test_case_name, "fail_body_fail_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "fail_body_pass_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else if (starts_with(test_case_name, "pass_body_fail_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "skip_body_pass_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else { + std::cerr << "Should not have been called for a test without " + "a cleanup routine" << '\n'; + std::abort(); + } + } + + /// Computes the result of a test case based on its termination status. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param control_directory Path to the directory where the interface may + /// have placed control files. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A test result. + model::test_result + compute_result(const optional< process::status >& status, + const fs::path& control_directory, + const fs::path& stdout_path, + const fs::path& stderr_path) const + { + // Do not use any ATF_* macros here. Some of the tests below invoke + // this code in a subprocess, and terminating such subprocess due to a + // failed ATF_* macro yields mysterious failures that are incredibly + // hard to debug. (Case in point: the signal_handling test is racy by + // nature, and the test run by exec_test() above may not have created + // the cookie we expect below. We don't want to "silently" exit if the + // file is not there.) + + if (!status) { + return model::test_result(model::test_result_broken, + "Timed out"); + } + + if (status.get().exited()) { + // Only sanity-check the work directory-related parameters in case + // of a clean exit. In all other cases, there is no guarantee that + // these were ever created. + const fs::path cookie = control_directory / "exec_test_was_called"; + if (!atf::utils::file_exists(cookie.str())) { + return model::test_result( + model::test_result_broken, + "compute_result's control_directory does not seem to point " + "to the right location"); + } + const std::string test_case_name = utils::read_file(cookie); + + if (!atf::utils::file_exists(stdout_path.str())) { + return model::test_result( + model::test_result_broken, + "compute_result's stdout_path does not exist"); + } + if (!atf::utils::file_exists(stderr_path.str())) { + return model::test_result( + model::test_result_broken, + "compute_result's stderr_path does not exist"); + } + + if (test_case_name == "skip_body_pass_cleanup") { + return model::test_result( + model::test_result_skipped, + F("Exit %s") % status.get().exitstatus()); + } else { + return model::test_result( + model::test_result_passed, + F("Exit %s") % status.get().exitstatus()); + } + } else { + return model::test_result( + model::test_result_failed, + F("Signal %s") % status.get().termsig()); + } + } +}; + + +} // anonymous namespace + + +/// Runs list_tests on the scheduler and returns the results. +/// +/// \param test_name The name of the test supported by our exec_list function. +/// \param user_config Optional user settings for the test. +/// +/// \return The loaded list of test cases. +static model::test_cases_map +check_integration_list(const char* test_name, const fs::path root, + const config::tree& user_config = engine::empty_config()) +{ + const model::test_program program = model::test_program_builder( + "mock", fs::path(test_name), root, "the-suite") + .build(); + + scheduler::scheduler_handle handle = scheduler::setup(); + const model::test_cases_map test_cases = handle.list_tests(&program, + user_config); + handle.cleanup(); + + return test_cases; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_some); +ATF_TEST_CASE_BODY(integration__list_some) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.first", "test"); + user_config.set_string("test_suites.the-suite.second", "TEST"); + user_config.set_string("test_suites.abc.unused", "unused"); + + const model::test_cases_map test_cases = check_integration_list( + "vars", fs::path("."), user_config); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("first_test").add("second_TEST").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_check_paths); +ATF_TEST_CASE_BODY(integration__list_check_paths) +{ + fs::mkdir_p(fs::path("dir1/dir2/dir3"), 0755); + atf::utils::create_file("dir1/dir2/dir3/check_i_exist", ""); + + const model::test_cases_map test_cases = check_integration_list( + "dir2/dir3/check_i_exist", fs::path("dir1")); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("found").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_timeout); +ATF_TEST_CASE_BODY(integration__list_timeout) +{ + scheduler::list_timeout = datetime::delta(1, 0); + const model::test_cases_map test_cases = check_integration_list( + "timeout", fs::path(".")); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("sleeping").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_fail); +ATF_TEST_CASE_BODY(integration__list_fail) +{ + const model::test_cases_map test_cases = check_integration_list( + "misbehave", fs::path(".")); + + ATF_REQUIRE_EQ(1, test_cases.size()); + const model::test_case& test_case = test_cases.begin()->second; + ATF_REQUIRE_EQ("__test_cases_list__", test_case.name()); + ATF_REQUIRE(test_case.fake_result()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_broken, + "misbehaved in parse_list"), + test_case.fake_result().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_empty); +ATF_TEST_CASE_BODY(integration__list_empty) +{ + const model::test_cases_map test_cases = check_integration_list( + "empty", fs::path(".")); + + ATF_REQUIRE_EQ(1, test_cases.size()); + const model::test_case& test_case = test_cases.begin()->second; + ATF_REQUIRE_EQ("__test_cases_list__", test_case.name()); + ATF_REQUIRE(test_case.fake_result()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_broken, + "Empty test cases list"), + test_case.fake_result().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_one); +ATF_TEST_CASE_BODY(integration__run_one) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("exit 41").build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + const scheduler::exec_handle exec_handle = handle.spawn_test( + program, "exit 41", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(exec_handle, result_handle->original_pid()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 41"), + test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_many); +ATF_TEST_CASE_BODY(integration__run_many) +{ + static const std::size_t num_test_programs = 30; + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + // We mess around with the "current time" below, so make sure the tests do + // not spuriously exceed their deadline by bumping it to a large number. + const model::metadata infinite_timeout = model::metadata_builder() + .set_timeout(datetime::delta(1000000L, 0)).build(); + + std::size_t total_tests = 0; + std::map< scheduler::exec_handle, model::test_program_ptr > + exp_test_programs; + std::map< scheduler::exec_handle, std::string > exp_test_case_names; + std::map< scheduler::exec_handle, datetime::timestamp > exp_start_times; + std::map< scheduler::exec_handle, int > exp_exit_statuses; + for (std::size_t i = 0; i < num_test_programs; ++i) { + const std::string test_case_0 = F("exit %s") % (i * 3 + 0); + const std::string test_case_1 = F("exit %s") % (i * 3 + 1); + const std::string test_case_2 = F("exit %s") % (i * 3 + 2); + + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path(F("program-%s") % i), + fs::current_path(), "the-suite") + .set_metadata(infinite_timeout) + .add_test_case(test_case_0) + .add_test_case(test_case_1) + .add_test_case(test_case_2) + .build_ptr(); + + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 40, 0, i); + + scheduler::exec_handle exec_handle; + + datetime::set_mock_now(start_time); + exec_handle = handle.spawn_test(program, test_case_0, user_config); + exp_test_programs.insert(std::make_pair(exec_handle, program)); + exp_test_case_names.insert(std::make_pair(exec_handle, test_case_0)); + exp_start_times.insert(std::make_pair(exec_handle, start_time)); + exp_exit_statuses.insert(std::make_pair(exec_handle, i * 3)); + ++total_tests; + + datetime::set_mock_now(start_time); + exec_handle = handle.spawn_test(program, test_case_1, user_config); + exp_test_programs.insert(std::make_pair(exec_handle, program)); + exp_test_case_names.insert(std::make_pair(exec_handle, test_case_1)); + exp_start_times.insert(std::make_pair(exec_handle, start_time)); + exp_exit_statuses.insert(std::make_pair(exec_handle, i * 3 + 1)); + ++total_tests; + + datetime::set_mock_now(start_time); + exec_handle = handle.spawn_test(program, test_case_2, user_config); + exp_test_programs.insert(std::make_pair(exec_handle, program)); + exp_test_case_names.insert(std::make_pair(exec_handle, test_case_2)); + exp_start_times.insert(std::make_pair(exec_handle, start_time)); + exp_exit_statuses.insert(std::make_pair(exec_handle, i * 3 + 2)); + ++total_tests; + } + + for (std::size_t i = 0; i < total_tests; ++i) { + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 50, 10, i); + datetime::set_mock_now(end_time); + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + const scheduler::exec_handle exec_handle = + result_handle->original_pid(); + + const model::test_program_ptr test_program = exp_test_programs.find( + exec_handle)->second; + const std::string& test_case_name = exp_test_case_names.find( + exec_handle)->second; + const datetime::timestamp& start_time = exp_start_times.find( + exec_handle)->second; + const int exit_status = exp_exit_statuses.find(exec_handle)->second; + + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, + F("Exit %s") % exit_status), + test_result_handle->test_result()); + + ATF_REQUIRE_EQ(test_program, test_result_handle->test_program()); + ATF_REQUIRE_EQ(test_case_name, test_result_handle->test_case_name()); + + ATF_REQUIRE_EQ(start_time, result_handle->start_time()); + ATF_REQUIRE_EQ(end_time, result_handle->end_time()); + + result_handle->cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists( + result_handle->stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + result_handle->stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + result_handle->work_directory().str())); + + result_handle.reset(); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_check_paths); +ATF_TEST_CASE_BODY(integration__run_check_paths) +{ + fs::mkdir_p(fs::path("dir1/dir2/dir3"), 0755); + atf::utils::create_file("dir1/dir2/dir3/program", ""); + + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("dir2/dir3/program"), fs::path("dir1"), "the-suite") + .add_test_case("check_i_exist").build_ptr(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "check_i_exist", engine::default_config()); + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__parameters_and_output); +ATF_TEST_CASE_BODY(integration__parameters_and_output) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("print_params").build_ptr(); + + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.one", "first variable"); + user_config.set_string("test_suites.the-suite.two", "second variable"); + + scheduler::scheduler_handle handle = scheduler::setup(); + + const scheduler::exec_handle exec_handle = handle.spawn_test( + program, "print_params", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + ATF_REQUIRE_EQ(exec_handle, result_handle->original_pid()); + ATF_REQUIRE_EQ(program, test_result_handle->test_program()); + ATF_REQUIRE_EQ("print_params", test_result_handle->test_case_name()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + + const fs::path stdout_file = result_handle->stdout_file(); + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), + "Test program: the-program\n" + "Test case: print_params\n" + "one=first variable\n" + "two=second variable\n")); + const fs::path stderr_file = result_handle->stderr_file(); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: print_params\n")); + + result_handle->cleanup(); + ATF_REQUIRE(!fs::exists(stdout_file)); + ATF_REQUIRE(!fs::exists(stderr_file)); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__fake_result); +ATF_TEST_CASE_BODY(integration__fake_result) +{ + const model::test_result fake_result(model::test_result_skipped, + "Some fake details"); + + model::test_cases_map test_cases; + test_cases.insert(model::test_cases_map::value_type( + "__fake__", model::test_case("__fake__", "ABC", fake_result))); + + const model::test_program_ptr program(new model::test_program( + "mock", fs::path("the-program"), fs::current_path(), "the-suite", + model::metadata_builder().build(), test_cases)); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "__fake__", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(fake_result, test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__head_skips); +ATF_TEST_CASE_BODY(integration__cleanup__head_skips) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("skip_me", + model::metadata_builder() + .add_required_config("variable-that-does-not-exist") + .set_has_cleanup(true) + .build()) + .build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "skip_me", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result( + model::test_result_skipped, + "Required configuration property " + "'variable-that-does-not-exist' not defined"), + test_result_handle->test_result()); + ATF_REQUIRE(!atf::utils::grep_file("exec_cleanup was called", + result_handle->stdout_file().str())); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +/// Runs a test to verify the behavior of cleanup routines. +/// +/// \param test_case The name of the test case to invoke. +/// \param exp_result The expected test result of the execution. +static void +do_cleanup_test(const char* test_case, + const model::test_result& exp_result) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case(test_case) + .set_metadata(model::metadata_builder().set_has_cleanup(true).build()) + .build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, test_case, user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + ATF_REQUIRE(atf::utils::compare_file( + result_handle->stdout_file().str(), + "exec_cleanup was called\n")); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_skips); +ATF_TEST_CASE_BODY(integration__cleanup__body_skips) +{ + do_cleanup_test( + "skip_body_pass_cleanup", + model::test_result(model::test_result_skipped, "Exit 0")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_bad__cleanup_ok); +ATF_TEST_CASE_BODY(integration__cleanup__body_bad__cleanup_ok) +{ + do_cleanup_test( + "fail_body_pass_cleanup", + model::test_result(model::test_result_failed, "Signal 15")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_ok__cleanup_bad); +ATF_TEST_CASE_BODY(integration__cleanup__body_ok__cleanup_bad) +{ + do_cleanup_test( + "pass_body_fail_cleanup", + model::test_result(model::test_result_broken, "Test case cleanup " + "did not terminate successfully")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_bad__cleanup_bad); +ATF_TEST_CASE_BODY(integration__cleanup__body_bad__cleanup_bad) +{ + do_cleanup_test( + "fail_body_fail_cleanup", + model::test_result(model::test_result_failed, "Signal 15")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__timeout); +ATF_TEST_CASE_BODY(integration__cleanup__timeout) +{ + scheduler::cleanup_timeout = datetime::delta(1, 0); + do_cleanup_test( + "cleanup_timeout", + model::test_result(model::test_result_broken, "Test case cleanup " + "timed out")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__check_requirements); +ATF_TEST_CASE_BODY(integration__check_requirements) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("exit 12") + .set_metadata(model::metadata_builder() + .add_required_config("abcde").build()) + .build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "exit 12", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result( + model::test_result_skipped, + "Required configuration property 'abcde' not defined"), + test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__stacktrace); +ATF_TEST_CASE_BODY(integration__stacktrace) +{ + utils::prepare_coredump_test(this); + + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("unknown-dumps-core").build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "unknown-dumps-core", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_failed, + F("Signal %s") % SIGABRT), + test_result_handle->test_result()); + ATF_REQUIRE(!atf::utils::grep_file("attempting to gather stack trace", + result_handle->stdout_file().str())); + ATF_REQUIRE( atf::utils::grep_file("attempting to gather stack trace", + result_handle->stderr_file().str())); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +/// Runs a test to verify the dumping of the list of existing files on failure. +/// +/// \param test_case The name of the test case to invoke. +/// \param exp_stderr Expected contents of stderr. +static void +do_check_list_files_on_failure(const char* test_case, const char* exp_stderr) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case(test_case).build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, test_case, user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + atf::utils::cat_file(result_handle->stdout_file().str(), "child stdout: "); + ATF_REQUIRE(atf::utils::compare_file(result_handle->stdout_file().str(), + "")); + atf::utils::cat_file(result_handle->stderr_file().str(), "child stderr: "); + ATF_REQUIRE(atf::utils::compare_file(result_handle->stderr_file().str(), + exp_stderr)); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_files_on_failure__none); +ATF_TEST_CASE_BODY(integration__list_files_on_failure__none) +{ + do_check_list_files_on_failure("fail", "This should not be clobbered\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_files_on_failure__some); +ATF_TEST_CASE_BODY(integration__list_files_on_failure__some) +{ + do_check_list_files_on_failure( + "create_files_and_fail", + "This should not be clobbered\n" + "Files left in work directory after failure: " + "dir1, first file, second-file\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__prevent_clobbering_control_files); +ATF_TEST_CASE_BODY(integration__prevent_clobbering_control_files) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("delete_all").build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "delete_all", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(debug_test); +ATF_TEST_CASE_BODY(debug_test) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("print_params").build_ptr(); + + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.one", "first variable"); + user_config.set_string("test_suites.the-suite.two", "second variable"); + + scheduler::scheduler_handle handle = scheduler::setup(); + + const fs::path stdout_file("custom-stdout.txt"); + const fs::path stderr_file("custom-stderr.txt"); + + scheduler::result_handle_ptr result_handle = handle.debug_test( + program, "print_params", user_config, stdout_file, stderr_file); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + ATF_REQUIRE_EQ(program, test_result_handle->test_program()); + ATF_REQUIRE_EQ("print_params", test_result_handle->test_case_name()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + + // The original output went to a file. It's only an artifact of + // debug_test() that we later get a copy in our own files. + ATF_REQUIRE(stdout_file != result_handle->stdout_file()); + ATF_REQUIRE(stderr_file != result_handle->stderr_file()); + + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); + + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), + "Test program: the-program\n" + "Test case: print_params\n" + "one=first variable\n" + "two=second variable\n")); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: print_params\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ensure_valid_interface); +ATF_TEST_CASE_BODY(ensure_valid_interface) +{ + scheduler::ensure_valid_interface("mock"); + + ATF_REQUIRE_THROW_RE(engine::error, "Unsupported test interface 'mock2'", + scheduler::ensure_valid_interface("mock2")); + scheduler::register_interface( + "mock2", std::shared_ptr< scheduler::interface >(new mock_interface())); + scheduler::ensure_valid_interface("mock2"); + + // Standard interfaces should not be present unless registered. + ATF_REQUIRE_THROW_RE(engine::error, "Unsupported test interface 'plain'", + scheduler::ensure_valid_interface("plain")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(registered_interface_names); +ATF_TEST_CASE_BODY(registered_interface_names) +{ + std::set< std::string > exp_names; + + exp_names.insert("mock"); + ATF_REQUIRE_EQ(exp_names, scheduler::registered_interface_names()); + + scheduler::register_interface( + "mock2", std::shared_ptr< scheduler::interface >(new mock_interface())); + exp_names.insert("mock2"); + ATF_REQUIRE_EQ(exp_names, scheduler::registered_interface_names()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_context); +ATF_TEST_CASE_BODY(current_context) +{ + const model::context context = scheduler::current_context(); + ATF_REQUIRE_EQ(fs::current_path(), context.cwd()); + ATF_REQUIRE(utils::getallenv() == context.env()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_config__empty); +ATF_TEST_CASE_BODY(generate_config__empty) +{ + const config::tree user_config = engine::empty_config(); + + const config::properties_map exp_props; + + ATF_REQUIRE_EQ(exp_props, + scheduler::generate_config(user_config, "missing")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_config__no_matches); +ATF_TEST_CASE_BODY(generate_config__no_matches) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("architecture", "foo"); + user_config.set_string("test_suites.one.var1", "value 1"); + + const config::properties_map exp_props; + + ATF_REQUIRE_EQ(exp_props, + scheduler::generate_config(user_config, "two")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_config__some_matches); +ATF_TEST_CASE_BODY(generate_config__some_matches) +{ + std::vector< passwd::user > mock_users; + mock_users.push_back(passwd::user("nobody", 1234, 5678)); + passwd::set_mock_users_for_testing(mock_users); + + config::tree user_config = engine::empty_config(); + user_config.set_string("architecture", "foo"); + user_config.set_string("unprivileged_user", "nobody"); + user_config.set_string("test_suites.one.var1", "value 1"); + user_config.set_string("test_suites.two.var2", "value 2"); + + config::properties_map exp_props; + exp_props["unprivileged-user"] = "nobody"; + exp_props["var1"] = "value 1"; + + ATF_REQUIRE_EQ(exp_props, + scheduler::generate_config(user_config, "one")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "mock", std::shared_ptr< scheduler::interface >(new mock_interface())); + + ATF_ADD_TEST_CASE(tcs, integration__list_some); + ATF_ADD_TEST_CASE(tcs, integration__list_check_paths); + ATF_ADD_TEST_CASE(tcs, integration__list_timeout); + ATF_ADD_TEST_CASE(tcs, integration__list_fail); + ATF_ADD_TEST_CASE(tcs, integration__list_empty); + + ATF_ADD_TEST_CASE(tcs, integration__run_one); + ATF_ADD_TEST_CASE(tcs, integration__run_many); + + ATF_ADD_TEST_CASE(tcs, integration__run_check_paths); + ATF_ADD_TEST_CASE(tcs, integration__parameters_and_output); + + ATF_ADD_TEST_CASE(tcs, integration__fake_result); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__head_skips); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_skips); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_ok__cleanup_bad); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_bad__cleanup_ok); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_bad__cleanup_bad); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__timeout); + ATF_ADD_TEST_CASE(tcs, integration__check_requirements); + ATF_ADD_TEST_CASE(tcs, integration__stacktrace); + ATF_ADD_TEST_CASE(tcs, integration__list_files_on_failure__none); + ATF_ADD_TEST_CASE(tcs, integration__list_files_on_failure__some); + ATF_ADD_TEST_CASE(tcs, integration__prevent_clobbering_control_files); + + ATF_ADD_TEST_CASE(tcs, debug_test); + + ATF_ADD_TEST_CASE(tcs, ensure_valid_interface); + ATF_ADD_TEST_CASE(tcs, registered_interface_names); + + ATF_ADD_TEST_CASE(tcs, current_context); + + ATF_ADD_TEST_CASE(tcs, generate_config__empty); + ATF_ADD_TEST_CASE(tcs, generate_config__no_matches); + ATF_ADD_TEST_CASE(tcs, generate_config__some_matches); +} diff --git a/contrib/kyua/engine/tap.cpp b/contrib/kyua/engine/tap.cpp new file mode 100644 index 000000000000..85e23857f5b7 --- /dev/null +++ b/contrib/kyua/engine/tap.cpp @@ -0,0 +1,191 @@ +// 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 "engine/tap.hpp" + +extern "C" { +#include +} + +#include + +#include "engine/exceptions.hpp" +#include "engine/tap_parser.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::optional; + + +namespace { + + +/// Computes the result of a TAP test program termination. +/// +/// Timeouts and bad TAP data must be handled by the caller. Here we assume +/// that we have been able to successfully parse the TAP output. +/// +/// \param summary Parsed TAP data for the test program. +/// \param status Exit status of the test program. +/// +/// \return A test result. +static model::test_result +tap_to_result(const engine::tap_summary& summary, + const process::status& status) +{ + if (summary.bailed_out()) { + return model::test_result(model::test_result_failed, "Bailed out"); + } + + if (summary.plan() == engine::all_skipped_plan) { + return model::test_result(model::test_result_skipped, + summary.all_skipped_reason()); + } + + if (summary.not_ok_count() == 0) { + if (status.exitstatus() == EXIT_SUCCESS) { + return model::test_result(model::test_result_passed); + } else { + return model::test_result( + model::test_result_broken, + F("Dubious test program: reported all tests as passed " + "but returned exit code %s") % status.exitstatus()); + } + } else { + const std::size_t total = summary.ok_count() + summary.not_ok_count(); + return model::test_result(model::test_result_failed, + F("%s of %s tests failed") % + summary.not_ok_count() % total); + } +} + + +} // anonymous namespace + + +/// Executes a test program's list operation. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +void +engine::tap_interface::exec_list( + const model::test_program& /* test_program */, + const config::properties_map& /* vars */) const +{ + ::_exit(EXIT_SUCCESS); +} + + +/// Computes the test cases list of a test program. +/// +/// \return A list of test cases. +model::test_cases_map +engine::tap_interface::parse_list( + const optional< process::status >& /* status */, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + return model::test_cases_map_builder().add("main").build(); +} + + +/// Executes a test case of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +void +engine::tap_interface::exec_test( + const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& /* control_directory */) const +{ + PRE(test_case_name == "main"); + + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + utils::setenv(F("TEST_ENV_%s") % (*iter).first, (*iter).second); + } + + process::args_vector args; + process::exec(test_program.absolute_path(), args); +} + + +/// Computes the result of a test case based on its termination status. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// \param stdout_path Path to the file containing the stdout of the test. +/// +/// \return A test result. +model::test_result +engine::tap_interface::compute_result( + const optional< process::status >& status, + const fs::path& /* control_directory */, + const fs::path& stdout_path, + const fs::path& /* stderr_path */) const +{ + if (!status) { + return model::test_result(model::test_result_broken, + "Test case timed out"); + } else { + if (status.get().signaled()) { + return model::test_result( + model::test_result_broken, + F("Received signal %s") % status.get().termsig()); + } else { + try { + const tap_summary summary = parse_tap_output(stdout_path); + return tap_to_result(summary, status.get()); + } catch (const load_error& e) { + return model::test_result( + model::test_result_broken, + F("TAP test program yielded invalid data: %s") % e.what()); + } + } + } +} diff --git a/contrib/kyua/engine/tap.hpp b/contrib/kyua/engine/tap.hpp new file mode 100644 index 000000000000..b46bf28f0240 --- /dev/null +++ b/contrib/kyua/engine/tap.hpp @@ -0,0 +1,67 @@ +// 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 engine/tap.hpp +/// Execution engine for test programs that output the TAP protocol. + +#if !defined(ENGINE_TAP_HPP) +#define ENGINE_TAP_HPP + +#include "engine/scheduler.hpp" + +namespace engine { + + +/// Implementation of the scheduler interface for tap test programs. +class tap_interface : public engine::scheduler::interface { +public: + void exec_list(const model::test_program&, + const utils::config::properties_map&) const UTILS_NORETURN; + + model::test_cases_map parse_list( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&) const; + + void exec_test(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + model::test_result compute_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_TAP_HPP) diff --git a/contrib/kyua/engine/tap_helpers.cpp b/contrib/kyua/engine/tap_helpers.cpp new file mode 100644 index 000000000000..4f9505c78dec --- /dev/null +++ b/contrib/kyua/engine/tap_helpers.cpp @@ -0,0 +1,202 @@ +// 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. + +extern "C" { +#include + +#include + +extern char** environ; +} + +#include +#include +#include +#include + +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; + + +namespace { + + +/// Logs an error message and exits the test with an error code. +/// +/// \param str The error message to log. +static void +fail(const std::string& str) +{ + std::cerr << str << '\n'; + std::exit(EXIT_FAILURE); +} + + +/// A test scenario that validates the TEST_ENV_* variables. +static void +test_check_configuration_variables(void) +{ + std::set< std::string > vars; + char** iter; + for (iter = environ; *iter != NULL; ++iter) { + if (std::strstr(*iter, "TEST_ENV_") == *iter) { + vars.insert(*iter); + } + } + + std::set< std::string > exp_vars; + exp_vars.insert("TEST_ENV_first=some value"); + exp_vars.insert("TEST_ENV_second=some other value"); + if (vars == exp_vars) { + std::cout << "1..1\n" + << "ok 1\n"; + } else { + std::cout << "1..1\n" + << "not ok 1\n" + << F(" Expected: %s\nFound: %s\n") % exp_vars % vars; + } +} + + +/// A test scenario that crashes. +static void +test_crash(void) +{ + utils::abort_without_coredump(); +} + + +/// A test scenario that reports some tests as failed. +static void +test_fail(void) +{ + std::cout << "1..5\n" + << "ok 1 - This is good!\n" + << "not ok 2\n" + << "ok 3 - TODO Consider this as passed\n" + << "ok 4\n" + << "not ok 5\n"; +} + + +/// A test scenario that passes. +static void +test_pass(void) +{ + std::cout << "1..4\n" + << "ok 1 - This is good!\n" + << "non-result data\n" + << "ok 2 - SKIP Consider this as passed\n" + << "ok 3 - TODO Consider this as passed\n" + << "ok 4\n"; +} + + +/// A test scenario that passes but then exits with non-zero. +static void +test_pass_but_exit_failure(void) +{ + std::cout << "1..2\n" + << "ok 1\n" + << "ok 2\n"; + std::exit(70); +} + + +/// A test scenario that times out. +/// +/// Note that the timeout is defined in the Kyuafile, as the TAP interface has +/// no means for test programs to specify this by themselves. +static void +test_timeout(void) +{ + std::cout << "1..2\n" + << "ok 1\n"; + + ::sleep(10); + const fs::path control_dir = fs::path(utils::getenv("CONTROL_DIR").get()); + std::ofstream file((control_dir / "cookie").c_str()); + if (!file) + fail("Failed to create the control cookie"); + file.close(); +} + + +} // anonymous namespace + + +/// Entry point to the test program. +/// +/// The caller can select which test scenario to run by modifying the program's +/// basename on disk (either by a copy or by a hard link). +/// +/// \todo It may be worth to split this binary into separate, smaller binaries, +/// one for every "test scenario". We use this program as a dispatcher for +/// different "main"s, the only reason being to keep the amount of helper test +/// programs to a minimum. However, putting this each function in its own +/// binary could simplify many other things. +/// +/// \param argc The number of CLI arguments. +/// \param argv The CLI arguments themselves. These are not used because +/// Kyua will not pass any arguments to the plain test program. +int +main(int argc, char** argv) +{ + if (argc != 1) { + std::cerr << "No arguments allowed; select the test scenario with the " + "program's basename\n"; + return EXIT_FAILURE; + } + + const std::string& test_scenario = fs::path(argv[0]).leaf_name(); + + if (test_scenario == "check_configuration_variables") + test_check_configuration_variables(); + else if (test_scenario == "crash") + test_crash(); + else if (test_scenario == "fail") + test_fail(); + else if (test_scenario == "pass") + test_pass(); + else if (test_scenario == "pass_but_exit_failure") + test_pass_but_exit_failure(); + else if (test_scenario == "timeout") + test_timeout(); + else { + std::cerr << "Unknown test scenario\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/contrib/kyua/engine/tap_parser.cpp b/contrib/kyua/engine/tap_parser.cpp new file mode 100644 index 000000000000..d41328534fad --- /dev/null +++ b/contrib/kyua/engine/tap_parser.cpp @@ -0,0 +1,438 @@ +// 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 "engine/tap_parser.hpp" + +#include + +#include "engine/exceptions.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" +#include "utils/text/regex.hpp" + +namespace fs = utils::fs; +namespace text = utils::text; + +using utils::optional; + + +/// TAP plan representing all tests being skipped. +const engine::tap_plan engine::all_skipped_plan(1, 0); + + +namespace { + + +/// Implementation of the TAP parser. +/// +/// This is a class only to simplify keeping global constant values around (like +/// prebuilt regular expressions). +class tap_parser : utils::noncopyable { + /// Regular expression to match plan lines. + text::regex _plan_regex; + + /// Regular expression to match a TODO and extract the reason. + text::regex _todo_regex; + + /// Regular expression to match a SKIP and extract the reason. + text::regex _skip_regex; + + /// Regular expression to match a single test result. + text::regex _result_regex; + + /// Checks if a line contains a TAP plan and extracts its data. + /// + /// \param line The line to try to parse. + /// \param [in,out] out_plan Used to store the found plan, if any. The same + /// output variable should be given to all calls to this function so + /// that duplicate plan entries can be discovered. + /// \param [out] out_all_skipped_reason Used to store the reason for all + /// tests being skipped, if any. If this is set to a non-empty value, + /// then the out_plan is set to 1..0. + /// + /// \return True if the line matched a plan; false otherwise. + /// + /// \throw engine::format_error If the input is invalid. + /// \throw text::error If the input is invalid. + bool + try_parse_plan(const std::string& line, + optional< engine::tap_plan >& out_plan, + std::string& out_all_skipped_reason) + { + const text::regex_matches plan_matches = _plan_regex.match(line); + if (!plan_matches) + return false; + const engine::tap_plan plan( + text::to_type< std::size_t >(plan_matches.get(1)), + text::to_type< std::size_t >(plan_matches.get(2))); + + if (out_plan) + throw engine::format_error( + F("Found duplicate plan %s..%s (saw %s..%s earlier)") % + plan.first % plan.second % + out_plan.get().first % out_plan.get().second); + + std::string all_skipped_reason; + const text::regex_matches skip_matches = _skip_regex.match(line); + if (skip_matches) { + if (plan != engine::all_skipped_plan) { + throw engine::format_error(F("Skipped plan must be %s..%s") % + engine::all_skipped_plan.first % + engine::all_skipped_plan.second); + } + all_skipped_reason = skip_matches.get(2); + if (all_skipped_reason.empty()) + all_skipped_reason = "No reason specified"; + } else { + if (plan.first > plan.second) + throw engine::format_error(F("Found reversed plan %s..%s") % + plan.first % plan.second); + } + + INV(!out_plan); + out_plan = plan; + out_all_skipped_reason = all_skipped_reason; + + POST(out_plan); + POST(out_all_skipped_reason.empty() || + out_plan.get() == engine::all_skipped_plan); + + return true; + } + + /// Checks if a line contains a TAP test result and extracts its data. + /// + /// \param line The line to try to parse. + /// \param [in,out] out_ok_count Accumulator for 'ok' results. + /// \param [in,out] out_not_ok_count Accumulator for 'not ok' results. + /// \param [out] out_bailed_out Set to true if the test bailed out. + /// + /// \return True if the line matched a result; false otherwise. + /// + /// \throw engine::format_error If the input is invalid. + /// \throw text::error If the input is invalid. + bool + try_parse_result(const std::string& line, std::size_t& out_ok_count, + std::size_t& out_not_ok_count, bool& out_bailed_out) + { + PRE(!out_bailed_out); + + const text::regex_matches result_matches = _result_regex.match(line); + if (result_matches) { + if (result_matches.get(1) == "ok") { + ++out_ok_count; + } else { + INV(result_matches.get(1) == "not ok"); + if (_todo_regex.match(line) || _skip_regex.match(line)) { + ++out_ok_count; + } else { + ++out_not_ok_count; + } + } + return true; + } else { + if (line.find("Bail out!") == 0) { + out_bailed_out = true; + return true; + } else { + return false; + } + } + } + +public: + /// Sets up the TAP parser state. + tap_parser(void) : + _plan_regex(text::regex::compile("^([0-9]+)\\.\\.([0-9]+)", 2)), + _todo_regex(text::regex::compile("TODO[ \t]*(.*)$", 2, true)), + _skip_regex(text::regex::compile("(SKIP|Skipped:?)[ \t]*(.*)$", 2, + true)), + _result_regex(text::regex::compile("^(not ok|ok)[ \t-]+[0-9]*", 1)) + { + } + + /// Parses an input file containing TAP output. + /// + /// \param input The stream to read from. + /// + /// \return The results of the parsing in the form of a tap_summary object. + /// + /// \throw engine::format_error If there are any syntax errors in the input. + /// \throw text::error If there are any syntax errors in the input. + engine::tap_summary + parse(std::ifstream& input) + { + optional< engine::tap_plan > plan; + std::string all_skipped_reason; + bool bailed_out = false; + std::size_t ok_count = 0, not_ok_count = 0; + + std::string line; + while (!bailed_out && std::getline(input, line)) { + if (try_parse_result(line, ok_count, not_ok_count, bailed_out)) + continue; + (void)try_parse_plan(line, plan, all_skipped_reason); + } + + if (bailed_out) { + return engine::tap_summary::new_bailed_out(); + } else { + if (!plan) + throw engine::format_error( + "Output did not contain any TAP plan and the program did " + "not bail out"); + + if (plan.get() == engine::all_skipped_plan) { + return engine::tap_summary::new_all_skipped(all_skipped_reason); + } else { + const std::size_t exp_count = plan.get().second - + plan.get().first + 1; + const std::size_t actual_count = ok_count + not_ok_count; + if (exp_count != actual_count) { + throw engine::format_error( + "Reported plan differs from actual executed tests"); + } + return engine::tap_summary::new_results(plan.get(), ok_count, + not_ok_count); + } + } + } +}; + + +} // anonymous namespace + + +/// Constructs a TAP summary with the results of parsing a TAP output. +/// +/// \param bailed_out_ Whether the test program bailed out early or not. +/// \param plan_ The TAP plan. +/// \param all_skipped_reason_ The reason for skipping all tests, if any. +/// \param ok_count_ Number of 'ok' test results. +/// \param not_ok_count_ Number of 'not ok' test results. +engine::tap_summary::tap_summary(const bool bailed_out_, + const tap_plan& plan_, + const std::string& all_skipped_reason_, + const std::size_t ok_count_, + const std::size_t not_ok_count_) : + _bailed_out(bailed_out_), _plan(plan_), + _all_skipped_reason(all_skipped_reason_), + _ok_count(ok_count_), _not_ok_count(not_ok_count_) +{ +} + + +/// Constructs a TAP summary for a bailed out test program. +/// +/// \return The new tap_summary object. +engine::tap_summary +engine::tap_summary::new_bailed_out(void) +{ + return tap_summary(true, tap_plan(0, 0), "", 0, 0); +} + + +/// Constructs a TAP summary for a test program that skipped all tests. +/// +/// \param reason Textual reason describing why the tests were skipped. +/// +/// \return The new tap_summary object. +engine::tap_summary +engine::tap_summary::new_all_skipped(const std::string& reason) +{ + return tap_summary(false, all_skipped_plan, reason, 0, 0); +} + + +/// Constructs a TAP summary for a test program that reported results. +/// +/// \param plan_ The TAP plan. +/// \param ok_count_ Total number of 'ok' results. +/// \param not_ok_count_ Total number of 'not ok' results. +/// +/// \return The new tap_summary object. +engine::tap_summary +engine::tap_summary::new_results(const tap_plan& plan_, + const std::size_t ok_count_, + const std::size_t not_ok_count_) +{ + PRE((plan_.second - plan_.first + 1) == (ok_count_ + not_ok_count_)); + return tap_summary(false, plan_, "", ok_count_, not_ok_count_); +} + + +/// Checks whether the test program bailed out early or not. +/// +/// \return True if the test program aborted execution before completing. +bool +engine::tap_summary::bailed_out(void) const +{ + return _bailed_out; +} + + +/// Gets the TAP plan of the test program. +/// +/// \pre bailed_out() must be false. +/// +/// \return The TAP plan. If 1..0, then all_skipped_reason() will have some +/// contents. +const engine::tap_plan& +engine::tap_summary::plan(void) const +{ + PRE(!_bailed_out); + return _plan; +} + + +/// Gets the reason for skipping all the tests, if any. +/// +/// \pre bailed_out() must be false. +/// \pre plan() returns 1..0. +/// +/// \return The reason for skipping all the tests. +const std::string& +engine::tap_summary::all_skipped_reason(void) const +{ + PRE(!_bailed_out); + PRE(_plan == all_skipped_plan); + return _all_skipped_reason; +} + + +/// Gets the number of 'ok' test results. +/// +/// \pre bailed_out() must be false. +/// +/// \return The number of test results that reported 'ok'. +std::size_t +engine::tap_summary::ok_count(void) const +{ + PRE(!bailed_out()); + PRE(_all_skipped_reason.empty()); + return _ok_count; +} + + +/// Gets the number of 'not ok' test results. +/// +/// \pre bailed_out() must be false. +/// +/// \return The number of test results that reported 'not ok'. +std::size_t +engine::tap_summary::not_ok_count(void) const +{ + PRE(!_bailed_out); + PRE(_all_skipped_reason.empty()); + return _not_ok_count; +} + + +/// Checks two tap_summary objects for equality. +/// +/// \param other The object to compare this one to. +/// +/// \return True if the two objects are equal; false otherwise. +bool +engine::tap_summary::operator==(const tap_summary& other) const +{ + return (_bailed_out == other._bailed_out && + _plan == other._plan && + _all_skipped_reason == other._all_skipped_reason && + _ok_count == other._ok_count && + _not_ok_count == other._not_ok_count); +} + + +/// Checks two tap_summary objects for inequality. +/// +/// \param other The object to compare this one to. +/// +/// \return True if the two objects are different; false otherwise. +bool +engine::tap_summary::operator!=(const tap_summary& other) const +{ + return !(*this == other); +} + + +/// Formats a tap_summary into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param summary The summary to format. +/// +/// \return The output stream. +std::ostream& +engine::operator<<(std::ostream& output, const tap_summary& summary) +{ + output << "tap_summary{"; + if (summary.bailed_out()) { + output << "bailed_out=true"; + } else { + const tap_plan& plan = summary.plan(); + output << "bailed_out=false" + << ", plan=" << plan.first << ".." << plan.second; + if (plan == all_skipped_plan) { + output << ", all_skipped_reason=" << summary.all_skipped_reason(); + } else { + output << ", ok_count=" << summary.ok_count() + << ", not_ok_count=" << summary.not_ok_count(); + } + } + output << "}"; + return output; +} + + +/// Parses an input file containing the TAP output of a test program. +/// +/// \param filename Path to the file to parse. +/// +/// \return The parsed data in the form of a tap_summary. +/// +/// \throw load_error If there are any problems parsing the file. Such problems +/// should be considered as test program breakage. +engine::tap_summary +engine::parse_tap_output(const utils::fs::path& filename) +{ + std::ifstream input(filename.str().c_str()); + if (!input) + throw engine::load_error(filename, "Failed to open TAP output file"); + + try { + return tap_summary(tap_parser().parse(input)); + } catch (const engine::format_error& e) { + throw engine::load_error(filename, e.what()); + } catch (const text::error& e) { + throw engine::load_error(filename, e.what()); + } +} diff --git a/contrib/kyua/engine/tap_parser.hpp b/contrib/kyua/engine/tap_parser.hpp new file mode 100644 index 000000000000..84cea908f5ba --- /dev/null +++ b/contrib/kyua/engine/tap_parser.hpp @@ -0,0 +1,99 @@ +// 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 engine/tap_parser.hpp +/// Utilities to parse TAP test program output. + +#if !defined(ENGINE_TAP_PARSER_HPP) +#define ENGINE_TAP_PARSER_HPP + +#include "engine/tap_parser_fwd.hpp" + +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace engine { + + +/// TAP plan representing all tests being skipped. +extern const engine::tap_plan all_skipped_plan; + + +/// TAP output representation and parser. +class tap_summary { + /// Whether the test program bailed out early or not. + bool _bailed_out; + + /// The TAP plan. Only valid if not bailed out. + tap_plan _plan; + + /// If not empty, the reason why all tests were skipped. + std::string _all_skipped_reason; + + /// Total number of 'ok' tests. Only valid if not balied out. + std::size_t _ok_count; + + /// Total number of 'not ok' tests. Only valid if not balied out. + std::size_t _not_ok_count; + + tap_summary(const bool, const tap_plan&, const std::string&, + const std::size_t, const std::size_t); + +public: + // Yes, these three constructors indicate that we really ought to have three + // different classes and select between them at runtime. But doing so would + // be overly complex for our really simple needs here. + static tap_summary new_bailed_out(void); + static tap_summary new_all_skipped(const std::string&); + static tap_summary new_results(const tap_plan&, const std::size_t, + const std::size_t); + + bool bailed_out(void) const; + const tap_plan& plan(void) const; + const std::string& all_skipped_reason(void) const; + std::size_t ok_count(void) const; + std::size_t not_ok_count(void) const; + + bool operator==(const tap_summary&) const; + bool operator!=(const tap_summary&) const; +}; + + +std::ostream& operator<<(std::ostream&, const tap_summary&); + + +tap_summary parse_tap_output(const utils::fs::path&); + + +} // namespace engine + + +#endif // !defined(ENGINE_TAP_PARSER_HPP) diff --git a/contrib/kyua/engine/tap_parser_fwd.hpp b/contrib/kyua/engine/tap_parser_fwd.hpp new file mode 100644 index 000000000000..481ed2f42267 --- /dev/null +++ b/contrib/kyua/engine/tap_parser_fwd.hpp @@ -0,0 +1,50 @@ +// 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 engine/tap_parser_fwd.hpp +/// Forward declarations for engine/tap_parser.hpp + +#if !defined(ENGINE_TAP_PARSER_FWD_HPP) +#define ENGINE_TAP_PARSER_FWD_HPP + +#include +#include + +namespace engine { + + +/// Representation of the TAP plan line. +typedef std::pair< std::size_t, std::size_t > tap_plan; + + +class tap_summary; + + +} // namespace engine + +#endif // !defined(ENGINE_TAP_PARSER_FWD_HPP) diff --git a/contrib/kyua/engine/tap_parser_test.cpp b/contrib/kyua/engine/tap_parser_test.cpp new file mode 100644 index 000000000000..af993bfab4ab --- /dev/null +++ b/contrib/kyua/engine/tap_parser_test.cpp @@ -0,0 +1,465 @@ +// 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 "engine/tap_parser.hpp" + +#include + +#include + +#include "engine/exceptions.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Helper to execute parse_tap_output() on inline text contents. +/// +/// \param contents The TAP output to parse. +/// +/// \return The tap_summary object resultingafter the parse. +/// +/// \throw engine::load_error If parse_tap_output() fails. +static engine::tap_summary +do_parse(const std::string& contents) +{ + std::ofstream output("tap.txt"); + ATF_REQUIRE(output); + output << contents; + output.close(); + return engine::parse_tap_output(fs::path("tap.txt")); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__bailed_out); +ATF_TEST_CASE_BODY(tap_summary__bailed_out) +{ + const engine::tap_summary summary = engine::tap_summary::new_bailed_out(); + ATF_REQUIRE(summary.bailed_out()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__some_results); +ATF_TEST_CASE_BODY(tap_summary__some_results) +{ + const engine::tap_summary summary = engine::tap_summary::new_results( + engine::tap_plan(1, 5), 3, 2); + ATF_REQUIRE(!summary.bailed_out()); + ATF_REQUIRE_EQ(engine::tap_plan(1, 5), summary.plan()); + ATF_REQUIRE_EQ(3, summary.ok_count()); + ATF_REQUIRE_EQ(2, summary.not_ok_count()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__all_skipped); +ATF_TEST_CASE_BODY(tap_summary__all_skipped) +{ + const engine::tap_summary summary = engine::tap_summary::new_all_skipped( + "Skipped"); + ATF_REQUIRE(!summary.bailed_out()); + ATF_REQUIRE_EQ(engine::tap_plan(1, 0), summary.plan()); + ATF_REQUIRE_EQ("Skipped", summary.all_skipped_reason()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__equality_operators); +ATF_TEST_CASE_BODY(tap_summary__equality_operators) +{ + const engine::tap_summary bailed_out = + engine::tap_summary::new_bailed_out(); + const engine::tap_summary all_skipped_1 = + engine::tap_summary::new_all_skipped("Reason 1"); + const engine::tap_summary results_1 = + engine::tap_summary::new_results(engine::tap_plan(1, 5), 3, 2); + + // Self-equality checks. + ATF_REQUIRE( bailed_out == bailed_out); + ATF_REQUIRE(!(bailed_out != bailed_out)); + ATF_REQUIRE( all_skipped_1 == all_skipped_1); + ATF_REQUIRE(!(all_skipped_1 != all_skipped_1)); + ATF_REQUIRE( results_1 == results_1); + ATF_REQUIRE(!(results_1 != results_1)); + + // Cross-equality checks. + ATF_REQUIRE(!(bailed_out == all_skipped_1)); + ATF_REQUIRE( bailed_out != all_skipped_1); + ATF_REQUIRE(!(bailed_out == results_1)); + ATF_REQUIRE( bailed_out != results_1); + ATF_REQUIRE(!(all_skipped_1 == results_1)); + ATF_REQUIRE( all_skipped_1 != results_1); + + // Checks for the all_skipped "type". + const engine::tap_summary all_skipped_2 = + engine::tap_summary::new_all_skipped("Reason 2"); + ATF_REQUIRE(!(all_skipped_1 == all_skipped_2)); + ATF_REQUIRE( all_skipped_1 != all_skipped_2); + + + // Checks for the results "type", different plan. + const engine::tap_summary results_2 = + engine::tap_summary::new_results(engine::tap_plan(2, 6), + results_1.ok_count(), + results_1.not_ok_count()); + ATF_REQUIRE(!(results_1 == results_2)); + ATF_REQUIRE( results_1 != results_2); + + + // Checks for the results "type", different counts. + const engine::tap_summary results_3 = + engine::tap_summary::new_results(results_1.plan(), + results_1.not_ok_count(), + results_1.ok_count()); + ATF_REQUIRE(!(results_1 == results_3)); + ATF_REQUIRE( results_1 != results_3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__output); +ATF_TEST_CASE_BODY(tap_summary__output) +{ + { + const engine::tap_summary summary = + engine::tap_summary::new_bailed_out(); + ATF_REQUIRE_EQ( + "tap_summary{bailed_out=true}", + (F("%s") % summary).str()); + } + + { + const engine::tap_summary summary = + engine::tap_summary::new_results(engine::tap_plan(5, 10), 2, 4); + ATF_REQUIRE_EQ( + "tap_summary{bailed_out=false, plan=5..10, ok_count=2, " + "not_ok_count=4}", + (F("%s") % summary).str()); + } + + { + const engine::tap_summary summary = + engine::tap_summary::new_all_skipped("Who knows"); + ATF_REQUIRE_EQ( + "tap_summary{bailed_out=false, plan=1..0, " + "all_skipped_reason=Who knows}", + (F("%s") % summary).str()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__only_one_result); +ATF_TEST_CASE_BODY(parse_tap_output__only_one_result) +{ + const engine::tap_summary summary = do_parse( + "1..1\n" + "ok - 1\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 1), 1, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__all_pass); +ATF_TEST_CASE_BODY(parse_tap_output__all_pass) +{ + const engine::tap_summary summary = do_parse( + "1..8\n" + "ok - 1\n" + " Some diagnostic message\n" + "ok - 2 This test also passed\n" + "garbage line\n" + "ok - 3 This test passed\n" + "not ok 4 # SKIP Some reason\n" + "not ok 5 # TODO Another reason\n" + "ok - 6 Doesn't make a difference SKIP\n" + "ok - 7 Doesn't make a difference either TODO\n" + "ok # Also works without a number\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 8), 8, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__some_fail); +ATF_TEST_CASE_BODY(parse_tap_output__some_fail) +{ + const engine::tap_summary summary = do_parse( + "garbage line\n" + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n" + "not ok - 3 This test failed\n" + "1..6\n" + "not ok - 4 This test failed\n" + "ok - 5 This test passed\n" + "not ok # Fails as well without a number\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 6), 2, 4); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_and_todo_variants); +ATF_TEST_CASE_BODY(parse_tap_output__skip_and_todo_variants) +{ + const engine::tap_summary summary = do_parse( + "1..8\n" + "not ok - 1 # SKIP Some reason\n" + "not ok - 2 # skip Some reason\n" + "not ok - 3 # Skipped Some reason\n" + "not ok - 4 # skipped Some reason\n" + "not ok - 5 # Skipped: Some reason\n" + "not ok - 6 # skipped: Some reason\n" + "not ok - 7 # TODO Some reason\n" + "not ok - 8 # todo Some reason\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 8), 8, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_all_with_reason); +ATF_TEST_CASE_BODY(parse_tap_output__skip_all_with_reason) +{ + const engine::tap_summary summary = do_parse( + "1..0 SKIP Some reason for skipping\n" + "ok - 1\n" + " Some diagnostic message\n" + "ok - 6 Doesn't make a difference SKIP\n" + "ok - 7 Doesn't make a difference either TODO\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_all_skipped("Some reason for skipping"); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_all_without_reason); +ATF_TEST_CASE_BODY(parse_tap_output__skip_all_without_reason) +{ + const engine::tap_summary summary = do_parse( + "1..0 unrecognized # garbage skip\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_all_skipped("No reason specified"); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_all_invalid); +ATF_TEST_CASE_BODY(parse_tap_output__skip_all_invalid) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, + "Skipped plan must be 1\\.\\.0", + do_parse("1..3 # skip\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__plan_at_end); +ATF_TEST_CASE_BODY(parse_tap_output__plan_at_end) +{ + const engine::tap_summary summary = do_parse( + "ok - 1\n" + " Some diagnostic message\n" + "ok - 2 This test also passed\n" + "garbage line\n" + "ok - 3 This test passed\n" + "not ok 4 # SKIP Some reason\n" + "not ok 5 # TODO Another reason\n" + "ok - 6 Doesn't make a difference SKIP\n" + "ok - 7 Doesn't make a difference either TODO\n" + "1..7\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 7), 7, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__stray_oks); +ATF_TEST_CASE_BODY(parse_tap_output__stray_oks) +{ + const engine::tap_summary summary = do_parse( + "1..3\n" + "ok - 1\n" + "ok\n" + "ok - 2 This test also passed\n" + "not ok\n" + "ok - 3 This test passed\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 3), 3, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__no_plan); +ATF_TEST_CASE_BODY(parse_tap_output__no_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Output did not contain any TAP plan", + do_parse( + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__double_plan); +ATF_TEST_CASE_BODY(parse_tap_output__double_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Found duplicate plan", + do_parse( + "garbage line\n" + "1..5\n" + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n" + "1..8\n" + "ok\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__inconsistent_plan); +ATF_TEST_CASE_BODY(parse_tap_output__inconsistent_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Reported plan differs from actual executed tests", + do_parse( + "1..3\n" + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__inconsistent_trailing_plan); +ATF_TEST_CASE_BODY(parse_tap_output__inconsistent_trailing_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Reported plan differs from actual executed tests", + do_parse( + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n" + "1..3\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__insane_plan); +ATF_TEST_CASE_BODY(parse_tap_output__insane_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, "Invalid value", + do_parse("120830981209831..234891793874080981092803981092312\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__reversed_plan); +ATF_TEST_CASE_BODY(parse_tap_output__reversed_plan) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, + "Found reversed plan 8\\.\\.5", + do_parse("8..5\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__bail_out); +ATF_TEST_CASE_BODY(parse_tap_output__bail_out) +{ + const engine::tap_summary summary = do_parse( + "1..3\n" + "not ok - 1 This test failed\n" + "Bail out! There is some unknown problem\n" + "ok - 2 This test passed\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_bailed_out(); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__bail_out_wins_over_no_plan); +ATF_TEST_CASE_BODY(parse_tap_output__bail_out_wins_over_no_plan) +{ + const engine::tap_summary summary = do_parse( + "not ok - 1 This test failed\n" + "Bail out! There is some unknown problem\n" + "ok - 2 This test passed\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_bailed_out(); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__open_failure); +ATF_TEST_CASE_BODY(parse_tap_output__open_failure) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, "hello.txt.*Failed to open", + engine::parse_tap_output(fs::path("hello.txt"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, tap_summary__bailed_out); + ATF_ADD_TEST_CASE(tcs, tap_summary__some_results); + ATF_ADD_TEST_CASE(tcs, tap_summary__all_skipped); + ATF_ADD_TEST_CASE(tcs, tap_summary__equality_operators); + ATF_ADD_TEST_CASE(tcs, tap_summary__output); + + ATF_ADD_TEST_CASE(tcs, parse_tap_output__only_one_result); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__all_pass); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__some_fail); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_and_todo_variants); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_all_without_reason); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_all_with_reason); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_all_invalid); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__plan_at_end); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__stray_oks); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__no_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__double_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__inconsistent_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__inconsistent_trailing_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__insane_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__reversed_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__bail_out); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__bail_out_wins_over_no_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__open_failure); +} diff --git a/contrib/kyua/engine/tap_test.cpp b/contrib/kyua/engine/tap_test.cpp new file mode 100644 index 000000000000..f4253a68e727 --- /dev/null +++ b/contrib/kyua/engine/tap_test.cpp @@ -0,0 +1,218 @@ +// 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 "engine/tap.hpp" + +extern "C" { +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.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" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Copies the tap helper to the work directory, selecting a specific helper. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param name Name of the new binary to create. Must match the name of a +/// valid helper, as the binary name is used to select it. +static void +copy_tap_helper(const atf::tests::tc* tc, const char* name) +{ + const fs::path srcdir(tc->get_config_var("srcdir")); + atf::utils::copy_file((srcdir / "tap_helpers").str(), name); +} + + +/// Runs one tap test program and checks its result. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param test_case_name Name of the "test case" to select from the helper +/// program. +/// \param exp_result The expected result. +/// \param metadata The test case metadata. +/// \param user_config User-provided configuration variables. +static void +run_one(const atf::tests::tc* tc, const char* test_case_name, + const model::test_result& exp_result, + const model::metadata& metadata = model::metadata_builder().build(), + const config::tree& user_config = engine::empty_config()) +{ + copy_tap_helper(tc, test_case_name); + const model::test_program_ptr program = model::test_program_builder( + "tap", fs::path(test_case_name), fs::current_path(), "the-suite") + .add_test_case("main", metadata).build_ptr(); + + scheduler::scheduler_handle handle = scheduler::setup(); + (void)handle.spawn_test(program, "main", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + atf::utils::cat_file(result_handle->stdout_file().str(), "stdout: "); + atf::utils::cat_file(result_handle->stderr_file().str(), "stderr: "); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(list); +ATF_TEST_CASE_BODY(list) +{ + const model::test_program program = model::test_program_builder( + "tap", fs::path("non-existent"), fs::path("."), "unused-suite") + .build(); + + scheduler::scheduler_handle handle = scheduler::setup(); + const model::test_cases_map test_cases = handle.list_tests( + &program, engine::empty_config()); + handle.cleanup(); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("main").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__all_tests_pass); +ATF_TEST_CASE_BODY(test__all_tests_pass) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__some_tests_fail); +ATF_TEST_CASE_BODY(test__some_tests_fail) +{ + const model::test_result exp_result(model::test_result_failed, + "2 of 5 tests failed"); + run_one(this, "fail", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__all_tests_pass_but_exit_failure); +ATF_TEST_CASE_BODY(test__all_tests_pass_but_exit_failure) +{ + const model::test_result exp_result( + model::test_result_broken, + "Dubious test program: reported all tests as passed but returned exit " + "code 70"); + run_one(this, "pass_but_exit_failure", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__signal_is_broken); +ATF_TEST_CASE_BODY(test__signal_is_broken) +{ + const model::test_result exp_result(model::test_result_broken, + F("Received signal %s") % SIGABRT); + run_one(this, "crash", exp_result); +} + + +ATF_TEST_CASE(test__timeout_is_broken); +ATF_TEST_CASE_HEAD(test__timeout_is_broken) +{ + set_md_var("timeout", "60"); +} +ATF_TEST_CASE_BODY(test__timeout_is_broken) +{ + utils::setenv("CONTROL_DIR", fs::current_path().str()); + + const model::metadata metadata = model::metadata_builder() + .set_timeout(datetime::delta(1, 0)).build(); + const model::test_result exp_result(model::test_result_broken, + "Test case timed out"); + run_one(this, "timeout", exp_result, metadata); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__configuration_variables); +ATF_TEST_CASE_BODY(test__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.a-suite.first", "unused"); + user_config.set_string("test_suites.the-suite.first", "some value"); + user_config.set_string("test_suites.the-suite.second", "some other value"); + user_config.set_string("test_suites.other-suite.first", "unused"); + + const model::test_result exp_result(model::test_result_passed); + run_one(this, "check_configuration_variables", exp_result, + model::metadata_builder().build(), user_config); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "tap", std::shared_ptr< scheduler::interface >( + new engine::tap_interface())); + + ATF_ADD_TEST_CASE(tcs, list); + + ATF_ADD_TEST_CASE(tcs, test__all_tests_pass); + ATF_ADD_TEST_CASE(tcs, test__all_tests_pass_but_exit_failure); + ATF_ADD_TEST_CASE(tcs, test__some_tests_fail); + ATF_ADD_TEST_CASE(tcs, test__signal_is_broken); + ATF_ADD_TEST_CASE(tcs, test__timeout_is_broken); + ATF_ADD_TEST_CASE(tcs, test__configuration_variables); +} diff --git a/contrib/kyua/examples/Kyuafile b/contrib/kyua/examples/Kyuafile new file mode 100644 index 000000000000..2c8f39baad58 --- /dev/null +++ b/contrib/kyua/examples/Kyuafile @@ -0,0 +1,5 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="syntax_test"} diff --git a/contrib/kyua/examples/Kyuafile.top b/contrib/kyua/examples/Kyuafile.top new file mode 100644 index 000000000000..3c28945f0cf5 --- /dev/null +++ b/contrib/kyua/examples/Kyuafile.top @@ -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. + +-- Example top-level Kyuafile. +-- +-- This sample top-level Kyuafile looks for any */Kyuafile files and includes +-- them in order to process all the test cases within a test suite. +-- +-- This file is supposed to be installed in the root directory of the tests +-- hierarchy; typically, this is /usr/tests/Kyuafile (note that the .top +-- extension has been dropped). Third-party packages install tests as +-- subdirectories of /usr/tests. When doing so, they should not have to update +-- the contents of the top-level Kyuafile; in other words, Kyua needs to +-- discover tests in such subdirectories automatically. + +syntax(2) + +for file in fs.files(".") do + if file == "." or file == ".." then + -- Skip these special entries. + else + local kyuafile = fs.join(file, "Kyuafile") + if fs.exists(kyuafile) then + include(kyuafile) + end + end +end diff --git a/contrib/kyua/examples/Makefile.am.inc b/contrib/kyua/examples/Makefile.am.inc new file mode 100644 index 000000000000..3c9b27dae6a9 --- /dev/null +++ b/contrib/kyua/examples/Makefile.am.inc @@ -0,0 +1,45 @@ +# 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. + +dist_examples_DATA = examples/Kyuafile.top +dist_examples_DATA += examples/kyua.conf + +if WITH_ATF +tests_examplesdir = $(pkgtestsdir)/examples + +tests_examples_DATA = examples/Kyuafile +EXTRA_DIST += $(tests_examples_DATA) + +tests_examples_PROGRAMS = examples/syntax_test +examples_syntax_test_SOURCES = examples/syntax_test.cpp +examples_syntax_test_CPPFLAGS = -DKYUA_EXAMPLESDIR="\"$(examplesdir)\"" +examples_syntax_test_CXXFLAGS = $(ENGINE_CFLAGS) $(UTILS_CFLAGS) \ + $(ATF_CXX_CFLAGS) +examples_syntax_test_LDADD = $(ENGINE_LIBS) $(UTILS_LIBS) \ + $(ATF_CXX_LIBS) +endif diff --git a/contrib/kyua/examples/kyua.conf b/contrib/kyua/examples/kyua.conf new file mode 100644 index 000000000000..83418a320dc4 --- /dev/null +++ b/contrib/kyua/examples/kyua.conf @@ -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. + +-- Example file for the configuration of Kyua. +-- +-- All the values shown here do not reflect the default values that Kyua +-- is using on this installation: these are just fictitious settings that +-- may or may not work. +-- +-- To write your own configuration file, it is recommended that you start +-- from a blank file and then define only those settings that you want to +-- override. If you want to use this file as a template, you will have +-- to comment out all the settings first to prevent any side-effects. + +-- The file must start by declaring the name and version of its format. +syntax(2) + +-- Name of the system architecture (aka processor type). +architecture = "x86_64" + +-- Maximum number of jobs (such as test case runs) to execute concurrently. +parallelism = 16 + +-- Name of the system platform (aka machine type). +platform = "amd64" + +-- The name or UID of the unprivileged user. +-- +-- If set, this user must exist in the system and his privileges will be +-- used to run test cases that need regular privileges when Kyua is +-- executed as root. +unprivileged_user = "nobody" + +-- Set actual configuration properties for the test suite named 'kyua'. +test_suites.kyua.run_coredump_tests = "false" + +-- Set fictitious configuration properties for the test suite named 'FreeBSD'. +test_suites.FreeBSD.iterations = "1000" +test_suites.FreeBSD.run_old_tests = "false" + +-- Set fictitious configuration properties for the test suite named 'NetBSD'. +test_suites.NetBSD.file_systems = "ffs lfs ext2fs" +test_suites.NetBSD.iterations = "100" +test_suites.NetBSD.run_broken_tests = "true" diff --git a/contrib/kyua/examples/syntax_test.cpp b/contrib/kyua/examples/syntax_test.cpp new file mode 100644 index 000000000000..a90acb810d4f --- /dev/null +++ b/contrib/kyua/examples/syntax_test.cpp @@ -0,0 +1,210 @@ +// 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. + +extern "C" { +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/kyuafile.hpp" +#include "engine/plain.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Gets the path to an example file. +/// +/// \param name The name of the example file. +/// +/// \return A path to the desired example file. This can either be inside the +/// source tree before installing Kyua or in the target installation directory +/// after installation. +static fs::path +example_file(const char* name) +{ + const fs::path examplesdir(utils::getenv_with_default( + "KYUA_EXAMPLESDIR", KYUA_EXAMPLESDIR)); + return examplesdir / name; +} + + +} // anonymous namespace + + +ATF_TEST_CASE(kyua_conf); +ATF_TEST_CASE_HEAD(kyua_conf) +{ + utils::logging::set_inmemory(); + set_md_var("require.files", example_file("kyua.conf").str()); +} +ATF_TEST_CASE_BODY(kyua_conf) +{ + std::vector< passwd::user > users; + users.push_back(passwd::user("nobody", 1, 2)); + passwd::set_mock_users_for_testing(users); + + const config::tree user_config = engine::load_config( + example_file("kyua.conf")); + + ATF_REQUIRE_EQ( + "x86_64", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ( + 16, + user_config.lookup< config::positive_int_node >("parallelism")); + ATF_REQUIRE_EQ( + "amd64", + user_config.lookup< config::string_node >("platform")); + + ATF_REQUIRE_EQ( + "nobody", + user_config.lookup< engine::user_node >("unprivileged_user").name); + + config::properties_map exp_test_suites; + exp_test_suites["test_suites.kyua.run_coredump_tests"] = "false"; + exp_test_suites["test_suites.FreeBSD.iterations"] = "1000"; + exp_test_suites["test_suites.FreeBSD.run_old_tests"] = "false"; + exp_test_suites["test_suites.NetBSD.file_systems"] = "ffs lfs ext2fs"; + exp_test_suites["test_suites.NetBSD.iterations"] = "100"; + exp_test_suites["test_suites.NetBSD.run_broken_tests"] = "true"; + ATF_REQUIRE(exp_test_suites == user_config.all_properties("test_suites")); +} + + +ATF_TEST_CASE(kyuafile_top__no_matches); +ATF_TEST_CASE_HEAD(kyuafile_top__no_matches) +{ + utils::logging::set_inmemory(); + set_md_var("require.files", example_file("Kyuafile.top").str()); +} +ATF_TEST_CASE_BODY(kyuafile_top__no_matches) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("root"), 0755); + const fs::path source_path = example_file("Kyuafile.top"); + ATF_REQUIRE(::symlink(source_path.c_str(), "root/Kyuafile") != -1); + + atf::utils::create_file("root/file", ""); + fs::mkdir(fs::path("root/subdir"), 0755); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + fs::path("root/Kyuafile"), none, engine::default_config(), handle); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.source_root()); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.build_root()); + ATF_REQUIRE(kyuafile.test_programs().empty()); + + handle.cleanup(); +} + + +ATF_TEST_CASE(kyuafile_top__some_matches); +ATF_TEST_CASE_HEAD(kyuafile_top__some_matches) +{ + utils::logging::set_inmemory(); + set_md_var("require.files", example_file("Kyuafile.top").str()); +} +ATF_TEST_CASE_BODY(kyuafile_top__some_matches) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("root"), 0755); + const fs::path source_path = example_file("Kyuafile.top"); + ATF_REQUIRE(::symlink(source_path.c_str(), "root/Kyuafile") != -1); + + atf::utils::create_file("root/file", ""); + + fs::mkdir(fs::path("root/subdir1"), 0755); + atf::utils::create_file("root/subdir1/Kyuafile", + "syntax(2)\n" + "plain_test_program{name='a', test_suite='b'}\n"); + atf::utils::create_file("root/subdir1/a", ""); + + fs::mkdir(fs::path("root/subdir2"), 0755); + atf::utils::create_file("root/subdir2/Kyuafile", + "syntax(2)\n" + "plain_test_program{name='c', test_suite='d'}\n"); + atf::utils::create_file("root/subdir2/c", ""); + atf::utils::create_file("root/subdir2/Kyuafile.etc", "invalid"); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + fs::path("root/Kyuafile"), none, engine::default_config(), handle); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.source_root()); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.build_root()); + + const model::test_program exp_test_program_a = model::test_program_builder( + "plain", fs::path("subdir1/a"), fs::path("root").to_absolute(), "b") + .add_test_case("main") + .build(); + const model::test_program exp_test_program_c = model::test_program_builder( + "plain", fs::path("subdir2/c"), fs::path("root").to_absolute(), "d") + .add_test_case("main") + .build(); + + ATF_REQUIRE_EQ(2, kyuafile.test_programs().size()); + ATF_REQUIRE((exp_test_program_a == *kyuafile.test_programs()[0] && + exp_test_program_c == *kyuafile.test_programs()[1]) + || + (exp_test_program_a == *kyuafile.test_programs()[1] && + exp_test_program_c == *kyuafile.test_programs()[0])); + + handle.cleanup(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + + ATF_ADD_TEST_CASE(tcs, kyua_conf); + + ATF_ADD_TEST_CASE(tcs, kyuafile_top__no_matches); + ATF_ADD_TEST_CASE(tcs, kyuafile_top__some_matches); +} diff --git a/contrib/kyua/integration/Kyuafile b/contrib/kyua/integration/Kyuafile new file mode 100644 index 000000000000..2ebb4ec8acca --- /dev/null +++ b/contrib/kyua/integration/Kyuafile @@ -0,0 +1,16 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="cmd_about_test"} +atf_test_program{name="cmd_config_test"} +atf_test_program{name="cmd_db_exec_test"} +atf_test_program{name="cmd_db_migrate_test"} +atf_test_program{name="cmd_debug_test"} +atf_test_program{name="cmd_help_test"} +atf_test_program{name="cmd_list_test"} +atf_test_program{name="cmd_report_html_test"} +atf_test_program{name="cmd_report_junit_test"} +atf_test_program{name="cmd_report_test"} +atf_test_program{name="cmd_test_test"} +atf_test_program{name="global_test"} diff --git a/contrib/kyua/integration/Makefile.am.inc b/contrib/kyua/integration/Makefile.am.inc new file mode 100644 index 000000000000..cf9ad86d5730 --- /dev/null +++ b/contrib/kyua/integration/Makefile.am.inc @@ -0,0 +1,150 @@ +# 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 WITH_ATF +tests_integrationdir = $(pkgtestsdir)/integration + +tests_integration_DATA = integration/Kyuafile +EXTRA_DIST += $(tests_integration_DATA) + +ATF_SH_BUILD = \ + $(MKDIR_P) integration; \ + echo "\#! $(ATF_SH)" >integration/$${name}; \ + echo "\# AUTOMATICALLY GENERATED FROM Makefile" >>integration/$${name}; \ + if [ -n "$${substs}" ]; then \ + cat $(srcdir)/integration/utils.sh $(srcdir)/integration/$${name}.sh \ + | sed "$${substs}" >>integration/$${name}; \ + else \ + cat $(srcdir)/integration/utils.sh $(srcdir)/integration/$${name}.sh \ + >>integration/$${name}; \ + fi; \ + chmod +x integration/$${name} + +ATF_SH_DEPS = \ + $(srcdir)/integration/utils.sh \ + Makefile + +EXTRA_DIST += integration/utils.sh + +tests_integration_SCRIPTS = integration/cmd_about_test +CLEANFILES += integration/cmd_about_test +EXTRA_DIST += integration/cmd_about_test.sh +integration/cmd_about_test: $(srcdir)/integration/cmd_about_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_about_test"; \ + substs='s,__KYUA_DOCDIR__,$(docdir),g'; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_config_test +CLEANFILES += integration/cmd_config_test +EXTRA_DIST += integration/cmd_config_test.sh +integration/cmd_config_test: $(srcdir)/integration/cmd_config_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_config_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_db_exec_test +CLEANFILES += integration/cmd_db_exec_test +EXTRA_DIST += integration/cmd_db_exec_test.sh +integration/cmd_db_exec_test: $(srcdir)/integration/cmd_db_exec_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_db_exec_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_db_migrate_test +CLEANFILES += integration/cmd_db_migrate_test +EXTRA_DIST += integration/cmd_db_migrate_test.sh +integration/cmd_db_migrate_test: $(srcdir)/integration/cmd_db_migrate_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_db_migrate_test"; \ + substs='s,__KYUA_STOREDIR__,$(storedir),g'; \ + substs="$${substs};s,__KYUA_STORETESTDATADIR__,$(tests_storedir),g"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_debug_test +CLEANFILES += integration/cmd_debug_test +EXTRA_DIST += integration/cmd_debug_test.sh +integration/cmd_debug_test: $(srcdir)/integration/cmd_debug_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_debug_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_help_test +CLEANFILES += integration/cmd_help_test +EXTRA_DIST += integration/cmd_help_test.sh +integration/cmd_help_test: $(srcdir)/integration/cmd_help_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_help_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_list_test +CLEANFILES += integration/cmd_list_test +EXTRA_DIST += integration/cmd_list_test.sh +integration/cmd_list_test: $(srcdir)/integration/cmd_list_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_list_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_report_test +CLEANFILES += integration/cmd_report_test +EXTRA_DIST += integration/cmd_report_test.sh +integration/cmd_report_test: $(srcdir)/integration/cmd_report_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_report_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_report_html_test +CLEANFILES += integration/cmd_report_html_test +EXTRA_DIST += integration/cmd_report_html_test.sh +integration/cmd_report_html_test: \ + $(srcdir)/integration/cmd_report_html_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_report_html_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_report_junit_test +CLEANFILES += integration/cmd_report_junit_test +EXTRA_DIST += integration/cmd_report_junit_test.sh +integration/cmd_report_junit_test: \ + $(srcdir)/integration/cmd_report_junit_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_report_junit_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_test_test +CLEANFILES += integration/cmd_test_test +EXTRA_DIST += integration/cmd_test_test.sh +integration/cmd_test_test: $(srcdir)/integration/cmd_test_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_test_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/global_test +CLEANFILES += integration/global_test +EXTRA_DIST += integration/global_test.sh +integration/global_test: $(srcdir)/integration/global_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="global_test"; \ + $(ATF_SH_BUILD) +endif + +include integration/helpers/Makefile.am.inc diff --git a/contrib/kyua/integration/cmd_about_test.sh b/contrib/kyua/integration/cmd_about_test.sh new file mode 100755 index 000000000000..06d5da5ac4c2 --- /dev/null +++ b/contrib/kyua/integration/cmd_about_test.sh @@ -0,0 +1,158 @@ +# 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. + + +# Location of installed documents. Used to validate the output of the about +# messages against the golden files. +: "${KYUA_DOCDIR:=__KYUA_DOCDIR__}" + + +# Common code to validate the output of all about information. +# +# \param file The name of the file with the output. +check_all() { + local file="${1}"; shift + + grep -E 'kyua .*[0-9]+\.[0-9]+' "${file}" || \ + atf_fail 'No version reported' + grep 'Copyright' "${file}" || atf_fail 'No license reported' + grep '^\*[^<>]*$' "${file}" || atf_fail 'No authors reported' + grep '^\*.*<.*@.*>$' "${file}" || atf_fail 'No contributors reported' + grep 'Homepage' "${file}" || atf_fail 'No homepage reported' +} + + +utils_test_case all_topics__installed +all_topics__installed_head() { + atf_set "require.files" "${KYUA_DOCDIR}/AUTHORS" \ + "${KYUA_DOCDIR}/CONTRIBUTORS" "${KYUA_DOCDIR}/LICENSE" +} +all_topics__installed_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua about + check_all stdout +} + + +utils_test_case all_topics__override +all_topics__override_body() { + mkdir docs + echo "* Author (no email)" >docs/AUTHORS + echo "* Contributor " >docs/CONTRIBUTORS + echo "Copyright text" >docs/LICENSE + export KYUA_DOCDIR=docs + atf_check -s exit:0 -o save:stdout -e empty kyua about + check_all stdout +} + + +utils_test_case topic__authors__installed +topic__authors__installed_head() { + atf_set "require.files" "${KYUA_DOCDIR}/AUTHORS" \ + "${KYUA_DOCDIR}/CONTRIBUTORS" +} +topic__authors__installed_body() { + grep -h '^\* ' "${KYUA_DOCDIR}/AUTHORS" "${KYUA_DOCDIR}/CONTRIBUTORS" \ + >expout + atf_check -s exit:0 -o file:expout -e empty kyua about authors +} + + +utils_test_case topic__authors__override +topic__authors__override_body() { + mkdir docs + echo "* Author (no email)" >docs/AUTHORS + echo "* Contributor " >docs/CONTRIBUTORS + export KYUA_DOCDIR=docs + cat docs/AUTHORS docs/CONTRIBUTORS >expout + atf_check -s exit:0 -o file:expout -e empty kyua about authors +} + + +utils_test_case topic__license__installed +topic__license__installed_head() { + atf_set "require.files" "${KYUA_DOCDIR}/LICENSE" +} +topic__license__installed_body() { + atf_check -s exit:0 -o file:"${KYUA_DOCDIR}/LICENSE" -e empty \ + kyua about license +} + + +utils_test_case topic__license__override +topic__license__override_body() { + mkdir docs + echo "Copyright text" >docs/LICENSE + export KYUA_DOCDIR=docs + atf_check -s exit:0 -o file:docs/LICENSE -e empty kyua about license +} + + +utils_test_case topic__version +topic__version_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua about version + + local lines="$(wc -l stdout | awk '{ print $1 }')" + [ "${lines}" -eq 1 ] || atf_fail "Version query returned more than one line" + + grep -E '^kyua (.*) [0-9]+\.[0-9]+$' stdout || \ + atf_fail "Invalid version message" +} + + +utils_test_case topic__invalid +topic__invalid_body() { + cat >experr <stderr <"${HOME}/.kyua/kyua.conf" <expout <"${HOME}/.kyua/kyua.conf" <expout <experr <kyua.conf <kyua.conf <.kyua/kyua.conf <kyua.conf <kyua.conf <experr <"${HOME}/.kyua/kyua.conf" <config <experr <experr <Kyuafile <expout <expout2 + atf_check -s exit:0 -o file:expout2 -e empty \ + kyua db-exec --no-headers "SELECT * FROM data ORDER BY a" +} + + +atf_init_test_cases() { + atf_add_test_case one_arg + atf_add_test_case many_args + atf_add_test_case no_args + atf_add_test_case invalid_statement + atf_add_test_case no_create_store + + atf_add_test_case results_file__default_home + atf_add_test_case results_file__explicit__ok + atf_add_test_case results_file__explicit__fail + + atf_add_test_case no_headers_flag +} diff --git a/contrib/kyua/integration/cmd_db_migrate_test.sh b/contrib/kyua/integration/cmd_db_migrate_test.sh new file mode 100755 index 000000000000..404a4e774019 --- /dev/null +++ b/contrib/kyua/integration/cmd_db_migrate_test.sh @@ -0,0 +1,167 @@ +# Copyright 2013 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. + + +# Location of installed schema files. +: "${KYUA_STOREDIR:=__KYUA_STOREDIR__}" + + +# Location of installed test data files. +: "${KYUA_STORETESTDATADIR:=__KYUA_STORETESTDATADIR__}" + + +# Creates an empty old-style action database. +# +# \param ... Files that contain SQL commands to be run. +create_historical_db() { + mkdir -p "${HOME}/.kyua" + cat "${@}" | sqlite3 "${HOME}/.kyua/store.db" +} + + +# Creates an empty results file. +# +# \param ... Files that contain SQL commands to be run. +create_results_file() { + mkdir -p "${HOME}/.kyua/store" + local dbname="results.$(utils_test_suite_id)-20140718-173200-123456.db" + cat "${@}" | sqlite3 "${HOME}/.kyua/store/${dbname}" +} + + +utils_test_case upgrade__from_v1 +upgrade__from_v1_head() { + atf_set require.files \ + "${KYUA_STORETESTDATADIR}/schema_v1.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v1.sql" \ + "${KYUA_STOREDIR}/migrate_v1_v2.sql" \ + "${KYUA_STOREDIR}/migrate_v2_v3.sql" + atf_set require.progs "sqlite3" +} +upgrade__from_v1_body() { + create_historical_db "${KYUA_STORETESTDATADIR}/schema_v1.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v1.sql" + atf_check -s exit:0 -o empty -e empty kyua db-migrate + for f in \ + "results.test_suite_root.20130108-111331-000000.db" \ + "results.usr_tests.20130108-123832-000000.db" \ + "results.usr_tests.20130108-112635-000000.db" + do + [ -f "${HOME}/.kyua/store/${f}" ] || atf_fail "Expected file ${f}" \ + "was not created" + done + [ ! -f "${HOME}/.kyua/store.db" ] || atf_fail "Historical database not" \ + "deleted" +} + + +utils_test_case upgrade__from_v2 +upgrade__from_v2_head() { + atf_set require.files \ + "${KYUA_STORETESTDATADIR}/schema_v2.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v2.sql" \ + "${KYUA_STOREDIR}/migrate_v2_v3.sql" + atf_set require.progs "sqlite3" +} +upgrade__from_v2_body() { + create_historical_db "${KYUA_STORETESTDATADIR}/schema_v2.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v2.sql" + atf_check -s exit:0 -o empty -e empty kyua db-migrate + for f in \ + "results.test_suite_root.20130108-111331-000000.db" \ + "results.usr_tests.20130108-123832-000000.db" \ + "results.usr_tests.20130108-112635-000000.db" + do + [ -f "${HOME}/.kyua/store/${f}" ] || atf_fail "Expected file ${f}" \ + "was not created" + done + [ ! -f "${HOME}/.kyua/store.db" ] || atf_fail "Historical database not" \ + "deleted" +} + + +utils_test_case already_up_to_date +already_up_to_date_head() { + atf_set require.files "${KYUA_STOREDIR}/schema_v3.sql" + atf_set require.progs "sqlite3" +} +already_up_to_date_body() { + create_results_file "${KYUA_STOREDIR}/schema_v3.sql" + atf_check -s exit:1 -o empty -e match:"already at schema version" \ + kyua db-migrate +} + + +utils_test_case need_upgrade +need_upgrade_head() { + atf_set require.files "${KYUA_STORETESTDATADIR}/schema_v1.sql" + atf_set require.progs "sqlite3" +} +need_upgrade_body() { + create_results_file "${KYUA_STORETESTDATADIR}/schema_v1.sql" + atf_check -s exit:2 -o empty \ + -e match:"database has schema version 1.*use db-migrate" kyua report +} + + +utils_test_case results_file__ok +results_file__ok_body() { + echo "This is not a valid database" >test.db + atf_check -s exit:1 -o empty -e match:"Migration failed" \ + kyua db-migrate --results-file ./test.db +} + + +utils_test_case results_file__fail +results_file__fail_body() { + atf_check -s exit:1 -o empty -e match:"No previous results.*test.db" \ + kyua db-migrate --results-file ./test.db +} + + +utils_test_case too_many_arguments +too_many_arguments_body() { + cat >stderr <Kyuafile <experr <Kyuafile <experr <Kyuafile <expout < passed +EOF +cat >experr <Kyuafile <expout < failed: This fails on purpose +EOF + cat >experr <Kyuafile <experr <experr <experr <Kyuafile <expout < passed +EOF + atf_check -s exit:0 -o file:expout -e empty kyua debug \ + --stdout=saved.out --stderr=saved.err single:with_cleanup + + cat >expout <experr <Kyuafile <expout < passed +EOF + atf_check -s exit:0 -o file:expout -e empty kyua debug \ + --stdout=saved.out --stderr=saved.err second:pass + + cat >expout <experr <root/Kyuafile <root/subdir/Kyuafile <expout < failed: This fails on purpose +EOF + cat >experr <Kyuafile <expout < passed +EOF + cat >experr <"my-config" <Kyuafile <Kyuafile <expout < passed +EOF +cat >experr <Kyuafile <myfile <experr <Kyuafile <Kyuafile <non_executable + + cat >experr <experr <expout < broken: Invalid header for test case list; expecting Content-Type for application/X-atf-tp version 1, got '' +EOF + # CHECK_STYLE_ENABLE + atf_check -s exit:1 -o file:expout -e empty kyua debug \ + crash_on_list:__test_cases_list__ + + # CHECK_STYLE_DISABLE + cat >expout < broken: Permission denied to run test program +EOF + # CHECK_STYLE_ENABLE + atf_check -s exit:1 -o file:expout -e empty kyua debug \ + non_executable:__test_cases_list__ +} + + +atf_init_test_cases() { + atf_add_test_case no_args + atf_add_test_case many_args + atf_add_test_case one_arg__ok_pass + atf_add_test_case one_arg__ok_fail + atf_add_test_case one_arg__no_match + atf_add_test_case one_arg__no_test_case + atf_add_test_case one_arg__bad_filter + + atf_add_test_case body_and_cleanup + + atf_add_test_case stdout_stderr_flags + + atf_add_test_case args_are_relative + + atf_add_test_case only_load_used_test_programs + + atf_add_test_case config_behavior + + atf_add_test_case build_root_flag + atf_add_test_case kyuafile_flag__ok + atf_add_test_case missing_kyuafile + atf_add_test_case bogus_kyuafile + atf_add_test_case bogus_test_program +} diff --git a/contrib/kyua/integration/cmd_help_test.sh b/contrib/kyua/integration/cmd_help_test.sh new file mode 100755 index 000000000000..d8afbd0e6aba --- /dev/null +++ b/contrib/kyua/integration/cmd_help_test.sh @@ -0,0 +1,93 @@ +# 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_test_case global +global_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua help + grep -E 'kyua .*[0-9]+\.[0-9]+' stdout || atf_fail 'No version reported' + grep '^Usage: kyua' stdout || atf_fail 'No usage line printed' + grep -- '--loglevel' stdout || atf_fail 'Generic options not printed' + if grep -- '--show' stdout; then + atf_fail 'One option of the about subcommand appeared in the output' + fi + grep 'about *Shows detailed' stdout || atf_fail 'Commands not printed' +} + + +utils_test_case one_command +one_command_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua help test + grep -E 'kyua .*[0-9]+\.[0-9]+' stdout || atf_fail 'No version reported' + grep '^Usage: kyua' stdout || atf_fail 'No usage line printed' + grep '^Run tests' stdout || atf_fail 'No description printed' + grep -- '--loglevel' stdout || atf_fail 'Generic options not printed' + grep -- '--kyuafile' stdout || atf_fail 'Command options not printed' + if grep 'about: Shows detailed' stdout; then + atf_fail 'Printed table of commands, but should not have done so' + fi +} + + +utils_test_case ignore_bad_config +ignore_bad_config_body() { + echo 'this is an invalid configuration file' >bad-config + atf_check -s exit:0 -o save:stdout -e empty kyua -c bad-config help + grep '^Usage: kyua' stdout || atf_fail 'No usage line printed' + grep -- '--loglevel' stdout || atf_fail 'Generic options not printed' +} + + +utils_test_case unknown_command +unknown_command_body() { + cat >stderr <stderr <Kyuafile <subdir/Kyuafile <expout <Kyuafile <subdir/Kyuafile <expout <Kyuafile <expout <Kyuafile <expout <experr <experr <Kyuafile <subdir/Kyuafile <expout <experr <experr <Kyuafile <experr <Kyuafile <expout <experr <root/Kyuafile <root/subdir/Kyuafile <expout <Kyuafile <expout <"my-config" <Kyuafile <Kyuafile <first + utils_cp_helper simple_all_pass build/first + + cat >subdir/Kyuafile <subdir/second + utils_cp_helper simple_some_fail build/subdir/second + + cat >expout <Kyuafile <myfile <expout <Kyuafile <myfile <expout <Kyuafile <subdir/Kyuafile <expout <Kyuafile <experr <Kyuafile <experr <experr <subdir/Kyuafile <experr <subdir/Kyuafile <experr <Kyuafile <Kyuafile <non_executable + + cat >expout <Kyuafile <subdir/Kyuafile <subdir/ok + +# CHECK_STYLE_DISABLE + cat >experr <Kyuafile <"${dbfile_name}" + rm stdout + + # Ensure the results of 'report-html' come from the database. + rm Kyuafile simple_all_pass simple_some_fail metadata +} + + +# Ensure a file has a set of strings. +# +# \param file The name of the file to check. +# \param ... List of strings to check. +check_in_file() { + local file="${1}"; shift + + while [ ${#} -gt 0 ]; do + echo "Checking for presence of '${1}' in ${file}" + if grep "${1}" "${file}" >/dev/null; then + : + else + atf_fail "Test case output not found in HTML page ${file}" + fi + shift + done +} + + +# Ensure a file does not have a set of strings. +# +# \param file The name of the file to check. +# \param ... List of strings to check. +check_not_in_file() { + local file="${1}"; shift + + while [ ${#} -gt 0 ]; do + echo "Checking for lack of '${1}' in ${file}" + if grep "${1}" "${file}" >/dev/null; then + atf_fail "Spurious test case output found in HTML page" + fi + shift + done +} + + +utils_test_case default_behavior__ok +default_behavior__ok_body() { + run_tests "mock1" unused_dbfile_name + + atf_check -s exit:0 -o ignore -e empty kyua report-html + for f in \ + html/index.html \ + html/context.html \ + html/simple_all_pass_skip.html \ + html/simple_some_fail_fail.html + do + test -f "${f}" || atf_fail "Missing ${f}" + done + + atf_check -o match:"2 TESTS FAILING" cat html/index.html + + check_in_file html/simple_all_pass_skip.html \ + "This is the stdout of skip" "This is the stderr of skip" + check_not_in_file html/simple_all_pass_skip.html \ + "This is the stdout of pass" "This is the stderr of pass" \ + "This is the stdout of fail" "This is the stderr of fail" \ + "Test case did not write anything to" + + check_in_file html/simple_some_fail_fail.html \ + "This is the stdout of fail" "This is the stderr of fail" + check_not_in_file html/simple_some_fail_fail.html \ + "This is the stdout of pass" "This is the stderr of pass" \ + "This is the stdout of skip" "This is the stderr of skip" \ + "Test case did not write anything to" + + check_in_file html/metadata_one_property.html \ + "description = Does nothing but has one metadata property" + check_not_in_file html/metadata_one_property.html \ + "allowed_architectures = some-architecture" + + check_in_file html/metadata_many_properties.html \ + "allowed_architectures = some-architecture" + check_not_in_file html/metadata_many_properties.html \ + "description = Does nothing but has one metadata property" +} + + +utils_test_case default_behavior__no_store +default_behavior__no_store_body() { + echo 'kyua: E: No previous results file found for test suite' \ + "$(utils_test_suite_id)." >experr + atf_check -s exit:2 -o empty -e file:experr kyua report-html +} + + +utils_test_case results_file__explicit +results_file__explicit_body() { + run_tests "mock1" dbfile_name1 + run_tests "mock2" dbfile_name2 + + atf_check -s exit:0 -o ignore -e empty kyua report-html \ + --results-file="$(cat dbfile_name1)" + grep "MOCK.*mock1" html/context.html || atf_fail "Invalid context in report" + + rm -rf html + atf_check -s exit:0 -o ignore -e empty kyua report-html \ + --results-file="$(cat dbfile_name2)" + grep "MOCK.*mock2" html/context.html || atf_fail "Invalid context in report" +} + + +utils_test_case results_file__not_found +results_file__not_found_body() { + atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ + kyua report-html --results-file=foo +} + + +utils_test_case force__yes +force__yes_body() { + run_tests "mock1" unused_dbfile_name + + atf_check -s exit:0 -o ignore -e empty kyua report-html + test -f html/index.html || atf_fail "Expected file not created" + rm html/index.html + atf_check -s exit:0 -o ignore -e empty kyua report-html --force + test -f html/index.html || atf_fail "Expected file not created" +} + + +utils_test_case force__no +force__no_body() { + run_tests "mock1" unused_dbfile_name + + atf_check -s exit:0 -o ignore -e empty kyua report-html + test -f html/index.html || atf_fail "Expected file not created" + rm html/index.html + +cat >experr <experr + atf_check -s exit:2 -o empty -e file:experr kyua report-html \ + --results-filter=passed,foo-bar +} + + +atf_init_test_cases() { + atf_add_test_case default_behavior__ok + atf_add_test_case default_behavior__no_store + + atf_add_test_case results_file__explicit + atf_add_test_case results_file__not_found + + atf_add_test_case force__yes + atf_add_test_case force__no + + atf_add_test_case output__explicit + + atf_add_test_case results_filter__ok + atf_add_test_case results_filter__invalid +} diff --git a/contrib/kyua/integration/cmd_report_junit_test.sh b/contrib/kyua/integration/cmd_report_junit_test.sh new file mode 100755 index 000000000000..af1a464f6004 --- /dev/null +++ b/contrib/kyua/integration/cmd_report_junit_test.sh @@ -0,0 +1,300 @@ +# 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. + + +# Executes a mock test suite to generate data in the database. +# +# \param mock_env The value to store in a MOCK variable in the environment. +# Use this to be able to differentiate executions by inspecting the +# context of the output. +# \param dbfile_name File to which to write the path to the generated database +# file. +run_tests() { + local mock_env="${1}"; shift + local dbfile_name="${1}"; shift + + cat >Kyuafile <"${dbfile_name}" + rm stdout + + # Ensure the results of 'report-junit' come from the database. + rm Kyuafile simple_all_pass +} + + +# Removes the contents of a properties tag from stdout. +strip_properties='awk " +BEGIN { skip = 0; } + +/<\/properties>/ { + print \"\"; + skip = 0; + next; +} + +// { + print \"\"; + print \"CONTENTS STRIPPED BY TEST\"; + skip = 1; + next; +} + +{ if (!skip) print; }"' + + +utils_test_case default_behavior__ok +default_behavior__ok_body() { + utils_install_times_wrapper + + run_tests "mock1 +this should not be seen +mock1 new line" unused_dbfile_name + + cat >expout < + + +CONTENTS STRIPPED BY TEST + + +This is the stdout of pass + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of pass + + + + +This is the stdout of skip + +Skipped result details +---------------------- + +The reason for skipping is this + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of skip + + + +EOF + atf_check -s exit:0 -o file:expout -e empty -x "kyua report-junit" \ + "| ${strip_properties}" +} + + +utils_test_case default_behavior__no_store +default_behavior__no_store_body() { + echo 'kyua: E: No previous results file found for test suite' \ + "$(utils_test_suite_id)." >experr + atf_check -s exit:2 -o empty -e file:experr kyua report-junit +} + + +utils_test_case results_file__explicit +results_file__explicit_body() { + run_tests "mock1" dbfile_name1 + run_tests "mock2" dbfile_name2 + + atf_check -s exit:0 -o match:"MOCK.*mock1" -o not-match:"MOCK.*mock2" \ + -e empty kyua report-junit --results-file="$(cat dbfile_name1)" + atf_check -s exit:0 -o not-match:"MOCK.*mock1" -o match:"MOCK.*mock2" \ + -e empty kyua report-junit --results-file="$(cat dbfile_name2)" +} + + +utils_test_case results_file__not_found +results_file__not_found_body() { + atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ + kyua report-junit --results-file=foo +} + + +utils_test_case output__explicit +output__explicit_body() { + run_tests unused_mock unused_dbfile_name + + cat >report < + + +CONTENTS STRIPPED BY TEST + + +This is the stdout of pass + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of pass + + + + +This is the stdout of skip + +Skipped result details +---------------------- + +The reason for skipping is this + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of skip + + + +EOF + + atf_check -s exit:0 -o file:report -e empty -x kyua report-junit \ + --output=/dev/stdout "| ${strip_properties} | ${utils_strip_times}" + atf_check -s exit:0 -o empty -e save:stderr kyua report-junit \ + --output=/dev/stderr + atf_check -s exit:0 -o file:report -x cat stderr \ + "| ${strip_properties} | ${utils_strip_times}" + + atf_check -s exit:0 -o empty -e empty kyua report-junit \ + --output=my-file + atf_check -s exit:0 -o file:report -x cat my-file \ + "| ${strip_properties} | ${utils_strip_times}" +} + + +atf_init_test_cases() { + atf_add_test_case default_behavior__ok + atf_add_test_case default_behavior__no_store + + atf_add_test_case results_file__explicit + atf_add_test_case results_file__not_found + + atf_add_test_case output__explicit +} diff --git a/contrib/kyua/integration/cmd_report_test.sh b/contrib/kyua/integration/cmd_report_test.sh new file mode 100755 index 000000000000..18a5db386dfd --- /dev/null +++ b/contrib/kyua/integration/cmd_report_test.sh @@ -0,0 +1,381 @@ +# 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. + + +# Executes a mock test suite to generate data in the database. +# +# \param mock_env The value to store in a MOCK variable in the environment. +# Use this to be able to differentiate executions by inspecting the +# context of the output. +# \param dbfile_name File to which to write the path to the generated database +# file. +run_tests() { + local mock_env="${1}"; shift + local dbfile_name="${1}"; shift + + cat >Kyuafile <"${dbfile_name}" + rm stdout + + # Ensure the results of 'report' come from the database. + rm Kyuafile simple_all_pass +} + + +utils_test_case default_behavior__ok +default_behavior__ok_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report + + run_tests "mock2" dbfile_name2 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name2) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report +} + + +utils_test_case default_behavior__no_store +default_behavior__no_store_body() { + echo 'kyua: E: No previous results file found for test suite' \ + "$(utils_test_suite_id)." >experr + atf_check -s exit:2 -o empty -e file:experr kyua report +} + + +utils_test_case results_file__explicit +results_file__explicit_body() { + run_tests "mock1" dbfile_name1 + run_tests "mock2" dbfile_name2 + + atf_check -s exit:0 -o match:"MOCK=mock1" -o not-match:"MOCK=mock2" \ + -e empty kyua report --results-file="$(cat dbfile_name1)" \ + --verbose + atf_check -s exit:0 -o not-match:"MOCK=mock1" -o match:"MOCK=mock2" \ + -e empty kyua report --results-file="$(cat dbfile_name2)" \ + --verbose +} + + +utils_test_case results_file__not_found +results_file__not_found_body() { + atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ + kyua report --results-file=foo +} + + +utils_test_case output__explicit +output__explicit_body() { + run_tests unused_mock dbfile_name + + cat >report < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + + atf_check -s exit:0 -o file:report -e empty -x kyua report \ + --output=/dev/stdout "| ${utils_strip_times_but_not_ids}" + atf_check -s exit:0 -o empty -e save:stderr kyua report \ + --output=/dev/stderr + atf_check -s exit:0 -o file:report -x cat stderr \ + "| ${utils_strip_times_but_not_ids}" + + atf_check -s exit:0 -o empty -e empty kyua report \ + --output=my-file + atf_check -s exit:0 -o file:report -x cat my-file \ + "| ${utils_strip_times_but_not_ids}" +} + + +utils_test_case filter__ok +filter__ok_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + simple_all_pass:skip +} + + +utils_test_case filter__ok_passed_excluded_by_default +filter__ok_passed_excluded_by_default_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + # Passed results are excluded by default so they are not displayed even if + # requested with a test case filter. This might be somewhat confusing... + cat >expout < Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 0 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + simple_all_pass:pass + cat >expout < Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 0 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter= simple_all_pass:pass +} + + +utils_test_case filter__no_match +filter__no_match_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + cat >experr <expout < Execution context +Current directory: ${real_cwd} +Environment variables: +EOF + # $_ is a bash variable. To keep our tests stable, we override its value + # below to match the hardcoded value in run_tests. + env \ + HOME="${real_cwd}" \ + MOCK="mock1 +has multiple lines +and terminates here" \ + _='fake-value' \ + "$(atf_get_srcdir)/helpers/dump_env" ' ' ' ' >>expout + cat >>expout < simple_all_pass:skip +Result: skipped: The reason for skipping is this +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Metadata: + allowed_architectures is empty + allowed_platforms is empty + description is empty + has_cleanup = false + is_exclusive = false + required_configs is empty + required_disk_space = 0 + required_files is empty + required_memory = 0 + required_programs is empty + required_user is empty + timeout = 300 + +Standard output: +This is the stdout of skip + +Standard error: +This is the stderr of skip +===> Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty -x kyua report --verbose \ + "| ${utils_strip_times_but_not_ids}" +} + + +utils_test_case results_filter__empty +results_filter__empty_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report --results-filter= +} + + +utils_test_case results_filter__one +results_filter__one_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter=passed +} + + +utils_test_case results_filter__multiple_all_match +results_filter__multiple_all_match_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter=skipped,passed +} + + +utils_test_case results_filter__multiple_some_match +results_filter__multiple_some_match_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter=skipped,xfail,broken,failed +} + + +atf_init_test_cases() { + atf_add_test_case default_behavior__ok + atf_add_test_case default_behavior__no_store + + atf_add_test_case results_file__explicit + atf_add_test_case results_file__not_found + + atf_add_test_case filter__ok + atf_add_test_case filter__ok_passed_excluded_by_default + atf_add_test_case filter__no_match + + atf_add_test_case verbose + + atf_add_test_case output__explicit + + atf_add_test_case results_filter__empty + atf_add_test_case results_filter__one + atf_add_test_case results_filter__multiple_all_match + atf_add_test_case results_filter__multiple_some_match +} diff --git a/contrib/kyua/integration/cmd_test_test.sh b/contrib/kyua/integration/cmd_test_test.sh new file mode 100755 index 000000000000..bc8c62daf223 --- /dev/null +++ b/contrib/kyua/integration/cmd_test_test.sh @@ -0,0 +1,1071 @@ +# 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_test_case one_test_program__all_pass +one_test_program__all_pass_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < passed [S.UUUs] +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + + utils_cp_helper simple_all_pass . + atf_check -s exit:0 -o file:expout -e empty kyua test +} + + +utils_test_case one_test_program__some_fail +one_test_program__some_fail_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: This fails on purpose [S.UUUs] +simple_some_fail:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/2 passed (1 failed) +EOF + + utils_cp_helper simple_some_fail . + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case many_test_programs__all_pass +many_test_programs__all_pass_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +fourth:main -> skipped: Required file '/non-existent/foo' not found [S.UUUs] +second:pass -> passed [S.UUUs] +second:skip -> skipped: The reason for skipping is this [S.UUUs] +third:pass -> passed [S.UUUs] +third:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +7/7 passed (0 failed) +EOF + + utils_cp_helper simple_all_pass first + utils_cp_helper simple_all_pass second + utils_cp_helper simple_all_pass third + echo "not executed" >fourth; chmod +x fourth + atf_check -s exit:0 -o file:expout -e empty kyua test +} + + +utils_test_case many_test_programs__some_fail +many_test_programs__some_fail_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: This fails on purpose [S.UUUs] +first:pass -> passed [S.UUUs] +fourth:main -> failed: Returned non-success exit status 76 [S.UUUs] +second:fail -> failed: This fails on purpose [S.UUUs] +second:pass -> passed [S.UUUs] +third:pass -> passed [S.UUUs] +third:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +4/7 passed (3 failed) +EOF + + utils_cp_helper simple_some_fail first + utils_cp_helper simple_some_fail second + utils_cp_helper simple_all_pass third + echo '#! /bin/sh' >fourth + echo 'exit 76' >>fourth + chmod +x fourth + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case expect__all_pass +expect__all_pass_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < expected_failure: This is the reason for death [S.UUUs] +expect_all_pass:exit -> expected_failure: Exiting with correct code [S.UUUs] +expect_all_pass:failure -> expected_failure: Oh no: Forced failure [S.UUUs] +expect_all_pass:signal -> expected_failure: Exiting with correct signal [S.UUUs] +expect_all_pass:timeout -> expected_failure: This times out [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +5/5 passed (0 failed) +EOF +# CHECK_STYLE_ENABLE + + utils_cp_helper expect_all_pass . + atf_check -s exit:0 -o file:expout -e empty kyua test +} + + +utils_test_case expect__some_fail +expect__some_fail_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: Test case was expected to terminate abruptly but it continued execution [S.UUUs] +expect_some_fail:exit -> failed: Test case expected to exit with code 12 but got code 34 [S.UUUs] +expect_some_fail:failure -> failed: Test case was expecting a failure but none were raised [S.UUUs] +expect_some_fail:pass -> passed [S.UUUs] +expect_some_fail:signal -> failed: Test case expected to receive signal 15 but got 9 [S.UUUs] +expect_some_fail:timeout -> failed: Test case was expected to hang but it continued execution [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/6 passed (5 failed) +EOF +# CHECK_STYLE_ENABLE + + utils_cp_helper expect_some_fail . + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case premature_exit +premature_exit_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < broken: Premature exit; test case received signal 9 [S.UUUs] +bogus_test_cases:exit -> broken: Premature exit; test case exited with code 0 [S.UUUs] +bogus_test_cases:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/3 passed (2 failed) +EOF +# CHECK_STYLE_ENABLE + + utils_cp_helper bogus_test_cases . + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case no_args +no_args_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/simple_some_fail:fail -> failed: This fails on purpose [S.UUUs] +subdir/simple_some_fail:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +3/4 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case one_arg__subdir +one_arg__subdir_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +subdir/simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF +# CHECK_STYLE_ENABLE + atf_check -s exit:0 -o file:expout -e empty kyua test subdir +} + + +utils_test_case one_arg__test_case +one_arg__test_case_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/1 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test first:skip +} + + +utils_test_case one_arg__test_program +one_arg__test_program_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: This fails on purpose [S.UUUs] +second:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/2 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test second +} + + +utils_test_case one_arg__invalid +one_arg__invalid_body() { +cat >experr <experr <Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +subdir/second:fail -> failed: This fails on purpose [S.UUUs] +subdir/second:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/3 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test subdir first:pass +} + + +utils_test_case many_args__invalid +many_args__invalid_body() { +cat >experr <experr <Kyuafile <expout <experr <Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +third:fail -> failed: This fails on purpose [S.UUUs] +third:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +3/4 passed (1 failed) +EOF + + cat >experr <root/Kyuafile <root/subdir/Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/fourth:fail -> failed: This fails on purpose [S.UUUs] + +Results file id is $(utils_results_id root) +Results saved to $(utils_results_file root) + +2/3 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test \ + -k "$(pwd)/root/Kyuafile" first subdir/fourth:fail +} + + +utils_test_case only_load_used_test_programs +only_load_used_test_programs_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + CREATE_COOKIE="$(pwd)/cookie"; export CREATE_COOKIE + atf_check -s exit:0 -o file:expout -e empty kyua test first + if [ -f "${CREATE_COOKIE}" ]; then + atf_fail "An unmatched test case has been executed, which harms" \ + "performance" + fi +} + + +utils_test_case config_behavior +config_behavior_body() { + cat >"my-config" <Kyuafile <Kyuafile <expout < failed: This fails on purpose [S.UUUs] +some-program:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/2 passed (1 failed) +EOF + + atf_check -s exit:1 -o file:expout -e empty kyua test + +cat >expout <Kyuafile <Kyuafile <Kyuafile <Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/second:pass -> passed [S.UUUs] +subdir/second:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/third:pass -> passed [S.UUUs] +subdir/third:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +6/6 passed (0 failed) +EOF + + mkdir build + mkdir build/subdir + utils_cp_helper simple_all_pass build/first + utils_cp_helper simple_all_pass build/subdir/second + utils_cp_helper simple_all_pass build/subdir/third + + atf_check -s exit:0 -o file:expout -e empty kyua test --build-root=build +} + + +utils_test_case kyuafile_flag__no_args +kyuafile_flag__no_args_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <myfile <expout < passed [S.UUUs] +sometest:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test -k myfile + atf_check -s exit:0 -o file:expout -e empty kyua test --kyuafile=myfile +} + + +utils_test_case kyuafile_flag__some_args +kyuafile_flag__some_args_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <myfile <expout < passed [S.UUUs] +sometest:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test -k myfile sometest + cat >expout < passed [S.UUUs] +sometest:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test --kyuafile=myfile \ + sometest +} + + +utils_test_case interrupt +interrupt_body() { + cat >Kyuafile <stdout 2>stderr & + pid=${!} + echo "Kyua subprocess is PID ${pid}" + + while [ ! -f body ]; do + echo "Waiting for body to start" + sleep 1 + done + echo "Body started" + sleep 1 + + echo "Sending INT signal to ${pid}" + kill -INT ${pid} + echo "Waiting for process ${pid} to exit" + wait ${pid} + ret=${?} + sed -e 's,^,kyua stdout:,' stdout + sed -e 's,^,kyua stderr:,' stderr + echo "Process ${pid} exited" + [ ${ret} -ne 0 ] || atf_fail 'No error code reported' + + [ -f cleanup ] || atf_fail 'Cleanup part not executed after signal' + atf_expect_pass + + atf_check -s exit:0 -o ignore -e empty grep 'Signal caught' stderr + atf_check -s exit:0 -o ignore -e empty \ + grep 'kyua: E: Interrupted by signal' stderr +} + + +utils_test_case exclusive_tests +exclusive_tests_body() { + cat >Kyuafile <>Kyuafile + done + utils_cp_helper race . + + atf_check \ + -s exit:0 \ + -o match:"100/100 passed" \ + kyua \ + -v parallelism=20 \ + -v test_suites.integration.shared_file="$(pwd)/shared_file" \ + test +} + + +utils_test_case no_test_program_match +no_test_program_match_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout <experr <Kyuafile <expout <experr <experr <subdir/Kyuafile <experr <subdir/Kyuafile <experr <"${HOME}/.kyua/kyua.conf" <Kyuafile <Kyuafile <non_executable + +# CHECK_STYLE_DISABLE + cat >expout < broken: Invalid header for test case list; expecting Content-Type for application/X-atf-tp version 1, got '' [S.UUUs] +non_executable:__test_cases_list__ -> broken: Permission denied to run test program [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +0/2 passed (2 failed) +EOF +# CHECK_STYLE_ENABLE + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case missing_test_program +missing_test_program_body() { + cat >Kyuafile <subdir/Kyuafile <subdir/ok + +# CHECK_STYLE_DISABLE + cat >experr <experr <experr <experr < +#include +#include +#include + + +int +main(void) +{ + std::cerr << "This is not a valid test program!\n"; + + const char* cookie = std::getenv("CREATE_COOKIE"); + if (cookie != NULL && std::strlen(cookie) > 0) { + std::ofstream file(cookie); + if (!file) + std::abort(); + file << "Cookie file\n"; + file.close(); + } + + return EXIT_SUCCESS; +} diff --git a/contrib/kyua/integration/helpers/bogus_test_cases.cpp b/contrib/kyua/integration/helpers/bogus_test_cases.cpp new file mode 100644 index 000000000000..1a7c27031e1b --- /dev/null +++ b/contrib/kyua/integration/helpers/bogus_test_cases.cpp @@ -0,0 +1,64 @@ +// 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. + +extern "C" { +#include +#include +} + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(die); +ATF_TEST_CASE_BODY(die) +{ + ::kill(::getpid(), SIGKILL); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exit); +ATF_TEST_CASE_BODY(exit) +{ + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, die); + ATF_ADD_TEST_CASE(tcs, exit); + ATF_ADD_TEST_CASE(tcs, pass); +} diff --git a/contrib/kyua/integration/helpers/config.cpp b/contrib/kyua/integration/helpers/config.cpp new file mode 100644 index 000000000000..aa4fef291725 --- /dev/null +++ b/contrib/kyua/integration/helpers/config.cpp @@ -0,0 +1,58 @@ +// 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 + +#include + + +ATF_TEST_CASE(get_variable); +ATF_TEST_CASE_HEAD(get_variable) +{ + const char* output = ::getenv("CONFIG_VAR_FILE"); + if (output == NULL) { + set_md_var("require.config", "the-variable"); + } else { + if (has_config_var("the-variable")) { + atf::utils::create_file(output, get_config_var("the-variable") + + std::string("\n")); + } else { + atf::utils::create_file(output, "NOT DEFINED\n"); + } + } +} +ATF_TEST_CASE_BODY(get_variable) +{ + ATF_REQUIRE_EQ("value2", get_config_var("the-variable")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, get_variable); +} diff --git a/contrib/kyua/integration/helpers/dump_env.cpp b/contrib/kyua/integration/helpers/dump_env.cpp new file mode 100644 index 000000000000..a2e8313a0062 --- /dev/null +++ b/contrib/kyua/integration/helpers/dump_env.cpp @@ -0,0 +1,74 @@ +// 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. + +// Dumps all environment variables. +// +// This helper program allows comparing the printed environment variables +// to what 'kyua report --verbose' may output. It does so by sorting the +// variables and allowing the caller to customize how the output looks +// like (indentation for each line and for continuation lines). + +#include +#include + +#include "utils/env.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +int +main(const int argc, const char* const* const argv) +{ + if (argc != 3) { + std::cerr << "Usage: dump_env \n"; + return EXIT_FAILURE; + } + const char* prefix = argv[1]; + const char* continuation_prefix = argv[2]; + + const std::map< std::string, std::string > env = utils::getallenv(); + for (std::map< std::string, std::string >::const_iterator + iter = env.begin(); iter != env.end(); ++iter) { + const std::string& name = (*iter).first; + const std::vector< std::string > value = text::split( + (*iter).second, '\n'); + + if (value.empty()) { + std::cout << prefix << name << "=\n"; + } else { + std::cout << prefix << name << '=' << value[0] << '\n'; + for (std::vector< std::string >::size_type i = 1; + i < value.size(); ++i) { + std::cout << continuation_prefix << value[i] << '\n'; + } + } + } + + return EXIT_SUCCESS; +} diff --git a/contrib/kyua/integration/helpers/expect_all_pass.cpp b/contrib/kyua/integration/helpers/expect_all_pass.cpp new file mode 100644 index 000000000000..a7df16e3a783 --- /dev/null +++ b/contrib/kyua/integration/helpers/expect_all_pass.cpp @@ -0,0 +1,92 @@ +// 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. + +extern "C" { +#include +#include +} + +#include + +#include + +#include "utils/test_utils.ipp" + + +ATF_TEST_CASE_WITHOUT_HEAD(die); +ATF_TEST_CASE_BODY(die) +{ + expect_death("This is the reason for death"); + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exit); +ATF_TEST_CASE_BODY(exit) +{ + expect_exit(12, "Exiting with correct code"); + std::exit(12); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(failure); +ATF_TEST_CASE_BODY(failure) +{ + expect_fail("Oh no"); + fail("Forced failure"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(signal); +ATF_TEST_CASE_BODY(signal) +{ + expect_signal(SIGTERM, "Exiting with correct signal"); + ::kill(::getpid(), SIGTERM); +} + + +ATF_TEST_CASE(timeout); +ATF_TEST_CASE_HEAD(timeout) +{ + set_md_var("timeout", "1"); +} +ATF_TEST_CASE_BODY(timeout) +{ + expect_timeout("This times out"); + ::sleep(10); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, die); + ATF_ADD_TEST_CASE(tcs, exit); + ATF_ADD_TEST_CASE(tcs, failure); + ATF_ADD_TEST_CASE(tcs, signal); + ATF_ADD_TEST_CASE(tcs, timeout); +} diff --git a/contrib/kyua/integration/helpers/expect_some_fail.cpp b/contrib/kyua/integration/helpers/expect_some_fail.cpp new file mode 100644 index 000000000000..da1a9f0ceb39 --- /dev/null +++ b/contrib/kyua/integration/helpers/expect_some_fail.cpp @@ -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. + +extern "C" { +#include +#include +} + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(die); +ATF_TEST_CASE_BODY(die) +{ + expect_death("Won't die"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exit); +ATF_TEST_CASE_BODY(exit) +{ + expect_exit(12, "Invalid exit code"); + std::exit(34); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(failure); +ATF_TEST_CASE_BODY(failure) +{ + expect_fail("Does not fail"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_TEST_CASE_WITHOUT_HEAD(signal); +ATF_TEST_CASE_BODY(signal) +{ + expect_signal(SIGTERM, "Invalid signal"); + ::kill(::getpid(), SIGKILL); +} + + +ATF_TEST_CASE(timeout); +ATF_TEST_CASE_HEAD(timeout) +{ + set_md_var("timeout", "1"); +} +ATF_TEST_CASE_BODY(timeout) +{ + expect_timeout("Does not time out"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, die); + ATF_ADD_TEST_CASE(tcs, exit); + ATF_ADD_TEST_CASE(tcs, failure); + ATF_ADD_TEST_CASE(tcs, pass); + ATF_ADD_TEST_CASE(tcs, signal); + ATF_ADD_TEST_CASE(tcs, timeout); +} diff --git a/contrib/kyua/integration/helpers/interrupts.cpp b/contrib/kyua/integration/helpers/interrupts.cpp new file mode 100644 index 000000000000..b6c5a948098c --- /dev/null +++ b/contrib/kyua/integration/helpers/interrupts.cpp @@ -0,0 +1,62 @@ +// 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. + +extern "C" { +#include +} + +#include + +#include + + +ATF_TEST_CASE_WITH_CLEANUP(block_body); +ATF_TEST_CASE_HEAD(block_body) +{ + set_md_var("require.config", "body-cookie cleanup-cookie"); +} +ATF_TEST_CASE_BODY(block_body) +{ + const std::string cookie(get_config_var("body-cookie")); + std::ofstream output(cookie.c_str()); + output.close(); + for (;;) + ::pause(); +} +ATF_TEST_CASE_CLEANUP(block_body) +{ + const std::string cookie(get_config_var("cleanup-cookie")); + std::ofstream output(cookie.c_str()); + output.close(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, block_body); +} diff --git a/contrib/kyua/integration/helpers/metadata.cpp b/contrib/kyua/integration/helpers/metadata.cpp new file mode 100644 index 000000000000..8005d7d9b68d --- /dev/null +++ b/contrib/kyua/integration/helpers/metadata.cpp @@ -0,0 +1,95 @@ +// 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 +#include + +#include + +#include "utils/test_utils.ipp" + + +ATF_TEST_CASE_WITHOUT_HEAD(no_properties); +ATF_TEST_CASE_BODY(no_properties) +{ +} + + +ATF_TEST_CASE(one_property); +ATF_TEST_CASE_HEAD(one_property) +{ + set_md_var("descr", "Does nothing but has one metadata property"); +} +ATF_TEST_CASE_BODY(one_property) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(many_properties); +ATF_TEST_CASE_HEAD(many_properties) +{ + set_md_var("descr", " A description with some padding"); + set_md_var("require.arch", "some-architecture"); + set_md_var("require.config", "var1 var2 var3"); + set_md_var("require.files", "/my/file1 /some/other/file"); + set_md_var("require.machine", "some-platform"); + set_md_var("require.progs", "bin1 bin2 /nonexistent/bin3"); + set_md_var("require.user", "root"); + set_md_var("X-no-meaning", "I am a custom variable"); +} +ATF_TEST_CASE_BODY(many_properties) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITH_CLEANUP(with_cleanup); +ATF_TEST_CASE_HEAD(with_cleanup) +{ + set_md_var("timeout", "250"); +} +ATF_TEST_CASE_BODY(with_cleanup) +{ + std::cout << "Body message to stdout\n"; + std::cerr << "Body message to stderr\n"; +} +ATF_TEST_CASE_CLEANUP(with_cleanup) +{ + std::cout << "Cleanup message to stdout\n"; + std::cerr << "Cleanup message to stderr\n"; +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, no_properties); + ATF_ADD_TEST_CASE(tcs, one_property); + ATF_ADD_TEST_CASE(tcs, many_properties); + ATF_ADD_TEST_CASE(tcs, with_cleanup); +} diff --git a/contrib/kyua/integration/helpers/race.cpp b/contrib/kyua/integration/helpers/race.cpp new file mode 100644 index 000000000000..39d4b04f3923 --- /dev/null +++ b/contrib/kyua/integration/helpers/race.cpp @@ -0,0 +1,99 @@ +// 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 integration/helpers/race.cpp +/// Creates a file and reads it back, looking for races. +/// +/// This program should fail with high chances if it is called multiple times at +/// once with TEST_ENV_shared_file pointing to the same file. + +extern "C" { +#include + +#include +} + +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/env.hpp" +#include "utils/optional.ipp" +#include "utils/stream.hpp" + +namespace fs = utils::fs; + +using utils::optional; + + +/// Entry point to the helper test program. +/// +/// \return EXIT_SUCCESS if no race is detected; EXIT_FAILURE otherwise. +int +main(void) +{ + const optional< std::string > shared_file = utils::getenv( + "TEST_ENV_shared_file"); + if (!shared_file) { + std::cerr << "Environment variable TEST_ENV_shared_file not defined\n"; + std::exit(EXIT_FAILURE); + } + const fs::path shared_path(shared_file.get()); + + if (fs::exists(shared_path)) { + std::cerr << "Shared file already exists; created by a concurrent " + "test?"; + std::exit(EXIT_FAILURE); + } + + const std::string contents = F("%s") % ::getpid(); + + std::ofstream output(shared_path.c_str()); + if (!output) { + std::cerr << "Failed to create shared file; conflict with a concurrent " + "test?"; + std::exit(EXIT_FAILURE); + } + output << contents; + output.close(); + + ::usleep(10000); + + const std::string read_contents = utils::read_file(shared_path); + if (read_contents != contents) { + std::cerr << "Shared file contains unexpected contents; modified by a " + "concurrent test?"; + std::exit(EXIT_FAILURE); + } + + fs::unlink(shared_path); + std::exit(EXIT_SUCCESS); +} diff --git a/contrib/kyua/integration/helpers/simple_all_pass.cpp b/contrib/kyua/integration/helpers/simple_all_pass.cpp new file mode 100644 index 000000000000..4e168b4cca5f --- /dev/null +++ b/contrib/kyua/integration/helpers/simple_all_pass.cpp @@ -0,0 +1,55 @@ +// 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 + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ + std::cout << "This is the stdout of pass\n"; + std::cerr << "This is the stderr of pass\n"; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(skip); +ATF_TEST_CASE_BODY(skip) +{ + std::cout << "This is the stdout of skip\n"; + std::cerr << "This is the stderr of skip\n"; + skip("The reason for skipping is this"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, pass); + ATF_ADD_TEST_CASE(tcs, skip); +} diff --git a/contrib/kyua/integration/helpers/simple_some_fail.cpp b/contrib/kyua/integration/helpers/simple_some_fail.cpp new file mode 100644 index 000000000000..909ffb6e2ee1 --- /dev/null +++ b/contrib/kyua/integration/helpers/simple_some_fail.cpp @@ -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. + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(fail); +ATF_TEST_CASE_BODY(fail) +{ + std::cout << "This is the stdout of fail\n"; + std::cerr << "This is the stderr of fail\n"; + fail("This fails on purpose"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, fail); + ATF_ADD_TEST_CASE(tcs, pass); +} diff --git a/contrib/kyua/integration/utils.sh b/contrib/kyua/integration/utils.sh new file mode 100755 index 000000000000..99565a1c9857 --- /dev/null +++ b/contrib/kyua/integration/utils.sh @@ -0,0 +1,177 @@ +# 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. + + +# Subcommand to strip out the durations and timestamps in a report. +# +# This is to make the reports deterministic and thus easily testable. The +# time deltas are replaced by the fixed string S.UUU and the timestamps are +# replaced by the fixed strings YYYYMMDD.HHMMSS.ssssss and +# YYYY-MM-DDTHH:MM:SS.ssssssZ depending on their original format. +# +# This variable should be used as shown here: +# +# atf_check ... -x kyua report "| ${utils_strip_times}" +# +# Use the utils_install_times_wrapper function to create a 'kyua' wrapper +# script that automatically does this. +# CHECK_STYLE_DISABLE +utils_strip_times='sed -E \ + -e "s,( |\[|\")[0-9][0-9]*.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ + -e "s,[0-9]{8}-[0-9]{6}-[0-9]{6},YYYYMMDD-HHMMSS-ssssss,g" \ + -e "s,[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z,YYYY-MM-DDTHH:MM:SS.ssssssZ,g"' +# CHECK_STYLE_ENABLE + + +# Same as utils_strip_times but avoids stripping timestamp-based report IDs. +# +# This is to make the reports deterministic and thus easily testable. The +# time deltas are replaced by the fixed string S.UUU and the timestamps are +# replaced by the fixed string YYYY-MM-DDTHH:MM:SS.ssssssZ. +# CHECK_STYLE_DISABLE +utils_strip_times_but_not_ids='sed -E \ + -e "s,( |\[|\")[0-9][0-9]*.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ + -e "s,[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z,YYYY-MM-DDTHH:MM:SS.ssssssZ,g"' +# CHECK_STYLE_ENABLE + + +# Computes the results id for a test suite run. +# +# The computed path is "generic" in the sense that it does not include a +# real timestamp: it only includes a placeholder. This function should be +# used along the utils_strip_times function so that the timestamps of +# the real results files are stripped out. +# +# \param path Optional path to use; if not given, use the cwd. +utils_results_id() { + local test_suite_id="$(utils_test_suite_id "${@}")" + echo "${test_suite_id}.YYYYMMDD-HHMMSS-ssssss" +} + + +# Computes the results file for a test suite run. +# +# The computed path is "generic" in the sense that it does not include a +# real timestamp: it only includes a placeholder. This function should be +# used along the utils_strip_times function so that the timestampts of the +# real results files are stripped out. +# +# \param path Optional path to use; if not given, use the cwd. +utils_results_file() { + echo "${HOME}/.kyua/store/results.$(utils_results_id "${@}").db" +} + + +# Copies a helper binary from the source directory to the work directory. +# +# \param name The name of the binary to copy. +# \param destination The target location for the binary; can be either +# a directory name or a file name. +utils_cp_helper() { + local name="${1}"; shift + local destination="${1}"; shift + + ln -s "$(atf_get_srcdir)"/helpers/"${name}" "${destination}" +} + + +# Creates a 'kyua' binary in the path that strips timing data off the output. +# +# Call this on test cases that wish to replace timing data in the *stdout* of +# Kyua with the deterministic strings. This is to be used by tests that +# validate the 'test' and 'report' subcommands. +utils_install_times_wrapper() { + [ ! -x kyua ] || return + cat >kyua <kyua.tmpout +result=\${?} +cat kyua.tmpout | ${utils_strip_times} +exit \${result} +EOF + chmod +x kyua + PATH="$(pwd):${PATH}" +} + + +# Creates a 'kyua' binary in the path that makes the output of 'test' stable. +# +# Call this on test cases that wish to replace timing data with deterministic +# strings and that need the result lines in the output to be sorted +# lexicographically. The latter hides the indeterminism caused by parallel +# execution so that the output can be verified. For these reasons, this is to +# be used exclusively by tests that validate the 'test' subcommand. +utils_install_stable_test_wrapper() { + [ ! -x kyua ] || return + cat >kyua <kyua.tmpout +result=\${?} +cat kyua.tmpout | ${utils_strip_times} >kyua.tmpout2 + +# Sort the test result lines but keep the rest intact. +grep '[^ ]*:[^ ]*' kyua.tmpout2 | sort >kyua.tmpout3 +grep -v '[^ ]*:[^ ]*' kyua.tmpout2 >kyua.tmpout4 +cat kyua.tmpout3 kyua.tmpout4 + +exit \${result} +EOF + chmod +x kyua + PATH="$(pwd):${PATH}" +} + + +# Defines a test case with a default head. +utils_test_case() { + local name="${1}"; shift + + atf_test_case "${name}" + eval "${name}_head() { + atf_set require.progs kyua + }" +} + + +# Computes the test suite identifier for results files files. +# +# \param path Optional path to use; if not given, use the cwd. +utils_test_suite_id() { + local path= + if [ ${#} -gt 0 ]; then + path="$(cd ${1} && pwd)"; shift + else + path="$(pwd)" + fi + echo "${path}" | sed -e 's,^/,,' -e 's,/,_,g' +} diff --git a/contrib/kyua/m4/ax_cxx_compile_stdcxx.m4 b/contrib/kyua/m4/ax_cxx_compile_stdcxx.m4 new file mode 100644 index 000000000000..43087b2e6889 --- /dev/null +++ b/contrib/kyua/m4/ax_cxx_compile_stdcxx.m4 @@ -0,0 +1,951 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional]) +# +# DESCRIPTION +# +# Check for baseline language coverage in the compiler for the specified +# version of the C++ standard. If necessary, add switches to CXX and +# CXXCPP to enable support. VERSION may be '11' (for the C++11 standard) +# or '14' (for the C++14 standard). +# +# The second argument, if specified, indicates whether you insist on an +# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g. +# -std=c++11). If neither is specified, you get whatever works, with +# preference for an extended mode. +# +# The third argument, if specified 'mandatory' or if left unspecified, +# indicates that baseline support for the specified C++ standard is +# required and that the macro should error out if no mode with that +# support is found. If specified 'optional', then configuration proceeds +# regardless, after defining HAVE_CXX${VERSION} if and only if a +# supporting mode is found. +# +# LICENSE +# +# Copyright (c) 2008 Benjamin Kosnik +# Copyright (c) 2012 Zack Weinberg +# Copyright (c) 2013 Roy Stogner +# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov +# Copyright (c) 2015 Paul Norman +# Copyright (c) 2015 Moritz Klammler +# Copyright (c) 2016, 2018 Krzesimir Nowak +# Copyright (c) 2019 Enji Cooper +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 11 + +dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro +dnl (serial version number 13). + +AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl + m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"], + [$1], [14], [ax_cxx_compile_alternatives="14 1y"], + [$1], [17], [ax_cxx_compile_alternatives="17 1z"], + [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$2], [], [], + [$2], [ext], [], + [$2], [noext], [], + [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true], + [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true], + [$3], [optional], [ax_cxx_compile_cxx$1_required=false], + [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])]) + AC_LANG_PUSH([C++])dnl + ac_success=no + + m4_if([$2], [noext], [], [dnl + if test x$ac_success = xno; then + for alternative in ${ax_cxx_compile_alternatives}; do + switch="-std=gnu++${alternative}" + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + fi]) + + m4_if([$2], [ext], [], [dnl + if test x$ac_success = xno; then + dnl HP's aCC needs +std=c++11 according to: + dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf + dnl Cray's crayCC needs "-h std=c++11" + for alternative in ${ax_cxx_compile_alternatives}; do + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + if test x$ac_success = xyes; then + break + fi + done + fi]) + AC_LANG_POP([C++]) + if test x$ax_cxx_compile_cxx$1_required = xtrue; then + if test x$ac_success = xno; then + AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.]) + fi + fi + if test x$ac_success = xno; then + HAVE_CXX$1=0 + AC_MSG_NOTICE([No compiler with C++$1 support was found]) + else + HAVE_CXX$1=1 + AC_DEFINE(HAVE_CXX$1,1, + [define if the compiler supports basic C++$1 syntax]) + fi + AC_SUBST(HAVE_CXX$1) +]) + + +dnl Test body for checking C++11 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 +) + + +dnl Test body for checking C++14 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 +) + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 +) + +dnl Tests for new features in C++11 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[ + +// If the compiler admits that it is not ready for C++11, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201103L + +#error "This is not a C++11 compiler" + +#else + +namespace cxx11 +{ + + namespace test_static_assert + { + + template + struct check + { + static_assert(sizeof(int) <= sizeof(T), "not big enough"); + }; + + } + + namespace test_final_override + { + + struct Base + { + virtual ~Base() {} + virtual void f() {} + }; + + struct Derived : public Base + { + virtual ~Derived() override {} + virtual void f() override {} + }; + + } + + namespace test_double_right_angle_brackets + { + + template < typename T > + struct check {}; + + typedef check single_type; + typedef check> double_type; + typedef check>> triple_type; + typedef check>>> quadruple_type; + + } + + namespace test_decltype + { + + int + f() + { + int a = 1; + decltype(a) b = 2; + return a + b; + } + + } + + namespace test_type_deduction + { + + template < typename T1, typename T2 > + struct is_same + { + static const bool value = false; + }; + + template < typename T > + struct is_same + { + static const bool value = true; + }; + + template < typename T1, typename T2 > + auto + add(T1 a1, T2 a2) -> decltype(a1 + a2) + { + return a1 + a2; + } + + int + test(const int c, volatile int v) + { + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == false, ""); + auto ac = c; + auto av = v; + auto sumi = ac + av + 'x'; + auto sumf = ac + av + 1.0; + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == true, ""); + return (sumf > 0.0) ? sumi : add(c, v); + } + + } + + namespace test_noexcept + { + + int f() { return 0; } + int g() noexcept { return 0; } + + static_assert(noexcept(f()) == false, ""); + static_assert(noexcept(g()) == true, ""); + + } + + namespace test_constexpr + { + + template < typename CharT > + unsigned long constexpr + strlen_c_r(const CharT *const s, const unsigned long acc) noexcept + { + return *s ? strlen_c_r(s + 1, acc + 1) : acc; + } + + template < typename CharT > + unsigned long constexpr + strlen_c(const CharT *const s) noexcept + { + return strlen_c_r(s, 0UL); + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("1") == 1UL, ""); + static_assert(strlen_c("example") == 7UL, ""); + static_assert(strlen_c("another\0example") == 7UL, ""); + + } + + namespace test_rvalue_references + { + + template < int N > + struct answer + { + static constexpr int value = N; + }; + + answer<1> f(int&) { return answer<1>(); } + answer<2> f(const int&) { return answer<2>(); } + answer<3> f(int&&) { return answer<3>(); } + + void + test() + { + int i = 0; + const int c = 0; + static_assert(decltype(f(i))::value == 1, ""); + static_assert(decltype(f(c))::value == 2, ""); + static_assert(decltype(f(0))::value == 3, ""); + } + + } + + namespace test_uniform_initialization + { + + struct test + { + static const int zero {}; + static const int one {1}; + }; + + static_assert(test::zero == 0, ""); + static_assert(test::one == 1, ""); + + } + + namespace test_lambdas + { + + void + test1() + { + auto lambda1 = [](){}; + auto lambda2 = lambda1; + lambda1(); + lambda2(); + } + + int + test2() + { + auto a = [](int i, int j){ return i + j; }(1, 2); + auto b = []() -> int { return '0'; }(); + auto c = [=](){ return a + b; }(); + auto d = [&](){ return c; }(); + auto e = [a, &b](int x) mutable { + const auto identity = [](int y){ return y; }; + for (auto i = 0; i < a; ++i) + a += b--; + return x + identity(a + b); + }(0); + return a + b + c + d + e; + } + + int + test3() + { + const auto nullary = [](){ return 0; }; + const auto unary = [](int x){ return x; }; + using nullary_t = decltype(nullary); + using unary_t = decltype(unary); + const auto higher1st = [](nullary_t f){ return f(); }; + const auto higher2nd = [unary](nullary_t f1){ + return [unary, f1](unary_t f2){ return f2(unary(f1())); }; + }; + return higher1st(nullary) + higher2nd(nullary)(unary); + } + + } + + namespace test_variadic_templates + { + + template + struct sum; + + template + struct sum + { + static constexpr auto value = N0 + sum::value; + }; + + template <> + struct sum<> + { + static constexpr auto value = 0; + }; + + static_assert(sum<>::value == 0, ""); + static_assert(sum<1>::value == 1, ""); + static_assert(sum<23>::value == 23, ""); + static_assert(sum<1, 2>::value == 3, ""); + static_assert(sum<5, 5, 11>::value == 21, ""); + static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, ""); + + } + + // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae + // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function + // because of this. + namespace test_template_alias_sfinae + { + + struct foo {}; + + template + using member = typename T::member_type; + + template + void func(...) {} + + template + void func(member*) {} + + void test(); + + void test() { func(0); } + + } + +} // namespace cxx11 + +#endif // __cplusplus >= 201103L + +]]) + + +dnl Tests for new features in C++14 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[ + +// If the compiler admits that it is not ready for C++14, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201402L + +#error "This is not a C++14 compiler" + +#else + +namespace cxx14 +{ + + namespace test_polymorphic_lambdas + { + + int + test() + { + const auto lambda = [](auto&&... args){ + const auto istiny = [](auto x){ + return (sizeof(x) == 1UL) ? 1 : 0; + }; + const int aretiny[] = { istiny(args)... }; + return aretiny[0]; + }; + return lambda(1, 1L, 1.0f, '1'); + } + + } + + namespace test_binary_literals + { + + constexpr auto ivii = 0b0000000000101010; + static_assert(ivii == 42, "wrong value"); + + } + + namespace test_generalized_constexpr + { + + template < typename CharT > + constexpr unsigned long + strlen_c(const CharT *const s) noexcept + { + auto length = 0UL; + for (auto p = s; *p; ++p) + ++length; + return length; + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("x") == 1UL, ""); + static_assert(strlen_c("test") == 4UL, ""); + static_assert(strlen_c("another\0test") == 7UL, ""); + + } + + namespace test_lambda_init_capture + { + + int + test() + { + auto x = 0; + const auto lambda1 = [a = x](int b){ return a + b; }; + const auto lambda2 = [a = lambda1(x)](){ return a; }; + return lambda2(); + } + + } + + namespace test_digit_separators + { + + constexpr auto ten_million = 100'000'000; + static_assert(ten_million == 100000000, ""); + + } + + namespace test_return_type_deduction + { + + auto f(int& x) { return x; } + decltype(auto) g(int& x) { return x; } + + template < typename T1, typename T2 > + struct is_same + { + static constexpr auto value = false; + }; + + template < typename T > + struct is_same + { + static constexpr auto value = true; + }; + + int + test() + { + auto x = 0; + static_assert(is_same::value, ""); + static_assert(is_same::value, ""); + return x; + } + + } + +} // namespace cxx14 + +#endif // __cplusplus >= 201402L + +]]) + + +dnl Tests for new features in C++17 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[ + +// If the compiler admits that it is not ready for C++17, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201703L + +#error "This is not a C++17 compiler" + +#else + +#include +#include +#include + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template + int multiply(Args... args) + { + return (args * ... * 1); + } + + template + bool all(Args... args) + { + return (args && ...); + } + + } + + namespace test_extended_static_assert + { + + static_assert (true); + + } + + namespace test_auto_brace_init_list + { + + auto foo = {5}; + auto bar {5}; + + static_assert(std::is_same, decltype(foo)>::value); + static_assert(std::is_same::value); + } + + namespace test_typename_in_template_template_parameter + { + + template typename X> struct D; + + } + + namespace test_fallthrough_nodiscard_maybe_unused_attributes + { + + int f1() + { + return 42; + } + + [[nodiscard]] int f2() + { + [[maybe_unused]] auto unused = f1(); + + switch (f1()) + { + case 17: + f1(); + [[fallthrough]]; + case 42: + f1(); + } + return f1(); + } + + } + + namespace test_extended_aggregate_initialization + { + + struct base1 + { + int b1, b2 = 42; + }; + + struct base2 + { + base2() { + b3 = 42; + } + int b3; + }; + + struct derived : base1, base2 + { + int d; + }; + + derived d1 {{1, 2}, {}, 4}; // full initialization + derived d2 {{}, {}, 4}; // value-initialized bases + + } + + namespace test_general_range_based_for_loop + { + + struct iter + { + int i; + + int& operator* () + { + return i; + } + + const int& operator* () const + { + return i; + } + + iter& operator++() + { + ++i; + return *this; + } + }; + + struct sentinel + { + int i; + }; + + bool operator== (const iter& i, const sentinel& s) + { + return i.i == s.i; + } + + bool operator!= (const iter& i, const sentinel& s) + { + return !(i == s); + } + + struct range + { + iter begin() const + { + return {0}; + } + + sentinel end() const + { + return {5}; + } + }; + + void f() + { + range r {}; + + for (auto i : r) + { + [[maybe_unused]] auto v = i; + } + } + + } + + namespace test_lambda_capture_asterisk_this_by_value + { + + struct t + { + int i; + int foo() + { + return [*this]() + { + return i; + }(); + } + }; + + } + + namespace test_enum_class_construction + { + + enum class byte : unsigned char + {}; + + byte foo {42}; + + } + + namespace test_constexpr_if + { + + template + int f () + { + if constexpr(cond) + { + return 13; + } + else + { + return 42; + } + } + + } + + namespace test_selection_statement_with_initializer + { + + int f() + { + return 13; + } + + int f2() + { + if (auto i = f(); i > 0) + { + return 3; + } + + switch (auto i = f(); i + 4) + { + case 17: + return 2; + + default: + return 1; + } + } + + } + + namespace test_template_argument_deduction_for_class_templates + { + + template + struct pair + { + pair (T1 p1, T2 p2) + : m1 {p1}, + m2 {p2} + {} + + T1 m1; + T2 m2; + }; + + void f() + { + [[maybe_unused]] auto p = pair{13, 42u}; + } + + } + + namespace test_non_type_auto_template_parameters + { + + template + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair& + { + return pr; + } + + struct S + { + int x1 : 2; + volatile double y1; + }; + + S f3() + { + return {}; + } + + auto [ x1, y1 ] = f1(); + auto& [ xr1, yr1 ] = f1(); + auto [ x2, y2 ] = f2(); + auto& [ xr2, yr2 ] = f2(); + const auto [ x3, y3 ] = f3(); + + } + + namespace test_exception_spec_type_system + { + + struct Good {}; + struct Bad {}; + + void g1() noexcept; + void g2(); + + template + Bad + f(T*, T*); + + template + Good + f(T1*, T2*); + + static_assert (std::is_same_v); + + } + + namespace test_inline_variables + { + + template void f(T) + {} + + template inline T g(T) + { + return T{}; + } + + template<> inline void f<>(int) + {} + + template<> int g<>(int) + { + return 5; + } + + } + +} // namespace cxx17 + +#endif // __cplusplus < 201703L + +]]) diff --git a/contrib/kyua/m4/compiler-features.m4 b/contrib/kyua/m4/compiler-features.m4 new file mode 100644 index 000000000000..840f292383d5 --- /dev/null +++ b/contrib/kyua/m4/compiler-features.m4 @@ -0,0 +1,122 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_ATTRIBUTE_NORETURN +dnl +dnl Checks if the current compiler has a way to mark functions that do not +dnl return and defines ATTRIBUTE_NORETURN to the appropriate string. +dnl +AC_DEFUN([KYUA_ATTRIBUTE_NORETURN], [ + dnl This check is overly simple and should be fixed. For example, + dnl Sun's cc does support the noreturn attribute but CC (the C++ + dnl compiler) does not. And in that case, CC just raises a warning + dnl during compilation, not an error. + AC_CACHE_CHECK( + [whether __attribute__((noreturn)) is supported], + [kyua_cv_attribute_noreturn], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([], [ +#if ((__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || __GNUC__ > 2) + return 0; +#else + return 1; +#endif + ])], + [kyua_cv_attribute_noreturn=yes], + [kyua_cv_attribute_noreturn=no]) + ]) + if test "${kyua_cv_attribute_noreturn}" = yes; then + attribute_value="__attribute__((noreturn))" + else + attribute_value="" + fi + AC_SUBST([ATTRIBUTE_NORETURN], [${attribute_value}]) +]) + + +dnl +dnl KYUA_ATTRIBUTE_PURE +dnl +dnl Checks if the current compiler has a way to mark functions as pure. +dnl +AC_DEFUN([KYUA_ATTRIBUTE_PURE], [ + AC_CACHE_CHECK( + [whether __attribute__((__pure__)) is supported], + [kyua_cv_attribute_pure], [ + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([ +static int function(int, int) __attribute__((__pure__)); + +static int +function(int a, int b) +{ + return a + b; +}], [ + return function(3, 4); +])], + [kyua_cv_attribute_pure=yes], + [kyua_cv_attribute_pure=no]) + ]) + if test "${kyua_cv_attribute_pure}" = yes; then + attribute_value="__attribute__((__pure__))" + else + attribute_value="" + fi + AC_SUBST([ATTRIBUTE_PURE], [${attribute_value}]) +]) + + +dnl +dnl KYUA_ATTRIBUTE_UNUSED +dnl +dnl Checks if the current compiler has a way to mark parameters as unused +dnl so that the -Wunused-parameter warning can be avoided. +dnl +AC_DEFUN([KYUA_ATTRIBUTE_UNUSED], [ + AC_CACHE_CHECK( + [whether __attribute__((__unused__)) is supported], + [kyua_cv_attribute_unused], [ + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([ +static void +function(int a __attribute__((__unused__))) +{ +}], [ + function(3); + return 0; +])], + [kyua_cv_attribute_unused=yes], + [kyua_cv_attribute_unused=no]) + ]) + if test "${kyua_cv_attribute_unused}" = yes; then + attribute_value="__attribute__((__unused__))" + else + attribute_value="" + fi + AC_SUBST([ATTRIBUTE_UNUSED], [${attribute_value}]) +]) diff --git a/contrib/kyua/m4/compiler-flags.m4 b/contrib/kyua/m4/compiler-flags.m4 new file mode 100644 index 000000000000..f8dd555118d4 --- /dev/null +++ b/contrib/kyua/m4/compiler-flags.m4 @@ -0,0 +1,169 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file compiler-flags.m4 +dnl +dnl Macros to check for the existence of compiler flags. The macros in this +dnl file support both C and C++. +dnl +dnl Be aware that, in order to detect a flag accurately, we may need to enable +dnl strict warning checking in the compiler (i.e. enable -Werror). Some +dnl compilers, e.g. Clang, report unknown -W flags as warnings unless -Werror is +dnl selected. This fact would confuse the flag checks below because we would +dnl conclude that a flag is valid while in reality it is not. To resolve this, +dnl the macros below will pass -Werror to the compiler along with any other flag +dnl being checked. + + +dnl Checks for a compiler flag and sets a result variable. +dnl +dnl This is an auxiliary macro for the implementation of _KYUA_FLAG. +dnl +dnl \param 1 The shell variable containing the compiler name. Used for +dnl reporting purposes only. C or CXX. +dnl \param 2 The shell variable containing the flags for the compiler. +dnl CFLAGS or CXXFLAGS. +dnl \param 3 The name of the compiler flag to check for. +dnl \param 4 The shell variable to set with the result of the test. Will +dnl be set to 'yes' if the flag is valid, 'no' otherwise. +dnl \param 5 Additional, optional flags to pass to the C compiler while +dnl looking for the flag in $3. We use this here to pass -Werror to the +dnl flag checks (unless we are checking for -Werror already). +AC_DEFUN([_KYUA_FLAG_AUX], [ + if test x"${$4-unset}" = xunset; then + AC_MSG_CHECKING(whether ${$1} supports $3) + saved_flags="${$2}" + $4=no + $2="${$2} $5 $3" + # The inclusion of a header file in the test program below is needed + # because some compiler flags that we test for may actually not be + # compatible with other flags, and such compatibility checks are + # performed within the system header files. + # + # As an example, if we are testing for -D_FORTIFY_SOURCE=2 and the + # compilation is being done with -O2, Linux's /usr/include/features.h + # will abort the compilation of our code later on. By including a + # generic header file here that pulls in features.h we ensure that + # this test is accurate for the build stage. + AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], [return 0;])], + AC_MSG_RESULT(yes) + $4=yes, + AC_MSG_RESULT(no)) + $2="${saved_flags}" + fi +]) + + +dnl Checks for a compiler flag and appends it to a result variable. +dnl +dnl \param 1 The shell variable containing the compiler name. Used for +dnl reporting purposes only. CC or CXX. +dnl \param 2 The shell variable containing the flags for the compiler. +dnl CFLAGS or CXXFLAGS. +dnl \param 3 The name of the compiler flag to check for. +dnl \param 4 The shell variable to which to append $3 if the flag is valid. +AC_DEFUN([_KYUA_FLAG], [ + _KYUA_FLAG_AUX([$1], [$2], [-Werror], [kyua_$1_has_werror]) + if test "$3" = "-Werror"; then + found=${kyua_$1_has_werror} + else + found=unset + if test ${kyua_$1_has_werror} = yes; then + _KYUA_FLAG_AUX([$1], [$2], [$3], [found], [-Werror]) + else + _KYUA_FLAG_AUX([$1], [$2], [$3], [found], []) + fi + fi + if test ${found} = yes; then + $4="${$4} $3" + fi +]) + + +dnl Checks for a C compiler flag and appends it to a variable. +dnl +dnl \pre The current language is C. +dnl +dnl \param 1 The name of the compiler flag to check for. +dnl \param 2 The shell variable to which to append $1 if the flag is valid. +AC_DEFUN([KYUA_CC_FLAG], [ + AC_LANG_ASSERT([C]) + _KYUA_FLAG([CC], [CFLAGS], [$1], [$2]) +]) + + +dnl Checks for a C++ compiler flag and appends it to a variable. +dnl +dnl \pre The current language is C++. +dnl +dnl \param 1 The name of the compiler flag to check for. +dnl \param 2 The shell variable to which to append $1 if the flag is valid. +AC_DEFUN([KYUA_CXX_FLAG], [ + AC_LANG_ASSERT([C++]) + _KYUA_FLAG([CXX], [CXXFLAGS], [$1], [$2]) +]) + + +dnl Checks for a set of C compiler flags and appends them to CFLAGS. +dnl +dnl The checks are performed independently and only when all the checks are +dnl done, the output variable is modified. +dnl +dnl \param 1 Whitespace-separated list of C flags to check. +AC_DEFUN([KYUA_CC_FLAGS], [ + AC_LANG_PUSH([C]) + valid_cflags= + for f in $1; do + KYUA_CC_FLAG(${f}, valid_cflags) + done + if test -n "${valid_cflags}"; then + CFLAGS="${CFLAGS} ${valid_cflags}" + fi + AC_LANG_POP([C]) +]) + + +dnl Checks for a set of C++ compiler flags and appends them to CXXFLAGS. +dnl +dnl The checks are performed independently and only when all the checks are +dnl done, the output variable is modified. +dnl +dnl \pre The current language is C++. +dnl +dnl \param 1 Whitespace-separated list of C flags to check. +AC_DEFUN([KYUA_CXX_FLAGS], [ + AC_LANG_PUSH([C++]) + valid_cxxflags= + for f in $1; do + KYUA_CXX_FLAG(${f}, valid_cxxflags) + done + if test -n "${valid_cxxflags}"; then + CXXFLAGS="${CXXFLAGS} ${valid_cxxflags}" + fi + AC_LANG_POP([C++]) +]) diff --git a/contrib/kyua/m4/developer-mode.m4 b/contrib/kyua/m4/developer-mode.m4 new file mode 100644 index 000000000000..ad946056f63c --- /dev/null +++ b/contrib/kyua/m4/developer-mode.m4 @@ -0,0 +1,112 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file developer-mode.m4 +dnl +dnl "Developer mode" is a mode in which the build system reports any +dnl build-time warnings as fatal errors. This helps in minimizing the +dnl amount of trivial coding problems introduced in the code. +dnl Unfortunately, this is not bullet-proof due to the wide variety of +dnl compilers available and their different warning diagnostics. +dnl +dnl When developer mode support is added to a package, the compilation will +dnl gain a bunch of extra warning diagnostics. These will NOT be enforced +dnl unless developer mode is enabled. +dnl +dnl Developer mode is enabled when the user requests it through the +dnl configure command line, or when building from the repository. The +dnl latter is to minimize the risk of committing new code with warnings +dnl into the tree. + + +dnl Adds "developer mode" support to the package. +dnl +dnl This macro performs the actual definition of the --enable-developer +dnl flag and implements all of its logic. See the file-level comment for +dnl details as to what this implies. +AC_DEFUN([KYUA_DEVELOPER_MODE], [ + m4_foreach([language], [$1], [m4_set_add([languages], language)]) + + AC_ARG_ENABLE( + [developer], + AS_HELP_STRING([--enable-developer], [enable developer features]),, + [if test -d "${srcdir}/.git"; then + AC_MSG_NOTICE([building from HEAD; developer mode autoenabled]) + enable_developer=yes + else + enable_developer=no + fi]) + + # + # The following warning flags should also be enabled but cannot be. + # Reasons given below. + # + # -Wold-style-cast: Raises errors when using TIOCGWINSZ, at least under + # Mac OS X. This is due to the way _IOR is defined. + # + + try_c_cxx_flags="-D_FORTIFY_SOURCE=2 \ + -Wall \ + -Wcast-qual \ + -Wextra \ + -Wpointer-arith \ + -Wredundant-decls \ + -Wreturn-type \ + -Wshadow \ + -Wsign-compare \ + -Wswitch \ + -Wwrite-strings" + + try_c_flags="-Wmissing-prototypes \ + -Wno-traditional \ + -Wstrict-prototypes" + + try_cxx_flags="-Wabi \ + -Wctor-dtor-privacy \ + -Wno-deprecated \ + -Wno-non-template-friend \ + -Wno-pmf-conversions \ + -Wnon-virtual-dtor \ + -Woverloaded-virtual \ + -Wreorder \ + -Wsign-promo \ + -Wsynth" + + if test ${enable_developer} = yes; then + try_werror=yes + try_c_cxx_flags="${try_c_cxx_flags} -g -Werror" + else + try_werror=no + try_c_cxx_flags="${try_c_cxx_flags} -DNDEBUG" + fi + + m4_set_contains([languages], [C], + [KYUA_CC_FLAGS(${try_c_cxx_flags} ${try_c_flags})]) + m4_set_contains([languages], [C++], + [KYUA_CXX_FLAGS(${try_c_cxx_flags} ${try_cxx_flags})]) +]) diff --git a/contrib/kyua/m4/doxygen.m4 b/contrib/kyua/m4/doxygen.m4 new file mode 100644 index 000000000000..24fd2a408f88 --- /dev/null +++ b/contrib/kyua/m4/doxygen.m4 @@ -0,0 +1,62 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_DOXYGEN +dnl +dnl Adds a --with-doxygen flag to the configure script and, when Doxygen support +dnl is requested by the user, sets DOXYGEN to the path of the Doxygen binary and +dnl enables the WITH_DOXYGEN Automake conditional. +dnl +AC_DEFUN([KYUA_DOXYGEN], [ + AC_ARG_WITH([doxygen], + AS_HELP_STRING([--with-doxygen], + [build documentation for internal APIs]), + [], + [with_doxygen=auto]) + + if test "${with_doxygen}" = yes; then + AC_PATH_PROG([DOXYGEN], [doxygen], []) + if test -z "${DOXYGEN}"; then + AC_MSG_ERROR([Doxygen explicitly requested but not found]) + fi + elif test "${with_doxygen}" = auto; then + AC_PATH_PROG([DOXYGEN], [doxygen], []) + elif test "${with_doxygen}" = no; then + DOXYGEN= + else + AC_MSG_CHECKING([for doxygen]) + DOXYGEN="${with_doxygen}" + AC_MSG_RESULT([${DOXYGEN}]) + if test ! -x "${DOXYGEN}"; then + AC_MSG_ERROR([Doxygen binary ${DOXYGEN} is not executable]) + fi + fi + AM_CONDITIONAL([WITH_DOXYGEN], [test -n "${DOXYGEN}"]) + AC_SUBST([DOXYGEN]) +]) diff --git a/contrib/kyua/m4/fs.m4 b/contrib/kyua/m4/fs.m4 new file mode 100644 index 000000000000..7cb103eb1370 --- /dev/null +++ b/contrib/kyua/m4/fs.m4 @@ -0,0 +1,125 @@ +dnl Copyright 2011 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file m4/fs.m4 +dnl File system related checks. +dnl +dnl The macros in this file check for features required in the utils/fs +dnl module. The global KYUA_FS_MODULE macro will call all checks required +dnl for the library. + + +dnl KYUA_FS_GETCWD_DYN +dnl +dnl Checks whether getcwd(NULL, 0) works; i.e. if getcwd(3) can dynamically +dnl allocate the output buffer to fit the whole current path. +AC_DEFUN([KYUA_FS_GETCWD_DYN], [ + AC_CACHE_CHECK( + [whether getcwd(NULL, 0) works], + [kyua_cv_getcwd_dyn], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +], [ + char *cwd = getcwd(NULL, 0); + return (cwd != NULL) ? EXIT_SUCCESS : EXIT_FAILURE; +])], + [kyua_cv_getcwd_dyn=yes], + [kyua_cv_getcwd_dyn=no]) + ]) + if test "${kyua_cv_getcwd_dyn}" = yes; then + AC_DEFINE_UNQUOTED([HAVE_GETCWD_DYN], [1], + [Define to 1 if getcwd(NULL, 0) works]) + fi +]) + + +dnl KYUA_FS_LCHMOD +dnl +dnl Checks whether lchmod(3) exists and if it works. Some systems, such as +dnl Ubuntu 10.04.1 LTS, provide a lchmod(3) stub that is not implemented yet +dnl allows programs to compile cleanly (albeit for a warning). It would be +dnl nice to detect if lchmod(3) works at run time to prevent side-effects of +dnl this test but doing so means we will keep receiving a noisy compiler +dnl warning. +AC_DEFUN([KYUA_FS_LCHMOD], [ + AC_CACHE_CHECK( + [for a working lchmod], + [kyua_cv_lchmod_works], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +#include +#include +#include +], [ + int fd = open("conftest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) { + perror("creation of conftest.txt failed"); + return EXIT_FAILURE; + } + + return lchmod("conftest.txt", 0640) != -1 ? EXIT_SUCCESS : EXIT_FAILURE; +])], + [kyua_cv_lchmod_works=yes], + [kyua_cv_lchmod_works=no]) + ]) + rm -f conftest.txt + if test "${kyua_cv_lchmod_works}" = yes; then + AC_DEFINE_UNQUOTED([HAVE_WORKING_LCHMOD], [1], + [Define to 1 if your lchmod works]) + fi +]) + + +dnl KYUA_FS_UNMOUNT +dnl +dnl Detect the correct method to unmount a file system. +AC_DEFUN([KYUA_FS_UNMOUNT], [ + AC_CHECK_FUNCS([unmount], [have_unmount2=yes], [have_unmount2=no]) + if test "${have_unmount2}" = no; then + have_umount8=yes + AC_PATH_PROG([UMOUNT], [umount], [have_umount8=no]) + if test "${have_umount8}" = yes; then + AC_DEFINE_UNQUOTED([UMOUNT], ["${UMOUNT}"], + [Set to the path of umount(8)]) + else + AC_MSG_ERROR([Don't know how to unmount a file system]) + fi + fi +]) + + +dnl KYUA_FS_MODULE +dnl +dnl Performs all checks needed by the utils/fs library. +AC_DEFUN([KYUA_FS_MODULE], [ + AC_CHECK_HEADERS([sys/mount.h sys/statvfs.h sys/vfs.h]) + AC_CHECK_FUNCS([statfs statvfs]) + KYUA_FS_GETCWD_DYN + KYUA_FS_LCHMOD + KYUA_FS_UNMOUNT +]) diff --git a/contrib/kyua/m4/getopt.m4 b/contrib/kyua/m4/getopt.m4 new file mode 100644 index 000000000000..f58635330704 --- /dev/null +++ b/contrib/kyua/m4/getopt.m4 @@ -0,0 +1,213 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +dnl Checks if getopt(3) supports a + sign to enforce POSIX correctness. +dnl +dnl In the GNU implementation of getopt(3), we need to pass a + sign at +dnl the beginning of the options string to request POSIX behavior. +dnl +dnl Defines HAVE_GETOPT_GNU if a + sign is supported. +AC_DEFUN([_KYUA_GETOPT_GNU], [ + AC_CACHE_CHECK( + [whether getopt allows a + sign for POSIX behavior optreset], + [kyua_cv_getopt_gnu], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +#include ], [ + int argc = 4; + char* argv@<:@5@:>@ = { + strdup("conftest"), + strdup("-+"), + strdup("-a"), + strdup("bar"), + NULL + }; + int ch; + int seen_a = 0, seen_plus = 0; + + while ((ch = getopt(argc, argv, "+a:")) != -1) { + switch (ch) { + case 'a': + seen_a = 1; + break; + + case '+': + seen_plus = 1; + break; + + case '?': + default: + ; + } + } + + return (seen_a && !seen_plus) ? EXIT_SUCCESS : EXIT_FAILURE; +])], + [kyua_cv_getopt_gnu=yes], + [kyua_cv_getopt_gnu=no]) + ]) + if test "${kyua_cv_getopt_gnu}" = yes; then + AC_DEFINE([HAVE_GETOPT_GNU], [1], + [Define to 1 if getopt allows a + sign for POSIX behavior]) + fi +]) + +dnl Checks if optreset exists to reset the processing of getopt(3) options. +dnl +dnl getopt(3) has an optreset global variable to reset internal state +dnl before calling getopt(3) again. However, optreset is not standard and +dnl is only present in the BSD versions of getopt(3). +dnl +dnl Defines HAVE_GETOPT_WITH_OPTRESET if optreset exists. +AC_DEFUN([_KYUA_GETOPT_WITH_OPTRESET], [ + AC_CACHE_CHECK( + [whether getopt has optreset], + [kyua_cv_getopt_optreset], [ + AC_COMPILE_IFELSE([AC_LANG_SOURCE([ +#include +#include + +int +main(void) +{ + optreset = 1; + return EXIT_SUCCESS; +} +])], + [kyua_cv_getopt_optreset=yes], + [kyua_cv_getopt_optreset=no]) + ]) + if test "${kyua_cv_getopt_optreset}" = yes; then + AC_DEFINE([HAVE_GETOPT_WITH_OPTRESET], [1], + [Define to 1 if getopt has optreset]) + fi +]) + + +dnl Checks the value to pass to optind to reset getopt(3) processing. +dnl +dnl The standard value to pass to optind to reset the processing of command +dnl lines with getopt(3) is 1. However, the GNU extensions to getopt_long(3) +dnl are not properly reset unless optind is set to 0, causing crashes later +dnl on and incorrect option processing behavior. +dnl +dnl Sets the GETOPT_OPTIND_RESET_VALUE macro to the integer value that has to +dnl be passed to optind to reset option processing. +AC_DEFUN([_KYUA_GETOPT_OPTIND_RESET_VALUE], [ + AC_CACHE_CHECK( + [for the optind value to reset getopt processing], + [kyua_cv_getopt_optind_reset_value], [ + AC_RUN_IFELSE([AC_LANG_SOURCE([ +#include +#include +#include + +static void +first_pass(void) +{ + int argc, ch, flag; + char* argv@<:@5@:>@; + + argc = 4; + argv@<:@0@:>@ = strdup("progname"); + argv@<:@1@:>@ = strdup("-a"); + argv@<:@2@:>@ = strdup("foo"); + argv@<:@3@:>@ = strdup("bar"); + argv@<:@4@:>@ = NULL; + + flag = 0; + while ((ch = getopt(argc, argv, "+:a")) != -1) { + switch (ch) { + case 'a': + flag = 1; + break; + default: + break; + } + } + if (!flag) { + exit(EXIT_FAILURE); + } +} + +static void +second_pass(void) +{ + int argc, ch, flag; + char* argv@<:@5@:>@; + + argc = 4; + argv@<:@0@:>@ = strdup("progname"); + argv@<:@1@:>@ = strdup("-b"); + argv@<:@2@:>@ = strdup("foo"); + argv@<:@3@:>@ = strdup("bar"); + argv@<:@4@:>@ = NULL; + + flag = 0; + while ((ch = getopt(argc, argv, "b")) != -1) { + switch (ch) { + case 'b': + flag = 1; + break; + default: + break; + } + } + if (!flag) { + exit(EXIT_FAILURE); + } +} + +int +main(void) +{ + /* We do two passes in two different functions to prevent the reuse of + * variables and, specially, to force the use of two different argument + * vectors. */ + first_pass(); + optind = 0; + second_pass(); + return EXIT_SUCCESS; +} +])], + [kyua_cv_getopt_optind_reset_value=0], + [kyua_cv_getopt_optind_reset_value=1]) + ]) + AC_DEFINE_UNQUOTED([GETOPT_OPTIND_RESET_VALUE], + [${kyua_cv_getopt_optind_reset_value}], + [Define to the optind value to reset getopt processing]) +]) + + +dnl Wrapper macro to detect all getopt(3) necessary features. +AC_DEFUN([KYUA_GETOPT], [ + _KYUA_GETOPT_GNU + _KYUA_GETOPT_OPTIND_RESET_VALUE + _KYUA_GETOPT_WITH_OPTRESET +]) diff --git a/contrib/kyua/m4/memory.m4 b/contrib/kyua/m4/memory.m4 new file mode 100644 index 000000000000..3d9a83a20ab5 --- /dev/null +++ b/contrib/kyua/m4/memory.m4 @@ -0,0 +1,122 @@ +dnl Copyright 2012 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file m4/memory.m4 +dnl +dnl Macros to configure the utils::memory module. + + +dnl Entry point to detect all features needed by utils::memory. +dnl +dnl This looks for a mechanism to check the available physical memory in the +dnl system. +AC_DEFUN([KYUA_MEMORY], [ + memory_query=unknown + memory_mib=none + + _KYUA_SYSCTLBYNAME([have_sysctlbyname=yes], [have_sysctlbyname=no]) + if test "${have_sysctlbyname}" = yes; then + _KYUA_SYSCTL_MIB([hw.usermem64], [hw_usermem64], + [memory_mib="hw.usermem64"], []) + if test "${memory_mib}" = none; then + _KYUA_SYSCTL_MIB([hw.usermem], [hw_usermem], + [memory_mib="hw.usermem"], []) + fi + if test "${memory_mib}" != none; then + memory_query=sysctlbyname + fi + fi + + if test "${memory_query}" = unknown; then + AC_MSG_WARN([Don't know how to query the amount of physical memory]) + AC_MSG_WARN([The test case's require.memory property will not work]) + fi + + AC_DEFINE_UNQUOTED([MEMORY_QUERY_TYPE], ["${memory_query}"], + [Define to the memory query type]) + AC_DEFINE_UNQUOTED([MEMORY_QUERY_SYSCTL_MIB], ["${memory_mib}"], + [Define to the name of the sysctl MIB]) +]) + + +dnl Detects the availability of the sysctlbyname(3) function. +dnl +dnl \param action_if_found Code to run if the function is found. +dnl \param action_if_not_found Code to run if the function is not found. +AC_DEFUN([_KYUA_SYSCTLBYNAME], [ + AC_CHECK_HEADERS([sys/types.h sys/sysctl.h]) dnl Darwin 11.2 + AC_CHECK_HEADERS([sys/param.h sys/sysctl.h]) dnl NetBSD 6.0 + + AC_CHECK_FUNCS([sysctlbyname], [$1], [$2]) +]) + + +dnl Looks for a specific sysctl MIB. +dnl +dnl \pre sysctlbyname(3) must be present in the system. +dnl +dnl \param mib_name The name of the MIB to check for. +dnl \param flat_mib_name The name of the MIB as a shell variable, for use in +dnl cache variable names. This should be automatically computed with +dnl m4_bpatsubst or similar, but my inability to make the code readable +dnl made me add this parameter instead. +dnl \param action_if_found Code to run if the MIB is found. +dnl \param action_if_not_found Code to run if the MIB is not found. +AC_DEFUN([_KYUA_SYSCTL_MIB], [ + AC_CACHE_CHECK( + [if the $1 sysctl MIB exists], + [kyua_cv_sysctl_$2], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([ +#if defined(HAVE_SYS_TYPES_H) +# include +#endif +#if defined(HAVE_SYS_PARAM_H) +# include +#endif +#if defined(HAVE_SYS_SYSCTL_H) +# include +#endif +#include +#include +], [ + int64_t memory; + size_t memory_length = sizeof(memory); + if (sysctlbyname("$1", &memory, &memory_length, NULL, 0) == -1) + return EXIT_FAILURE; + else + return EXIT_SUCCESS; +])], + [kyua_cv_sysctl_$2=yes], + [kyua_cv_sysctl_$2=no]) + ]) + if test "${kyua_cv_sysctl_$2}" = yes; then + m4_default([$3], [:]) + else + m4_default([$4], [:]) + fi +]) diff --git a/contrib/kyua/m4/signals.m4 b/contrib/kyua/m4/signals.m4 new file mode 100644 index 000000000000..8e8b56e1eb73 --- /dev/null +++ b/contrib/kyua/m4/signals.m4 @@ -0,0 +1,92 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_LAST_SIGNO +dnl +dnl Detect the last valid signal number. +dnl +AC_DEFUN([KYUA_LAST_SIGNO], [ + AC_CACHE_CHECK( + [for the last valid signal], + [kyua_cv_signals_lastno], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +#include +#include +#include +#include ], [ + static const int max_signals = 256; + int i; + FILE *f; + + i = 0; + while (i < max_signals) { + i++; + if (i != SIGKILL && i != SIGSTOP) { + struct sigaction sa; + int ret; + + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + ret = sigaction(i, &sa, NULL); + if (ret == -1) { + warn("sigaction(%d) failed", i); + if (errno == EINVAL) { + i--; + break; + } else + err(EXIT_FAILURE, "sigaction failed"); + } + } + } + if (i == max_signals) + errx(EXIT_FAILURE, "too many signals"); + + f = fopen("conftest.cnt", "w"); + if (f == NULL) + err(EXIT_FAILURE, "failed to open file"); + + fprintf(f, "%d\n", i); + fclose(f); + + return EXIT_SUCCESS; +])], + [if test ! -f conftest.cnt; then + kyua_cv_signals_lastno=15 + else + kyua_cv_signals_lastno=$(cat conftest.cnt) + rm -f conftest.cnt + fi], + [kyua_cv_signals_lastno=15]) + ]) + AC_DEFINE_UNQUOTED([LAST_SIGNO], [${kyua_cv_signals_lastno}], + [Define to the last valid signal number]) +]) diff --git a/contrib/kyua/m4/uname.m4 b/contrib/kyua/m4/uname.m4 new file mode 100644 index 000000000000..bcb3d0d39a71 --- /dev/null +++ b/contrib/kyua/m4/uname.m4 @@ -0,0 +1,63 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_UNAME_ARCHITECTURE +dnl +dnl Checks for the current architecture name (aka processor type) and defines +dnl the KYUA_ARCHITECTURE macro to its value. +dnl +AC_DEFUN([KYUA_UNAME_ARCHITECTURE], [ + AC_MSG_CHECKING([for architecture name]) + AC_ARG_VAR([KYUA_ARCHITECTURE], + [Name of the system architecture (aka processor type)]) + if test x"${KYUA_ARCHITECTURE-unset}" = x"unset"; then + KYUA_ARCHITECTURE="$(uname -p)" + fi + AC_DEFINE_UNQUOTED([KYUA_ARCHITECTURE], "${KYUA_ARCHITECTURE}", + [Name of the system architecture (aka processor type)]) + AC_MSG_RESULT([${KYUA_ARCHITECTURE}]) +]) + +dnl +dnl KYUA_UNAME_PLATFORM +dnl +dnl Checks for the current platform name (aka machine name) and defines +dnl the KYUA_PLATFORM macro to its value. +dnl +AC_DEFUN([KYUA_UNAME_PLATFORM], [ + AC_MSG_CHECKING([for platform name]) + AC_ARG_VAR([KYUA_PLATFORM], + [Name of the system platform (aka machine name)]) + if test x"${KYUA_PLATFORM-unset}" = x"unset"; then + KYUA_PLATFORM="$(uname -m)" + fi + AC_DEFINE_UNQUOTED([KYUA_PLATFORM], "${KYUA_PLATFORM}", + [Name of the system platform (aka machine name)]) + AC_MSG_RESULT([${KYUA_PLATFORM}]) +]) diff --git a/contrib/kyua/main.cpp b/contrib/kyua/main.cpp new file mode 100644 index 000000000000..4344248f89db --- /dev/null +++ b/contrib/kyua/main.cpp @@ -0,0 +1,50 @@ +// 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 "cli/main.hpp" + + +/// Program entry point. +/// +/// The whole purpose of this extremely-simple function is to delegate execution +/// to an internal module that does not contain a proper ::main() function. +/// This is to allow unit-testing of the internal code. +/// +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// +/// \return 0 on success, some other integer on error. +/// +/// \throw std::exception This throws any uncaught exception. Such exceptions +/// are bugs, but we let them propagate so that the runtime will abort and +/// dump core. +int +main(const int argc, const char* const* const argv) +{ + return cli::main(argc, argv); +} diff --git a/contrib/kyua/misc/Makefile.am.inc b/contrib/kyua/misc/Makefile.am.inc new file mode 100644 index 000000000000..e235c7ee364e --- /dev/null +++ b/contrib/kyua/misc/Makefile.am.inc @@ -0,0 +1,32 @@ +# 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. + +dist_misc_DATA = misc/context.html +dist_misc_DATA += misc/index.html +dist_misc_DATA += misc/report.css +dist_misc_DATA += misc/test_result.html diff --git a/contrib/kyua/misc/context.html b/contrib/kyua/misc/context.html new file mode 100644 index 000000000000..cb8f16c582fb --- /dev/null +++ b/contrib/kyua/misc/context.html @@ -0,0 +1,55 @@ + + + + + Execution context + + + + + +

Execution context

+ +
    +
  • Work directory: %%cwd%%
  • +
+ +

Environment variables

+ +
    +%loop env_var iter +
  • %%env_var(iter)%%: %%env_var_value(iter)%%
  • +%endloop +
+ + + diff --git a/contrib/kyua/misc/index.html b/contrib/kyua/misc/index.html new file mode 100644 index 000000000000..ca53ff3623fb --- /dev/null +++ b/contrib/kyua/misc/index.html @@ -0,0 +1,187 @@ + + + + + + Tests summary + + + + + + +

Summary of test results

+ +

Overall result: +%if bad_tests_count + %%bad_tests_count%% TESTS FAILING +%else + ALL TESTS PASSING +%endif +

+ + + + + + + + + + +%if length(broken_test_cases) + + + + +%else + + + + +%endif +%if length(failed_test_cases) + + + + +%else + + + + +%endif + +%if length(xfail_test_cases) + +%else + +%endif + + + +%if length(skipped_test_cases) + +%else + +%endif + + + +%if length(passed_test_cases) + +%else + +%endif + + + +
Test case resultCount
Broken%%length(broken_test_cases)%%
Broken%%broken_tests_count%%
Failed%%length(failed_test_cases)%%
Failed%%failed_tests_count%%
Expected failuresExpected failures%%xfail_tests_count%%
SkippedSkipped%%skipped_tests_count%%
PassedPassed%%passed_tests_count%%
+ +

Execution context

+ +

Timing data:

+ +
    +
  • Start time: %%start_time%%
  • +
  • End time: %%end_time%%
  • +
  • Duration: %%duration%%
  • +
+ + +%if length(broken_test_cases) +

Broken test cases

+ + +%endif + + +%if length(failed_test_cases) +

Failed test cases

+ + +%endif + + +%if length(xfail_test_cases) +

Expected failures

+ + +%endif + + +%if length(skipped_test_cases) +

Skipped test cases

+ + +%endif + + +%if length(passed_test_cases) +

Passed test cases

+ + +%endif + + + + diff --git a/contrib/kyua/misc/report.css b/contrib/kyua/misc/report.css new file mode 100644 index 000000000000..ede4c5255fd6 --- /dev/null +++ b/contrib/kyua/misc/report.css @@ -0,0 +1,78 @@ +/* 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. */ + +body { + background: white; + text-color: black; +} + +h1 { + color: #00d000; +} + +h2 { + color: #00a000; +} + +p.overall font.good { + color: #00ff00; +} + +p.overall font.bad { + color: #ff0000; +} + +pre { + background-color: #e0f0e0; + margin-left: 20px; + margin-right: 20px; + padding: 5px; +} + +table.tests-count { + border-width: 1; + border-style: solid; + border-color: #b0e0b0; + padding: 0; +} + +table.tests-count td { + padding: 3px; +} + +table.tests-count td.numeric { + text-align: right; +} + +table.tests-count tr.bad { + background: #e0b0b0; +} + +table.tests-count thead tr { + background: #b0e0b0; +} diff --git a/contrib/kyua/misc/test_result.html b/contrib/kyua/misc/test_result.html new file mode 100644 index 000000000000..4c4a4132b66c --- /dev/null +++ b/contrib/kyua/misc/test_result.html @@ -0,0 +1,76 @@ + + + + + Test case: %%test_case%% + + + + + +

Test case: %%test_case%%

+ +
    +
  • Test program: %%test_program%%
  • +
  • Result: %%result%%
  • +
  • Start time: %%start_time%%
  • +
  • End time: %%end_time%%
  • +
  • Duration: %%duration%%
  • +
  • Execution context
  • +
+ +

Metadata

+ +
    +%loop metadata_var iter +
  • %%metadata_var(iter)%% = %%metadata_value(iter)%%
  • +%endloop +
+ +

Standard output

+ +%if defined(stdout) +
%%stdout%%
+%else +Test case did not write anything to stdout. +%endif + +

Standard error

+ +%if defined(stderr) +
%%stderr%%
+%else +Test case did not write anything to stderr. +%endif + + + diff --git a/contrib/kyua/model/Kyuafile b/contrib/kyua/model/Kyuafile new file mode 100644 index 000000000000..9dae3b9c64ce --- /dev/null +++ b/contrib/kyua/model/Kyuafile @@ -0,0 +1,10 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="context_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="metadata_test"} +atf_test_program{name="test_case_test"} +atf_test_program{name="test_program_test"} +atf_test_program{name="test_result_test"} diff --git a/contrib/kyua/model/Makefile.am.inc b/contrib/kyua/model/Makefile.am.inc new file mode 100644 index 000000000000..2bd33914f680 --- /dev/null +++ b/contrib/kyua/model/Makefile.am.inc @@ -0,0 +1,89 @@ +# 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. + +MODEL_CFLAGS = $(UTILS_CFLAGS) +MODEL_LIBS = libmodel.a $(UTILS_LIBS) + +noinst_LIBRARIES += libmodel.a +libmodel_a_CPPFLAGS = $(UTILS_CFLAGS) +libmodel_a_SOURCES = model/context.cpp +libmodel_a_SOURCES += model/context.hpp +libmodel_a_SOURCES += model/context_fwd.hpp +libmodel_a_SOURCES += model/exceptions.cpp +libmodel_a_SOURCES += model/exceptions.hpp +libmodel_a_SOURCES += model/metadata.cpp +libmodel_a_SOURCES += model/metadata.hpp +libmodel_a_SOURCES += model/metadata_fwd.hpp +libmodel_a_SOURCES += model/test_case.cpp +libmodel_a_SOURCES += model/test_case.hpp +libmodel_a_SOURCES += model/test_case_fwd.hpp +libmodel_a_SOURCES += model/test_program.cpp +libmodel_a_SOURCES += model/test_program.hpp +libmodel_a_SOURCES += model/test_program_fwd.hpp +libmodel_a_SOURCES += model/test_result.cpp +libmodel_a_SOURCES += model/test_result.hpp +libmodel_a_SOURCES += model/test_result_fwd.hpp +libmodel_a_SOURCES += model/types.hpp + +if WITH_ATF +tests_modeldir = $(pkgtestsdir)/model + +tests_model_DATA = model/Kyuafile +EXTRA_DIST += $(tests_model_DATA) + +tests_model_PROGRAMS = model/context_test +model_context_test_SOURCES = model/context_test.cpp +model_context_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_context_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/exceptions_test +model_exceptions_test_SOURCES = model/exceptions_test.cpp +model_exceptions_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_exceptions_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/metadata_test +model_metadata_test_SOURCES = model/metadata_test.cpp +model_metadata_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_metadata_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/test_case_test +model_test_case_test_SOURCES = model/test_case_test.cpp +model_test_case_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_test_case_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/test_program_test +model_test_program_test_SOURCES = model/test_program_test.cpp +model_test_program_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_test_program_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/test_result_test +model_test_result_test_SOURCES = model/test_result_test.cpp +model_test_result_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_test_result_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +endif diff --git a/contrib/kyua/model/README b/contrib/kyua/model/README new file mode 100644 index 000000000000..cf13a82b7338 --- /dev/null +++ b/contrib/kyua/model/README @@ -0,0 +1,11 @@ +This directory contains the classes that form the data model of Kyua. + +The classes in this directory are intended to be pure data types without +any complex logic. As such, they are simple containers and support the +common operations you would expect from them: in particular, comparisons +and formatting for debugging purposes. + +All the classes in the data model have to have an on-disk representation +provided by the store module; if they don't, they don't belong in the +model. Some of these classes may also have special behavior at run-time, +and this is provided by the engine module. diff --git a/contrib/kyua/model/context.cpp b/contrib/kyua/model/context.cpp new file mode 100644 index 000000000000..5afe89759d94 --- /dev/null +++ b/contrib/kyua/model/context.cpp @@ -0,0 +1,159 @@ +// 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 "model/context.hpp" + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace text = utils::text; + + +/// Internal implementation of a context. +struct model::context::impl : utils::noncopyable { + /// The current working directory. + fs::path _cwd; + + /// The environment variables. + std::map< std::string, std::string > _env; + + /// Constructor. + /// + /// \param cwd_ The current working directory. + /// \param env_ The environment variables. + impl(const fs::path& cwd_, + const std::map< std::string, std::string >& env_) : + _cwd(cwd_), + _env(env_) + { + } + + /// Equality comparator. + /// + /// \param other The object to compare to. + /// + /// \return True if the two objects are equal; false otherwise. + bool + operator==(const impl& other) const + { + return _cwd == other._cwd && _env == other._env; + } +}; + + +/// Constructs a new context. +/// +/// \param cwd_ The current working directory. +/// \param env_ The environment variables. +model::context::context(const fs::path& cwd_, + const std::map< std::string, std::string >& env_) : + _pimpl(new impl(cwd_, env_)) +{ +} + + +/// Destructor. +model::context::~context(void) +{ +} + + +/// Returns the current working directory of the context. +/// +/// \return A path. +const fs::path& +model::context::cwd(void) const +{ + return _pimpl->_cwd; +} + + +/// Returns the environment variables of the context. +/// +/// \return A variable name to variable value mapping. +const std::map< std::string, std::string >& +model::context::env(void) const +{ + return _pimpl->_env; +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +model::context::operator==(const context& other) const +{ + return *_pimpl == *other._pimpl; +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::context::operator!=(const context& other) const +{ + return !(*this == other); +} + + +/// 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& +model::operator<<(std::ostream& output, const context& object) +{ + output << F("context{cwd=%s, env=[") + % text::quote(object.cwd().str(), '\''); + + const std::map< std::string, std::string >& env = object.env(); + bool first = true; + for (std::map< std::string, std::string >::const_iterator + iter = env.begin(); iter != env.end(); ++iter) { + if (!first) + output << ", "; + first = false; + + output << F("%s=%s") % (*iter).first + % text::quote((*iter).second, '\''); + } + + output << "]}"; + return output; +} diff --git a/contrib/kyua/model/context.hpp b/contrib/kyua/model/context.hpp new file mode 100644 index 000000000000..d11ae8ba80b9 --- /dev/null +++ b/contrib/kyua/model/context.hpp @@ -0,0 +1,76 @@ +// 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 model/context.hpp +/// Representation of runtime contexts. + +#if !defined(MODEL_CONTEXT_HPP) +#define MODEL_CONTEXT_HPP + +#include "model/context_fwd.hpp" + +#include +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace model { + + +/// Representation of a runtime context. +/// +/// The instances of this class are unique (i.e. copying the objects only yields +/// a shallow copy that shares the same internal implementation). This is a +/// requirement for the 'store' API model. +class context { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + +public: + context(const utils::fs::path&, + const std::map< std::string, std::string >&); + ~context(void); + + const utils::fs::path& cwd(void) const; + const std::map< std::string, std::string >& env(void) const; + + bool operator==(const context&) const; + bool operator!=(const context&) const; +}; + + +std::ostream& operator<<(std::ostream&, const context&); + + +} // namespace model + +#endif // !defined(MODEL_CONTEXT_HPP) diff --git a/contrib/kyua/model/context_fwd.hpp b/contrib/kyua/model/context_fwd.hpp new file mode 100644 index 000000000000..000ed864e948 --- /dev/null +++ b/contrib/kyua/model/context_fwd.hpp @@ -0,0 +1,43 @@ +// 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 model/context_fwd.hpp +/// Forward declarations for model/context.hpp + +#if !defined(MODEL_CONTEXT_FWD_HPP) +#define MODEL_CONTEXT_FWD_HPP + +namespace model { + + +class context; + + +} // namespace model + +#endif // !defined(MODEL_CONTEXT_FWD_HPP) diff --git a/contrib/kyua/model/context_test.cpp b/contrib/kyua/model/context_test.cpp new file mode 100644 index 000000000000..8990990710f2 --- /dev/null +++ b/contrib/kyua/model/context_test.cpp @@ -0,0 +1,106 @@ +// 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 "model/context.hpp" + +#include +#include +#include + +#include + +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(ctor_and_getters); +ATF_TEST_CASE_BODY(ctor_and_getters) +{ + std::map< std::string, std::string > env; + env["foo"] = "first"; + env["bar"] = "second"; + const model::context context(fs::path("/foo/bar"), env); + ATF_REQUIRE_EQ(fs::path("/foo/bar"), context.cwd()); + ATF_REQUIRE(env == context.env()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne); +ATF_TEST_CASE_BODY(operators_eq_and_ne) +{ + std::map< std::string, std::string > env; + env["foo"] = "first"; + const model::context context1(fs::path("/foo/bar"), env); + const model::context context2(fs::path("/foo/bar"), env); + const model::context context3(fs::path("/foo/baz"), env); + env["bar"] = "second"; + const model::context context4(fs::path("/foo/bar"), env); + ATF_REQUIRE( context1 == context2); + ATF_REQUIRE(!(context1 != context2)); + ATF_REQUIRE(!(context1 == context3)); + ATF_REQUIRE( context1 != context3); + ATF_REQUIRE(!(context1 == context4)); + ATF_REQUIRE( context1 != context4); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__empty_env); +ATF_TEST_CASE_BODY(output__empty_env) +{ + const std::map< std::string, std::string > env; + const model::context context(fs::path("/foo/bar"), env); + + std::ostringstream str; + str << context; + ATF_REQUIRE_EQ("context{cwd='/foo/bar', env=[]}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__some_env); +ATF_TEST_CASE_BODY(output__some_env) +{ + std::map< std::string, std::string > env; + env["foo"] = "first"; + env["bar"] = "second' var"; + const model::context context(fs::path("/foo/bar"), env); + + std::ostringstream str; + str << context; + ATF_REQUIRE_EQ("context{cwd='/foo/bar', env=[bar='second\\' var', " + "foo='first']}", str.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne); + ATF_ADD_TEST_CASE(tcs, output__empty_env); + ATF_ADD_TEST_CASE(tcs, output__some_env); +} diff --git a/contrib/kyua/model/exceptions.cpp b/contrib/kyua/model/exceptions.cpp new file mode 100644 index 000000000000..dc511a2b7e8f --- /dev/null +++ b/contrib/kyua/model/exceptions.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 "model/exceptions.hpp" + +#include "utils/format/macros.hpp" + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +model::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +model::error::~error(void) throw() +{ +} + + +/// Constructs a new format_error. +/// +/// \param message The plain-text error message. +model::format_error::format_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +model::format_error::~format_error(void) throw() +{ +} + + +/// Constructs a new not_found_error. +/// +/// \param message The plain-text error message. +model::not_found_error::not_found_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +model::not_found_error::~not_found_error(void) throw() +{ +} diff --git a/contrib/kyua/model/exceptions.hpp b/contrib/kyua/model/exceptions.hpp new file mode 100644 index 000000000000..ff4970fc37d7 --- /dev/null +++ b/contrib/kyua/model/exceptions.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 model/exceptions.hpp +/// Exception types raised by the model module. +/// +/// There is no model/exceptions_fwd.hpp counterpart because this file is +/// inteded to be used only from within .cpp files to either raise or +/// handle raised exceptions, neither of which are possible with just +/// forward declarations. + +#if !defined(MODEL_EXCEPTIONS_HPP) +#define MODEL_EXCEPTIONS_HPP + +#include + +namespace model { + + +/// Base exception for model errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error while parsing external data. +class format_error : public error { +public: + explicit format_error(const std::string&); + virtual ~format_error(void) throw(); +}; + + +/// A requested element could not be found. +class not_found_error : public error { +public: + explicit not_found_error(const std::string&); + virtual ~not_found_error(void) throw(); +}; + + +} // namespace model + +#endif // !defined(MODEL_EXCEPTIONS_HPP) diff --git a/contrib/kyua/model/exceptions_test.cpp b/contrib/kyua/model/exceptions_test.cpp new file mode 100644 index 000000000000..e9c17c0cc19a --- /dev/null +++ b/contrib/kyua/model/exceptions_test.cpp @@ -0,0 +1,65 @@ +// 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 "model/exceptions.hpp" + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const model::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_error); +ATF_TEST_CASE_BODY(format_error) +{ + const model::format_error e("Some other text"); + ATF_REQUIRE(std::strcmp("Some other text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(not_found_error); +ATF_TEST_CASE_BODY(not_found_error) +{ + const model::not_found_error e("Missing foo"); + ATF_REQUIRE(std::strcmp("Missing foo", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, format_error); + ATF_ADD_TEST_CASE(tcs, not_found_error); +} diff --git a/contrib/kyua/model/metadata.cpp b/contrib/kyua/model/metadata.cpp new file mode 100644 index 000000000000..d27e3237dcf2 --- /dev/null +++ b/contrib/kyua/model/metadata.cpp @@ -0,0 +1,1068 @@ +// 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 "model/metadata.hpp" + +#include + +#include "model/exceptions.hpp" +#include "model/types.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/config/nodes.ipp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.hpp" +#include "utils/units.hpp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace text = utils::text; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Global instance of defaults. +/// +/// This exists so that the getters in metadata can return references instead +/// of object copies. Use get_defaults() to query. +static optional< config::tree > defaults; + + +/// A leaf node that holds a bytes quantity. +class bytes_node : public config::native_leaf_node< units::bytes > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< bytes_node > new_node(new bytes_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Pushes the node's value onto the Lua stack. + void + push_lua(lutok::state& /* state */) const + { + UNREACHABLE; + } + + /// Sets the value of the node from an entry in the Lua stack. + void + set_lua(lutok::state& /* state */, const int /* index */) + { + UNREACHABLE; + } +}; + + +/// A leaf node that holds a time delta. +class delta_node : public config::typed_leaf_node< datetime::delta > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< delta_node > new_node(new delta_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// 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. + void + set_string(const std::string& raw_value) + { + unsigned int seconds; + try { + seconds = text::to_type< unsigned int >(raw_value); + } catch (const text::error& e) { + throw config::value_error(F("Invalid time delta %s") % raw_value); + } + set(datetime::delta(seconds, 0)); + } + + /// 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. + std::string + to_string(void) const + { + return F("%s") % value().seconds; + } + + /// Pushes the node's value onto the Lua stack. + void + push_lua(lutok::state& /* state */) const + { + UNREACHABLE; + } + + /// Sets the value of the node from an entry in the Lua stack. + void + set_lua(lutok::state& /* state */, const int /* index */) + { + UNREACHABLE; + } +}; + + +/// A leaf node that holds a "required user" property. +/// +/// This node is just a string, but it provides validation of the only allowed +/// values. +class user_node : public config::string_node { + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< user_node > new_node(new user_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Checks a given user textual representation for validity. + /// + /// \param user The value to validate. + /// + /// \throw config::value_error If the value is not valid. + void + validate(const value_type& user) const + { + if (!user.empty() && user != "root" && user != "unprivileged") + throw config::value_error("Invalid required user value"); + } +}; + + +/// A leaf node that holds a set of paths. +/// +/// This node type is used to represent the value of the required files and +/// required programs, for example, and these do not allow relative paths. We +/// check this here. +class paths_set_node : public config::base_set_node< fs::path > { + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< paths_set_node > new_node(new paths_set_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Converts a single path to the native type. + /// + /// \param raw_value The value to parse. + /// + /// \return The parsed value. + /// + /// \throw config::value_error If the value is invalid. + fs::path + parse_one(const std::string& raw_value) const + { + try { + return fs::path(raw_value); + } catch (const fs::error& e) { + throw config::value_error(e.what()); + } + } + + /// Checks a collection of paths for validity. + /// + /// \param paths The value to validate. + /// + /// \throw config::value_error If the value is not valid. + void + validate(const value_type& paths) const + { + for (value_type::const_iterator iter = paths.begin(); + iter != paths.end(); ++iter) { + const fs::path& path = *iter; + if (!path.is_absolute() && path.ncomponents() > 1) + throw config::value_error(F("Relative path '%s' not allowed") % + *iter); + } + } +}; + + +/// Initializes a tree to hold test case requirements. +/// +/// \param [in,out] tree The tree to initialize. +static void +init_tree(config::tree& tree) +{ + tree.define< config::strings_set_node >("allowed_architectures"); + tree.define< config::strings_set_node >("allowed_platforms"); + tree.define_dynamic("custom"); + tree.define< config::string_node >("description"); + tree.define< config::bool_node >("has_cleanup"); + tree.define< config::bool_node >("is_exclusive"); + tree.define< config::strings_set_node >("required_configs"); + tree.define< bytes_node >("required_disk_space"); + tree.define< paths_set_node >("required_files"); + tree.define< bytes_node >("required_memory"); + tree.define< paths_set_node >("required_programs"); + tree.define< user_node >("required_user"); + tree.define< delta_node >("timeout"); +} + + +/// Sets default values on a tree object. +/// +/// \param [in,out] tree The tree to configure. +static void +set_defaults(config::tree& tree) +{ + tree.set< config::strings_set_node >("allowed_architectures", + model::strings_set()); + tree.set< config::strings_set_node >("allowed_platforms", + model::strings_set()); + tree.set< config::string_node >("description", ""); + tree.set< config::bool_node >("has_cleanup", false); + tree.set< config::bool_node >("is_exclusive", false); + tree.set< config::strings_set_node >("required_configs", + model::strings_set()); + tree.set< bytes_node >("required_disk_space", units::bytes(0)); + tree.set< paths_set_node >("required_files", model::paths_set()); + tree.set< bytes_node >("required_memory", units::bytes(0)); + tree.set< paths_set_node >("required_programs", model::paths_set()); + tree.set< user_node >("required_user", ""); + // TODO(jmmv): We shouldn't be setting a default timeout like this. See + // Issue 5 for details. + tree.set< delta_node >("timeout", datetime::delta(300, 0)); +} + + +/// Queries the global defaults tree object with lazy initialization. +/// +/// \return A metadata tree. This object is statically allocated so it is +/// acceptable to obtain references to it and its members. +const config::tree& +get_defaults(void) +{ + if (!defaults) { + config::tree props; + init_tree(props); + set_defaults(props); + defaults = props; + } + return defaults.get(); +} + + +/// Looks up a value in a tree with error rewriting. +/// +/// \tparam NodeType The type of the node. +/// \param tree The tree in which to insert the value. +/// \param key The key to set. +/// +/// \return A read-write reference to the value in the node. +/// +/// \throw model::error If the key is not known or if the value is not valid. +template< class NodeType > +typename NodeType::value_type& +lookup_rw(config::tree& tree, const std::string& key) +{ + try { + return tree.lookup_rw< NodeType >(key); + } catch (const config::unknown_key_error& e) { + throw model::error(F("Unknown metadata property %s") % key); + } catch (const config::value_error& e) { + throw model::error(F("Invalid value for metadata property %s: %s") % + key % e.what()); + } +} + + +/// Sets a value in a tree with error rewriting. +/// +/// \tparam NodeType The type of the node. +/// \param tree The tree in which to insert the value. +/// \param key The key to set. +/// \param value The value to set the node to. +/// +/// \throw model::error If the key is not known or if the value is not valid. +template< class NodeType > +void +set(config::tree& tree, const std::string& key, + const typename NodeType::value_type& value) +{ + try { + tree.set< NodeType >(key, value); + } catch (const config::unknown_key_error& e) { + throw model::error(F("Unknown metadata property %s") % key); + } catch (const config::value_error& e) { + throw model::error(F("Invalid value for metadata property %s: %s") % + key % e.what()); + } +} + + +} // anonymous namespace + + +/// Internal implementation of the metadata class. +struct model::metadata::impl : utils::noncopyable { + /// Metadata properties. + config::tree props; + + /// Constructor. + /// + /// \param props_ Metadata properties of the test. + impl(const utils::config::tree& props_) : + props(props_) + { + } + + /// Equality comparator. + /// + /// \param other The other object to compare this one to. + /// + /// \return True if this object and other are equal; false otherwise. + bool + operator==(const impl& other) const + { + return (get_defaults().combine(props) == + get_defaults().combine(other.props)); + } +}; + + +/// Constructor. +/// +/// \param props Metadata properties of the test. +model::metadata::metadata(const utils::config::tree& props) : + _pimpl(new impl(props)) +{ +} + + +/// Destructor. +model::metadata::~metadata(void) +{ +} + + +/// Applies a set of overrides to this metadata object. +/// +/// \param overrides The overrides to apply. Any values explicitly set in this +/// other object will override any possible values set in this object. +/// +/// \return A new metadata object with the combination. +model::metadata +model::metadata::apply_overrides(const metadata& overrides) const +{ + return metadata(_pimpl->props.combine(overrides._pimpl->props)); +} + + +/// Returns the architectures allowed by the test. +/// +/// \return Set of architectures, or empty if this does not apply. +const model::strings_set& +model::metadata::allowed_architectures(void) const +{ + if (_pimpl->props.is_set("allowed_architectures")) { + return _pimpl->props.lookup< config::strings_set_node >( + "allowed_architectures"); + } else { + return get_defaults().lookup< config::strings_set_node >( + "allowed_architectures"); + } +} + + +/// Returns the platforms allowed by the test. +/// +/// \return Set of platforms, or empty if this does not apply. +const model::strings_set& +model::metadata::allowed_platforms(void) const +{ + if (_pimpl->props.is_set("allowed_platforms")) { + return _pimpl->props.lookup< config::strings_set_node >( + "allowed_platforms"); + } else { + return get_defaults().lookup< config::strings_set_node >( + "allowed_platforms"); + } +} + + +/// Returns all the user-defined metadata properties. +/// +/// \return A key/value map of properties. +model::properties_map +model::metadata::custom(void) const +{ + return _pimpl->props.all_properties("custom", true); +} + + +/// Returns the description of the test. +/// +/// \return Textual description; may be empty. +const std::string& +model::metadata::description(void) const +{ + if (_pimpl->props.is_set("description")) { + return _pimpl->props.lookup< config::string_node >("description"); + } else { + return get_defaults().lookup< config::string_node >("description"); + } +} + + +/// Returns whether the test has a cleanup part or not. +/// +/// \return True if there is a cleanup part; false otherwise. +bool +model::metadata::has_cleanup(void) const +{ + if (_pimpl->props.is_set("has_cleanup")) { + return _pimpl->props.lookup< config::bool_node >("has_cleanup"); + } else { + return get_defaults().lookup< config::bool_node >("has_cleanup"); + } +} + + +/// Returns whether the test is exclusive or not. +/// +/// \return True if the test has to be run on its own, not concurrently with any +/// other tests; false otherwise. +bool +model::metadata::is_exclusive(void) const +{ + if (_pimpl->props.is_set("is_exclusive")) { + return _pimpl->props.lookup< config::bool_node >("is_exclusive"); + } else { + return get_defaults().lookup< config::bool_node >("is_exclusive"); + } +} + + +/// Returns the list of configuration variables needed by the test. +/// +/// \return Set of configuration variables. +const model::strings_set& +model::metadata::required_configs(void) const +{ + if (_pimpl->props.is_set("required_configs")) { + return _pimpl->props.lookup< config::strings_set_node >( + "required_configs"); + } else { + return get_defaults().lookup< config::strings_set_node >( + "required_configs"); + } +} + + +/// Returns the amount of free disk space required by the test. +/// +/// \return Number of bytes, or 0 if this does not apply. +const units::bytes& +model::metadata::required_disk_space(void) const +{ + if (_pimpl->props.is_set("required_disk_space")) { + return _pimpl->props.lookup< bytes_node >("required_disk_space"); + } else { + return get_defaults().lookup< bytes_node >("required_disk_space"); + } +} + + +/// Returns the list of files needed by the test. +/// +/// \return Set of paths. +const model::paths_set& +model::metadata::required_files(void) const +{ + if (_pimpl->props.is_set("required_files")) { + return _pimpl->props.lookup< paths_set_node >("required_files"); + } else { + return get_defaults().lookup< paths_set_node >("required_files"); + } +} + + +/// Returns the amount of memory required by the test. +/// +/// \return Number of bytes, or 0 if this does not apply. +const units::bytes& +model::metadata::required_memory(void) const +{ + if (_pimpl->props.is_set("required_memory")) { + return _pimpl->props.lookup< bytes_node >("required_memory"); + } else { + return get_defaults().lookup< bytes_node >("required_memory"); + } +} + + +/// Returns the list of programs needed by the test. +/// +/// \return Set of paths. +const model::paths_set& +model::metadata::required_programs(void) const +{ + if (_pimpl->props.is_set("required_programs")) { + return _pimpl->props.lookup< paths_set_node >("required_programs"); + } else { + return get_defaults().lookup< paths_set_node >("required_programs"); + } +} + + +/// Returns the user required by the test. +/// +/// \return One of unprivileged, root or empty. +const std::string& +model::metadata::required_user(void) const +{ + if (_pimpl->props.is_set("required_user")) { + return _pimpl->props.lookup< user_node >("required_user"); + } else { + return get_defaults().lookup< user_node >("required_user"); + } +} + + +/// Returns the timeout of the test. +/// +/// \return A time delta; should be compared to default_timeout to see if it has +/// been overriden. +const datetime::delta& +model::metadata::timeout(void) const +{ + if (_pimpl->props.is_set("timeout")) { + return _pimpl->props.lookup< delta_node >("timeout"); + } else { + return get_defaults().lookup< delta_node >("timeout"); + } +} + + +/// Externalizes the metadata to a set of key/value textual pairs. +/// +/// \return A key/value representation of the metadata. +model::properties_map +model::metadata::to_properties(void) const +{ + const config::tree fully_specified = get_defaults().combine(_pimpl->props); + return fully_specified.all_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 +model::metadata::operator==(const metadata& other) const +{ + return _pimpl == other._pimpl || *_pimpl == *other._pimpl; +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::metadata::operator!=(const metadata& other) const +{ + return !(*this == other); +} + + +/// 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& +model::operator<<(std::ostream& output, const metadata& object) +{ + output << "metadata{"; + + bool first = true; + const model::properties_map props = object.to_properties(); + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + if (!first) + output << ", "; + output << F("%s=%s") % (*iter).first % + text::quote((*iter).second, '\''); + first = false; + } + + output << "}"; + return output; +} + + +/// Internal implementation of the metadata_builder class. +struct model::metadata_builder::impl : utils::noncopyable { + /// Collection of requirements. + config::tree props; + + /// Whether we have created a metadata object or not. + bool built; + + /// Constructor. + impl(void) : + built(false) + { + init_tree(props); + } + + /// Constructor. + /// + /// \param base The base model to construct a copy from. + impl(const model::metadata& base) : + props(base._pimpl->props.deep_copy()), + built(false) + { + } +}; + + +/// Constructor. +model::metadata_builder::metadata_builder(void) : + _pimpl(new impl()) +{ +} + + +/// Constructor. +model::metadata_builder::metadata_builder(const model::metadata& base) : + _pimpl(new impl(base)) +{ +} + + +/// Destructor. +model::metadata_builder::~metadata_builder(void) +{ +} + + +/// Accumulates an additional allowed architecture. +/// +/// \param arch The architecture. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_allowed_architecture(const std::string& arch) +{ + if (!_pimpl->props.is_set("allowed_architectures")) { + _pimpl->props.set< config::strings_set_node >( + "allowed_architectures", + get_defaults().lookup< config::strings_set_node >( + "allowed_architectures")); + } + lookup_rw< config::strings_set_node >( + _pimpl->props, "allowed_architectures").insert(arch); + return *this; +} + + +/// Accumulates an additional allowed platform. +/// +/// \param platform The platform. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_allowed_platform(const std::string& platform) +{ + if (!_pimpl->props.is_set("allowed_platforms")) { + _pimpl->props.set< config::strings_set_node >( + "allowed_platforms", + get_defaults().lookup< config::strings_set_node >( + "allowed_platforms")); + } + lookup_rw< config::strings_set_node >( + _pimpl->props, "allowed_platforms").insert(platform); + return *this; +} + + +/// Accumulates a single user-defined property. +/// +/// \param key Name of the property to define. +/// \param value Value of the property. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_custom(const std::string& key, + const std::string& value) +{ + _pimpl->props.set_string(F("custom.%s") % key, value); + return *this; +} + + +/// Accumulates an additional required configuration variable. +/// +/// \param var The name of the configuration variable. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_required_config(const std::string& var) +{ + if (!_pimpl->props.is_set("required_configs")) { + _pimpl->props.set< config::strings_set_node >( + "required_configs", + get_defaults().lookup< config::strings_set_node >( + "required_configs")); + } + lookup_rw< config::strings_set_node >( + _pimpl->props, "required_configs").insert(var); + return *this; +} + + +/// Accumulates an additional required file. +/// +/// \param path The path to the file. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_required_file(const fs::path& path) +{ + if (!_pimpl->props.is_set("required_files")) { + _pimpl->props.set< paths_set_node >( + "required_files", + get_defaults().lookup< paths_set_node >("required_files")); + } + lookup_rw< paths_set_node >(_pimpl->props, "required_files").insert(path); + return *this; +} + + +/// Accumulates an additional required program. +/// +/// \param path The path to the program. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_required_program(const fs::path& path) +{ + if (!_pimpl->props.is_set("required_programs")) { + _pimpl->props.set< paths_set_node >( + "required_programs", + get_defaults().lookup< paths_set_node >("required_programs")); + } + lookup_rw< paths_set_node >(_pimpl->props, + "required_programs").insert(path); + return *this; +} + + +/// Sets the architectures allowed by the test. +/// +/// \param as Set of architectures. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_allowed_architectures( + const model::strings_set& as) +{ + set< config::strings_set_node >(_pimpl->props, "allowed_architectures", as); + return *this; +} + + +/// Sets the platforms allowed by the test. +/// +/// \return ps Set of platforms. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_allowed_platforms(const model::strings_set& ps) +{ + set< config::strings_set_node >(_pimpl->props, "allowed_platforms", ps); + return *this; +} + + +/// Sets the user-defined properties. +/// +/// \param props The custom properties to set. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_custom(const model::properties_map& props) +{ + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) + _pimpl->props.set_string(F("custom.%s") % (*iter).first, + (*iter).second); + return *this; +} + + +/// Sets the description of the test. +/// +/// \param description Textual description of the test. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_description(const std::string& description) +{ + set< config::string_node >(_pimpl->props, "description", description); + return *this; +} + + +/// Sets whether the test has a cleanup part or not. +/// +/// \param cleanup True if the test has a cleanup part; false otherwise. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_has_cleanup(const bool cleanup) +{ + set< config::bool_node >(_pimpl->props, "has_cleanup", cleanup); + return *this; +} + + +/// Sets whether the test is exclusive or not. +/// +/// \param exclusive True if the test is exclusive; false otherwise. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_is_exclusive(const bool exclusive) +{ + set< config::bool_node >(_pimpl->props, "is_exclusive", exclusive); + return *this; +} + + +/// Sets the list of configuration variables needed by the test. +/// +/// \param vars Set of configuration variables. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_configs(const model::strings_set& vars) +{ + set< config::strings_set_node >(_pimpl->props, "required_configs", vars); + return *this; +} + + +/// Sets the amount of free disk space required by the test. +/// +/// \param bytes Number of bytes. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_disk_space(const units::bytes& bytes) +{ + set< bytes_node >(_pimpl->props, "required_disk_space", bytes); + return *this; +} + + +/// Sets the list of files needed by the test. +/// +/// \param files Set of paths. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_files(const model::paths_set& files) +{ + set< paths_set_node >(_pimpl->props, "required_files", files); + return *this; +} + + +/// Sets the amount of memory required by the test. +/// +/// \param bytes Number of bytes. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_memory(const units::bytes& bytes) +{ + set< bytes_node >(_pimpl->props, "required_memory", bytes); + return *this; +} + + +/// Sets the list of programs needed by the test. +/// +/// \param progs Set of paths. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_programs(const model::paths_set& progs) +{ + set< paths_set_node >(_pimpl->props, "required_programs", progs); + return *this; +} + + +/// Sets the user required by the test. +/// +/// \param user One of unprivileged, root or empty. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_user(const std::string& user) +{ + set< user_node >(_pimpl->props, "required_user", user); + return *this; +} + + +/// Sets a metadata property by name from its textual representation. +/// +/// \param key The property to set. +/// \param value The value to set the property to. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid or the key does not exist. +model::metadata_builder& +model::metadata_builder::set_string(const std::string& key, + const std::string& value) +{ + try { + _pimpl->props.set_string(key, value); + } catch (const config::unknown_key_error& e) { + throw model::format_error(F("Unknown metadata property %s") % key); + } catch (const config::value_error& e) { + throw model::format_error( + F("Invalid value for metadata property %s: %s") % key % e.what()); + } + return *this; +} + + +/// Sets the timeout of the test. +/// +/// \param timeout The timeout to set. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_timeout(const datetime::delta& timeout) +{ + set< delta_node >(_pimpl->props, "timeout", timeout); + return *this; +} + + +/// Creates a new metadata object. +/// +/// \pre This has not yet been called. We only support calling this function +/// once due to the way the internal tree works: we pass around references, not +/// deep copies, so if we allowed a second build, we'd encourage reusing the +/// same builder to construct different metadata objects, and this could have +/// unintended consequences. +/// +/// \return The constructed metadata object. +model::metadata +model::metadata_builder::build(void) const +{ + PRE(!_pimpl->built); + _pimpl->built = true; + + return metadata(_pimpl->props); +} diff --git a/contrib/kyua/model/metadata.hpp b/contrib/kyua/model/metadata.hpp new file mode 100644 index 000000000000..c7dd4519f122 --- /dev/null +++ b/contrib/kyua/model/metadata.hpp @@ -0,0 +1,130 @@ +// 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 model/metadata.hpp +/// Definition of the "test metadata" concept. + +#if !defined(MODEL_METADATA_HPP) +#define MODEL_METADATA_HPP + +#include "model/metadata_fwd.hpp" + +#include +#include +#include + +#include "model/types.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/units_fwd.hpp" + +namespace model { + + +/// Collection of metadata properties of a test. +class metadata { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class metadata_builder; + +public: + metadata(const utils::config::tree&); + ~metadata(void); + + metadata apply_overrides(const metadata&) const; + + const strings_set& allowed_architectures(void) const; + const strings_set& allowed_platforms(void) const; + model::properties_map custom(void) const; + const std::string& description(void) const; + bool has_cleanup(void) const; + bool is_exclusive(void) const; + const strings_set& required_configs(void) const; + const utils::units::bytes& required_disk_space(void) const; + const paths_set& required_files(void) const; + const utils::units::bytes& required_memory(void) const; + const paths_set& required_programs(void) const; + const std::string& required_user(void) const; + const utils::datetime::delta& timeout(void) const; + + model::properties_map to_properties(void) const; + + bool operator==(const metadata&) const; + bool operator!=(const metadata&) const; +}; + + +std::ostream& operator<<(std::ostream&, const metadata&); + + +/// Builder for a metadata object. +class metadata_builder : utils::noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + metadata_builder(void); + explicit metadata_builder(const metadata&); + ~metadata_builder(void); + + metadata_builder& add_allowed_architecture(const std::string&); + metadata_builder& add_allowed_platform(const std::string&); + metadata_builder& add_custom(const std::string&, const std::string&); + metadata_builder& add_required_config(const std::string&); + metadata_builder& add_required_file(const utils::fs::path&); + metadata_builder& add_required_program(const utils::fs::path&); + + metadata_builder& set_allowed_architectures(const strings_set&); + metadata_builder& set_allowed_platforms(const strings_set&); + metadata_builder& set_custom(const model::properties_map&); + metadata_builder& set_description(const std::string&); + metadata_builder& set_has_cleanup(const bool); + metadata_builder& set_is_exclusive(const bool); + metadata_builder& set_required_configs(const strings_set&); + metadata_builder& set_required_disk_space(const utils::units::bytes&); + metadata_builder& set_required_files(const paths_set&); + metadata_builder& set_required_memory(const utils::units::bytes&); + metadata_builder& set_required_programs(const paths_set&); + metadata_builder& set_required_user(const std::string&); + metadata_builder& set_string(const std::string&, const std::string&); + metadata_builder& set_timeout(const utils::datetime::delta&); + + metadata build(void) const; +}; + + +} // namespace model + +#endif // !defined(MODEL_METADATA_HPP) diff --git a/contrib/kyua/model/metadata_fwd.hpp b/contrib/kyua/model/metadata_fwd.hpp new file mode 100644 index 000000000000..8a8c5c09d77b --- /dev/null +++ b/contrib/kyua/model/metadata_fwd.hpp @@ -0,0 +1,44 @@ +// 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 model/metadata_fwd.hpp +/// Forward declarations for model/metadata.hpp + +#if !defined(MODEL_METADATA_FWD_HPP) +#define MODEL_METADATA_FWD_HPP + +namespace model { + + +class metadata; +class metadata_builder; + + +} // namespace model + +#endif // !defined(MODEL_METADATA_FWD_HPP) diff --git a/contrib/kyua/model/metadata_test.cpp b/contrib/kyua/model/metadata_test.cpp new file mode 100644 index 000000000000..7b22653ec1a2 --- /dev/null +++ b/contrib/kyua/model/metadata_test.cpp @@ -0,0 +1,461 @@ +// 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 "model/metadata.hpp" + +#include + +#include + +#include "model/types.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(defaults); +ATF_TEST_CASE_BODY(defaults) +{ + const model::metadata md = model::metadata_builder().build(); + ATF_REQUIRE(md.allowed_architectures().empty()); + ATF_REQUIRE(md.allowed_platforms().empty()); + ATF_REQUIRE(md.allowed_platforms().empty()); + ATF_REQUIRE(md.custom().empty()); + ATF_REQUIRE(md.description().empty()); + ATF_REQUIRE(!md.has_cleanup()); + ATF_REQUIRE(!md.is_exclusive()); + ATF_REQUIRE(md.required_configs().empty()); + ATF_REQUIRE_EQ(units::bytes(0), md.required_disk_space()); + ATF_REQUIRE(md.required_files().empty()); + ATF_REQUIRE_EQ(units::bytes(0), md.required_memory()); + ATF_REQUIRE(md.required_programs().empty()); + ATF_REQUIRE(md.required_user().empty()); + ATF_REQUIRE(datetime::delta(300, 0) == md.timeout()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(add); +ATF_TEST_CASE_BODY(add) +{ + model::strings_set architectures; + architectures.insert("1-architecture"); + architectures.insert("2-architecture"); + + model::strings_set platforms; + platforms.insert("1-platform"); + platforms.insert("2-platform"); + + model::properties_map custom; + custom["1-custom"] = "first"; + custom["2-custom"] = "second"; + + model::strings_set configs; + configs.insert("1-config"); + configs.insert("2-config"); + + model::paths_set files; + files.insert(fs::path("1-file")); + files.insert(fs::path("2-file")); + + model::paths_set programs; + programs.insert(fs::path("1-program")); + programs.insert(fs::path("2-program")); + + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .add_custom("1-custom", "first") + .add_custom("2-custom", "second") + .add_required_config("1-config") + .add_required_file(fs::path("1-file")) + .add_required_program(fs::path("1-program")) + .add_allowed_architecture("2-architecture") + .add_allowed_platform("2-platform") + .add_required_config("2-config") + .add_required_file(fs::path("2-file")) + .add_required_program(fs::path("2-program")) + .build(); + + ATF_REQUIRE(architectures == md.allowed_architectures()); + ATF_REQUIRE(platforms == md.allowed_platforms()); + ATF_REQUIRE(custom == md.custom()); + ATF_REQUIRE(configs == md.required_configs()); + ATF_REQUIRE(files == md.required_files()); + ATF_REQUIRE(programs == md.required_programs()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(copy); +ATF_TEST_CASE_BODY(copy) +{ + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .build(); + + const model::metadata md2 = model::metadata_builder(md1) + .add_allowed_architecture("2-architecture") + .build(); + + ATF_REQUIRE_EQ(1, md1.allowed_architectures().size()); + ATF_REQUIRE_EQ(2, md2.allowed_architectures().size()); + ATF_REQUIRE_EQ(1, md1.allowed_platforms().size()); + ATF_REQUIRE_EQ(1, md2.allowed_platforms().size()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(apply_overrides); +ATF_TEST_CASE_BODY(apply_overrides) +{ + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .set_description("Explicit description") + .build(); + + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("2-architecture") + .set_description("") + .set_timeout(datetime::delta(500, 0)) + .build(); + + const model::metadata merge_1_2 = model::metadata_builder() + .add_allowed_architecture("2-architecture") + .add_allowed_platform("1-platform") + .set_description("") + .set_timeout(datetime::delta(500, 0)) + .build(); + ATF_REQUIRE_EQ(merge_1_2, md1.apply_overrides(md2)); + + const model::metadata merge_2_1 = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .set_description("Explicit description") + .set_timeout(datetime::delta(500, 0)) + .build(); + ATF_REQUIRE_EQ(merge_2_1, md2.apply_overrides(md1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_setters); +ATF_TEST_CASE_BODY(override_all_with_setters) +{ + model::strings_set architectures; + architectures.insert("the-architecture"); + + model::strings_set platforms; + platforms.insert("the-platforms"); + + model::properties_map custom; + custom["first"] = "hello"; + custom["second"] = "bye"; + + const std::string description = "Some long text"; + + model::strings_set configs; + configs.insert("the-configs"); + + model::paths_set files; + files.insert(fs::path("the-files")); + + const units::bytes disk_space(6789); + + const units::bytes memory(12345); + + model::paths_set programs; + programs.insert(fs::path("the-programs")); + + const std::string user = "root"; + + const datetime::delta timeout(123, 0); + + const model::metadata md = model::metadata_builder() + .set_allowed_architectures(architectures) + .set_allowed_platforms(platforms) + .set_custom(custom) + .set_description(description) + .set_has_cleanup(true) + .set_is_exclusive(true) + .set_required_configs(configs) + .set_required_disk_space(disk_space) + .set_required_files(files) + .set_required_memory(memory) + .set_required_programs(programs) + .set_required_user(user) + .set_timeout(timeout) + .build(); + + ATF_REQUIRE(architectures == md.allowed_architectures()); + ATF_REQUIRE(platforms == md.allowed_platforms()); + ATF_REQUIRE(custom == md.custom()); + ATF_REQUIRE_EQ(description, md.description()); + ATF_REQUIRE(md.has_cleanup()); + ATF_REQUIRE(md.is_exclusive()); + ATF_REQUIRE(configs == md.required_configs()); + ATF_REQUIRE_EQ(disk_space, md.required_disk_space()); + ATF_REQUIRE(files == md.required_files()); + ATF_REQUIRE_EQ(memory, md.required_memory()); + ATF_REQUIRE(programs == md.required_programs()); + ATF_REQUIRE_EQ(user, md.required_user()); + ATF_REQUIRE(timeout == md.timeout()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_set_string); +ATF_TEST_CASE_BODY(override_all_with_set_string) +{ + model::strings_set architectures; + architectures.insert("a1"); + architectures.insert("a2"); + + model::strings_set platforms; + platforms.insert("p1"); + platforms.insert("p2"); + + model::properties_map custom; + custom["user-defined"] = "the-value"; + + const std::string description = "Another long text"; + + model::strings_set configs; + configs.insert("config-var"); + + model::paths_set files; + files.insert(fs::path("plain")); + files.insert(fs::path("/absolute/path")); + + const units::bytes disk_space( + static_cast< uint64_t >(16) * 1024 * 1024 * 1024); + + const units::bytes memory(1024 * 1024); + + model::paths_set programs; + programs.insert(fs::path("program")); + programs.insert(fs::path("/absolute/prog")); + + const std::string user = "unprivileged"; + + const datetime::delta timeout(45, 0); + + const model::metadata md = model::metadata_builder() + .set_string("allowed_architectures", "a1 a2") + .set_string("allowed_platforms", "p1 p2") + .set_string("custom.user-defined", "the-value") + .set_string("description", "Another long text") + .set_string("has_cleanup", "true") + .set_string("is_exclusive", "true") + .set_string("required_configs", "config-var") + .set_string("required_disk_space", "16G") + .set_string("required_files", "plain /absolute/path") + .set_string("required_memory", "1M") + .set_string("required_programs", "program /absolute/prog") + .set_string("required_user", "unprivileged") + .set_string("timeout", "45") + .build(); + + ATF_REQUIRE(architectures == md.allowed_architectures()); + ATF_REQUIRE(platforms == md.allowed_platforms()); + ATF_REQUIRE(custom == md.custom()); + ATF_REQUIRE_EQ(description, md.description()); + ATF_REQUIRE(md.has_cleanup()); + ATF_REQUIRE(md.is_exclusive()); + ATF_REQUIRE(configs == md.required_configs()); + ATF_REQUIRE_EQ(disk_space, md.required_disk_space()); + ATF_REQUIRE(files == md.required_files()); + ATF_REQUIRE_EQ(memory, md.required_memory()); + ATF_REQUIRE(programs == md.required_programs()); + ATF_REQUIRE_EQ(user, md.required_user()); + ATF_REQUIRE(timeout == md.timeout()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_properties); +ATF_TEST_CASE_BODY(to_properties) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("abc") + .add_required_file(fs::path("foo")) + .add_required_file(fs::path("bar")) + .set_required_memory(units::bytes(1024)) + .add_custom("foo", "bar") + .build(); + + model::properties_map props; + props["allowed_architectures"] = "abc"; + props["allowed_platforms"] = ""; + props["custom.foo"] = "bar"; + props["description"] = ""; + props["has_cleanup"] = "false"; + props["is_exclusive"] = "false"; + props["required_configs"] = ""; + props["required_disk_space"] = "0"; + props["required_files"] = "bar foo"; + props["required_memory"] = "1.00K"; + props["required_programs"] = ""; + props["required_user"] = ""; + props["timeout"] = "300"; + ATF_REQUIRE_EQ(props, md.to_properties()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__empty); +ATF_TEST_CASE_BODY(operators_eq_and_ne__empty) +{ + const model::metadata md1 = model::metadata_builder().build(); + const model::metadata md2 = model::metadata_builder().build(); + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__copy) +{ + const model::metadata md1 = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + const model::metadata md2 = md1; + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal); +ATF_TEST_CASE_BODY(operators_eq_and_ne__equal) +{ + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("a") + .add_allowed_architecture("b") + .add_custom("foo", "bar") + .build(); + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("b") + .add_allowed_architecture("a") + .add_custom("foo", "bar") + .build(); + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal_overriden_defaults); +ATF_TEST_CASE_BODY(operators_eq_and_ne__equal_overriden_defaults) +{ + const model::metadata defaults = model::metadata_builder().build(); + + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("a") + .build(); + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("a") + .set_timeout(defaults.timeout()) + .build(); + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__different); +ATF_TEST_CASE_BODY(operators_eq_and_ne__different) +{ + const model::metadata md1 = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + const model::metadata md2 = model::metadata_builder() + .add_custom("foo", "bar") + .add_custom("baz", "foo bar") + .build(); + ATF_REQUIRE(!(md1 == md2)); + ATF_REQUIRE( md1 != md2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__defaults); +ATF_TEST_CASE_BODY(output__defaults) +{ + std::ostringstream str; + str << model::metadata_builder().build(); + ATF_REQUIRE_EQ("metadata{allowed_architectures='', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', " + "required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__some_values); +ATF_TEST_CASE_BODY(output__some_values) +{ + std::ostringstream str; + str << model::metadata_builder() + .add_allowed_architecture("abc") + .add_required_file(fs::path("foo")) + .add_required_file(fs::path("bar")) + .set_is_exclusive(true) + .set_required_memory(units::bytes(1024)) + .build(); + ATF_REQUIRE_EQ( + "metadata{allowed_architectures='abc', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='true', " + "required_configs='', " + "required_disk_space='0', required_files='bar foo', " + "required_memory='1.00K', " + "required_programs='', required_user='', timeout='300'}", + str.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, defaults); + ATF_ADD_TEST_CASE(tcs, add); + ATF_ADD_TEST_CASE(tcs, copy); + ATF_ADD_TEST_CASE(tcs, apply_overrides); + ATF_ADD_TEST_CASE(tcs, override_all_with_setters); + ATF_ADD_TEST_CASE(tcs, override_all_with_set_string); + ATF_ADD_TEST_CASE(tcs, to_properties); + + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__empty); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal_overriden_defaults); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__different); + + ATF_ADD_TEST_CASE(tcs, output__defaults); + ATF_ADD_TEST_CASE(tcs, output__some_values); + + // TODO(jmmv): Add tests for error conditions (invalid keys and invalid + // values). +} diff --git a/contrib/kyua/model/test_case.cpp b/contrib/kyua/model/test_case.cpp new file mode 100644 index 000000000000..f5f6a979eed3 --- /dev/null +++ b/contrib/kyua/model/test_case.cpp @@ -0,0 +1,339 @@ +// 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 "model/test_case.hpp" + +#include "model/metadata.hpp" +#include "model/test_result.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Internal implementation for a test_case. +struct model::test_case::impl : utils::noncopyable { + /// Name of the test case; must be unique within the test program. + std::string name; + + /// Metadata of the container test program. + /// + /// Yes, this is a pointer. Yes, we do not own the object pointed to. + /// However, because this is only intended to point to the metadata object + /// of test programs _containing_ this test case, we can assume that the + /// referenced object will be alive for the lifetime of this test case. + const model::metadata* md_defaults; + + /// Test case metadata. + model::metadata md; + + /// Fake result to return instead of running the test case. + optional< model::test_result > fake_result; + + /// Constructor. + /// + /// \param name_ The name of the test case within the test program. + /// \param md_defaults_ Metadata of the container test program. + /// \param md_ Metadata of the test case. + /// \param fake_result_ Fake result to return instead of running the test + /// case. + impl(const std::string& name_, + const model::metadata* md_defaults_, + const model::metadata& md_, + const optional< model::test_result >& fake_result_) : + name(name_), + md_defaults(md_defaults_), + md(md_), + fake_result(fake_result_) + { + } + + /// Gets the test case metadata. + /// + /// This combines the test case's metadata with any possible test program + /// metadata, using the latter as defaults. + /// + /// \return The test case metadata. + model::metadata + get_metadata(void) const + { + if (md_defaults != NULL) { + return md_defaults->apply_overrides(md); + } else { + return md; + } + } + + /// Equality comparator. + /// + /// \param other The other object to compare this one to. + /// + /// \return True if this object and other are equal; false otherwise. + bool + operator==(const impl& other) const + { + return (name == other.name && + get_metadata() == other.get_metadata() && + fake_result == other.fake_result); + } +}; + + +/// Constructs a new test case from an already-built impl oject. +/// +/// \param pimpl_ The internal representation of the test case. +model::test_case::test_case(std::shared_ptr< impl > pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Constructs a new test case. +/// +/// \param name_ The name of the test case within the test program. +/// \param md_ Metadata of the test case. +model::test_case::test_case(const std::string& name_, + const model::metadata& md_) : + _pimpl(new impl(name_, NULL, md_, none)) +{ +} + + + +/// Constructs a new fake test case. +/// +/// A fake test case is a test case that is not really defined by the test +/// program. Such test cases have a name surrounded by '__' and, when executed, +/// they return a fixed, pre-recorded result. +/// +/// This is necessary for the cases where listing the test cases of a test +/// program fails. In this scenario, we generate a single test case within +/// the test program that unconditionally returns a failure. +/// +/// TODO(jmmv): Need to get rid of this. We should be able to report the +/// status of test programs independently of test cases, as some interfaces +/// don't know about the latter at all. +/// +/// \param name_ The name to give to this fake test case. This name has to be +/// prefixed and suffixed by '__' to clearly denote that this is internal. +/// \param description_ The description of the test case, if any. +/// \param test_result_ The fake result to return when this test case is run. +model::test_case::test_case( + const std::string& name_, + const std::string& description_, + const model::test_result& test_result_) : + _pimpl(new impl( + name_, + NULL, + model::metadata_builder().set_description(description_).build(), + utils::make_optional(test_result_))) +{ + PRE_MSG(name_.length() > 4 && name_.substr(0, 2) == "__" && + name_.substr(name_.length() - 2) == "__", + "Invalid fake name provided to fake test case"); +} + + +/// Destroys a test case. +model::test_case::~test_case(void) +{ +} + + +/// Constructs a new test case applying metadata defaults. +/// +/// This method is intended to be used by the container test program when +/// ownership of the test is given to it. At that point, the test case receives +/// the default metadata properties of the test program, not the global +/// defaults. +/// +/// \param defaults The metadata properties to use as defaults. The provided +/// object's lifetime MUST extend the lifetime of the test case. Because +/// this is only intended to point at the metadata of the test program +/// containing this test case, this assumption should hold. +/// +/// \return A new test case. +model::test_case +model::test_case::apply_metadata_defaults(const metadata* defaults) const +{ + return test_case(std::shared_ptr< impl >(new impl( + _pimpl->name, + defaults, + _pimpl->md, + _pimpl->fake_result))); +} + + +/// Gets the test case name. +/// +/// \return The test case name, relative to the test program. +const std::string& +model::test_case::name(void) const +{ + return _pimpl->name; +} + + +/// Gets the test case metadata. +/// +/// This combines the test case's metadata with any possible test program +/// metadata, using the latter as defaults. You should use this method in +/// generaland not get_raw_metadata(). +/// +/// \return The test case metadata. +model::metadata +model::test_case::get_metadata(void) const +{ + return _pimpl->get_metadata(); +} + + +/// Gets the original test case metadata without test program overrides. +/// +/// This method should be used for storage purposes as serialized test cases +/// should record exactly whatever the test case reported and not what the test +/// program may have provided. The final values will be reconstructed at load +/// time. +/// +/// \return The test case metadata. +const model::metadata& +model::test_case::get_raw_metadata(void) const +{ + return _pimpl->md; +} + + +/// Gets the fake result pre-stored for this test case. +/// +/// \return A fake result, or none if not defined. +optional< model::test_result > +model::test_case::fake_result(void) const +{ + return _pimpl->fake_result; +} + + +/// Equality comparator. +/// +/// \warning Because test cases reference their container test programs, and +/// test programs include test cases, we cannot perform a full comparison here: +/// otherwise, we'd enter an inifinte loop. Therefore, out of necessity, this +/// does NOT compare whether the container test programs of the affected test +/// cases are the same. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +model::test_case::operator==(const test_case& other) const +{ + return _pimpl == other._pimpl || *_pimpl == *other._pimpl; +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::test_case::operator!=(const test_case& other) const +{ + return !(*this == other); +} + + +/// 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& +model::operator<<(std::ostream& output, const test_case& object) +{ + output << F("test_case{name=%s, metadata=%s}") + % text::quote(object.name(), '\'') + % object.get_metadata(); + return output; +} + + +/// Adds an already-constructed test case. +/// +/// \param test_case The test case to add. +/// +/// \return A reference to this builder. +model::test_cases_map_builder& +model::test_cases_map_builder::add(const test_case& test_case) +{ + _test_cases.insert( + test_cases_map::value_type(test_case.name(), test_case)); + return *this; +} + + +/// Constructs and adds a new test case with default metadata. +/// +/// \param test_case_name The name of the test case to add. +/// +/// \return A reference to this builder. +model::test_cases_map_builder& +model::test_cases_map_builder::add(const std::string& test_case_name) +{ + return add(test_case(test_case_name, metadata_builder().build())); +} + + +/// Constructs and adds a new test case with explicit metadata. +/// +/// \param test_case_name The name of the test case to add. +/// \param metadata The metadata of the test case. +/// +/// \return A reference to this builder. +model::test_cases_map_builder& +model::test_cases_map_builder::add(const std::string& test_case_name, + const metadata& metadata) +{ + return add(test_case(test_case_name, metadata)); +} + + +/// Creates a new test_cases_map. +/// +/// \return The constructed test_cases_map. +model::test_cases_map +model::test_cases_map_builder::build(void) const +{ + return _test_cases; +} diff --git a/contrib/kyua/model/test_case.hpp b/contrib/kyua/model/test_case.hpp new file mode 100644 index 000000000000..3c6fe32c8e62 --- /dev/null +++ b/contrib/kyua/model/test_case.hpp @@ -0,0 +1,98 @@ +// 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 model/test_case.hpp +/// Definition of the "test case" concept. + +#if !defined(MODEL_TEST_CASE_HPP) +#define MODEL_TEST_CASE_HPP + +#include "model/test_case_fwd.hpp" + +#include +#include +#include + +#include "model/metadata_fwd.hpp" +#include "model/test_result_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional_fwd.hpp" + +namespace model { + + +/// Representation of a test case. +/// +/// Test cases, on their own, are useless. They only make sense in the context +/// of the container test program and as such this class should not be used +/// directly. +class test_case { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + test_case(std::shared_ptr< impl >); + +public: + test_case(const std::string&, const metadata&); + test_case(const std::string&, const std::string&, const test_result&); + ~test_case(void); + + test_case apply_metadata_defaults(const metadata*) const; + + const std::string& name(void) const; + metadata get_metadata(void) const; + const metadata& get_raw_metadata(void) const; + utils::optional< test_result > fake_result(void) const; + + bool operator==(const test_case&) const; + bool operator!=(const test_case&) const; +}; + + +/// Builder for a test_cases_map object. +class test_cases_map_builder : utils::noncopyable { + /// Accumulator for the map being built. + test_cases_map _test_cases; + +public: + test_cases_map_builder& add(const test_case&); + test_cases_map_builder& add(const std::string&); + test_cases_map_builder& add(const std::string&, const metadata&); + + test_cases_map build(void) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_case&); + + +} // namespace model + +#endif // !defined(MODEL_TEST_CASE_HPP) diff --git a/contrib/kyua/model/test_case_fwd.hpp b/contrib/kyua/model/test_case_fwd.hpp new file mode 100644 index 000000000000..72a40e513083 --- /dev/null +++ b/contrib/kyua/model/test_case_fwd.hpp @@ -0,0 +1,51 @@ +// 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 model/test_case_fwd.hpp +/// Forward declarations for model/test_case.hpp + +#if !defined(MODEL_TEST_CASE_FWD_HPP) +#define MODEL_TEST_CASE_FWD_HPP + +#include +#include + +namespace model { + + +class test_case; +class test_cases_map_builder; + + +/// Collection of test cases keyed by their name. +typedef std::map< std::string, model::test_case > test_cases_map; + + +} // namespace model + +#endif // !defined(MODEL_TEST_CASE_FWD_HPP) diff --git a/contrib/kyua/model/test_case_test.cpp b/contrib/kyua/model/test_case_test.cpp new file mode 100644 index 000000000000..1a55de0fab42 --- /dev/null +++ b/contrib/kyua/model/test_case_test.cpp @@ -0,0 +1,263 @@ +// 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 "model/test_case.hpp" + +#include + +#include + +#include "model/metadata.hpp" +#include "model/test_result.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__ctor_and_getters) +ATF_TEST_CASE_BODY(test_case__ctor_and_getters) +{ + const model::metadata md = model::metadata_builder() + .add_custom("first", "value") + .build(); + const model::test_case test_case("foo", md); + ATF_REQUIRE_EQ("foo", test_case.name()); + ATF_REQUIRE_EQ(md, test_case.get_metadata()); + ATF_REQUIRE_EQ(md, test_case.get_raw_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__fake_result) +ATF_TEST_CASE_BODY(test_case__fake_result) +{ + const model::test_result result(model::test_result_skipped, + "Some reason"); + const model::test_case test_case("__foo__", "Some description", result); + ATF_REQUIRE_EQ("__foo__", test_case.name()); + ATF_REQUIRE_EQ(result, test_case.fake_result().get()); + + const model::metadata exp_metadata = model::metadata_builder() + .set_description("Some description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, test_case.get_metadata()); + ATF_REQUIRE_EQ(exp_metadata, test_case.get_raw_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__real_test_case) +ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__real_test_case) +{ + const model::metadata overrides = model::metadata_builder() + .add_required_config("the-variable") + .set_description("The test case") + .build(); + const model::test_case base_test_case("foo", overrides); + + const model::metadata defaults = model::metadata_builder() + .set_description("Default description") + .set_timeout(datetime::delta(10, 0)) + .build(); + + const model::test_case test_case = base_test_case.apply_metadata_defaults( + &defaults); + + const model::metadata expected = model::metadata_builder() + .add_required_config("the-variable") + .set_description("The test case") + .set_timeout(datetime::delta(10, 0)) + .build(); + ATF_REQUIRE_EQ(expected, test_case.get_metadata()); + ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata()); + + // Ensure the original (although immutable) test case was not touched. + ATF_REQUIRE_EQ(overrides, base_test_case.get_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__fake_test_case) +ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__fake_test_case) +{ + const model::test_result result(model::test_result_skipped, "Irrelevant"); + const model::test_case base_test_case("__foo__", "Fake test", result); + + const model::metadata overrides = model::metadata_builder() + .set_description("Fake test") + .build(); + + const model::metadata defaults = model::metadata_builder() + .add_allowed_platform("some-value") + .set_description("Default description") + .build(); + + const model::test_case test_case = base_test_case.apply_metadata_defaults( + &defaults); + + const model::metadata expected = model::metadata_builder() + .add_allowed_platform("some-value") + .set_description("Fake test") + .build(); + ATF_REQUIRE_EQ(expected, test_case.get_metadata()); + ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__copy) +{ + const model::test_case tc1("name", model::metadata_builder().build()); + const model::test_case tc2 = tc1; + ATF_REQUIRE( tc1 == tc2); + ATF_REQUIRE(!(tc1 != tc2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__not_copy); +ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__not_copy) +{ + const std::string base_name("name"); + const model::metadata base_metadata = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + + const model::test_case base_tc(base_name, base_metadata); + + // Construct with all same values. + { + const model::test_case other_tc(base_name, base_metadata); + + ATF_REQUIRE( base_tc == other_tc); + ATF_REQUIRE(!(base_tc != other_tc)); + } + + // Construct with all same values but different metadata objects. + { + const model::metadata other_metadata = model::metadata_builder() + .add_custom("foo", "bar") + .set_timeout(base_metadata.timeout()) + .build(); + const model::test_case other_tc(base_name, other_metadata); + + ATF_REQUIRE( base_tc == other_tc); + ATF_REQUIRE(!(base_tc != other_tc)); + } + + // Different name. + { + const model::test_case other_tc("other", base_metadata); + + ATF_REQUIRE(!(base_tc == other_tc)); + ATF_REQUIRE( base_tc != other_tc); + } + + // Different metadata. + { + const model::test_case other_tc(base_name, + model::metadata_builder().build()); + + ATF_REQUIRE(!(base_tc == other_tc)); + ATF_REQUIRE( base_tc != other_tc); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__output); +ATF_TEST_CASE_BODY(test_case__output) +{ + const model::test_case tc1( + "the-name", model::metadata_builder() + .add_allowed_platform("foo").add_custom("bar", "baz").build()); + std::ostringstream str; + str << tc1; + ATF_REQUIRE_EQ( + "test_case{name='the-name', " + "metadata=metadata{allowed_architectures='', allowed_platforms='foo', " + "custom.bar='baz', description='', has_cleanup='false', " + "is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_cases_map__builder); +ATF_TEST_CASE_BODY(test_cases_map__builder) +{ + model::test_cases_map_builder builder; + model::test_cases_map exp_test_cases; + + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); + + builder.add("default-metadata"); + { + const model::test_case tc1("default-metadata", + model::metadata_builder().build()); + exp_test_cases.insert( + model::test_cases_map::value_type(tc1.name(), tc1)); + } + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); + + builder.add("with-metadata", + model::metadata_builder().set_description("text").build()); + { + const model::test_case tc1("with-metadata", + model::metadata_builder() + .set_description("text").build()); + exp_test_cases.insert( + model::test_cases_map::value_type(tc1.name(), tc1)); + } + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); + + const model::test_case tc1("fully_built", + model::metadata_builder() + .set_description("something else").build()); + builder.add(tc1); + exp_test_cases.insert(model::test_cases_map::value_type(tc1.name(), tc1)); + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, test_case__ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, test_case__fake_result); + + ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__real_test_case); + ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__fake_test_case); + + ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__not_copy); + + ATF_ADD_TEST_CASE(tcs, test_case__output); + + ATF_ADD_TEST_CASE(tcs, test_cases_map__builder); +} diff --git a/contrib/kyua/model/test_program.cpp b/contrib/kyua/model/test_program.cpp new file mode 100644 index 000000000000..b9181aa49537 --- /dev/null +++ b/contrib/kyua/model/test_program.cpp @@ -0,0 +1,452 @@ +// 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 "model/test_program.hpp" + +#include +#include + +#include "model/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_result.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace text = utils::text; + +using utils::none; + + +/// Internal implementation of a test_program. +struct model::test_program::impl : utils::noncopyable { + /// Name of the test program interface. + std::string interface_name; + + /// Name of the test program binary relative to root. + fs::path binary; + + /// Root of the test suite containing the test program. + fs::path root; + + /// Name of the test suite this program belongs to. + std::string test_suite_name; + + /// Metadata of the test program. + model::metadata md; + + /// List of test cases in the test program. + /// + /// Must be queried via the test_program::test_cases() method. + model::test_cases_map test_cases; + + /// Constructor. + /// + /// \param interface_name_ Name of the test program interface. + /// \param binary_ The name of the test program binary relative to root_. + /// \param root_ The root of the test suite containing the test program. + /// \param test_suite_name_ The name of the test suite this program + /// belongs to. + /// \param md_ Metadata of the test program. + /// \param test_cases_ The collection of test cases in the test program. + impl(const std::string& interface_name_, const fs::path& binary_, + const fs::path& root_, const std::string& test_suite_name_, + const model::metadata& md_, const model::test_cases_map& test_cases_) : + interface_name(interface_name_), + binary(binary_), + root(root_), + test_suite_name(test_suite_name_), + md(md_) + { + PRE_MSG(!binary.is_absolute(), + F("The program '%s' must be relative to the root of the test " + "suite '%s'") % binary % root); + + set_test_cases(test_cases_); + } + + /// Sets the list of test cases of the test program. + /// + /// \param test_cases_ The new list of test cases. + void + set_test_cases(const model::test_cases_map& test_cases_) + { + for (model::test_cases_map::const_iterator iter = test_cases_.begin(); + iter != test_cases_.end(); ++iter) { + const std::string& name = (*iter).first; + const model::test_case& test_case = (*iter).second; + + PRE_MSG(name == test_case.name(), + F("The test case '%s' has been registered with the " + "non-matching name '%s'") % name % test_case.name()); + + test_cases.insert(model::test_cases_map::value_type( + name, test_case.apply_metadata_defaults(&md))); + } + INV(test_cases.size() == test_cases_.size()); + } +}; + + +/// Constructs a new test program. +/// +/// \param interface_name_ Name of the test program interface. +/// \param binary_ The name of the test program binary relative to root_. +/// \param root_ The root of the test suite containing the test program. +/// \param test_suite_name_ The name of the test suite this program belongs to. +/// \param md_ Metadata of the test program. +/// \param test_cases_ The collection of test cases in the test program. +model::test_program::test_program(const std::string& interface_name_, + const fs::path& binary_, + const fs::path& root_, + const std::string& test_suite_name_, + const model::metadata& md_, + const model::test_cases_map& test_cases_) : + _pimpl(new impl(interface_name_, binary_, root_, test_suite_name_, md_, + test_cases_)) +{ +} + + +/// Destroys a test program. +model::test_program::~test_program(void) +{ +} + + +/// Gets the name of the test program interface. +/// +/// \return An interface name. +const std::string& +model::test_program::interface_name(void) const +{ + return _pimpl->interface_name; +} + + +/// Gets the path to the test program relative to the root of the test suite. +/// +/// \return The relative path to the test program binary. +const fs::path& +model::test_program::relative_path(void) const +{ + return _pimpl->binary; +} + + +/// Gets the absolute path to the test program. +/// +/// \return The absolute path to the test program binary. +const fs::path +model::test_program::absolute_path(void) const +{ + const fs::path full_path = _pimpl->root / _pimpl->binary; + return full_path.is_absolute() ? full_path : full_path.to_absolute(); +} + + +/// Gets the root of the test suite containing this test program. +/// +/// \return The path to the root of the test suite. +const fs::path& +model::test_program::root(void) const +{ + return _pimpl->root; +} + + +/// Gets the name of the test suite containing this test program. +/// +/// \return The name of the test suite. +const std::string& +model::test_program::test_suite_name(void) const +{ + return _pimpl->test_suite_name; +} + + +/// Gets the metadata of the test program. +/// +/// \return The metadata. +const model::metadata& +model::test_program::get_metadata(void) const +{ + return _pimpl->md; +} + + +/// Gets a test case by its name. +/// +/// \param name The name of the test case to locate. +/// +/// \return The requested test case. +/// +/// \throw not_found_error If the specified test case is not in the test +/// program. +const model::test_case& +model::test_program::find(const std::string& name) const +{ + const test_cases_map& tcs = test_cases(); + + const test_cases_map::const_iterator iter = tcs.find(name); + if (iter == tcs.end()) + throw not_found_error(F("Unknown test case %s in test program %s") % + name % relative_path()); + return (*iter).second; +} + + +/// Gets the list of test cases from the test program. +/// +/// \return The list of test cases provided by the test program. +const model::test_cases_map& +model::test_program::test_cases(void) const +{ + return _pimpl->test_cases; +} + + +/// Sets the list of test cases of the test program. +/// +/// This can only be called once and it may only be called from within +/// overridden test_cases() before that method ever returns a value for the +/// first time. Any other invocations will result in inconsistent program +/// state. +/// +/// \param test_cases_ The new list of test cases. +void +model::test_program::set_test_cases(const model::test_cases_map& test_cases_) +{ + PRE(_pimpl->test_cases.empty()); + _pimpl->set_test_cases(test_cases_); +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +model::test_program::operator==(const test_program& other) const +{ + return _pimpl == other._pimpl || ( + _pimpl->interface_name == other._pimpl->interface_name && + _pimpl->binary == other._pimpl->binary && + _pimpl->root == other._pimpl->root && + _pimpl->test_suite_name == other._pimpl->test_suite_name && + _pimpl->md == other._pimpl->md && + test_cases() == other.test_cases()); +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::test_program::operator!=(const test_program& other) const +{ + return !(*this == other); +} + + +/// Less-than comparator. +/// +/// A test program is considered to be less than another if and only if the +/// former's absolute path is less than the absolute path of the latter. In +/// other words, the absolute path is used here as the test program's +/// identifier. +/// +/// This simplistic less-than operator overload is provided so that test +/// programs can be held in sets and other containers. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object sorts before the other object; false otherwise. +bool +model::test_program::operator<(const test_program& other) const +{ + return absolute_path() < other.absolute_path(); +} + + +/// 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& +model::operator<<(std::ostream& output, const test_program& object) +{ + output << F("test_program{interface=%s, binary=%s, root=%s, test_suite=%s, " + "metadata=%s, test_cases=%s}") + % text::quote(object.interface_name(), '\'') + % text::quote(object.relative_path().str(), '\'') + % text::quote(object.root().str(), '\'') + % text::quote(object.test_suite_name(), '\'') + % object.get_metadata() + % object.test_cases(); + return output; +} + + +/// Internal implementation of the test_program_builder class. +struct model::test_program_builder::impl : utils::noncopyable { + /// Partially-constructed program with only the required properties. + model::test_program prototype; + + /// Optional metadata for the test program. + model::metadata metadata; + + /// Collection of test cases. + model::test_cases_map test_cases; + + /// Whether we have created a test_program object or not. + bool built; + + /// Constructor. + /// + /// \param prototype_ The partially constructed program with only the + /// required properties. + impl(const model::test_program& prototype_) : + prototype(prototype_), + metadata(model::metadata_builder().build()), + built(false) + { + } +}; + + +/// Constructs a new builder with non-optional values. +/// +/// \param interface_name_ Name of the test program interface. +/// \param binary_ The name of the test program binary relative to root_. +/// \param root_ The root of the test suite containing the test program. +/// \param test_suite_name_ The name of the test suite this program belongs to. +model::test_program_builder::test_program_builder( + const std::string& interface_name_, const fs::path& binary_, + const fs::path& root_, const std::string& test_suite_name_) : + _pimpl(new impl(model::test_program(interface_name_, binary_, root_, + test_suite_name_, + model::metadata_builder().build(), + model::test_cases_map()))) +{ +} + + +/// Destructor. +model::test_program_builder::~test_program_builder(void) +{ +} + + +/// Accumulates an additional test case with default metadata. +/// +/// \param test_case_name The name of the test case. +/// +/// \return A reference to this builder. +model::test_program_builder& +model::test_program_builder::add_test_case(const std::string& test_case_name) +{ + return add_test_case(test_case_name, model::metadata_builder().build()); +} + + +/// Accumulates an additional test case. +/// +/// \param test_case_name The name of the test case. +/// \param metadata The test case metadata. +/// +/// \return A reference to this builder. +model::test_program_builder& +model::test_program_builder::add_test_case(const std::string& test_case_name, + const model::metadata& metadata) +{ + const model::test_case test_case(test_case_name, metadata); + PRE_MSG(_pimpl->test_cases.find(test_case_name) == _pimpl->test_cases.end(), + F("Attempted to re-register test case '%s'") % test_case_name); + _pimpl->test_cases.insert(model::test_cases_map::value_type( + test_case_name, test_case)); + return *this; +} + + +/// Sets the test program metadata. +/// +/// \return metadata The metadata for the test program. +/// +/// \return A reference to this builder. +model::test_program_builder& +model::test_program_builder::set_metadata(const model::metadata& metadata) +{ + _pimpl->metadata = metadata; + return *this; +} + + +/// Creates a new test_program object. +/// +/// \pre This has not yet been called. We only support calling this function +/// once. +/// +/// \return The constructed test_program object. +model::test_program +model::test_program_builder::build(void) const +{ + PRE(!_pimpl->built); + _pimpl->built = true; + + return test_program(_pimpl->prototype.interface_name(), + _pimpl->prototype.relative_path(), + _pimpl->prototype.root(), + _pimpl->prototype.test_suite_name(), + _pimpl->metadata, + _pimpl->test_cases); +} + + +/// Creates a new dynamically-allocated test_program object. +/// +/// \pre This has not yet been called. We only support calling this function +/// once. +/// +/// \return The constructed test_program object. +model::test_program_ptr +model::test_program_builder::build_ptr(void) const +{ + const test_program result = build(); + return test_program_ptr(new test_program(result)); +} diff --git a/contrib/kyua/model/test_program.hpp b/contrib/kyua/model/test_program.hpp new file mode 100644 index 000000000000..974ec2a12d19 --- /dev/null +++ b/contrib/kyua/model/test_program.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 model/test_program.hpp +/// Definition of the "test program" concept. + +#if !defined(MODEL_TEST_PROGRAM_HPP) +#define MODEL_TEST_PROGRAM_HPP + +#include "model/test_program_fwd.hpp" + +#include +#include +#include +#include + +#include "model/metadata_fwd.hpp" +#include "model/test_case_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace model { + + +/// Representation of a test program. +class test_program { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + +protected: + void set_test_cases(const model::test_cases_map&); + +public: + test_program(const std::string&, const utils::fs::path&, + const utils::fs::path&, const std::string&, + const model::metadata&, const model::test_cases_map&); + virtual ~test_program(void); + + const std::string& interface_name(void) const; + const utils::fs::path& root(void) const; + const utils::fs::path& relative_path(void) const; + const utils::fs::path absolute_path(void) const; + const std::string& test_suite_name(void) const; + const model::metadata& get_metadata(void) const; + + const model::test_case& find(const std::string&) const; + virtual const model::test_cases_map& test_cases(void) const; + + bool operator==(const test_program&) const; + bool operator!=(const test_program&) const; + bool operator<(const test_program&) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_program&); + + +/// Builder for a test_program object. +class test_program_builder : utils::noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + test_program_builder(const std::string&, const utils::fs::path&, + const utils::fs::path&, const std::string&); + ~test_program_builder(void); + + test_program_builder& add_test_case(const std::string&); + test_program_builder& add_test_case(const std::string&, + const model::metadata&); + + test_program_builder& set_metadata(const model::metadata&); + + test_program build(void) const; + test_program_ptr build_ptr(void) const; +}; + + +} // namespace model + +#endif // !defined(MODEL_TEST_PROGRAM_HPP) diff --git a/contrib/kyua/model/test_program_fwd.hpp b/contrib/kyua/model/test_program_fwd.hpp new file mode 100644 index 000000000000..100f017c30a6 --- /dev/null +++ b/contrib/kyua/model/test_program_fwd.hpp @@ -0,0 +1,55 @@ +// 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 model/test_program_fwd.hpp +/// Forward declarations for model/test_program.hpp + +#if !defined(MODEL_TEST_PROGRAM_FWD_HPP) +#define MODEL_TEST_PROGRAM_FWD_HPP + +#include +#include + + +namespace model { + + +class test_program; + + +/// Pointer to a test program. +typedef std::shared_ptr< test_program > test_program_ptr; + + +/// Collection of test programs. +typedef std::vector< test_program_ptr > test_programs_vector; + + +} // namespace model + +#endif // !defined(MODEL_TEST_PROGRAM_FWD_HPP) diff --git a/contrib/kyua/model/test_program_test.cpp b/contrib/kyua/model/test_program_test.cpp new file mode 100644 index 000000000000..f9a8f7e59da3 --- /dev/null +++ b/contrib/kyua/model/test_program_test.cpp @@ -0,0 +1,711 @@ +// 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 "model/test_program.hpp" + +extern "C" { +#include + +#include +} + +#include +#include + +#include + +#include "model/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_result.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" + +namespace fs = utils::fs; + + +namespace { + + +/// Test program that sets its test cases lazily. +/// +/// This test class exists to test the behavior of a test_program object when +/// the class is extended to offer lazy loading of test cases. We simulate such +/// lazy loading here by storing the list of test cases aside at construction +/// time and later setting it lazily the first time test_cases() is called. +class lazy_test_program : public model::test_program { + /// Whether set_test_cases() has yet been called or not. + mutable bool _set_test_cases_called; + + /// The list of test cases for this test program. + /// + /// Only use this in the call to set_test_cases(). All other reads of the + /// test cases list should happen via the parent class' test_cases() method. + model::test_cases_map _lazy_test_cases; + +public: + /// Constructs a new test program. + /// + /// \param interface_name_ Name of the test program interface. + /// \param binary_ The name of the test program binary relative to root_. + /// \param root_ The root of the test suite containing the test program. + /// \param test_suite_name_ The name of the test suite. + /// \param metadata_ Metadata of the test program. + /// \param test_cases_ The collection of test cases in the test program. + lazy_test_program(const std::string& interface_name_, + const utils::fs::path& binary_, + const utils::fs::path& root_, + const std::string& test_suite_name_, + const model::metadata& metadata_, + const model::test_cases_map& test_cases_) : + test_program(interface_name_, binary_, root_, test_suite_name_, + metadata_, model::test_cases_map()), + _set_test_cases_called(false), + _lazy_test_cases(test_cases_) + { + } + + /// Lazily sets the test cases on the parent and returns them. + /// + /// \return The list of test cases. + const model::test_cases_map& + test_cases(void) const + { + if (!_set_test_cases_called) { + const_cast< lazy_test_program* >(this)->set_test_cases( + _lazy_test_cases); + _set_test_cases_called = true; + } + return test_program::test_cases(); + } +}; + + +} // anonymous namespace + + +/// Runs a ctor_and_getters test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_ctor_and_getters(void) +{ + const model::metadata tp_md = model::metadata_builder() + .add_custom("first", "foo") + .add_custom("second", "bar") + .build(); + const model::metadata tc_md = model::metadata_builder() + .add_custom("first", "baz") + .build(); + + const TestProgram test_program( + "mock", fs::path("binary"), fs::path("root"), "suite-name", tp_md, + model::test_cases_map_builder().add("foo", tc_md).build()); + + + ATF_REQUIRE_EQ("mock", test_program.interface_name()); + ATF_REQUIRE_EQ(fs::path("binary"), test_program.relative_path()); + ATF_REQUIRE_EQ(fs::current_path() / "root/binary", + test_program.absolute_path()); + ATF_REQUIRE_EQ(fs::path("root"), test_program.root()); + ATF_REQUIRE_EQ("suite-name", test_program.test_suite_name()); + ATF_REQUIRE_EQ(tp_md, test_program.get_metadata()); + + const model::metadata exp_tc_md = model::metadata_builder() + .add_custom("first", "baz") + .add_custom("second", "bar") + .build(); + const model::test_cases_map exp_tcs = model::test_cases_map_builder() + .add("foo", exp_tc_md) + .build(); + ATF_REQUIRE_EQ(exp_tcs, test_program.test_cases()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ctor_and_getters); +ATF_TEST_CASE_BODY(ctor_and_getters) +{ + check_ctor_and_getters< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__ctor_and_getters); +ATF_TEST_CASE_BODY(derived__ctor_and_getters) +{ + check_ctor_and_getters< lazy_test_program >(); +} + + +/// Runs a find_ok test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_find_ok(void) +{ + const model::test_case test_case("main", model::metadata_builder().build()); + + const TestProgram test_program( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), + model::test_cases_map_builder().add(test_case).build()); + + const model::test_case& found_test_case = test_program.find("main"); + ATF_REQUIRE_EQ(test_case, found_test_case); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__ok); +ATF_TEST_CASE_BODY(find__ok) +{ + check_find_ok< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__find__ok); +ATF_TEST_CASE_BODY(derived__find__ok) +{ + check_find_ok< lazy_test_program >(); +} + + +/// Runs a find_missing test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_find_missing(void) +{ + const TestProgram test_program( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), + model::test_cases_map_builder().add("main").build()); + + ATF_REQUIRE_THROW_RE(model::not_found_error, + "case.*abc.*program.*non-existent", + test_program.find("abc")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__missing); +ATF_TEST_CASE_BODY(find__missing) +{ + check_find_missing< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__find__missing); +ATF_TEST_CASE_BODY(derived__find__missing) +{ + check_find_missing< lazy_test_program >(); +} + + +/// Runs a metadata_inheritance test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_metadata_inheritance(void) +{ + const model::test_cases_map test_cases = model::test_cases_map_builder() + .add("inherit-all") + .add("inherit-some", + model::metadata_builder() + .set_description("Overriden description") + .build()) + .add("inherit-none", + model::metadata_builder() + .add_allowed_architecture("overriden-arch") + .add_allowed_platform("overriden-platform") + .set_description("Overriden description") + .build()) + .build(); + + const model::metadata metadata = model::metadata_builder() + .add_allowed_architecture("base-arch") + .set_description("Base description") + .build(); + const TestProgram test_program( + "plain", fs::path("non-existent"), fs::path("."), "suite-name", + metadata, test_cases); + + { + const model::metadata exp_metadata = model::metadata_builder() + .add_allowed_architecture("base-arch") + .set_description("Base description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, + test_program.find("inherit-all").get_metadata()); + } + + { + const model::metadata exp_metadata = model::metadata_builder() + .add_allowed_architecture("base-arch") + .set_description("Overriden description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, + test_program.find("inherit-some").get_metadata()); + } + + { + const model::metadata exp_metadata = model::metadata_builder() + .add_allowed_architecture("overriden-arch") + .add_allowed_platform("overriden-platform") + .set_description("Overriden description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, + test_program.find("inherit-none").get_metadata()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(metadata_inheritance); +ATF_TEST_CASE_BODY(metadata_inheritance) +{ + check_metadata_inheritance< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__metadata_inheritance); +ATF_TEST_CASE_BODY(derived__metadata_inheritance) +{ + check_metadata_inheritance< lazy_test_program >(); +} + + +/// Runs a operators_eq_and_ne__copy test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_operators_eq_and_ne__copy(void) +{ + const TestProgram tp1( + "plain", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + const TestProgram tp2 = tp1; + ATF_REQUIRE( tp1 == tp2); + ATF_REQUIRE(!(tp1 != tp2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__copy) +{ + check_operators_eq_and_ne__copy< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__copy) +{ + check_operators_eq_and_ne__copy< lazy_test_program >(); +} + + +/// Runs a operators_eq_and_ne__not_copy test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_operators_eq_and_ne__not_copy(void) +{ + const std::string base_interface("plain"); + const fs::path base_relative_path("the/test/program"); + const fs::path base_root("/the/root"); + const std::string base_test_suite("suite-name"); + const model::metadata base_metadata = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + + const model::test_cases_map base_tcs = model::test_cases_map_builder() + .add("main", model::metadata_builder() + .add_custom("second", "baz") + .build()) + .build(); + + const TestProgram base_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, base_tcs); + + // Construct with all same values. + { + const model::test_cases_map other_tcs = model::test_cases_map_builder() + .add("main", model::metadata_builder() + .add_custom("second", "baz") + .build()) + .build(); + + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, other_tcs); + + ATF_REQUIRE( base_tp == other_tp); + ATF_REQUIRE(!(base_tp != other_tp)); + } + + // Construct with same final metadata values but using a different + // intermediate representation. The original test program has one property + // in the base test program definition and another in the test case; here, + // we put both definitions explicitly in the test case. + { + const model::test_cases_map other_tcs = model::test_cases_map_builder() + .add("main", model::metadata_builder() + .add_custom("foo", "bar") + .add_custom("second", "baz") + .build()) + .build(); + + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, other_tcs); + + ATF_REQUIRE( base_tp == other_tp); + ATF_REQUIRE(!(base_tp != other_tp)); + } + + // Different interface. + { + const TestProgram other_tp( + "atf", base_relative_path, base_root, base_test_suite, + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different relative path. + { + const TestProgram other_tp( + base_interface, fs::path("a/b/c"), base_root, base_test_suite, + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different root. + { + const TestProgram other_tp( + base_interface, base_relative_path, fs::path("."), base_test_suite, + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different test suite. + { + const TestProgram other_tp( + base_interface, base_relative_path, base_root, "different-suite", + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different metadata. + { + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + model::metadata_builder().build(), base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different test cases. + { + const model::test_cases_map other_tcs = model::test_cases_map_builder() + .add("foo").build(); + + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, other_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__not_copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__not_copy) +{ + check_operators_eq_and_ne__not_copy< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__not_copy); +ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__not_copy) +{ + check_operators_eq_and_ne__not_copy< lazy_test_program >(); +} + + +/// Runs a operator_lt test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_operator_lt(void) +{ + const TestProgram tp1( + "plain", fs::path("a/b/c"), fs::path("/foo/bar"), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + const TestProgram tp2( + "atf", fs::path("c"), fs::path("/foo/bar"), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + const TestProgram tp3( + "plain", fs::path("a/b/c"), fs::path("/abc"), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + + ATF_REQUIRE(!(tp1 < tp1)); + + ATF_REQUIRE( tp1 < tp2); + ATF_REQUIRE(!(tp2 < tp1)); + + ATF_REQUIRE(!(tp1 < tp3)); + ATF_REQUIRE( tp3 < tp1); + + // And now, test the actual reason why we want to have an < overload by + // attempting to put the various programs in a set. + std::set< TestProgram > programs; + programs.insert(tp1); + programs.insert(tp2); + programs.insert(tp3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operator_lt); +ATF_TEST_CASE_BODY(operator_lt) +{ + check_operator_lt< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__operator_lt); +ATF_TEST_CASE_BODY(derived__operator_lt) +{ + check_operator_lt< lazy_test_program >(); +} + + +/// Runs a output__no_test_cases test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_output__no_test_cases(void) +{ + TestProgram tp( + "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name", + model::metadata_builder().add_allowed_architecture("a").build(), + model::test_cases_map()); + + std::ostringstream str; + str << tp; + ATF_REQUIRE_EQ( + "test_program{interface='plain', binary='binary/path', " + "root='/the/root', test_suite='suite-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}, " + "test_cases=map()}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__no_test_cases); +ATF_TEST_CASE_BODY(output__no_test_cases) +{ + check_output__no_test_cases< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__output__no_test_cases); +ATF_TEST_CASE_BODY(derived__output__no_test_cases) +{ + check_output__no_test_cases< lazy_test_program >(); +} + + +/// Runs a output__some_test_cases test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_output__some_test_cases(void) +{ + const model::test_cases_map test_cases = model::test_cases_map_builder() + .add("the-name", model::metadata_builder() + .add_allowed_platform("foo") + .add_custom("bar", "baz") + .build()) + .add("another-name") + .build(); + + const TestProgram tp = TestProgram( + "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name", + model::metadata_builder().add_allowed_architecture("a").build(), + test_cases); + + std::ostringstream str; + str << tp; + ATF_REQUIRE_EQ( + "test_program{interface='plain', binary='binary/path', " + "root='/the/root', test_suite='suite-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}, " + "test_cases=map(" + "another-name=test_case{name='another-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}}, " + "the-name=test_case{name='the-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='foo', " + "custom.bar='baz', description='', has_cleanup='false', " + "is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}})}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__some_test_cases); +ATF_TEST_CASE_BODY(output__some_test_cases) +{ + check_output__some_test_cases< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__output__some_test_cases); +ATF_TEST_CASE_BODY(derived__output__some_test_cases) +{ + check_output__some_test_cases< lazy_test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(builder__defaults); +ATF_TEST_CASE_BODY(builder__defaults) +{ + const model::test_program expected( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), model::test_cases_map()); + + const model::test_program built = model::test_program_builder( + "mock", fs::path("non-existent"), fs::path("."), "suite-name") + .build(); + + ATF_REQUIRE_EQ(built, expected); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(builder__overrides); +ATF_TEST_CASE_BODY(builder__overrides) +{ + const model::metadata md = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + const model::test_cases_map tcs = model::test_cases_map_builder() + .add("first") + .add("second", md) + .build(); + const model::test_program expected( + "mock", fs::path("binary"), fs::path("root"), "suite-name", md, tcs); + + const model::test_program built = model::test_program_builder( + "mock", fs::path("binary"), fs::path("root"), "suite-name") + .add_test_case("first") + .add_test_case("second", md) + .set_metadata(md) + .build(); + + ATF_REQUIRE_EQ(built, expected); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(builder__ptr); +ATF_TEST_CASE_BODY(builder__ptr) +{ + const model::test_program expected( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), model::test_cases_map()); + + const model::test_program_ptr built = model::test_program_builder( + "mock", fs::path("non-existent"), fs::path("."), "suite-name") + .build_ptr(); + + ATF_REQUIRE_EQ(*built, expected); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, find__ok); + ATF_ADD_TEST_CASE(tcs, find__missing); + ATF_ADD_TEST_CASE(tcs, metadata_inheritance); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__not_copy); + ATF_ADD_TEST_CASE(tcs, operator_lt); + ATF_ADD_TEST_CASE(tcs, output__no_test_cases); + ATF_ADD_TEST_CASE(tcs, output__some_test_cases); + + ATF_ADD_TEST_CASE(tcs, derived__ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, derived__find__ok); + ATF_ADD_TEST_CASE(tcs, derived__find__missing); + ATF_ADD_TEST_CASE(tcs, derived__metadata_inheritance); + ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__not_copy); + ATF_ADD_TEST_CASE(tcs, derived__operator_lt); + ATF_ADD_TEST_CASE(tcs, derived__output__no_test_cases); + ATF_ADD_TEST_CASE(tcs, derived__output__some_test_cases); + + ATF_ADD_TEST_CASE(tcs, builder__defaults); + ATF_ADD_TEST_CASE(tcs, builder__overrides); + ATF_ADD_TEST_CASE(tcs, builder__ptr); +} diff --git a/contrib/kyua/model/test_result.cpp b/contrib/kyua/model/test_result.cpp new file mode 100644 index 000000000000..7392e77f5561 --- /dev/null +++ b/contrib/kyua/model/test_result.cpp @@ -0,0 +1,142 @@ +// 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 "model/test_result.hpp" + +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +/// Constructs a base result. +/// +/// \param type_ The type of the result. +/// \param reason_ The reason explaining the result, if any. It is OK for this +/// to be empty, which is actually the default. +model::test_result::test_result(const test_result_type type_, + const std::string& reason_) : + _type(type_), + _reason(reason_) +{ +} + + +/// Returns the type of the result. +/// +/// \return A result type. +model::test_result_type +model::test_result::type(void) const +{ + return _type; +} + + +/// Returns the reason explaining the result. +/// +/// \return A textual reason, possibly empty. +const std::string& +model::test_result::reason(void) const +{ + return _reason; +} + + +/// True if the test case result has a positive connotation. +/// +/// \return Whether the test case is good or not. +bool +model::test_result::good(void) const +{ + switch (_type) { + case test_result_expected_failure: + case test_result_passed: + case test_result_skipped: + return true; + + case test_result_broken: + case test_result_failed: + return false; + } + UNREACHABLE; +} + + +/// Equality comparator. +/// +/// \param other The test result to compare to. +/// +/// \return True if the other object is equal to this one, false otherwise. +bool +model::test_result::operator==(const test_result& other) const +{ + return _type == other._type && _reason == other._reason; +} + + +/// Inequality comparator. +/// +/// \param other The test result to compare to. +/// +/// \return True if the other object is different from this one, false +/// otherwise. +bool +model::test_result::operator!=(const test_result& other) const +{ + return !(*this == other); +} + + +/// 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& +model::operator<<(std::ostream& output, const test_result& object) +{ + std::string result_name; + switch (object.type()) { + case test_result_broken: result_name = "broken"; break; + case test_result_expected_failure: result_name = "expected_failure"; break; + case test_result_failed: result_name = "failed"; break; + case test_result_passed: result_name = "passed"; break; + case test_result_skipped: result_name = "skipped"; break; + } + const std::string& reason = object.reason(); + if (reason.empty()) { + output << F("model::test_result{type=%s}") + % text::quote(result_name, '\''); + } else { + output << F("model::test_result{type=%s, reason=%s}") + % text::quote(result_name, '\'') % text::quote(reason, '\''); + } + return output; +} diff --git a/contrib/kyua/model/test_result.hpp b/contrib/kyua/model/test_result.hpp new file mode 100644 index 000000000000..b9c439ce789a --- /dev/null +++ b/contrib/kyua/model/test_result.hpp @@ -0,0 +1,79 @@ +// 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 model/test_result.hpp +/// Definition of the "test result" concept. + +#if !defined(MODEL_TEST_RESULT_HPP) +#define MODEL_TEST_RESULT_HPP + +#include "model/test_result_fwd.hpp" + +#include +#include + +namespace model { + + +/// Representation of a single test result. +/// +/// A test result is a simple pair of (type, reason). The type indicates the +/// semantics of the results, and the optional reason provides an extra +/// description of the result type. +/// +/// In general, a 'passed' result will not have a reason attached, because a +/// successful test case does not deserve any kind of explanation. We used to +/// special-case this with a very complex class hierarchy, but it proved to +/// result in an extremely-complex to maintain code base that provided no +/// benefits. As a result, we allow any test type to carry a reason. +class test_result { + /// The type of the result. + test_result_type _type; + + /// A description of the result; may be empty. + std::string _reason; + +public: + test_result(const test_result_type, const std::string& = ""); + + test_result_type type(void) const; + const std::string& reason(void) const; + + bool good(void) const; + + bool operator==(const test_result&) const; + bool operator!=(const test_result&) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_result&); + + +} // namespace model + +#endif // !defined(MODEL_TEST_RESULT_HPP) diff --git a/contrib/kyua/model/test_result_fwd.hpp b/contrib/kyua/model/test_result_fwd.hpp new file mode 100644 index 000000000000..d7871e81d23e --- /dev/null +++ b/contrib/kyua/model/test_result_fwd.hpp @@ -0,0 +1,53 @@ +// 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 model/test_result_fwd.hpp +/// Forward declarations for model/test_result.hpp + +#if !defined(MODEL_TEST_RESULT_FWD_HPP) +#define MODEL_TEST_RESULT_FWD_HPP + +namespace model { + + +/// Definitions for all possible test case results. +enum test_result_type { + test_result_broken, + test_result_expected_failure, + test_result_failed, + test_result_passed, + test_result_skipped, +}; + + +class test_result; + + +} // namespace model + +#endif // !defined(MODEL_TEST_RESULT_FWD_HPP) diff --git a/contrib/kyua/model/test_result_test.cpp b/contrib/kyua/model/test_result_test.cpp new file mode 100644 index 000000000000..355587d37aee --- /dev/null +++ b/contrib/kyua/model/test_result_test.cpp @@ -0,0 +1,185 @@ +// 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 "model/test_result.hpp" + +#include + +#include + + +/// Creates a test case to validate the getters. +/// +/// \param name The name of the test case; "__getters" will be appended. +/// \param expected_type The expected type of the result. +/// \param expected_reason The expected reason for the result. +/// \param result The result to query. +#define GETTERS_TEST(name, expected_type, expected_reason, result) \ + ATF_TEST_CASE_WITHOUT_HEAD(name ## __getters); \ + ATF_TEST_CASE_BODY(name ## __getters) \ + { \ + ATF_REQUIRE(expected_type == result.type()); \ + ATF_REQUIRE_EQ(expected_reason, result.reason()); \ + } + + +/// Creates a test case to validate the good() method. +/// +/// \param name The name of the test case; "__good" will be appended. +/// \param expected The expected result of good(). +/// \param result_type The result type to check. +#define GOOD_TEST(name, expected, result_type) \ + ATF_TEST_CASE_WITHOUT_HEAD(name ## __good); \ + ATF_TEST_CASE_BODY(name ## __good) \ + { \ + ATF_REQUIRE_EQ(expected, model::test_result(result_type).good()); \ + } + + +/// Creates a test case to validate the operator<< method. +/// +/// \param name The name of the test case; "__output" will be appended. +/// \param expected The expected string in the output. +/// \param result The result to format. +#define OUTPUT_TEST(name, expected, result) \ + ATF_TEST_CASE_WITHOUT_HEAD(name ## __output); \ + ATF_TEST_CASE_BODY(name ## __output) \ + { \ + std::ostringstream output; \ + output << "prefix" << result << "suffix"; \ + ATF_REQUIRE_EQ("prefix" + std::string(expected) + "suffix", \ + output.str()); \ + } + + +GETTERS_TEST( + broken, + model::test_result_broken, + "The reason", + model::test_result(model::test_result_broken, "The reason")); +GETTERS_TEST( + expected_failure, + model::test_result_expected_failure, + "The reason", + model::test_result(model::test_result_expected_failure, "The reason")); +GETTERS_TEST( + failed, + model::test_result_failed, + "The reason", + model::test_result(model::test_result_failed, "The reason")); +GETTERS_TEST( + passed, + model::test_result_passed, + "", + model::test_result(model::test_result_passed)); +GETTERS_TEST( + skipped, + model::test_result_skipped, + "The reason", + model::test_result(model::test_result_skipped, "The reason")); + + +GOOD_TEST(broken, false, model::test_result_broken); +GOOD_TEST(expected_failure, true, model::test_result_expected_failure); +GOOD_TEST(failed, false, model::test_result_failed); +GOOD_TEST(passed, true, model::test_result_passed); +GOOD_TEST(skipped, true, model::test_result_skipped); + + +OUTPUT_TEST( + broken, + "model::test_result{type='broken', reason='foo'}", + model::test_result(model::test_result_broken, "foo")); +OUTPUT_TEST( + expected_failure, + "model::test_result{type='expected_failure', reason='abc def'}", + model::test_result(model::test_result_expected_failure, "abc def")); +OUTPUT_TEST( + failed, + "model::test_result{type='failed', reason='some \\'string'}", + model::test_result(model::test_result_failed, "some 'string")); +OUTPUT_TEST( + passed, + "model::test_result{type='passed'}", + model::test_result(model::test_result_passed, "")); +OUTPUT_TEST( + skipped, + "model::test_result{type='skipped', reason='last message'}", + model::test_result(model::test_result_skipped, "last message")); + + +ATF_TEST_CASE_WITHOUT_HEAD(operator_eq); +ATF_TEST_CASE_BODY(operator_eq) +{ + const model::test_result result1(model::test_result_broken, "Foo"); + const model::test_result result2(model::test_result_broken, "Foo"); + const model::test_result result3(model::test_result_broken, "Bar"); + const model::test_result result4(model::test_result_failed, "Foo"); + + ATF_REQUIRE( result1 == result1); + ATF_REQUIRE( result1 == result2); + ATF_REQUIRE(!(result1 == result3)); + ATF_REQUIRE(!(result1 == result4)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operator_ne); +ATF_TEST_CASE_BODY(operator_ne) +{ + const model::test_result result1(model::test_result_broken, "Foo"); + const model::test_result result2(model::test_result_broken, "Foo"); + const model::test_result result3(model::test_result_broken, "Bar"); + const model::test_result result4(model::test_result_failed, "Foo"); + + ATF_REQUIRE(!(result1 != result1)); + ATF_REQUIRE(!(result1 != result2)); + ATF_REQUIRE( result1 != result3); + ATF_REQUIRE( result1 != result4); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, broken__getters); + ATF_ADD_TEST_CASE(tcs, broken__good); + ATF_ADD_TEST_CASE(tcs, broken__output); + ATF_ADD_TEST_CASE(tcs, expected_failure__getters); + ATF_ADD_TEST_CASE(tcs, expected_failure__good); + ATF_ADD_TEST_CASE(tcs, expected_failure__output); + ATF_ADD_TEST_CASE(tcs, failed__getters); + ATF_ADD_TEST_CASE(tcs, failed__good); + ATF_ADD_TEST_CASE(tcs, failed__output); + ATF_ADD_TEST_CASE(tcs, passed__getters); + ATF_ADD_TEST_CASE(tcs, passed__good); + ATF_ADD_TEST_CASE(tcs, passed__output); + ATF_ADD_TEST_CASE(tcs, skipped__getters); + ATF_ADD_TEST_CASE(tcs, skipped__good); + ATF_ADD_TEST_CASE(tcs, skipped__output); + ATF_ADD_TEST_CASE(tcs, operator_eq); + ATF_ADD_TEST_CASE(tcs, operator_ne); +} diff --git a/contrib/kyua/model/types.hpp b/contrib/kyua/model/types.hpp new file mode 100644 index 000000000000..e877b6f58d46 --- /dev/null +++ b/contrib/kyua/model/types.hpp @@ -0,0 +1,61 @@ +// 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 model/types.hpp +/// Definition of miscellaneous base types required by our classes. +/// +/// We consider objects coming from the STL and from the utils module to be +/// base types. + +#if !defined(MODEL_TYPES_HPP) +#define MODEL_TYPES_HPP + +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace model { + + +/// Collection of paths. +typedef std::set< utils::fs::path > paths_set; + + +/// Collection of strings. +typedef std::set< std::string > strings_set; + + +/// Collection of test properties in their textual form. +typedef std::map< std::string, std::string > properties_map; + + +} // namespace model + +#endif // !defined(MODEL_TYPES_HPP) diff --git a/contrib/kyua/store/Kyuafile b/contrib/kyua/store/Kyuafile new file mode 100644 index 000000000000..ada2f7c0e88c --- /dev/null +++ b/contrib/kyua/store/Kyuafile @@ -0,0 +1,15 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="dbtypes_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="layout_test"} +atf_test_program{name="metadata_test"} +atf_test_program{name="migrate_test"} +atf_test_program{name="read_backend_test"} +atf_test_program{name="read_transaction_test"} +atf_test_program{name="schema_inttest"} +atf_test_program{name="transaction_test"} +atf_test_program{name="write_backend_test"} +atf_test_program{name="write_transaction_test"} diff --git a/contrib/kyua/store/Makefile.am.inc b/contrib/kyua/store/Makefile.am.inc new file mode 100644 index 000000000000..13c7c70a10d9 --- /dev/null +++ b/contrib/kyua/store/Makefile.am.inc @@ -0,0 +1,145 @@ +# 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. + +STORE_CFLAGS = $(MODEL_CFLAGS) $(UTILS_CFLAGS) +STORE_LIBS = libstore.a $(MODEL_LIBS) $(UTILS_LIBS) + +noinst_LIBRARIES += libstore.a +libstore_a_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\" +libstore_a_CPPFLAGS += $(UTILS_CFLAGS) +libstore_a_SOURCES = store/dbtypes.cpp +libstore_a_SOURCES += store/dbtypes.hpp +libstore_a_SOURCES += store/exceptions.cpp +libstore_a_SOURCES += store/exceptions.hpp +libstore_a_SOURCES += store/layout.cpp +libstore_a_SOURCES += store/layout.hpp +libstore_a_SOURCES += store/layout_fwd.hpp +libstore_a_SOURCES += store/metadata.cpp +libstore_a_SOURCES += store/metadata.hpp +libstore_a_SOURCES += store/metadata_fwd.hpp +libstore_a_SOURCES += store/migrate.cpp +libstore_a_SOURCES += store/migrate.hpp +libstore_a_SOURCES += store/read_backend.cpp +libstore_a_SOURCES += store/read_backend.hpp +libstore_a_SOURCES += store/read_backend_fwd.hpp +libstore_a_SOURCES += store/read_transaction.cpp +libstore_a_SOURCES += store/read_transaction.hpp +libstore_a_SOURCES += store/read_transaction_fwd.hpp +libstore_a_SOURCES += store/write_backend.cpp +libstore_a_SOURCES += store/write_backend.hpp +libstore_a_SOURCES += store/write_backend_fwd.hpp +libstore_a_SOURCES += store/write_transaction.cpp +libstore_a_SOURCES += store/write_transaction.hpp +libstore_a_SOURCES += store/write_transaction_fwd.hpp + +dist_store_DATA = store/migrate_v1_v2.sql +dist_store_DATA += store/migrate_v2_v3.sql +dist_store_DATA += store/schema_v3.sql + +if WITH_ATF +tests_storedir = $(pkgtestsdir)/store + +tests_store_DATA = store/Kyuafile +tests_store_DATA += store/schema_v1.sql +tests_store_DATA += store/schema_v2.sql +tests_store_DATA += store/testdata_v1.sql +tests_store_DATA += store/testdata_v2.sql +tests_store_DATA += store/testdata_v3_1.sql +tests_store_DATA += store/testdata_v3_2.sql +tests_store_DATA += store/testdata_v3_3.sql +tests_store_DATA += store/testdata_v3_4.sql +EXTRA_DIST += $(tests_store_DATA) + +tests_store_PROGRAMS = store/dbtypes_test +store_dbtypes_test_SOURCES = store/dbtypes_test.cpp +store_dbtypes_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_dbtypes_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/exceptions_test +store_exceptions_test_SOURCES = store/exceptions_test.cpp +store_exceptions_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_exceptions_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/layout_test +store_layout_test_SOURCES = store/layout_test.cpp +store_layout_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +store_layout_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/metadata_test +store_metadata_test_SOURCES = store/metadata_test.cpp +store_metadata_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_metadata_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/migrate_test +store_migrate_test_SOURCES = store/migrate_test.cpp +store_migrate_test_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\" +store_migrate_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +store_migrate_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/read_backend_test +store_read_backend_test_SOURCES = store/read_backend_test.cpp +store_read_backend_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_read_backend_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/read_transaction_test +store_read_transaction_test_SOURCES = store/read_transaction_test.cpp +store_read_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_read_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/schema_inttest +store_schema_inttest_SOURCES = store/schema_inttest.cpp +store_schema_inttest_CPPFLAGS = -DKYUA_STORETESTDATADIR=\"$(tests_storedir)\" +store_schema_inttest_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_schema_inttest_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/transaction_test +store_transaction_test_SOURCES = store/transaction_test.cpp +store_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/write_backend_test +store_write_backend_test_SOURCES = store/write_backend_test.cpp +store_write_backend_test_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\" +store_write_backend_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_write_backend_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/write_transaction_test +store_write_transaction_test_SOURCES = store/write_transaction_test.cpp +store_write_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_write_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) \ + $(ATF_CXX_LIBS) +endif diff --git a/contrib/kyua/store/dbtypes.cpp b/contrib/kyua/store/dbtypes.cpp new file mode 100644 index 000000000000..3ff755aa3307 --- /dev/null +++ b/contrib/kyua/store/dbtypes.cpp @@ -0,0 +1,255 @@ +// 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 "store/dbtypes.hpp" + +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace sqlite = utils::sqlite; + + +/// Binds a boolean value to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param value The value to bind. +void +store::bind_bool(sqlite::statement& stmt, const char* field, const bool value) +{ + stmt.bind(field, value ? "true" : "false"); +} + + +/// Binds a time delta to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param delta The value to bind. +void +store::bind_delta(sqlite::statement& stmt, const char* field, + const datetime::delta& delta) +{ + stmt.bind(field, static_cast< int64_t >(delta.to_microseconds())); +} + + +/// Binds a string to a statement parameter. +/// +/// If the string is not empty, this binds the string itself. Otherwise, it +/// binds a NULL value. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param str The string to bind. +void +store::bind_optional_string(sqlite::statement& stmt, const char* field, + const std::string& str) +{ + if (str.empty()) + stmt.bind(field, sqlite::null()); + else + stmt.bind(field, str); +} + + +/// Binds a test result type to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param type The result type to bind. +void +store::bind_test_result_type(sqlite::statement& stmt, const char* field, + const model::test_result_type& type) +{ + switch (type) { + case model::test_result_broken: + stmt.bind(field, "broken"); + break; + + case model::test_result_expected_failure: + stmt.bind(field, "expected_failure"); + break; + + case model::test_result_failed: + stmt.bind(field, "failed"); + break; + + case model::test_result_passed: + stmt.bind(field, "passed"); + break; + + case model::test_result_skipped: + stmt.bind(field, "skipped"); + break; + + default: + UNREACHABLE; + } +} + + +/// Binds a timestamp to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param timestamp The value to bind. +void +store::bind_timestamp(sqlite::statement& stmt, const char* field, + const datetime::timestamp& timestamp) +{ + stmt.bind(field, timestamp.to_microseconds()); +} + + +/// Queries a boolean value from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +bool +store::column_bool(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_text) + throw store::integrity_error(F("Boolean value in column %s is not a " + "string") % column); + const std::string value = stmt.column_text(id); + if (value == "true") + return true; + else if (value == "false") + return false; + else + throw store::integrity_error(F("Unknown boolean value '%s'") % value); +} + + +/// Queries a time delta from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +datetime::delta +store::column_delta(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_integer) + throw store::integrity_error(F("Time delta in column %s is not an " + "integer") % column); + return datetime::delta::from_microseconds(stmt.column_int64(id)); +} + + +/// Queries an optional string from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +std::string +store::column_optional_string(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + switch (stmt.column_type(id)) { + case sqlite::type_text: + return stmt.column_text(id); + case sqlite::type_null: + return ""; + default: + throw integrity_error(F("Invalid string type in column %s") % column); + } +} + + +/// Queries a test result type from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +model::test_result_type +store::column_test_result_type(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_text) + throw store::integrity_error(F("Result type in column %s is not a " + "string") % column); + const std::string type = stmt.column_text(id); + if (type == "passed") { + return model::test_result_passed; + } else if (type == "broken") { + return model::test_result_broken; + } else if (type == "expected_failure") { + return model::test_result_expected_failure; + } else if (type == "failed") { + return model::test_result_failed; + } else if (type == "skipped") { + return model::test_result_skipped; + } else { + throw store::integrity_error(F("Unknown test result type %s") % type); + } +} + + +/// Queries a timestamp from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +datetime::timestamp +store::column_timestamp(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_integer) + throw store::integrity_error(F("Timestamp in column %s is not an " + "integer") % column); + const int64_t value = stmt.column_int64(id); + if (value < 0) + throw store::integrity_error(F("Timestamp in column %s must be " + "positive") % column); + return datetime::timestamp::from_microseconds(value); +} diff --git a/contrib/kyua/store/dbtypes.hpp b/contrib/kyua/store/dbtypes.hpp new file mode 100644 index 000000000000..919d088d0ecd --- /dev/null +++ b/contrib/kyua/store/dbtypes.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 store/dbtypes.hpp +/// Functions to internalize/externalize various types. +/// +/// These helper functions are only provided to help in the implementation of +/// other modules. Therefore, this header file should never be included from +/// other header files. + +#if defined(STORE_DBTYPES_HPP) +# error "Do not include dbtypes.hpp multiple times" +#endif // !defined(STORE_DBTYPES_HPP) +#define STORE_DBTYPES_HPP + +#include + +#include "model/test_result_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/sqlite/statement_fwd.hpp" + +namespace store { + + +void bind_bool(utils::sqlite::statement&, const char*, const bool); +void bind_delta(utils::sqlite::statement&, const char*, + const utils::datetime::delta&); +void bind_optional_string(utils::sqlite::statement&, const char*, + const std::string&); +void bind_test_result_type(utils::sqlite::statement&, const char*, + const model::test_result_type&); +void bind_timestamp(utils::sqlite::statement&, const char*, + const utils::datetime::timestamp&); +bool column_bool(utils::sqlite::statement&, const char*); +utils::datetime::delta column_delta(utils::sqlite::statement&, const char*); +std::string column_optional_string(utils::sqlite::statement&, const char*); +model::test_result_type column_test_result_type( + utils::sqlite::statement&, const char*); +utils::datetime::timestamp column_timestamp(utils::sqlite::statement&, + const char*); + + +} // namespace store diff --git a/contrib/kyua/store/dbtypes_test.cpp b/contrib/kyua/store/dbtypes_test.cpp new file mode 100644 index 000000000000..abe229eab2b6 --- /dev/null +++ b/contrib/kyua/store/dbtypes_test.cpp @@ -0,0 +1,234 @@ +// 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 "store/dbtypes.hpp" + +#include + +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "utils/datetime.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; + + +namespace { + + +/// Validates that a particular bind_x/column_x sequence works. +/// +/// \param bind The store::bind_* function to put the value. +/// \param value The value to store and validate. +/// \param column The store::column_* function to get the value. +template< typename Type1, typename Type2, typename Type3 > +static void +do_ok_test(void (*bind)(sqlite::statement&, const char*, Type1), + Type2 value, + Type3 (*column)(sqlite::statement&, const char*)) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE test (column DONTCARE)"); + + sqlite::statement insert = db.create_statement("INSERT INTO test " + "VALUES (:v)"); + bind(insert, ":v", value); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + ATF_REQUIRE(query.step()); + ATF_REQUIRE(column(query, "column") == value); + ATF_REQUIRE(!query.step()); +} + + +/// Validates an error condition of column_*. +/// +/// \param value The invalid value to insert into the database. +/// \param column The store::column_* function to get the value. +/// \param error_regexp The expected message in the raised integrity_error. +template< typename Type1, typename Type2 > +static void +do_invalid_test(Type1 value, + Type2 (*column)(sqlite::statement&, const char*), + const std::string& error_regexp) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE test (column DONTCARE)"); + + sqlite::statement insert = db.create_statement("INSERT INTO test " + "VALUES (:v)"); + insert.bind(":v", value); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + ATF_REQUIRE(query.step()); + ATF_REQUIRE_THROW_RE(store::integrity_error, error_regexp, + column(query, "column")); + ATF_REQUIRE(!query.step()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(bool__ok); +ATF_TEST_CASE_BODY(bool__ok) +{ + do_ok_test(store::bind_bool, true, store::column_bool); + do_ok_test(store::bind_bool, false, store::column_bool); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool__get_invalid_type); +ATF_TEST_CASE_BODY(bool__get_invalid_type) +{ + do_invalid_test(123, store::column_bool, "not a string"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool__get_invalid_value); +ATF_TEST_CASE_BODY(bool__get_invalid_value) +{ + do_invalid_test("foo", store::column_bool, "Unknown boolean.*foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__ok); +ATF_TEST_CASE_BODY(delta__ok) +{ + do_ok_test(store::bind_delta, datetime::delta(15, 34), store::column_delta); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__get_invalid_type); +ATF_TEST_CASE_BODY(delta__get_invalid_type) +{ + do_invalid_test(15.6, store::column_delta, "not an integer"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(optional_string__ok); +ATF_TEST_CASE_BODY(optional_string__ok) +{ + do_ok_test(store::bind_optional_string, "", store::column_optional_string); + do_ok_test(store::bind_optional_string, "a", store::column_optional_string); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(optional_string__get_invalid_type); +ATF_TEST_CASE_BODY(optional_string__get_invalid_type) +{ + do_invalid_test(35, store::column_optional_string, "Invalid string"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__ok); +ATF_TEST_CASE_BODY(test_result_type__ok) +{ + do_ok_test(store::bind_test_result_type, + model::test_result_passed, + store::column_test_result_type); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__get_invalid_type); +ATF_TEST_CASE_BODY(test_result_type__get_invalid_type) +{ + do_invalid_test(12, store::column_test_result_type, "not a string"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__get_invalid_value); +ATF_TEST_CASE_BODY(test_result_type__get_invalid_value) +{ + do_invalid_test("foo", store::column_test_result_type, + "Unknown test result type foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__ok); +ATF_TEST_CASE_BODY(timestamp__ok) +{ + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_microseconds(0), + store::column_timestamp); + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_microseconds(123), + store::column_timestamp); + + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_values(2012, 2, 9, 23, 15, 51, 987654), + store::column_timestamp); + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_values(1980, 1, 2, 3, 4, 5, 0), + store::column_timestamp); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__get_invalid_type); +ATF_TEST_CASE_BODY(timestamp__get_invalid_type) +{ + do_invalid_test(35.6, store::column_timestamp, "not an integer"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__get_invalid_value); +ATF_TEST_CASE_BODY(timestamp__get_invalid_value) +{ + do_invalid_test(-1234, store::column_timestamp, "must be positive"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, bool__ok); + ATF_ADD_TEST_CASE(tcs, bool__get_invalid_type); + ATF_ADD_TEST_CASE(tcs, bool__get_invalid_value); + + ATF_ADD_TEST_CASE(tcs, delta__ok); + ATF_ADD_TEST_CASE(tcs, delta__get_invalid_type); + + ATF_ADD_TEST_CASE(tcs, optional_string__ok); + ATF_ADD_TEST_CASE(tcs, optional_string__get_invalid_type); + + ATF_ADD_TEST_CASE(tcs, test_result_type__ok); + ATF_ADD_TEST_CASE(tcs, test_result_type__get_invalid_type); + ATF_ADD_TEST_CASE(tcs, test_result_type__get_invalid_value); + + ATF_ADD_TEST_CASE(tcs, timestamp__ok); + ATF_ADD_TEST_CASE(tcs, timestamp__get_invalid_type); + ATF_ADD_TEST_CASE(tcs, timestamp__get_invalid_value); +} diff --git a/contrib/kyua/store/exceptions.cpp b/contrib/kyua/store/exceptions.cpp new file mode 100644 index 000000000000..7459f3db75ac --- /dev/null +++ b/contrib/kyua/store/exceptions.cpp @@ -0,0 +1,88 @@ +// 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 "store/exceptions.hpp" + +#include "utils/format/macros.hpp" + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +store::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +store::error::~error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +store::integrity_error::integrity_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +store::integrity_error::~integrity_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param version Version of the current schema. +store::old_schema_error::old_schema_error(const int version) : + error(F("The database contains version %s of the schema, which is " + "stale and needs to be upgraded") % version), + _old_version(version) +{ +} + + +/// Destructor for the error. +store::old_schema_error::~old_schema_error(void) throw() +{ +} + + +/// Returns the current schema version in the database. +/// +/// \return A version number. +int +store::old_schema_error::old_version(void) const +{ + return _old_version; +} diff --git a/contrib/kyua/store/exceptions.hpp b/contrib/kyua/store/exceptions.hpp new file mode 100644 index 000000000000..e27c7a02fe3a --- /dev/null +++ b/contrib/kyua/store/exceptions.hpp @@ -0,0 +1,72 @@ +// 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 store/exceptions.hpp +/// Exception types raised by the store module. + +#if !defined(STORE_EXCEPTIONS_HPP) +#define STORE_EXCEPTIONS_HPP + +#include + +namespace store { + + +/// Base exception for store errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// The data in the database is inconsistent. +class integrity_error : public error { +public: + explicit integrity_error(const std::string&); + virtual ~integrity_error(void) throw(); +}; + + +/// The database schema is old and needs a migration. +class old_schema_error : public error { + /// Version in the database that caused this error. + int _old_version; + +public: + explicit old_schema_error(const int); + virtual ~old_schema_error(void) throw(); + + int old_version(void) const; +}; + + +} // namespace store + + +#endif // !defined(STORE_EXCEPTIONS_HPP) diff --git a/contrib/kyua/store/exceptions_test.cpp b/contrib/kyua/store/exceptions_test.cpp new file mode 100644 index 000000000000..ce364e26293c --- /dev/null +++ b/contrib/kyua/store/exceptions_test.cpp @@ -0,0 +1,65 @@ +// 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 "store/exceptions.hpp" + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const store::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integrity_error); +ATF_TEST_CASE_BODY(integrity_error) +{ + const store::integrity_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(old_schema_error); +ATF_TEST_CASE_BODY(old_schema_error) +{ + const store::old_schema_error e(15); + ATF_REQUIRE_MATCH("version 15 .*upgraded", e.what()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, integrity_error); + ATF_ADD_TEST_CASE(tcs, old_schema_error); +} diff --git a/contrib/kyua/store/layout.cpp b/contrib/kyua/store/layout.cpp new file mode 100644 index 000000000000..f69cd96cb48d --- /dev/null +++ b/contrib/kyua/store/layout.cpp @@ -0,0 +1,264 @@ +// 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 "store/layout.hpp" + +#include +#include + +#include "store/exceptions.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/fs/operations.hpp" +#include "utils/logging/macros.hpp" +#include "utils/env.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/regex.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using utils::optional; + + +namespace { + + +/// Finds the results file for the latest run of the given test suite. +/// +/// \param test_suite Identifier of the test suite to query. +/// +/// \return Path to the located database holding the most recent data for the +/// given test suite. +/// +/// \throw store::error If no previous results file can be found. +static fs::path +find_latest(const std::string& test_suite) +{ + const fs::path store_dir = layout::query_store_dir(); + try { + const text::regex preg = text::regex::compile( + F("^results.%s.[0-9]{8}-[0-9]{6}-[0-9]{6}.db$") % test_suite, 0); + + std::string latest; + + const fs::directory dir(store_dir); + for (fs::directory::const_iterator iter = dir.begin(); + iter != dir.end(); ++iter) { + const text::regex_matches matches = preg.match(iter->name); + if (matches) { + if (latest.empty() || iter->name > latest) { + latest = iter->name; + } + } else { + // Not a database file; skip. + } + } + + if (latest.empty()) + throw store::error( + F("No previous results file found for test suite %s") + % test_suite); + + return store_dir / latest; + } catch (const fs::system_error& e) { + LW(F("Failed to open store dir %s: %s") % store_dir % e.what()); + throw store::error(F("No previous results file found for test suite %s") + % test_suite); + } catch (const text::regex_error& e) { + throw store::error(e.what()); + } +} + + +/// Computes the identifier of a new tests results file. +/// +/// \param test_suite Identifier of the test suite. +/// \param when Timestamp to attach to the identifier. +/// +/// \return Identifier of the file to be created. +static std::string +new_id(const std::string& test_suite, const datetime::timestamp& when) +{ + const std::string when_datetime = when.strftime("%Y%m%d-%H%M%S"); + const int when_ms = static_cast(when.to_microseconds() % 1000000); + return F("%s.%s-%06s") % test_suite % when_datetime % when_ms; +} + + +} // anonymous namespace + + +/// Value to request the creation of a new results file with an automatic name. +/// +/// Can be passed to new_db(). +const char* layout::results_auto_create_name = "NEW"; + + +/// Value to request the opening of the latest results file. +/// +/// Can be passed to find_results(). +const char* layout::results_auto_open_name = "LATEST"; + + +/// Resolves the results file for the given identifier. +/// +/// \param id Identifier of the test suite to open. +/// +/// \return Path to the requested file, if any. +/// +/// \throw store::error If there is no matching entry. +fs::path +layout::find_results(const std::string& id) +{ + LI(F("Searching for a results file with id %s") % id); + + if (id == results_auto_open_name) { + const std::string test_suite = test_suite_for_path(fs::current_path()); + return find_latest(test_suite); + } else { + const fs::path id_as_path(id); + + if (fs::exists(id_as_path) && !fs::is_directory(id_as_path)) { + if (id_as_path.is_absolute()) + return id_as_path; + else + return id_as_path.to_absolute(); + } else if (id.find('/') == std::string::npos) { + const fs::path candidate = + query_store_dir() / (F("results.%s.db") % id); + if (fs::exists(candidate)) { + return candidate; + } else { + return find_latest(id); + } + } else { + INV(id.find('/') != std::string::npos); + return find_latest(test_suite_for_path(id_as_path)); + } + } +} + + +/// Computes the path to a new database for the given test suite. +/// +/// \param id Identifier of the test suite to create. +/// \param root Path to the root of the test suite being run, needed to properly +/// autogenerate the identifiers. +/// +/// \return Identifier of the created results file, if applicable, and the path +/// to such file. +layout::results_id_file_pair +layout::new_db(const std::string& id, const fs::path& root) +{ + std::string generated_id; + optional< fs::path > path; + + if (id == results_auto_create_name) { + generated_id = new_id(test_suite_for_path(root), + datetime::timestamp::now()); + path = query_store_dir() / (F("results.%s.db") % generated_id); + fs::mkdir_p(path.get().branch_path(), 0755); + } else { + path = fs::path(id); + } + + return std::make_pair(generated_id, path.get()); +} + + +/// Computes the path to a new database for the given test suite. +/// +/// \param root Path to the root of the test suite being run; needed to properly +/// autogenerate the identifiers. +/// \param when Timestamp for the test suite being run; needed to properly +/// autogenerate the identifiers. +/// +/// \return Identifier of the created results file, if applicable, and the path +/// to such file. +fs::path +layout::new_db_for_migration(const fs::path& root, + const datetime::timestamp& when) +{ + const std::string generated_id = new_id(test_suite_for_path(root), when); + const fs::path path = query_store_dir() / ( + F("results.%s.db") % generated_id); + fs::mkdir_p(path.branch_path(), 0755); + return path; +} + + +/// Gets the path to the store directory. +/// +/// Note that this function does not create the determined directory. It is the +/// responsibility of the caller to do so. +/// +/// \return Path to the directory holding all the database files. +fs::path +layout::query_store_dir(void) +{ + const optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path& home_path = home.get(); + if (home_path.is_absolute()) + return home_path / ".kyua/store"; + else + return home_path.to_absolute() / ".kyua/store"; + } else { + LW("HOME not defined; creating store database in current " + "directory"); + return fs::current_path(); + } +} + + +/// Returns the test suite name for the current directory. +/// +/// \return The identifier of the current test suite. +std::string +layout::test_suite_for_path(const fs::path& path) +{ + std::string test_suite; + if (path.is_absolute()) + test_suite = path.str(); + else + test_suite = path.to_absolute().str(); + PRE(!test_suite.empty() && test_suite[0] == '/'); + + std::replace(test_suite.begin(), test_suite.end(), '/', '_'); + test_suite.erase(0, 1); + + return test_suite; +} diff --git a/contrib/kyua/store/layout.hpp b/contrib/kyua/store/layout.hpp new file mode 100644 index 000000000000..48ab89c45104 --- /dev/null +++ b/contrib/kyua/store/layout.hpp @@ -0,0 +1,84 @@ +// 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 store/layout.hpp +/// File system layout definition for the Kyua data files. +/// +/// Tests results files are all stored in a centralized directory by default. +/// In the general case, we do not want the user to have to worry about files: +/// we expose an identifier-based interface where each tests results file has a +/// unique identifier. However, we also want to give full freedom to the user +/// to store such files wherever he likes so we have to deal with paths as well. +/// +/// When creating a new results file, the inputs to resolve the path can be: +/// - NEW: Automatic generation of a new results file, so we want to return its +/// public identifier and the path for internal consumption. +/// - A path: The user provided the specific location where he wants the file +/// stored, so we just obey that. There is no public identifier in this case +/// because there is no naming scheme imposed on the generated files. +/// +/// When opening an existing results file, the inputs to resolve the path can +/// be: +/// - LATEST: Given the current directory, we derive the corresponding test +/// suite name and find the latest timestamped file in the centralized +/// location. +/// - A path: If the file exists, we just open that. If it doesn't exist or if +/// it is a directory, we try to resolve that as a test suite name and locate +/// the latest matching timestamped file. +/// - Everything else: Treated as a test suite identifier, so we try to locate +/// the latest matchin timestamped file. + +#if !defined(STORE_LAYOUT_HPP) +#define STORE_LAYOUT_HPP + +#include "store/layout_fwd.hpp" + +#include + +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace store { +namespace layout { + + +extern const char* results_auto_create_name; +extern const char* results_auto_open_name; + +utils::fs::path find_results(const std::string&); +results_id_file_pair new_db(const std::string&, const utils::fs::path&); +utils::fs::path new_db_for_migration(const utils::fs::path&, + const utils::datetime::timestamp&); +utils::fs::path query_store_dir(void); +std::string test_suite_for_path(const utils::fs::path&); + + +} // namespace layout +} // namespace store + +#endif // !defined(STORE_LAYOUT_HPP) diff --git a/contrib/kyua/store/layout_fwd.hpp b/contrib/kyua/store/layout_fwd.hpp new file mode 100644 index 000000000000..72d05a27c66a --- /dev/null +++ b/contrib/kyua/store/layout_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 store/layout_fwd.hpp +/// Forward declarations for store/layout.hpp + +#if !defined(STORE_LAYOUT_FWD_HPP) +#define STORE_LAYOUT_FWD_HPP + +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace store { +namespace layout { + + +/// A pair with the user-visible ID of the results file and its path. +/// +/// It is possible for the ID (first component) to be empty in the cases where +/// the user explicitly requested to create the database in a specific path. +typedef std::pair< std::string, utils::fs::path > results_id_file_pair; + + +} // namespace layout +} // namespace store + +#endif // !defined(STORE_LAYOUT_FWD_HPP) diff --git a/contrib/kyua/store/layout_test.cpp b/contrib/kyua/store/layout_test.cpp new file mode 100644 index 000000000000..8564d3aef93c --- /dev/null +++ b/contrib/kyua/store/layout_test.cpp @@ -0,0 +1,350 @@ +// 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 "store/layout.hpp" + +extern "C" { +#include +} + +#include + +#include + +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__latest); +ATF_TEST_CASE_BODY(find_results__latest) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const std::string test_suite = layout::test_suite_for_path( + fs::current_path()); + const std::string base = (store_dir / ( + "results." + test_suite + ".")).str(); + + atf::utils::create_file(base + "20140613-194515-000000.db", ""); + ATF_REQUIRE_EQ(base + "20140613-194515-000000.db", + layout::find_results("LATEST").str()); + + atf::utils::create_file(base + "20140614-194515-123456.db", ""); + ATF_REQUIRE_EQ(base + "20140614-194515-123456.db", + layout::find_results("LATEST").str()); + + atf::utils::create_file(base + "20130614-194515-999999.db", ""); + ATF_REQUIRE_EQ(base + "20140614-194515-123456.db", + layout::find_results("LATEST").str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__directory); +ATF_TEST_CASE_BODY(find_results__directory) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const fs::path dir1("dir1/foo"); + fs::mkdir_p(dir1, 0755); + const fs::path dir2("dir1/bar"); + fs::mkdir_p(dir2, 0755); + + const std::string base1 = (store_dir / ( + "results." + layout::test_suite_for_path(dir1) + ".")).str(); + const std::string base2 = (store_dir / ( + "results." + layout::test_suite_for_path(dir2) + ".")).str(); + + atf::utils::create_file(base1 + "20140613-194515-000000.db", ""); + ATF_REQUIRE_EQ(base1 + "20140613-194515-000000.db", + layout::find_results(dir1.str()).str()); + + atf::utils::create_file(base2 + "20140615-111111-000000.db", ""); + ATF_REQUIRE_EQ(base2 + "20140615-111111-000000.db", + layout::find_results(dir2.str()).str()); + + atf::utils::create_file(base1 + "20140614-194515-123456.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(dir1.str()).str()); + + atf::utils::create_file(base1 + "20130614-194515-999999.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(dir1.str()).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__file); +ATF_TEST_CASE_BODY(find_results__file) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + atf::utils::create_file("a-file.db", ""); + ATF_REQUIRE_EQ(fs::path("a-file.db").to_absolute(), + layout::find_results("a-file.db")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__id); +ATF_TEST_CASE_BODY(find_results__id) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const fs::path dir1("dir1/foo"); + fs::mkdir_p(dir1, 0755); + const fs::path dir2("dir1/bar"); + fs::mkdir_p(dir2, 0755); + + const std::string id1 = layout::test_suite_for_path(dir1); + const std::string base1 = (store_dir / ("results." + id1 + ".")).str(); + const std::string id2 = layout::test_suite_for_path(dir2); + const std::string base2 = (store_dir / ("results." + id2 + ".")).str(); + + atf::utils::create_file(base1 + "20140613-194515-000000.db", ""); + ATF_REQUIRE_EQ(base1 + "20140613-194515-000000.db", + layout::find_results(id1).str()); + + atf::utils::create_file(base2 + "20140615-111111-000000.db", ""); + ATF_REQUIRE_EQ(base2 + "20140615-111111-000000.db", + layout::find_results(id2).str()); + + atf::utils::create_file(base1 + "20140614-194515-123456.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(id1).str()); + + atf::utils::create_file(base1 + "20130614-194515-999999.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(id1).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__id_with_timestamp); +ATF_TEST_CASE_BODY(find_results__id_with_timestamp) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const fs::path dir1("dir1/foo"); + fs::mkdir_p(dir1, 0755); + const fs::path dir2("dir1/bar"); + fs::mkdir_p(dir2, 0755); + + const std::string id1 = layout::test_suite_for_path(dir1); + const std::string base1 = (store_dir / ("results." + id1 + ".")).str(); + const std::string id2 = layout::test_suite_for_path(dir2); + const std::string base2 = (store_dir / ("results." + id2 + ".")).str(); + + atf::utils::create_file(base1 + "20140613-194515-000000.db", ""); + atf::utils::create_file(base2 + "20140615-111111-000000.db", ""); + atf::utils::create_file(base1 + "20140614-194515-123456.db", ""); + atf::utils::create_file(base1 + "20130614-194515-999999.db", ""); + + ATF_REQUIRE_MATCH( + "_dir1_foo.20140613-194515-000000.db$", + layout::find_results(id1 + ".20140613-194515-000000").str()); + + ATF_REQUIRE_MATCH( + "_dir1_foo.20140614-194515-123456.db$", + layout::find_results(id1 + ".20140614-194515-123456").str()); + + ATF_REQUIRE_MATCH( + "_dir1_bar.20140615-111111-000000.db$", + layout::find_results(id2 + ".20140615-111111-000000").str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__not_found); +ATF_TEST_CASE_BODY(find_results__not_found) +{ + ATF_REQUIRE_THROW_RE( + store::error, + "No previous results file found for test suite foo_bar", + layout::find_results("foo_bar")); + + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + ATF_REQUIRE_THROW_RE( + store::error, + "No previous results file found for test suite foo_bar", + layout::find_results("foo_bar")); + + const char* candidates[] = { + "results.foo.20140613-194515-012345.db", // Bad test suite. + "results.foo_bar.20140613-194515-012345", // Missing extension. + "foo_bar.20140613-194515-012345.db", // Missing prefix. + "results.foo_bar.2010613-194515-012345.db", // Bad date. + "results.foo_bar.20140613-19515-012345.db", // Bad time. + "results.foo_bar.20140613-194515-01245.db", // Bad microseconds. + NULL, + }; + for (const char** candidate = candidates; *candidate != NULL; ++candidate) { + std::cout << "Current candidate: " << *candidate << '\n'; + atf::utils::create_file((store_dir / *candidate).str(), ""); + ATF_REQUIRE_THROW_RE( + store::error, + "No previous results file found for test suite foo_bar", + layout::find_results("foo_bar")); + } + + atf::utils::create_file( + (store_dir / "results.foo_bar.20140613-194515-012345.db").str(), ""); + layout::find_results("foo_bar"); // Expected not to throw. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(new_db__new); +ATF_TEST_CASE_BODY(new_db__new) +{ + datetime::set_mock_now(2014, 6, 13, 19, 45, 15, 5000); + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + const layout::results_id_file_pair results = layout::new_db( + "NEW", fs::path("/some/path/to/the/suite")); + ATF_REQUIRE( fs::exists(fs::path(".kyua/store"))); + ATF_REQUIRE( fs::is_directory(fs::path(".kyua/store"))); + + const std::string id = "some_path_to_the_suite.20140613-194515-005000"; + ATF_REQUIRE_EQ(id, results.first); + ATF_REQUIRE_EQ(layout::query_store_dir() / ("results." + id + ".db"), + results.second); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(new_db__explicit); +ATF_TEST_CASE_BODY(new_db__explicit) +{ + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + const layout::results_id_file_pair results = layout::new_db( + "foo/results-file.db", fs::path("unused")); + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + ATF_REQUIRE(!fs::exists(fs::path("foo"))); + + ATF_REQUIRE(results.first.empty()); + ATF_REQUIRE_EQ(fs::path("foo/results-file.db"), results.second); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(new_db_for_migration); +ATF_TEST_CASE_BODY(new_db_for_migration) +{ + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + const fs::path results_file = layout::new_db_for_migration( + fs::path("/some/root"), + datetime::timestamp::from_values(2014, 7, 30, 10, 5, 20, 76500)); + ATF_REQUIRE( fs::exists(fs::path(".kyua/store"))); + ATF_REQUIRE( fs::is_directory(fs::path(".kyua/store"))); + + ATF_REQUIRE_EQ( + layout::query_store_dir() / + "results.some_root.20140730-100520-076500.db", + results_file); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__home_absolute); +ATF_TEST_CASE_BODY(query_store_dir__home_absolute) +{ + const fs::path home = fs::current_path() / "homedir"; + utils::setenv("HOME", home.str()); + const fs::path store_dir = layout::query_store_dir(); + ATF_REQUIRE(store_dir.is_absolute()); + ATF_REQUIRE_EQ(home / ".kyua/store", store_dir); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__home_relative); +ATF_TEST_CASE_BODY(query_store_dir__home_relative) +{ + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + const fs::path store_dir = layout::query_store_dir(); + ATF_REQUIRE(store_dir.is_absolute()); + ATF_REQUIRE_MATCH((home / ".kyua/store").str(), store_dir.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__no_home); +ATF_TEST_CASE_BODY(query_store_dir__no_home) +{ + utils::unsetenv("HOME"); + ATF_REQUIRE_EQ(fs::current_path(), layout::query_store_dir()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_suite_for_path__absolute); +ATF_TEST_CASE_BODY(test_suite_for_path__absolute) +{ + ATF_REQUIRE_EQ("dir1_dir2_dir3", + layout::test_suite_for_path(fs::path("/dir1/dir2/dir3"))); + ATF_REQUIRE_EQ("dir1", + layout::test_suite_for_path(fs::path("/dir1"))); + ATF_REQUIRE_EQ("dir1_dir2", + layout::test_suite_for_path(fs::path("/dir1///dir2"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_suite_for_path__relative); +ATF_TEST_CASE_BODY(test_suite_for_path__relative) +{ + const std::string test_suite = layout::test_suite_for_path( + fs::path("foo/bar")); + ATF_REQUIRE_MATCH("_foo_bar$", test_suite); + ATF_REQUIRE_MATCH("^[^_]", test_suite); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, find_results__latest); + ATF_ADD_TEST_CASE(tcs, find_results__directory); + ATF_ADD_TEST_CASE(tcs, find_results__file); + ATF_ADD_TEST_CASE(tcs, find_results__id); + ATF_ADD_TEST_CASE(tcs, find_results__id_with_timestamp); + ATF_ADD_TEST_CASE(tcs, find_results__not_found); + + ATF_ADD_TEST_CASE(tcs, new_db__new); + ATF_ADD_TEST_CASE(tcs, new_db__explicit); + + ATF_ADD_TEST_CASE(tcs, new_db_for_migration); + + ATF_ADD_TEST_CASE(tcs, query_store_dir__home_absolute); + ATF_ADD_TEST_CASE(tcs, query_store_dir__home_relative); + ATF_ADD_TEST_CASE(tcs, query_store_dir__no_home); + + ATF_ADD_TEST_CASE(tcs, test_suite_for_path__absolute); + ATF_ADD_TEST_CASE(tcs, test_suite_for_path__relative); +} diff --git a/contrib/kyua/store/metadata.cpp b/contrib/kyua/store/metadata.cpp new file mode 100644 index 000000000000..2d90fe8cb267 --- /dev/null +++ b/contrib/kyua/store/metadata.cpp @@ -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. + +#include "store/metadata.hpp" + +#include "store/exceptions.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Fetches an integer column from a statement of the 'metadata' table. +/// +/// \param stmt The statement from which to get the column value. +/// \param column The name of the column to retrieve. +/// +/// \return The value of the column. +/// +/// \throw store::integrity_error If there is a problem fetching the value +/// caused by an invalid schema or data. +static int64_t +int64_column(sqlite::statement& stmt, const char* column) +{ + int index; + try { + index = stmt.column_id(column); + } catch (const sqlite::invalid_column_error& e) { + UNREACHABLE_MSG("Invalid column specification; the SELECT statement " + "should have caught this"); + } + if (stmt.column_type(index) != sqlite::type_integer) + throw store::integrity_error(F("The '%s' column in 'metadata' table " + "has an invalid type") % column); + return stmt.column_int64(index); +} + + +} // anonymous namespace + + +/// Constructs a new metadata object. +/// +/// \param schema_version_ The schema version. +/// \param timestamp_ The time at which this version was created. +store::metadata::metadata(const int schema_version_, const int64_t timestamp_) : + _schema_version(schema_version_), + _timestamp(timestamp_) +{ +} + + +/// Returns the timestamp of this entry. +/// +/// \return The timestamp in this metadata entry. +int64_t +store::metadata::timestamp(void) const +{ + return _timestamp; +} + + +/// Returns the schema version. +/// +/// \return The schema version in this metadata entry. +int +store::metadata::schema_version(void) const +{ + return _schema_version; +} + + +/// Reads the latest metadata entry from the database. +/// +/// \param db The database from which to read the metadata from. +/// +/// \return The current metadata of the database. It is not OK for the metadata +/// table to be empty, so this is guaranteed to return a value unless there is +/// an error. +/// +/// \throw store::integrity_error If the metadata in the database is empty, +/// has an invalid schema or its contents are bogus. +store::metadata +store::metadata::fetch_latest(sqlite::database& db) +{ + try { + sqlite::statement stmt = db.create_statement( + "SELECT schema_version, timestamp FROM metadata " + "ORDER BY schema_version DESC LIMIT 1"); + if (!stmt.step()) + throw store::integrity_error("The 'metadata' table is empty"); + + const int schema_version_ = + static_cast< int >(int64_column(stmt, "schema_version")); + const int64_t timestamp_ = int64_column(stmt, "timestamp"); + + if (stmt.step()) + UNREACHABLE_MSG("Got more than one result from a query that " + "does not permit this; any pragmas defined?"); + + return metadata(schema_version_, timestamp_); + } catch (const sqlite::error& e) { + throw store::integrity_error(F("Invalid metadata schema: %s") % + e.what()); + } +} diff --git a/contrib/kyua/store/metadata.hpp b/contrib/kyua/store/metadata.hpp new file mode 100644 index 000000000000..c155af6d5897 --- /dev/null +++ b/contrib/kyua/store/metadata.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 store/metadata.hpp +/// Representation of the database metadata. + +#if !defined(STORE_METADATA_HPP) +#define STORE_METADATA_HPP + +#include "store/metadata_fwd.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/sqlite/database_fwd.hpp" + +namespace store { + + +/// Representation of the database metadata. +class metadata { + /// Current version of the database schema. + int _schema_version; + + /// Timestamp of the last metadata entry in the database. + int64_t _timestamp; + + metadata(const int, const int64_t); + +public: + int64_t timestamp(void) const; + int schema_version(void) const; + + static metadata fetch_latest(utils::sqlite::database&); +}; + + +} // namespace store + +#endif // !defined(STORE_METADATA_HPP) diff --git a/contrib/kyua/store/metadata_fwd.hpp b/contrib/kyua/store/metadata_fwd.hpp new file mode 100644 index 000000000000..39aa8c2448d4 --- /dev/null +++ b/contrib/kyua/store/metadata_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 store/metadata_fwd.hpp +/// Forward declarations for store/metadata.hpp + +#if !defined(STORE_METADATA_FWD_HPP) +#define STORE_METADATA_FWD_HPP + +namespace store { + + +class metadata; + + +} // namespace store + +#endif // !defined(STORE_METADATA_FWD_HPP) diff --git a/contrib/kyua/store/metadata_test.cpp b/contrib/kyua/store/metadata_test.cpp new file mode 100644 index 000000000000..e32f1ae38dfb --- /dev/null +++ b/contrib/kyua/store/metadata_test.cpp @@ -0,0 +1,154 @@ +// 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 "store/metadata.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/write_backend.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" + +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Creates a test in-memory database. +/// +/// When using this function, you must define a 'require.files' property in this +/// case pointing to store::detail::schema_file(). +/// +/// The database created by this function mimics a real complete database, but +/// without any predefined values. I.e. for our particular case, the metadata +/// table is empty. +/// +/// \return A SQLite database instance. +static sqlite::database +create_database(void) +{ + sqlite::database db = sqlite::database::in_memory(); + store::detail::initialize(db); + db.exec("DELETE FROM metadata"); + return db; +} + + +} // anonymous namespace + + +ATF_TEST_CASE(fetch_latest__ok); +ATF_TEST_CASE_HEAD(fetch_latest__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(fetch_latest__ok) +{ + sqlite::database db = create_database(); + db.exec("INSERT INTO metadata (schema_version, timestamp) " + "VALUES (512, 5678)"); + db.exec("INSERT INTO metadata (schema_version, timestamp) " + "VALUES (256, 1234)"); + + const store::metadata metadata = store::metadata::fetch_latest(db); + ATF_REQUIRE_EQ(5678L, metadata.timestamp()); + ATF_REQUIRE_EQ(512, metadata.schema_version()); +} + + +ATF_TEST_CASE(fetch_latest__empty_metadata); +ATF_TEST_CASE_HEAD(fetch_latest__empty_metadata) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(fetch_latest__empty_metadata) +{ + sqlite::database db = create_database(); + ATF_REQUIRE_THROW_RE(store::integrity_error, "metadata.*empty", + store::metadata::fetch_latest(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(fetch_latest__no_timestamp); +ATF_TEST_CASE_BODY(fetch_latest__no_timestamp) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE metadata (schema_version INTEGER)"); + db.exec("INSERT INTO metadata VALUES (3)"); + + ATF_REQUIRE_THROW_RE(store::integrity_error, + "Invalid metadata.*timestamp", + store::metadata::fetch_latest(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(fetch_latest__no_schema_version); +ATF_TEST_CASE_BODY(fetch_latest__no_schema_version) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE metadata (timestamp INTEGER)"); + db.exec("INSERT INTO metadata VALUES (3)"); + + ATF_REQUIRE_THROW_RE(store::integrity_error, + "Invalid metadata.*schema_version", + store::metadata::fetch_latest(db)); +} + + +ATF_TEST_CASE(fetch_latest__invalid_timestamp); +ATF_TEST_CASE_HEAD(fetch_latest__invalid_timestamp) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(fetch_latest__invalid_timestamp) +{ + sqlite::database db = create_database(); + db.exec("INSERT INTO metadata (schema_version, timestamp) " + "VALUES (3, 'foo')"); + + ATF_REQUIRE_THROW_RE(store::integrity_error, + "timestamp.*invalid type", + store::metadata::fetch_latest(db)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, fetch_latest__ok); + ATF_ADD_TEST_CASE(tcs, fetch_latest__empty_metadata); + ATF_ADD_TEST_CASE(tcs, fetch_latest__no_timestamp); + ATF_ADD_TEST_CASE(tcs, fetch_latest__no_schema_version); + ATF_ADD_TEST_CASE(tcs, fetch_latest__invalid_timestamp); +} diff --git a/contrib/kyua/store/migrate.cpp b/contrib/kyua/store/migrate.cpp new file mode 100644 index 000000000000..9ec97c231184 --- /dev/null +++ b/contrib/kyua/store/migrate.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 "store/migrate.hpp" + +#include + +#include "store/dbtypes.hpp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/metadata.hpp" +#include "store/read_backend.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/env.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/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/text/operations.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Schema version at which we switched to results files. +const int first_chunked_schema_version = 3; + + +/// Queries the schema version of the given database. +/// +/// \param file The database from which to query the schema version. +/// +/// \return The schema version number. +static int +get_schema_version(const fs::path& file) +{ + sqlite::database db = store::detail::open_and_setup( + file, sqlite::open_readonly); + return store::metadata::fetch_latest(db).schema_version(); +} + + +/// Performs a single migration step. +/// +/// Both action_id and old_database are little hacks to support the migration +/// from the historical database to chunked files. We'd use a more generic +/// "replacements" map, but it's not worth it. +/// +/// \param file Database on which to apply the migration step. +/// \param version_from Current schema version in the database. +/// \param version_to Schema version to migrate to. +/// \param action_id If not none, replace ACTION_ID in the migration file with +/// this value. +/// \param old_database If not none, replace OLD_DATABASE in the migration +/// file with this value. +/// +/// \throw error If there is a problem applying the migration. +static void +migrate_schema_step(const fs::path& file, + const int version_from, + const int version_to, + const optional< int64_t > action_id = none, + const optional< fs::path > old_database = none) +{ + LI(F("Migrating schema of %s from version %s to %s") % file % version_from + % version_to); + + PRE(version_to == version_from + 1); + + sqlite::database db = store::detail::open_and_setup( + file, sqlite::open_readwrite); + + const fs::path migration = store::detail::migration_file(version_from, + version_to); + + std::string migration_string; + try { + migration_string = utils::read_file(migration); + } catch (const std::runtime_error& unused_e) { + throw store::error(F("Cannot read migration file '%s'") % migration); + } + if (action_id) { + migration_string = text::replace_all(migration_string, "@ACTION_ID@", + F("%s") % action_id.get()); + } + if (old_database) { + migration_string = text::replace_all(migration_string, "@OLD_DATABASE@", + old_database.get().str()); + } + try { + db.exec(migration_string); + } catch (const sqlite::error& e) { + throw store::error(F("Schema migration failed: %s") % e.what()); + } +} + + +/// Given a historical database, chunks it up into results files. +/// +/// The given database is DELETED on success given that it will have been +/// split up into various different files. +/// +/// \param old_file Path to the old database. +static void +chunk_database(const fs::path& old_file) +{ + PRE(get_schema_version(old_file) == first_chunked_schema_version - 1); + + LI(F("Need to split %s into per-action files") % old_file); + + sqlite::database old_db = store::detail::open_and_setup( + old_file, sqlite::open_readonly); + + sqlite::statement actions_stmt = old_db.create_statement( + "SELECT action_id, cwd FROM actions NATURAL JOIN contexts"); + + sqlite::statement start_time_stmt = old_db.create_statement( + "SELECT test_results.start_time AS start_time " + "FROM test_programs " + " JOIN test_cases " + " ON test_programs.test_program_id == test_cases.test_program_id" + " JOIN test_results " + " ON test_cases.test_case_id == test_results.test_case_id " + "WHERE test_programs.action_id == :action_id " + "ORDER BY start_time LIMIT 1"); + + while (actions_stmt.step()) { + const int64_t action_id = actions_stmt.safe_column_int64("action_id"); + const fs::path cwd(actions_stmt.safe_column_text("cwd")); + + LI(F("Extracting action %s") % action_id); + + start_time_stmt.reset(); + start_time_stmt.bind(":action_id", action_id); + if (!start_time_stmt.step()) { + LI(F("Skipping empty action %s") % action_id); + continue; + } + const datetime::timestamp start_time = store::column_timestamp( + start_time_stmt, "start_time"); + start_time_stmt.step_without_results(); + + const fs::path new_file = store::layout::new_db_for_migration( + cwd, start_time); + if (fs::exists(new_file)) { + LI(F("Skipping action because %s already exists") % new_file); + continue; + } + + LI(F("Creating %s for previous action %s") % new_file % action_id); + + try { + fs::mkdir_p(new_file.branch_path(), 0755); + sqlite::database db = store::detail::open_and_setup( + new_file, sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + db.close(); + migrate_schema_step(new_file, + first_chunked_schema_version - 1, + first_chunked_schema_version, + utils::make_optional(action_id), + utils::make_optional(old_file)); + } catch (...) { + // TODO(jmmv): Handle this better. + fs::unlink(new_file); + } + } + + fs::unlink(old_file); +} + + +} // anonymous namespace + + +/// Calculates the path to a schema migration file. +/// +/// \param version_from The version from which the database is being upgraded. +/// \param version_to The version to which the database is being upgraded. +/// +/// \return The path to the installed migrate_vX_vY.sql file. +fs::path +store::detail::migration_file(const int version_from, const int version_to) +{ + return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR)) + / (F("migrate_v%s_v%s.sql") % version_from % version_to); +} + + +/// Backs up a database for schema migration purposes. +/// +/// \todo We should probably use the SQLite backup API instead of doing a raw +/// file copy. We issue our backup call with the database already open, but +/// because it is quiescent, it's OK to do so. +/// +/// \param source Location of the database to be backed up. +/// \param old_version Version of the database's CURRENT schema, used to +/// determine the name of the backup file. +/// +/// \throw error If there is a problem during the backup. +void +store::detail::backup_database(const fs::path& source, const int old_version) +{ + const fs::path target(F("%s.v%s.backup") % source.str() % old_version); + + LI(F("Backing up database %s to %s") % source % target); + try { + fs::copy(source, target); + } catch (const fs::error& e) { + throw store::error(e.what()); + } +} + + +/// Migrates the schema of a database to the current version. +/// +/// The algorithm implemented here performs a migration step for every +/// intermediate version between the schema version in the database to the +/// version implemented in this file. This should permit upgrades from +/// arbitrary old databases. +/// +/// \param file The database whose schema to upgrade. +/// +/// \throw error If there is a problem with the migration. +void +store::migrate_schema(const utils::fs::path& file) +{ + const int version_from = get_schema_version(file); + const int version_to = detail::current_schema_version; + if (version_from == version_to) { + throw error(F("Database already at schema version %s; migration not " + "needed") % version_from); + } else if (version_from > version_to) { + throw error(F("Database at schema version %s, which is newer than the " + "supported version %s") % version_from % version_to); + } + + detail::backup_database(file, version_from); + + int i; + for (i = version_from; i < first_chunked_schema_version - 1; ++i) { + migrate_schema_step(file, i, i + 1); + } + chunk_database(file); + INV(version_to == first_chunked_schema_version); +} diff --git a/contrib/kyua/store/migrate.hpp b/contrib/kyua/store/migrate.hpp new file mode 100644 index 000000000000..a2622edc0f87 --- /dev/null +++ b/contrib/kyua/store/migrate.hpp @@ -0,0 +1,55 @@ +// 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 store/migrate.hpp +/// Utilities to upgrade a database with an old schema to the latest one. + +#if !defined(STORE_MIGRATE_HPP) +#define STORE_MIGRATE_HPP + +#include "utils/fs/path_fwd.hpp" + +namespace store { + + +namespace detail { + + +utils::fs::path migration_file(const int, const int); +void backup_database(const utils::fs::path&, const int); + + +} // anonymous namespace + + +void migrate_schema(const utils::fs::path&); + + +} // namespace store + +#endif // !defined(STORE_MIGRATE_HPP) diff --git a/contrib/kyua/store/migrate_test.cpp b/contrib/kyua/store/migrate_test.cpp new file mode 100644 index 000000000000..b45cc9e5e39e --- /dev/null +++ b/contrib/kyua/store/migrate_test.cpp @@ -0,0 +1,132 @@ +// 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 "store/migrate.hpp" + +extern "C" { +#include +} + +#include + +#include "store/exceptions.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__ok); +ATF_TEST_CASE_BODY(detail__backup_database__ok) +{ + atf::utils::create_file("test.db", "The DB\n"); + store::detail::backup_database(fs::path("test.db"), 13); + ATF_REQUIRE(fs::exists(fs::path("test.db"))); + ATF_REQUIRE(fs::exists(fs::path("test.db.v13.backup"))); + ATF_REQUIRE(atf::utils::compare_file("test.db.v13.backup", "The DB\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__ok_overwrite); +ATF_TEST_CASE_BODY(detail__backup_database__ok_overwrite) +{ + atf::utils::create_file("test.db", "Original contents"); + atf::utils::create_file("test.db.v1.backup", "Overwrite me"); + store::detail::backup_database(fs::path("test.db"), 1); + ATF_REQUIRE(fs::exists(fs::path("test.db"))); + ATF_REQUIRE(fs::exists(fs::path("test.db.v1.backup"))); + ATF_REQUIRE(atf::utils::compare_file("test.db.v1.backup", + "Original contents")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__fail_open); +ATF_TEST_CASE_BODY(detail__backup_database__fail_open) +{ + ATF_REQUIRE_THROW_RE(store::error, "Cannot open.*foo.db", + store::detail::backup_database(fs::path("foo.db"), 5)); +} + + +ATF_TEST_CASE_WITH_CLEANUP(detail__backup_database__fail_create); +ATF_TEST_CASE_HEAD(detail__backup_database__fail_create) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(detail__backup_database__fail_create) +{ + ATF_REQUIRE(::mkdir("dir", 0755) != -1); + atf::utils::create_file("dir/test.db", "Does not need to be valid"); + ATF_REQUIRE(::chmod("dir", 0111) != -1); + ATF_REQUIRE_THROW_RE( + store::error, "Cannot create.*dir/test.db.v13.backup", + store::detail::backup_database(fs::path("dir/test.db"), 13)); +} +ATF_TEST_CASE_CLEANUP(detail__backup_database__fail_create) +{ + 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(detail__migration_file__builtin); +ATF_TEST_CASE_BODY(detail__migration_file__builtin) +{ + utils::unsetenv("KYUA_STOREDIR"); + ATF_REQUIRE_EQ(fs::path(KYUA_STOREDIR) / "migrate_v5_v9.sql", + store::detail::migration_file(5, 9)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__migration_file__overriden); +ATF_TEST_CASE_BODY(detail__migration_file__overriden) +{ + utils::setenv("KYUA_STOREDIR", "/tmp/test"); + ATF_REQUIRE_EQ(fs::path("/tmp/test/migrate_v5_v9.sql"), + store::detail::migration_file(5, 9)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__backup_database__ok); + ATF_ADD_TEST_CASE(tcs, detail__backup_database__ok_overwrite); + ATF_ADD_TEST_CASE(tcs, detail__backup_database__fail_open); + ATF_ADD_TEST_CASE(tcs, detail__backup_database__fail_create); + + ATF_ADD_TEST_CASE(tcs, detail__migration_file__builtin); + ATF_ADD_TEST_CASE(tcs, detail__migration_file__overriden); + + // Tests for migrate_schema are in schema_inttest. This is because, for + // such tests to be meaningful, they need to be integration tests and don't + // really fit the goal of this unit-test module. +} diff --git a/contrib/kyua/store/migrate_v1_v2.sql b/contrib/kyua/store/migrate_v1_v2.sql new file mode 100644 index 000000000000..52d2f6a8e00c --- /dev/null +++ b/contrib/kyua/store/migrate_v1_v2.sql @@ -0,0 +1,357 @@ +-- Copyright 2013 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 store/v1-to-v2.sql +-- Migration of a database with version 1 of the schema to version 2. +-- +-- Version 2 appeared in revision 9a73561a1e3975bba4cbfd19aee6b2365a39519e +-- and its changes were: +-- +-- * Changed the primary key of the metadata table to be the +-- schema_version, not the timestamp. Because timestamps only have +-- second resolution, the old schema made testing of schema migrations +-- difficult. +-- +-- * Introduced the metadatas table, which holds the metadata of all test +-- programs and test cases in an abstract manner regardless of their +-- interface. +-- +-- * Added the metadata_id field to the test_programs and test_cases +-- tables, referencing the new metadatas table. +-- +-- * Changed the precision of the timeout metadata field to be in seconds +-- rather than in microseconds. There is no data loss, and the code that +-- writes the metadata is simplified. +-- +-- * Removed the atf_* and plain_* tables. +-- +-- * Added missing indexes to improve the performance of reports. +-- +-- * Added missing column affinities to the absolute_path and relative_path +-- columns of the test_programs table. + + +-- TODO(jmmv): Implement addition of missing affinities. + + +-- +-- Change primary key of the metadata table. +-- + + +CREATE TABLE new_metadata ( + schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1), + timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0) +); + +INSERT INTO new_metadata (schema_version, timestamp) + SELECT schema_version, timestamp FROM metadata; + +DROP TABLE metadata; +ALTER TABLE new_metadata RENAME TO metadata; + + +-- +-- Add the new tables, columns and indexes. +-- + + +CREATE TABLE metadatas ( + metadata_id INTEGER NOT NULL, + property_name TEXT NOT NULL, + property_value TEXT, + + PRIMARY KEY (metadata_id, property_name) +); + + +-- Upgrade the test_programs table by adding missing column affinities and +-- the new metadata_id column. +CREATE TABLE new_test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + action_id INTEGER REFERENCES actions, + + absolute_path TEXT NOT NULL, + root TEXT NOT NULL, + relative_path TEXT NOT NULL, + test_suite_name TEXT NOT NULL, + metadata_id INTEGER, + interface TEXT NOT NULL +); +PRAGMA foreign_keys = OFF; +INSERT INTO new_test_programs (test_program_id, action_id, absolute_path, + root, relative_path, test_suite_name, + interface) + SELECT test_program_id, action_id, absolute_path, root, relative_path, + test_suite_name, interface FROM test_programs; +DROP TABLE test_programs; +ALTER TABLE new_test_programs RENAME TO test_programs; +PRAGMA foreign_keys = ON; + + +ALTER TABLE test_cases ADD COLUMN metadata_id INTEGER; + + +CREATE INDEX index_metadatas_by_id + ON metadatas (metadata_id); +CREATE INDEX index_test_programs_by_action_id + ON test_programs (action_id); +CREATE INDEX index_test_cases_by_test_programs_id + ON test_cases (test_program_id); + + +-- +-- Data migration +-- +-- This is, by far, the trickiest part of the migration. +-- TODO(jmmv): Describe the trickiness in here. +-- + + +-- Auxiliary table to construct the final contents of the metadatas table. +-- +-- We construct the contents by writing a row for every metadata property of +-- every test program and test case. Entries corresponding to a test program +-- will have the test_program_id field set to not NULL and entries corresponding +-- to test cases will have the test_case_id set to not NULL. +-- +-- The tricky part, however, is to create the individual identifiers for every +-- metadata entry. We do this by picking the minimum ROWID of a particular set +-- of properties that map to a single test_program_id or test_case_id. +CREATE TABLE tmp_metadatas ( + test_program_id INTEGER DEFAULT NULL, + test_case_id INTEGER DEFAULT NULL, + interface TEXT NOT NULL, + property_name TEXT NOT NULL, + property_value TEXT NOT NULL, + + UNIQUE (test_program_id, test_case_id, property_name) +); +CREATE INDEX index_tmp_metadatas_by_test_case_id + ON tmp_metadatas (test_case_id); +CREATE INDEX index_tmp_metadatas_by_test_program_id + ON tmp_metadatas (test_program_id); + + +-- Populate default metadata values for all test programs and test cases. +-- +-- We do this first to ensure that all test programs and test cases have +-- explicit values for their metadata. Because we want to keep historical data +-- for the tests, we must record these values unconditionally instead of relying +-- on the built-in values in the code. +-- +-- Once this is done, we override any values explicity set by the tests. +CREATE TABLE tmp_default_metadata ( + default_name TEXT PRIMARY KEY, + default_value TEXT NOT NULL +); +INSERT INTO tmp_default_metadata VALUES ('allowed_architectures', ''); +INSERT INTO tmp_default_metadata VALUES ('allowed_platforms', ''); +INSERT INTO tmp_default_metadata VALUES ('description', ''); +INSERT INTO tmp_default_metadata VALUES ('has_cleanup', 'false'); +INSERT INTO tmp_default_metadata VALUES ('required_configs', ''); +INSERT INTO tmp_default_metadata VALUES ('required_files', ''); +INSERT INTO tmp_default_metadata VALUES ('required_memory', '0'); +INSERT INTO tmp_default_metadata VALUES ('required_programs', ''); +INSERT INTO tmp_default_metadata VALUES ('required_user', ''); +INSERT INTO tmp_default_metadata VALUES ('timeout', '300'); +INSERT INTO tmp_metadatas + SELECT test_program_id, NULL, interface, default_name, default_value + FROM test_programs JOIN tmp_default_metadata; +INSERT INTO tmp_metadatas + SELECT NULL, test_case_id, interface, default_name, default_value + FROM test_programs JOIN test_cases + ON test_cases.test_program_id = test_programs.test_program_id + JOIN tmp_default_metadata; +DROP TABLE tmp_default_metadata; + + +-- Populate metadata overrides from plain test programs. +UPDATE tmp_metadatas + SET property_value = ( + SELECT CAST(timeout / 1000000 AS TEXT) FROM plain_test_programs AS aux + WHERE aux.test_program_id = tmp_metadatas.test_program_id) + WHERE test_program_id IS NOT NULL AND property_name = 'timeout' + AND interface = 'plain'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT DISTINCT CAST(timeout / 1000000 AS TEXT) + FROM test_cases AS aux JOIN plain_test_programs + ON aux.test_program_id == plain_test_programs.test_program_id + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'timeout' + AND interface = 'plain'; + + +CREATE INDEX index_tmp_atf_test_cases_multivalues_by_test_case_id + ON atf_test_cases_multivalues (test_case_id); + + +-- Populate metadata overrides from ATF test cases. +UPDATE atf_test_cases SET description = '' WHERE description IS NULL; +UPDATE atf_test_cases SET required_user = '' WHERE required_user IS NULL; + +UPDATE tmp_metadatas + SET property_value = ( + SELECT description FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'description' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT has_cleanup FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'has_cleanup' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT CAST(timeout / 1000000 AS TEXT) FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'timeout' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT CAST(required_memory AS TEXT) FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'required_memory' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT required_user FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'required_user' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.arch') + WHERE test_case_id IS NOT NULL AND property_name = 'allowed_architectures' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.arch'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.machine') + WHERE test_case_id IS NOT NULL AND property_name = 'allowed_platforms' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.machine'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.config') + WHERE test_case_id IS NOT NULL AND property_name = 'required_configs' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.config'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.files') + WHERE test_case_id IS NOT NULL AND property_name = 'required_files' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.files'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.progs') + WHERE test_case_id IS NOT NULL AND property_name = 'required_programs' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.progs'); + + +-- Fill metadata_id pointers in the test_programs and test_cases tables. +UPDATE test_programs + SET metadata_id = ( + SELECT MIN(ROWID) FROM tmp_metadatas + WHERE tmp_metadatas.test_program_id = test_programs.test_program_id + ); +UPDATE test_cases + SET metadata_id = ( + SELECT MIN(ROWID) FROM tmp_metadatas + WHERE tmp_metadatas.test_case_id = test_cases.test_case_id + ); + + +-- Populate the metadatas table based on tmp_metadatas. +INSERT INTO metadatas (metadata_id, property_name, property_value) + SELECT ( + SELECT MIN(ROWID) FROM tmp_metadatas AS s + WHERE s.test_program_id = tmp_metadatas.test_program_id + ), property_name, property_value + FROM tmp_metadatas WHERE test_program_id IS NOT NULL; +INSERT INTO metadatas (metadata_id, property_name, property_value) + SELECT ( + SELECT MIN(ROWID) FROM tmp_metadatas AS s + WHERE s.test_case_id = tmp_metadatas.test_case_id + ), property_name, property_value + FROM tmp_metadatas WHERE test_case_id IS NOT NULL; + + +-- Drop temporary entities used during the migration. +DROP INDEX index_tmp_atf_test_cases_multivalues_by_test_case_id; +DROP INDEX index_tmp_metadatas_by_test_program_id; +DROP INDEX index_tmp_metadatas_by_test_case_id; +DROP TABLE tmp_metadatas; + + +-- +-- Drop obsolete tables. +-- + + +DROP TABLE atf_test_cases; +DROP TABLE atf_test_cases_multivalues; +DROP TABLE plain_test_programs; + + +-- +-- Update the metadata version. +-- + + +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 2); diff --git a/contrib/kyua/store/migrate_v2_v3.sql b/contrib/kyua/store/migrate_v2_v3.sql new file mode 100644 index 000000000000..7e6061cccf11 --- /dev/null +++ b/contrib/kyua/store/migrate_v2_v3.sql @@ -0,0 +1,120 @@ +-- 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 store/v2-to-v3.sql +-- Migration of a database with version 2 of the schema to version 3. +-- +-- Version 3 appeared in revision 084d740b1da635946d153475156e335ddfc4aed6 +-- and its changes were: +-- +-- * Removal of historical data. +-- +-- Because from v2 to v3 we went from a unified database to many separate +-- databases, this file is parameterized on @ACTION_ID@. The file has to +-- be executed once per action with this string replaced. + + +ATTACH DATABASE "@OLD_DATABASE@" AS old_store; + + +-- New database already contains a record for v3. Just import older entries. +INSERT INTO metadata SELECT * FROM old_store.metadata; + +INSERT INTO contexts + SELECT cwd + FROM old_store.actions + NATURAL JOIN old_store.contexts + WHERE action_id == @ACTION_ID@; + +INSERT INTO env_vars + SELECT var_name, var_value + FROM old_store.actions + NATURAL JOIN old_store.contexts + NATURAL JOIN old_store.env_vars + WHERE action_id == @ACTION_ID@; + +INSERT INTO metadatas + SELECT metadata_id, property_name, property_value + FROM old_store.metadatas + WHERE metadata_id IN ( + SELECT test_programs.metadata_id + FROM old_store.test_programs + WHERE action_id == @ACTION_ID@ + ) OR metadata_id IN ( + SELECT test_cases.metadata_id + FROM old_store.test_programs JOIN old_store.test_cases + ON test_programs.test_program_id == test_cases.test_program_id + WHERE action_id == @ACTION_ID@ + ); + +INSERT INTO test_programs + SELECT test_program_id, absolute_path, root, relative_path, + test_suite_name, metadata_id, interface + FROM old_store.test_programs + WHERE action_id == @ACTION_ID@; + +INSERT INTO test_cases + SELECT test_cases.test_case_id, test_cases.test_program_id, + test_cases.name, test_cases.metadata_id + FROM old_store.test_cases JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + +INSERT INTO test_results + SELECT test_results.test_case_id, test_results.result_type, + test_results.result_reason, test_results.start_time, test_results.end_time + FROM old_store.test_results + JOIN old_store.test_cases + ON test_results.test_case_id == test_cases.test_case_id + JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + +INSERT INTO files + SELECT files.file_id, files.contents + FROM old_store.files + JOIN old_store.test_case_files + ON files.file_id == test_case_files.file_id + JOIN old_store.test_cases + ON test_case_files.test_case_id == test_cases.test_case_id + JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + +INSERT INTO test_case_files + SELECT test_case_files.test_case_id, test_case_files.file_name, + test_case_files.file_id + FROM old_store.test_case_files + JOIN old_store.test_cases + ON test_case_files.test_case_id == test_cases.test_case_id + JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + + +DETACH DATABASE old_store; diff --git a/contrib/kyua/store/read_backend.cpp b/contrib/kyua/store/read_backend.cpp new file mode 100644 index 000000000000..bc5b860d402c --- /dev/null +++ b/contrib/kyua/store/read_backend.cpp @@ -0,0 +1,160 @@ +// 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 "store/read_backend.hpp" + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + + +/// Opens a database and defines session pragmas. +/// +/// This auxiliary function ensures that, every time we open a SQLite database, +/// we define the same set of pragmas for it. +/// +/// \param file The database file to be opened. +/// \param flags The flags for the open; see sqlite::database::open. +/// +/// \return The opened database. +/// +/// \throw store::error If there is a problem opening or creating the database. +sqlite::database +store::detail::open_and_setup(const fs::path& file, const int flags) +{ + try { + sqlite::database database = sqlite::database::open(file, flags); + database.exec("PRAGMA foreign_keys = ON"); + return database; + } catch (const sqlite::error& e) { + throw store::error(F("Cannot open '%s': %s") % file % e.what()); + } +} + + +/// Internal implementation for the backend. +struct store::read_backend::impl : utils::noncopyable { + /// The SQLite database this backend talks to. + sqlite::database database; + + /// Constructor. + /// + /// \param database_ The SQLite database instance. + /// \param metadata_ The metadata for the loaded database. This must match + /// the schema version we implement in this module; otherwise, a + /// migration is necessary. + /// + /// \throw integrity_error If the schema in the database is too modern, + /// which might indicate some form of corruption or an old binary. + /// \throw old_schema_error If the schema in the database is older than our + /// currently-implemented version and needs an upgrade. The caller can + /// use migrate_schema() to fix this problem. + impl(sqlite::database& database_, const metadata& metadata_) : + database(database_) + { + const int database_version = metadata_.schema_version(); + + if (database_version == detail::current_schema_version) { + // OK. + } else if (database_version < detail::current_schema_version) { + throw old_schema_error(database_version); + } else if (database_version > detail::current_schema_version) { + throw integrity_error( + F("Database at schema version %s, which is newer than the " + "supported version %s") + % database_version % detail::current_schema_version); + } + } +}; + + +/// Constructs a new backend. +/// +/// \param pimpl_ The internal data. +store::read_backend::read_backend(impl* pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Destructor. +store::read_backend::~read_backend(void) +{ +} + + +/// Opens a database in read-only mode. +/// +/// \param file The database file to be opened. +/// +/// \return The backend representation. +/// +/// \throw store::error If there is any problem opening the database. +store::read_backend +store::read_backend::open_ro(const fs::path& file) +{ + sqlite::database db = detail::open_and_setup(file, sqlite::open_readonly); + return read_backend(new impl(db, metadata::fetch_latest(db))); +} + + +/// Closes the SQLite database. +void +store::read_backend::close(void) +{ + _pimpl->database.close(); +} + + +/// Gets the connection to the SQLite database. +/// +/// \return A database connection. +sqlite::database& +store::read_backend::database(void) +{ + return _pimpl->database; +} + + +/// Opens a read-only transaction. +/// +/// \return A new transaction. +store::read_transaction +store::read_backend::start_read(void) +{ + return read_transaction(*this); +} diff --git a/contrib/kyua/store/read_backend.hpp b/contrib/kyua/store/read_backend.hpp new file mode 100644 index 000000000000..2ddb6e650c86 --- /dev/null +++ b/contrib/kyua/store/read_backend.hpp @@ -0,0 +1,77 @@ +// 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 store/read_backend.hpp +/// Interface to the backend database for read-only operations. + +#if !defined(STORE_READ_BACKEND_HPP) +#define STORE_READ_BACKEND_HPP + +#include "store/read_backend_fwd.hpp" + +#include + +#include "store/read_transaction_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/sqlite/database_fwd.hpp" + +namespace store { + + +namespace detail { + + +utils::sqlite::database open_and_setup(const utils::fs::path&, const int); + + +} // anonymous namespace + + +/// Public interface to the database store for read-only operations. +class read_backend { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + read_backend(impl*); + +public: + ~read_backend(void); + + static read_backend open_ro(const utils::fs::path&); + void close(void); + + utils::sqlite::database& database(void); + read_transaction start_read(void); +}; + + +} // namespace store + +#endif // !defined(STORE_READ_BACKEND_HPP) diff --git a/contrib/kyua/store/read_backend_fwd.hpp b/contrib/kyua/store/read_backend_fwd.hpp new file mode 100644 index 000000000000..4d7f5aa1429b --- /dev/null +++ b/contrib/kyua/store/read_backend_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 store/read_backend_fwd.hpp +/// Forward declarations for store/read_backend.hpp + +#if !defined(STORE_READ_BACKEND_FWD_HPP) +#define STORE_READ_BACKEND_FWD_HPP + +namespace store { + + +class read_backend; + + +} // namespace store + +#endif // !defined(STORE_READ_BACKEND_FWD_HPP) diff --git a/contrib/kyua/store/read_backend_test.cpp b/contrib/kyua/store/read_backend_test.cpp new file mode 100644 index 000000000000..062966cd226d --- /dev/null +++ b/contrib/kyua/store/read_backend_test.cpp @@ -0,0 +1,152 @@ +// 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 "store/read_backend.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "store/write_backend.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" + +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__open_and_setup__ok); +ATF_TEST_CASE_BODY(detail__open_and_setup__ok) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + db.exec("CREATE TABLE one (foo INTEGER PRIMARY KEY AUTOINCREMENT);"); + db.exec("CREATE TABLE two (foo INTEGER REFERENCES one);"); + db.close(); + } + + sqlite::database db = store::detail::open_and_setup( + fs::path("test.db"), sqlite::open_readwrite); + db.exec("INSERT INTO one (foo) VALUES (12);"); + // Ensure foreign keys have been enabled. + db.exec("INSERT INTO two (foo) VALUES (12);"); + ATF_REQUIRE_THROW(sqlite::error, + db.exec("INSERT INTO two (foo) VALUES (34);")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__open_and_setup__missing_file); +ATF_TEST_CASE_BODY(detail__open_and_setup__missing_file) +{ + ATF_REQUIRE_THROW_RE(store::error, "Cannot open 'missing.db': ", + store::detail::open_and_setup(fs::path("missing.db"), + sqlite::open_readonly)); + ATF_REQUIRE(!fs::exists(fs::path("missing.db"))); +} + + +ATF_TEST_CASE(read_backend__open_ro__ok); +ATF_TEST_CASE_HEAD(read_backend__open_ro__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(read_backend__open_ro__ok) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + } + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_backend__open_ro__missing_file); +ATF_TEST_CASE_BODY(read_backend__open_ro__missing_file) +{ + ATF_REQUIRE_THROW_RE(store::error, "Cannot open 'missing.db': ", + store::read_backend::open_ro(fs::path("missing.db"))); + ATF_REQUIRE(!fs::exists(fs::path("missing.db"))); +} + + +ATF_TEST_CASE(read_backend__open_ro__integrity_error); +ATF_TEST_CASE_HEAD(read_backend__open_ro__integrity_error) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(read_backend__open_ro__integrity_error) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + db.exec("DELETE FROM metadata"); + } + ATF_REQUIRE_THROW_RE(store::integrity_error, "metadata.*empty", + store::read_backend::open_ro(fs::path("test.db"))); +} + + +ATF_TEST_CASE(read_backend__close); +ATF_TEST_CASE_HEAD(read_backend__close) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(read_backend__close) +{ + store::write_backend::open_rw(fs::path("test.db")); // Create database. + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); + backend.close(); + ATF_REQUIRE_THROW(utils::sqlite::error, + backend.database().exec("SELECT * FROM metadata")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__open_and_setup__ok); + ATF_ADD_TEST_CASE(tcs, detail__open_and_setup__missing_file); + + ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__ok); + ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__missing_file); + ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__integrity_error); + ATF_ADD_TEST_CASE(tcs, read_backend__close); +} diff --git a/contrib/kyua/store/read_transaction.cpp b/contrib/kyua/store/read_transaction.cpp new file mode 100644 index 000000000000..68539c8346e0 --- /dev/null +++ b/contrib/kyua/store/read_transaction.cpp @@ -0,0 +1,532 @@ +// 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 "store/read_transaction.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/dbtypes.hpp" +#include "store/exceptions.hpp" +#include "store/read_backend.hpp" +#include "utils/datetime.hpp" +#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/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/transaction.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +namespace { + + +/// Retrieves the environment variables of the context. +/// +/// \param db The SQLite database. +/// +/// \return The environment variables of the specified context. +/// +/// \throw sqlite::error If there is a problem loading the variables. +static std::map< std::string, std::string > +get_env_vars(sqlite::database& db) +{ + std::map< std::string, std::string > env; + + sqlite::statement stmt = db.create_statement( + "SELECT var_name, var_value FROM env_vars"); + + while (stmt.step()) { + const std::string name = stmt.safe_column_text("var_name"); + const std::string value = stmt.safe_column_text("var_value"); + env[name] = value; + } + + return env; +} + + +/// Retrieves a metadata object. +/// +/// \param db The SQLite database. +/// \param metadata_id The identifier of the metadata. +/// +/// \return A new metadata object. +static model::metadata +get_metadata(sqlite::database& db, const int64_t metadata_id) +{ + model::metadata_builder builder; + + sqlite::statement stmt = db.create_statement( + "SELECT * FROM metadatas WHERE metadata_id == :metadata_id"); + stmt.bind(":metadata_id", metadata_id); + while (stmt.step()) { + const std::string name = stmt.safe_column_text("property_name"); + const std::string value = stmt.safe_column_text("property_value"); + builder.set_string(name, value); + } + + return builder.build(); +} + + +/// Gets a file from the database. +/// +/// \param db The database to query the file from. +/// \param file_id The identifier of the file to be queried. +/// +/// \return A textual representation of the file contents. +/// +/// \throw integrity_error If there is any problem in the loaded data or if the +/// file cannot be found. +static std::string +get_file(sqlite::database& db, const int64_t file_id) +{ + sqlite::statement stmt = db.create_statement( + "SELECT contents FROM files WHERE file_id == :file_id"); + stmt.bind(":file_id", file_id); + if (!stmt.step()) + throw store::integrity_error(F("Cannot find referenced file %s") % + file_id); + + try { + const sqlite::blob raw_contents = stmt.safe_column_blob("contents"); + const std::string contents( + static_cast< const char *>(raw_contents.memory), raw_contents.size); + + const bool more = stmt.step(); + INV(!more); + + return contents; + } catch (const sqlite::error& e) { + throw store::integrity_error(e.what()); + } +} + + +/// Gets all the test cases within a particular test program. +/// +/// \param db The database to query the information from. +/// \param test_program_id The identifier of the test program whose test cases +/// to query. +/// +/// \return The collection of loaded test cases. +/// +/// \throw integrity_error If there is any problem in the loaded data. +static model::test_cases_map +get_test_cases(sqlite::database& db, const int64_t test_program_id) +{ + model::test_cases_map_builder test_cases; + + sqlite::statement stmt = db.create_statement( + "SELECT name, metadata_id " + "FROM test_cases WHERE test_program_id == :test_program_id"); + stmt.bind(":test_program_id", test_program_id); + while (stmt.step()) { + const std::string name = stmt.safe_column_text("name"); + const int64_t metadata_id = stmt.safe_column_int64("metadata_id"); + + const model::metadata metadata = get_metadata(db, metadata_id); + LD(F("Loaded test case '%s'") % name); + test_cases.add(name, metadata); + } + + return test_cases.build(); +} + + +/// Retrieves a result from the database. +/// +/// \param stmt The statement with the data for the result to load. +/// \param type_column The name of the column containing the type of the result. +/// \param reason_column The name of the column containing the reason for the +/// result, if any. +/// +/// \return The loaded result. +/// +/// \throw integrity_error If the data in the database is invalid. +static model::test_result +parse_result(sqlite::statement& stmt, const char* type_column, + const char* reason_column) +{ + try { + const model::test_result_type type = + store::column_test_result_type(stmt, type_column); + if (type == model::test_result_passed) { + if (stmt.column_type(stmt.column_id(reason_column)) != + sqlite::type_null) + throw store::integrity_error("Result of type 'passed' has a " + "non-NULL reason"); + return model::test_result(type); + } else { + return model::test_result(type, + stmt.safe_column_text(reason_column)); + } + } catch (const sqlite::error& e) { + throw store::integrity_error(e.what()); + } +} + + +} // anonymous namespace + + +/// Loads a specific test program from the database. +/// +/// \param backend_ The store backend we are dealing with. +/// \param id The identifier of the test program to load. +/// +/// \return The instantiated test program. +/// +/// \throw integrity_error If the data read from the database cannot be properly +/// interpreted. +model::test_program_ptr +store::detail::get_test_program(read_backend& backend_, const int64_t id) +{ + sqlite::database& db = backend_.database(); + + model::test_program_ptr test_program; + sqlite::statement stmt = db.create_statement( + "SELECT * FROM test_programs WHERE test_program_id == :id"); + stmt.bind(":id", id); + stmt.step(); + const std::string interface = stmt.safe_column_text("interface"); + test_program.reset(new model::test_program( + interface, + fs::path(stmt.safe_column_text("relative_path")), + fs::path(stmt.safe_column_text("root")), + stmt.safe_column_text("test_suite_name"), + get_metadata(db, stmt.safe_column_int64("metadata_id")), + get_test_cases(db, id))); + const bool more = stmt.step(); + INV(!more); + + LD(F("Loaded test program '%s'") % test_program->relative_path()); + return test_program; +} + + +/// Internal implementation for a results iterator. +struct store::results_iterator::impl : utils::noncopyable { + /// The store backend we are dealing with. + store::read_backend _backend; + + /// The statement to iterate on. + sqlite::statement _stmt; + + /// A cache for the last loaded test program. + optional< std::pair< int64_t, model::test_program_ptr > > + _last_test_program; + + /// Whether the iterator is still valid or not. + bool _valid; + + /// Constructor. + /// + /// \param backend_ The store backend implementation. + impl(store::read_backend& backend_) : + _backend(backend_), + _stmt(backend_.database().create_statement( + "SELECT test_programs.test_program_id, " + " test_programs.interface, " + " test_cases.test_case_id, test_cases.name, " + " test_results.result_type, test_results.result_reason, " + " test_results.start_time, test_results.end_time " + "FROM test_programs " + " JOIN test_cases " + " ON test_programs.test_program_id = test_cases.test_program_id " + " JOIN test_results " + " ON test_cases.test_case_id = test_results.test_case_id " + "ORDER BY test_programs.absolute_path, test_cases.name")) + { + _valid = _stmt.step(); + } +}; + + +/// Constructor. +/// +/// \param pimpl_ The internal implementation details of the iterator. +store::results_iterator::results_iterator( + std::shared_ptr< impl > pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Destructor. +store::results_iterator::~results_iterator(void) +{ +} + + +/// Moves the iterator forward by one result. +/// +/// \return The iterator itself. +store::results_iterator& +store::results_iterator::operator++(void) +{ + _pimpl->_valid = _pimpl->_stmt.step(); + return *this; +} + + +/// Checks whether the iterator is still valid. +/// +/// \return True if there is more elements to iterate on, false otherwise. +store::results_iterator::operator bool(void) const +{ + return _pimpl->_valid; +} + + +/// Gets the test program this result belongs to. +/// +/// \return The representation of a test program. +const model::test_program_ptr +store::results_iterator::test_program(void) const +{ + const int64_t id = _pimpl->_stmt.safe_column_int64("test_program_id"); + if (!_pimpl->_last_test_program || + _pimpl->_last_test_program.get().first != id) + { + const model::test_program_ptr tp = detail::get_test_program( + _pimpl->_backend, id); + _pimpl->_last_test_program = std::make_pair(id, tp); + } + return _pimpl->_last_test_program.get().second; +} + + +/// Gets the name of the test case pointed by the iterator. +/// +/// The caller can look up the test case data by using the find() method on the +/// test program returned by test_program(). +/// +/// \return A test case name, unique within the test program. +std::string +store::results_iterator::test_case_name(void) const +{ + return _pimpl->_stmt.safe_column_text("name"); +} + + +/// Gets the result of the test case pointed by the iterator. +/// +/// \return A test case result. +model::test_result +store::results_iterator::result(void) const +{ + return parse_result(_pimpl->_stmt, "result_type", "result_reason"); +} + + +/// Gets the start time of the test case execution. +/// +/// \return The time when the test started execution. +datetime::timestamp +store::results_iterator::start_time(void) const +{ + return column_timestamp(_pimpl->_stmt, "start_time"); +} + + +/// Gets the end time of the test case execution. +/// +/// \return The time when the test finished execution. +datetime::timestamp +store::results_iterator::end_time(void) const +{ + return column_timestamp(_pimpl->_stmt, "end_time"); +} + + +/// Gets a file from a test case. +/// +/// \param db The database to query the file from. +/// \param test_case_id The identifier of the test case. +/// \param filename The name of the file to be retrieved. +/// +/// \return A textual representation of the file contents. +/// +/// \throw integrity_error If there is any problem in the loaded data or if the +/// file cannot be found. +static std::string +get_test_case_file(sqlite::database& db, const int64_t test_case_id, + const char* filename) +{ + sqlite::statement stmt = db.create_statement( + "SELECT file_id FROM test_case_files " + "WHERE test_case_id == :test_case_id AND file_name == :file_name"); + stmt.bind(":test_case_id", test_case_id); + stmt.bind(":file_name", filename); + if (stmt.step()) + return get_file(db, stmt.safe_column_int64("file_id")); + else + return ""; +} + + +/// Gets the contents of stdout of a test case. +/// +/// \return A textual representation of the stdout contents of the test case. +/// This may of course be empty if the test case didn't print anything. +std::string +store::results_iterator::stdout_contents(void) const +{ + return get_test_case_file(_pimpl->_backend.database(), + _pimpl->_stmt.safe_column_int64("test_case_id"), + "__STDOUT__"); +} + + +/// Gets the contents of stderr of a test case. +/// +/// \return A textual representation of the stderr contents of the test case. +/// This may of course be empty if the test case didn't print anything. +std::string +store::results_iterator::stderr_contents(void) const +{ + return get_test_case_file(_pimpl->_backend.database(), + _pimpl->_stmt.safe_column_int64("test_case_id"), + "__STDERR__"); +} + + +/// Internal implementation for a store read-only transaction. +struct store::read_transaction::impl : utils::noncopyable { + /// The backend instance. + store::read_backend& _backend; + + /// The SQLite database this transaction deals with. + sqlite::database _db; + + /// The backing SQLite transaction. + sqlite::transaction _tx; + + /// Opens a transaction. + /// + /// \param backend_ The backend this transaction is connected to. + impl(read_backend& backend_) : + _backend(backend_), + _db(backend_.database()), + _tx(backend_.database().begin_transaction()) + { + } +}; + + +/// Creates a new read-only transaction. +/// +/// \param backend_ The backend this transaction belongs to. +store::read_transaction::read_transaction(read_backend& backend_) : + _pimpl(new impl(backend_)) +{ +} + + +/// Destructor. +store::read_transaction::~read_transaction(void) +{ +} + + +/// Finishes the transaction. +/// +/// This actually commits the result of the transaction, but because the +/// transaction is read-only, we use a different term to denote that there is no +/// distinction between commit and rollback. +/// +/// \throw error If there is any problem when talking to the database. +void +store::read_transaction::finish(void) +{ + try { + _pimpl->_tx.commit(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Retrieves an context from the database. +/// +/// \return The retrieved context. +/// +/// \throw error If there is a problem loading the context. +model::context +store::read_transaction::get_context(void) +{ + try { + sqlite::statement stmt = _pimpl->_db.create_statement( + "SELECT cwd FROM contexts"); + if (!stmt.step()) + throw error("Error loading context: no data"); + + return model::context(fs::path(stmt.safe_column_text("cwd")), + get_env_vars(_pimpl->_db)); + } catch (const sqlite::error& e) { + throw error(F("Error loading context: %s") % e.what()); + } +} + + +/// Creates a new iterator to scan tests results. +/// +/// \return The constructed iterator. +/// +/// \throw error If there is any problem constructing the iterator. +store::results_iterator +store::read_transaction::get_results(void) +{ + try { + return results_iterator(std::shared_ptr< results_iterator::impl >( + new results_iterator::impl(_pimpl->_backend))); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} diff --git a/contrib/kyua/store/read_transaction.hpp b/contrib/kyua/store/read_transaction.hpp new file mode 100644 index 000000000000..7dd20db782eb --- /dev/null +++ b/contrib/kyua/store/read_transaction.hpp @@ -0,0 +1,120 @@ +// 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 store/read_transaction.hpp +/// Implementation of read-only transactions on the backend. + +#if !defined(STORE_READ_TRANSACTION_HPP) +#define STORE_READ_TRANSACTION_HPP + +#include "store/read_transaction_fwd.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result_fwd.hpp" +#include "store/read_backend_fwd.hpp" +#include "store/read_transaction_fwd.hpp" +#include "utils/datetime_fwd.hpp" + +namespace store { + + +namespace detail { + + +model::test_program_ptr get_test_program(read_backend&, const int64_t); + + +} // namespace detail + + +/// Iterator for the set of test case results that are part of an action. +/// +/// \todo Note that this is not a "standard" C++ iterator. I have chosen to +/// implement a different interface because it makes things easier to represent +/// an SQL statement state. Rewrite as a proper C++ iterator, inheriting from +/// std::iterator. +class results_iterator { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class read_transaction; + results_iterator(std::shared_ptr< impl >); + +public: + ~results_iterator(void); + + results_iterator& operator++(void); + operator bool(void) const; + + const model::test_program_ptr test_program(void) const; + std::string test_case_name(void) const; + model::test_result result(void) const; + utils::datetime::timestamp start_time(void) const; + utils::datetime::timestamp end_time(void) const; + + std::string stdout_contents(void) const; + std::string stderr_contents(void) const; +}; + + +/// Representation of a read-only transaction. +/// +/// Transactions are the entry place for high-level calls that access the +/// database. +class read_transaction { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class read_backend; + read_transaction(read_backend&); + +public: + ~read_transaction(void); + + void finish(void); + + model::context get_context(void); + results_iterator get_results(void); +}; + + +} // namespace store + +#endif // !defined(STORE_READ_TRANSACTION_HPP) diff --git a/contrib/kyua/store/read_transaction_fwd.hpp b/contrib/kyua/store/read_transaction_fwd.hpp new file mode 100644 index 000000000000..4aae92d9825c --- /dev/null +++ b/contrib/kyua/store/read_transaction_fwd.hpp @@ -0,0 +1,44 @@ +// 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 store/read_transaction_fwd.hpp +/// Forward declarations for store/read_transaction.hpp + +#if !defined(STORE_READ_TRANSACTION_FWD_HPP) +#define STORE_READ_TRANSACTION_FWD_HPP + +namespace store { + + +class read_transaction; +class results_iterator; + + +} // namespace store + +#endif // !defined(STORE_READ_TRANSACTION_FWD_HPP) diff --git a/contrib/kyua/store/read_transaction_test.cpp b/contrib/kyua/store/read_transaction_test.cpp new file mode 100644 index 000000000000..711faa674fbe --- /dev/null +++ b/contrib/kyua/store/read_transaction_test.cpp @@ -0,0 +1,262 @@ +// 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 "store/read_transaction.hpp" + +#include +#include + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "store/read_backend.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE(get_context__missing); +ATF_TEST_CASE_HEAD(get_context__missing) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_context__missing) +{ + store::write_backend::open_rw(fs::path("test.db")); // Create database. + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: no data", tx.get_context()); +} + + +ATF_TEST_CASE(get_context__invalid_cwd); +ATF_TEST_CASE_HEAD(get_context__invalid_cwd) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_context__invalid_cwd) +{ + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + + sqlite::statement stmt = backend.database().create_statement( + "INSERT INTO contexts (cwd) VALUES (:cwd)"); + const char buffer[10] = "foo bar"; + stmt.bind(":cwd", sqlite::blob(buffer, sizeof(buffer))); + stmt.step_without_results(); + } + + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: .*cwd.*not a string", + tx.get_context()); +} + + +ATF_TEST_CASE(get_context__invalid_env_vars); +ATF_TEST_CASE_HEAD(get_context__invalid_env_vars) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_context__invalid_env_vars) +{ + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test-bad-name.db")); + backend.database().exec("INSERT INTO contexts (cwd) " + "VALUES ('/foo/bar')"); + const char buffer[10] = "foo bar"; + + sqlite::statement stmt = backend.database().create_statement( + "INSERT INTO env_vars (var_name, var_value) " + "VALUES (:var_name, 'abc')"); + stmt.bind(":var_name", sqlite::blob(buffer, sizeof(buffer))); + stmt.step_without_results(); + } + { + store::read_backend backend = store::read_backend::open_ro( + fs::path("test-bad-name.db")); + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: .*var_name.*not a string", + tx.get_context()); + } + + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test-bad-value.db")); + backend.database().exec("INSERT INTO contexts (cwd) " + "VALUES ('/foo/bar')"); + const char buffer[10] = "foo bar"; + + sqlite::statement stmt = backend.database().create_statement( + "INSERT INTO env_vars (var_name, var_value) " + "VALUES ('abc', :var_value)"); + stmt.bind(":var_value", sqlite::blob(buffer, sizeof(buffer))); + stmt.step_without_results(); + } + { + store::read_backend backend = store::read_backend::open_ro( + fs::path("test-bad-value.db")); + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: .*var_value.*not a string", + tx.get_context()); + } +} + + +ATF_TEST_CASE(get_results__none); +ATF_TEST_CASE_HEAD(get_results__none) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_results__none) +{ + store::write_backend::open_rw(fs::path("test.db")); // Create database. + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + store::read_transaction tx = backend.start_read(); + store::results_iterator iter = tx.get_results(); + ATF_REQUIRE(!iter); +} + + +ATF_TEST_CASE(get_results__many); +ATF_TEST_CASE_HEAD(get_results__many) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_results__many) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + + store::write_transaction tx = backend.start_write(); + + const model::context context(fs::path("/foo/bar"), + std::map< std::string, std::string >()); + tx.put_context(context); + + const datetime::timestamp start_time1 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 10, 00, 0); + const datetime::timestamp end_time1 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 15, 30, 1234); + const datetime::timestamp start_time2 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 15, 40, 987); + const datetime::timestamp end_time2 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 16, 0, 0); + + atf::utils::create_file("unused.txt", "unused file\n"); + + const model::test_program test_program_1 = model::test_program_builder( + "plain", fs::path("a/prog1"), fs::path("/the/root"), "suite1") + .add_test_case("main") + .build(); + const model::test_result result_1(model::test_result_passed); + { + const int64_t tp_id = tx.put_test_program(test_program_1); + const int64_t tc_id = tx.put_test_case(test_program_1, "main", tp_id); + atf::utils::create_file("prog1.out", "stdout of prog1\n"); + tx.put_test_case_file("__STDOUT__", fs::path("prog1.out"), tc_id); + tx.put_test_case_file("unused.txt", fs::path("unused.txt"), tc_id); + tx.put_result(result_1, tc_id, start_time1, end_time1); + } + + const model::test_program test_program_2 = model::test_program_builder( + "plain", fs::path("b/prog2"), fs::path("/the/root"), "suite2") + .add_test_case("main") + .build(); + const model::test_result result_2(model::test_result_failed, + "Some text"); + { + const int64_t tp_id = tx.put_test_program(test_program_2); + const int64_t tc_id = tx.put_test_case(test_program_2, "main", tp_id); + atf::utils::create_file("prog2.err", "stderr of prog2\n"); + tx.put_test_case_file("__STDERR__", fs::path("prog2.err"), tc_id); + tx.put_test_case_file("unused.txt", fs::path("unused.txt"), tc_id); + tx.put_result(result_2, tc_id, start_time2, end_time2); + } + + tx.commit(); + backend.close(); + + store::read_backend backend2 = store::read_backend::open_ro( + fs::path("test.db")); + store::read_transaction tx2 = backend2.start_read(); + store::results_iterator iter = tx2.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_1, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ("stdout of prog1\n", iter.stdout_contents()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(result_1, iter.result()); + ATF_REQUIRE_EQ(start_time1, iter.start_time()); + ATF_REQUIRE_EQ(end_time1, iter.end_time()); + ATF_REQUIRE(++iter); + ATF_REQUIRE_EQ(test_program_2, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE_EQ("stderr of prog2\n", iter.stderr_contents()); + ATF_REQUIRE_EQ(result_2, iter.result()); + ATF_REQUIRE_EQ(start_time2, iter.start_time()); + ATF_REQUIRE_EQ(end_time2, iter.end_time()); + ATF_REQUIRE(!++iter); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, get_context__missing); + ATF_ADD_TEST_CASE(tcs, get_context__invalid_cwd); + ATF_ADD_TEST_CASE(tcs, get_context__invalid_env_vars); + + ATF_ADD_TEST_CASE(tcs, get_results__none); + ATF_ADD_TEST_CASE(tcs, get_results__many); +} diff --git a/contrib/kyua/store/schema_inttest.cpp b/contrib/kyua/store/schema_inttest.cpp new file mode 100644 index 000000000000..cd528b0c48d8 --- /dev/null +++ b/contrib/kyua/store/schema_inttest.cpp @@ -0,0 +1,492 @@ +// Copyright 2013 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 + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/migrate.hpp" +#include "store/read_backend.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/stream.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; +namespace units = utils::units; + + +namespace { + + +/// Gets a data file from the tests directory. +/// +/// We cannot use the srcdir property because the required files are not there +/// when building with an object directory. In those cases, the data files +/// remainin the source directory while the resulting test program is in the +/// object directory, thus having the wrong value for its srcdir property. +/// +/// \param name Basename of the test data file to query. +/// +/// \return The actual path to the requested data file. +static fs::path +testdata_file(const std::string& name) +{ + const fs::path testdatadir(utils::getenv_with_default( + "KYUA_STORETESTDATADIR", KYUA_STORETESTDATADIR)); + return testdatadir / name; +} + + +/// Validates the contents of the action with identifier 1. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_1(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/some/root"); + std::map< std::string, std::string > environment; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(!iter); +} + + +/// Validates the contents of the action with identifier 2. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_2(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/test/suite/root"); + std::map< std::string, std::string > environment; + environment["HOME"] = "/home/test"; + environment["PATH"] = "/bin:/usr/bin"; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + const model::test_program test_program_1 = model::test_program_builder( + "plain", fs::path("foo_test"), fs::path("/test/suite/root"), + "suite-name") + .add_test_case("main") + .build(); + const model::test_result result_1(model::test_result_passed); + + const model::test_program test_program_2 = model::test_program_builder( + "plain", fs::path("subdir/another_test"), fs::path("/test/suite/root"), + "subsuite-name") + .add_test_case("main", + model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .set_metadata(model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .build(); + const model::test_result result_2(model::test_result_failed, + "Exited with code 1"); + + const model::test_program test_program_3 = model::test_program_builder( + "plain", fs::path("subdir/bar_test"), fs::path("/test/suite/root"), + "subsuite-name") + .add_test_case("main") + .build(); + const model::test_result result_3(model::test_result_broken, + "Received signal 1"); + + const model::test_program test_program_4 = model::test_program_builder( + "plain", fs::path("top_test"), fs::path("/test/suite/root"), + "suite-name") + .add_test_case("main") + .build(); + const model::test_result result_4(model::test_result_expected_failure, + "Known bug"); + + const model::test_program test_program_5 = model::test_program_builder( + "plain", fs::path("last_test"), fs::path("/test/suite/root"), + "suite-name") + .add_test_case("main") + .build(); + const model::test_result result_5(model::test_result_skipped, + "Does not apply"); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_1, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_1, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643611000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643621000500LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_5, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_5, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643632000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643638000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_2, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_2, iter.result()); + ATF_REQUIRE_EQ("Test stdout", iter.stdout_contents()); + ATF_REQUIRE_EQ("Test stderr", iter.stderr_contents()); + ATF_REQUIRE_EQ(1357643622001200LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643622900021LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_3, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_3, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643623500000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643630981932LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_4, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_4, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643631000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643631020000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(!iter); +} + + +/// Validates the contents of the action with identifier 3. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_3(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/usr/tests"); + std::map< std::string, std::string > environment; + environment["PATH"] = "/bin:/usr/bin"; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + const model::test_program test_program_6 = model::test_program_builder( + "atf", fs::path("complex_test"), fs::path("/usr/tests"), + "suite-name") + .add_test_case("this_passes") + .add_test_case("this_fails", + model::metadata_builder() + .set_description("Test description") + .set_has_cleanup(true) + .set_required_memory(units::bytes(128)) + .set_required_user("root") + .build()) + .add_test_case("this_skips", + model::metadata_builder() + .add_allowed_architecture("powerpc") + .add_allowed_architecture("x86_64") + .add_allowed_platform("amd64") + .add_allowed_platform("macppc") + .add_required_config("X-foo") + .add_required_config("unprivileged_user") + .add_required_file(fs::path("/the/data/file")) + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("cp")) + .set_description("Test explanation") + .set_has_cleanup(true) + .set_required_memory(units::bytes(512)) + .set_required_user("unprivileged") + .set_timeout(datetime::delta(600, 0)) + .build()) + .build(); + const model::test_result result_6(model::test_result_passed); + const model::test_result result_7(model::test_result_failed, + "Some reason"); + const model::test_result result_8(model::test_result_skipped, + "Another reason"); + + const model::test_program test_program_7 = model::test_program_builder( + "atf", fs::path("simple_test"), fs::path("/usr/tests"), + "subsuite-name") + .add_test_case("main", + model::metadata_builder() + .set_description("More text") + .set_has_cleanup(true) + .set_required_memory(units::bytes(128)) + .set_required_user("unprivileged") + .build()) + .build(); + const model::test_result result_9(model::test_result_failed, + "Exited with code 1"); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_6, *iter.test_program()); + ATF_REQUIRE_EQ("this_fails", iter.test_case_name()); + ATF_REQUIRE_EQ(result_7, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357648719000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648720897182LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_6, *iter.test_program()); + ATF_REQUIRE_EQ("this_passes", iter.test_case_name()); + ATF_REQUIRE_EQ(result_6, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357648712000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648718000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_6, *iter.test_program()); + ATF_REQUIRE_EQ("this_skips", iter.test_case_name()); + ATF_REQUIRE_EQ(result_8, iter.result()); + ATF_REQUIRE_EQ("Another stdout", iter.stdout_contents()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357648729182013LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648730000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_7, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_9, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE_EQ("Another stderr", iter.stderr_contents()); + ATF_REQUIRE_EQ(1357648740120000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648750081700LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(!iter); +} + + +/// Validates the contents of the action with identifier 4. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_4(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/usr/tests"); + std::map< std::string, std::string > environment; + environment["LANG"] = "C"; + environment["PATH"] = "/bin:/usr/bin"; + environment["TERM"] = "xterm"; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + const model::test_program test_program_8 = model::test_program_builder( + "plain", fs::path("subdir/another_test"), fs::path("/usr/tests"), + "subsuite-name") + .add_test_case("main", + model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .set_metadata(model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .build(); + const model::test_result result_10(model::test_result_failed, + "Exit failure"); + + const model::test_program test_program_9 = model::test_program_builder( + "atf", fs::path("complex_test"), fs::path("/usr/tests"), + "suite-name") + .add_test_case("this_passes") + .add_test_case("this_fails", + model::metadata_builder() + .set_description("Test description") + .set_required_user("root") + .build()) + .build(); + const model::test_result result_11(model::test_result_passed); + const model::test_result result_12(model::test_result_failed, + "Some reason"); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_9, *iter.test_program()); + ATF_REQUIRE_EQ("this_fails", iter.test_case_name()); + ATF_REQUIRE_EQ(result_12, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357644397100000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357644399005000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_9, *iter.test_program()); + ATF_REQUIRE_EQ("this_passes", iter.test_case_name()); + ATF_REQUIRE_EQ(result_11, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357644396500000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357644397000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_8, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_10, iter.result()); + ATF_REQUIRE_EQ("Test stdout", iter.stdout_contents()); + ATF_REQUIRE_EQ("Test stderr", iter.stderr_contents()); + ATF_REQUIRE_EQ(1357644395000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357644396000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(!iter); +} + + +} // anonymous namespace + + +#define CURRENT_SCHEMA_TEST(dataset) \ + ATF_TEST_CASE(current_schema_ ##dataset); \ + ATF_TEST_CASE_HEAD(current_schema_ ##dataset) \ + { \ + logging::set_inmemory(); \ + const std::string required_files = \ + store::detail::schema_file().str() + " " + \ + testdata_file("testdata_v3_" #dataset ".sql").str(); \ + set_md_var("require.files", required_files); \ + } \ + ATF_TEST_CASE_BODY(current_schema_ ##dataset) \ + { \ + const fs::path testpath("test.db"); \ + \ + sqlite::database db = sqlite::database::open( \ + testpath, sqlite::open_readwrite | sqlite::open_create); \ + db.exec(utils::read_file(store::detail::schema_file())); \ + db.exec(utils::read_file(testdata_file(\ + "testdata_v3_" #dataset ".sql"))); \ + db.close(); \ + \ + check_action_ ## dataset (testpath); \ + } +CURRENT_SCHEMA_TEST(1); +CURRENT_SCHEMA_TEST(2); +CURRENT_SCHEMA_TEST(3); +CURRENT_SCHEMA_TEST(4); + + +#define MIGRATE_SCHEMA_TEST(from_version) \ + ATF_TEST_CASE(migrate_schema__from_v ##from_version); \ + ATF_TEST_CASE_HEAD(migrate_schema__from_v ##from_version) \ + { \ + logging::set_inmemory(); \ + \ + const char* schema = "schema_v" #from_version ".sql"; \ + const char* testdata = "testdata_v" #from_version ".sql"; \ + \ + std::string required_files = \ + testdata_file(schema).str() + " " + testdata_file(testdata).str(); \ + for (int i = from_version; i < store::detail::current_schema_version; \ + ++i) \ + required_files += " " + store::detail::migration_file( \ + i, i + 1).str(); \ + \ + set_md_var("require.files", required_files); \ + } \ + ATF_TEST_CASE_BODY(migrate_schema__from_v ##from_version) \ + { \ + const char* schema = "schema_v" #from_version ".sql"; \ + const char* testdata = "testdata_v" #from_version ".sql"; \ + \ + const fs::path testpath("test.db"); \ + \ + sqlite::database db = sqlite::database::open( \ + testpath, sqlite::open_readwrite | sqlite::open_create); \ + db.exec(utils::read_file(testdata_file(schema))); \ + db.exec(utils::read_file(testdata_file(testdata))); \ + db.close(); \ + \ + store::migrate_schema(fs::path("test.db")); \ + \ + check_action_2(fs::path(".kyua/store/" \ + "results.test_suite_root.20130108-111331-000000.db")); \ + check_action_3(fs::path(".kyua/store/" \ + "results.usr_tests.20130108-123832-000000.db")); \ + check_action_4(fs::path(".kyua/store/" \ + "results.usr_tests.20130108-112635-000000.db")); \ + } +MIGRATE_SCHEMA_TEST(1); +MIGRATE_SCHEMA_TEST(2); + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, current_schema_1); + ATF_ADD_TEST_CASE(tcs, current_schema_2); + ATF_ADD_TEST_CASE(tcs, current_schema_3); + ATF_ADD_TEST_CASE(tcs, current_schema_4); + + ATF_ADD_TEST_CASE(tcs, migrate_schema__from_v1); + ATF_ADD_TEST_CASE(tcs, migrate_schema__from_v2); +} diff --git a/contrib/kyua/store/schema_v1.sql b/contrib/kyua/store/schema_v1.sql new file mode 100644 index 000000000000..fbc7355bcd85 --- /dev/null +++ b/contrib/kyua/store/schema_v1.sql @@ -0,0 +1,314 @@ +-- 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 store/schema_v1.sql +-- Definition of the database schema. +-- +-- The whole contents of this file are wrapped in a transaction. We want +-- to ensure that the initial contents of the database (the table layout as +-- well as any predefined values) are written atomically to simplify error +-- handling in our code. + + +BEGIN TRANSACTION; + + +-- ------------------------------------------------------------------------- +-- Metadata. +-- ------------------------------------------------------------------------- + + +-- Database-wide properties. +-- +-- Rows in this table are immutable: modifying the metadata implies writing +-- a new record with a larger timestamp value, and never updating previous +-- records. When extracting data from this table, the only "valid" row is +-- the one with the highest timestamp. All the other rows are meaningless. +-- +-- In other words, this table keeps the history of the database metadata. +-- The only reason for doing this is for debugging purposes. It may come +-- in handy to know when a particular database-wide operation happened if +-- it turns out that the database got corrupted. +CREATE TABLE metadata ( + timestamp TIMESTAMP PRIMARY KEY CHECK (timestamp >= 0), + schema_version INTEGER NOT NULL CHECK (schema_version >= 1) +); + + +-- ------------------------------------------------------------------------- +-- Contexts. +-- ------------------------------------------------------------------------- + + +-- Execution contexts. +-- +-- A context represents the execution environment of a particular action. +-- Because every action is invoked by the user, the context may have +-- changed. We record such information for information and debugging +-- purposes. +CREATE TABLE contexts ( + context_id INTEGER PRIMARY KEY AUTOINCREMENT, + cwd TEXT NOT NULL + + -- TODO(jmmv): Record the run-time configuration. +); + + +-- Environment variables of a context. +CREATE TABLE env_vars ( + context_id INTEGER REFERENCES contexts, + var_name TEXT NOT NULL, + var_value TEXT NOT NULL, + + PRIMARY KEY (context_id, var_name) +); + + +-- ------------------------------------------------------------------------- +-- Actions. +-- ------------------------------------------------------------------------- + + +-- Representation of user-initiated actions. +-- +-- An action is an operation initiated by the user. At the moment, the +-- only operation Kyua supports is the "test" operation (in the future we +-- should be able to store, e.g. build logs). To keep things simple the +-- database schema is restricted to represent one single action. +CREATE TABLE actions ( + action_id INTEGER PRIMARY KEY AUTOINCREMENT, + context_id INTEGER REFERENCES contexts +); + + +-- ------------------------------------------------------------------------- +-- Test suites. +-- +-- The tables in this section represent all the components that form a test +-- suite. This includes data about the test suite itself (test programs +-- and test cases), and also the data about particular runs (test results). +-- +-- As you will notice, every object belongs to a particular action, has a +-- unique identifier and there is no attempt to deduplicate data. This +-- comes from the fact that a test suite is not "stable" over time: i.e. on +-- each execution of the test suite, test programs and test cases may have +-- come and gone. This has the interesting result of making the +-- distinction of a test case and a test result a pure syntactic +-- difference, because there is always a 1:1 relation. +-- +-- The code that performs the processing of the actions is the component in +-- charge of finding correlations between test programs and test cases +-- across different actions. +-- ------------------------------------------------------------------------- + + +-- Representation of a test program. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + action_id INTEGER REFERENCES actions, + + -- The absolute path to the test program. This should not be necessary + -- because it is basically the concatenation of root and relative_path. + -- However, this allows us to very easily search for test programs + -- regardless of where they were executed from. (I.e. different + -- combinations of root + relative_path can map to the same absolute path). + absolute_path NOT NULL, + + -- The path to the root of the test suite (where the Kyuafile lives). + root TEXT NOT NULL, + + -- The path to the test program, relative to the root. + relative_path NOT NULL, + + -- Name of the test suite the test program belongs to. + test_suite_name TEXT NOT NULL, + + -- The name of the test program interface. + -- + -- Note that this indicates both the interface for the test program and + -- its test cases. See below for the corresponding detail tables. + interface TEXT NOT NULL +); + + +-- Representation of a test case. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_cases ( + test_case_id INTEGER PRIMARY KEY AUTOINCREMENT, + test_program_id INTEGER REFERENCES test_programs, + name TEXT NOT NULL +); + + +-- Representation of test case results. +-- +-- Note that there is a 1:1 relation between test cases and their results. +-- This is a result of storing the information of a test case on every +-- single action. +CREATE TABLE test_results ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + result_type TEXT NOT NULL, + result_reason TEXT, + + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL +); + + +-- Collection of output files of the test case. +CREATE TABLE test_case_files ( + test_case_id INTEGER NOT NULL REFERENCES test_cases, + + -- The raw name of the file. + -- + -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold + -- the stdout and stderr of the test case, respectively. If any of + -- these are empty, there will be no corresponding entry in this table + -- (hence why we do not allow NULLs in these fields). + file_name TEXT NOT NULL, + + -- Pointer to the file itself. + file_id INTEGER NOT NULL REFERENCES files, + + PRIMARY KEY (test_case_id, file_name) +); + + +-- ------------------------------------------------------------------------- +-- Detail tables for the 'atf' test interface. +-- ------------------------------------------------------------------------- + + +-- Properties specific to 'atf' test cases. +-- +-- This table contains the representation of singly-valued properties such +-- as 'timeout'. Properties that can have more than one (textual) value +-- are stored in the atf_test_cases_multivalues table. +-- +-- Note that all properties can be NULL because test cases are not required +-- to define them. +CREATE TABLE atf_test_cases ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + + -- Free-form description of the text case. + description TEXT, + + -- Either 'true' or 'false', indicating whether the test case has a + -- cleanup routine or not. + has_cleanup TEXT, + + -- The timeout for the test case in microseconds. + timeout INTEGER, + + -- The amount of physical memory required by the test case. + required_memory INTEGER, + + -- Either 'root' or 'unprivileged', indicating the privileges required by + -- the test case. + required_user TEXT +); + + +-- Representation of test case properties that have more than one value. +-- +-- While we could store the flattened values of the properties as provided +-- by the test case itself, we choose to store the processed, split +-- representation. This allows us to perform queries about the test cases +-- directly on the database without doing text processing; for example, +-- "get all test cases that require /bin/ls". +CREATE TABLE atf_test_cases_multivalues ( + test_case_id INTEGER REFERENCES test_cases, + + -- The name of the property; for example, 'require.progs'. + property_name TEXT NOT NULL, + + -- One of the values of the property. + property_value TEXT NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Detail tables for the 'plain' test interface. +-- ------------------------------------------------------------------------- + + +-- Properties specific to 'plain' test programs. +CREATE TABLE plain_test_programs ( + test_program_id INTEGER PRIMARY KEY REFERENCES test_programs, + + -- The timeout for the test cases in this test program. While this + -- setting has a default value for test programs, we explicitly record + -- the information here. The "default value" used when the test + -- program was run might change over time, so we want to know what it + -- was exactly when this was run. + timeout INTEGER NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Verbatim files. +-- ------------------------------------------------------------------------- + + +-- Copies of files or logs generated during testing. +-- +-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a +-- hash to the file contents and use that as the primary key instead. +CREATE TABLE files ( + file_id INTEGER PRIMARY KEY, + + contents BLOB NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Initialization of values. +-- ------------------------------------------------------------------------- + + +-- Create a new metadata record. +-- +-- For every new database, we want to ensure that the metadata is valid if +-- the database creation (i.e. the whole transaction) succeeded. +-- +-- If you modify the value of the schema version in this statement, you +-- will also have to modify the version encoded in the backend module. +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 1); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/schema_v2.sql b/contrib/kyua/store/schema_v2.sql new file mode 100644 index 000000000000..48bd1727f91b --- /dev/null +++ b/contrib/kyua/store/schema_v2.sql @@ -0,0 +1,293 @@ +-- 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 store/schema_v2.sql +-- Definition of the database schema. +-- +-- The whole contents of this file are wrapped in a transaction. We want +-- to ensure that the initial contents of the database (the table layout as +-- well as any predefined values) are written atomically to simplify error +-- handling in our code. + + +BEGIN TRANSACTION; + + +-- ------------------------------------------------------------------------- +-- Metadata. +-- ------------------------------------------------------------------------- + + +-- Database-wide properties. +-- +-- Rows in this table are immutable: modifying the metadata implies writing +-- a new record with a new schema_version greater than all existing +-- records, and never updating previous records. When extracting data from +-- this table, the only "valid" row is the one with the highest +-- scheam_version. All the other rows are meaningless and only exist for +-- historical purposes. +-- +-- In other words, this table keeps the history of the database metadata. +-- The only reason for doing this is for debugging purposes. It may come +-- in handy to know when a particular database-wide operation happened if +-- it turns out that the database got corrupted. +CREATE TABLE metadata ( + schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1), + timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0) +); + + +-- ------------------------------------------------------------------------- +-- Contexts. +-- ------------------------------------------------------------------------- + + +-- Execution contexts. +-- +-- A context represents the execution environment of a particular action. +-- Because every action is invoked by the user, the context may have +-- changed. We record such information for information and debugging +-- purposes. +CREATE TABLE contexts ( + context_id INTEGER PRIMARY KEY AUTOINCREMENT, + cwd TEXT NOT NULL + + -- TODO(jmmv): Record the run-time configuration. +); + + +-- Environment variables of a context. +CREATE TABLE env_vars ( + context_id INTEGER REFERENCES contexts, + var_name TEXT NOT NULL, + var_value TEXT NOT NULL, + + PRIMARY KEY (context_id, var_name) +); + + +-- ------------------------------------------------------------------------- +-- Actions. +-- ------------------------------------------------------------------------- + + +-- Representation of user-initiated actions. +-- +-- An action is an operation initiated by the user. At the moment, the +-- only operation Kyua supports is the "test" operation (in the future we +-- should be able to store, e.g. build logs). To keep things simple the +-- database schema is restricted to represent one single action. +CREATE TABLE actions ( + action_id INTEGER PRIMARY KEY AUTOINCREMENT, + context_id INTEGER REFERENCES contexts +); + + +-- ------------------------------------------------------------------------- +-- Test suites. +-- +-- The tables in this section represent all the components that form a test +-- suite. This includes data about the test suite itself (test programs +-- and test cases), and also the data about particular runs (test results). +-- +-- As you will notice, every object belongs to a particular action, has a +-- unique identifier and there is no attempt to deduplicate data. This +-- comes from the fact that a test suite is not "stable" over time: i.e. on +-- each execution of the test suite, test programs and test cases may have +-- come and gone. This has the interesting result of making the +-- distinction of a test case and a test result a pure syntactic +-- difference, because there is always a 1:1 relation. +-- +-- The code that performs the processing of the actions is the component in +-- charge of finding correlations between test programs and test cases +-- across different actions. +-- ------------------------------------------------------------------------- + + +-- Representation of the metadata objects. +-- +-- The way this table works is like this: every time we record a metadata +-- object, we calculate what its identifier should be as the last rowid of +-- the table. All properties of that metadata object thus receive the same +-- identifier. +CREATE TABLE metadatas ( + metadata_id INTEGER NOT NULL, + + -- The name of the property. + property_name TEXT NOT NULL, + + -- One of the values of the property. + property_value TEXT, + + PRIMARY KEY (metadata_id, property_name) +); + + +-- Optimize the loading of the metadata of any single entity. +-- +-- The metadata_id column of the metadatas table is not enough to act as a +-- primary key, yet we need to locate entries in the metadatas table solely by +-- their identifier. +-- +-- TODO(jmmv): I think this index is useless given that the primary key in the +-- metadatas table includes the metadata_id as the first component. Need to +-- verify this and drop the index or this comment appropriately. +CREATE INDEX index_metadatas_by_id + ON metadatas (metadata_id); + + +-- Representation of a test program. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + action_id INTEGER REFERENCES actions, + + -- The absolute path to the test program. This should not be necessary + -- because it is basically the concatenation of root and relative_path. + -- However, this allows us to very easily search for test programs + -- regardless of where they were executed from. (I.e. different + -- combinations of root + relative_path can map to the same absolute path). + absolute_path TEXT NOT NULL, + + -- The path to the root of the test suite (where the Kyuafile lives). + root TEXT NOT NULL, + + -- The path to the test program, relative to the root. + relative_path TEXT NOT NULL, + + -- Name of the test suite the test program belongs to. + test_suite_name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER, + + -- The name of the test program interface. + -- + -- Note that this indicates both the interface for the test program and + -- its test cases. See below for the corresponding detail tables. + interface TEXT NOT NULL +); + + +-- Optimize the lookup of test programs by the action they belong to. +CREATE INDEX index_test_programs_by_action_id + ON test_programs (action_id); + + +-- Representation of a test case. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_cases ( + test_case_id INTEGER PRIMARY KEY AUTOINCREMENT, + test_program_id INTEGER REFERENCES test_programs, + name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER +); + + +-- Optimize the loading of all test cases that are part of a test program. +CREATE INDEX index_test_cases_by_test_programs_id + ON test_cases (test_program_id); + + +-- Representation of test case results. +-- +-- Note that there is a 1:1 relation between test cases and their results. +-- This is a result of storing the information of a test case on every +-- single action. +CREATE TABLE test_results ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + result_type TEXT NOT NULL, + result_reason TEXT, + + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL +); + + +-- Collection of output files of the test case. +CREATE TABLE test_case_files ( + test_case_id INTEGER NOT NULL REFERENCES test_cases, + + -- The raw name of the file. + -- + -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold + -- the stdout and stderr of the test case, respectively. If any of + -- these are empty, there will be no corresponding entry in this table + -- (hence why we do not allow NULLs in these fields). + file_name TEXT NOT NULL, + + -- Pointer to the file itself. + file_id INTEGER NOT NULL REFERENCES files, + + PRIMARY KEY (test_case_id, file_name) +); + + +-- ------------------------------------------------------------------------- +-- Verbatim files. +-- ------------------------------------------------------------------------- + + +-- Copies of files or logs generated during testing. +-- +-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a +-- hash to the file contents and use that as the primary key instead. +CREATE TABLE files ( + file_id INTEGER PRIMARY KEY, + + contents BLOB NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Initialization of values. +-- ------------------------------------------------------------------------- + + +-- Create a new metadata record. +-- +-- For every new database, we want to ensure that the metadata is valid if +-- the database creation (i.e. the whole transaction) succeeded. +-- +-- If you modify the value of the schema version in this statement, you +-- will also have to modify the version encoded in the backend module. +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 2); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/schema_v3.sql b/contrib/kyua/store/schema_v3.sql new file mode 100644 index 000000000000..26e8359e1994 --- /dev/null +++ b/contrib/kyua/store/schema_v3.sql @@ -0,0 +1,255 @@ +-- 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 store/schema_v3.sql +-- Definition of the database schema. +-- +-- The whole contents of this file are wrapped in a transaction. We want +-- to ensure that the initial contents of the database (the table layout as +-- well as any predefined values) are written atomically to simplify error +-- handling in our code. + + +BEGIN TRANSACTION; + + +-- ------------------------------------------------------------------------- +-- Metadata. +-- ------------------------------------------------------------------------- + + +-- Database-wide properties. +-- +-- Rows in this table are immutable: modifying the metadata implies writing +-- a new record with a new schema_version greater than all existing +-- records, and never updating previous records. When extracting data from +-- this table, the only "valid" row is the one with the highest +-- scheam_version. All the other rows are meaningless and only exist for +-- historical purposes. +-- +-- In other words, this table keeps the history of the database metadata. +-- The only reason for doing this is for debugging purposes. It may come +-- in handy to know when a particular database-wide operation happened if +-- it turns out that the database got corrupted. +CREATE TABLE metadata ( + schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1), + timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0) +); + + +-- ------------------------------------------------------------------------- +-- Contexts. +-- ------------------------------------------------------------------------- + + +-- Execution contexts. +-- +-- A context represents the execution environment of the test run. +-- We record such information for information and debugging purposes. +CREATE TABLE contexts ( + cwd TEXT NOT NULL + + -- TODO(jmmv): Record the run-time configuration. +); + + +-- Environment variables of a context. +CREATE TABLE env_vars ( + var_name TEXT PRIMARY KEY, + var_value TEXT NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Test suites. +-- +-- The tables in this section represent all the components that form a test +-- suite. This includes data about the test suite itself (test programs +-- and test cases), and also the data about particular runs (test results). +-- +-- As you will notice, every object has a unique identifier and there is no +-- attempt to deduplicate data. This has the interesting result of making +-- the distinction of a test case and a test result a pure syntactic +-- difference, because there is always a 1:1 relation. +-- ------------------------------------------------------------------------- + + +-- Representation of the metadata objects. +-- +-- The way this table works is like this: every time we record a metadata +-- object, we calculate what its identifier should be as the last rowid of +-- the table. All properties of that metadata object thus receive the same +-- identifier. +CREATE TABLE metadatas ( + metadata_id INTEGER NOT NULL, + + -- The name of the property. + property_name TEXT NOT NULL, + + -- One of the values of the property. + property_value TEXT, + + PRIMARY KEY (metadata_id, property_name) +); + + +-- Optimize the loading of the metadata of any single entity. +-- +-- The metadata_id column of the metadatas table is not enough to act as a +-- primary key, yet we need to locate entries in the metadatas table solely by +-- their identifier. +-- +-- TODO(jmmv): I think this index is useless given that the primary key in the +-- metadatas table includes the metadata_id as the first component. Need to +-- verify this and drop the index or this comment appropriately. +CREATE INDEX index_metadatas_by_id + ON metadatas (metadata_id); + + +-- Representation of a test program. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + + -- The absolute path to the test program. This should not be necessary + -- because it is basically the concatenation of root and relative_path. + -- However, this allows us to very easily search for test programs + -- regardless of where they were executed from. (I.e. different + -- combinations of root + relative_path can map to the same absolute path). + absolute_path TEXT NOT NULL, + + -- The path to the root of the test suite (where the Kyuafile lives). + root TEXT NOT NULL, + + -- The path to the test program, relative to the root. + relative_path TEXT NOT NULL, + + -- Name of the test suite the test program belongs to. + test_suite_name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER, + + -- The name of the test program interface. + -- + -- Note that this indicates both the interface for the test program and + -- its test cases. See below for the corresponding detail tables. + interface TEXT NOT NULL +); + + +-- Representation of a test case. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_cases ( + test_case_id INTEGER PRIMARY KEY AUTOINCREMENT, + test_program_id INTEGER REFERENCES test_programs, + name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER +); + + +-- Optimize the loading of all test cases that are part of a test program. +CREATE INDEX index_test_cases_by_test_programs_id + ON test_cases (test_program_id); + + +-- Representation of test case results. +-- +-- Note that there is a 1:1 relation between test cases and their results. +CREATE TABLE test_results ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + result_type TEXT NOT NULL, + result_reason TEXT, + + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL +); + + +-- Collection of output files of the test case. +CREATE TABLE test_case_files ( + test_case_id INTEGER NOT NULL REFERENCES test_cases, + + -- The raw name of the file. + -- + -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold + -- the stdout and stderr of the test case, respectively. If any of + -- these are empty, there will be no corresponding entry in this table + -- (hence why we do not allow NULLs in these fields). + file_name TEXT NOT NULL, + + -- Pointer to the file itself. + file_id INTEGER NOT NULL REFERENCES files, + + PRIMARY KEY (test_case_id, file_name) +); + + +-- ------------------------------------------------------------------------- +-- Verbatim files. +-- ------------------------------------------------------------------------- + + +-- Copies of files or logs generated during testing. +-- +-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a +-- hash to the file contents and use that as the primary key instead. +CREATE TABLE files ( + file_id INTEGER PRIMARY KEY, + + contents BLOB NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Initialization of values. +-- ------------------------------------------------------------------------- + + +-- Create a new metadata record. +-- +-- For every new database, we want to ensure that the metadata is valid if +-- the database creation (i.e. the whole transaction) succeeded. +-- +-- If you modify the value of the schema version in this statement, you +-- will also have to modify the version encoded in the backend module. +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 3); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/testdata_v1.sql b/contrib/kyua/store/testdata_v1.sql new file mode 100644 index 000000000000..75c4d439ac96 --- /dev/null +++ b/contrib/kyua/store/testdata_v1.sql @@ -0,0 +1,330 @@ +-- Copyright 2013 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 store/testdata_v1.sql +-- Populates a v1 database with some test data. + + +BEGIN TRANSACTION; + + +-- +-- Action 1: Empty context and no test programs nor test cases. +-- + + +-- context_id 1 +INSERT INTO contexts (context_id, cwd) VALUES (1, '/some/root'); + +-- action_id 1 +INSERT INTO actions (action_id, context_id) VALUES (1, 1); + + +-- +-- Action 2: Plain test programs only. +-- +-- This action contains 5 test programs, each with one test case, and each +-- reporting one of all possible result types. +-- + + +-- context_id 2 +INSERT INTO contexts (context_id, cwd) VALUES (2, '/test/suite/root'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'HOME', '/home/test'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'PATH', '/bin:/usr/bin'); + +-- action_id 2 +INSERT INTO actions (action_id, context_id) VALUES (2, 2); + +-- test_program_id 1 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (1, 2, '/test/suite/root/foo_test', '/test/suite/root', + 'foo_test', 'suite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (1, 300000000); + +-- test_case_id 1 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (1, 1, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500); + +-- test_program_id 2 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (2, 2, '/test/suite/root/subdir/another_test', '/test/suite/root', + 'subdir/another_test', 'subsuite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (2, 10000000); + +-- test_case_id 2 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (2, 2, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (2, 'failed', 'Exited with code 1', + 1357643622001200, 1357643622900021); + +-- file_id 1 +INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDOUT__', 1); + +-- file_id 2 +INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDERR__', 2); + +-- test_program_id 3 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (3, 2, '/test/suite/root/subdir/bar_test', '/test/suite/root', + 'subdir/bar_test', 'subsuite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (3, 300000000); + +-- test_case_id 3 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (3, 3, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (3, 'broken', 'Received signal 1', + 1357643623500000, 1357643630981932); + +-- test_program_id 4 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (4, 2, '/test/suite/root/top_test', '/test/suite/root', + 'top_test', 'suite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (4, 300000000); + +-- test_case_id 4 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (4, 4, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (4, 'expected_failure', 'Known bug', + 1357643631000000, 1357643631020000); + +-- test_program_id 5 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (5, 2, '/test/suite/root/last_test', '/test/suite/root', + 'last_test', 'suite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (5, 300000000); + +-- test_case_id 5 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (5, 5, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000); + + +-- +-- Action 3: ATF test programs only. +-- + + +-- context_id 3 +INSERT INTO contexts (context_id, cwd) VALUES (3, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (3, 'PATH', '/bin:/usr/bin'); + +-- action_id 3 +INSERT INTO actions (action_id, context_id) VALUES (3, 3); + +-- test_program_id 6 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (6, 3, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 'atf'); + +-- test_case_id 6, passed, no optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (6, 6, 'this_passes'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (6, NULL, 'false', 300000000, 0, NULL); + +-- test_case_id 7, failed, optional non-multivalue metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (7, 6, 'this_fails'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (7, 'Test description', 'true', 300000000, 128, 'root'); + +-- test_case_id 8, skipped, all optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (8, 6, 'this_skips'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (8, 'Test explanation', 'true', 600000000, 512, 'unprivileged'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.arch', 'x86_64'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.arch', 'powerpc'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.machine', 'amd64'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.machine', 'macppc'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.config', 'unprivileged_user'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.config', 'X-foo'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.files', '/the/data/file'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.progs', 'cp'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.progs', '/bin/ls'); + +-- file_id 3 +INSERT INTO files (file_id, contents) + VALUES (3, x'416e6f74686572207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (8, '__STDOUT__', 3); + +-- test_program_id 7 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (7, 3, '/usr/tests/simple_test', '/usr/tests', + 'simple_test', 'subsuite-name', 'atf'); + +-- test_case_id 9 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (9, 7, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (9, 'failed', 'Exited with code 1', + 1357648740120000, 1357648750081700); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (9, 'More text', 'true', 300000000, 128, 'unprivileged'); + +-- file_id 4 +INSERT INTO files (file_id, contents) + VALUES (4, x'416e6f7468657220737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (9, '__STDERR__', 4); + + +-- +-- Action 4: Mixture of test programs. +-- + + +-- context_id 4 +INSERT INTO contexts (context_id, cwd) VALUES (4, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'LANG', 'C'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'PATH', '/bin:/usr/bin'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'TERM', 'xterm'); + +-- action_id 4 +INSERT INTO actions (action_id, context_id) VALUES (4, 4); + +-- test_program_id 8 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (8, 4, '/usr/tests/subdir/another_test', '/usr/tests', + 'subdir/another_test', 'subsuite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (8, 10000000); + +-- test_case_id 10 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (10, 8, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000); + +-- file_id 5 +INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDOUT__', 5); + +-- file_id 6 +INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDERR__', 6); + +-- test_program_id 9 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (9, 4, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 'atf'); + +-- test_case_id 11 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (11, 9, 'this_passes'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (11, NULL, 'false', 300000000, 0, NULL); + +-- test_case_id 12 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (12, 9, 'this_fails'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (12, 'Test description', 'false', 300000000, 0, 'root'); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/testdata_v2.sql b/contrib/kyua/store/testdata_v2.sql new file mode 100644 index 000000000000..838da4c25956 --- /dev/null +++ b/contrib/kyua/store/testdata_v2.sql @@ -0,0 +1,462 @@ +-- Copyright 2013 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 store/testdata_v2.sql +-- Populates a v2 database with some test data. + + +BEGIN TRANSACTION; + + +-- +-- Action 1: Empty context and no test programs nor test cases. +-- + + +-- context_id 1 +INSERT INTO contexts (context_id, cwd) VALUES (1, '/some/root'); + +-- action_id 1 +INSERT INTO actions (action_id, context_id) VALUES (1, 1); + + +-- +-- Action 2: Plain test programs only. +-- +-- This action contains 5 test programs, each with one test case, and each +-- reporting one of all possible result types. +-- + + +-- context_id 2 +INSERT INTO contexts (context_id, cwd) VALUES (2, '/test/suite/root'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'HOME', '/home/test'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'PATH', '/bin:/usr/bin'); + +-- action_id 2 +INSERT INTO actions (action_id, context_id) VALUES (2, 2); + +-- metadata_id 1 +INSERT INTO metadatas VALUES (1, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (1, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (1, 'description', ''); +INSERT INTO metadatas VALUES (1, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (1, 'required_configs', ''); +INSERT INTO metadatas VALUES (1, 'required_files', ''); +INSERT INTO metadatas VALUES (1, 'required_memory', '0'); +INSERT INTO metadatas VALUES (1, 'required_programs', ''); +INSERT INTO metadatas VALUES (1, 'required_user', ''); +INSERT INTO metadatas VALUES (1, 'timeout', '300'); + +-- test_program_id 1 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (1, 2, '/test/suite/root/foo_test', '/test/suite/root', + 'foo_test', 'suite-name', 1, 'plain'); + +-- test_case_id 1 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (1, 1, 'main', 1); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500); + +-- metadata_id 2 +INSERT INTO metadatas VALUES (2, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (2, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (2, 'description', ''); +INSERT INTO metadatas VALUES (2, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (2, 'required_configs', ''); +INSERT INTO metadatas VALUES (2, 'required_files', ''); +INSERT INTO metadatas VALUES (2, 'required_memory', '0'); +INSERT INTO metadatas VALUES (2, 'required_programs', ''); +INSERT INTO metadatas VALUES (2, 'required_user', ''); +INSERT INTO metadatas VALUES (2, 'timeout', '10'); + +-- test_program_id 2 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (2, 2, '/test/suite/root/subdir/another_test', '/test/suite/root', + 'subdir/another_test', 'subsuite-name', 2, 'plain'); + +-- test_case_id 2 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (2, 2, 'main', 2); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (2, 'failed', 'Exited with code 1', + 1357643622001200, 1357643622900021); + +-- file_id 1 +INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDOUT__', 1); + +-- file_id 2 +INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDERR__', 2); + +-- metadata_id 3 +INSERT INTO metadatas VALUES (3, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (3, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (3, 'description', ''); +INSERT INTO metadatas VALUES (3, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (3, 'required_configs', ''); +INSERT INTO metadatas VALUES (3, 'required_files', ''); +INSERT INTO metadatas VALUES (3, 'required_memory', '0'); +INSERT INTO metadatas VALUES (3, 'required_programs', ''); +INSERT INTO metadatas VALUES (3, 'required_user', ''); +INSERT INTO metadatas VALUES (3, 'timeout', '300'); + +-- test_program_id 3 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (3, 2, '/test/suite/root/subdir/bar_test', '/test/suite/root', + 'subdir/bar_test', 'subsuite-name', 3, 'plain'); + +-- test_case_id 3 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (3, 3, 'main', 3); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (3, 'broken', 'Received signal 1', + 1357643623500000, 1357643630981932); + +-- metadata_id 4 +INSERT INTO metadatas VALUES (4, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (4, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (4, 'description', ''); +INSERT INTO metadatas VALUES (4, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (4, 'required_configs', ''); +INSERT INTO metadatas VALUES (4, 'required_files', ''); +INSERT INTO metadatas VALUES (4, 'required_memory', '0'); +INSERT INTO metadatas VALUES (4, 'required_programs', ''); +INSERT INTO metadatas VALUES (4, 'required_user', ''); +INSERT INTO metadatas VALUES (4, 'timeout', '300'); + +-- test_program_id 4 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (4, 2, '/test/suite/root/top_test', '/test/suite/root', + 'top_test', 'suite-name', 4, 'plain'); + +-- test_case_id 4 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (4, 4, 'main', 4); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (4, 'expected_failure', 'Known bug', + 1357643631000000, 1357643631020000); + +-- metadata_id 5 +INSERT INTO metadatas VALUES (5, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (5, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (5, 'description', ''); +INSERT INTO metadatas VALUES (5, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (5, 'required_configs', ''); +INSERT INTO metadatas VALUES (5, 'required_files', ''); +INSERT INTO metadatas VALUES (5, 'required_memory', '0'); +INSERT INTO metadatas VALUES (5, 'required_programs', ''); +INSERT INTO metadatas VALUES (5, 'required_user', ''); +INSERT INTO metadatas VALUES (5, 'timeout', '300'); + +-- test_program_id 5 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (5, 2, '/test/suite/root/last_test', '/test/suite/root', + 'last_test', 'suite-name', 5, 'plain'); + +-- test_case_id 5 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (5, 5, 'main', 5); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000); + + +-- +-- Action 3: ATF test programs only. +-- + + +-- context_id 3 +INSERT INTO contexts (context_id, cwd) VALUES (3, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (3, 'PATH', '/bin:/usr/bin'); + +-- action_id 3 +INSERT INTO actions (action_id, context_id) VALUES (3, 3); + +-- metadata_id 6 +INSERT INTO metadatas VALUES (6, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (6, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (6, 'description', ''); +INSERT INTO metadatas VALUES (6, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (6, 'required_configs', ''); +INSERT INTO metadatas VALUES (6, 'required_files', ''); +INSERT INTO metadatas VALUES (6, 'required_memory', '0'); +INSERT INTO metadatas VALUES (6, 'required_programs', ''); +INSERT INTO metadatas VALUES (6, 'required_user', ''); +INSERT INTO metadatas VALUES (6, 'timeout', '300'); + +-- test_program_id 6 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (6, 3, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 6, 'atf'); + +-- metadata_id 7 +INSERT INTO metadatas VALUES (7, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (7, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (7, 'description', ''); +INSERT INTO metadatas VALUES (7, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (7, 'required_configs', ''); +INSERT INTO metadatas VALUES (7, 'required_files', ''); +INSERT INTO metadatas VALUES (7, 'required_memory', '0'); +INSERT INTO metadatas VALUES (7, 'required_programs', ''); +INSERT INTO metadatas VALUES (7, 'required_user', ''); +INSERT INTO metadatas VALUES (7, 'timeout', '300'); + +-- test_case_id 6, passed, no optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (6, 6, 'this_passes', 7); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000); + +-- metadata_id 8 +INSERT INTO metadatas VALUES (8, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (8, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (8, 'description', 'Test description'); +INSERT INTO metadatas VALUES (8, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (8, 'required_configs', ''); +INSERT INTO metadatas VALUES (8, 'required_files', ''); +INSERT INTO metadatas VALUES (8, 'required_memory', '128'); +INSERT INTO metadatas VALUES (8, 'required_programs', ''); +INSERT INTO metadatas VALUES (8, 'required_user', 'root'); +INSERT INTO metadatas VALUES (8, 'timeout', '300'); + +-- test_case_id 7, failed, optional non-multivalue metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (7, 6, 'this_fails', 8); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182); + +-- metadata_id 9 +INSERT INTO metadatas VALUES (9, 'allowed_architectures', 'powerpc x86_64'); +INSERT INTO metadatas VALUES (9, 'allowed_platforms', 'amd64 macppc'); +INSERT INTO metadatas VALUES (9, 'description', 'Test explanation'); +INSERT INTO metadatas VALUES (9, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (9, 'required_configs', 'unprivileged_user X-foo'); +INSERT INTO metadatas VALUES (9, 'required_files', '/the/data/file'); +INSERT INTO metadatas VALUES (9, 'required_memory', '512'); +INSERT INTO metadatas VALUES (9, 'required_programs', 'cp /bin/ls'); +INSERT INTO metadatas VALUES (9, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (9, 'timeout', '600'); + +-- test_case_id 8, skipped, all optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (8, 6, 'this_skips', 9); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000); + +-- file_id 3 +INSERT INTO files (file_id, contents) + VALUES (3, x'416e6f74686572207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (8, '__STDOUT__', 3); + +-- metadata_id 10 +INSERT INTO metadatas VALUES (10, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (10, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (10, 'description', ''); +INSERT INTO metadatas VALUES (10, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (10, 'required_configs', ''); +INSERT INTO metadatas VALUES (10, 'required_files', ''); +INSERT INTO metadatas VALUES (10, 'required_memory', '0'); +INSERT INTO metadatas VALUES (10, 'required_programs', ''); +INSERT INTO metadatas VALUES (10, 'required_user', ''); +INSERT INTO metadatas VALUES (10, 'timeout', '300'); + +-- test_program_id 7 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (7, 3, '/usr/tests/simple_test', '/usr/tests', + 'simple_test', 'subsuite-name', 10, 'atf'); + +-- metadata_id 11 +INSERT INTO metadatas VALUES (11, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (11, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (11, 'description', 'More text'); +INSERT INTO metadatas VALUES (11, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (11, 'required_configs', ''); +INSERT INTO metadatas VALUES (11, 'required_files', ''); +INSERT INTO metadatas VALUES (11, 'required_memory', '128'); +INSERT INTO metadatas VALUES (11, 'required_programs', ''); +INSERT INTO metadatas VALUES (11, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (11, 'timeout', '300'); + +-- test_case_id 9 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (9, 7, 'main', 11); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (9, 'failed', 'Exited with code 1', + 1357648740120000, 1357648750081700); + +-- file_id 4 +INSERT INTO files (file_id, contents) + VALUES (4, x'416e6f7468657220737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (9, '__STDERR__', 4); + + +-- +-- Action 4: Mixture of test programs. +-- + + +-- context_id 4 +INSERT INTO contexts (context_id, cwd) VALUES (4, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'LANG', 'C'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'PATH', '/bin:/usr/bin'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'TERM', 'xterm'); + +-- action_id 4 +INSERT INTO actions (action_id, context_id) VALUES (4, 4); + +-- metadata_id 12 +INSERT INTO metadatas VALUES (12, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (12, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (12, 'description', ''); +INSERT INTO metadatas VALUES (12, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (12, 'required_configs', ''); +INSERT INTO metadatas VALUES (12, 'required_files', ''); +INSERT INTO metadatas VALUES (12, 'required_memory', '0'); +INSERT INTO metadatas VALUES (12, 'required_programs', ''); +INSERT INTO metadatas VALUES (12, 'required_user', ''); +INSERT INTO metadatas VALUES (12, 'timeout', '10'); + +-- test_program_id 8 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (8, 4, '/usr/tests/subdir/another_test', '/usr/tests', + 'subdir/another_test', 'subsuite-name', 12, 'plain'); + +-- test_case_id 10 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (10, 8, 'main', 12); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000); + +-- file_id 5 +INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDOUT__', 5); + +-- file_id 6 +INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDERR__', 6); + +-- metadata_id 13 +INSERT INTO metadatas VALUES (13, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (13, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (13, 'description', ''); +INSERT INTO metadatas VALUES (13, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (13, 'required_configs', ''); +INSERT INTO metadatas VALUES (13, 'required_files', ''); +INSERT INTO metadatas VALUES (13, 'required_memory', '0'); +INSERT INTO metadatas VALUES (13, 'required_programs', ''); +INSERT INTO metadatas VALUES (13, 'required_user', ''); +INSERT INTO metadatas VALUES (13, 'timeout', '300'); + +-- test_program_id 9 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (9, 4, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 14, 'atf'); + +-- metadata_id 15 +INSERT INTO metadatas VALUES (15, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (15, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (15, 'description', ''); +INSERT INTO metadatas VALUES (15, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (15, 'required_configs', ''); +INSERT INTO metadatas VALUES (15, 'required_files', ''); +INSERT INTO metadatas VALUES (15, 'required_memory', '0'); +INSERT INTO metadatas VALUES (15, 'required_programs', ''); +INSERT INTO metadatas VALUES (15, 'required_user', ''); +INSERT INTO metadatas VALUES (15, 'timeout', '300'); + +-- test_case_id 11 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (11, 9, 'this_passes', 15); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000); + +-- metadata_id 16 +INSERT INTO metadatas VALUES (16, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (16, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (16, 'description', 'Test description'); +INSERT INTO metadatas VALUES (16, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (16, 'required_configs', ''); +INSERT INTO metadatas VALUES (16, 'required_files', ''); +INSERT INTO metadatas VALUES (16, 'required_memory', '0'); +INSERT INTO metadatas VALUES (16, 'required_programs', ''); +INSERT INTO metadatas VALUES (16, 'required_user', 'root'); +INSERT INTO metadatas VALUES (16, 'timeout', '300'); + +-- test_case_id 12 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (12, 9, 'this_fails', 16); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/testdata_v3_1.sql b/contrib/kyua/store/testdata_v3_1.sql new file mode 100644 index 000000000000..9715db490ba0 --- /dev/null +++ b/contrib/kyua/store/testdata_v3_1.sql @@ -0,0 +1,42 @@ +-- 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 store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- Empty context and no test programs nor test cases. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/some/root'); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/testdata_v3_2.sql b/contrib/kyua/store/testdata_v3_2.sql new file mode 100644 index 000000000000..0ef42a328c7c --- /dev/null +++ b/contrib/kyua/store/testdata_v3_2.sql @@ -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. + +-- \file store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- This contains 5 test programs, each with one test case, and each +-- reporting one of all possible result types. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/test/suite/root'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('HOME', '/home/test'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('PATH', '/bin:/usr/bin'); + +-- metadata_id 1 +INSERT INTO metadatas VALUES (1, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (1, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (1, 'description', ''); +INSERT INTO metadatas VALUES (1, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (1, 'required_configs', ''); +INSERT INTO metadatas VALUES (1, 'required_files', ''); +INSERT INTO metadatas VALUES (1, 'required_memory', '0'); +INSERT INTO metadatas VALUES (1, 'required_programs', ''); +INSERT INTO metadatas VALUES (1, 'required_user', ''); +INSERT INTO metadatas VALUES (1, 'timeout', '300'); + +-- test_program_id 1 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (1, '/test/suite/root/foo_test', '/test/suite/root', + 'foo_test', 'suite-name', 1, 'plain'); + +-- test_case_id 1 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (1, 1, 'main', 1); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500); + +-- metadata_id 2 +INSERT INTO metadatas VALUES (2, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (2, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (2, 'description', ''); +INSERT INTO metadatas VALUES (2, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (2, 'required_configs', ''); +INSERT INTO metadatas VALUES (2, 'required_files', ''); +INSERT INTO metadatas VALUES (2, 'required_memory', '0'); +INSERT INTO metadatas VALUES (2, 'required_programs', ''); +INSERT INTO metadatas VALUES (2, 'required_user', ''); +INSERT INTO metadatas VALUES (2, 'timeout', '10'); + +-- test_program_id 2 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (2, '/test/suite/root/subdir/another_test', '/test/suite/root', + 'subdir/another_test', 'subsuite-name', 2, 'plain'); + +-- test_case_id 2 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (2, 2, 'main', 2); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (2, 'failed', 'Exited with code 1', + 1357643622001200, 1357643622900021); + +-- file_id 1 +INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDOUT__', 1); + +-- file_id 2 +INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDERR__', 2); + +-- metadata_id 3 +INSERT INTO metadatas VALUES (3, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (3, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (3, 'description', ''); +INSERT INTO metadatas VALUES (3, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (3, 'required_configs', ''); +INSERT INTO metadatas VALUES (3, 'required_files', ''); +INSERT INTO metadatas VALUES (3, 'required_memory', '0'); +INSERT INTO metadatas VALUES (3, 'required_programs', ''); +INSERT INTO metadatas VALUES (3, 'required_user', ''); +INSERT INTO metadatas VALUES (3, 'timeout', '300'); + +-- test_program_id 3 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (3, '/test/suite/root/subdir/bar_test', '/test/suite/root', + 'subdir/bar_test', 'subsuite-name', 3, 'plain'); + +-- test_case_id 3 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (3, 3, 'main', 3); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (3, 'broken', 'Received signal 1', + 1357643623500000, 1357643630981932); + +-- metadata_id 4 +INSERT INTO metadatas VALUES (4, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (4, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (4, 'description', ''); +INSERT INTO metadatas VALUES (4, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (4, 'required_configs', ''); +INSERT INTO metadatas VALUES (4, 'required_files', ''); +INSERT INTO metadatas VALUES (4, 'required_memory', '0'); +INSERT INTO metadatas VALUES (4, 'required_programs', ''); +INSERT INTO metadatas VALUES (4, 'required_user', ''); +INSERT INTO metadatas VALUES (4, 'timeout', '300'); + +-- test_program_id 4 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (4, '/test/suite/root/top_test', '/test/suite/root', + 'top_test', 'suite-name', 4, 'plain'); + +-- test_case_id 4 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (4, 4, 'main', 4); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (4, 'expected_failure', 'Known bug', + 1357643631000000, 1357643631020000); + +-- metadata_id 5 +INSERT INTO metadatas VALUES (5, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (5, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (5, 'description', ''); +INSERT INTO metadatas VALUES (5, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (5, 'required_configs', ''); +INSERT INTO metadatas VALUES (5, 'required_files', ''); +INSERT INTO metadatas VALUES (5, 'required_memory', '0'); +INSERT INTO metadatas VALUES (5, 'required_programs', ''); +INSERT INTO metadatas VALUES (5, 'required_user', ''); +INSERT INTO metadatas VALUES (5, 'timeout', '300'); + +-- test_program_id 5 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (5, '/test/suite/root/last_test', '/test/suite/root', + 'last_test', 'suite-name', 5, 'plain'); + +-- test_case_id 5 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (5, 5, 'main', 5); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/testdata_v3_3.sql b/contrib/kyua/store/testdata_v3_3.sql new file mode 100644 index 000000000000..80d5a6b9a6e2 --- /dev/null +++ b/contrib/kyua/store/testdata_v3_3.sql @@ -0,0 +1,171 @@ +-- 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 store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- ATF test programs only. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/usr/tests'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('PATH', '/bin:/usr/bin'); + +-- metadata_id 6 +INSERT INTO metadatas VALUES (6, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (6, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (6, 'description', ''); +INSERT INTO metadatas VALUES (6, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (6, 'required_configs', ''); +INSERT INTO metadatas VALUES (6, 'required_files', ''); +INSERT INTO metadatas VALUES (6, 'required_memory', '0'); +INSERT INTO metadatas VALUES (6, 'required_programs', ''); +INSERT INTO metadatas VALUES (6, 'required_user', ''); +INSERT INTO metadatas VALUES (6, 'timeout', '300'); + +-- test_program_id 6 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (6, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 6, 'atf'); + +-- metadata_id 7 +INSERT INTO metadatas VALUES (7, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (7, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (7, 'description', ''); +INSERT INTO metadatas VALUES (7, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (7, 'required_configs', ''); +INSERT INTO metadatas VALUES (7, 'required_files', ''); +INSERT INTO metadatas VALUES (7, 'required_memory', '0'); +INSERT INTO metadatas VALUES (7, 'required_programs', ''); +INSERT INTO metadatas VALUES (7, 'required_user', ''); +INSERT INTO metadatas VALUES (7, 'timeout', '300'); + +-- test_case_id 6, passed, no optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (6, 6, 'this_passes', 7); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000); + +-- metadata_id 8 +INSERT INTO metadatas VALUES (8, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (8, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (8, 'description', 'Test description'); +INSERT INTO metadatas VALUES (8, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (8, 'required_configs', ''); +INSERT INTO metadatas VALUES (8, 'required_files', ''); +INSERT INTO metadatas VALUES (8, 'required_memory', '128'); +INSERT INTO metadatas VALUES (8, 'required_programs', ''); +INSERT INTO metadatas VALUES (8, 'required_user', 'root'); +INSERT INTO metadatas VALUES (8, 'timeout', '300'); + +-- test_case_id 7, failed, optional non-multivalue metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (7, 6, 'this_fails', 8); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182); + +-- metadata_id 9 +INSERT INTO metadatas VALUES (9, 'allowed_architectures', 'powerpc x86_64'); +INSERT INTO metadatas VALUES (9, 'allowed_platforms', 'amd64 macppc'); +INSERT INTO metadatas VALUES (9, 'description', 'Test explanation'); +INSERT INTO metadatas VALUES (9, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (9, 'required_configs', 'unprivileged_user X-foo'); +INSERT INTO metadatas VALUES (9, 'required_files', '/the/data/file'); +INSERT INTO metadatas VALUES (9, 'required_memory', '512'); +INSERT INTO metadatas VALUES (9, 'required_programs', 'cp /bin/ls'); +INSERT INTO metadatas VALUES (9, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (9, 'timeout', '600'); + +-- test_case_id 8, skipped, all optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (8, 6, 'this_skips', 9); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000); + +-- file_id 3 +INSERT INTO files (file_id, contents) + VALUES (3, x'416e6f74686572207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (8, '__STDOUT__', 3); + +-- metadata_id 10 +INSERT INTO metadatas VALUES (10, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (10, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (10, 'description', ''); +INSERT INTO metadatas VALUES (10, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (10, 'required_configs', ''); +INSERT INTO metadatas VALUES (10, 'required_files', ''); +INSERT INTO metadatas VALUES (10, 'required_memory', '0'); +INSERT INTO metadatas VALUES (10, 'required_programs', ''); +INSERT INTO metadatas VALUES (10, 'required_user', ''); +INSERT INTO metadatas VALUES (10, 'timeout', '300'); + +-- test_program_id 7 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (7, '/usr/tests/simple_test', '/usr/tests', + 'simple_test', 'subsuite-name', 10, 'atf'); + +-- metadata_id 11 +INSERT INTO metadatas VALUES (11, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (11, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (11, 'description', 'More text'); +INSERT INTO metadatas VALUES (11, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (11, 'required_configs', ''); +INSERT INTO metadatas VALUES (11, 'required_files', ''); +INSERT INTO metadatas VALUES (11, 'required_memory', '128'); +INSERT INTO metadatas VALUES (11, 'required_programs', ''); +INSERT INTO metadatas VALUES (11, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (11, 'timeout', '300'); + +-- test_case_id 9 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (9, 7, 'main', 11); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (9, 'failed', 'Exited with code 1', + 1357648740120000, 1357648750081700); + +-- file_id 4 +INSERT INTO files (file_id, contents) + VALUES (4, x'416e6f7468657220737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (9, '__STDERR__', 4); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/testdata_v3_4.sql b/contrib/kyua/store/testdata_v3_4.sql new file mode 100644 index 000000000000..1007bc7adac4 --- /dev/null +++ b/contrib/kyua/store/testdata_v3_4.sql @@ -0,0 +1,141 @@ +-- 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 store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- Mixture of test programs. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/usr/tests'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('LANG', 'C'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('PATH', '/bin:/usr/bin'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('TERM', 'xterm'); + +-- metadata_id 12 +INSERT INTO metadatas VALUES (12, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (12, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (12, 'description', ''); +INSERT INTO metadatas VALUES (12, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (12, 'required_configs', ''); +INSERT INTO metadatas VALUES (12, 'required_files', ''); +INSERT INTO metadatas VALUES (12, 'required_memory', '0'); +INSERT INTO metadatas VALUES (12, 'required_programs', ''); +INSERT INTO metadatas VALUES (12, 'required_user', ''); +INSERT INTO metadatas VALUES (12, 'timeout', '10'); + +-- test_program_id 8 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (8, '/usr/tests/subdir/another_test', '/usr/tests', + 'subdir/another_test', 'subsuite-name', 12, 'plain'); + +-- test_case_id 10 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (10, 8, 'main', 12); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000); + +-- file_id 5 +INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDOUT__', 5); + +-- file_id 6 +INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDERR__', 6); + +-- metadata_id 13 +INSERT INTO metadatas VALUES (13, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (13, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (13, 'description', ''); +INSERT INTO metadatas VALUES (13, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (13, 'required_configs', ''); +INSERT INTO metadatas VALUES (13, 'required_files', ''); +INSERT INTO metadatas VALUES (13, 'required_memory', '0'); +INSERT INTO metadatas VALUES (13, 'required_programs', ''); +INSERT INTO metadatas VALUES (13, 'required_user', ''); +INSERT INTO metadatas VALUES (13, 'timeout', '300'); + +-- test_program_id 9 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (9, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 14, 'atf'); + +-- metadata_id 15 +INSERT INTO metadatas VALUES (15, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (15, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (15, 'description', ''); +INSERT INTO metadatas VALUES (15, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (15, 'required_configs', ''); +INSERT INTO metadatas VALUES (15, 'required_files', ''); +INSERT INTO metadatas VALUES (15, 'required_memory', '0'); +INSERT INTO metadatas VALUES (15, 'required_programs', ''); +INSERT INTO metadatas VALUES (15, 'required_user', ''); +INSERT INTO metadatas VALUES (15, 'timeout', '300'); + +-- test_case_id 11 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (11, 9, 'this_passes', 15); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000); + +-- metadata_id 16 +INSERT INTO metadatas VALUES (16, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (16, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (16, 'description', 'Test description'); +INSERT INTO metadatas VALUES (16, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (16, 'required_configs', ''); +INSERT INTO metadatas VALUES (16, 'required_files', ''); +INSERT INTO metadatas VALUES (16, 'required_memory', '0'); +INSERT INTO metadatas VALUES (16, 'required_programs', ''); +INSERT INTO metadatas VALUES (16, 'required_user', 'root'); +INSERT INTO metadatas VALUES (16, 'timeout', '300'); + +-- test_case_id 12 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (12, 9, 'this_fails', 16); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000); + + +COMMIT TRANSACTION; diff --git a/contrib/kyua/store/transaction_test.cpp b/contrib/kyua/store/transaction_test.cpp new file mode 100644 index 000000000000..62db8bf1ffbe --- /dev/null +++ b/contrib/kyua/store/transaction_test.cpp @@ -0,0 +1,170 @@ +// 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 +#include + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "store/read_backend.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace units = utils::units; + + +namespace { + + +/// Puts and gets a context and validates the results. +/// +/// \param exp_context The context to save and restore. +static void +check_get_put_context(const model::context& exp_context) +{ + const fs::path test_db("test.db"); + + if (fs::exists(test_db)) + fs::unlink(test_db); + + { + store::write_backend backend = store::write_backend::open_rw(test_db); + store::write_transaction tx = backend.start_write(); + tx.put_context(exp_context); + tx.commit(); + } + { + store::read_backend backend = store::read_backend::open_ro(test_db); + store::read_transaction tx = backend.start_read(); + model::context context = tx.get_context(); + tx.finish(); + + ATF_REQUIRE(exp_context == context); + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE(get_put_context__ok); +ATF_TEST_CASE_HEAD(get_put_context__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_put_context__ok) +{ + std::map< std::string, std::string > env1; + env1["A1"] = "foo"; + env1["A2"] = "bar"; + std::map< std::string, std::string > env2; + check_get_put_context(model::context(fs::path("/foo/bar"), env1)); + check_get_put_context(model::context(fs::path("/foo/bar"), env1)); + check_get_put_context(model::context(fs::path("/foo/baz"), env2)); +} + + +ATF_TEST_CASE(get_put_test_case__ok); +ATF_TEST_CASE_HEAD(get_put_test_case__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_put_test_case__ok) +{ + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("powerpc") + .add_allowed_architecture("x86_64") + .add_allowed_platform("amd64") + .add_allowed_platform("macppc") + .add_custom("user1", "value1") + .add_custom("user2", "value2") + .add_required_config("var1") + .add_required_config("var2") + .add_required_config("var3") + .add_required_file(fs::path("/file1/yes")) + .add_required_file(fs::path("/file2/foo")) + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("cp")) + .set_description("The description") + .set_has_cleanup(true) + .set_required_memory(units::bytes::parse("1k")) + .set_required_user("root") + .set_timeout(datetime::delta(520, 0)) + .build(); + + const model::test_program test_program = model::test_program_builder( + "atf", fs::path("the/binary"), fs::path("/some/root"), "the-suite") + .add_test_case("tc1") + .add_test_case("tc2", md2) + .build(); + + int64_t test_program_id; + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + + store::write_transaction tx = backend.start_write(); + test_program_id = tx.put_test_program(test_program); + tx.put_test_case(test_program, "tc1", test_program_id); + tx.put_test_case(test_program, "tc2", test_program_id); + tx.commit(); + } + + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + + store::read_transaction tx = backend.start_read(); + const model::test_program_ptr loaded_test_program = + store::detail::get_test_program(backend, test_program_id); + ATF_REQUIRE(test_program == *loaded_test_program); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, get_put_context__ok); + + ATF_ADD_TEST_CASE(tcs, get_put_test_case__ok); +} diff --git a/contrib/kyua/store/write_backend.cpp b/contrib/kyua/store/write_backend.cpp new file mode 100644 index 000000000000..7a3eb167f88f --- /dev/null +++ b/contrib/kyua/store/write_backend.cpp @@ -0,0 +1,208 @@ +// 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 "store/write_backend.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "store/read_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + + +/// The current schema version. +/// +/// Any new database gets this schema version. Existing databases with an older +/// schema version must be first migrated to the current schema with +/// migrate_schema() before they can be used. +/// +/// This must be kept in sync with the value in the corresponding schema_vX.sql +/// file, where X matches this version number. +/// +/// This variable is not const to allow tests to modify it. No other code +/// should change its value. +int store::detail::current_schema_version = 3; + + +namespace { + + +/// Checks if a database is empty (i.e. if it is new). +/// +/// \param db The database to check. +/// +/// \return True if the database is empty. +static bool +empty_database(sqlite::database& db) +{ + sqlite::statement stmt = db.create_statement("SELECT * FROM sqlite_master"); + return !stmt.step(); +} + + +} // anonymous namespace + + +/// Calculates the path to the schema file for the database. +/// +/// \return The path to the installed schema_vX.sql file that matches the +/// current_schema_version. +fs::path +store::detail::schema_file(void) +{ + return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR)) + / (F("schema_v%s.sql") % current_schema_version); +} + + +/// Initializes an empty database. +/// +/// \param db The database to initialize. +/// +/// \return The metadata record written into the new database. +/// +/// \throw store::error If there is a problem initializing the database. +store::metadata +store::detail::initialize(sqlite::database& db) +{ + PRE(empty_database(db)); + + const fs::path schema = schema_file(); + + LI(F("Populating new database with schema from %s") % schema); + try { + db.exec(utils::read_file(schema)); + + const metadata metadata = metadata::fetch_latest(db); + LI(F("New metadata entry %s") % metadata.timestamp()); + if (metadata.schema_version() != detail::current_schema_version) { + UNREACHABLE_MSG(F("current_schema_version is out of sync with " + "%s") % schema); + } + return metadata; + } catch (const store::integrity_error& e) { + // Could be raised by metadata::fetch_latest. + UNREACHABLE_MSG("Inconsistent code while creating a database"); + } catch (const sqlite::error& e) { + throw error(F("Failed to initialize database: %s") % e.what()); + } catch (const std::runtime_error& e) { + throw error(F("Cannot read database schema '%s'") % schema); + } +} + + +/// Internal implementation for the backend. +struct store::write_backend::impl : utils::noncopyable { + /// The SQLite database this backend talks to. + sqlite::database database; + + /// Constructor. + /// + /// \param database_ The SQLite database instance. + impl(sqlite::database& database_) : database(database_) + { + } +}; + + +/// Constructs a new backend. +/// +/// \param pimpl_ The internal data. +store::write_backend::write_backend(impl* pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Destructor. +store::write_backend::~write_backend(void) +{ +} + + +/// Opens a database in read-write mode and creates it if necessary. +/// +/// \param file The database file to be opened. +/// +/// \return The backend representation. +/// +/// \throw store::error If there is any problem opening or creating +/// the database. +store::write_backend +store::write_backend::open_rw(const fs::path& file) +{ + sqlite::database db = detail::open_and_setup( + file, sqlite::open_readwrite | sqlite::open_create); + if (!empty_database(db)) + throw error(F("%s already exists and is not empty; cannot open " + "for write") % file); + detail::initialize(db); + return write_backend(new impl(db)); +} + + +/// Closes the SQLite database. +void +store::write_backend::close(void) +{ + _pimpl->database.close(); +} + + +/// Gets the connection to the SQLite database. +/// +/// \return A database connection. +sqlite::database& +store::write_backend::database(void) +{ + return _pimpl->database; +} + + +/// Opens a write-only transaction. +/// +/// \return A new transaction. +store::write_transaction +store::write_backend::start_write(void) +{ + return write_transaction(*this); +} diff --git a/contrib/kyua/store/write_backend.hpp b/contrib/kyua/store/write_backend.hpp new file mode 100644 index 000000000000..a1d46f1450c0 --- /dev/null +++ b/contrib/kyua/store/write_backend.hpp @@ -0,0 +1,81 @@ +// 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 store/write_backend.hpp +/// Interface to the backend database for write-only operations. + +#if !defined(STORE_WRITE_BACKEND_HPP) +#define STORE_WRITE_BACKEND_HPP + +#include "store/write_backend_fwd.hpp" + +#include + +#include "store/metadata_fwd.hpp" +#include "store/write_transaction_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/sqlite/database_fwd.hpp" + +namespace store { + + +namespace detail { + + +utils::fs::path schema_file(void); +metadata initialize(utils::sqlite::database&); + + +} // anonymous namespace + + +/// Public interface to the database store for write-only operations. +class write_backend { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class metadata; + + write_backend(impl*); + +public: + ~write_backend(void); + + static write_backend open_rw(const utils::fs::path&); + void close(void); + + utils::sqlite::database& database(void); + write_transaction start_write(void); +}; + + +} // namespace store + +#endif // !defined(STORE_WRITE_BACKEND_HPP) diff --git a/contrib/kyua/store/write_backend_fwd.hpp b/contrib/kyua/store/write_backend_fwd.hpp new file mode 100644 index 000000000000..8f2ea12d25cb --- /dev/null +++ b/contrib/kyua/store/write_backend_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 store/write_backend_fwd.hpp +/// Forward declarations for store/write_backend.hpp + +#if !defined(STORE_WRITE_BACKEND_FWD_HPP) +#define STORE_WRITE_BACKEND_FWD_HPP + +namespace store { + + +namespace detail { + + +extern int current_schema_version; + + +} // namespace detail + + +class write_backend; + + +} // namespace store + +#endif // !defined(STORE_WRITE_BACKEND_FWD_HPP) diff --git a/contrib/kyua/store/write_backend_test.cpp b/contrib/kyua/store/write_backend_test.cpp new file mode 100644 index 000000000000..a1052154aaae --- /dev/null +++ b/contrib/kyua/store/write_backend_test.cpp @@ -0,0 +1,204 @@ +// 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 "store/write_backend.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE(detail__initialize__ok); +ATF_TEST_CASE_HEAD(detail__initialize__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(detail__initialize__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + const datetime::timestamp before = datetime::timestamp::now(); + const store::metadata md = store::detail::initialize(db); + const datetime::timestamp after = datetime::timestamp::now(); + + ATF_REQUIRE(md.timestamp() >= before.to_seconds()); + ATF_REQUIRE(md.timestamp() <= after.to_microseconds()); + ATF_REQUIRE_EQ(store::detail::current_schema_version, md.schema_version()); + + // Query some known tables to ensure they were created. + db.exec("SELECT * FROM metadata"); + + // And now query some know values. + sqlite::statement stmt = db.create_statement( + "SELECT COUNT(*) FROM metadata"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(1, stmt.column_int(0)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__initialize__missing_schema); +ATF_TEST_CASE_BODY(detail__initialize__missing_schema) +{ + utils::setenv("KYUA_STOREDIR", "/non-existent"); + store::detail::current_schema_version = 712; + + sqlite::database db = sqlite::database::in_memory(); + ATF_REQUIRE_THROW_RE(store::error, + "Cannot read.*'/non-existent/schema_v712.sql'", + store::detail::initialize(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__initialize__sqlite_error); +ATF_TEST_CASE_BODY(detail__initialize__sqlite_error) +{ + utils::setenv("KYUA_STOREDIR", "."); + store::detail::current_schema_version = 712; + + atf::utils::create_file("schema_v712.sql", "foo_bar_baz;\n"); + + sqlite::database db = sqlite::database::in_memory(); + ATF_REQUIRE_THROW_RE(store::error, "Failed to initialize.*:.*foo_bar_baz", + store::detail::initialize(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__schema_file__builtin); +ATF_TEST_CASE_BODY(detail__schema_file__builtin) +{ + utils::unsetenv("KYUA_STOREDIR"); + ATF_REQUIRE_EQ(fs::path(KYUA_STOREDIR) / "schema_v3.sql", + store::detail::schema_file()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__schema_file__overriden); +ATF_TEST_CASE_BODY(detail__schema_file__overriden) +{ + utils::setenv("KYUA_STOREDIR", "/tmp/test"); + store::detail::current_schema_version = 123; + ATF_REQUIRE_EQ(fs::path("/tmp/test/schema_v123.sql"), + store::detail::schema_file()); +} + + +ATF_TEST_CASE(write_backend__open_rw__ok_if_empty); +ATF_TEST_CASE_HEAD(write_backend__open_rw__ok_if_empty) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__open_rw__ok_if_empty) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + } + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); +} + + +ATF_TEST_CASE(write_backend__open_rw__error_if_not_empty); +ATF_TEST_CASE_HEAD(write_backend__open_rw__error_if_not_empty) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__open_rw__error_if_not_empty) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + } + ATF_REQUIRE_THROW_RE(store::error, "test.db already exists", + store::write_backend::open_rw(fs::path("test.db"))); +} + + +ATF_TEST_CASE(write_backend__open_rw__create_missing); +ATF_TEST_CASE_HEAD(write_backend__open_rw__create_missing) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__open_rw__create_missing) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); +} + + +ATF_TEST_CASE(write_backend__close); +ATF_TEST_CASE_HEAD(write_backend__close) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__close) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); + backend.close(); + ATF_REQUIRE_THROW(utils::sqlite::error, + backend.database().exec("SELECT * FROM metadata")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__initialize__ok); + ATF_ADD_TEST_CASE(tcs, detail__initialize__missing_schema); + ATF_ADD_TEST_CASE(tcs, detail__initialize__sqlite_error); + + ATF_ADD_TEST_CASE(tcs, detail__schema_file__builtin); + ATF_ADD_TEST_CASE(tcs, detail__schema_file__overriden); + + ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__ok_if_empty); + ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__error_if_not_empty); + ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__create_missing); + ATF_ADD_TEST_CASE(tcs, write_backend__close); +} diff --git a/contrib/kyua/store/write_transaction.cpp b/contrib/kyua/store/write_transaction.cpp new file mode 100644 index 000000000000..134a13a30494 --- /dev/null +++ b/contrib/kyua/store/write_transaction.cpp @@ -0,0 +1,440 @@ +// 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 "store/write_transaction.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "model/types.hpp" +#include "store/dbtypes.hpp" +#include "store/exceptions.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#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/stream.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/transaction.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Stores the environment variables of a context. +/// +/// \param db The SQLite database. +/// \param env The environment variables to store. +/// +/// \throw sqlite::error If there is a problem storing the variables. +static void +put_env_vars(sqlite::database& db, + const std::map< std::string, std::string >& env) +{ + sqlite::statement stmt = db.create_statement( + "INSERT INTO env_vars (var_name, var_value) " + "VALUES (:var_name, :var_value)"); + for (std::map< std::string, std::string >::const_iterator iter = + env.begin(); iter != env.end(); iter++) { + stmt.bind(":var_name", (*iter).first); + stmt.bind(":var_value", (*iter).second); + stmt.step_without_results(); + stmt.reset(); + } +} + + +/// Calculates the last rowid of a table. +/// +/// \param db The SQLite database. +/// \param table Name of the table. +/// +/// \return The last rowid; 0 if the table is empty. +static int64_t +last_rowid(sqlite::database& db, const std::string& table) +{ + sqlite::statement stmt = db.create_statement( + F("SELECT MAX(ROWID) AS max_rowid FROM %s") % table); + stmt.step(); + if (stmt.column_type(0) == sqlite::type_null) { + return 0; + } else { + INV(stmt.column_type(0) == sqlite::type_integer); + return stmt.column_int64(0); + } +} + + +/// Stores a metadata object. +/// +/// \param db The database into which to store the information. +/// \param md The metadata to store. +/// +/// \return The identifier of the new metadata object. +static int64_t +put_metadata(sqlite::database& db, const model::metadata& md) +{ + const model::properties_map props = md.to_properties(); + + const int64_t metadata_id = last_rowid(db, "metadatas"); + + sqlite::statement stmt = db.create_statement( + "INSERT INTO metadatas (metadata_id, property_name, property_value) " + "VALUES (:metadata_id, :property_name, :property_value)"); + stmt.bind(":metadata_id", metadata_id); + + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + stmt.bind(":property_name", (*iter).first); + stmt.bind(":property_value", (*iter).second); + stmt.step_without_results(); + stmt.reset(); + } + + return metadata_id; +} + + +/// Stores an arbitrary file into the database as a BLOB. +/// +/// \param db The database into which to store the file. +/// \param path Path to the file to be stored. +/// +/// \return The identifier of the stored file, or none if the file was empty. +/// +/// \throw sqlite::error If there are problems writing to the database. +static optional< int64_t > +put_file(sqlite::database& db, const fs::path& path) +{ + std::ifstream input(path.c_str()); + if (!input) + throw store::error(F("Cannot open file %s") % path); + + try { + if (utils::stream_length(input) == 0) + return none; + } catch (const std::runtime_error& e) { + // Skipping empty files is an optimization. If we fail to calculate the + // size of the file, just ignore the problem. If there are real issues + // with the file, the read below will fail anyway. + LD(F("Cannot determine if file is empty: %s") % e.what()); + } + + // TODO(jmmv): This will probably cause an unreasonable amount of memory + // consumption if we decide to store arbitrary files in the database (other + // than stdout or stderr). Should this happen, we need to investigate a + // better way to feel blobs into SQLite. + const std::string contents = utils::read_stream(input); + + sqlite::statement stmt = db.create_statement( + "INSERT INTO files (contents) VALUES (:contents)"); + stmt.bind(":contents", sqlite::blob(contents.c_str(), contents.length())); + stmt.step_without_results(); + + return optional< int64_t >(db.last_insert_rowid()); +} + + +} // anonymous namespace + + +/// Internal implementation for a store write-only transaction. +struct store::write_transaction::impl : utils::noncopyable { + /// The backend instance. + store::write_backend& _backend; + + /// The SQLite database this transaction deals with. + sqlite::database _db; + + /// The backing SQLite transaction. + sqlite::transaction _tx; + + /// Opens a transaction. + /// + /// \param backend_ The backend this transaction is connected to. + impl(write_backend& backend_) : + _backend(backend_), + _db(backend_.database()), + _tx(backend_.database().begin_transaction()) + { + } +}; + + +/// Creates a new write-only transaction. +/// +/// \param backend_ The backend this transaction belongs to. +store::write_transaction::write_transaction(write_backend& backend_) : + _pimpl(new impl(backend_)) +{ +} + + +/// Destructor. +store::write_transaction::~write_transaction(void) +{ +} + + +/// Commits the transaction. +/// +/// \throw error If there is any problem when talking to the database. +void +store::write_transaction::commit(void) +{ + try { + _pimpl->_tx.commit(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Rolls the transaction back. +/// +/// \throw error If there is any problem when talking to the database. +void +store::write_transaction::rollback(void) +{ + try { + _pimpl->_tx.rollback(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a context into the database. +/// +/// \pre The context has not been put yet. +/// \post The context is stored into the database with a new identifier. +/// +/// \param context The context to put. +/// +/// \throw error If there is any problem when talking to the database. +void +store::write_transaction::put_context(const model::context& context) +{ + try { + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO contexts (cwd) VALUES (:cwd)"); + stmt.bind(":cwd", context.cwd().str()); + stmt.step_without_results(); + + put_env_vars(_pimpl->_db, context.env()); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a test program into the database. +/// +/// \pre The test program has not been put yet. +/// \post The test program is stored into the database with a new identifier. +/// +/// \param test_program The test program to put. +/// +/// \return The identifier of the inserted test program. +/// +/// \throw error If there is any problem when talking to the database. +int64_t +store::write_transaction::put_test_program( + const model::test_program& test_program) +{ + try { + const int64_t metadata_id = put_metadata( + _pimpl->_db, test_program.get_metadata()); + + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_programs (absolute_path, " + " root, relative_path, test_suite_name, " + " metadata_id, interface) " + "VALUES (:absolute_path, :root, :relative_path, " + " :test_suite_name, :metadata_id, :interface)"); + stmt.bind(":absolute_path", test_program.absolute_path().str()); + // TODO(jmmv): The root is not necessarily absolute. We need to ensure + // that we can recover the absolute path of the test program. Maybe we + // need to change the test_program to always ensure root is absolute? + stmt.bind(":root", test_program.root().str()); + stmt.bind(":relative_path", test_program.relative_path().str()); + stmt.bind(":test_suite_name", test_program.test_suite_name()); + stmt.bind(":metadata_id", metadata_id); + stmt.bind(":interface", test_program.interface_name()); + stmt.step_without_results(); + return _pimpl->_db.last_insert_rowid(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a test case into the database. +/// +/// \pre The test case has not been put yet. +/// \post The test case is stored into the database with a new identifier. +/// +/// \param test_program The program containing the test case to be stored. +/// \param test_case_name The name of the test case to put. +/// \param test_program_id The test program this test case belongs to. +/// +/// \return The identifier of the inserted test case. +/// +/// \throw error If there is any problem when talking to the database. +int64_t +store::write_transaction::put_test_case(const model::test_program& test_program, + const std::string& test_case_name, + const int64_t test_program_id) +{ + const model::test_case& test_case = test_program.find(test_case_name); + + try { + const int64_t metadata_id = put_metadata( + _pimpl->_db, test_case.get_raw_metadata()); + + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_cases (test_program_id, name, metadata_id) " + "VALUES (:test_program_id, :name, :metadata_id)"); + stmt.bind(":test_program_id", test_program_id); + stmt.bind(":name", test_case.name()); + stmt.bind(":metadata_id", metadata_id); + stmt.step_without_results(); + return _pimpl->_db.last_insert_rowid(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Stores a file generated by a test case into the database as a BLOB. +/// +/// \param name The name of the file to store in the database. This needs to be +/// unique per test case. The caller is free to decide what names to use +/// for which files. For example, it might make sense to always call +/// __STDOUT__ the stdout of the test case so that it is easy to locate. +/// \param path The path to the file to be stored. +/// \param test_case_id The identifier of the test case this file belongs to. +/// +/// \return The identifier of the stored file, or none if the file was empty. +/// +/// \throw store::error If there are problems writing to the database. +optional< int64_t > +store::write_transaction::put_test_case_file(const std::string& name, + const fs::path& path, + const int64_t test_case_id) +{ + LD(F("Storing %s (%s) of test case %s") % name % path % test_case_id); + try { + const optional< int64_t > file_id = put_file(_pimpl->_db, path); + if (!file_id) { + LD("Not storing empty file"); + return none; + } + + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_case_files (test_case_id, file_name, file_id) " + "VALUES (:test_case_id, :file_name, :file_id)"); + stmt.bind(":test_case_id", test_case_id); + stmt.bind(":file_name", name); + stmt.bind(":file_id", file_id.get()); + stmt.step_without_results(); + + return optional< int64_t >(_pimpl->_db.last_insert_rowid()); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a result into the database. +/// +/// \pre The result has not been put yet. +/// \post The result is stored into the database with a new identifier. +/// +/// \param result The result to put. +/// \param test_case_id The test case this result corresponds to. +/// \param start_time The time when the test started to run. +/// \param end_time The time when the test finished running. +/// +/// \return The identifier of the inserted result. +/// +/// \throw error If there is any problem when talking to the database. +int64_t +store::write_transaction::put_result(const model::test_result& result, + const int64_t test_case_id, + const datetime::timestamp& start_time, + const datetime::timestamp& end_time) +{ + try { + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_results (test_case_id, result_type, " + " result_reason, start_time, " + " end_time) " + "VALUES (:test_case_id, :result_type, :result_reason, " + " :start_time, :end_time)"); + stmt.bind(":test_case_id", test_case_id); + + store::bind_test_result_type(stmt, ":result_type", result.type()); + if (result.reason().empty()) + stmt.bind(":result_reason", sqlite::null()); + else + stmt.bind(":result_reason", result.reason()); + + store::bind_timestamp(stmt, ":start_time", start_time); + store::bind_timestamp(stmt, ":end_time", end_time); + + stmt.step_without_results(); + const int64_t result_id = _pimpl->_db.last_insert_rowid(); + + return result_id; + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} diff --git a/contrib/kyua/store/write_transaction.hpp b/contrib/kyua/store/write_transaction.hpp new file mode 100644 index 000000000000..5c73d20af788 --- /dev/null +++ b/contrib/kyua/store/write_transaction.hpp @@ -0,0 +1,89 @@ +// 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 store/write_transaction.hpp +/// Implementation of write-only transactions on the backend. + +#if !defined(STORE_WRITE_TRANSACTION_HPP) +#define STORE_WRITE_TRANSACTION_HPP + +#include "store/write_transaction_fwd.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result_fwd.hpp" +#include "store/write_backend_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace store { + + +/// Representation of a write-only transaction. +/// +/// Transactions are the entry place for high-level calls that access the +/// database. +class write_transaction { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class write_backend; + write_transaction(write_backend&); + +public: + ~write_transaction(void); + + void commit(void); + void rollback(void); + + void put_context(const model::context&); + int64_t put_test_program(const model::test_program&); + int64_t put_test_case(const model::test_program&, const std::string&, + const int64_t); + utils::optional< int64_t > put_test_case_file(const std::string&, + const utils::fs::path&, + const int64_t); + int64_t put_result(const model::test_result&, const int64_t, + const utils::datetime::timestamp&, + const utils::datetime::timestamp&); +}; + + +} // namespace store + +#endif // !defined(STORE_WRITE_TRANSACTION_HPP) diff --git a/contrib/kyua/store/write_transaction_fwd.hpp b/contrib/kyua/store/write_transaction_fwd.hpp new file mode 100644 index 000000000000..1d2357a52dbe --- /dev/null +++ b/contrib/kyua/store/write_transaction_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 store/write_transaction_fwd.hpp +/// Forward declarations for store/write_transaction.hpp + +#if !defined(STORE_WRITE_TRANSACTION_FWD_HPP) +#define STORE_WRITE_TRANSACTION_FWD_HPP + +namespace store { + + +class write_transaction; + + +} // namespace store + +#endif // !defined(STORE_WRITE_TRANSACTION_FWD_HPP) diff --git a/contrib/kyua/store/write_transaction_test.cpp b/contrib/kyua/store/write_transaction_test.cpp new file mode 100644 index 000000000000..984e328dcdae --- /dev/null +++ b/contrib/kyua/store/write_transaction_test.cpp @@ -0,0 +1,416 @@ +// 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 "store/write_transaction.hpp" + +#include +#include +#include + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +namespace { + + +/// Performs a test for a working put_result +/// +/// \param result The result object to put. +/// \param result_type The textual name of the result to expect in the +/// database. +/// \param exp_reason The reason to expect in the database. This is separate +/// from the result parameter so that we can handle passed() here as well. +/// Just provide NULL in this case. +static void +do_put_result_ok_test(const model::test_result& result, + const char* result_type, const char* exp_reason) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2012, 01, 30, 22, 10, 00, 0); + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2012, 01, 30, 22, 15, 30, 123456); + tx.put_result(result, 312, start_time, end_time); + tx.commit(); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT test_case_id, result_type, result_reason " + "FROM test_results"); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(312, stmt.column_int64(0)); + ATF_REQUIRE_EQ(result_type, stmt.column_text(1)); + if (exp_reason != NULL) + ATF_REQUIRE_EQ(exp_reason, stmt.column_text(2)); + else + ATF_REQUIRE(stmt.column_type(2) == sqlite::type_null); + ATF_REQUIRE(!stmt.step()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE(commit__ok); +ATF_TEST_CASE_HEAD(commit__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(commit__ok) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + backend.database().exec("CREATE TABLE a (b INTEGER PRIMARY KEY)"); + backend.database().exec("SELECT * FROM a"); + tx.commit(); + backend.database().exec("SELECT * FROM a"); +} + + +ATF_TEST_CASE(commit__fail); +ATF_TEST_CASE_HEAD(commit__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(commit__fail) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + const model::context context(fs::path("/foo/bar"), + std::map< std::string, std::string >()); + { + store::write_transaction tx = backend.start_write(); + tx.put_context(context); + backend.database().exec( + "CREATE TABLE foo (" + "a REFERENCES env_vars(var_name) DEFERRABLE INITIALLY DEFERRED)"); + backend.database().exec("INSERT INTO foo VALUES (\"WHAT\")"); + ATF_REQUIRE_THROW(store::error, tx.commit()); + } + // If the code attempts to maintain any state regarding the already-put + // objects and the commit does not clean up correctly, this would fail in + // some manner. + store::write_transaction tx = backend.start_write(); + tx.put_context(context); + tx.commit(); +} + + +ATF_TEST_CASE(rollback__ok); +ATF_TEST_CASE_HEAD(rollback__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(rollback__ok) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + backend.database().exec("CREATE TABLE a_table (b INTEGER PRIMARY KEY)"); + backend.database().exec("SELECT * FROM a_table"); + tx.rollback(); + ATF_REQUIRE_THROW_RE(sqlite::error, "a_table", + backend.database().exec("SELECT * FROM a_table")); +} + + +ATF_TEST_CASE(put_test_program__ok); +ATF_TEST_CASE_HEAD(put_test_program__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_program__ok) +{ + const model::metadata md = model::metadata_builder() + .add_custom("var1", "value1") + .add_custom("var2", "value2") + .build(); + const model::test_program test_program( + "mock", fs::path("the/binary"), fs::path("/some//root"), + "the-suite", md, model::test_cases_map()); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const int64_t test_program_id = tx.put_test_program(test_program); + tx.commit(); + + { + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_programs"); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(test_program_id, + stmt.safe_column_int64("test_program_id")); + ATF_REQUIRE_EQ("/some/root/the/binary", + stmt.safe_column_text("absolute_path")); + ATF_REQUIRE_EQ("/some/root", stmt.safe_column_text("root")); + ATF_REQUIRE_EQ("the/binary", stmt.safe_column_text("relative_path")); + ATF_REQUIRE_EQ("the-suite", stmt.safe_column_text("test_suite_name")); + ATF_REQUIRE(!stmt.step()); + } +} + + +ATF_TEST_CASE(put_test_case__fail); +ATF_TEST_CASE_HEAD(put_test_case__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case__fail) +{ + const model::test_program test_program = model::test_program_builder( + "plain", fs::path("the/binary"), fs::path("/some/root"), "the-suite") + .add_test_case("main") + .build(); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + ATF_REQUIRE_THROW(store::error, tx.put_test_case(test_program, "main", -1)); + tx.commit(); +} + + +ATF_TEST_CASE(put_test_case_file__empty); +ATF_TEST_CASE_HEAD(put_test_case_file__empty) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case_file__empty) +{ + atf::utils::create_file("input.txt", ""); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const optional< int64_t > file_id = tx.put_test_case_file( + "my-file", fs::path("input.txt"), 123L); + tx.commit(); + ATF_REQUIRE(!file_id); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_case_files NATURAL JOIN files"); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE(put_test_case_file__some); +ATF_TEST_CASE_HEAD(put_test_case_file__some) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case_file__some) +{ + const char contents[] = "This is a test!"; + + atf::utils::create_file("input.txt", contents); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const optional< int64_t > file_id = tx.put_test_case_file( + "my-file", fs::path("input.txt"), 123L); + tx.commit(); + ATF_REQUIRE(file_id); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_case_files NATURAL JOIN files"); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(123L, stmt.safe_column_int64("test_case_id")); + ATF_REQUIRE_EQ("my-file", stmt.safe_column_text("file_name")); + const sqlite::blob blob = stmt.safe_column_blob("contents"); + ATF_REQUIRE(std::strlen(contents) == static_cast< std::size_t >(blob.size)); + ATF_REQUIRE(std::memcmp(contents, blob.memory, blob.size) == 0); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE(put_test_case_file__fail); +ATF_TEST_CASE_HEAD(put_test_case_file__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case_file__fail) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + ATF_REQUIRE_THROW(store::error, + tx.put_test_case_file("foo", fs::path("missing"), 1L)); + tx.commit(); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_case_files NATURAL JOIN files"); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE(put_result__ok__broken); +ATF_TEST_CASE_HEAD(put_result__ok__broken) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__broken) +{ + const model::test_result result(model::test_result_broken, "a b cd"); + do_put_result_ok_test(result, "broken", "a b cd"); +} + + +ATF_TEST_CASE(put_result__ok__expected_failure); +ATF_TEST_CASE_HEAD(put_result__ok__expected_failure) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__expected_failure) +{ + const model::test_result result(model::test_result_expected_failure, + "a b cd"); + do_put_result_ok_test(result, "expected_failure", "a b cd"); +} + + +ATF_TEST_CASE(put_result__ok__failed); +ATF_TEST_CASE_HEAD(put_result__ok__failed) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__failed) +{ + const model::test_result result(model::test_result_failed, "a b cd"); + do_put_result_ok_test(result, "failed", "a b cd"); +} + + +ATF_TEST_CASE(put_result__ok__passed); +ATF_TEST_CASE_HEAD(put_result__ok__passed) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__passed) +{ + const model::test_result result(model::test_result_passed); + do_put_result_ok_test(result, "passed", NULL); +} + + +ATF_TEST_CASE(put_result__ok__skipped); +ATF_TEST_CASE_HEAD(put_result__ok__skipped) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__skipped) +{ + const model::test_result result(model::test_result_skipped, "a b cd"); + do_put_result_ok_test(result, "skipped", "a b cd"); +} + + +ATF_TEST_CASE(put_result__fail); +ATF_TEST_CASE_HEAD(put_result__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__fail) +{ + const model::test_result result(model::test_result_broken, "foo"); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + const datetime::timestamp zero = datetime::timestamp::from_microseconds(0); + ATF_REQUIRE_THROW(store::error, tx.put_result(result, -1, zero, zero)); + tx.commit(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, commit__ok); + ATF_ADD_TEST_CASE(tcs, commit__fail); + ATF_ADD_TEST_CASE(tcs, rollback__ok); + + ATF_ADD_TEST_CASE(tcs, put_test_program__ok); + ATF_ADD_TEST_CASE(tcs, put_test_case__fail); + ATF_ADD_TEST_CASE(tcs, put_test_case_file__empty); + ATF_ADD_TEST_CASE(tcs, put_test_case_file__some); + ATF_ADD_TEST_CASE(tcs, put_test_case_file__fail); + + ATF_ADD_TEST_CASE(tcs, put_result__ok__broken); + ATF_ADD_TEST_CASE(tcs, put_result__ok__expected_failure); + ATF_ADD_TEST_CASE(tcs, put_result__ok__failed); + ATF_ADD_TEST_CASE(tcs, put_result__ok__passed); + ATF_ADD_TEST_CASE(tcs, put_result__ok__skipped); + ATF_ADD_TEST_CASE(tcs, put_result__fail); +} diff --git a/contrib/kyua/utils/.gitignore b/contrib/kyua/utils/.gitignore new file mode 100644 index 000000000000..b33d720f27a4 --- /dev/null +++ b/contrib/kyua/utils/.gitignore @@ -0,0 +1,2 @@ +defs.hpp +stacktrace_helper diff --git a/contrib/kyua/utils/Kyuafile b/contrib/kyua/utils/Kyuafile new file mode 100644 index 000000000000..042ad77a3fe4 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/Makefile.am.inc b/contrib/kyua/utils/Makefile.am.inc new file mode 100644 index 000000000000..d6690bdbecde --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/auto_array.hpp b/contrib/kyua/utils/auto_array.hpp new file mode 100644 index 000000000000..0cc3d0e0afd5 --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/auto_array.ipp b/contrib/kyua/utils/auto_array.ipp new file mode 100644 index 000000000000..fd29311def8c --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/auto_array_fwd.hpp b/contrib/kyua/utils/auto_array_fwd.hpp new file mode 100644 index 000000000000..e1522a25bf7d --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/auto_array_test.cpp b/contrib/kyua/utils/auto_array_test.cpp new file mode 100644 index 000000000000..041eb65863ba --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#include + +#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/contrib/kyua/utils/cmdline/Kyuafile b/contrib/kyua/utils/cmdline/Kyuafile new file mode 100644 index 000000000000..d5e6f7122b07 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/Makefile.am.inc b/contrib/kyua/utils/cmdline/Makefile.am.inc new file mode 100644 index 000000000000..65081cbeafee --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/base_command.cpp b/contrib/kyua/utils/cmdline/base_command.cpp new file mode 100644 index 000000000000..837ded9cffab --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/base_command.hpp b/contrib/kyua/utils/cmdline/base_command.hpp new file mode 100644 index 000000000000..819dfe98dad3 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/cmdline/base_command.ipp b/contrib/kyua/utils/cmdline/base_command.ipp new file mode 100644 index 000000000000..5696637085d7 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/base_command_fwd.hpp b/contrib/kyua/utils/cmdline/base_command_fwd.hpp new file mode 100644 index 000000000000..c94db1ae2d05 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/base_command_test.cpp b/contrib/kyua/utils/cmdline/base_command_test.cpp new file mode 100644 index 000000000000..20df8ea49512 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/cmdline/commands_map.hpp b/contrib/kyua/utils/cmdline/commands_map.hpp new file mode 100644 index 000000000000..5378a6f2c471 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#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/contrib/kyua/utils/cmdline/commands_map.ipp b/contrib/kyua/utils/cmdline/commands_map.ipp new file mode 100644 index 000000000000..8be87ab3b5cc --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/commands_map_fwd.hpp b/contrib/kyua/utils/cmdline/commands_map_fwd.hpp new file mode 100644 index 000000000000..a81a852790da --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/commands_map_test.cpp b/contrib/kyua/utils/cmdline/commands_map_test.cpp new file mode 100644 index 000000000000..47a7404f64fb --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/cmdline/exceptions.cpp b/contrib/kyua/utils/cmdline/exceptions.cpp new file mode 100644 index 000000000000..fa9ba2218a7f --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/exceptions.hpp b/contrib/kyua/utils/cmdline/exceptions.hpp new file mode 100644 index 000000000000..59f99e835ce1 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/cmdline/exceptions_test.cpp b/contrib/kyua/utils/cmdline/exceptions_test.cpp new file mode 100644 index 000000000000..b541e08f6995 --- /dev/null +++ b/contrib/kyua/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 + +#include + +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/contrib/kyua/utils/cmdline/globals.cpp b/contrib/kyua/utils/cmdline/globals.cpp new file mode 100644 index 000000000000..76e0231fa36b --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/globals.hpp b/contrib/kyua/utils/cmdline/globals.hpp new file mode 100644 index 000000000000..ab7904d69520 --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/cmdline/globals_test.cpp b/contrib/kyua/utils/cmdline/globals_test.cpp new file mode 100644 index 000000000000..5c2ac7cc2d6c --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/cmdline/options.cpp b/contrib/kyua/utils/cmdline/options.cpp new file mode 100644 index 000000000000..61736e31c11e --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/cmdline/options.hpp b/contrib/kyua/utils/cmdline/options.hpp new file mode 100644 index 000000000000..f3a83889e491 --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#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: +/// +///
    +///
  • A public option_type typedef that defines the type of the +/// option.
  • +/// +///
  • 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.
  • +/// +///
  • 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.
  • +///
+/// +/// 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/contrib/kyua/utils/cmdline/options_fwd.hpp b/contrib/kyua/utils/cmdline/options_fwd.hpp new file mode 100644 index 000000000000..8b45797e3920 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/options_test.cpp b/contrib/kyua/utils/cmdline/options_test.cpp new file mode 100644 index 000000000000..82fd706a191a --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/cmdline/parser.cpp b/contrib/kyua/utils/cmdline/parser.cpp new file mode 100644 index 000000000000..5c83f6d69cc4 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include +#include + +#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/contrib/kyua/utils/cmdline/parser.hpp b/contrib/kyua/utils/cmdline/parser.hpp new file mode 100644 index 000000000000..657fd1f01dd3 --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +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/contrib/kyua/utils/cmdline/parser.ipp b/contrib/kyua/utils/cmdline/parser.ipp new file mode 100644 index 000000000000..820826a15bfe --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/parser_fwd.hpp b/contrib/kyua/utils/cmdline/parser_fwd.hpp new file mode 100644 index 000000000000..a136e99a47ac --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/cmdline/parser_test.cpp b/contrib/kyua/utils/cmdline/parser_test.cpp new file mode 100644 index 000000000000..96370d279d2e --- /dev/null +++ b/contrib/kyua/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 +#include +#include +} + +#include +#include +#include +#include +#include +#include + +#include + +#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/contrib/kyua/utils/cmdline/ui.cpp b/contrib/kyua/utils/cmdline/ui.cpp new file mode 100644 index 000000000000..a682360a4259 --- /dev/null +++ b/contrib/kyua/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 +#include + +#if defined(HAVE_TERMIOS_H) +# include +#endif +#include +} + +#include + +#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/contrib/kyua/utils/cmdline/ui.hpp b/contrib/kyua/utils/cmdline/ui.hpp new file mode 100644 index 000000000000..433bbe903b03 --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/cmdline/ui_fwd.hpp b/contrib/kyua/utils/cmdline/ui_fwd.hpp new file mode 100644 index 000000000000..4417beb1a8e8 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/cmdline/ui_mock.cpp b/contrib/kyua/utils/cmdline/ui_mock.cpp new file mode 100644 index 000000000000..b77943cf147b --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/cmdline/ui_mock.hpp b/contrib/kyua/utils/cmdline/ui_mock.hpp new file mode 100644 index 000000000000..2c37683af7f3 --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#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/contrib/kyua/utils/cmdline/ui_test.cpp b/contrib/kyua/utils/cmdline/ui_test.cpp new file mode 100644 index 000000000000..92c64baf95a3 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +#if defined(HAVE_TERMIOS_H) +# include +#endif +#include +} + +#include +#include + +#include + +#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/contrib/kyua/utils/config/Kyuafile b/contrib/kyua/utils/config/Kyuafile new file mode 100644 index 000000000000..c607a1757275 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/config/Makefile.am.inc b/contrib/kyua/utils/config/Makefile.am.inc new file mode 100644 index 000000000000..7c276ec4e798 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/config/exceptions.cpp b/contrib/kyua/utils/config/exceptions.cpp new file mode 100644 index 000000000000..e9afdf7ea6f7 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/config/exceptions.hpp b/contrib/kyua/utils/config/exceptions.hpp new file mode 100644 index 000000000000..2096e67f43c8 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/config/exceptions_test.cpp b/contrib/kyua/utils/config/exceptions_test.cpp new file mode 100644 index 000000000000..a82fb9ea8f0c --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/config/keys.cpp b/contrib/kyua/utils/config/keys.cpp new file mode 100644 index 000000000000..574eee14dcd2 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/config/keys.hpp b/contrib/kyua/utils/config/keys.hpp new file mode 100644 index 000000000000..ad258d69fc08 --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/config/keys_fwd.hpp b/contrib/kyua/utils/config/keys_fwd.hpp new file mode 100644 index 000000000000..101272698b65 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/config/keys_test.cpp b/contrib/kyua/utils/config/keys_test.cpp new file mode 100644 index 000000000000..dc30f0fc8806 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/config/lua_module.cpp b/contrib/kyua/utils/config/lua_module.cpp new file mode 100644 index 000000000000..891f07302e0a --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/config/lua_module.hpp b/contrib/kyua/utils/config/lua_module.hpp new file mode 100644 index 000000000000..7f0d5d0b4c5f --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/config/lua_module_test.cpp b/contrib/kyua/utils/config/lua_module_test.cpp new file mode 100644 index 000000000000..484d129c4021 --- /dev/null +++ b/contrib/kyua/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 + +#include +#include +#include + +#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/contrib/kyua/utils/config/nodes.cpp b/contrib/kyua/utils/config/nodes.cpp new file mode 100644 index 000000000000..1c6e848daf07 --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/config/nodes.hpp b/contrib/kyua/utils/config/nodes.hpp new file mode 100644 index 000000000000..6b766ff5d8f7 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/config/nodes.ipp b/contrib/kyua/utils/config/nodes.ipp new file mode 100644 index 000000000000..9e0a1228cccd --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/config/nodes_fwd.hpp b/contrib/kyua/utils/config/nodes_fwd.hpp new file mode 100644 index 000000000000..b03328e79e95 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/config/nodes_test.cpp b/contrib/kyua/utils/config/nodes_test.cpp new file mode 100644 index 000000000000..e762d3aac38c --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/config/parser.cpp b/contrib/kyua/utils/config/parser.cpp new file mode 100644 index 000000000000..7bfe5517fdd0 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#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/contrib/kyua/utils/config/parser.hpp b/contrib/kyua/utils/config/parser.hpp new file mode 100644 index 000000000000..cb69e756cbe8 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/config/parser_fwd.hpp b/contrib/kyua/utils/config/parser_fwd.hpp new file mode 100644 index 000000000000..6278b6c95c12 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/config/parser_test.cpp b/contrib/kyua/utils/config/parser_test.cpp new file mode 100644 index 000000000000..f5445f55c490 --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/config/tree.cpp b/contrib/kyua/utils/config/tree.cpp new file mode 100644 index 000000000000..1aa2d85b89cd --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/config/tree.hpp b/contrib/kyua/utils/config/tree.hpp new file mode 100644 index 000000000000..cad0a9b4fc0b --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/config/tree.ipp b/contrib/kyua/utils/config/tree.ipp new file mode 100644 index 000000000000..a79acc3be184 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/config/tree_fwd.hpp b/contrib/kyua/utils/config/tree_fwd.hpp new file mode 100644 index 000000000000..e494d8c0f4ee --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/config/tree_test.cpp b/contrib/kyua/utils/config/tree_test.cpp new file mode 100644 index 000000000000..b6efd64a84a6 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/datetime.cpp b/contrib/kyua/utils/datetime.cpp new file mode 100644 index 000000000000..ae3fdb62fe55 --- /dev/null +++ b/contrib/kyua/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 + +#include +} + +#include + +#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/contrib/kyua/utils/datetime.hpp b/contrib/kyua/utils/datetime.hpp new file mode 100644 index 000000000000..0c24f332f6d3 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include +#include +#include + + +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/contrib/kyua/utils/datetime_fwd.hpp b/contrib/kyua/utils/datetime_fwd.hpp new file mode 100644 index 000000000000..1dd886070a34 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/datetime_test.cpp b/contrib/kyua/utils/datetime_test.cpp new file mode 100644 index 000000000000..9f8ff50cd0f8 --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include +#include + +#include + +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/contrib/kyua/utils/defs.hpp.in b/contrib/kyua/utils/defs.hpp.in new file mode 100644 index 000000000000..62fc50d0e525 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/env.cpp b/contrib/kyua/utils/env.cpp new file mode 100644 index 000000000000..b0d995c0ff31 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#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/contrib/kyua/utils/env.hpp b/contrib/kyua/utils/env.hpp new file mode 100644 index 000000000000..2370ee490dc1 --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/env_test.cpp b/contrib/kyua/utils/env_test.cpp new file mode 100644 index 000000000000..1b16266443af --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/format/Kyuafile b/contrib/kyua/utils/format/Kyuafile new file mode 100644 index 000000000000..344ae455422c --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/format/Makefile.am.inc b/contrib/kyua/utils/format/Makefile.am.inc new file mode 100644 index 000000000000..a37fc4057079 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/format/containers.hpp b/contrib/kyua/utils/format/containers.hpp new file mode 100644 index 000000000000..7334c250de4e --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include +#include +#include + + +// 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/contrib/kyua/utils/format/containers.ipp b/contrib/kyua/utils/format/containers.ipp new file mode 100644 index 000000000000..11d8e2914149 --- /dev/null +++ b/contrib/kyua/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 + + +/// 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 << ""; + } 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/contrib/kyua/utils/format/containers_test.cpp b/contrib/kyua/utils/format/containers_test.cpp new file mode 100644 index 000000000000..e1c452da2df6 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include +#include +#include + +#include + + + +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("", std::shared_ptr< char >()); + do_check("", 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/contrib/kyua/utils/format/exceptions.cpp b/contrib/kyua/utils/format/exceptions.cpp new file mode 100644 index 000000000000..299b1d23cd8d --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/format/exceptions.hpp b/contrib/kyua/utils/format/exceptions.hpp new file mode 100644 index 000000000000..a28376df9c08 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/format/exceptions_test.cpp b/contrib/kyua/utils/format/exceptions_test.cpp new file mode 100644 index 000000000000..28d401e57dad --- /dev/null +++ b/contrib/kyua/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 + +#include + +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/contrib/kyua/utils/format/formatter.cpp b/contrib/kyua/utils/format/formatter.cpp new file mode 100644 index 000000000000..99cfd40f03ab --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#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/contrib/kyua/utils/format/formatter.hpp b/contrib/kyua/utils/format/formatter.hpp new file mode 100644 index 000000000000..8c6188745a2e --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/format/formatter.ipp b/contrib/kyua/utils/format/formatter.ipp new file mode 100644 index 000000000000..6fad024b704f --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/format/formatter_fwd.hpp b/contrib/kyua/utils/format/formatter_fwd.hpp new file mode 100644 index 000000000000..72c9e5ebf196 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/format/formatter_test.cpp b/contrib/kyua/utils/format/formatter_test.cpp new file mode 100644 index 000000000000..fdae785b1db7 --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/format/macros.hpp b/contrib/kyua/utils/format/macros.hpp new file mode 100644 index 000000000000..09ef14ea485e --- /dev/null +++ b/contrib/kyua/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 must not be included from other header files. + +#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/contrib/kyua/utils/fs/Kyuafile b/contrib/kyua/utils/fs/Kyuafile new file mode 100644 index 000000000000..66cb918fca92 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/fs/Makefile.am.inc b/contrib/kyua/utils/fs/Makefile.am.inc new file mode 100644 index 000000000000..2acdadafa79b --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/fs/auto_cleaners.cpp b/contrib/kyua/utils/fs/auto_cleaners.cpp new file mode 100644 index 000000000000..94ef94465e57 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/fs/auto_cleaners.hpp b/contrib/kyua/utils/fs/auto_cleaners.hpp new file mode 100644 index 000000000000..f3e6937e3cea --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/fs/auto_cleaners_fwd.hpp b/contrib/kyua/utils/fs/auto_cleaners_fwd.hpp new file mode 100644 index 000000000000..c0cfa6333a1a --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/fs/auto_cleaners_test.cpp b/contrib/kyua/utils/fs/auto_cleaners_test.cpp new file mode 100644 index 000000000000..da4bbeb2da68 --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#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/contrib/kyua/utils/fs/directory.cpp b/contrib/kyua/utils/fs/directory.cpp new file mode 100644 index 000000000000..ff7ad5e34357 --- /dev/null +++ b/contrib/kyua/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 + +#include +} + +#include +#include + +#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/contrib/kyua/utils/fs/directory.hpp b/contrib/kyua/utils/fs/directory.hpp new file mode 100644 index 000000000000..53c37ec86450 --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#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/contrib/kyua/utils/fs/directory_fwd.hpp b/contrib/kyua/utils/fs/directory_fwd.hpp new file mode 100644 index 000000000000..50886551ca88 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/fs/directory_test.cpp b/contrib/kyua/utils/fs/directory_test.cpp new file mode 100644 index 000000000000..4c1aa2d010f4 --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/fs/exceptions.cpp b/contrib/kyua/utils/fs/exceptions.cpp new file mode 100644 index 000000000000..102e9069ee3c --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/fs/exceptions.hpp b/contrib/kyua/utils/fs/exceptions.hpp new file mode 100644 index 000000000000..32b4af2ce463 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/fs/exceptions_test.cpp b/contrib/kyua/utils/fs/exceptions_test.cpp new file mode 100644 index 000000000000..e67a846506cc --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/fs/lua_module.cpp b/contrib/kyua/utils/fs/lua_module.cpp new file mode 100644 index 000000000000..dec410927e1a --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include +#include +#include + +#include +#include +#include + +#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/contrib/kyua/utils/fs/lua_module.hpp b/contrib/kyua/utils/fs/lua_module.hpp new file mode 100644 index 000000000000..c9c303b15eb7 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/fs/lua_module_test.cpp b/contrib/kyua/utils/fs/lua_module_test.cpp new file mode 100644 index 000000000000..263632ded13f --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#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/contrib/kyua/utils/fs/operations.cpp b/contrib/kyua/utils/fs/operations.cpp new file mode 100644 index 000000000000..7a96d0b2058a --- /dev/null +++ b/contrib/kyua/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 +#if defined(HAVE_SYS_MOUNT_H) +# include +#endif +#include +#if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS) +# include +#endif +#if defined(HAVE_SYS_VFS_H) +# include +#endif +#include + +#include +} + +#include +#include +#include +#include +#include +#include +#include + +#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/contrib/kyua/utils/fs/operations.hpp b/contrib/kyua/utils/fs/operations.hpp new file mode 100644 index 000000000000..bd7560ffc048 --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/fs/operations_test.cpp b/contrib/kyua/utils/fs/operations_test.cpp new file mode 100644 index 000000000000..f1349351166e --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include + +#include + +#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/contrib/kyua/utils/fs/path.cpp b/contrib/kyua/utils/fs/path.cpp new file mode 100644 index 000000000000..465ed49c4c2a --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/fs/path.hpp b/contrib/kyua/utils/fs/path.hpp new file mode 100644 index 000000000000..fe55fd55f234 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/fs/path_fwd.hpp b/contrib/kyua/utils/fs/path_fwd.hpp new file mode 100644 index 000000000000..4e6856073553 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/fs/path_test.cpp b/contrib/kyua/utils/fs/path_test.cpp new file mode 100644 index 000000000000..30ad3110de31 --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#include + +#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/contrib/kyua/utils/logging/Kyuafile b/contrib/kyua/utils/logging/Kyuafile new file mode 100644 index 000000000000..0853a335c6ae --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/logging/Makefile.am.inc b/contrib/kyua/utils/logging/Makefile.am.inc new file mode 100644 index 000000000000..7d88f16859d7 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/logging/macros.hpp b/contrib/kyua/utils/logging/macros.hpp new file mode 100644 index 000000000000..73dd0a60ef87 --- /dev/null +++ b/contrib/kyua/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 must not be included from other header files. + +#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/contrib/kyua/utils/logging/macros_test.cpp b/contrib/kyua/utils/logging/macros_test.cpp new file mode 100644 index 000000000000..fe3ee63cd533 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/logging/operations.cpp b/contrib/kyua/utils/logging/operations.cpp new file mode 100644 index 000000000000..88f25361fa18 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include +#include +#include + +#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/contrib/kyua/utils/logging/operations.hpp b/contrib/kyua/utils/logging/operations.hpp new file mode 100644 index 000000000000..1bb72219dcae --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/logging/operations_fwd.hpp b/contrib/kyua/utils/logging/operations_fwd.hpp new file mode 100644 index 000000000000..0e3edd7993ec --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/logging/operations_test.cpp b/contrib/kyua/utils/logging/operations_test.cpp new file mode 100644 index 000000000000..402f36e62904 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#include + +#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/contrib/kyua/utils/memory.cpp b/contrib/kyua/utils/memory.cpp new file mode 100644 index 000000000000..ca121f6f4dec --- /dev/null +++ b/contrib/kyua/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 +#endif +#if defined(HAVE_SYS_PARAM_H) +# include +#endif +#if defined(HAVE_SYS_SYSCTL_H) +# include +#endif +} + +#include +#include +#include +#include + +#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/contrib/kyua/utils/memory.hpp b/contrib/kyua/utils/memory.hpp new file mode 100644 index 000000000000..5a956a82005a --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/memory_test.cpp b/contrib/kyua/utils/memory_test.cpp new file mode 100644 index 000000000000..66750fbe9c6c --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/noncopyable.hpp b/contrib/kyua/utils/noncopyable.hpp new file mode 100644 index 000000000000..6a0ad6bf713a --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/optional.hpp b/contrib/kyua/utils/optional.hpp new file mode 100644 index 000000000000..a4557bff5dc8 --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/optional.ipp b/contrib/kyua/utils/optional.ipp new file mode 100644 index 000000000000..3e2f3f878f2a --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/optional_fwd.hpp b/contrib/kyua/utils/optional_fwd.hpp new file mode 100644 index 000000000000..931dbbfe88da --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/optional_test.cpp b/contrib/kyua/utils/optional_test.cpp new file mode 100644 index 000000000000..debd8949852e --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +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/contrib/kyua/utils/passwd.cpp b/contrib/kyua/utils/passwd.cpp new file mode 100644 index 000000000000..32a16bb4d462 --- /dev/null +++ b/contrib/kyua/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 + +#include +#include +} + +#include + +#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/contrib/kyua/utils/passwd.hpp b/contrib/kyua/utils/passwd.hpp new file mode 100644 index 000000000000..e0b17c547080 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/passwd_fwd.hpp b/contrib/kyua/utils/passwd_fwd.hpp new file mode 100644 index 000000000000..bedbd34c8af8 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/passwd_test.cpp b/contrib/kyua/utils/passwd_test.cpp new file mode 100644 index 000000000000..720ecb32e5fe --- /dev/null +++ b/contrib/kyua/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 + +#include +#include +} + +#include +#include + +#include + +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/contrib/kyua/utils/process/.gitignore b/contrib/kyua/utils/process/.gitignore new file mode 100644 index 000000000000..fb3291b39e0c --- /dev/null +++ b/contrib/kyua/utils/process/.gitignore @@ -0,0 +1 @@ +helpers diff --git a/contrib/kyua/utils/process/Kyuafile b/contrib/kyua/utils/process/Kyuafile new file mode 100644 index 000000000000..92e62cfac6fc --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/Makefile.am.inc b/contrib/kyua/utils/process/Makefile.am.inc new file mode 100644 index 000000000000..3cff02e7e455 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/child.cpp b/contrib/kyua/utils/process/child.cpp new file mode 100644 index 000000000000..fef09ccaad3b --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +#include +#include +} + +#include +#include +#include + +#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/contrib/kyua/utils/process/child.hpp b/contrib/kyua/utils/process/child.hpp new file mode 100644 index 000000000000..2c9450f6500a --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#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/contrib/kyua/utils/process/child.ipp b/contrib/kyua/utils/process/child.ipp new file mode 100644 index 000000000000..aa90373652fd --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/process/child_fwd.hpp b/contrib/kyua/utils/process/child_fwd.hpp new file mode 100644 index 000000000000..4d4caa17d58c --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/child_test.cpp b/contrib/kyua/utils/process/child_test.cpp new file mode 100644 index 000000000000..69de9991ae13 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include + +#include + +#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/contrib/kyua/utils/process/deadline_killer.cpp b/contrib/kyua/utils/process/deadline_killer.cpp new file mode 100644 index 000000000000..ed733e402f76 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/deadline_killer.hpp b/contrib/kyua/utils/process/deadline_killer.hpp new file mode 100644 index 000000000000..8b337a0f9d8c --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/deadline_killer_fwd.hpp b/contrib/kyua/utils/process/deadline_killer_fwd.hpp new file mode 100644 index 000000000000..fca3c5dc57c7 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/deadline_killer_test.cpp b/contrib/kyua/utils/process/deadline_killer_test.cpp new file mode 100644 index 000000000000..06c89660ac31 --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include + +#include + +#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/contrib/kyua/utils/process/exceptions.cpp b/contrib/kyua/utils/process/exceptions.cpp new file mode 100644 index 000000000000..d7590c330499 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/process/exceptions.hpp b/contrib/kyua/utils/process/exceptions.hpp new file mode 100644 index 000000000000..3bf740459864 --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/process/exceptions_test.cpp b/contrib/kyua/utils/process/exceptions_test.cpp new file mode 100644 index 000000000000..375b635fc173 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/process/executor.cpp b/contrib/kyua/utils/process/executor.cpp new file mode 100644 index 000000000000..dbdf31268f86 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +} + +#include +#include +#include +#include + +#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/contrib/kyua/utils/process/executor.hpp b/contrib/kyua/utils/process/executor.hpp new file mode 100644 index 000000000000..858ad9c815aa --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/process/executor.ipp b/contrib/kyua/utils/process/executor.ipp new file mode 100644 index 000000000000..e91f994673d7 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/executor_fwd.hpp b/contrib/kyua/utils/process/executor_fwd.hpp new file mode 100644 index 000000000000..ec63227993f3 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/executor_test.cpp b/contrib/kyua/utils/process/executor_test.cpp new file mode 100644 index 000000000000..13ae69bd44ed --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#include +#include +} + +#include +#include +#include +#include +#include + +#include + +#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/contrib/kyua/utils/process/fdstream.cpp b/contrib/kyua/utils/process/fdstream.cpp new file mode 100644 index 000000000000..ccd7a1f91b0c --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/fdstream.hpp b/contrib/kyua/utils/process/fdstream.hpp new file mode 100644 index 000000000000..e785b0ac4282 --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/process/fdstream_fwd.hpp b/contrib/kyua/utils/process/fdstream_fwd.hpp new file mode 100644 index 000000000000..8d369ea0bfa5 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/fdstream_test.cpp b/contrib/kyua/utils/process/fdstream_test.cpp new file mode 100644 index 000000000000..8420568216f0 --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#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/contrib/kyua/utils/process/helpers.cpp b/contrib/kyua/utils/process/helpers.cpp new file mode 100644 index 000000000000..15deecd95f24 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + + +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/contrib/kyua/utils/process/isolation.cpp b/contrib/kyua/utils/process/isolation.cpp new file mode 100644 index 000000000000..90dd08d5772d --- /dev/null +++ b/contrib/kyua/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 + +#include +#include +#include +} + +#include +#include +#include +#include + +#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/contrib/kyua/utils/process/isolation.hpp b/contrib/kyua/utils/process/isolation.hpp new file mode 100644 index 000000000000..69793a76c7b4 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/isolation_test.cpp b/contrib/kyua/utils/process/isolation_test.cpp new file mode 100644 index 000000000000..dc723cc65c88 --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#include +} + +#include +#include +#include +#include + +#include + +#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/contrib/kyua/utils/process/operations.cpp b/contrib/kyua/utils/process/operations.cpp new file mode 100644 index 000000000000..abcc49f2a443 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +#include +} + +#include +#include +#include +#include + +#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/contrib/kyua/utils/process/operations.hpp b/contrib/kyua/utils/process/operations.hpp new file mode 100644 index 000000000000..773f9d38bb74 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/operations_fwd.hpp b/contrib/kyua/utils/process/operations_fwd.hpp new file mode 100644 index 000000000000..bd23fdc2c691 --- /dev/null +++ b/contrib/kyua/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 +#include + +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/contrib/kyua/utils/process/operations_test.cpp b/contrib/kyua/utils/process/operations_test.cpp new file mode 100644 index 000000000000..e9c1ebb65a3d --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +#include +} + +#include +#include + +#include + +#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/contrib/kyua/utils/process/status.cpp b/contrib/kyua/utils/process/status.cpp new file mode 100644 index 000000000000..a3cea8e09ebd --- /dev/null +++ b/contrib/kyua/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 +} + +#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/contrib/kyua/utils/process/status.hpp b/contrib/kyua/utils/process/status.hpp new file mode 100644 index 000000000000..b14ff55c01a2 --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/process/status_fwd.hpp b/contrib/kyua/utils/process/status_fwd.hpp new file mode 100644 index 000000000000..3a14683dc15c --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/status_test.cpp b/contrib/kyua/utils/process/status_test.cpp new file mode 100644 index 000000000000..5a3e19eeaf18 --- /dev/null +++ b/contrib/kyua/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 + +#include +#include +} + +#include + +#include + +#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/contrib/kyua/utils/process/system.cpp b/contrib/kyua/utils/process/system.cpp new file mode 100644 index 000000000000..ac41ddb7daa7 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +#include +} + +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/contrib/kyua/utils/process/system.hpp b/contrib/kyua/utils/process/system.hpp new file mode 100644 index 000000000000..a794876f3579 --- /dev/null +++ b/contrib/kyua/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 +} + +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/contrib/kyua/utils/process/systembuf.cpp b/contrib/kyua/utils/process/systembuf.cpp new file mode 100644 index 000000000000..661b336221ac --- /dev/null +++ b/contrib/kyua/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 +} + +#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/contrib/kyua/utils/process/systembuf.hpp b/contrib/kyua/utils/process/systembuf.hpp new file mode 100644 index 000000000000..c89c9108dc4b --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#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/contrib/kyua/utils/process/systembuf_fwd.hpp b/contrib/kyua/utils/process/systembuf_fwd.hpp new file mode 100644 index 000000000000..b3e341336b1d --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/process/systembuf_test.cpp b/contrib/kyua/utils/process/systembuf_test.cpp new file mode 100644 index 000000000000..ef9ff1930cf6 --- /dev/null +++ b/contrib/kyua/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 + +#include +#include +} + +#include + +#include + +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/contrib/kyua/utils/sanity.cpp b/contrib/kyua/utils/sanity.cpp new file mode 100644 index 000000000000..7978167d83ff --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include +#include +#include +#include + +#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/contrib/kyua/utils/sanity.hpp b/contrib/kyua/utils/sanity.hpp new file mode 100644 index 000000000000..6b126f984999 --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/sanity_fwd.hpp b/contrib/kyua/utils/sanity_fwd.hpp new file mode 100644 index 000000000000..98a897c0ff39 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sanity_test.cpp b/contrib/kyua/utils/sanity_test.cpp new file mode 100644 index 000000000000..54844fb75d64 --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include +#include + +#include + +#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/contrib/kyua/utils/signals/Kyuafile b/contrib/kyua/utils/signals/Kyuafile new file mode 100644 index 000000000000..09da3e166cd2 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/signals/Makefile.am.inc b/contrib/kyua/utils/signals/Makefile.am.inc new file mode 100644 index 000000000000..b01089c80fea --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/signals/exceptions.cpp b/contrib/kyua/utils/signals/exceptions.cpp new file mode 100644 index 000000000000..70e0dbe8a5d1 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/signals/exceptions.hpp b/contrib/kyua/utils/signals/exceptions.hpp new file mode 100644 index 000000000000..35cd2c9e8168 --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/signals/exceptions_test.cpp b/contrib/kyua/utils/signals/exceptions_test.cpp new file mode 100644 index 000000000000..40db536f1a8c --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/signals/interrupts.cpp b/contrib/kyua/utils/signals/interrupts.cpp new file mode 100644 index 000000000000..956a83c66802 --- /dev/null +++ b/contrib/kyua/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 + +#include +#include +} + +#include +#include +#include + +#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/contrib/kyua/utils/signals/interrupts.hpp b/contrib/kyua/utils/signals/interrupts.hpp new file mode 100644 index 000000000000..b181114bb245 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/signals/interrupts_fwd.hpp b/contrib/kyua/utils/signals/interrupts_fwd.hpp new file mode 100644 index 000000000000..e4dfe68d54e2 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/signals/interrupts_test.cpp b/contrib/kyua/utils/signals/interrupts_test.cpp new file mode 100644 index 000000000000..ef8758d8d5f1 --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include +#include + +#include + +#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/contrib/kyua/utils/signals/misc.cpp b/contrib/kyua/utils/signals/misc.cpp new file mode 100644 index 000000000000..b9eb1c402a28 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#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/contrib/kyua/utils/signals/misc.hpp b/contrib/kyua/utils/signals/misc.hpp new file mode 100644 index 000000000000..ad3763feabc4 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/signals/misc_test.cpp b/contrib/kyua/utils/signals/misc_test.cpp new file mode 100644 index 000000000000..76f36b0e5082 --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include + +#include + +#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/contrib/kyua/utils/signals/programmer.cpp b/contrib/kyua/utils/signals/programmer.cpp new file mode 100644 index 000000000000..c47d1cf85038 --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#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/contrib/kyua/utils/signals/programmer.hpp b/contrib/kyua/utils/signals/programmer.hpp new file mode 100644 index 000000000000..5ac5318f0bb9 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/signals/programmer_fwd.hpp b/contrib/kyua/utils/signals/programmer_fwd.hpp new file mode 100644 index 000000000000..55dfd34af2eb --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/signals/programmer_test.cpp b/contrib/kyua/utils/signals/programmer_test.cpp new file mode 100644 index 000000000000..0e95f84974b1 --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include + +#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/contrib/kyua/utils/signals/timer.cpp b/contrib/kyua/utils/signals/timer.cpp new file mode 100644 index 000000000000..698b9835dc10 --- /dev/null +++ b/contrib/kyua/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 + +#include +} + +#include +#include +#include +#include + +#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/contrib/kyua/utils/signals/timer.hpp b/contrib/kyua/utils/signals/timer.hpp new file mode 100644 index 000000000000..1174effe2b48 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/signals/timer_fwd.hpp b/contrib/kyua/utils/signals/timer_fwd.hpp new file mode 100644 index 000000000000..a3cf3e205d70 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/signals/timer_test.cpp b/contrib/kyua/utils/signals/timer_test.cpp new file mode 100644 index 000000000000..61e9cac6b088 --- /dev/null +++ b/contrib/kyua/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 +#include +} + +#include +#include +#include + +#include + +#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/contrib/kyua/utils/sqlite/Kyuafile b/contrib/kyua/utils/sqlite/Kyuafile new file mode 100644 index 000000000000..47a8b95dac92 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/Makefile.am.inc b/contrib/kyua/utils/sqlite/Makefile.am.inc new file mode 100644 index 000000000000..6064a641c14f --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/c_gate.cpp b/contrib/kyua/utils/sqlite/c_gate.cpp new file mode 100644 index 000000000000..e89ac5332ea0 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/c_gate.hpp b/contrib/kyua/utils/sqlite/c_gate.hpp new file mode 100644 index 000000000000..0ca9d79c4815 --- /dev/null +++ b/contrib/kyua/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 +} + +#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/contrib/kyua/utils/sqlite/c_gate_fwd.hpp b/contrib/kyua/utils/sqlite/c_gate_fwd.hpp new file mode 100644 index 000000000000..771efeeff463 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/c_gate_test.cpp b/contrib/kyua/utils/sqlite/c_gate_test.cpp new file mode 100644 index 000000000000..edf46f76c902 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/sqlite/database.cpp b/contrib/kyua/utils/sqlite/database.cpp new file mode 100644 index 000000000000..41935c3b017d --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#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/contrib/kyua/utils/sqlite/database.hpp b/contrib/kyua/utils/sqlite/database.hpp new file mode 100644 index 000000000000..ca91a6a360c6 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#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/contrib/kyua/utils/sqlite/database_fwd.hpp b/contrib/kyua/utils/sqlite/database_fwd.hpp new file mode 100644 index 000000000000..209342f159d6 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/database_test.cpp b/contrib/kyua/utils/sqlite/database_test.cpp new file mode 100644 index 000000000000..70f057b9b793 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/sqlite/exceptions.cpp b/contrib/kyua/utils/sqlite/exceptions.cpp new file mode 100644 index 000000000000..cc2d42cab16c --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#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/contrib/kyua/utils/sqlite/exceptions.hpp b/contrib/kyua/utils/sqlite/exceptions.hpp new file mode 100644 index 000000000000..a9450fce5c33 --- /dev/null +++ b/contrib/kyua/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 +#include + +#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/contrib/kyua/utils/sqlite/exceptions_test.cpp b/contrib/kyua/utils/sqlite/exceptions_test.cpp new file mode 100644 index 000000000000..d9e81038cc2f --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#include + +#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/contrib/kyua/utils/sqlite/statement.cpp b/contrib/kyua/utils/sqlite/statement.cpp new file mode 100644 index 000000000000..0ae2af2d57ca --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#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/contrib/kyua/utils/sqlite/statement.hpp b/contrib/kyua/utils/sqlite/statement.hpp new file mode 100644 index 000000000000..bcd1831e4841 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#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/contrib/kyua/utils/sqlite/statement.ipp b/contrib/kyua/utils/sqlite/statement.ipp new file mode 100644 index 000000000000..3f219016a2a9 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/statement_fwd.hpp b/contrib/kyua/utils/sqlite/statement_fwd.hpp new file mode 100644 index 000000000000..26634c965018 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/statement_test.cpp b/contrib/kyua/utils/sqlite/statement_test.cpp new file mode 100644 index 000000000000..40bc92cb5c0e --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#include + +#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/contrib/kyua/utils/sqlite/test_utils.hpp b/contrib/kyua/utils/sqlite/test_utils.hpp new file mode 100644 index 000000000000..bf35d209a164 --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/sqlite/transaction.cpp b/contrib/kyua/utils/sqlite/transaction.cpp new file mode 100644 index 000000000000..e0235fef9c57 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/transaction.hpp b/contrib/kyua/utils/sqlite/transaction.hpp new file mode 100644 index 000000000000..71f3b0c93f4a --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/sqlite/transaction_fwd.hpp b/contrib/kyua/utils/sqlite/transaction_fwd.hpp new file mode 100644 index 000000000000..7773d8380458 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/sqlite/transaction_test.cpp b/contrib/kyua/utils/sqlite/transaction_test.cpp new file mode 100644 index 000000000000..d53e6fba4378 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/stacktrace.cpp b/contrib/kyua/utils/stacktrace.cpp new file mode 100644 index 000000000000..11636b31959f --- /dev/null +++ b/contrib/kyua/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 +#include + +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include + +#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/contrib/kyua/utils/stacktrace.hpp b/contrib/kyua/utils/stacktrace.hpp new file mode 100644 index 000000000000..13648e2c71cb --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/stacktrace_helper.cpp b/contrib/kyua/utils/stacktrace_helper.cpp new file mode 100644 index 000000000000..f01e8c809797 --- /dev/null +++ b/contrib/kyua/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 + + +int +main(void) +{ + std::abort(); +} diff --git a/contrib/kyua/utils/stacktrace_test.cpp b/contrib/kyua/utils/stacktrace_test.cpp new file mode 100644 index 000000000000..ca87e7087f5a --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +#include +#include +} + +#include +#include + +#include + +#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/contrib/kyua/utils/stream.cpp b/contrib/kyua/utils/stream.cpp new file mode 100644 index 000000000000..ee3ab417f753 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#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/contrib/kyua/utils/stream.hpp b/contrib/kyua/utils/stream.hpp new file mode 100644 index 000000000000..5c9316e72810 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include +#include + +#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/contrib/kyua/utils/stream_test.cpp b/contrib/kyua/utils/stream_test.cpp new file mode 100644 index 000000000000..7c4f3b5c6b4a --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/test_utils.ipp b/contrib/kyua/utils/test_utils.ipp new file mode 100644 index 000000000000..f21d0f4cc172 --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include + +#include + +#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/contrib/kyua/utils/text/Kyuafile b/contrib/kyua/utils/text/Kyuafile new file mode 100644 index 000000000000..e4e870e9c648 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/text/Makefile.am.inc b/contrib/kyua/utils/text/Makefile.am.inc new file mode 100644 index 000000000000..d474ae191bf5 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/text/exceptions.cpp b/contrib/kyua/utils/text/exceptions.cpp new file mode 100644 index 000000000000..1692cfea7edb --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/text/exceptions.hpp b/contrib/kyua/utils/text/exceptions.hpp new file mode 100644 index 000000000000..da0cfd98fb88 --- /dev/null +++ b/contrib/kyua/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 + +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/contrib/kyua/utils/text/exceptions_test.cpp b/contrib/kyua/utils/text/exceptions_test.cpp new file mode 100644 index 000000000000..1d3c3910900a --- /dev/null +++ b/contrib/kyua/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 + +#include + +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/contrib/kyua/utils/text/operations.cpp b/contrib/kyua/utils/text/operations.cpp new file mode 100644 index 000000000000..5a4345d979c7 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/text/operations.hpp b/contrib/kyua/utils/text/operations.hpp new file mode 100644 index 000000000000..6d15be553b06 --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +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/contrib/kyua/utils/text/operations.ipp b/contrib/kyua/utils/text/operations.ipp new file mode 100644 index 000000000000..511cd6840a08 --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/text/operations_test.cpp b/contrib/kyua/utils/text/operations_test.cpp new file mode 100644 index 000000000000..2d5ab36c9090 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#include + +#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& 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/contrib/kyua/utils/text/regex.cpp b/contrib/kyua/utils/text/regex.cpp new file mode 100644 index 000000000000..b078ba88f6b4 --- /dev/null +++ b/contrib/kyua/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 + +#include +} + +#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/contrib/kyua/utils/text/regex.hpp b/contrib/kyua/utils/text/regex.hpp new file mode 100644 index 000000000000..b3d20c246735 --- /dev/null +++ b/contrib/kyua/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 +#include + + +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/contrib/kyua/utils/text/regex_fwd.hpp b/contrib/kyua/utils/text/regex_fwd.hpp new file mode 100644 index 000000000000..e9010324c10d --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/text/regex_test.cpp b/contrib/kyua/utils/text/regex_test.cpp new file mode 100644 index 000000000000..7ea5ee485aad --- /dev/null +++ b/contrib/kyua/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 + +#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/contrib/kyua/utils/text/table.cpp b/contrib/kyua/utils/text/table.cpp new file mode 100644 index 000000000000..4a2c72f8053f --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#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/contrib/kyua/utils/text/table.hpp b/contrib/kyua/utils/text/table.hpp new file mode 100644 index 000000000000..5fd7c50c991c --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +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/contrib/kyua/utils/text/table_fwd.hpp b/contrib/kyua/utils/text/table_fwd.hpp new file mode 100644 index 000000000000..77c6b1fa8c78 --- /dev/null +++ b/contrib/kyua/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 +#include +#include + +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/contrib/kyua/utils/text/table_test.cpp b/contrib/kyua/utils/text/table_test.cpp new file mode 100644 index 000000000000..45928dae89c4 --- /dev/null +++ b/contrib/kyua/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 + +#include + +#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/contrib/kyua/utils/text/templates.cpp b/contrib/kyua/utils/text/templates.cpp new file mode 100644 index 000000000000..13cb27b1cce2 --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include + +#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/contrib/kyua/utils/text/templates.hpp b/contrib/kyua/utils/text/templates.hpp new file mode 100644 index 000000000000..ffbf28512d0d --- /dev/null +++ b/contrib/kyua/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 +#include +#include +#include +#include + +#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/contrib/kyua/utils/text/templates_fwd.hpp b/contrib/kyua/utils/text/templates_fwd.hpp new file mode 100644 index 000000000000..c806be0cf497 --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/text/templates_test.cpp b/contrib/kyua/utils/text/templates_test.cpp new file mode 100644 index 000000000000..4524dc61a416 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +#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/contrib/kyua/utils/units.cpp b/contrib/kyua/utils/units.cpp new file mode 100644 index 000000000000..bfb488fa2ed6 --- /dev/null +++ b/contrib/kyua/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 +} + +#include + +#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/contrib/kyua/utils/units.hpp b/contrib/kyua/utils/units.hpp new file mode 100644 index 000000000000..281788c3199f --- /dev/null +++ b/contrib/kyua/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 +} + +#include +#include +#include + +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/contrib/kyua/utils/units_fwd.hpp b/contrib/kyua/utils/units_fwd.hpp new file mode 100644 index 000000000000..3653d9727a2d --- /dev/null +++ b/contrib/kyua/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/contrib/kyua/utils/units_test.cpp b/contrib/kyua/utils/units_test.cpp new file mode 100644 index 000000000000..601265c95b49 --- /dev/null +++ b/contrib/kyua/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 +#include + +#include + +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); +} diff --git a/etc/mtree/BSD.root.dist b/etc/mtree/BSD.root.dist index a688d5806b3b..a8ff6e8e85d5 100644 --- a/etc/mtree/BSD.root.dist +++ b/etc/mtree/BSD.root.dist @@ -54,6 +54,8 @@ .. gss .. + kyua + .. mail .. mtree diff --git a/etc/mtree/BSD.usr.dist b/etc/mtree/BSD.usr.dist index 0a6c020f7ee2..669a70dded3c 100644 --- a/etc/mtree/BSD.usr.dist +++ b/etc/mtree/BSD.usr.dist @@ -313,6 +313,8 @@ .. .. .. + kyua + .. libusb20 .. libvgl @@ -450,6 +452,12 @@ .. .. .. + kyua + misc + .. + store + .. + .. locale af_ZA.ISO8859-1 .. diff --git a/lib/Makefile b/lib/Makefile index 389186b34f5b..4d72fab858ad 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -205,6 +205,7 @@ SUBDIR.${MK_RADIUS_SUPPORT}+= libradius SUBDIR.${MK_SENDMAIL}+= libmilter libsm libsmdb libsmutil SUBDIR.${MK_TELNET}+= libtelnet SUBDIR.${MK_TESTS_SUPPORT}+= atf +SUBDIR.${MK_TESTS_SUPPORT}.${MK_CXX}+= kyua SUBDIR.${MK_TESTS_SUPPORT}.${MK_CXX}+= liblutok SUBDIR.${MK_TESTS}+= tests SUBDIR.${MK_UNBOUND}+= libunbound diff --git a/lib/kyua/Makefile b/lib/kyua/Makefile new file mode 100644 index 000000000000..1a1ea8bf5828 --- /dev/null +++ b/lib/kyua/Makefile @@ -0,0 +1,10 @@ +# $FreeBSD$ + +SUBDIR= cli \ + drivers \ + engine \ + model \ + store \ + utils + +.include diff --git a/lib/kyua/Makefile.inc b/lib/kyua/Makefile.inc new file mode 100644 index 000000000000..52f19d3140b3 --- /dev/null +++ b/lib/kyua/Makefile.inc @@ -0,0 +1,20 @@ +# $FreeBSD$ + +KYUA_LIBDIR:= ${.PARSEDIR} + +.include "${KYUA_LIBDIR}/Makefile.kyua" + +LIB_CXX= kyua_${KYUA_LIB} +INTERNALLIB= + +.PATH: ${KYUA_SRCDIR}/${KYUA_LIB} +CFLAGS+= -I${KYUA_LIBDIR} \ + -I${KYUA_SRCDIR} \ + -I${SRCTOP}/contrib/lutok/include + +CFLAGS+= -DHAVE_CONFIG_H + +# kyua uses auto_ptr +CFLAGS+= -Wno-deprecated-declarations + +.include "${.PARSEDIR}/../Makefile.inc" diff --git a/lib/kyua/Makefile.kyua b/lib/kyua/Makefile.kyua new file mode 100644 index 000000000000..0ed1bd855aa3 --- /dev/null +++ b/lib/kyua/Makefile.kyua @@ -0,0 +1,11 @@ +# $FreeBSD$ + +KYUA_VERSION= 0.13 + +KYUA_CONFDIR= /etc/kyua +KYUA_DOCDIR= /nonexistant +KYUA_EGDIR= /usr/share/examples/kyua +KYUA_MISCDIR= /usr/share/kyua/misc +KYUA_STOREDIR= /usr/share/kyua/store + +KYUA_SRCDIR= ${SRCTOP}/contrib/kyua diff --git a/lib/kyua/cli/Makefile b/lib/kyua/cli/Makefile new file mode 100644 index 000000000000..d599927934e5 --- /dev/null +++ b/lib/kyua/cli/Makefile @@ -0,0 +1,28 @@ +# $FreeBSD$ + +KYUA_LIB= cli +INTERNALLIB= +CFLAGS+= -DKYUA_CONFDIR=\"${KYUA_CONFDIR}\" \ + -DKYUA_DOCDIR=\"${KYUA_CONFDIR}\" \ + -DKYUA_MISCDIR=\"${KYUA_MISCDIR}\" \ + -DPACKAGE=\"kyua\" \ + -DPACKAGE_NAME=\"Kyua\" \ + -DPACKAGE_VERSION=\"${KYUA_VERSION}\" \ + -DVERSION=\"${KYUA_VERSION}\" + +SRCS= cmd_about.cpp \ + cmd_config.cpp \ + cmd_db_exec.cpp \ + cmd_db_migrate.cpp \ + cmd_debug.cpp \ + cmd_help.cpp \ + cmd_list.cpp \ + cmd_report.cpp \ + cmd_report_html.cpp \ + cmd_report_junit.cpp \ + cmd_test.cpp \ + common.cpp \ + config.cpp \ + main.cpp + +.include diff --git a/lib/kyua/config.h b/lib/kyua/config.h new file mode 100644 index 000000000000..132972229364 --- /dev/null +++ b/lib/kyua/config.h @@ -0,0 +1,122 @@ +/* config.h. Generated from config.h.in by configure. */ +/* config.h.in. Generated from configure.ac by autoheader. */ + +/* Define to the optind value to reset getopt processing */ +#define GETOPT_OPTIND_RESET_VALUE 1 + +/* Define to 1 if getcwd(NULL, 0) works */ +#define HAVE_GETCWD_DYN 1 + +/* Define to 1 if getopt allows a + sign for POSIX behavior */ +/* #undef HAVE_GETOPT_GNU */ + +/* Define to 1 if getopt has optreset */ +#define HAVE_GETOPT_WITH_OPTRESET 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_INTTYPES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_MEMORY_H 1 + +/* Define to 1 if you have the `putenv' function. */ +#define HAVE_PUTENV 1 + +/* Define to 1 if you have the `setenv' function. */ +#define HAVE_SETENV 1 + +/* Define to 1 if you have the `statfs' function. */ +#define HAVE_STATFS 1 + +/* Define to 1 if you have the `statvfs' function. */ +#define HAVE_STATVFS 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRINGS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the `sysctlbyname' function. */ +#define HAVE_SYSCTLBYNAME 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_MOUNT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_PARAM_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STATVFS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STAT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_SYSCTL_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_VFS_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_TERMIOS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UNISTD_H 1 + +/* Define to 1 if you have the `unmount' function. */ +#define HAVE_UNMOUNT 1 + +/* Define to 1 if you have the `unsetenv' function. */ +#define HAVE_UNSETENV 1 + +/* Define to 1 if your lchmod works */ +#define HAVE_WORKING_LCHMOD 1 + +/* Define to the last valid signal number */ +#define LAST_SIGNO 128 + +/* Define to the name of the sysctl MIB */ +#define MEMORY_QUERY_SYSCTL_MIB "hw.usermem" + +/* Define to the memory query type */ +#define MEMORY_QUERY_TYPE "sysctlbyname" + +/* Name of package */ +#define PACKAGE "kyua" + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "kyua-discuss@googlegroups.com" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "Kyua" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "Kyua 0.13" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "kyua" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "https://github.com/jmmv/kyua/" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "0.13" + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* Set to the path of umount(8) */ +/* #undef UMOUNT */ + +/* Version number of package */ +#define VERSION "0.13" diff --git a/lib/kyua/drivers/Makefile b/lib/kyua/drivers/Makefile new file mode 100644 index 000000000000..67d2ea2e0156 --- /dev/null +++ b/lib/kyua/drivers/Makefile @@ -0,0 +1,11 @@ +# $FreeBSD$ + +KYUA_LIB= drivers + +SRCS= debug_test.cpp \ + list_tests.cpp \ + report_junit.cpp \ + run_tests.cpp \ + scan_results.cpp + +.include diff --git a/lib/kyua/engine/Makefile b/lib/kyua/engine/Makefile new file mode 100644 index 000000000000..fdacd5ae6703 --- /dev/null +++ b/lib/kyua/engine/Makefile @@ -0,0 +1,21 @@ +# $FreeBSD$ + +KYUA_LIB= engine +CFLAGS= -DKYUA_ARCHITECTURE=\"${MACHINE_ARCH}\" \ + -DKYUA_PLATFORM=\"${MACHINE}\" + +SRCS= atf.cpp \ + atf_list.cpp \ + atf_result.cpp \ + config.cpp \ + exceptions.cpp \ + filters.cpp \ + kyuafile.cpp \ + plain.cpp \ + requirements.cpp \ + scanner.cpp \ + tap.cpp \ + tap_parser.cpp \ + scheduler.cpp + +.include diff --git a/lib/kyua/model/Makefile b/lib/kyua/model/Makefile new file mode 100644 index 000000000000..1b3efd7cfc8f --- /dev/null +++ b/lib/kyua/model/Makefile @@ -0,0 +1,12 @@ +# $FreeBSD$ + +KYUA_LIB= model + +SRCS= context.cpp \ + exceptions.cpp \ + metadata.cpp \ + test_case.cpp \ + test_program.cpp \ + test_result.cpp + +.include diff --git a/lib/kyua/store/Makefile b/lib/kyua/store/Makefile new file mode 100644 index 000000000000..a96066cc26fd --- /dev/null +++ b/lib/kyua/store/Makefile @@ -0,0 +1,17 @@ +# $FreeBSD$ + +KYUA_LIB= store + +CFLAGS+= -DKYUA_STOREDIR=\"${KYUA_STOREDIR}\" + +SRCS= dbtypes.cpp \ + exceptions.cpp \ + layout.cpp \ + metadata.cpp \ + migrate.cpp \ + read_backend.cpp \ + read_transaction.cpp \ + write_backend.cpp \ + write_transaction.cpp + +.include diff --git a/lib/kyua/utils/Makefile b/lib/kyua/utils/Makefile new file mode 100644 index 000000000000..68388ca84c09 --- /dev/null +++ b/lib/kyua/utils/Makefile @@ -0,0 +1,64 @@ +# $FreeBSD$ + +KYUA_LIB= utils + +CFLAGS+= -I${SRCTOP}/contrib/sqlite3 +CFLAGS+= -DGDB=\"/usr/local/bin/gdb\" + +SRCS= datetime.cpp \ + env.cpp \ + memory.cpp \ + passwd.cpp \ + sanity.cpp \ + stacktrace.cpp \ + stream.cpp \ + units.cpp \ + cmdline/base_command.cpp \ + cmdline/exceptions.cpp \ + cmdline/globals.cpp \ + cmdline/options.cpp \ + cmdline/parser.cpp \ + cmdline/ui.cpp \ + cmdline/ui_mock.cpp \ + config/exceptions.cpp \ + config/keys.cpp \ + config/lua_module.cpp \ + config/nodes.cpp \ + config/parser.cpp \ + config/tree.cpp \ + format/exceptions.cpp \ + format/formatter.cpp \ + fs/auto_cleaners.cpp \ + fs/directory.cpp \ + fs/exceptions.cpp \ + fs/lua_module.cpp \ + fs/operations.cpp \ + fs/path.cpp \ + logging/operations.cpp \ + process/child.cpp \ + process/deadline_killer.cpp \ + process/exceptions.cpp \ + process/executor.cpp \ + process/fdstream.cpp \ + process/isolation.cpp \ + process/operations.cpp \ + process/status.cpp \ + process/system.cpp \ + process/systembuf.cpp \ + signals/exceptions.cpp \ + signals/interrupts.cpp \ + signals/misc.cpp \ + signals/programmer.cpp \ + signals/timer.cpp \ + sqlite/c_gate.cpp \ + sqlite/database.cpp \ + sqlite/exceptions.cpp \ + sqlite/statement.cpp \ + sqlite/transaction.cpp \ + text/exceptions.cpp \ + text/operations.cpp \ + text/regex.cpp \ + text/table.cpp \ + text/templates.cpp \ + +.include diff --git a/lib/kyua/utils/defs.hpp b/lib/kyua/utils/defs.hpp new file mode 100644 index 000000000000..32d1f9bb2344 --- /dev/null +++ b/lib/kyua/utils/defs.hpp @@ -0,0 +1,70 @@ +// 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)) + + +/// Macro to mark a parameter as unused. +/// +/// This macro has to be called on the name of a parameter during the +/// definition (not declaration) of a function. When doing so, it declares +/// the parameter as unused to silence compiler warnings and also renames +/// the parameter by prefixing "unused_" to it. This is to ensure that the +/// developer remembers to remove the call to this macro from the parameter +/// when he actually starts using it. +/// +/// \param name The name of the function parameter to mark as unused. +#define UTILS_UNUSED_PARAM(name) unused_ ## name UTILS_UNUSED + + +#endif // !defined(UTILS_DEFS_HPP) diff --git a/share/mk/src.libnames.mk b/share/mk/src.libnames.mk index 942a7b4798fd..cf833f9b5cee 100644 --- a/share/mk/src.libnames.mk +++ b/share/mk/src.libnames.mk @@ -40,6 +40,12 @@ _INTERNALLIBS= \ fifolog \ ifconfig \ ipf \ + kyua_cli \ + kyua_drivers \ + kyua_engine \ + kyua_model \ + kyua_store \ + kyua_utils \ lpr \ lua \ lutok \ @@ -257,6 +263,8 @@ _DP_bsnmp= crypto _DP_geom= bsdxml sbuf _DP_cam= sbuf _DP_kvm= elf +_DP_kyua_cli= lutok kyua_utils +_DP_kyua_store= sqlite3 _DP_casper= nv _DP_cap_dns= nv _DP_cap_fileargs= nv @@ -472,6 +480,24 @@ _LIB_OBJTOP?= ${OBJTOP} LIBELFTCDIR= ${_LIB_OBJTOP}/lib/libelftc LIBELFTC?= ${LIBELFTCDIR}/libelftc${PIE_SUFFIX}.a +LIBKYUA_CLIDIR= ${_LIB_OBJTOP}/lib/kyua/cli +LIBKYUA_CLI?= ${LIBKYUA_CLIDIR}/libkyua_cli${PIE_SUFFIX}.a + +LIBKYUA_DRIVERSDIR= ${_LIB_OBJTOP}/lib/kyua/drivers +LIBKYUA_DRIVERS?= ${LIBKYUA_DRIVERSDIR}/libkyua_drivers${PIE_SUFFIX}.a + +LIBKYUA_ENGINEDIR= ${_LIB_OBJTOP}/lib/kyua/engine +LIBKYUA_ENGINE?= ${LIBKYUA_ENGINEDIR}/libkyua_engine${PIE_SUFFIX}.a + +LIBKYUA_MODELDIR= ${_LIB_OBJTOP}/lib/kyua/model +LIBKYUA_MODEL?= ${LIBKYUA_MODELDIR}/libkyua_model${PIE_SUFFIX}.a + +LIBKYUA_STOREDIR= ${_LIB_OBJTOP}/lib/kyua/store +LIBKYUA_STORE?= ${LIBKYUA_STOREDIR}/libkyua_store${PIE_SUFFIX}.a + +LIBKYUA_UTILSDIR= ${_LIB_OBJTOP}/lib/kyua/utils +LIBKYUA_UTILS?= ${LIBKYUA_UTILSDIR}/libkyua_utils${PIE_SUFFIX}.a + LIBLUADIR= ${_LIB_OBJTOP}/lib/liblua LIBLUA?= ${LIBLUADIR}/liblua${PIE_SUFFIX}.a diff --git a/tools/build/mk/OptionalObsoleteFiles.inc b/tools/build/mk/OptionalObsoleteFiles.inc index f4fd56b06adb..6ed0b04df404 100644 --- a/tools/build/mk/OptionalObsoleteFiles.inc +++ b/tools/build/mk/OptionalObsoleteFiles.inc @@ -8865,6 +8865,7 @@ OLD_FILES+=${TESTS_FILES} .endif # Test suite. .if ${MK_TESTS_SUPPORT} == no +OLD_FILES+=usr/bin/kyua OLD_FILES+=usr/include/atf-c++.hpp OLD_FILES+=usr/include/atf-c++/build.hpp OLD_FILES+=usr/include/atf-c++/check.hpp @@ -8881,12 +8882,36 @@ OLD_FILES+=usr/include/atf-c/macros.h OLD_FILES+=usr/include/atf-c/tc.h OLD_FILES+=usr/include/atf-c/tp.h OLD_FILES+=usr/include/atf-c/utils.h +OLD_FILES+=usr/lib/debug/usr/bin/kyua.debug OLD_LIBS+=usr/lib/private/libatf-c++.so.2 OLD_LIBS+=usr/lib/private/libatf-c.so.1 +OLD_FILES+=usr/share/examples/kyua/kyua.conf +OLD_FILES+=usr/share/examples/kyua/Kyuafile.top +OLD_FILES+=usr/share/kyua/misc/context.html +OLD_FILES+=usr/share/kyua/misc/index.html +OLD_FILES+=usr/share/kyua/misc/report.css +OLD_FILES+=usr/share/kyua/misc/test_result.html +OLD_FILES+=usr/share/kyua/store/migrate_v1_v2.sql +OLD_FILES+=usr/share/kyua/store/migrate_v2_v3.sql +OLD_FILES+=usr/share/kyua/store/schema_v3.sql +OLD_FILES+=usr/share/man/man1/kyua-about.1.gz +OLD_FILES+=usr/share/man/man1/kyua-config.1.gz +OLD_FILES+=usr/share/man/man1/kyua-db-exec.1.gz +OLD_FILES+=usr/share/man/man1/kyua-db-migrate.1.gz +OLD_FILES+=usr/share/man/man1/kyua-debug.1.gz +OLD_FILES+=usr/share/man/man1/kyua-help.1.gz +OLD_FILES+=usr/share/man/man1/kyua-list.1.gz +OLD_FILES+=usr/share/man/man1/kyua-report-html.1.gz +OLD_FILES+=usr/share/man/man1/kyua-report-junit.1.gz +OLD_FILES+=usr/share/man/man1/kyua-report.1.gz +OLD_FILES+=usr/share/man/man1/kyua-test.1.gz +OLD_FILES+=usr/share/man/man1/kyua.1.gz OLD_FILES+=usr/share/man/man3/atf-c++.3.gz OLD_FILES+=usr/share/man/man3/atf-c-api++.3.gz OLD_FILES+=usr/share/man/man3/atf-c-api.3.gz OLD_FILES+=usr/share/man/man3/atf-c.3.gz +OLD_FILES+=usr/share/man/man5/kyua.conf.5.gz +OLD_FILES+=usr/share/man/man5/kyuafile.5.gz OLD_FILES+=usr/tests/lib/atf/Kyuafile OLD_FILES+=usr/tests/lib/atf/libatf-c++/Kyuafile OLD_FILES+=usr/tests/lib/atf/libatf-c++/atf_c++_test diff --git a/usr.bin/Makefile b/usr.bin/Makefile index cb166e99457e..de7c56d52b03 100644 --- a/usr.bin/Makefile +++ b/usr.bin/Makefile @@ -246,6 +246,7 @@ SUBDIR.${MK_QUOTAS}+= quota SUBDIR.${MK_SENDMAIL}+= vacation SUBDIR.${MK_TALK}+= talk SUBDIR.${MK_TELNET}+= telnet +SUBDIR.${MK_TESTS_SUPPORT}.${MK_CXX}+= kyua SUBDIR.${MK_TESTS}+= tests SUBDIR.${MK_TEXTPROC}+= ul SUBDIR.${MK_TFTP}+= tftp diff --git a/usr.bin/kyua/Makefile b/usr.bin/kyua/Makefile new file mode 100644 index 000000000000..0c4564041cb0 --- /dev/null +++ b/usr.bin/kyua/Makefile @@ -0,0 +1,64 @@ +# $FreeBSD$ + +.include "${SRCTOP}/lib/kyua/Makefile.kyua" + +.PATH: ${KYUA_SRCDIR} + +PROG_CXX= kyua +SRCS= main.cpp +LIBADD= kyua_cli kyua_drivers kyua_engine kyua_model kyua_store + +MAN= kyua-about.1 \ + kyua-config.1 \ + kyua-db-exec.1 \ + kyua-db-migrate.1 \ + kyua-debug.1 \ + kyua-help.1 \ + kyua-list.1 \ + kyua-report-html.1 \ + kyua-report-junit.1 \ + kyua-report.1 \ + kyua-test.1 \ + kyua.1 \ + kyua.conf.5 \ + kyuafile.5 + +CFLAGS+= -I${KYUA_SRCDIR} + +# kyua uses auto_ptr +CFLAGS+= -Wno-deprecated-declarations + +FILESGROUPS= EXAMPLES MISC STORE + +CONFS= kyua.conf +CONFSDIR= ${KYUA_CONFDIR} + +EXAMPLES= Kyuafile.top kyua.conf +EXAMPLESDIR= ${KYUA_EGDIR} +.PATH: ${KYUA_SRCDIR}/examples + +MISC= context.html index.html report.css test_result.html +MISCDIR= ${KYUA_MISCDIR} +.PATH: ${KYUA_SRCDIR}/misc + +STORE= migrate_v1_v2.sql migrate_v2_v3.sql schema_v3.sql +STOREDIR= ${KYUA_STOREDIR} +.PATH: ${KYUA_SRCDIR}/store + +CLEANFILES+= ${MAN} +.PATH: ${KYUA_SRCDIR}/doc +.for man in ${MAN} +${man}: ${man}.in + ${SH} ${KYUA_SRCDIR}/doc/manbuild.sh \ + -v "CONFDIR=${KYUA_CONFDIR}" \ + -v "DOCDIR=${KYUA_DOCDIR}" \ + -v "EGDIR=${KYUA_EGDIR}" \ + -v "MISCDIR=${KYUA_MISCDIR}" \ + -v "PACKAGE=kyua" \ + -v "STOREDIR=${KYUA_STOREDIR}" \ + -v "TESTSDIR=${TESTSBASE}" \ + -v "VERSION=${KYUA_VERSION}" \ + ${.ALLSRC} ${.TARGET} +.endfor + +.include