diff options
author | David Neto <dneto@google.com> | 2019-02-05 16:29:53 -0500 |
---|---|---|
committer | David Neto <dneto@google.com> | 2019-02-06 14:29:11 -0500 |
commit | b93cb2bdda93ed98f253b75a59582780c8460cc1 (patch) | |
tree | 46d3be97c3c2ce3b19b58a8a022db4eb4677c8f5 | |
parent | 325bc224e7fbd12fc97e382ddc4cfb75fbf1cebc (diff) | |
parent | b83b58d177b797edd1f94c5f10837f2cc2863f0a (diff) | |
download | effcee-b93cb2bdda93ed98f253b75a59582780c8460cc1.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' into up-shaderc2ndk-r21endk-r21dndk-r21cndk-r21bndk-r21-rc1ndk-r21-beta2ndk-r21-beta1ndk-r21ndk-release-r21
Initial drop of google/effcee from GitHub
Test: checkbuild.py on Linux; unit tests on Windows
Change-Id: Id931fb12867fc221e3f449fc32bec428f54f5c70
35 files changed, 4002 insertions, 0 deletions
diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..6fb6bc5 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,56 @@ +# Windows Build Configuration for AppVeyor +# http://www.appveyor.com/docs/appveyor-yml + +# version format +version: "{build}" + +os: + - Visual Studio 2017 + - Visual Studio 2015 + +platform: + - x64 + +configuration: + - Debug + - Release + +branches: + only: + - master + +matrix: + fast_finish: true + exclude: + - os: Visual Studio 2015 + configuration: Debug + +# scripts that run after cloning repository +install: + - git clone --depth=1 https://github.com/google/googletest.git third_party/googletest + - git clone --depth=1 https://github.com/google/re2.git third_party/re2 + +before_build: + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" (call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86_amd64) + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2017" (call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x86_amd64) + +build: + parallel: true # enable MSBuild parallel builds + verbosity: minimal + +build_script: + - mkdir build && cd build + - cmake .. -DCMAKE_INSTALL_PREFIX=install -DRE2_BUILD_TESTING=OFF + - cmake --build . --target install --config %CONFIGURATION% + +test_script: + - ctest -C %CONFIGURATION% --output-on-failure + +notifications: + - provider: Email + to: + - dneto@google.com + subject: 'Effcee Windows Build #{{buildVersion}}: {{status}}' + on_build_success: false + on_build_failure: true + on_build_status_changed: true diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..dd860a0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,6 @@ +--- +Language: Cpp +BasedOnStyle: Google +DerivePointerAlignment: false +PointerAlignment: Left +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acde7b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +build/ +build-*/ +out/ +*.pyc +*.swp +*~ +compile_commands.json +.ycm_extra_conf.py +cscope.* +third_party/re2/ +third_party/googletest/ +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ed046b6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,67 @@ +# Linux Build Configuration for Travis + +language: cpp + +os: + - linux + - osx + +# Use Ubuntu 14.04 LTS (Trusty) as the Linux testing environment. +sudo: required +dist: trusty + +env: + - EFFCEE_BUILD_TYPE=Release + - EFFCEE_BUILD_TYPE=Debug + +compiler: + - clang + - gcc + +matrix: + fast_finish: true # Show final status immediately if a test fails. + exclude: + # Skip GCC builds on macOS. + - os: osx + compiler: gcc + +cache: + apt: true + +branches: + only: + - master + +addons: + apt: + packages: + - clang + - ninja-build + +before_install: + # Install ninja on macOS. + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ninja; fi + +install: + - if [[ "$TRAVIS_OS_NAME" == "linux" && "$CC" == "clang" ]]; then + export CC=clang CXX=clang++; + fi + +before_script: + - git clone --depth=1 https://github.com/google/googletest third_party/googletest + - git clone --depth=1 https://github.com/google/re2 third_party/re2 + +script: + - mkdir build && cd build + - cmake -DCMAKE_BUILD_TYPE=${EFFCEE_BUILD_TYPE:-Debug} + -DRE2_BUILD_TESTING=OFF + -GNinja ..; + - ninja + - ctest + +notifications: + email: + recipients: + - dneto@google.com + on_success: change + on_failure: always @@ -0,0 +1,9 @@ +# This is the official list of Effcee authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as: +# Name or Organization <email address> +# The email address is not required for organizations. + +Google Inc. @@ -0,0 +1,13 @@ +Revision history for Effcee + +v2018.2-dev 2018-10-05 + - Fixes: + #23: Avoid StringPiece::as_string to enhance portability. + +v2018.1 2018-10-05 + - Require CMake 3.1 or later + - Require C++11 + - Travis-CI testing uses stock clang, instead of clang-3.6 (which is old by now) + +v2018.0 2018-10-05 + - Mature enough for production use by third party projects such as DXC and SPIRV-Tools. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..67087b7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.1) +project(effcee C CXX) +enable_testing() + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(EFFCEE_BUILD_TESTING "Enable testing for Effcee" ON) +if(${EFFCEE_BUILD_TESTING}) + message(STATUS "Configuring Effcee to build tests.") + if(MSVC) + # Our tests use ::testing::Combine. Force the ability to use it, working + # around googletest's possibly faulty compiler detection logic. + # See https://github.com/google/googletest/issues/1352 + add_definitions(-DGTEST_HAS_COMBINE=1) + endif(MSVC) +else() + message(STATUS "Configuring Effcee to avoid building tests.") +endif() + +option(EFFCEE_BUILD_SAMPLES "Enable building sample Effcee programs" ON) +if(${EFFCEE_BUILD_SAMPLES}) + message(STATUS "Configuring Effcee to build samples.") +else() + message(STATUS "Configuring Effcee to avoid building samples.") +endif() + +# RE2 needs Pthreads on non-WIN32 +set(CMAKE_THREAD_LIBS_INIT "") +find_package(Threads) + +include(cmake/setup_build.cmake) +include(cmake/utils.cmake) + +add_subdirectory(third_party) +add_subdirectory(effcee) + +if(${EFFCEE_BUILD_SAMPLES}) + add_subdirectory(examples) +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..96cf672 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing to Effcee + +Want to contribute? Great! First, read this page (including the small print at +the end). Then, have a look at [`DEVELOPMENT.howto.md`](DEVELOPMENT.howto.md), +which contains useful info to guide you along the way. + +## Before you contribute + +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) +(CLA), which you can 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'll tell us if +you know that your code infringes on other people's patents. You don't have to +sign the CLA until after you've 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. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +## The small print + +Contributions made by corporations are covered by a different agreement than +the one above, the Software Grant and Corporate Contributor License Agreement. diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..087914a --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,15 @@ +# People who have agreed to one of the CLAs and can contribute patches. +# 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. +# +# https://developers.google.com/open-source/cla/individual +# https://developers.google.com/open-source/cla/corporate +# +# Names should be added to this file as: +# Name <email address> + +Alan Baker <alanbaker@google.com> +Ehsan Nasiri <ehsannas@gmail.com> +David Neto <dneto@google.com> +Lei Zhang <antiagainst@google.com> diff --git a/DEVELOPMENT.howto.md b/DEVELOPMENT.howto.md new file mode 100644 index 0000000..ce2afdd --- /dev/null +++ b/DEVELOPMENT.howto.md @@ -0,0 +1,61 @@ +# Developing for Effcee + +Thank you for considering Effcee development! Please make sure you review +[`CONTRIBUTING.md`](CONTRIBUTING.md) for important preliminary info. + +## Building + +Instructions for first-time building can be found in [`README.md`](README.md). +Incremental build after a source change can be done using `ninja` (or +`cmake --build`) and `ctest` exactly as in the first-time procedure. + +## Issue tracking + +We use GitHub issues to track bugs, enhancement requests, and questions. +See [the project's Issues page](https://github.com/google/effcee/issues). + +For all but the most trivial changes, we prefer that you file an issue before +submitting a pull request. An issue gives us context for your change: what +problem are you solving, and why. It also allows us to provide feedback on +your proposed solution before you invest a lot of effort implementing it. + +## Code reviews + +All submissions are subject to review via the GitHub pull review process. +Reviews will cover: + +* *Correctness:* Does it work? Does it work in a multithreaded context? +* *Testing:* New functionality should be accompanied by tests. +* *Testability:* Can it easily be tested? This is proven with accompanying tests. +* *Design:* Is the solution fragile? Does it fit with the existing code? + Would it easily accommodate anticipated changes? +* *Ease of use:* Can a client get their work done with a minimum of fuss? + Are there unnecessarily surprising details? +* *Consistency:* Does it follow the style guidelines and the rest of the code? + Consistency reduces the work of future readers and maintainers. +* *Portability:* Does it work in many environments? + +To respond to feedback, submit one or more *new* commits to the pull request +branch. The project maintainer will normally clean up the submission by +squashing feedback response commits. We maintain a linear commit history, +so submission will be rebased onto master before merging. + +## Testing + +There is a lot we won't say about testing. However: + +* Most tests should be small scale, i.e. unit tests. +* Tests should run quickly. +* A test should: + * Check a single behaviour. This often corresponds to a use case. + * Have a three phase structure: setup, action, check. + +## Coding style + +For C++, we follow the +[Google C++ style guide](https://google.github.io/styleguide/cppguide.html). + +Use `clang-format` to format the code. + +For our Python files, we aim to follow the +[Google Python style guide](https://google.github.io/styleguide/pyguide.html). @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7cd0ac --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ +# Effcee + +[![Linux and OSX Build Status](https://travis-ci.org/google/effcee.svg)](https://travis-ci.org/google/effcee "Linux and OSX Build Status") + +Effcee is a C++ library for stateful pattern matching of strings, +inspired by LLVM's [FileCheck][FileCheck] command. + +Effcee: +- Is a library, so it can be used for quickly running tests in your own process. +- Is largely compatible with FileCheck, so tests and test-writing skills are + transferable. +- Has few dependencies: + - The C++11 standard library, and + - [RE2][RE2] for regular expression matching. + +## Example + +The following is from [examples/main.cc](examples/main.cc): + +```C++ + + #include <iostream> + #include <sstream> + + #include "effcee/effcee.h" + + // Checks standard input against the list of checks provided as command line + // arguments. + // + // Example: + // cat <<EOF >sample_data.txt + // Bees + // Make + // Delicious Honey + // EOF + // effcee-example <sample_data.txt "CHECK: Bees" "CHECK-NOT:Sting" "CHECK: Honey" + int main(int argc, char* argv[]) { + // Read the command arguments as a list of check rules. + std::ostringstream checks_stream; + for (int i = 1; i < argc; ++i) { + checks_stream << argv[i] << "\n"; + } + // Read stdin as the input to match. + std::stringstream input_stream; + std::cin >> input_stream.rdbuf(); + + // Attempt to match. The input and checks arguments can be provided as + // std::string or pointer to char. + auto result = effcee::Match(input_stream.str(), checks_stream.str(), + effcee::Options().SetChecksName("checks")); + + // Successful match result converts to true. + if (result) { + std::cout << "The input matched your check list!" << std::endl; + } else { + // Otherwise, you can get a status code and a detailed message. + switch (result.status()) { + case effcee::Result::Status::NoRules: + std::cout << "error: Expected check rules as command line arguments\n"; + break; + case effcee::Result::Status::Fail: + std::cout << "The input failed to match your check rules:\n"; + break; + default: + break; + } + std::cout << result.message() << std::endl; + return 1; + } + return 0; + } + +``` + +For more examples, see the matching tests +in [effcee/match_test.cc](effcee/match_test.cc). + +## Status + +Effcee is mature enough to be relied upon by +[third party projects](#what-uses-effcee), but could be improved. + +What works: +* All check types: CHECK, CHECK-NEXT, CHECK-SAME, CHECK-DAG, CHECK-LABEL, CHECK-NOT. +* Check strings can contain: + * fixed strings + * regular expressions + * variable definitions and uses +* Setting a custom check prefix. +* Accurate and helpful reporting of match failures. + +What is left to do: +* Add an option to define shorthands for regular expressions. + * For example, you could express that if the string `%%` appears where a + regular expression is expected, then it expands to the regular expression + for a local identifier in LLVM assembly language, i.e. + `%[-a-zA-Z$._][-a-zA-Z$._0-9]*`. + This enables you to write precise tests with less fuss. +* Better error reporting for failure to parse the checks list. +* Write a check language reference and tutorial. + +What is left to do, but lower priority: +* Match full lines. +* Strict whitespace. +* Implicit check-not. +* Variable scoping. + +## Licensing and contributing + +Effcee is licensed under terms of the [Apache 2.0 license](LICENSE). If you +are interested in contributing to this project, please see +[`CONTRIBUTING.md`](CONTRIBUTING.md). + +This is not an official Google product (experimental or otherwise), it is just +code that happens to be owned by Google. That may change if Effcee gains +contributions from others. See the [`CONTRIBUTING.md`](CONTRIBUTING.md) file +for more information. See also the [`AUTHORS`](AUTHORS) and +[`CONTRIBUTORS`](CONTRIBUTORS) files. + +## File organization + +- [`effcee`/](effcee) : library source code, and tests +- `third_party/`: third party open source packages, downloaded + separately +- [`examples/`](examples): example programs + +Effcee depends on the [RE2][RE2] regular expression library. + +Effcee tests depend on [Googletest][Googletest] and [Python][Python]. + +In the following sections, `$SOURCE_DIR` is the directory containing the +Effcee source code. + +## Getting and building Effcee + +1) Check out the source code: + +```sh +git clone https://github.com/google/effcee $SOURCE_DIR +cd $SOURCE_DIR/third_party +git clone https://github.com/google/googletest.git +git clone https://github.com/google/re2.git +cd $SOURCE_DIR/ +``` + +Note: There are two other ways to manage third party sources: +- If you are building Effcee as part of a larger CMake-based project, + add the RE2 and `googletest` projects before adding Effcee. +- Otherwise, you can set CMake variables to point to third party sources + if they are located somewhere else. See the [Build options](#build-options) below. + +2) Ensure you have the requisite tools -- see the tools subsection below. + +3) Decide where to place the build output. In the following steps, we'll call it + `$BUILD_DIR`. Any new directory should work. We recommend building outside + the source tree, but it is also common to build in a (new) subdirectory of + `$SOURCE_DIR`, such as `$SOURCE_DIR/build`. + +4a) Build and test with Ninja on Linux or Windows: + +```sh +cd $BUILD_DIR +cmake -GNinja -DCMAKE_BUILD_TYPE={Debug|Release|RelWithDebInfo} $SOURCE_DIR +ninja +ctest +``` + +4b) Or build and test with MSVC on Windows: + +```sh +cd $BUILD_DIR +cmake $SOURCE_DIR +cmake --build . --config {Release|Debug|MinSizeRel|RelWithDebInfo} +ctest -C {Release|Debug|MinSizeRel|RelWithDebInfo} +``` + +4c) Or build with MinGW on Linux for Windows: +(Skip building threaded unit tests due to +[Googletest bug 606](https://github.com/google/googletest/issues/606)) + +```sh +cd $BUILD_DIR +cmake -GNinja -DCMAKE_BUILD_TYPE={Debug|Release|RelWithDebInfo} $SOURCE_DIR \ + -DCMAKE_TOOLCHAIN_FILE=$SOURCE_DIR/cmake/linux-mingw-toolchain.cmake \ + -Dgtest_disable_pthreads=ON +ninja +``` + +After a successful build, you should have a `libeffcee` library under +the `$BUILD_DIR/effcee/` directory. + +The default behavior on MSVC is to link with the static CRT. If you would like +to change this behavior `-DEFFCEE_ENABLE_SHARED_CRT` may be passed on the +cmake configure line. + +### Tests + +By default, Effcee registers two tests with `ctest`: + +* `effcee-test`: All library tests, based on Googletest. +* `effcee-example`: Executes the example executable with sample inputs. + +Running `ctest` without arguments will run the tests for Effcee as well as for +RE2. + +You can disable Effcee's tests by using `-DEFFCEE_BUILD_TESTING=OFF` at +configuration time: + +```sh +cmake -GNinja -DEFFCEE_BUILD_TESTING=OFF ... +``` + +The RE2 tests run much longer, so if you're working on Effcee alone, we +suggest limiting ctest to tests with prefix `effcee`: + + ctest -R effcee + +Alternately, you can turn off RE2 tests entirely by using +`-DRE2_BUILD_TESTING=OFF` at configuration time: + +```sh +cmake -GNinja -DRE2_BUILD_TESTING=OFF ... +``` + +### Tools you'll need + +For building, testing, and profiling Effcee, the following tools should be +installed regardless of your OS: + +- A compiler supporting C++11. +- [CMake][CMake]: for generating compilation targets. +- [Python][Python]: for a test script. + +On Linux, if cross compiling to Windows: +- [MinGW][MinGW]: A GCC-based cross compiler targeting Windows + so that generated executables use the Microsoft C runtime libraries. + +On Windows, the following tools should be installed and available on your path: + +- Visual Studio 2015 or later. Previous versions of Visual Studio are not usable + with RE2 or Googletest. +- Git - including the associated tools, Bash, `diff`. + +### Build options + +Third party source locations: +- `EFFCEE_GOOGLETEST_DIR`: Location of `googletest` sources, if not under + `third_party`. +- `EFFCEE_RE2_DIR`: Location of `re2` sources, if not under `third_party`. +- `EFFCEE_THIRD_PARTY_ROOT_DIR`: Alternate location for `googletest` and + `re2` subdirectories. This is used if the sources are not located under + the `third_party` directory, and if the previous two variables are not set. + +Compilation options: +- `DISABLE_RTTI`. Disable runtime type information. Default is enabled. +- `DISABLE_EXCEPTIONS`. Disable exceptions. Default is enabled. +- `EFFCEE_ENABLE_SHARED_CRT`. See above. + +Controlling samples and tests: +- `EFFCEE_BUILD_SAMPLES`. Should Effcee examples be built? Defaults to `ON`. +- `EFFCEE_BUILD_TESTING`. Should Effcee tests be built? Defaults to `ON`. +- `RE2_BUILD_TESTING`. Should RE2 tests be built? Defaults to `ON`. + +## Bug tracking + +We track bugs using GitHub -- click on the "Issues" button on +[the project's GitHub page](https://github.com/google/effcee). + +## What uses Effcee? + +- [Tests](https://github.com/Microsoft/DirectXShaderCompiler/tree/master/tools/clang/test/CodeGenSPIRV) + for SPIR-V code generation in the [DXC][DXC] HLSL compiler. +- Tests for [SPIRV-Tools][SPIRV-Tools] + +## References + +[CMake]: https://cmake.org/ +[DXC]: https://github.com/Microsoft/DirectXShaderCompiler +[FileCheck]: http://llvm.org/docs/CommandGuide/FileCheck.html +[Googletest]: https://github.com/google/googletest +[MinGW]: http://www.mingw.org/ +[Python]: https://www.python.org/ +[RE2]: https://github.com/google/re2 +[SPIRV-Tools]: https://github.com/KhronosGroup/SPIRV-Tools diff --git a/cmake/linux-mingw-toolchain.cmake b/cmake/linux-mingw-toolchain.cmake new file mode 100644 index 0000000..9f7f676 --- /dev/null +++ b/cmake/linux-mingw-toolchain.cmake @@ -0,0 +1,35 @@ +# Copyright 2017 The Effcee Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SET(CMAKE_SYSTEM_NAME Windows) + +set(MINGW_COMPILER_PREFIX "i686-w64-mingw32" CACHE STRING + "What compiler prefix to use for mingw") + +set(MINGW_SYSROOT "/usr/${MINGW_COMPILER_PREFIX}" CACHE STRING + "What sysroot to use for mingw") + +# Which compilers to use for C and C++ +find_program(CMAKE_RC_COMPILER NAMES ${MINGW_COMPILER_PREFIX}-windres) +find_program(CMAKE_C_COMPILER NAMES ${MINGW_COMPILER_PREFIX}-gcc) +find_program(CMAKE_CXX_COMPILER NAMES ${MINGW_COMPILER_PREFIX}-g++) + +SET(CMAKE_FIND_ROOT_PATH ${MINGW_SYSROOT}) + +# Adjust the default behaviour of the FIND_XXX() commands: +# Search headers and libraries in the target environment; search +# programs in the host environment. +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/cmake/setup_build.cmake b/cmake/setup_build.cmake new file mode 100644 index 0000000..40749fc --- /dev/null +++ b/cmake/setup_build.cmake @@ -0,0 +1,83 @@ +# Copyright 2017 The Effcee Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# For cross-compilation, we need to use find_host_package +# in the remaining setup. But if not cross-compiling, then we +# need to alias find_host_package to find_package. +# Similar for find_host_program. +if(NOT COMMAND find_host_package) + macro(find_host_package) + find_package(${ARGN}) + endmacro() +endif() +if(NOT COMMAND find_host_program) + macro(find_host_program) + find_program(${ARGN}) + endmacro() +endif() + +if (ANDROID) + # For android let's preemptively find the correct packages so that + # child projects (e.g. googletest) do not fail to find them. + find_host_package(PythonInterp) +endif() + +foreach(PROGRAM echo python) + string(TOUPPER ${PROGRAM} PROG_UC) + if (ANDROID) + find_host_program(${PROG_UC}_EXE ${PROGRAM} REQUIRED) + else() + find_program(${PROG_UC}_EXE ${PROGRAM} REQUIRED) + endif() +endforeach(PROGRAM) + +option(DISABLE_RTTI "Disable RTTI in builds") +if(DISABLE_RTTI) + if(UNIX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti") + endif(UNIX) +endif(DISABLE_RTTI) + +option(DISABLE_EXCEPTIONS "Disables exceptions in builds") +if(DISABLE_EXCEPTIONS) + if(UNIX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") + endif(UNIX) +endif(DISABLE_EXCEPTIONS) + +if(WIN32) + # Ensure that gmock compiles the same as the rest of the code, otherwise + # failures will occur. + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +endif(WIN32) + +if(WIN32) +# On Windows, CMake by default compiles with the shared CRT. +# Default it to the static CRT. + option(EFFCEE_ENABLE_SHARED_CRT + "Use the shared CRT with MSVC instead of the static CRT" + ${EFFCEE_ENABLE_SHARED_CRT}) + if (NOT EFFCEE_ENABLE_SHARED_CRT) + if(MSVC) + # Link executables statically by replacing /MD with /MT everywhere. + foreach(flag_var + CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO) + if(${flag_var} MATCHES "/MD") + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endif(${flag_var} MATCHES "/MD") + endforeach(flag_var) + endif(MSVC) + endif(NOT EFFCEE_ENABLE_SHARED_CRT) +endif(WIN32) diff --git a/cmake/utils.cmake b/cmake/utils.cmake new file mode 100644 index 0000000..291be3c --- /dev/null +++ b/cmake/utils.cmake @@ -0,0 +1,61 @@ +# Copyright 2017 The Effcee Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Utility functions + +function(effcee_default_c_compile_options TARGET) + if (NOT "${MSVC}") + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + if (ENABLE_CODE_COVERAGE) + # The --coverage option is a synonym for -fprofile-arcs -ftest-coverage + # when compiling. + target_compile_options(${TARGET} PRIVATE -g -O0 --coverage) + # The --coverage option is a synonym for -lgcov when linking for gcc. + # For clang, it links in a different library, libclang_rt.profile, which + # requires clang to be built with compiler-rt. + target_link_libraries(${TARGET} PRIVATE --coverage) + endif() + if (NOT EFFCEE_ENABLE_SHARED_CRT) + if (WIN32) + # For MinGW cross compile, statically link to the libgcc runtime. + # But it still depends on MSVCRT.dll. + set_target_properties(${TARGET} PROPERTIES + LINK_FLAGS "-static -static-libgcc") + endif(WIN32) + endif(NOT EFFCEE_ENABLE_SHARED_CRT) + if (UNIX AND NOT MINGW) + target_link_libraries(${TARGET} PUBLIC -pthread) + endif() + else() + # disable warning C4800: 'int' : forcing value to bool 'true' or 'false' + # (performance warning) + target_compile_options(${TARGET} PRIVATE /wd4800) + endif() +endfunction(effcee_default_c_compile_options) + +function(effcee_default_compile_options TARGET) + effcee_default_c_compile_options(${TARGET}) + if (NOT "${MSVC}") + # RE2's public header requires C++11. So publicly required C++11 + target_compile_options(${TARGET} PUBLIC -std=c++11) + if (NOT EFFCEE_ENABLE_SHARED_CRT) + if (WIN32) + # For MinGW cross compile, statically link to the C++ runtime. + # But it still depends on MSVCRT.dll. + set_target_properties(${TARGET} PROPERTIES + LINK_FLAGS "-static -static-libgcc -static-libstdc++") + endif(WIN32) + endif(NOT EFFCEE_ENABLE_SHARED_CRT) + endif() +endfunction(effcee_default_compile_options) diff --git a/effcee/CMakeLists.txt b/effcee/CMakeLists.txt new file mode 100644 index 0000000..149f932 --- /dev/null +++ b/effcee/CMakeLists.txt @@ -0,0 +1,34 @@ +add_library(effcee + check.cc + match.cc) +effcee_default_compile_options(effcee) +# We need to expose RE2's StringPiece. +target_include_directories(effcee + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/.. ${EFFCEE_RE2_DIR}) +target_link_libraries(effcee PUBLIC re2 ${CMAKE_THREADS_LIB_INIT}) + +# TODO(dneto): Avoid installing gtest and gtest_main. ?! +install( + FILES + effcee.h + DESTINATION + include/effcee) +install(TARGETS effcee + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib) + +if(EFFCEE_BUILD_TESTING) + add_executable(effcee-test + check_test.cc + cursor_test.cc + diagnostic_test.cc + match_test.cc + options_test.cc + result_test.cc) + effcee_default_compile_options(effcee-test) + target_include_directories(effcee-test PRIVATE + ${gmock_SOURCE_DIR}/include + ${gtest_SOURCE_DIR}/include) + target_link_libraries(effcee-test PRIVATE effcee gmock gtest_main) + add_test(NAME effcee-test COMMAND effcee-test) +endif(EFFCEE_BUILD_TESTING) diff --git a/effcee/check.cc b/effcee/check.cc new file mode 100644 index 0000000..2751801 --- /dev/null +++ b/effcee/check.cc @@ -0,0 +1,257 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "check.h" + +#include <algorithm> +#include <cassert> +#include <memory> +#include <sstream> +#include <string> +#include <utility> + +#include "cursor.h" +#include "effcee.h" +#include "make_unique.h" +#include "to_string.h" + +using Status = effcee::Result::Status; +using StringPiece = effcee::StringPiece; +using Type = effcee::Check::Type; + +namespace { + +// Returns a table of suffix to type mappings. +const std::vector<std::pair<StringPiece, Type>>& TypeStringTable() { + static std::vector<std::pair<StringPiece, Type>> type_str_table{ + {"", Type::Simple}, {"-NEXT", Type::Next}, {"-SAME", Type::Same}, + {"-DAG", Type::DAG}, {"-LABEL", Type::Label}, {"-NOT", Type::Not}}; + return type_str_table; +} + +// Returns the Check::Type value matching the suffix part of a check rule +// prefix. Assumes |suffix| is valid. +Type TypeForSuffix(StringPiece suffix) { + const auto& type_str_table = TypeStringTable(); + const auto pair_iter = + std::find_if(type_str_table.begin(), type_str_table.end(), + [suffix](const std::pair<StringPiece, Type>& elem) { + return suffix == elem.first; + }); + assert(pair_iter != type_str_table.end()); + return pair_iter->second; +} +} // namespace + +namespace effcee { + +int Check::Part::CountCapturingGroups() { + if (type_ == Type::Regex) return RE2(param_).NumberOfCapturingGroups(); + if (type_ == Type::VarDef) return RE2(expression_).NumberOfCapturingGroups(); + return 0; +} + +Check::Check(Type type, StringPiece param) : type_(type), param_(param) { + parts_.push_back(make_unique<Check::Part>(Part::Type::Fixed, param)); +} + +bool Check::Part::MightMatch(const VarMapping& vars) const { + return type_ != Type::VarUse || + vars.find(ToString(VarUseName())) != vars.end(); +} + +std::string Check::Part::Regex(const VarMapping& vars) const { + switch (type_) { + case Type::Fixed: + return RE2::QuoteMeta(param_); + case Type::Regex: + return ToString(param_); + case Type::VarDef: + return std::string("(") + ToString(expression_) + ")"; + case Type::VarUse: { + auto where = vars.find(ToString(VarUseName())); + if (where != vars.end()) { + // Return the escaped form of the current value of the variable. + return RE2::QuoteMeta((*where).second); + } else { + // The variable is not yet set. Should not get here. + return ""; + } + } + } + return ""; // Unreachable. But we need to satisfy GCC. +} + +bool Check::Matches(StringPiece* input, StringPiece* captured, + VarMapping* vars) const { + if (parts_.empty()) return false; + for (auto& part : parts_) { + if (!part->MightMatch(*vars)) return false; + } + + std::unordered_map<int, std::string> var_def_indices; + + std::ostringstream consume_regex; + int num_captures = 1; // The outer capture. + for (auto& part : parts_) { + consume_regex << part->Regex(*vars); + const auto var_def_name = part->VarDefName(); + if (!var_def_name.empty()) { + var_def_indices[num_captures++] = ToString(var_def_name); + } + num_captures += part->NumCapturingGroups(); + } + std::unique_ptr<StringPiece[]> captures(new StringPiece[num_captures]); + const bool matched = RE2(consume_regex.str()) + .Match(*input, 0, input->size(), RE2::UNANCHORED, + captures.get(), num_captures); + if (matched) { + *captured = captures[0]; + input->remove_prefix(captured->end() - input->begin()); + // Update the variable mapping. + for (auto& var_def_index : var_def_indices) { + const int index = var_def_index.first; + (*vars)[var_def_index.second] = ToString(captures[index]); + } + } + + return matched; +} + +namespace { +// Returns a parts list for the given pattern. This splits out regular +// expressions as delimited by {{ and }}, and also variable uses and +// definitions. +Check::Parts PartsForPattern(StringPiece pattern) { + Check::Parts parts; + StringPiece fixed, regex, var; + + using Type = Check::Part::Type; + + while (!pattern.empty()) { + const auto regex_start = pattern.find("{{"); + const auto regex_end = pattern.find("}}"); + const auto var_start = pattern.find("[["); + const auto var_end = pattern.find("]]"); + const bool regex_exists = + regex_start < regex_end && regex_end < StringPiece::npos; + const bool var_exists = var_start < var_end && var_end < StringPiece::npos; + + if (regex_exists && (!var_exists || regex_start < var_start)) { + const auto consumed = + RE2::Consume(&pattern, "(.*?){{(.*?)}}", &fixed, ®ex); + if (!consumed) { + assert(consumed && + "Did not make forward progress for regex in check rule"); + } + if (!fixed.empty()) { + parts.emplace_back(make_unique<Check::Part>(Type::Fixed, fixed)); + } + if (!regex.empty()) { + parts.emplace_back(make_unique<Check::Part>(Type::Regex, regex)); + } + } else if (var_exists && (!regex_exists || var_start < regex_start)) { + const auto consumed = + RE2::Consume(&pattern, "(.*?)\\[\\[(.*?)\\]\\]", &fixed, &var); + if (!consumed) { + assert(consumed && + "Did not make forward progress for var in check rule"); + } + if (!fixed.empty()) { + parts.emplace_back(make_unique<Check::Part>(Type::Fixed, fixed)); + } + if (!var.empty()) { + auto colon = var.find(":"); + // A colon at the end is useless anyway, so just make it a variable + // use. + if (colon == StringPiece::npos || colon == var.size() - 1) { + parts.emplace_back(make_unique<Check::Part>(Type::VarUse, var)); + } else { + StringPiece name = var.substr(0, colon); + StringPiece expression = var.substr(colon + 1, StringPiece::npos); + parts.emplace_back( + make_unique<Check::Part>(Type::VarDef, var, name, expression)); + } + } + } else { + // There is no regex, no var def, no var use. Must be a fixed string. + parts.push_back(make_unique<Check::Part>(Type::Fixed, pattern)); + break; + } + } + + return parts; +} + +} // namespace + +std::pair<Result, CheckList> ParseChecks(StringPiece str, + const Options& options) { + // Returns a pair whose first member is a result constructed from the + // given status and message, and the second member is an empy pattern. + auto failure = [](Status status, StringPiece message) { + return std::make_pair(Result(status, message), CheckList{}); + }; + + if (options.prefix().size() == 0) + return failure(Status::BadOption, "Rule prefix is empty"); + if (RE2::FullMatch(options.prefix(), "\\s+")) + return failure(Status::BadOption, + "Rule prefix is whitespace. That's silly."); + + CheckList check_list; + + const auto quoted_prefix = RE2::QuoteMeta(options.prefix()); + // Match the following parts: + // .*? - Text that is not the rule prefix + // quoted_prefix - A Simple Check prefix + // (-NEXT|-SAME)? - An optional check type suffix. Two shown here. + // : - Colon + // \s* - Whitespace + // (.*?) - Captured parameter + // \s* - Whitespace + // $ - End of line + + const RE2 regexp(std::string(".*?") + quoted_prefix + + "(-NEXT|-SAME|-DAG|-LABEL|-NOT)?" + ":\\s*(.*?)\\s*$"); + Cursor cursor(str); + while (!cursor.Exhausted()) { + const auto line = cursor.RestOfLine(); + + StringPiece matched_param; + StringPiece suffix; + if (RE2::PartialMatch(line, regexp, &suffix, &matched_param)) { + const Type type = TypeForSuffix(suffix); + auto parts(PartsForPattern(matched_param)); + check_list.push_back(Check(type, matched_param, std::move(parts))); + } + cursor.AdvanceLine(); + } + + if (check_list.empty()) { + return failure( + Status::NoRules, + std::string("No check rules specified. Looking for prefix ") + + options.prefix()); + } + + if (check_list[0].type() == Type::Same) { + return failure(Status::BadRule, std::string(options.prefix()) + + "-SAME can't be the first check rule"); + } + + return std::make_pair(Result(Result::Status::Ok), check_list); +} +} // namespace effcee diff --git a/effcee/check.h b/effcee/check.h new file mode 100644 index 0000000..35a5c0b --- /dev/null +++ b/effcee/check.h @@ -0,0 +1,203 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef EFFCEE_CHECK_H +#define EFFCEE_CHECK_H + +#include <memory> +#include <string> +#include <unordered_map> +#include <utility> +#include <vector> + +#include "effcee.h" +#include "make_unique.h" + +namespace effcee { + +// A mapping from a name to a string value. +using VarMapping = std::unordered_map<std::string, std::string>; + +// A single check indicating something to be matched. +// +// A _positive_ check is _resolved_ when its parameter is matches a part of the +// in the input text. A _negative_ check is _resolved_ when its parameter does +// _not_ match a section of the input between context-dependent start and end +// points. +class Check { + public: + // The type Determines when the check is satisfied. The Not type denotes + // a negative check. The other types denote positive checks. + enum class Type { + Simple, // Matches a string. + Next, // Matches a string, on the line following previous match. + Same, // Matches a string, on the same line as the previous metch. + DAG, // Matches a string, unordered with respect to other + Label, // Like Simple, but resets local variables. + Not, // Given string is not found before next positive match. + }; + + // A Part is a contiguous segment of the check pattern. A part is + // distinguished by how it matches against input. + class Part { + public: + enum class Type { + Fixed, // A fixed string: characters are matched exactly, in sequence. + Regex, // A regular expression + VarDef, // A variable definition + VarUse, // A variable use + }; + + Part(Type type, StringPiece param) + : type_(type), + param_(param), + name_(), + expression_(), + num_capturing_groups_(CountCapturingGroups()) {} + + // A constructor for a VarDef variant. + Part(Type type, StringPiece param, StringPiece name, StringPiece expr) + : type_(type), + param_(param), + name_(name), + expression_(expr), + num_capturing_groups_(CountCapturingGroups()) {} + + // Returns true if this part might match a target string. The only case where + // this is false is for a VarUse part where the variable is not yet defined. + bool MightMatch(const VarMapping& vars) const; + + // Returns a regular expression to match this part, given a mapping of + // variable names to values. If this part is a fixed string or variable use + // then quoting has been applied. + std::string Regex(const VarMapping& vars) const; + + // Returns number of capturing subgroups in the regex for a Regex or VarDef + // part, and 0 for other parts. + int NumCapturingGroups() const { return num_capturing_groups_; } + + // If this is a VarDef, then returns the name of the variable. Otherwise + // returns an empty string. + StringPiece VarDefName() const { return name_; } + + // If this is a VarUse, then returns the name of the variable. Otherwise + // returns an empty string. + StringPiece VarUseName() const { + return type_ == Type::VarUse ? param_ : ""; + } + + private: + // Computes the number of capturing groups in this part. This is zero + // for Fixed and VarUse parts. + int CountCapturingGroups(); + + // The part type. + Type type_; + // The part parameter. For a Regex, VarDef, and VarUse, this does not + // have the delimiters. + StringPiece param_; + + // For a VarDef, the name of the variable. + StringPiece name_; + // For a VarDef, the regex matching the new value for the variable. + StringPiece expression_; + // The number of capturing subgroups in the regex for a Regex or VarDef + // part, and 0 for other kinds of parts. + int num_capturing_groups_; + }; + + using Parts = std::vector<std::unique_ptr<Part>>; + + // MSVC needs a default constructor. However, a default-constructed Check + // instance can't be used for matching. + Check() : type_(Type::Simple) {} + + // Construct a Check object of the given type and fixed parameter string. + // In particular, this retains a StringPiece reference to the |param| + // contents, so that string storage should remain valid for the duration + // of this object. + Check(Type type, StringPiece param); + + // Construct a Check object of the given type, with given parameter string + // and specified parts. + Check(Type type, StringPiece param, Parts&& parts) + : type_(type), param_(param), parts_(std::move(parts)) {} + + // Move constructor. + Check(Check&& other) : type_(other.type_), param_(other.param_) { + parts_.swap(other.parts_); + } + // Copy constructor. + Check(const Check& other) : type_(other.type_), param_(other.param_) { + for (const auto& part : other.parts_) { + parts_.push_back(make_unique<Part>(*part)); + } + } + // Copy and move assignment. + Check& operator=(Check other) { + type_ = other.type_; + param_ = other.param_; + std::swap(parts_, other.parts_); + return *this; + } + + // Accessors. + Type type() const { return type_; } + StringPiece param() const { return param_; } + const Parts& parts() const { return parts_; } + + // Tries to match the given string, using |vars| as the variable mapping + // context. A variable use, e.g. '[[X]]', matches the current value for + // that variable in vars, 'X' in this case. A variable definition, + // e.g. '[[XYZ:[0-9]+]]', will match against the regex provdided after the + // colon. If successful, returns true, advances |str| past the matched + // portion, saves the captured substring in |captured|, and sets the value + // of named variables in |vars| with the strings they matched. Otherwise + // returns false and does not update |str| or |captured|. Assumes this + // instance is not default-constructed. + bool Matches(StringPiece* str, StringPiece* captured, VarMapping* vars) const; + + private: + // The type of check. + Type type_; + + // The parameter as given in user input, if any. + StringPiece param_; + + // The parameter, broken down into parts. + Parts parts_; +}; + +// Equality operator for Check. +inline bool operator==(const Check& lhs, const Check& rhs) { + return lhs.type() == rhs.type() && lhs.param() == rhs.param(); +} + +// Inequality operator for Check. +inline bool operator!=(const Check& lhs, const Check& rhs) { + return !(lhs == rhs); +} + +using CheckList = std::vector<Check>; + +// Parses |checks_string|, returning a Result status object and the sequence +// of recognized checks, taking |options| into account. The result status +// object indicates success, or failure with a message. +// TODO(dneto): Only matches simple checks for now. +std::pair<Result, CheckList> ParseChecks(StringPiece checks_string, + const Options& options); + +} // namespace effcee + +#endif diff --git a/effcee/check_test.cc b/effcee/check_test.cc new file mode 100644 index 0000000..c9d0674 --- /dev/null +++ b/effcee/check_test.cc @@ -0,0 +1,342 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <vector> +#include "gmock/gmock.h" + +#include "check.h" + +namespace { + +using effcee::Check; +using effcee::Options; +using effcee::CheckList; +using effcee::ParseChecks; +using effcee::Result; +using effcee::StringPiece; +using ::testing::Combine; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::ValuesIn; + +using Part = effcee::Check::Part; +using Status = effcee::Result::Status; +using Type = Check::Type; +using VarMapping = effcee::VarMapping; + +// Check class + +// Returns a vector of all Check types. +std::vector<Type> AllTypes() { + return {Type::Simple, Type::Next, Type::Same, + Type::DAG, Type::Label, Type::Not}; +} + +using CheckTypeTest = ::testing::TestWithParam<Type>; + +TEST_P(CheckTypeTest, ConstructWithAnyType) { + Check check(GetParam(), ""); + EXPECT_THAT(check.type(), Eq(GetParam())); +} + +INSTANTIATE_TEST_SUITE_P(AllTypes, CheckTypeTest, ValuesIn(AllTypes())); + +using CheckParamTest = ::testing::TestWithParam<StringPiece>; + +TEST_P(CheckParamTest, ConstructWithSampleParamValue) { + Check check(Type::Simple, GetParam()); + // The contents are the same. + EXPECT_THAT(check.param(), Eq(GetParam())); + // The referenced storage is the same. + EXPECT_THAT(check.param().data(), Eq(GetParam().data())); +} + +INSTANTIATE_TEST_SUITE_P(SampleParams, CheckParamTest, + ValuesIn(std::vector<StringPiece>{ + "", "a b c", "The wind {{in}} the willows\n", + "Bring me back to the mountains of yore."})); + +// Equality operator +TEST(CheckEqualityTest, TrueWhenAllComponentsSame) { + EXPECT_TRUE(Check(Type::Simple, "abc") == Check(Type::Simple, "abc")); +} + +TEST(CheckEqualityTest, FalseWhenTypeDifferent) { + EXPECT_FALSE(Check(Type::Simple, "abc") == Check(Type::Next, "abc")); +} + +TEST(CheckEqualityTest, FalseWhenParamDifferent) { + EXPECT_FALSE(Check(Type::Simple, "abc") == Check(Type::Simple, "def")); +} + +// Inequality operator +TEST(CheckInequalityTest, FalseWhenAllComponentsSame) { + EXPECT_FALSE(Check(Type::Simple, "abc") != Check(Type::Simple, "abc")); +} + +TEST(CheckInequalityTest, TrueWhenTypeDifferent) { + EXPECT_TRUE(Check(Type::Simple, "abc") != Check(Type::Next, "abc")); +} + +TEST(CheckInequalityTest, TrueWhenParamDifferent) { + EXPECT_TRUE(Check(Type::Simple, "abc") != Check(Type::Simple, "def")); +} + +// ParseChecks free function + +TEST(ParseChecks, FreeFunctionLinks) { + std::pair<Result, CheckList> parsed(ParseChecks("", Options())); +} + +TEST(ParseChecks, FailWhenRulePrefixIsEmpty) { + const auto parsed(ParseChecks("CHECK: now", Options().SetPrefix(""))); + const Result& result = parsed.first; + const CheckList& pattern = parsed.second; + EXPECT_THAT(result.status(), Eq(Status::BadOption)); + EXPECT_THAT(result.message(), Eq("Rule prefix is empty")); + EXPECT_THAT(pattern.size(), Eq(0)); +} + +TEST(ParseChecks, FailWhenRulePrefixIsWhitespace) { + const auto parsed(ParseChecks("CHECK: now", Options().SetPrefix("\t\n "))); + const Result& result = parsed.first; + const CheckList& pattern = parsed.second; + EXPECT_THAT(result.status(), Eq(Status::BadOption)); + EXPECT_THAT(result.message(), + Eq("Rule prefix is whitespace. That's silly.")); + EXPECT_THAT(pattern.size(), Eq(0)); +} + +TEST(ParseChecks, FailWhenChecksAbsent) { + const auto parsed(ParseChecks("no checks", Options())); + const Result& result = parsed.first; + const CheckList& pattern = parsed.second; + EXPECT_THAT(result.status(), Eq(Status::NoRules)); + EXPECT_THAT(result.message(), + Eq("No check rules specified. Looking for prefix CHECK")); + EXPECT_THAT(pattern.size(), Eq(0)); +} + +TEST(ParseChecks, FailWhenChecksAbsentWithCustomPrefix) { + const auto parsed(ParseChecks("CHECK: now", Options().SetPrefix("FOO"))); + const Result& result = parsed.first; + const CheckList& pattern = parsed.second; + EXPECT_THAT(result.status(), Eq(Status::NoRules)); + EXPECT_THAT(result.message(), + Eq("No check rules specified. Looking for prefix FOO")); + EXPECT_THAT(pattern.size(), Eq(0)); +} + +TEST(ParseChecks, FindSimpleCheck) { + const auto parsed = ParseChecks("CHECK: now", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "now")}))); +} + +TEST(ParseChecks, FindSimpleCheckWithCustomPrefix) { + const auto parsed = ParseChecks("FOO: how", Options().SetPrefix("FOO")); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "how")}))); +} + +TEST(ParseChecks, FindSimpleCheckWithCustomPrefixHavingRegexpMetachars) { + const auto parsed = ParseChecks("[::alpha::]^\\d: how", + Options().SetPrefix("[::alpha::]^\\d")); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "how")}))); +} + +TEST(ParseChecks, FindSimpleCheckPartwayThroughLine) { + const auto parsed = ParseChecks("some other garbageCHECK: now", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "now")}))); +} + +TEST(ParseChecks, FindSimpleCheckCheckListWithoutSurroundingWhitespace) { + const auto parsed = ParseChecks("CHECK:now", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "now")}))); +} + +TEST(ParseChecks, FindSimpleCheckCheckListWhileStrippingSurroundingWhitespace) { + const auto parsed = ParseChecks("CHECK: \t now\t\t ", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "now")}))); +} + +TEST(ParseChecks, FindSimpleCheckCountsLinesCorrectly) { + const auto parsed = ParseChecks("\n\nCHECK: now", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "now")}))); +} + +TEST(ParseChecks, FindSimpleChecksOnSeparateLines) { + const auto parsed = + ParseChecks("CHECK: now\n\n\nCHECK: and \n CHECK: then", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, Eq(CheckList({Check(Type::Simple, "now"), + Check(Type::Simple, "and"), + Check(Type::Simple, "then")}))); +} + +TEST(ParseChecks, FindSimpleChecksOnlyOncePerLine) { + const auto parsed = ParseChecks("CHECK: now CHECK: then", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, + Eq(CheckList({Check(Type::Simple, "now CHECK: then")}))); +} + +// Test parsing of the different check rule types. + +using ParseChecksTypeTest = ::testing::TestWithParam< + std::tuple<std::string, std::pair<std::string, Type>>>; + +TEST_P(ParseChecksTypeTest, Successful) { + const auto& prefix = std::get<0>(GetParam()); + const auto& type_str = std::get<0>(std::get<1>(GetParam())); + const Type& type = std::get<1>(std::get<1>(GetParam())); + // A CHECK-SAME rule can't appear first, so insert a CHECK: rule first. + const std::string input = prefix + ": here\n" + prefix + type_str + ": now"; + const auto parsed = ParseChecks(input, Options().SetPrefix(prefix)); + EXPECT_THAT(parsed.first.status(), Eq(Status::Ok)); + EXPECT_THAT(parsed.second, + Eq(CheckList({Check(Type::Simple, "here"), Check(type, "now")}))); +} + +// Returns a vector of pairs. Each pair has first member being a check type +// suffix, and the second member is the corresponding check type. +std::vector<std::pair<std::string, Type>> AllCheckTypesAsPairs() { + return { + {"", Type::Simple}, {"-NEXT", Type::Next}, {"-SAME", Type::Same}, + {"-DAG", Type::DAG}, {"-LABEL", Type::Label}, {"-NOT", Type::Not}, + }; +} + +INSTANTIATE_TEST_SUITE_P(AllCheckTypes, ParseChecksTypeTest, + Combine(ValuesIn(std::vector<std::string>{"CHECK", + "FOO"}), + ValuesIn(AllCheckTypesAsPairs()))); + +using ParseChecksTypeFailTest = ::testing::TestWithParam< + std::tuple<std::string, std::pair<std::string, Type>>>; + +// This is just one way to fail. +TEST_P(ParseChecksTypeFailTest, FailureWhenNoColon) { + const auto& prefix = std::get<0>(GetParam()); + const auto& type_str = std::get<0>(std::get<1>(GetParam())); + const std::string input = prefix + type_str + "BAD now"; + const auto parsed = ParseChecks(input, Options().SetPrefix(prefix)); + EXPECT_THAT(parsed.first.status(), Eq(Status::NoRules)); + EXPECT_THAT(parsed.second, Eq(CheckList{})); +} + +INSTANTIATE_TEST_SUITE_P(AllCheckTypes, ParseChecksTypeFailTest, + Combine(ValuesIn(std::vector<std::string>{"CHECK", + "FOO"}), + ValuesIn(AllCheckTypesAsPairs()))); + +TEST(ParseChecks, CheckSameCantBeFirst) { + const auto parsed = ParseChecks("CHECK-SAME: now", Options()); + EXPECT_THAT(parsed.first.status(), Eq(Status::BadRule)); + EXPECT_THAT(parsed.first.message(), + HasSubstr("CHECK-SAME can't be the first check rule")); + EXPECT_THAT(parsed.second, Eq(CheckList({}))); +} + +TEST(ParseChecks, CheckSameCantBeFirstDifferentPrefix) { + const auto parsed = ParseChecks("BOO-SAME: now", Options().SetPrefix("BOO")); + EXPECT_THAT(parsed.first.status(), Eq(Status::BadRule)); + EXPECT_THAT(parsed.first.message(), + HasSubstr("BOO-SAME can't be the first check rule")); + EXPECT_THAT(parsed.second, Eq(CheckList({}))); +} + +// Check::Matches +struct CheckMatchCase { + std::string input; + Check check; + bool expected; + std::string remaining; + std::string captured; +}; + +using CheckMatchTest = ::testing::TestWithParam<CheckMatchCase>; + +TEST_P(CheckMatchTest, Samples) { + StringPiece str = GetParam().input; + StringPiece captured; + VarMapping vars; + const bool matched = GetParam().check.Matches(&str, &captured, &vars); + EXPECT_THAT(matched, Eq(GetParam().expected)) + << "Failed on input " << GetParam().input; + EXPECT_THAT(std::string(str.data(), str.size()), Eq(GetParam().remaining)); + EXPECT_THAT(std::string(captured.data(), captured.size()), + Eq(GetParam().captured)); + EXPECT_TRUE(vars.empty()); +} + +INSTANTIATE_TEST_SUITE_P( + Simple, CheckMatchTest, + ValuesIn(std::vector<CheckMatchCase>{ + {"hello", Check(Type::Simple, "hello"), true, "", "hello"}, + {"world", Check(Type::Simple, "hello"), false, "world", ""}, + {"in hello now", Check(Type::Simple, "hello"), true, " now", "hello"}, + {"hello", Check(Type::Same, "hello"), true, "", "hello"}, + {"world", Check(Type::Same, "hello"), false, "world", ""}, + {"in hello now", Check(Type::Same, "hello"), true, " now", "hello"}, + {"hello", Check(Type::Next, "hello"), true, "", "hello"}, + {"world", Check(Type::Next, "hello"), false, "world", ""}, + {"in hello now", Check(Type::Next, "hello"), true, " now", "hello"}, + {"hello", Check(Type::DAG, "hello"), true, "", "hello"}, + {"world", Check(Type::DAG, "hello"), false, "world", ""}, + {"in hello now", Check(Type::DAG, "hello"), true, " now", "hello"}, + {"hello", Check(Type::Label, "hello"), true, "", "hello"}, + {"world", Check(Type::Label, "hello"), false, "world", ""}, + {"in hello now", Check(Type::Label, "hello"), true, " now", "hello"}, + {"hello", Check(Type::Label, "hello"), true, "", "hello"}, + {"world", Check(Type::Label, "hello"), false, "world", ""}, + {"in hello now", Check(Type::Label, "hello"), true, " now", "hello"}, + {"hello", Check(Type::Not, "hello"), true, "", "hello"}, + {"world", Check(Type::Not, "hello"), false, "world", ""}, + {"in hello now", Check(Type::Not, "hello"), true, " now", "hello"}, + })); + +// Check::Part::Regex + +TEST(CheckPart, FixedPartRegex) { + VarMapping vm; + EXPECT_THAT(Part(Part::Type::Fixed, "abc").Regex(vm), Eq("abc")); + EXPECT_THAT(Part(Part::Type::Fixed, "a.bc").Regex(vm), Eq("a\\.bc")); + EXPECT_THAT(Part(Part::Type::Fixed, "a?bc").Regex(vm), Eq("a\\?bc")); + EXPECT_THAT(Part(Part::Type::Fixed, "a+bc").Regex(vm), Eq("a\\+bc")); + EXPECT_THAT(Part(Part::Type::Fixed, "a*bc").Regex(vm), Eq("a\\*bc")); + EXPECT_THAT(Part(Part::Type::Fixed, "a[b]").Regex(vm), Eq("a\\[b\\]")); + EXPECT_THAT(Part(Part::Type::Fixed, "a[-]").Regex(vm), Eq("a\\[\\-\\]")); + EXPECT_THAT(Part(Part::Type::Fixed, "a(-)b").Regex(vm), Eq("a\\(\\-\\)b")); +} + +TEST(CheckPart, RegexPartRegex) { + VarMapping vm; + EXPECT_THAT(Part(Part::Type::Regex, "abc").Regex(vm), Eq("abc")); + EXPECT_THAT(Part(Part::Type::Regex, "a.bc").Regex(vm), Eq("a.bc")); + EXPECT_THAT(Part(Part::Type::Regex, "a?bc").Regex(vm), Eq("a?bc")); + EXPECT_THAT(Part(Part::Type::Regex, "a+bc").Regex(vm), Eq("a+bc")); + EXPECT_THAT(Part(Part::Type::Regex, "a*bc").Regex(vm), Eq("a*bc")); + EXPECT_THAT(Part(Part::Type::Regex, "a[b]").Regex(vm), Eq("a[b]")); + EXPECT_THAT(Part(Part::Type::Regex, "a[-]").Regex(vm), Eq("a[-]")); + EXPECT_THAT(Part(Part::Type::Regex, "a(-)b").Regex(vm), Eq("a(-)b")); +} + +} // namespace + diff --git a/effcee/cursor.h b/effcee/cursor.h new file mode 100644 index 0000000..82133f9 --- /dev/null +++ b/effcee/cursor.h @@ -0,0 +1,97 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef EFFCEE_CURSOR_H +#define EFFCEE_CURSOR_H + +#include <sstream> +#include <string> + +#include "re2/stringpiece.h" + +namespace effcee { + +using StringPiece = re2::StringPiece; + +// Represents a position in a StringPiece, while tracking line number. +class Cursor { + + public: + explicit Cursor(StringPiece str) + : remaining_(str), line_num_(1) {} + + StringPiece remaining() const { return remaining_; } + // Returns the current 1-based line number. + int line_num() const { return line_num_; } + + // Returns true if the remaining text is empty. + bool Exhausted() const { return remaining_.empty(); } + + // Returns a string piece from the current position until the end of the line + // or the end of input, up to and including the newline. + StringPiece RestOfLine() const { + const auto newline_pos = remaining_.find('\n'); + return remaining_.substr(0, newline_pos + (newline_pos != StringPiece::npos)); + } + + // Advance |n| characters. Does not adjust line count. The next |n| + // characters should not contain newlines if line numbering is to remain + // up to date. Returns this object. + Cursor& Advance(size_t n) { remaining_.remove_prefix(n); return *this; } + + // Advances the cursor by a line. If no text remains, then does nothing. + // Otherwise removes the first line (including newline) and increments the + // line count. If there is no newline then the remaining string becomes + // empty. Returns this object. + Cursor& AdvanceLine() { + if (remaining_.size()) { + Advance(RestOfLine().size()); + ++line_num_; + } + return *this; + }; + + private: + // The remaining text, after all previous advancements. References the + // original string storage. + StringPiece remaining_; + // The current 1-based line number. + int line_num_; +}; + +// Returns string containing a description of the line containing a given +// subtext, with a message, and a caret displaying the subtext position. +// Assumes subtext does not contain a newline. +inline std::string LineMessage(StringPiece text, StringPiece subtext, + StringPiece message) { + Cursor c(text); + StringPiece full_line = c.RestOfLine(); + while (subtext.end() - full_line.end() > 0) { + c.AdvanceLine(); + full_line = c.RestOfLine(); + } + const char* full_line_newline = + full_line.find('\n') == StringPiece::npos ? "\n" : ""; + const size_t column = subtext.begin() - full_line.begin(); + + std::ostringstream out; + out << ":" << c.line_num() << ":" << (1 + column) << ": " << message << "\n" + << full_line << full_line_newline << std::string(column, ' ') << "^\n"; + + return out.str(); +} + +} // namespace effcee + +#endif diff --git a/effcee/cursor_test.cc b/effcee/cursor_test.cc new file mode 100644 index 0000000..ea2016d --- /dev/null +++ b/effcee/cursor_test.cc @@ -0,0 +1,179 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "gmock/gmock.h" + +#include "cursor.h" + +namespace { + +using effcee::Cursor; +using effcee::LineMessage; +using effcee::StringPiece; +using ::testing::Eq; +using ::testing::HasSubstr; + +// text method + +// remaining and Advance methods +TEST(Cursor, AdvanceReturnsTheCursorItself) { + Cursor c("foo"); + EXPECT_THAT(&c.Advance(1), Eq(&c)); +} + +TEST(Cursor, RemainingBeginsEqualToText) { + const char* original = "The Smiths"; + Cursor c(original); + EXPECT_THAT(c.remaining().begin(), Eq(original)); +} + +TEST(Cursor, RemainingDiminishesByPreviousAdvanceCalls) { + const char* original = "The Smiths are a great 80s band"; + Cursor c(original); + c.Advance(4); + EXPECT_THAT(c.remaining(), Eq("Smiths are a great 80s band")); + EXPECT_THAT(c.remaining().begin(), Eq(original + 4)); + c.Advance(11); + EXPECT_THAT(c.remaining(), Eq("a great 80s band")); + EXPECT_THAT(c.remaining().begin(), Eq(original + 15)); + c.Advance(c.remaining().size()); + EXPECT_THAT(c.remaining(), Eq("")); + EXPECT_THAT(c.remaining().begin(), Eq(original + 31)); +} + +// Exhausted method + +TEST(Cursor, ExhaustedImmediatelyWhenStartingWithEmptyString) { + Cursor c(""); + EXPECT_TRUE(c.Exhausted()); +} + +TEST(Cursor, ExhaustedWhenRemainingIsEmpty) { + Cursor c("boo"); + EXPECT_FALSE(c.Exhausted()); + c.Advance(2); + EXPECT_FALSE(c.Exhausted()); + c.Advance(1); + EXPECT_TRUE(c.Exhausted()); +} + +// RestOfLine method + +TEST(Cursor, RestOfLineOnEmptyReturnsEmpty) { + const char* original = ""; + Cursor c(original); + EXPECT_THAT(c.RestOfLine(), Eq("")); + EXPECT_THAT(c.RestOfLine().begin(), Eq(original)); +} + +TEST(Cursor, RestOfLineWithoutNewline) { + Cursor c("The end"); + EXPECT_THAT(c.RestOfLine(), Eq("The end")); +} + +TEST(Cursor, RestOfLineGetsLineUpToAndIncludingNewline) { + Cursor c("The end\nOf an era"); + EXPECT_THAT(c.RestOfLine(), Eq("The end\n")); +} + +TEST(Cursor, RestOfLineGetsOnlyFromRemainingText) { + Cursor c("The end\nOf an era"); + c.Advance(4); + EXPECT_THAT(c.remaining(), Eq("end\nOf an era")); + EXPECT_THAT(c.RestOfLine(), Eq("end\n")); +} + +// AdvanceLine and line_num methods + +TEST(Cursor, AdvanceLineReturnsTheCursorItself) { + Cursor c("foo\nbar"); + EXPECT_THAT(&c.AdvanceLine(), Eq(&c)); +} + +TEST(Cursor, AdvanceLineWalksThroughTextByLineAndCountsLines) { + const char* original = "The end\nOf an era\nIs here"; + Cursor c(original); + EXPECT_THAT(c.line_num(), Eq(1)); + c.AdvanceLine(); + EXPECT_THAT(c.line_num(), Eq(2)); + EXPECT_THAT(c.remaining(), Eq("Of an era\nIs here")); + EXPECT_THAT(c.remaining().begin(), Eq(original + 8)); + c.AdvanceLine(); + EXPECT_THAT(c.line_num(), Eq(3)); + EXPECT_THAT(c.remaining(), Eq("Is here")); + EXPECT_THAT(c.remaining().begin(), Eq(original + 18)); + c.AdvanceLine(); + EXPECT_THAT(c.line_num(), Eq(4)); + EXPECT_THAT(c.remaining(), Eq("")); + EXPECT_THAT(c.remaining().begin(), Eq(original + 25)); +} + +TEST(Cursor, AdvanceLineIsNoopAfterEndIsReached) { + Cursor c("One\nTwo"); + c.AdvanceLine(); + EXPECT_THAT(c.line_num(), Eq(2)); + EXPECT_THAT(c.remaining(), Eq("Two")); + c.AdvanceLine(); + EXPECT_THAT(c.line_num(), Eq(3)); + EXPECT_THAT(c.remaining(), Eq("")); + c.AdvanceLine(); + EXPECT_THAT(c.line_num(), Eq(3)); + EXPECT_THAT(c.remaining(), Eq("")); +} + +// LineMessage free function. + +TEST(LineMessage, SubtextIsFirst) { + StringPiece text("Foo\nBar"); + StringPiece subtext(text.begin(), 3); + EXPECT_THAT(LineMessage(text, subtext, "loves quiche"), + Eq(":1:1: loves quiche\nFoo\n^\n")); +} + +TEST(LineMessage, SubtextDoesNotEndInNewline) { + StringPiece text("Foo\nBar"); + StringPiece subtext(text.begin()+4, 3); + EXPECT_THAT(LineMessage(text, subtext, "loves quiche"), + Eq(":2:1: loves quiche\nBar\n^\n")); +} + +TEST(LineMessage, SubtextPartwayThroughItsLine) { + StringPiece text("Food Life\nBar"); + StringPiece subtext(text.begin() + 5, 3); // "Lif" + EXPECT_THAT(LineMessage(text, subtext, "loves quiche"), + Eq(":1:6: loves quiche\nFood Life\n ^\n")); +} + +TEST(LineMessage, SubtextOnSubsequentLine) { + StringPiece text("Food Life\nBar Fight\n"); + StringPiece subtext(text.begin() + 14, 5); // "Fight" + EXPECT_THAT(LineMessage(text, subtext, "loves quiche"), + Eq(":2:5: loves quiche\nBar Fight\n ^\n")); +} + +TEST(LineMessage, SubtextIsEmptyAndInMiddle) { + StringPiece text("Food"); + StringPiece subtext(text.begin() + 2, 0); + EXPECT_THAT(LineMessage(text, subtext, "loves quiche"), + Eq(":1:3: loves quiche\nFood\n ^\n")); +} + +TEST(LineMessage, SubtextIsEmptyAndAtVeryEnd) { + StringPiece text("Food"); + StringPiece subtext(text.begin() + 4, 0); + EXPECT_THAT(LineMessage(text, subtext, "loves quiche"), + Eq(":1:5: loves quiche\nFood\n ^\n")); +} + +} // namespace diff --git a/effcee/diagnostic.h b/effcee/diagnostic.h new file mode 100644 index 0000000..b348687 --- /dev/null +++ b/effcee/diagnostic.h @@ -0,0 +1,59 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef EFFCEE_DIAGNOSTIC_H +#define EFFCEE_DIAGNOSTIC_H + +#include <sstream> + +#include "effcee/effcee.h" + +namespace effcee { + +// A Diagnostic contains a Result::Status value and can accumulate message +// values via operator<<. It is convertible to a Result object containing the +// status and the stringified message. +class Diagnostic { + public: + explicit Diagnostic(Result::Status status) : status_(status) {} + + // Copy constructor. + Diagnostic(const Diagnostic& other) : status_(other.status_), message_() { + // We can't move an ostringstream. As a fallback, we'd like to use the + // std::ostringstream(std::string init_string) constructor. However, that + // initial string disappears inexplicably the first time we shift onto + // the |message_| member. So fall back further and use the default + // constructor and later use an explicit shift. + message_ << other.message_.str(); + } + + // Appends the given value to the accumulated message. + template <typename T> + Diagnostic& operator<<(const T& value) { + message_ << value; + return *this; + } + + // Converts this object to a result value containing the stored status and a + // stringified copy of the message. + operator Result() const { return Result(status_, message_.str()); } + + private: + Result::Status status_; + std::ostringstream message_; +}; + +} // namespace effcee + +#endif diff --git a/effcee/diagnostic_test.cc b/effcee/diagnostic_test.cc new file mode 100644 index 0000000..acc89e9 --- /dev/null +++ b/effcee/diagnostic_test.cc @@ -0,0 +1,75 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "gmock/gmock.h" + +#include "diagnostic.h" + +namespace { + +using effcee::Diagnostic; +using effcee::Result; +using testing::Eq; + +using Status = effcee::Result::Status; + +// Check conversion preserves status. +TEST(Diagnostic, ConvertsToResultWithSameOkStatus) { + const Diagnostic d(Status::Ok); + const Result r(d); + EXPECT_THAT(r.status(), Eq(Status::Ok)); +} + +TEST(Diagnostic, ConvertsToResultWithSameFailStatus) { + const Diagnostic d(Status::Fail); + const Result r(d); + EXPECT_THAT(r.status(), Eq(Status::Fail)); +} + +// Check conversion, with messages. + +TEST(Diagnostic, MessageDefaultsToEmpty) { + const Diagnostic d(Status::Ok); + const Result r(d); + EXPECT_THAT(r.message(), Eq("")); +} + +TEST(Diagnostic, MessageAccumulatesValuesOfDifferentTypes) { + Diagnostic d(Status::Ok); + d << "hello" << ' ' << 42 << " and " << 32u << " and " << 1.25; + const Result r(d); + EXPECT_THAT(r.message(), Eq("hello 42 and 32 and 1.25")); +} + +// Check copying + +TEST(Diagnostic, CopyRetainsOriginalMessage) { + Diagnostic d(Status::Ok); + d << "hello"; + Diagnostic d2 = d; + const Result r(d2); + EXPECT_THAT(r.message(), Eq("hello")); +} + +TEST(Diagnostic, ShiftOnCopyAppendsToOriginalMessage) { + Diagnostic d(Status::Ok); + d << "hello"; + Diagnostic d2 = d; + d2 << " world"; + const Result r(d2); + EXPECT_THAT(r.message(), Eq("hello world")); +} + + +} // namespace diff --git a/effcee/effcee.h b/effcee/effcee.h new file mode 100644 index 0000000..eb1b1bb --- /dev/null +++ b/effcee/effcee.h @@ -0,0 +1,113 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef EFFCEE_EFFCEE_H +#define EFFCEE_EFFCEE_H + +#include <string> +#include "re2/re2.h" + +namespace effcee { + +// TODO(dneto): Provide a check language tutorial / manual. + +// This does not implement the equivalents of FileCheck options: +// --match-full-lines +// --strict-whitespace +// --implicit-ch3eck-not +// --enable-var-scope + +using StringPiece = re2::StringPiece; + +// Options for matching. +class Options { + public: + Options() + : prefix_("CHECK"), input_name_("<stdin>"), checks_name_("<stdin>") {} + + // Sets rule prefix to a copy of |prefix|. Returns this object. + Options& SetPrefix(StringPiece prefix) { + prefix_ = std::string(prefix.begin(), prefix.end()); + return *this; + } + const std::string& prefix() const { return prefix_; } + + // Sets the input name. Returns this object. + // Use this for file names, for example. + Options& SetInputName(StringPiece name) { + input_name_ = std::string(name.begin(), name.end()); + return *this; + } + const std::string& input_name() const { return input_name_; } + + // Sets the checks input name. Returns this object. + // Use this for file names, for example. + Options& SetChecksName(StringPiece name) { + checks_name_ = std::string(name.begin(), name.end()); + return *this; + } + const std::string& checks_name() const { return checks_name_; } + + private: + std::string prefix_; + std::string input_name_; + std::string checks_name_; +}; + +// The result of an attempted match. +class Result { + public: + enum class Status { + Ok = 0, + Fail, // A failure to match + BadOption, // A bad option was specified + NoRules, // No rules were specified + BadRule, // A bad rule was specified + }; + + // Constructs a result with a given status. + explicit Result(Status status) : status_(status) {} + // Constructs a result with the given message. Keeps a copy of the message. + Result(Status status, StringPiece message) + : status_(status), message_({message.begin(), message.end()}) {} + + Status status() const { return status_; } + + // Returns true if the match was successful. + operator bool() const { return status_ == Status::Ok; } + + const std::string& message() const { return message_; } + + // Sets the error message to a copy of |message|. Returns this object. + Result& SetMessage(StringPiece message) { + message_ = std::string(message.begin(), message.end()); + return *this; + } + + private: + // Status code indicating success, or kind of failure. + Status status_; + + // Message describing the failure, if any. On success, this is empty. + std::string message_; +}; + +// Returns the result of attempting to match |text| against the pattern +// program in |checks|, with the given |options|. +Result Match(StringPiece text, StringPiece checks, + const Options& options = Options()); + +} // namespace effcee + +#endif diff --git a/effcee/make_unique.h b/effcee/make_unique.h new file mode 100644 index 0000000..f0b351d --- /dev/null +++ b/effcee/make_unique.h @@ -0,0 +1,32 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef EFFCEE_MAKE_UNIQUE_H +#define EFFCEE_MAKE_UNIQUE_H + +#include <memory> + +namespace effcee { + +// Constructs an object of type T and wraps it in a std::unique_ptr. +// This functionality comes with C++14. The following is the standard +// recipe for use with C++11. +template <typename T, typename... Args> +std::unique_ptr<T> make_unique(Args&&... args) { + return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); +} + +} // namespace effcee + +#endif diff --git a/effcee/match.cc b/effcee/match.cc new file mode 100644 index 0000000..cb9517f --- /dev/null +++ b/effcee/match.cc @@ -0,0 +1,265 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <algorithm> +#include <cassert> +#include <sstream> +#include <string> +#include <vector> + +#include "check.h" +#include "cursor.h" +#include "diagnostic.h" +#include "effcee.h" +#include "to_string.h" + +using effcee::Check; +using Status = effcee::Result::Status; +using Type = effcee::Check::Type; + +namespace effcee { + +Result Match(StringPiece input, StringPiece checks, const Options& options) { + const auto& parse_result = ParseChecks(checks, options); + if (!parse_result.first) return parse_result.first; + + // A mapping from variable names to values. This is updated when a check rule + // matches a variable definition. + VarMapping vars; + + // We think of the input string as a sequence of lines that can satisfy + // the checks. Walk through the rules until no unsatisfied checks are left. + // We will erase a check when it has been satisifed. + const CheckList& pattern = parse_result.second; + assert(pattern.size() > 0); + + // What checks are resolved? Entry |i| is true when check |i| in the + // pattern is resolved. + std::vector<bool> resolved(pattern.size(), false); + + // The matching algorithm scans both the input and the pattern from start + // to finish. At the start, all checks are unresolved. We try to match + // each line in the input against the unresolved checks in a sliding window + // in the pattern. When a positive check matches, we mark it as resolved. + // When a negative check matches, the algorithm terminates with failure. + // We mark a negative check as resolved when it is the earliest unresolved + // check and the first positive check after it is resolved. + + // Initially the pattern window is just the first element. + // |first_check| is the first unresolved check. + size_t first_check = 0; + const size_t num_checks = pattern.size(); + + // The 1-based line number of the most recent successful match. + int matched_line_num = 0; + + // Set up a cursor to scan the input, and helpers for generating diagnostics. + Cursor cursor(input); + // Points to the end of the previous positive match. + StringPiece previous_match_end = input.substr(0, 0); + + // Returns a failure diagnostic without a message.; + auto fail = []() { return Diagnostic(Status::Fail); }; + // Returns a string describing the filename, line, and column of a check rule, + // including the text of the check rule and a caret pointing to the parameter + // string. + auto check_msg = [&checks, &options](StringPiece where, StringPiece message) { + std::ostringstream out; + out << options.checks_name() << LineMessage(checks, where, message); + return out.str(); + }; + // Returns a string describing the filename, line, and column of an input + // string position, including the full line containing the position, and a + // caret pointing to the position. + auto input_msg = [&input, &options](StringPiece where, StringPiece message) { + std::ostringstream out; + out << options.input_name() << LineMessage(input, where, message); + return out.str(); + }; + // Returns a string describing the value of each variable use in the + // given check, in the context of the |where| portion of the input line. + auto var_notes = [&input_msg, &vars](StringPiece where, const Check& check) { + std::ostringstream out; + for (const auto& part : check.parts()) { + const auto var_use = part->VarUseName(); + if (!var_use.empty()) { + std::ostringstream phrase; + std::string var_use_str(ToString(var_use)); + if (vars.find(var_use_str) != vars.end()) { + phrase << "note: with variable \"" << var_use << "\" equal to \"" + << vars[var_use_str] << "\""; + } else { + phrase << "note: uses undefined variable \"" << var_use << "\""; + } + out << input_msg(where, phrase.str()); + } + } + return out.str(); + }; + + // For each line. + for (; !cursor.Exhausted(); cursor.AdvanceLine()) { + // Try to match the current line against the unresolved checks. + + // The number of characters the cursor should advance to accommodate a + // recent DAG check match. + size_t deferred_advance = 0; + + bool scan_this_line = true; + while (scan_this_line) { + // Skip the initial segment of resolved checks. Slides the left end of + // the pattern window toward the right. + while (first_check < num_checks && resolved[first_check]) ++first_check; + // We've reached the end of the pattern. Declare success. + if (first_check == num_checks) return Result(Result::Status::Ok); + + size_t first_unresolved_dag = num_checks; + size_t first_unresolved_negative = num_checks; + + bool resolved_something = false; + + for (size_t i = first_check; i < num_checks; ++i) { + if (resolved[i]) continue; + + const Check& check = pattern[i]; + + if (check.type() != Type::DAG) { + cursor.Advance(deferred_advance); + deferred_advance = 0; + } + const StringPiece rest_of_line = cursor.RestOfLine(); + StringPiece unconsumed = rest_of_line; + StringPiece captured; + + if (check.Matches(&unconsumed, &captured, &vars)) { + if (check.type() == Type::Not) { + return fail() << input_msg(captured, + "error: CHECK-NOT: string occurred!") + << check_msg( + check.param(), + "note: CHECK-NOT: pattern specified here") + << var_notes(captured, check); + } + + if (check.type() == Type::Same && + cursor.line_num() != matched_line_num) { + return fail() + << check_msg(check.param(), + "error: CHECK-SAME: is not on the same line as " + "previous match") + << input_msg(captured, "note: 'next' match was here") + << input_msg(previous_match_end, + "note: previous match ended here"); + } + + if (check.type() == Type::Next) { + if (cursor.line_num() == matched_line_num) { + return fail() + << check_msg(check.param(), + "error: CHECK-NEXT: is on the same line as " + "previous match") + << input_msg(captured, "note: 'next' match was here") + << input_msg(previous_match_end, + "note: previous match ended here") + << var_notes(previous_match_end, check); + } + if (cursor.line_num() > 1 + matched_line_num) { + // This must be valid since there was an intervening line. + const auto non_match = + Cursor(input) + .Advance(previous_match_end.begin() - input.begin()) + .AdvanceLine() + .RestOfLine(); + + return fail() + << check_msg(check.param(), + "error: CHECK-NEXT: is not on the line after " + "the previous match") + << input_msg(captured, "note: 'next' match was here") + << input_msg(previous_match_end, + "note: previous match ended here") + << input_msg(non_match, + "note: non-matching line after previous " + "match is here") + << var_notes(previous_match_end, check); + } + } + + if (check.type() != Type::DAG && first_unresolved_dag < i) { + return fail() + << check_msg(pattern[first_unresolved_dag].param(), + "error: expected string not found in input") + << input_msg(previous_match_end, "note: scanning from here") + << input_msg(captured, "note: next check matches here") + << var_notes(previous_match_end, check); + } + + resolved[i] = true; + matched_line_num = cursor.line_num(); + previous_match_end = unconsumed; + resolved_something = true; + + // Resolve any prior negative checks that precede an unresolved DAG. + for (auto j = first_unresolved_negative, + limit = std::min(first_unresolved_dag, i); + j < limit; ++j) { + resolved[j] = true; + } + + // Normally advance past the matched text. But DAG checks might need + // to match out of order on the same line. So only advance for + // non-DAG cases. + + const size_t advance_proposal = + rest_of_line.size() - unconsumed.size(); + if (check.type() == Type::DAG) { + deferred_advance = std::max(deferred_advance, advance_proposal); + } else { + cursor.Advance(advance_proposal); + } + + } else { + // This line did not match the check. + if (check.type() == Type::Not) { + first_unresolved_negative = std::min(first_unresolved_negative, i); + // An unresolved Not check stops the search for more DAG checks. + if (first_unresolved_dag < num_checks) i = num_checks; + } else if (check.type() == Type::DAG) { + first_unresolved_dag = std::min(first_unresolved_dag, i); + } else { + // An unresolved non-DAG check check stops this pass over the + // checks. + i = num_checks; + } + } + } + scan_this_line = resolved_something; + } + } + + // Fail if there are any unresolved positive checks. + for (auto i = first_check; i < num_checks; ++i) { + if (resolved[i]) continue; + const auto check = pattern[i]; + if (check.type() == Type::Not) continue; + + return fail() << check_msg(check.param(), + "error: expected string not found in input") + << input_msg(previous_match_end, "note: scanning from here") + << var_notes(previous_match_end, check); + } + + return Result(Result::Status::Ok); +} +} // namespace effcee diff --git a/effcee/match_test.cc b/effcee/match_test.cc new file mode 100644 index 0000000..65350f0 --- /dev/null +++ b/effcee/match_test.cc @@ -0,0 +1,894 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "gmock/gmock.h" + +#include "effcee.h" + +namespace { + +using effcee::Match; +using effcee::Options; +using ::testing::Eq; +using ::testing::HasSubstr; + +const char* kNotFound = "error: expected string not found in input"; +const char* kMissedSame = + "error: CHECK-SAME: is not on the same line as previous match"; +const char* kNextOnSame = + "error: CHECK-NEXT: is on the same line as previous match"; +const char* kNextTooLate = + "error: CHECK-NEXT: is not on the line after the previous match"; +const char* kNotStrFound = "error: CHECK-NOT: string occurred!"; + +// Match free function + +TEST(Match, FreeFunctionLinks) { + Match("", ""); + Match("", "", effcee::Options()); +} + +// Simple checks + +TEST(Match, OneSimpleCheckPass) { + const auto result = Match("Hello", "CHECK: Hello"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, OneSimpleCheckFail) { + const auto result = Match("World", "CHECK: Hello"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK: Hello")); +} + +TEST(Match, TwoSimpleChecksPass) { + const auto result = Match("Hello\nWorld", "CHECK: Hello\nCHECK: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, RepeatedCheckFails) { + const auto result = Match("Hello\nWorld", "CHECK: Hello\nCHECK: Hello"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); +} + +TEST(Match, TwoSimpleChecksPassWithSurroundingText) { + const auto input = R"(Say + Hello + World + Today)"; + const auto result = Match(input, "CHECK: Hello\nCHECK: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoSimpleChecksPassWithInterveningText) { + const auto input = R"(Hello + Between + World)"; + const auto result = Match(input, "CHECK: Hello\nCHECK: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoSimpleChecksPassWhenInSequenceSameLine) { + const auto result = Match("HelloWorld", "CHECK: Hello\nCHECK: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoSimpleChecksFailWhenReversed) { + const auto result = Match("HelloWorld", "CHECK: World\nCHECK: Hello"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK: Hello")); +} + +TEST(Match, SimpleThenSamePasses) { + const auto result = Match("HelloWorld", "CHECK: Hello\nCHECK-SAME: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, SimpleThenSamePassesWithInterveningOnSameLine) { + const auto result = Match("Hello...World", "CHECK: Hello\nCHECK-SAME: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, SimpleThenSameFailsIfOnNextLine) { + const auto result = Match("Hello\nWorld", "CHECK: Hello\nCHECK-SAME: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(),HasSubstr(kMissedSame)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-SAME: World")); +} + +TEST(Match, SimpleThenSameFailsIfOnMuchLaterLine) { + const auto result = + Match("Hello\n\nz\n\nWorld", "CHECK: Hello\nCHECK-SAME: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kMissedSame)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-SAME: World")); +} + +TEST(Match, SimpleThenSameFailsIfNeverMatched) { + const auto result = Match("Hello\nHome", "CHECK: Hello\nCHECK-SAME: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-SAME: World")); +} + +TEST(Match, SimpleThenNextOnSameLineFails) { + const auto result = Match("HelloWorld", "CHECK: Hello\nCHECK-NEXT: World"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNextOnSame)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NEXT: World")); +} + +TEST(Match, SimpleThenNextPassesIfOnNextLine) { + const auto result = Match("Hello\nWorld", "CHECK: Hello\nCHECK-NEXT: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, SimpleThenNextFailsIfOnAfterNextLine) { + const auto result = Match("Hello\nfoo\nWorld", "CHECK: Hello\nCHECK-NEXT: World"); + EXPECT_THAT(result.message(), HasSubstr(kNextTooLate)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NEXT: World")); +} + +TEST(Match, SimpleThenNextFailsIfNeverMatched) { + const auto result = + Match("Hello\nHome", "CHECK: Hello\nCHECK-NEXT: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NEXT: World")); +} + +// TODO: CHECK-NOT + +TEST(Match, AloneNotNeverSeenPasses) { + const auto result = Match("Hello", "CHECK-NOT: Borg"); + EXPECT_TRUE(result); +} + +TEST(Match, LeadingNotNeverSeenPasses) { + const auto result = Match("Hello", "CHECK-NOT: Borg\nCHECK: Hello"); + EXPECT_TRUE(result); +} + +TEST(Match, BetweenNotNeverSeenPasses) { + const auto result = + Match("HelloWorld", "CHECK: Hello\nCHECK-NOT: Borg\nCHECK: World"); + EXPECT_TRUE(result); +} + +TEST(Match, BetweenNotDotsNeverSeenPasses) { + // The before and after matches occur on the same line. + const auto result = + Match("Hello...World", "CHECK: Hello\nCHECK-NOT: Borg\nCHECK: World"); + EXPECT_TRUE(result); +} + +TEST(Match, BetweenNotLinesNeverSeenPasses) { + // The before and after matches occur on different lines. + const auto result = + Match("Hello\nz\nWorld", "CHECK: Hello\nCHECK-NOT: Borg\nCHECK: World"); + EXPECT_TRUE(result); +} + +TEST(Match, NotBetweenMatchesPasses) { + const auto result = + Match("Hello\nWorld\nBorg\n", "CHECK: Hello\nCHECK-NOT: Borg\nCHECK: World"); + EXPECT_TRUE(result); +} + +TEST(Match, NotBeforeFirstMatchPasses) { + const auto result = + Match("Hello\nWorld\nBorg\n", "CHECK-NOT: World\nCHECK: Hello"); + EXPECT_TRUE(result); +} + +TEST(Match, NotAfterLastMatchPasses) { + const auto result = + Match("Hello\nWorld\nBorg\n", "CHECK: World\nCHECK-NOT: Hello"); + EXPECT_TRUE(result); +} + +TEST(Match, NotBeforeFirstMatchFails) { + const auto result = + Match("Hello\nWorld\n", "CHECK-NOT: Hello\nCHECK: World"); + EXPECT_FALSE(result); +} + +TEST(Match, NotBetweenMatchesFails) { + const auto result = + Match("Hello\nWorld\nBorg\n", "CHECK: Hello\nCHECK-NOT: World\nCHECK: Borg"); + EXPECT_FALSE(result); +} + +TEST(Match, NotAfterLastMatchFails) { + const auto result = + Match("Hello\nWorld\n", "CHECK: Hello\nCHECK-NOT: World"); + EXPECT_FALSE(result); +} + +TEST(Match, TrailingNotNeverSeenPasses) { + const auto result = Match("Hello", "CHECK: Hello\nCHECK-NOT: Borg"); + EXPECT_TRUE(result); +} + +TEST(Match, AloneNotSeenFails) { + const auto result = Match("Borg", "CHECK-NOT: Borg"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNotStrFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NOT: Borg")); +} + +TEST(Match, LeadingNotSeenFails) { + const auto result = Match("Borg", "CHECK-NOT: Borg\nCHECK: Hello"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNotStrFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NOT: Borg")); +} + +TEST(Match, BetweenNotSeenFails) { + const auto result = + Match("HelloBorgWorld", "CHECK: Hello\nCHECK-NOT: Borg\nCHECK: World"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNotStrFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NOT: Borg")); +} + +TEST(Match, BetweenNotDotsSeenFails) { + const auto result = + Match("Hello.Borg.World", "CHECK: Hello\nCHECK-NOT: Borg\nCHECK: World"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNotStrFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NOT: Borg")); +} + +TEST(Match, BetweenNotLinesSeenFails) { + const auto result = Match("Hello\nBorg\nWorld", + "CHECK: Hello\nCHECK-NOT: Borg\nCHECK: World"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNotStrFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NOT: Borg")); +} + +TEST(Match, TrailingNotSeenFails) { + const auto result = Match("HelloBorg", "CHECK: Hello\nCHECK-NOT: Borg"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNotStrFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NOT: Borg")); +} + +// WIP: CHECK-LABEL + +TEST(Match, OneLabelCheckPass) { + const auto result = Match("Hello", "CHECK-LABEL: Hello"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, OneLabelCheckFail) { + const auto result = Match("World", "CHECK-LABEL: Hello"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-LABEL: Hello")); +} + +TEST(Match, TwoLabelChecksPass) { + const auto result = + Match("Hello\nWorld", "CHECK-LABEL: Hello\nCHECK-LABEL: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoLabelChecksPassWithSurroundingText) { + const auto input = R"(Say + Hello + World + Today)"; + const auto result = Match(input, "CHECK-LABEL: Hello\nCHECK-LABEL: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoLabelChecksPassWithInterveningText) { + const auto input = R"(Hello + Between + World)"; + const auto result = Match(input, "CHECK-LABEL: Hello\nCHECK-LABEL: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoLabelChecksPassWhenInSequenceSameLine) { + const auto result = + Match("HelloWorld", "CHECK-LABEL: Hello\nCHECK-LABEL: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoLabelChecksFailWhenReversed) { + const auto result = + Match("HelloWorld", "CHECK-LABEL: World\nCHECK-LABEL: Hello"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-LABEL: Hello")); +} + +// WIP: Mixture of Simple and Label checks + +TEST(Match, SimpleAndLabelChecksPass) { + const auto result = Match("Hello\nWorld", "CHECK: Hello\nCHECK-LABEL: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, LabelAndSimpleChecksPass) { + const auto result = Match("Hello\nWorld", "CHECK-LABEL: Hello\nCHECK: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, SimpleAndLabelChecksFails) { + const auto result = Match("Hello\nWorld", "CHECK: Hello\nCHECK-LABEL: Band"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-LABEL: Band")); +} + +TEST(Match, LabelAndSimpleChecksFails) { + const auto result = Match("Hello\nWorld", "CHECK-LABEL: Hello\nCHECK: Band"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK: Band")); +} + +// DAG checks: Part 1: Tests simlar to simple checks tests + +TEST(Match, OneDAGCheckPass) { + const auto result = Match("Hello", "CHECK-DAG: Hello"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, OneDAGCheckFail) { + const auto result = Match("World", "CHECK-DAG: Hello"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-DAG: Hello")); +} + +TEST(Match, TwoDAGChecksPass) { + const auto result = Match("Hello\nWorld", "CHECK-DAG: Hello\nCHECK-DAG: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoDAGChecksPassWithSurroundingText) { + const auto input = R"(Say + Hello + World + Today)"; + const auto result = Match(input, "CHECK-DAG: Hello\nCHECK-DAG: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoDAGChecksPassWithInterveningText) { + const auto input = R"(Hello + Between + World)"; + const auto result = Match(input, "CHECK-DAG: Hello\nCHECK-DAG: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoDAGChecksPassWhenInSequenceSameLine) { + const auto result = Match("HelloWorld", "CHECK-DAG: Hello\nCHECK-DAG: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, DAGThenSamePasses) { + const auto result = Match("HelloWorld", "CHECK-DAG: Hello\nCHECK-SAME: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, DAGThenSamePassesWithInterveningOnSameLine) { + const auto result = Match("Hello...World", "CHECK-DAG: Hello\nCHECK-SAME: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, DAGThenSameFailsIfOnNextLine) { + const auto result = Match("Hello\nWorld", "CHECK-DAG: Hello\nCHECK-SAME: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kMissedSame)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-SAME: World")); +} + +TEST(Match, DAGThenSameFailsIfOnMuchLaterLine) { + const auto result = + Match("Hello\n\nz\n\nWorld", "CHECK-DAG: Hello\nCHECK-SAME: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kMissedSame)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-SAME: World")); +} + +TEST(Match, DAGThenSameFailsIfNeverMatched) { + const auto result = Match("Hello\nHome", "CHECK-DAG: Hello\nCHECK-SAME: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-SAME: World")); +} + +TEST(Match, DAGThenNextOnSameLineFails) { + const auto result = Match("HelloWorld", "CHECK-DAG: Hello\nCHECK-NEXT: World"); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), HasSubstr(kNextOnSame)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NEXT: World")); +} + +TEST(Match, DAGThenNextPassesIfOnNextLine) { + const auto result = Match("Hello\nWorld", "CHECK-DAG: Hello\nCHECK-NEXT: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, DAGThenNextPassesIfOnAfterNextLine) { + const auto result = Match("Hello\nWorld", "CHECK-DAG: Hello\nCHECK-NEXT: World"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, DAGThenNextFailsIfNeverMatched) { + const auto result = + Match("Hello\nHome", "CHECK-DAG: Hello\nCHECK-NEXT: World"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NEXT: World")); +} + +// DAG checks: Part 2: Out of order matching + +TEST(Match, TwoDAGMatchedOutOfOrderPasses) { + const auto result = Match("Hello\nWorld", "CHECK-DAG: World\nCHECK-DAG: Hello"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, ThreeDAGMatchedOutOfOrderPasses) { + const auto result = + Match("Hello\nWorld\nNow", + "CHECK-DAG: Now\nCHECK-DAG: World\nCHECK-DAG: Hello"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, TwoDAGChecksPassWhenReversedMatchingSameLine) { + const auto result = Match("HelloWorld", "CHECK-DAG: World\nCHECK-DAG: Hello"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, DAGChecksGreedilyConsumeInput) { + const auto result = + Match("Hello\nBlocker\nWorld\n", + "CHECK-DAG: Hello\nCHECK-DAG: World\nCHECK: Blocker"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-DAG: World")); +} + +// DAG checks: Part 3: Interaction with Not checks + +TEST(Match, DAGsAreSeparatedByNot) { + // In this case the search for "Before" consumes the entire input. + const auto result = + Match("After\nBlocker\nBefore\n", + "CHECK-DAG: Before\nCHECK-NOT: nothing\nCHECK-DAG: After"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-DAG: After")); +} + +TEST(Match, TwoDAGsAreSeparatedByNot) { + const auto result = Match("After\nApres\nBlocker\nBefore\nAnte", + "CHECK-DAG: Ante\nCHECK-DAG: Before\nCHECK-NOT: " + "nothing\nCHECK-DAG: Apres\nCHECK-DAG: After"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-DAG: Apres")); +} + +// DAG checks: Part 4: Interaction with simple checks + +TEST(Match, DAGsAreTerminatedBySimple) { + const auto result = + Match("After\nsimple\nBefore\n", + "CHECK-DAG: Before\nCHECK: simple\nCHECK-DAG: After"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-DAG: Before")); +} + +TEST(Match, TwoDAGsAreTerminatedBySimple) { + const auto result = Match("After\nApres\nBlocker\nBefore\nAnte", + "CHECK-DAG: Ante\nCHECK-DAG: Before\nCHECK: " + "Blocker\nCHECK-DAG: Apres\nCHECK-DAG: After"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr(kNotFound)); + EXPECT_THAT(result.message(), HasSubstr("CHECK-DAG: Ante")); +} + +// Test detailed message text + +TEST(Match, MessageStringNotFoundWhenNeverMatchedAnything) { + const char* input = R"(Begin +Hello + World)"; + const char* checks = R"( +Hello + ; CHECK: Needle +)"; + const char* expected = R"(chklist:3:13: error: expected string not found in input + ; CHECK: Needle + ^ +myin.txt:1:1: note: scanning from here +Begin +^ +)"; + const auto result = + Match(input, checks, + Options().SetInputName("myin.txt").SetChecksName("chklist")); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageStringNotFoundAfterInitialMatch) { + const char* input = R"(Begin +Hello + World)"; + const char* checks = R"( +Hello + ; CHECK-LABEL: Hel + ; CHECK: Needle +)"; + const char* expected = R"(chklist:4:13: error: expected string not found in input + ; CHECK: Needle + ^ +myin.txt:2:4: note: scanning from here +Hello + ^ +)"; + const auto result = + Match(input, checks, + Options().SetInputName("myin.txt").SetChecksName("chklist")); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageCheckNotStringFoundAtStart) { + const auto result = + Match(" Cheese", "CHECK-NOT: Cheese", + Options().SetInputName("in").SetChecksName("checks")); + EXPECT_FALSE(result); + const char* expected = R"(in:1:3: error: CHECK-NOT: string occurred! + Cheese + ^ +checks:1:12: note: CHECK-NOT: pattern specified here +CHECK-NOT: Cheese + ^ +)"; + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageCheckNotStringFoundAfterInitialMatch) { + const auto result = + Match("Cream Cheese", "CHECK: Cream\nCHECK-NOT: Cheese", + Options().SetInputName("in").SetChecksName("checks")); + EXPECT_FALSE(result); + const char* expected = R"(in:1:10: error: CHECK-NOT: string occurred! +Cream Cheese + ^ +checks:2:12: note: CHECK-NOT: pattern specified here +CHECK-NOT: Cheese + ^ +)"; + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageCheckSameFails) { + const char* input = R"( +Bees +Make +Delicious Honey +)"; + const char* checks = R"( +CHECK: Make +CHECK-SAME: Honey +)"; + + const auto result = Match( + input, checks, Options().SetInputName("in").SetChecksName("checks")); + EXPECT_FALSE(result); + const char* expected = R"(checks:3:13: error: CHECK-SAME: is not on the same line as previous match +CHECK-SAME: Honey + ^ +in:4:11: note: 'next' match was here +Delicious Honey + ^ +in:3:5: note: previous match ended here +Make + ^ +)"; + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageCheckNextFailsSinceOnSameLine) { + const char* input = R"( +Bees +Make +Delicious Honey +)"; + const char* checks = R"( +CHECK: Bees +CHECK-NEXT: Honey +)"; + + const auto result = Match( + input, checks, Options().SetInputName("in").SetChecksName("checks")); + EXPECT_FALSE(result); + const char* expected = R"(checks:3:13: error: CHECK-NEXT: is not on the line after the previous match +CHECK-NEXT: Honey + ^ +in:4:11: note: 'next' match was here +Delicious Honey + ^ +in:2:5: note: previous match ended here +Bees + ^ +in:3:1: note: non-matching line after previous match is here +Make +^ +)"; + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageCheckNextFailsSinceLaterLine) { + const char* input = R"( +Bees Make Delicious Honey +)"; + const char* checks = R"( +CHECK: Make +CHECK-NEXT: Honey +)"; + + const auto result = Match( + input, checks, Options().SetInputName("in").SetChecksName("checks")); + EXPECT_FALSE(result); + const char* expected = R"(checks:3:13: error: CHECK-NEXT: is on the same line as previous match +CHECK-NEXT: Honey + ^ +in:2:21: note: 'next' match was here +Bees Make Delicious Honey + ^ +in:2:10: note: previous match ended here +Bees Make Delicious Honey + ^ +)"; + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageUnresolvedDAG) { + const char* input = R"( +Bees +Make +Delicious Honey +)"; + const char* checks = R"( +CHECK: ees +CHECK-DAG: Flowers +CHECK: Honey +)"; + + const auto result = Match( + input, checks, Options().SetInputName("in").SetChecksName("checks")); + EXPECT_FALSE(result); + const char* expected = R"(checks:3:12: error: expected string not found in input +CHECK-DAG: Flowers + ^ +in:2:5: note: scanning from here +Bees + ^ +in:4:11: note: next check matches here +Delicious Honey + ^ +)"; + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + + +// Regexp + +TEST(Match, CheckRegexPass) { + const auto result = Match("Hello", "CHECK: He{{ll}}o"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, CheckRegexWithFalseStartPass) { + // This examples has three false starts. That is, we match the first + // few parts of the pattern before we finally match it. + const auto result = Match("He Hel Hell Hello Helloo", "CHECK: He{{ll}}oo"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, CheckRegexWithRangePass) { + const auto result = Match("Hello", "CHECK: He{{[a-z]+}}o"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, CheckRegexMatchesEmptyPass) { + const auto result = Match("Heo", "CHECK: He{{[a-z]*}}o"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, CheckThreeRegexPass) { + // This proves that we parsed the check correctly, finding matching pairs + // of regexp delimiters {{ and }}. + const auto result = Match("Hello World", "CHECK: He{{[a-z]+}}o{{ +}}{{[Ww]}}orld"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, CheckRegexFail) { + const auto result = Match("Heo", "CHECK: He{{[a-z]*}}o"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, MessageStringRegexRegexWithFalseStartFail) { + const char* input = "He Hel Hell Hello Hello"; + const char* checks = "CHECK: He{{ll}}oo"; + const char* expected = R"(chklist:1:8: error: expected string not found in input +CHECK: He{{ll}}oo + ^ +myin.txt:1:1: note: scanning from here +He Hel Hell Hello Hello +^ +)"; + const auto result = + Match(input, checks, + Options().SetInputName("myin.txt").SetChecksName("chklist")); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + +TEST(Match, MessageStringRegexNotFoundWhenNeverMatchedAnything) { + const char* input = R"(Begin +Hello + World)"; + const char* checks = R"( +Hello + ; CHECK: He{{[0-9]+}}llo +)"; + const char* expected = R"(chklist:3:13: error: expected string not found in input + ; CHECK: He{{[0-9]+}}llo + ^ +myin.txt:1:1: note: scanning from here +Begin +^ +)"; + const auto result = + Match(input, checks, + Options().SetInputName("myin.txt").SetChecksName("chklist")); + EXPECT_FALSE(result); + EXPECT_THAT(result.message(), Eq(expected)) << result.message(); +} + + +// Statefulness: variable definitions and uses + +TEST(Match, VarDefFollowedByUsePass) { + const auto result = + Match("Hello\nHello", "CHECK: H[[X:[a-z]+]]o\nCHECK-NEXT: H[[X]]o"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, VarDefFollowedByUseFail) { + const auto result = + Match("Hello\n\nWorld", "CHECK: H[[X:[a-z]+]]o\nCHECK: H[[X]]o"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), + HasSubstr(":2:8: error: expected string not found in input")); + EXPECT_THAT(result.message(), + HasSubstr("note: with variable \"X\" equal to \"ell\"")); +} + +TEST(Match, VarDefFollowedByUseFailAfterDAG) { + const auto result = + Match("Hello\nWorld", + "CHECK: H[[X:[a-z]+]]o\nCHECK-DAG: box[[X]]\nCHECK: H[[X]]o"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), + HasSubstr(":2:12: error: expected string not found in input")); + EXPECT_THAT(result.message(), + HasSubstr("note: with variable \"X\" equal to \"ell\"")); +} + +TEST(Match, VarDefFollowedByUseInNotCheck) { + const auto result = + Match("Hello\nHello", "CHECK: H[[X:[a-z]+]]o\nCHECK-NOT: H[[X]]o"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), HasSubstr("CHECK-NOT: string occurred")); + EXPECT_THAT(result.message(), + HasSubstr("note: with variable \"X\" equal to \"ell\"")); +} + +TEST(Match, VarDefFollowedByUseInNextCheckRightLine) { + const auto result = + Match("Hello\nHello", "CHECK: H[[X:[a-z]+]]o\nCHECK-NEXT: Blad[[X]]"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), + HasSubstr(":2:13: error: expected string not found in input")); + EXPECT_THAT(result.message(), + HasSubstr("note: with variable \"X\" equal to \"ell\"")); +} + +TEST(Match, VarDefFollowedByUseInNextCheckBadLine) { + const auto result = + Match("Hello\n\nHello", "CHECK: H[[X:[a-z]+]]o\nCHECK-NEXT: H[[X]]o"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), + HasSubstr(":2:13: error: CHECK-NEXT: is not on the line after")); + EXPECT_THAT(result.message(), + HasSubstr("note: with variable \"X\" equal to \"ell\"")); +} + +TEST(Match, UndefinedVarNeverMatches) { + const auto result = Match("Hello HeXllo", "CHECK: He[[X]]llo"); + EXPECT_FALSE(result) << result.message(); + EXPECT_THAT(result.message(), + HasSubstr("note: uses undefined variable \"X\"")); +} + +TEST(Match, NoteSeveralUndefinedVariables) { + const auto result = Match("Hello HeXllo", "CHECK: He[[X]]l[[YZ]]lo[[Q]]"); + EXPECT_FALSE(result) << result.message(); + const char* substr = R"( +<stdin>:1:1: note: uses undefined variable "X" +Hello HeXllo +^ +<stdin>:1:1: note: uses undefined variable "YZ" +Hello HeXllo +^ +<stdin>:1:1: note: uses undefined variable "Q" +Hello HeXllo +^ +)"; + EXPECT_THAT(result.message(), HasSubstr(substr)); +} + +TEST(Match, OutOfOrderDefAndUseViaDAGChecks) { + // In this example the X variable should be set to 'l', and then match + // the earlier occurrence in 'Hello'. + const auto result = Match( + "Hello\nWorld", "CHECK-DAG: Wor[[X:[a-z]+]]d\nCHECK-DAG: He[[X]]lo"); + EXPECT_FALSE(result) << result.message(); +} + +TEST(Match, VarDefRegexCountsParenthesesProperlyPass) { + const auto result = Match( + "FirstabababSecondcdcd\n1ababab2cdcd", + "CHECK: First[[X:(ab)+]]Second[[Y:(cd)+]]\nCHECK: 1[[X]]2[[Y]]"); + EXPECT_TRUE(result) << result.message(); +} + +TEST(Match, VarDefRegexCountsParenthesesProperlyFail) { + const auto result = + Match("Firstababab1abab", "CHECK: First[[X:(ab)+]]\nCHECK: 1[[X]]"); + EXPECT_FALSE(result) << result.message(); + const char* substr = R"(<stdin>:2:8: error: expected string not found in input +CHECK: 1[[X]] + ^ +<stdin>:1:12: note: scanning from here +Firstababab1abab + ^ +<stdin>:1:12: note: with variable "X" equal to "ababab" +Firstababab1abab + ^ +)"; + EXPECT_THAT(result.message(), HasSubstr(substr)); +} + +} // namespace diff --git a/effcee/options_test.cc b/effcee/options_test.cc new file mode 100644 index 0000000..efbb94b --- /dev/null +++ b/effcee/options_test.cc @@ -0,0 +1,143 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "gmock/gmock.h" + +#include "effcee.h" + +namespace { + +using effcee::Options; +using ::testing::Eq; +using ::testing::Not; + +// Options class + +// Prefix property + +TEST(Options, DefaultPrefixIsCHECK) { + EXPECT_THAT(Options().prefix(), "CHECK"); +} + +TEST(Options, SetPrefixReturnsSelf) { + Options options; + const Options& other = options.SetPrefix(""); + EXPECT_THAT(&other, &options); +} + +TEST(Options, SetPrefixOnceSetsPrefix) { + Options options; + options.SetPrefix("foo"); + EXPECT_THAT(options.prefix(), Eq("foo")); +} + +TEST(Options, SetPrefixCopiesString) { + Options options; + std::string original("foo"); + options.SetPrefix(original); + EXPECT_THAT(options.prefix().data(), Not(Eq(original.data()))); +} + +TEST(Options, SetPrefixEmptyStringPossible) { + Options options; + // This is not useful. + options.SetPrefix(""); + EXPECT_THAT(options.prefix(), Eq("")); +} + +TEST(Options, SetPrefixTwiceRetainsLastPrefix) { + Options options; + options.SetPrefix("foo"); + options.SetPrefix("bar baz"); + EXPECT_THAT(options.prefix(), Eq("bar baz")); +} + + +// Input name property + +TEST(Options, DefaultInputNameIsStdin) { + EXPECT_THAT(Options().input_name(), "<stdin>"); +} + +TEST(Options, SetInputNameReturnsSelf) { + Options options; + const Options& other = options.SetInputName(""); + EXPECT_THAT(&other, &options); +} + +TEST(Options, SetInputNameOnceSetsInputName) { + Options options; + options.SetInputName("foo"); + EXPECT_THAT(options.input_name(), Eq("foo")); +} + +TEST(Options, SetInputNameCopiesString) { + Options options; + std::string original("foo"); + options.SetInputName(original); + EXPECT_THAT(options.input_name().data(), Not(Eq(original.data()))); +} + +TEST(Options, SetInputNameEmptyStringPossible) { + Options options; + options.SetInputName(""); + EXPECT_THAT(options.input_name(), Eq("")); +} + +TEST(Options, SetInputNameTwiceRetainsLastInputName) { + Options options; + options.SetInputName("foo"); + options.SetInputName("bar baz"); + EXPECT_THAT(options.input_name(), Eq("bar baz")); +} + +// Checks name property + +TEST(Options, DefaultChecksNameIsStdin) { + EXPECT_THAT(Options().checks_name(), "<stdin>"); +} + +TEST(Options, SetChecksNameReturnsSelf) { + Options options; + const Options& other = options.SetChecksName(""); + EXPECT_THAT(&other, &options); +} + +TEST(Options, SetChecksNameOnceSetsChecksName) { + Options options; + options.SetChecksName("foo"); + EXPECT_THAT(options.checks_name(), Eq("foo")); +} + +TEST(Options, SetChecksNameCopiesString) { + Options options; + std::string original("foo"); + options.SetChecksName(original); + EXPECT_THAT(options.checks_name().data(), Not(Eq(original.data()))); +} + +TEST(Options, SetChecksNameEmptyStringPossible) { + Options options; + options.SetChecksName(""); + EXPECT_THAT(options.checks_name(), Eq("")); +} + +TEST(Options, SetChecksNameTwiceRetainsLastChecksName) { + Options options; + options.SetChecksName("foo"); + options.SetChecksName("bar baz"); + EXPECT_THAT(options.checks_name(), Eq("bar baz")); +} + +} // namespace diff --git a/effcee/result_test.cc b/effcee/result_test.cc new file mode 100644 index 0000000..32cb741 --- /dev/null +++ b/effcee/result_test.cc @@ -0,0 +1,130 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <vector> +#include "gmock/gmock.h" + +#include "effcee.h" + +namespace { + +using effcee::Result; +using ::testing::Combine; +using ::testing::Eq; +using ::testing::Not; +using ::testing::ValuesIn; + +using Status = effcee::Result::Status; + +// Result class + +// Returns a vector of all failure status values. +std::vector<Status> AllFailStatusValues() { + return {Status::NoRules, Status::BadRule}; +} + +// Returns a vector of all status values. +std::vector<Status> AllStatusValues() { + auto result = AllFailStatusValues(); + result.push_back(Status::Ok); + return result; +} + +// Test one-argument constructor. + +using ResultStatusTest = ::testing::TestWithParam<Status>; + +TEST_P(ResultStatusTest, ConstructWithAnyStatus) { + Result result(GetParam()); + EXPECT_THAT(result.status(), Eq(GetParam())); +} + +INSTANTIATE_TEST_SUITE_P(AllStatus, ResultStatusTest, + ValuesIn(AllStatusValues())); + +// Test two-argument constructor. + +using ResultStatusMessageCase = std::tuple<Status, std::string>; + +using ResultStatusMessageTest = + ::testing::TestWithParam<ResultStatusMessageCase>; + +TEST_P(ResultStatusMessageTest, ConstructWithStatusAndMessage) { + Result result(std::get<0>(GetParam()), std::get<1>(GetParam())); + EXPECT_THAT(result.status(), Eq(std::get<0>(GetParam()))); + EXPECT_THAT(result.message(), Eq(std::get<1>(GetParam()))); +} + +INSTANTIATE_TEST_SUITE_P(SampleStatusAndMessage, ResultStatusMessageTest, + Combine(ValuesIn(AllStatusValues()), + ValuesIn(std::vector<std::string>{ + "", "foo bar", "and, how!\n"}))); + +TEST(ResultConversionTest, OkStatusConvertsToTrue) { + Result result(Status::Ok); + bool as_bool = result; + EXPECT_THAT(as_bool, Eq(true)); +} + +// Test conversion to bool. + +using ResultFailConversionTest = ::testing::TestWithParam<Status>; + +TEST_P(ResultFailConversionTest, AnyFailStatusConvertsToFalse) { + Result result(GetParam()); + bool as_bool = result; + EXPECT_THAT(as_bool, Eq(false)); +} + +INSTANTIATE_TEST_SUITE_P(FailStatus, ResultFailConversionTest, + ValuesIn(AllFailStatusValues())); + +TEST(ResultMessage, SetMessageReturnsSelf) { + Result result(Status::Ok); + Result& other = result.SetMessage(""); + EXPECT_THAT(&other, Eq(&result)); +} + +TEST(ResultMessage, MessageDefaultsToEmpty) { + Result result(Status::Ok); + EXPECT_THAT(result.message(), Eq("")); +} + +TEST(ResultMessage, SetMessageOnceSetsMessage) { + Result result(Status::Ok); + result.SetMessage("foo"); + EXPECT_THAT(result.message(), Eq("foo")); +} + +TEST(ResultMessage, SetMessageCopiesString) { + Result result(Status::Ok); + std::string original("foo"); + result.SetMessage(original); + EXPECT_THAT(result.message().data(), Not(Eq(original.data()))); +} + +TEST(ResultMessage, SetMessageEmtpyStringPossible) { + Result result(Status::Ok); + result.SetMessage(""); + EXPECT_THAT(result.message(), Eq("")); +} + +TEST(ResultMessage, SetMessageTwiceRetainsLastMessage) { + Result result(Status::Ok); + result.SetMessage("foo"); + result.SetMessage("bar baz"); + EXPECT_THAT(result.message(), Eq("bar baz")); +} + +} // namespace diff --git a/effcee/to_string.h b/effcee/to_string.h new file mode 100644 index 0000000..b2ccc4f --- /dev/null +++ b/effcee/to_string.h @@ -0,0 +1,29 @@ +// Copyright 2018 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef EFFCEE_TO_STRING_H +#define EFFCEE_TO_STRING_H + +#include <string> +#include "effcee.h" + +namespace effcee { + +// Returns a copy of a StringPiece, as a std::string. +inline std::string ToString(effcee::StringPiece s) { + return std::string(s.data(), s.size()); +} +} // namespace effcee + +#endif diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..32cdd29 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,26 @@ +add_executable(effcee-example main.cc) + +target_link_libraries(effcee-example effcee) +if(UNIX AND NOT MINGW) + set_target_properties(effcee-example PROPERTIES LINK_FLAGS -pthread) +endif() +if (WIN32 AND NOT MSVC) + # For MinGW cross-compile, statically link to the C++ runtime + set_target_properties(effcee-example PROPERTIES + LINK_FLAGS "-static -static-libgcc -static-libstdc++") +endif(WIN32 AND NOT MSVC) + + +if(EFFCEE_BUILD_TESTING) + add_test(NAME effcee-example + COMMAND ${PYTHON_EXE} + effcee-example-driver.py + $<TARGET_FILE:effcee-example> + example_data.txt + "CHECK: Hello" + "CHECK-SAME: world" + "CHECK-NEXT: Bees" + "CHECK-NOT: Sting" + "CHECK: Honey" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +endif(EFFCEE_BUILD_TESTING) diff --git a/examples/effcee-example-driver.py b/examples/effcee-example-driver.py new file mode 100644 index 0000000..e1b0eff --- /dev/null +++ b/examples/effcee-example-driver.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +# Copyright 2017 The Effcee Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Execute the effcee-example program, where the arguments are +the location of the example program, the input file, and a +list of check rules to match against the input. + +Args: + effcee-example: Path to the effcee-example executable + input_file: Data file containing the input to match + check1 .. checkN: Check rules to match +""" + +import subprocess +import sys + +def main(): + cmd = sys.argv[1] + input_file = sys.argv[2] + checks = sys.argv[3:] + args = [cmd] + args.extend(checks) + print(args) + with open(input_file) as input_stream: + sys.exit(subprocess.call(args, stdin=input_stream)) + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/examples/example_data.txt b/examples/example_data.txt new file mode 100644 index 0000000..f702057 --- /dev/null +++ b/examples/example_data.txt @@ -0,0 +1,4 @@ +Hello world +Bees +Make +Delicious Honey diff --git a/examples/main.cc b/examples/main.cc new file mode 100644 index 0000000..ea0a343 --- /dev/null +++ b/examples/main.cc @@ -0,0 +1,64 @@ +// Copyright 2017 The Effcee Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <iostream> +#include <sstream> + +#include "effcee/effcee.h" + +// Checks standard input against the list of checks provided as command line +// arguments. +// +// Example: +// cat <<EOF >sample_data.txt +// Bees +// Make +// Delicious Honey +// EOF +// effcee-example <sample_data.txt "CHECK: Bees" "CHECK-NOT:Sting" "CHECK: Honey" +int main(int argc, char* argv[]) { + // Read the command arguments as a list of check rules. + std::ostringstream checks_stream; + for (int i = 1; i < argc; ++i) { + checks_stream << argv[i] << "\n"; + } + // Read stdin as the input to match. + std::stringstream input_stream; + std::cin >> input_stream.rdbuf(); + + // Attempt to match. The input and checks arguments can be provided as + // std::string or pointer to char. + auto result = effcee::Match(input_stream.str(), checks_stream.str(), + effcee::Options().SetChecksName("checks")); + + // Successful match result converts to true. + if (result) { + std::cout << "The input matched your check list!" << std::endl; + } else { + // Otherwise, you can get a status code and a detailed message. + switch (result.status()) { + case effcee::Result::Status::NoRules: + std::cout << "error: Expected check rules as command line arguments\n"; + break; + case effcee::Result::Status::Fail: + std::cout << "The input failed to match your check rules:\n"; + break; + default: + break; + } + std::cout << result.message() << std::endl; + return 1; + } + return 0; +} diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 0000000..9ef4a22 --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,44 @@ +# Suppress all warnings from third-party projects. +set_property(DIRECTORY APPEND PROPERTY COMPILE_OPTIONS -w) + +# Set alternate root directory for third party sources. +set(EFFCEE_THIRD_PARTY_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}" CACHE STRING + "Root location of all third_party projects") + +# Find googletest and gmock +if(${googletest-distribution_SOURCE_DIR}) + set(EFFCEE_GOOGLETEST_DIR "${googletest-distribution_SOURCE_DIR}" CACHE STRING + "Location of googletest source") +else() + set(EFFCEE_GOOGLETEST_DIR "${EFFCEE_THIRD_PARTY_ROOT_DIR}/googletest" CACHE STRING + "Location of googletest source") +endif() + +# Find re2 +if(RE2_SOURCE_DIR) + set(EFFCEE_RE2_DIR "${RE2_SOURCE_DIR}" CACHE STRING "Location of re2 source" FORCE) +else() + set(EFFCEE_RE2_DIR "${EFFCEE_THIRD_PARTY_ROOT_DIR}/re2" CACHE STRING + "Location of re2 source") +endif() + +# Configure third party projects. +if(EFFCEE_BUILD_TESTING) + if (NOT TARGET gmock) + if (IS_DIRECTORY ${EFFCEE_GOOGLETEST_DIR}) + add_subdirectory(${EFFCEE_GOOGLETEST_DIR} googletest EXCLUDE_FROM_ALL) + endif() + endif() + if (NOT TARGET gmock) + message(FATAL_ERROR "gmock was not found - required for tests") + endif() +endif() + +if (NOT TARGET re2) + if (IS_DIRECTORY ${EFFCEE_RE2_DIR}) + add_subdirectory(${EFFCEE_RE2_DIR} re2 EXCLUDE_FROM_ALL) + endif() +endif() +if (NOT TARGET re2) + message(FATAL_ERROR "re2 was not found - required for compilation") +endif() |