diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a99e244..16cd127 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,12 +16,12 @@ jobs: strategy: fail-fast: false matrix: - compiler: [native, llvm-18, gcc-14] - os: [ubuntu-latest, windows-latest, macos-13, macos-15] + compiler: [native, llvm-20, gcc-14] + os: [ubuntu-latest, windows-latest, macos-14, macos-15] exclude: - os: windows-latest compiler: gcc-14 - - os: macos-13 + - os: macos-14 compiler: native # AppleClang is too old steps: @@ -33,17 +33,22 @@ jobs: - name: Install Clang run: | - brew install llvm@18 - brew link --force --overwrite llvm@18 - echo "CC=$(brew --prefix llvm@18)/bin/clang" >> $GITHUB_ENV - echo "CXX=$(brew --prefix llvm@18)/bin/clang++" >> $GITHUB_ENV - if: ${{ matrix.compiler == 'llvm-18' && (matrix.os == 'macos-13' || matrix.os == 'macos-15') }} - - - name: Use LLVM and Clang + brew install llvm@20 + brew link --force --overwrite llvm@20 + LLVM_PREFIX=$(brew --prefix llvm@20) + echo "CC=$LLVM_PREFIX/bin/clang" >> $GITHUB_ENV + echo "CXX=$LLVM_PREFIX/bin/clang++" >> $GITHUB_ENV + echo "LDFLAGS=-L$LLVM_PREFIX/lib/c++ -Wl,-rpath,$LLVM_PREFIX/lib/c++" >> $GITHUB_ENV + if: ${{ matrix.compiler == 'llvm-20' && (matrix.os == 'macos-14' || matrix.os == 'macos-15') }} + + - name: Install LLVM and Clang (Ubuntu) run: | - echo "CC=clang-18" >> $GITHUB_ENV - echo "CXX=clang++-18" >> $GITHUB_ENV - if: ${{ matrix.compiler == 'llvm-18' && (matrix.os != 'macos-13' && matrix.os != 'macos-15') }} + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 20 + echo "CC=clang-20" >> $GITHUB_ENV + echo "CXX=clang++-20" >> $GITHUB_ENV + if: ${{ matrix.compiler == 'llvm-20' && matrix.os == 'ubuntu-latest' }} - name: Use GCC run: | @@ -53,11 +58,11 @@ jobs: - name: Configure run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES - if: ${{ matrix.compiler != 'llvm-18' || matrix.os != 'windows-latest' }} + if: ${{ matrix.compiler != 'llvm-20' || matrix.os != 'windows-latest' }} - name: Configure ClangCL run: cmake -B build -DCPPSPEC_BUILD_TESTS=YES -G "Visual Studio 17 2022" -T ClangCL - if: ${{ matrix.compiler == 'llvm-18' && matrix.os == 'windows-latest' }} + if: ${{ matrix.compiler == 'llvm-20' && matrix.os == 'windows-latest' }} - name: Build run: cmake --build build --config Release diff --git a/README.md b/README.md index 4f4a2df..a8b4960 100644 --- a/README.md +++ b/README.md @@ -12,32 +12,38 @@ See [http://cppspec.readthedocs.org/](http://cppspec.readthedocs.org/) for full ## Requirements -C++Spec requires a compiler and standard library with support for C++23: Currently tested and confirmed working are: +C++Spec requires a compiler and standard library with C++23 support. Currently tested: + - LLVM/Clang 18 (on Linux, macOS, and Windows) - GCC 14.2 (on Linux and macOS) - MSVC 19.43 (on Windows) - AppleClang 16 (on macOS) -__Note:__ Only the tests require being compiled with C++23 support (`-std=c++23`). No other part of an existing project's build must be modified. +__Note:__ Only spec files require C++23 (`-std=c++23`). No other part of an existing project's build needs modification. ## Usage -The recommended usage is as a subproject integrated into your build system. For CMake this would look something like below: + +The recommended approach is to integrate C++Spec as a CMake subproject: + ```cmake FetchContent_Declare( - c++spec + cppspec GIT_REPOSITORY https://github.com/toroidal-code/cppspec GIT_TAG VERSION ) +FetchContent_MakeAvailable(cppspec) # Or using CPM CPMAddPackage("gh:toroidal-code/cppspec@VERSION") ``` -Specs can then be automatically added as targets with +Spec files are picked up automatically with: + ```cmake discover_specs(specs_folder) ``` -This will create a separate executable for every file ending in `_spec.cpp` in the given directory (recursive) and add them to CTest. + +This creates a separate CTest executable for every file ending in `_spec.cpp` in the given directory (recursive). ## Introduction @@ -49,18 +55,18 @@ If you've ever used RSpec or Jasmine, chances are you'll be familiar with C++Spe describe order_spec("Order", $ { it("sums the prices of its line items", _ { - Order order(); + Order order; - order.add_entry(LineItem().set_item(Item() - .set_price(Money(1.11, Money::USD)) - )); + order.add_entry(LineItem().set_item(Item() + .set_price(Money(1.11, Money::USD)) + )); - order.add_entry(LineItem().set_item(Item() - .set_price(Money(1.11, Money::USD)) - .set_quantity(2) - )); + order.add_entry(LineItem().set_item(Item() + .set_price(Money(1.11, Money::USD)) + .set_quantity(2) + )); - expect(order.total()).to_equal(Money(5.55, Money::USD)); + expect(order.total()).to_equal(Money(5.55, Money::USD)); }); }); diff --git a/docs/index.md b/docs/index.md index ee45bc4..6c873cd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,18 +4,51 @@ C++Spec is a behavior-driven development library with an RSpec-inspired DSL. ------------------------------------------------------------------------------- -## Overview ## +## Overview -C++Spec is a behavior-driven development library for C++ with an RSpec-inspired DSL. Designed with ease of use and rapid prototyping in mind, C++Spec offers an alternative to traditional testing libraries and frameworks. +C++Spec is a behavior-driven development library for C++ with an RSpec-inspired DSL. Designed +with ease of use and rapid prototyping in mind, C++Spec offers an alternative to traditional +testing libraries and frameworks. -Also see the [official GitHub pages site](http://toroidal-code.github.io/cppspec/) for more +Also see the [official GitHub pages site](http://toroidal-code.github.io/cppspec/) for more information. -## Installing ## +## Requirements -Either run `git clone https://github.com/toroidal-code/cppspec.git` or download the collated header -file (if it is available). You can also download a ZIP version of the repo. +C++Spec requires a compiler and standard library with C++23 support. Currently tested: -If you want to manually generate the collated `cppspec.hpp`, you can download the ccollate tool [here](https://raw.githubusercontent.com/toroidal-code/ccollate/master/ccollate.rb) and then run `./ccollate.rb include/cppspec.hpp > cppspec.hpp` in the -toplevel directory of the C++Spec repo. A `cppspec.hpp` file will then be -available in the root of the project for copying. +- LLVM/Clang 18 (Linux, macOS, Windows) +- GCC 14.2 (Linux, macOS) +- MSVC 19.43 (Windows) +- AppleClang 16 (macOS) + +## Installing + +The recommended approach is to integrate C++Spec as a CMake subproject: + +```cmake +include(FetchContent) +FetchContent_Declare( + cppspec + GIT_REPOSITORY https://github.com/toroidal-code/cppspec + GIT_TAG VERSION +) +FetchContent_MakeAvailable(cppspec) +``` + +Spec files are picked up automatically with: + +```cmake +discover_specs(specs_folder) +``` + +This creates a separate CTest executable for every file ending in `_spec.cpp` in the given +directory (recursive). + +Alternatively, clone the repository and add the `include/` directory to your include path: + +```sh +git clone https://github.com/toroidal-code/cppspec.git +``` + +Then `#include "cppspec.hpp"` in your spec files. diff --git a/docs/syntax/before_after.md b/docs/syntax/before_after.md index e69de29..e892f20 100644 --- a/docs/syntax/before_after.md +++ b/docs/syntax/before_after.md @@ -0,0 +1,131 @@ +# before_each, after_each, before_all, after_all + +C++Spec provides four lifecycle hooks to manage setup and teardown around examples. + +## before_each + +Runs once **before every `it`** in the enclosing `describe` or `context`: + +```cpp +namespace { int n = 0; } + +describe lifecycle_spec("lifecycle", $ { + before_each([] { n = 0; }); + + it("starts at zero", _ { + expect(n).to_equal(0); + }); + + it("can be set to 5", _ { + n = 5; + expect(n).to_equal(5); + }); + + it("is zero again (reset by before_each)", _ { + expect(n).to_equal(0); + }); +}); +``` + +## after_each + +Runs once **after every `it`** in the enclosing block. Useful for releasing resources or +asserting post-conditions: + +```cpp +namespace { std::FILE* f = nullptr; } + +describe file_spec("File handle", $ { + before_each([] { f = std::tmpfile(); }); + after_each([] { if (f) { std::fclose(f); f = nullptr; } }); + + it("is open after setup", _ { + expect(f).not_().to_be_null(); + }); +}); +``` + +## before_all + +Runs **once** before the first `it` in the enclosing block. Unlike `before_each`, it does +**not** re-run between examples — mutations made inside examples persist: + +```cpp +namespace { int init_count = 0; } + +describe before_all_spec("Expensive setup", $ { + before_all([] { init_count = 42; }); + + it("sees the value set by before_all", _ { + expect(init_count).to_equal(42); + }); + + it("mutations persist (before_all does not re-run)", _ { + init_count = 99; + expect(init_count).to_equal(99); + }); + + it("sees the mutated value from the previous it", _ { + expect(init_count).to_equal(99); + }); +}); +``` + +Use `before_all` only when setup is genuinely expensive and examples do not mutate shared +state, or when mutation is intentional (as above). + +## after_all + +Runs **once** after the last `it` in the enclosing block: + +```cpp +namespace { int x = 0; } + +describe after_all_spec("Timing", $ { + context("inner context", _ { + after_all([] { x = 777; }); + + it("x is still 0 while its run", _ { expect(x).to_equal(0); }); + it("x is still 0 here too", _ { expect(x).to_equal(0); }); + // after_all fires here → x = 777 + }); + + it("outer it sees x == 777 after inner context completed", _ { + expect(x).to_equal(777); + }); +}); +``` + +## Hook inheritance in nested contexts + +Hooks defined in a parent `describe` or `context` run for all `it` blocks in child contexts +as well. Additional hooks registered in a child context run **after** the parent's, stacking +up: + +```cpp +namespace { int total = 0; } + +describe stacked_spec("Stacked hooks", $ { + before_each([] { total = 0; total += 1; }); // runs first: total = 1 + + context("inner", _ { + before_each([] { total += 10; }); // runs second: total = 11 + + it("sees 11", _ { + expect(total).to_equal(11); + }); + }); + + it("outer it sees only 1 (inner before_each does not apply)", _ { + expect(total).to_equal(1); + }); +}); +``` + +Hook execution order for a nested `it`: + +1. Parent `before_each` hooks (outermost first) +2. Child `before_each` hooks +3. The `it` body runs +4. Child `after_each` hooks +5. Parent `after_each` hooks (outermost last) diff --git a/docs/syntax/describe.md b/docs/syntax/describe.md index 1d7f386..a35b29c 100644 --- a/docs/syntax/describe.md +++ b/docs/syntax/describe.md @@ -2,61 +2,121 @@ Every test suite begins with either `describe` or `describe_a`. # describe -Describes have the form of: +`describe` creates a named test suite: ```cpp -describe example_spec("An example", $ { }); +describe example_spec("An example", $ { + // it blocks and hooks go here +}); ``` -Each `describe` is a global instance of the `Description` class, the name of the spec being the name of the global variable that the test is contained in. +Each `describe` is a global instance of `Description`. The `$` macro expands to +`[](auto& self) -> void`. Everything inside the block (`it`, `context`, `before_each`, etc.) +resolves through `self` automatically via macros, so you write them unqualified. !!! important - Take note of the `$`. This is used whenever you write a `describe` or a `describe_a`. + Use `$` for `describe` and `context` bodies. Use `_` for `it` bodies. - -In conventional C++14, after macro-expansion the above snippet would be written as: +In expanded form, the above is equivalent to: ```cpp -Description example_spec("An example", [](&self auto) { }); +Description example_spec("An example", [](auto& self) -> void { }); ``` -The `Description` constructor takes two arguments: a string, and a lambda. For simplicity's sake, any lambdas passed to any C++Spec functions are referred to as "blocks", as the capture-list and arguments of the lambda are effectively never seen. +State shared between hooks and examples can live as a local variable inside the `$` block or +as a file-scope variable in an anonymous namespace: -# describe_a +```cpp +namespace { int n = 0; } + +describe counter_spec("Counter", $ { + before_each([] { n = 0; }); -A `describe_a` is more complex than `describe`. + it("starts at zero", _ { + expect(n).to_equal(0); + }); +}); +``` + +# describe_a -Unlike `describe` which creates instances of `Description`, `describe_a` creates instances of `ClassDescription`. `ClassDescription` is a template class, where the template's type variable is used to specialize the description and create a subject available to all statements in the description. The subject is available via the `subject` keyword. +`describe_a` creates a typed test suite with a *subject* — an instance of `T` available +to all examples. The template parameter determines the subject type. ```cpp template class ClassDescription : public Description { ... }; ``` -Also unlike `describe`, there are two forms of `describe_a`: one where the subject is explicit, and another where it is implied. +## Implicit subject -## Explicit subject describe_a +When no value is provided, `T` is default-constructed: -An explicit describe_a has the subject passed into it as the first argument if there is no provided description, or after the description if there is one. +```cpp +describe_a my_spec("MyClass", $ { + it("starts in a valid state", _ { + expect(subject.is_valid()).to_be_true(); + }); +}); +``` + +## Explicit subject -For example: +Pass a value after the description string: ```cpp -describe_a tc_spec(TestClass(arg1, arg2), $ { ... }); +describe_a point_spec("Point{3,4}", Point{3.0, 4.0}, $ { + it("has length 5", _ { + expect(subject.length()).to_be_within(1e-9).of(5.0); + }); +}); ``` -and +For containers, an initializer list is also accepted: ```cpp -describe_an atc_spec - ("The class AnotherTest class", AnotherTestClass(args...), $ { ... }); +describe_a> vec_spec({1, 2, 3}, $ { + it("contains 2", _ { + expect(subject).to_contain(2); + }); +}); ``` -## Implied subject describe_a +## Accessing the subject + +Inside a `describe_a` block there are two ways to access the subject: + +**`subject` keyword** — available inside any `it` body at any nesting depth: ```cpp -describe_a yatc_spec( $ { ... }); +it("accesses the subject", _ { + expect(subject.value()).to_equal(42); +}); ``` -With an implied subject, the default constructor of the templated class is called to create the subject. +**`is_expected()`** — shorthand for `expect(subject)`, reads naturally when asserting on +the subject itself: + +```cpp +it("has the right value", _ { + is_expected().to_equal(MyClass{42}); +}); +``` + +## describe_an + +`describe_an` is an alias for `describe_a` for grammatical convenience: + +```cpp +describe_an animal_spec("Animal", $ { ... }); +``` + +# Entry point + +Each spec file needs an entry point: + +```cpp +CPPSPEC_MAIN(my_spec); // single suite +CPPSPEC_MAIN(spec_a, spec_b, spec_c); // multiple suites +``` diff --git a/docs/syntax/it.md b/docs/syntax/it.md index 65e7c75..4e01b6a 100644 --- a/docs/syntax/it.md +++ b/docs/syntax/it.md @@ -1,30 +1,76 @@ -# It -`it`s are the examples of the spec. A `Description` holds a group of `it`s, where each `it` has at least one expectation. +# it -An `it` has the form of +`it` blocks are the individual examples of a spec. Each `it` should contain at least one +expectation that verifies a specific behaviour. + +## Named it + +The most common form takes a description string and a block: ```cpp -it("description string", _ {...}) +it("returns the correct sum", _ { + expect(1 + 1).to_equal(2); +}); ``` -An `it` can also have an implicit description that is generated by the contained expectation +The description appears in test output and should read like plain English when prefixed by its +enclosing `describe` or `context` name. + +!!! important + + Use `_` for all `it` bodies. `_` expands to `[=](auto& self) mutable -> void`. + +## Anonymous it + +Omit the description to have C++Spec generate one automatically from the contained expectation: ```cpp -it(_{...}) +it(_ { + expect(answer).to_equal(42); +}); +// Output: "is expected to equal 42" ``` -!!! important +## it inside describe_a - Take note of the `_`. This is used whenever you write an `it`, similar to `$` for `describe`. +Inside a `describe_a` block, the `subject` keyword and `is_expected()` are available: +```cpp +describe_a str_spec(std::string{"hello"}, $ { + it("has the right length", _ { + expect(subject.size()).to_equal(5u); + }); -Each `it` is run as part of the containing parent `Description`, whether that be the toplevel -`describe` or an `explain`. + it(_ { + is_expected().to_start_with("hel"); + }); +}); +``` + +## specify + +`specify` is an alias for `it`: + +```cpp +specify("correct total price", _ { + expect(cart.total()).to_equal(9.99); +}); +``` -So for example, +## Nesting with context + +`it` blocks live inside `describe`, `describe_a`, or `context` blocks. `context` (and its alias +`explain`) groups related examples and can inherit lifecycle hooks from its parent: ```cpp -describe an_example_it("An example of an it", $ { - it("looks like this", {...}); +describe stack_spec("Stack", $ { + context("when empty", _ { + it("has size 0", _ { expect(stack.size()).to_equal(0); }); + }); + + context("after one push", _ { + before_each([] { stack.push(1); }); + it("has size 1", _ { expect(stack.size()).to_equal(1); }); + }); }); ``` diff --git a/docs/syntax/let.md b/docs/syntax/let.md index 00232b3..a0faf92 100644 --- a/docs/syntax/let.md +++ b/docs/syntax/let.md @@ -1,23 +1,84 @@ +# let -# Let +`let` introduces a *memoized* value inside a `describe` or `context` block. The factory +lambda is called at most once per `it` — subsequent accesses within the same example return +the cached value. The cache is cleared automatically before the next `it` runs. -A `let` introduces a memoized local variable inside of a `Description`. This is best shown in an -example: +## Basic usage ```cpp -int _count = 0; +namespace { int _count = 0; } + describe let_spec("let", $ { - let(count, [&]{ return ++_count ;}); + let(count, [] { return ++_count; }); - it("memoizes the value", _ { - expect(count).to_equal(1); + it("memoizes the value within an it", _ { expect(count).to_equal(1); + expect(count).to_equal(1); // same call — factory not invoked again }); - it("is not cached across examples", _ { - expect(count).to_equal(2); + it("resets between its", _ { + expect(count).to_equal(2); // factory called fresh for this it }); }); ``` -As shown, `let`s allow setting a mutating variable before each `it`. +`let` creates a `Let&`. Dereference it with `*` to obtain the value, or use `->` to call +members directly. For simple types, `expect(count)` resolves via the `Let&` overload of +`expect` and calls `value()` automatically. + +```cpp +let(greeting, [] { return std::string{"hello"}; }); + +it("uses arrow access", _ { + expect(greeting->size()).to_equal(5u); +}); +``` + +## Why use let instead of a plain variable? + +A plain variable shared via capture is shared across all `it` blocks — mutation in one +example leaks into the next. A `let` value is always freshly computed at the start of each +`it`, so examples remain independent. + +```cpp +// Fragile: shared variable must be reset manually +namespace { std::vector v; } +before_each([] { v = {1, 2, 3}; }); + +// Clean: automatically fresh each it +let(v, ([] { return std::vector{1, 2, 3}; })); +``` + +## let in nested contexts + +Each `context` can define its own `let` values alongside those inherited from its parent: + +```cpp +describe nested_let_spec("nested let", $ { + let(base, [] { return 10; }); + + it("outer sees base", _ { + expect(*base).to_equal(10); + }); + + context("inner", _ { + let(derived, [] { return 20; }); + + it("inner sees both", _ { + expect(*base).to_equal(10); + expect(*derived).to_equal(20); + }); + }); +}); +``` + +!!! note + + When the `let` factory lambda contains commas (e.g. an initializer list), wrap the + lambda in parentheses to prevent the preprocessor from treating the commas as macro + argument separators: + + ```cpp + let(vec, ([] { return std::vector{1, 2, 3}; })); + ``` diff --git a/docs/syntax/matchers.md b/docs/syntax/matchers.md index e69de29..a6ee7b6 100644 --- a/docs/syntax/matchers.md +++ b/docs/syntax/matchers.md @@ -0,0 +1,203 @@ +# Matchers + +Matchers are the verbs of a C++Spec expectation: `expect(actual).to_equal(expected)`. +Every matcher can be negated by inserting `.not_()` before it: + +```cpp +expect(x).to_equal(0); // passes when x == 0 +expect(x).not_().to_equal(0); // passes when x != 0 +``` + +--- + +## Equality & boolean + +### to_equal + +Compares using `operator==`. Works with any type that defines equality: + +```cpp +expect(1 + 1).to_equal(2); +expect(std::string{"hi"}).to_equal("hi"); +``` + +### to_be_true / to_be_false + +Strict boolean check (value must be exactly `true` or `false`): + +```cpp +expect(vec.empty()).to_be_true(); +expect(result.ok()).to_be_false(); +``` + +### to_be_truthy / to_be_falsy + +Checks the result of `static_cast(value)`: + +```cpp +expect(1).to_be_truthy(); +expect(0).to_be_falsy(); +expect(std::optional{42}).to_be_truthy(); +``` + +### to_be_null + +Passes when a pointer is `nullptr`: + +```cpp +int* p = nullptr; +expect(p).to_be_null(); + +int x = 1; +expect(&x).not_().to_be_null(); +``` + +--- + +## Containers + +### to_contain (single element) + +Passes when the container holds the given element (uses `std::ranges::find`): + +```cpp +expect(std::vector{1, 2, 3}).to_contain(2); +expect(std::string{"hello"}).to_contain('e'); +``` + +### to_contain (multiple elements) + +Passes when **all** listed elements are present. Negated form passes when **none** are present: + +```cpp +expect(std::vector{1, 2, 3, 4, 5}).to_contain({1, 3, 5}); // all must be in vec +expect(std::vector{1, 2, 3}).not_().to_contain({7, 8, 9}); // none may be in vec +``` + +### to_start_with + +For strings, checks the leading prefix. For containers, checks the leading sub-sequence: + +```cpp +expect(std::string{"hello world"}).to_start_with("hello"); +expect(std::vector{1, 2, 3, 4}).to_start_with({1, 2}); +``` + +### to_end_with + +For strings, checks the trailing suffix. For containers, checks the trailing sub-sequence: + +```cpp +expect(std::string{"hello world"}).to_end_with("world"); +expect(std::vector{1, 2, 3, 4}).to_end_with({3, 4}); +``` + +--- + +## Numeric + +### to_be_between + +Passes when the value falls within the given range (inclusive by default): + +```cpp +expect(5).to_be_between(1, 10); +expect(5).to_be_between(1, 10, RangeMode::exclusive); // strict bounds +``` + +### to_be_within + +Floating-point proximity check. Use `.of(expected)` to specify the target: + +```cpp +expect(3.14159).to_be_within(0.001).of(std::numbers::pi); +``` + +### to_be_less_than / to_be_greater_than + +```cpp +expect(3).to_be_less_than(5); +expect(7).to_be_greater_than(4); +``` + +--- + +## Strings + +### to_match + +Full regex match against `std::regex`. Accepts a pattern string or a pre-built `std::regex`: + +```cpp +expect(std::string{"hello123"}).to_match("[a-z]+[0-9]+"); +expect(std::string{"HELLO"}).to_match(std::regex("hello", std::regex::icase)); +``` + +Note: `to_match` uses `std::regex_match` which requires the **entire** string to match the +pattern. + +--- + +## Custom predicate + +### to_satisfy + +Accepts any callable `(const T&) -> bool`. Useful when no built-in matcher fits: + +```cpp +expect(42).to_satisfy([](int n) { return n % 2 == 0; }); +``` + +--- + +## Exceptions + +`to_throw` requires the expected value to be a **non-void callable** (use +`std::function` if needed). + +### to_throw + +Passes when the callable throws any exception: + +```cpp +std::function f = [] -> void* { throw std::runtime_error("boom"); }; +expect(f).to_throw(); +``` + +### to_throw\ + +Passes when the callable throws an exception of type `E` (or derived from `E`). Inside a +generic `_` block, use `.template to_throw()`: + +```cpp +it("catches the specific type", _ { + std::function f = [] -> void* { throw MyException{}; }; + expect(f).template to_throw(); + expect(f).template to_throw(); // base class also matches +}); +``` + +--- + +## std::optional / std::expected + +### to_have_value + +Passes when an `optional` or `expected` holds a value: + +```cpp +std::optional opt{42}; +expect(opt).to_have_value(); + +std::expected result = 42; +expect(result).to_have_value(); +``` + +### to_have_error + +Passes when a `std::expected` holds an error: + +```cpp +std::expected err = std::unexpected{"oops"}; +expect(err).to_have_error(); +``` diff --git a/docs/testing.md b/docs/testing.md index f77d37a..25ea757 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,31 +1,32 @@ # Testing -Running tests requires a `Description` object to provide the spec. Each spec is independent. +Running tests requires one or more spec objects. Each spec is independent and self-contained. -Tests are added to a `Runner` manually, which is passed a `Formatter` object on instantiation. +Specs are handed to `CppSpec::parse` which returns a runner. The runner executes all specs and +returns a `Result`. Use `CPPSPEC_MAIN` for the common single-file case. -Once a spec has been added to a `Runner`, the runner is then executed, returning a `Result` object. - -`Result` objects are able to be implicitly casted to `bool`, and can thus be used for returning -directly from the `main` function. +## Formatters +There are a number of formatter options for printing to a terminal: `verbose`, `progress`, and +`tap`. `progress` prints a series of dots while `verbose` prints a fully RSpec-like list of +tests, colouring them to show their status and result. -## Formatters +Pass a formatter to `CppSpec::parse`: -There are a number of `Formatter` subclasses for printing to a terminal, including `Verbose`, `Progress`, and `TAP`. `Progress` prints out as a serious of periods, while `Verbose` prints a -fully RSpec-like list of tests, coloring them to show their status and result. +```cpp +CppSpec::parse(argc, argv, CppSpec::Formatters::verbose) +``` ## Example -Here's an example spec and the associated runner: - ```cpp #include +#include #include "cppspec.hpp" describe strcmp_spec("int strcmp ( const char * str1, const char * str2 )", $ { - auto greater_than_zero = [](int i){return i>=0;}; - auto less_than_zero = [](int i){return i<0;}; + auto greater_than_zero = [](int i){ return i >= 0; }; + auto less_than_zero = [](int i){ return i < 0; }; it("returns 0 only when strings are equal", _ { expect(strcmp("hello", "hello")).to_equal(0); @@ -42,11 +43,5 @@ describe strcmp_spec("int strcmp ( const char * str1, const char * str2 )", $ { }); }); - -int main(){ - return CppSpec::Runner(CppSpec::Formatters::verbose) - .add_spec(strcmp_spec) - .exec() ? EXIT_SUCCESS : EXIT_FAILURE; -} - +CPPSPEC_MAIN(strcmp_spec); ``` diff --git a/gh-pages/index.md b/gh-pages/index.md index c67a330..3ccf2c0 100644 --- a/gh-pages/index.md +++ b/gh-pages/index.md @@ -5,29 +5,27 @@ profile: true C++Spec is a behavior-driven development library for C++ with an RSpec-inspired DSL. Designed with ease of use and rapid prototyping in mind, C++Spec offers an alternative to traditional testing libraries and frameworks. Some things that make C++Spec different than other testing libraries: -- A clean, readable syntax -- As few macros as possibles +- A clean, readable syntax inspired by RSpec and Jasmine - Use as a library, not a framework -- Easily extensible with custom matchers. -- Support for the RSpec and Jasmine constructs you'd expect, such as describe, context, it, expect, and let. -- Can automatically generate documentation strings based on your tests -- Cross-platform with no need to change complex build settings. +- Easily extensible with custom matchers +- Support for the constructs you'd expect: `describe`, `context`, `it`, `expect`, and `let` +- Can automatically generate description strings based on your tests +- Cross-platform with no need to change complex build settings ## An example: ```cpp -describe_a > -int_list_spec("A list of ints", {1,2,3}, $ { +describe_a> int_list_spec("A list of ints", {1, 2, 3}, $ { it("is doubly-linked", _ { expect(subject.front()).to_equal(1); expect(subject.back()).to_equal(3); }); - it(_{ is_expected().to_include(6); }); - it(_{ is_expected().to_include({1,2,3}); }); - it(_{ is_expected().not_().to_include(4); }); + it(_ { is_expected().to_contain(6); }); + it(_ { is_expected().to_contain({1, 2, 3}); }); + it(_ { is_expected().not_().to_contain(4); }); }); ``` @@ -36,28 +34,51 @@ int_list_spec("A list of ints", {1,2,3}, $ { A list of ints is doubly-linked - should include 6 -expected [1,2,3] to include 6 - should include 1, 2, and 3 - should not include 4 + is expected to contain 6 +expected [1,2,3] to contain 6 + is expected to contain 1, 2, and 3 + is expected not to contain 4 ## Usage: -Download the [header file]() and put it in your project either alongside your tests or in a folder that is in your `INCLUDE` path. Then, simply `#include "cppspec.hpp"` and you're ready to go. Both user and API documentation is available at the top of this page, and a tutorial will soon be available. +The recommended approach is to integrate C++Spec as a subproject via CMake's `FetchContent`: -## How does it work? +```cmake +FetchContent_Declare( + cppspec + GIT_REPOSITORY https://github.com/toroidal-code/cppspec + GIT_TAG VERSION +) +FetchContent_MakeAvailable(cppspec) +``` -C++Spec utilizes templated classes and functions as well as C++14 features in order to automatically deduce the types of your objects and construct your tests. +Spec files are picked up automatically with: + +```cmake +discover_specs(specs_folder) +``` + +This creates a separate CTest executable for every file ending in `_spec.cpp` in the given +directory (recursive). Full documentation is available via the links at the top of this page. + +## How does it work? -Lambdas are passed to functions (such as `context` and `it`) in order to build an execution tree at runtime. A formatter object visits each node when the tests are run and prints the status of the tests and any errors. +C++Spec uses templated classes and lambdas to build an execution tree at runtime. The `$` +and `_` macros expand to lambda literals that receive a typed `self` reference, giving `it`, +`expect`, `let`, and other DSL calls access to the enclosing description context. A formatter +visits each node when the tests run and prints the status and any failures. ## I'm getting really long compiler errors. What's going on? -Due to how the library is constructed with both templates and type-deduced (auto) lambdas, error messages from the compiler can be difficult to understand. Errors also tend to cascade and make the structures above the problem code also fail to compile, further obfuscating what's actually going wrong. +Due to the library's use of templates and type-deduced lambdas, error messages from the +compiler can be difficult to understand. Errors also tend to cascade, making structures above +the problem code fail to compile and further obscuring what is actually wrong. -Usually, the only information you want is the actual error, not all of the template substitution notes. You can reduce the template backtrace by using the flag `-ftemplate-backtrace-limit=1` when compiling with GCC and Clang. +Usually the only information you need is the actual error itself, not the template +substitution notes. You can reduce the template backtrace with the flag +`-ftemplate-backtrace-limit=1` when compiling with GCC or Clang. diff --git a/include/class_description.hpp b/include/class_description.hpp index c51c085..c616876 100755 --- a/include/class_description.hpp +++ b/include/class_description.hpp @@ -235,10 +235,10 @@ ClassContext& Description::context(std::initializer_list init_list, */ template ItCD& ClassDescription::it(const char* name, std::function&)> block, std::source_location location) { + exec_before_eaches(); auto* it = this->make_child>(location, this->subject, name, block); it->timed_run(); exec_after_eaches(); - exec_before_eaches(); return *it; } @@ -265,10 +265,10 @@ ItCD& ClassDescription::it(const char* name, std::function&)> */ template ItCD& ClassDescription::it(std::function&)> block, std::source_location location) { + exec_before_eaches(); auto* it = this->make_child>(location, this->subject, block); it->timed_run(); exec_after_eaches(); - exec_before_eaches(); return *it; } diff --git a/include/cppspec.hpp b/include/cppspec.hpp index 75c2b7d..8a05400 100755 --- a/include/cppspec.hpp +++ b/include/cppspec.hpp @@ -31,21 +31,21 @@ #define before_each self.before_each #define after_all self.after_all #define after_each self.after_each -#define let(name, body) auto(name) = self.let(body); +#define let(name, body) auto& name = self.let(body); #ifdef CPPSPEC_SEMIHOSTED -#define CPPSPEC_MAIN(spec) \ - int main(int argc, char** const argv) { \ - return CppSpec::parse(argc, argv).add_spec(spec).exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; \ - } \ - extern "C" int _getentropy(void* buf, size_t buflen) { \ - return -1; \ +#define CPPSPEC_MAIN(...) \ + int main(int argc, char** const argv) { \ + return CppSpec::parse(argc, argv).add_specs(__VA_ARGS__).exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; \ + } \ + extern "C" int _getentropy(void* buf, size_t buflen) { \ + return -1; \ } #else -#define CPPSPEC_MAIN(spec) \ - int main(int argc, char** const argv) { \ - return CppSpec::parse(argc, argv).add_spec(spec).exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; \ +#define CPPSPEC_MAIN(...) \ + int main(int argc, char** const argv) { \ + return CppSpec::parse(argc, argv).add_specs(__VA_ARGS__).exec().is_success() ? EXIT_SUCCESS : EXIT_FAILURE; \ } #endif diff --git a/include/description.hpp b/include/description.hpp index d3f03c5..dfbb003 100755 --- a/include/description.hpp +++ b/include/description.hpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include #include @@ -30,6 +32,7 @@ class Description : public Runnable { private: Block block; + std::list> owned_lets_; protected: std::string description; @@ -96,8 +99,8 @@ class Description : public Runnable { /********* Let *********/ - template - auto let(T body) -> Let; + template + auto& let(F factory); void reset_lets() noexcept; /********* Standard getters *********/ @@ -120,18 +123,18 @@ using Context = Description; /*========= Description::it =========*/ inline ItD& Description::it(const char* description, ItD::Block block, std::source_location location) { + exec_before_eaches(); auto* it = this->make_child(location, description, block); it->timed_run(); exec_after_eaches(); - exec_before_eaches(); return *it; } inline ItD& Description::it(ItD::Block block, std::source_location location) { + exec_before_eaches(); auto* it = this->make_child(location, block); it->timed_run(); exec_after_eaches(); - exec_before_eaches(); return *it; } @@ -149,15 +152,7 @@ inline Context& Description::context(const char* description, Block body, std::s /*========= Description:: each/alls =========*/ inline void Description::before_each(VoidBlock b) { - before_eaches.push_back(b); - - // Due to how lambdas and their contexts are passed around, we need to prime - // the environment by executing the before_each, so that when an 'it' - // declaration's lambda captures that env, it has the correct values for the - // variables. Truthfully, 'before_each' is a misnomer, as they are not - // getting executed directly before the lambda's execution as one might - // expect, but instead before the *next* lambda is declared. - b(); + before_eaches.push_back(std::move(b)); } inline void Description::before_all(VoidBlock b) { @@ -188,27 +183,16 @@ inline void Description::exec_after_eaches() { /*========= Description::let =========*/ -/** - * @brief Object generator for Let. - * - * @param body the body of the let statement - * - * @return a new Let object - */ -template -auto Description::let(T block) -> Let { - // In reality, this gets inlined due to the fact that it's - // a templated function. Otherwise we wouldn't be able to - // add the address of the Let, return the Let by value, - // and still be able to do a valid deference of the Let - // pointer later on when we needed to reset the Let. - - Let let(block); // Create a Let - lets.push_front(&let); // Add it to our list - return let; // Hand it object off +template +auto& Description::let(F factory) { + using T = decltype(std::declval()()); + auto ptr = std::make_unique>(std::move(factory)); + auto* raw = ptr.get(); + owned_lets_.push_back(std::move(ptr)); + lets.push_front(raw); + return *raw; } -// TODO: Should this be protected? inline void Description::reset_lets() noexcept { // For every let in our list, reset it. for (auto& let : lets) { diff --git a/include/expectations/expectation.hpp b/include/expectations/expectation.hpp index 795ae51..6985d56 100755 --- a/include/expectations/expectation.hpp +++ b/include/expectations/expectation.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -116,7 +117,9 @@ class Expectation { void to_match(std::string str, std::string msg = ""); void to_partially_match(std::regex regex, std::string msg = ""); void to_partially_match(std::string str, std::string msg = ""); - void to_satisfy(std::function /*test*/, std::string msg = ""); + template + requires std::invocable && std::convertible_to, bool> + void to_satisfy(F test, std::string msg = ""); void to_start_with(std::string start, std::string msg = ""); template @@ -355,8 +358,10 @@ void Expectation::to_partially_match(std::regex regex, std::string msg) { * @return Whether the expectation succeeds or fails. */ template -void Expectation::to_satisfy(std::function test, std::string msg) { - Matchers::Satisfy(*this, test).set_message(std::move(msg)).run(); +template + requires std::invocable && std::convertible_to, bool> +void Expectation::to_satisfy(F test, std::string msg) { + Matchers::Satisfy(*this, std::function(std::move(test))).set_message(std::move(msg)).run(); } template @@ -377,8 +382,8 @@ void Expectation::to_end_with(std::string ending, std::string msg) { template template -void Expectation::to_end_with(std::initializer_list start_sequence, std::string msg) { - Matchers::StartWith>(*this, start_sequence).set_message(std::move(msg)).run(); +void Expectation::to_end_with(std::initializer_list end_sequence, std::string msg) { + Matchers::EndWith>(*this, end_sequence).set_message(std::move(msg)).run(); } template @@ -438,7 +443,7 @@ template class ExpectationFunc : public Expectation()())> { using block_ret_t = decltype(std::declval()()); F block; - std::shared_ptr computed = nullptr; + std::optional computed = std::nullopt; public: ExpectationFunc(ExpectationFunc const& copy, std::source_location location) @@ -473,8 +478,8 @@ class ExpectationFunc : public Expectation()())> { /** @brief Get the target of the expectation. */ block_ret_t& get_target() & override { - if (computed == nullptr) { - computed = std::make_shared(block()); + if (!computed.has_value()) { + computed.emplace(block()); } return *computed; } diff --git a/include/let.hpp b/include/let.hpp index f3a05bc..015ec72 100755 --- a/include/let.hpp +++ b/include/let.hpp @@ -43,8 +43,7 @@ class Let : public LetBase { explicit Let(block_t body) noexcept : LetBase(), body(body) {} T* operator->() { - value(); - return result.operator->(); + return std::addressof(value()); } T& operator*() & { return value(); } diff --git a/include/matchers/be_nullptr.hpp b/include/matchers/be_nullptr.hpp index af0f44a..153394b 100644 --- a/include/matchers/be_nullptr.hpp +++ b/include/matchers/be_nullptr.hpp @@ -7,7 +7,7 @@ namespace CppSpec::Matchers { template -class BeNullptr : MatcherBase { +class BeNullptr : public MatcherBase { public: explicit BeNullptr(Expectation& expectation) : MatcherBase(expectation) {} diff --git a/include/matchers/satisfy.hpp b/include/matchers/satisfy.hpp index 6de1f2a..84ce6f1 100644 --- a/include/matchers/satisfy.hpp +++ b/include/matchers/satisfy.hpp @@ -33,12 +33,12 @@ class Satisfy : public MatcherBase //, BeHelpers> template std::string Satisfy::failure_message() { - return std::format("expected {} to evaluate to true", MatcherBase::actual()); + return std::format("expected {} to evaluate to true", Pretty::to_word(MatcherBase::actual())); } template std::string Satisfy::failure_message_when_negated() { - return std::format("expected {} to evaluate to false", MatcherBase::actual()); + return std::format("expected {} to evaluate to false", Pretty::to_word(MatcherBase::actual())); } template diff --git a/include/matchers/strings/end_with.hpp b/include/matchers/strings/end_with.hpp index 6331666..6bf094d 100644 --- a/include/matchers/strings/end_with.hpp +++ b/include/matchers/strings/end_with.hpp @@ -18,7 +18,7 @@ class EndWith : public MatcherBase { bool match() override { A& actual = this->actual(); E& expected = this->expected(); - return std::equal(expected.rbegin(), expected.rend(), actual.rbegin()); + return std::equal(std::ranges::rbegin(expected), std::ranges::rend(expected), std::ranges::rbegin(actual)); } }; diff --git a/include/matchers/strings/match.hpp b/include/matchers/strings/match.hpp index 6470eda..eb0fd1c 100644 --- a/include/matchers/strings/match.hpp +++ b/include/matchers/strings/match.hpp @@ -8,7 +8,7 @@ namespace CppSpec::Matchers { template -class Match : MatcherBase { +class Match : public MatcherBase { public: explicit Match(Expectation& expectation, std::string expected) : MatcherBase(expectation, std::regex(expected)) {} diff --git a/include/result.hpp b/include/result.hpp index 3badd51..7b14897 100644 --- a/include/result.hpp +++ b/include/result.hpp @@ -1,7 +1,7 @@ /** @file */ #pragma once -#include +#include #include #include #include @@ -25,17 +25,13 @@ class Result { [[nodiscard]] bool is_error() const noexcept { return status_ == Status::Error; } static Result reduce(const Result& lhs, const Result& rhs) noexcept { - if (lhs.is_failure()) { - return lhs; - } else if (rhs.is_failure()) { - return rhs; - } else if (lhs.is_success()) { - return lhs; - } else if (rhs.is_success()) { - return rhs; - } else { - return lhs; - } + if (lhs.is_failure()) return lhs; + if (rhs.is_failure()) return rhs; + if (lhs.is_error()) return lhs; + if (rhs.is_error()) return rhs; + if (lhs.is_success()) return lhs; + if (rhs.is_success()) return rhs; + return lhs; } /*--------- Location helper functions ------------*/ @@ -45,11 +41,9 @@ class Result { } [[nodiscard]] std::string get_type() const noexcept { return type; } - [[nodiscard]] std::string get_type() noexcept { return type; } void set_type(std::string type) noexcept { this->type = std::move(type); } /*--------- Message helper functions -------------*/ - std::string get_message() noexcept { return message; } [[nodiscard]] std::string get_message() const noexcept { return message; } Result& set_message(std::string message) noexcept { this->message = std::move(message); diff --git a/include/runnable.hpp b/include/runnable.hpp index 2623048..2fed767 100755 --- a/include/runnable.hpp +++ b/include/runnable.hpp @@ -99,10 +99,9 @@ class Runnable { virtual void timed_run() { using namespace std::chrono; start_time_ = system_clock::now(); - time_point start_time = high_resolution_clock::now(); + auto start = steady_clock::now(); run(); - time_point end = high_resolution_clock::now(); - runtime_ = end - start_time; + runtime_ = steady_clock::now() - start; } [[nodiscard]] std::chrono::duration get_runtime() const { return runtime_; } diff --git a/spec/context_spec.cpp b/spec/context_spec.cpp new file mode 100644 index 0000000..29c9fc5 --- /dev/null +++ b/spec/context_spec.cpp @@ -0,0 +1,141 @@ +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +namespace { +int depth_val = 0; +int prop_n = 0; +int sib_shared = 0; +int explain_v = 0; +} // namespace + +// Deep nesting: 3+ levels +describe deep_nesting_spec("deep nesting", $ { + context("level 1", _ { + depth_val = 1; + + context("level 2", _ { + depth_val = 2; + + context("level 3", _ { + depth_val = 3; + + it("is at depth 3", _ { + expect(depth_val).to_equal(3); + }); + }); + + it("is at depth 3 after level-3 context ran", _ { + expect(depth_val).to_equal(3); + }); + }); + }); +}); + +// before_each propagates through multiple levels +describe propagation_spec("before_each propagates through levels", $ { + before_each([] { prop_n = 0; }); // level 0: reset to 0 + + it("level-0 it: n == 0", _ { + expect(prop_n).to_equal(0); + }); + + context("level 1", _ { + before_each([] { prop_n += 1; }); // adds 1 after outer resets + + it("level-1 it: n == 1 (0 + 1)", _ { + expect(prop_n).to_equal(1); + }); + + context("level 2", _ { + before_each([] { prop_n += 10; }); // adds 10 after outer hooks + + it("level-2 it: n == 11 (0 + 1 + 10)", _ { + expect(prop_n).to_equal(11); + }); + + it("each level-2 it is fresh: n == 11 again", _ { + expect(prop_n).to_equal(11); + }); + }); + + it("back in level-1: n == 1 (level-2 hook not active here)", _ { + expect(prop_n).to_equal(1); + }); + }); + + it("back in level-0: n == 0", _ { + expect(prop_n).to_equal(0); + }); +}); + +// Multiple contexts at the same level are independent +describe sibling_contexts_spec("sibling contexts are independent", $ { + before_each([] { sib_shared = 100; }); + + context("context A", _ { + before_each([] { sib_shared += 1; }); + + it("A sees 101", _ { + expect(sib_shared).to_equal(101); + }); + }); + + context("context B", _ { + before_each([] { sib_shared += 2; }); + + it("B sees 102", _ { + expect(sib_shared).to_equal(102); + }); + }); + + context("context C (no extra hook)", _ { + it("C sees 100 (only outer hook)", _ { + expect(sib_shared).to_equal(100); + }); + }); +}); + +// Context with typed subject (ClassDescription created inline) +describe typed_context_spec("typed context via context(name, subject, body)", $ { + context("int subject == 42", 42, _ { + it("is_expected() returns 42", _ { + is_expected().to_equal(42); + }); + + it("subject is greater than 0", _ { + is_expected().to_be_greater_than(0); + }); + }); + + context("string subject", std::string{"cppspec"}, _ { + it("subject starts with cpp", _ { + is_expected().to_start_with("cpp"); + }); + + it("subject ends with spec", _ { + is_expected().to_end_with("spec"); + }); + }); +}); + +// explain is an alias for context +describe explain_alias_spec("explain is an alias for context", $ { + before_each([] { explain_v = 5; }); + + explain("the value", _ { + it("equals 5", _ { + expect(explain_v).to_equal(5); + }); + + it("is greater than 0", _ { + expect(explain_v).to_be_greater_than(0); + }); + }); +}); + +CPPSPEC_MAIN(deep_nesting_spec, propagation_spec, sibling_contexts_spec, + typed_context_spec, explain_alias_spec); diff --git a/spec/describe_a_advanced_spec.cpp b/spec/describe_a_advanced_spec.cpp new file mode 100644 index 0000000..0fdbaac --- /dev/null +++ b/spec/describe_a_advanced_spec.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +struct Counter { + int value = 0; + void increment() { value++; } + void reset() { value = 0; } + [[nodiscard]] bool is_zero() const { return value == 0; } +}; + +struct Point { + double x, y; + [[nodiscard]] double length() const { return std::sqrt(x * x + y * y); } + bool operator==(const Point& o) const { return x == o.x && y == o.y; } +}; + +// describe_a with implicit subject (default constructor) +describe_a counter_spec("Counter", $ { + it("default constructs to zero", _ { + expect(subject.value).to_equal(0); + expect(subject.is_zero()).to_be_true(); + }); + + it("increment increases value", _ { + Counter c; + c.increment(); + expect(c.value).to_equal(1); + expect(c.is_zero()).to_be_false(); + }); + + context("with a fresh counter for typed access", Counter{}, _ { + it("starts at zero", _ { + expect(subject.value).to_equal(0); + }); + + it("is_zero after reset is true", _ { + subject.reset(); + expect(subject.is_zero()).to_be_true(); + }); + }); +}); + +// describe_a with explicit subject +describe_a point_spec("Point{3,4}", Point{3.0, 4.0}, $ { + it("has x == 3", _ { + expect(subject.x).to_equal(3.0); + }); + + it("has y == 4", _ { + expect(subject.y).to_equal(4.0); + }); + + it("has length 5 (3-4-5 triangle)", _ { + expect(subject.length()).to_be_within(1e-9).of(5.0); + }); + + it("is_expected() refers to the subject", _ { + is_expected().to_equal(Point{3.0, 4.0}); + }); +}); + +// describe_a with initializer-list subject +describe_a> vec_spec({10, 20, 30, 40, 50}, $ { + it("contains 30", _ { + expect(subject).to_contain(30); + }); + + it("starts with {10, 20}", _ { + expect(subject).to_start_with({10, 20}); + }); + + it("ends with {40, 50}", _ { + expect(subject).to_end_with({40, 50}); + }); + + it("has size 5 via satisfy", _ { + expect(subject).to_satisfy([](const std::vector& x) { return x.size() == 5; }); + }); + + it("is_expected() also works", _ { + is_expected().to_contain(10); + }); +}); + +// describe_a with nested context: is_expected() and subject accessible +describe_a inheritance_spec("string describe_a inheritance", + std::string{"hello"}, $ { + it("starts with hel", _ { + expect(subject).to_start_with("hel"); + }); + + it("is_expected() works in plain it", _ { + is_expected().to_end_with("llo"); + is_expected().to_equal("hello"); + }); + + context("nested plain context (same subject)", _ { + it("can still use is_expected() from nested context", _ { + is_expected().to_start_with("hel"); + }); + + it("subject access also works", _ { + expect(subject).to_equal("hello"); + }); + }); +}); + +CPPSPEC_MAIN(counter_spec, point_spec, vec_spec, inheritance_spec); diff --git a/spec/describe_a_spec.cpp b/spec/describe_a_spec.cpp index d71f36d..701a5ac 100644 --- a/spec/describe_a_spec.cpp +++ b/spec/describe_a_spec.cpp @@ -41,11 +41,4 @@ describe_a describe_a_syntax_spec("describe_a syntax", $ { }); // clang-format on -int main(int argc, char** argv) { - return CppSpec::parse(argc, argv) - .add_specs(describe_a_implicit_spec, describe_a_explicit_spec, describe_a_syntax_spec) - .exec() - .is_success() - ? EXIT_SUCCESS - : EXIT_FAILURE; -} +CPPSPEC_MAIN(describe_a_implicit_spec, describe_a_explicit_spec, describe_a_syntax_spec); diff --git a/spec/dsl_spec.cpp b/spec/dsl_spec.cpp new file mode 100644 index 0000000..90a1a79 --- /dev/null +++ b/spec/dsl_spec.cpp @@ -0,0 +1,80 @@ +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +namespace { +int dsl_n = 0; +int dsl_callcount = 0; +int dsl_x = 0; +} // namespace + +// before_each deferred semantics +describe before_each_spec("before_each deferred semantics", $ { + before_each([] { dsl_n = 42; }); + + it("runs before_each before first it", _ { + expect(dsl_n).to_equal(42); + dsl_n = 99; + }); + + it("resets via before_each before second it", _ { + expect(dsl_n).to_equal(42); + }); +}); + +// let memoization and reset +describe let_spec("let memoization", $ { + let(val, [] { return ++dsl_callcount; }); + + it("memoizes within an it block", _ { + int a = *val; + int b = *val; + expect(a).to_equal(b); + }); + + it("resets between it blocks", _ { + int v = *val; + expect(v).to_equal(2); // call_count incremented once per it + }); +}); + +// context nesting +describe context_spec("context nesting", $ { + before_each([] { dsl_x = 1; }); + + it("outer it sees before_each", _ { + expect(dsl_x).to_equal(1); + }); + + context("nested context", _ { + it("inner it also sees outer before_each", _ { + expect(dsl_x).to_equal(1); + }); + }); +}); + +// describe_a typed subject +struct MyValue { + int val; + explicit MyValue(int v) : val(v) {} +}; + +describe_a describe_a_spec("describe_a", MyValue(42), $ { + it("subject is accessible via subject keyword", _ { + expect(subject.val).to_equal(42); + }); + + it("is_expected() works", _ { + expect(subject.val).to_equal(42); + }); + + context("nested context inherits subject", _ { + it("can still access subject", _ { + expect(subject.val).to_equal(42); + }); + }); +}); + +CPPSPEC_MAIN(before_each_spec, let_spec, context_spec, describe_a_spec); diff --git a/spec/let_spec.cpp b/spec/let_spec.cpp new file mode 100644 index 0000000..ec25bb8 --- /dev/null +++ b/spec/let_spec.cpp @@ -0,0 +1,117 @@ +#include +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +namespace { +int let_calls_1 = 0; +int let_a_calls = 0; +int let_b_calls = 0; +} // namespace + +// Memoization within a single it block +describe let_memoize_spec("let memoization within an it", $ { + let(expensive, [] { return ++let_calls_1; }); + + it("calls factory only once per it", _ { + int a = *expensive; + int b = *expensive; + int c = *expensive; + expect(a).to_equal(b); + expect(b).to_equal(c); + expect(let_calls_1).to_equal(1); + }); + + it("resets between its: factory called again", _ { + int v = *expensive; + expect(let_calls_1).to_equal(2); + expect(v).to_equal(2); + }); +}); + +// Multiple lets in the same describe +describe multiple_lets_spec("multiple lets in one describe", $ { + let(a, [] { return ++let_a_calls; }); + let(b, [] { return ++let_b_calls * 10; }); + + it("both lets are independent", _ { + expect(*a).to_equal(1); + expect(*b).to_equal(10); + expect(*a).to_equal(1); // memoized + expect(*b).to_equal(10); // memoized + }); + + it("both reset for next it", _ { + expect(*a).to_equal(2); + expect(*b).to_equal(20); + }); +}); + +// Let with a complex type +describe let_complex_type_spec("let with complex type", $ { + let(vec, ([] { return std::vector{1, 2, 3, 4, 5}; })); + + it("produces expected vector", _ { + expect(*vec).to_contain(3); + expect(*vec).to_start_with({1, 2}); + expect(*vec).to_end_with({4, 5}); + }); + + it("each it gets a fresh vector", _ { + vec->push_back(99); + expect(*vec).to_contain(99); + }); + + it("modifications do not persist across its", _ { + expect(*vec).not_().to_contain(99); + }); +}); + +// Let with string +describe let_string_spec("let with string", $ { + let(greeting, [] { return std::string{"hello"}; }); + + it("starts with expected prefix", _ { + expect(*greeting).to_start_with("hel"); + }); + + it("ends with expected suffix", _ { + expect(*greeting).to_end_with("llo"); + }); + + it("equality check", _ { + expect(*greeting).to_equal("hello"); + }); +}); + +// Let in nested context +describe let_nested_spec("let in nested context", $ { + let(outer_val, [] { return 100; }); + + it("outer it uses outer let", _ { + expect(*outer_val).to_equal(100); + }); + + context("nested context with its own let", _ { + let(inner_val, [] { return 200; }); + + it("inner it uses inner let", _ { + expect(*inner_val).to_equal(200); + }); + + it("inner it can also use outer let", _ { + expect(*outer_val).to_equal(100); + expect(*inner_val).to_equal(200); + }); + }); + + it("outer it after nested context: outer let still works", _ { + expect(*outer_val).to_equal(100); + }); +}); + +CPPSPEC_MAIN(let_memoize_spec, multiple_lets_spec, let_complex_type_spec, + let_string_spec, let_nested_spec); diff --git a/spec/lifecycle_spec.cpp b/spec/lifecycle_spec.cpp new file mode 100644 index 0000000..d08c390 --- /dev/null +++ b/spec/lifecycle_spec.cpp @@ -0,0 +1,141 @@ +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +namespace { +int before_each_calls = 0; +int after_each_state = 0; +int before_all_init = 0; +int after_all_x = 0; +int hook_n = 0; +int stacked_total = 0; +} // namespace + +// before_each runs once per it, not once per describe +describe before_each_ordering_spec("before_each ordering", $ { + before_each([] { before_each_calls++; }); + + it("first it: calls == 1", _ { + expect(before_each_calls).to_equal(1); + }); + + it("second it: calls == 2 (incremented again)", _ { + expect(before_each_calls).to_equal(2); + }); + + it("third it: calls == 3", _ { + expect(before_each_calls).to_equal(3); + }); +}); + +// after_each runs after every it +describe after_each_ordering_spec("after_each ordering", $ { + before_each([] { after_each_state = 10; }); + after_each([] { after_each_state = 0; }); + + it("sees state == 10 at start", _ { + expect(after_each_state).to_equal(10); + after_each_state = 99; // mutate — after_each resets it + }); + + it("sees state == 10 again (reset by after_each)", _ { + expect(after_each_state).to_equal(10); + }); + + it("state is always clean", _ { + expect(after_each_state).to_equal(10); + }); +}); + +// before_all runs exactly once at the start of the block +describe before_all_spec("before_all runs once", $ { + before_all([] { before_all_init = 42; }); + + it("sees the before_all value", _ { + expect(before_all_init).to_equal(42); + }); + + it("value is not reset between its", _ { + expect(before_all_init).to_equal(42); + before_all_init = 99; + }); + + it("now sees the modified value (before_all did NOT re-run)", _ { + expect(before_all_init).to_equal(99); + }); +}); + +// after_all fires after all its in a context are done +describe after_all_timing_spec("after_all timing", $ { + context("inner context with after_all", _ { + after_all([] { after_all_x = 777; }); + + it("runs while x is still 0", _ { + expect(after_all_x).to_equal(0); + }); + + it("still 0 before after_all fires", _ { + expect(after_all_x).to_equal(0); + }); + // after_all fires here + }); + + it("outer it sees x == 777 after inner after_all ran", _ { + expect(after_all_x).to_equal(777); + }); +}); + +// hooks propagate into nested contexts +describe hook_propagation_spec("hook propagation into nested contexts", $ { + before_each([] { hook_n = 1; }); + after_each([] { hook_n = 0; }); + + it("outer it sees n == 1", _ { + expect(hook_n).to_equal(1); + }); + + context("nested context inherits outer hooks", _ { + it("inner it also sees n == 1 from outer before_each", _ { + expect(hook_n).to_equal(1); + }); + + it("inner it gets fresh n each time", _ { + expect(hook_n).to_equal(1); + hook_n = 99; + }); + + it("after_each reset: n is 1 again", _ { + expect(hook_n).to_equal(1); + }); + }); + + it("outer it after nested still gets n == 1", _ { + expect(hook_n).to_equal(1); + }); +}); + +// additional before_each in nested context stacks on top of parent +describe stacked_hooks_spec("stacked hooks in nested contexts", $ { + before_each([] { stacked_total = 0; stacked_total += 1; }); // outer: reset then add 1 + + context("inner adds more", _ { + before_each([] { stacked_total += 10; }); // runs after outer: 0 + 1 + 10 = 11 + + it("sees total == 11 (1 from outer + 10 from inner)", _ { + expect(stacked_total).to_equal(11); + }); + + it("each it starts fresh: total == 11 again", _ { + expect(stacked_total).to_equal(11); + }); + }); + + it("outer it only gets outer before_each: total == 1", _ { + expect(stacked_total).to_equal(1); + }); +}); + +CPPSPEC_MAIN(before_each_ordering_spec, after_each_ordering_spec, before_all_spec, + after_all_timing_spec, hook_propagation_spec, stacked_hooks_spec); diff --git a/spec/matchers/contain_spec.cpp b/spec/matchers/contain_spec.cpp new file mode 100644 index 0000000..dcb80b8 --- /dev/null +++ b/spec/matchers/contain_spec.cpp @@ -0,0 +1,85 @@ +#include +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +describe contain_spec("to_contain matcher", $ { + context("with a single element", _ { + it("passes when element is in vector", _ { + expect(std::vector{1, 2, 3}).to_contain(2); + }); + + it("fails when element is not in vector", _ { + expect(std::vector{1, 2, 3}).not_().to_contain(9); + }); + + it("passes when element is in list", _ { + expect(std::list{10, 20, 30}).to_contain(20); + }); + + it("fails when element is not in list", _ { + expect(std::list{10, 20, 30}).not_().to_contain(99); + }); + + it("passes for a string containing a char", _ { + expect(std::string{"hello"}).to_contain('e'); + }); + + it("fails when char not in string", _ { + expect(std::string{"hello"}).not_().to_contain('z'); + }); + }); + + context("with multiple elements (initializer list)", _ { + it("passes when all elements are present", _ { + expect(std::vector{1, 2, 3, 4, 5}).to_contain({1, 3, 5}); + }); + + it("not_().to_contain passes when none of the elements are present", _ { + expect(std::vector{1, 2, 3}).not_().to_contain({7, 9}); + }); + + it("passes for a single-element list", _ { + expect(std::vector{5, 6, 7}).to_contain({6}); + }); + }); + + context("with initializer-list actual", _ { + it("passes when element found", _ { + expect({10, 20, 30}).to_contain(20); + }); + + it("passes for subset", _ { + expect({1, 2, 3, 4}).to_contain({2, 4}); + }); + + it("fails when subset missing", _ { + expect({1, 2, 3}).not_().to_contain({4, 5}); + }); + }); + + context("negation", _ { + it("not_().to_contain passes when all items absent", _ { + expect(std::vector{1, 2, 3}).not_().to_contain({7, 8, 9}); + }); + + it("passes not_() when no items found", _ { + expect(std::vector{1, 2, 3}).not_().to_contain({4, 5, 6}); + }); + }); + + context("with strings", _ { + it("checks if string vector contains a string", _ { + expect(std::vector{"foo", "bar", "baz"}).to_contain(std::string{"bar"}); + }); + + it("negated passes when string absent", _ { + expect(std::vector{"foo", "bar"}).not_().to_contain(std::string{"quux"}); + }); + }); +}); + +CPPSPEC_MAIN(contain_spec); diff --git a/spec/matchers/expected_spec.cpp b/spec/matchers/expected_spec.cpp new file mode 100644 index 0000000..794c436 --- /dev/null +++ b/spec/matchers/expected_spec.cpp @@ -0,0 +1,90 @@ +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +struct ParseError { std::string msg; }; +struct NetworkError { int code; }; + +describe expected_spec("to_have_value and to_have_error matchers", $ { + context("std::optional", _ { + it("to_have_value passes when optional has a value", _ { + std::optional opt{42}; + expect(opt).to_have_value(); + }); + + it("to_have_value fails when optional is empty", _ { + std::optional opt{}; + expect(opt).not_().to_have_value(); + }); + + it("can access the value after to_have_value", _ { + std::optional opt{"hello"}; + expect(opt).to_have_value(); + expect(opt.value()).to_equal("hello"); + }); + + it("optional is empty", _ { + std::optional opt{}; + expect(opt).not_().to_have_value(); + }); + }); + + context("std::expected with value", _ { + it("to_have_value passes when expected contains a value", _ { + std::expected e{42}; + expect(e).to_have_value(); + }); + + it("not_().to_have_error passes when value present", _ { + std::expected e{99}; + expect(e).not_().to_have_error(); + }); + + it("to_have_value passes for string value", _ { + std::expected e{"hello"}; + expect(e).to_have_value(); + }); + }); + + context("std::expected with error", _ { + it("to_have_error passes when expected contains an error", _ { + std::expected e{std::unexpected(ParseError{"bad input"})}; + expect(e).to_have_error(); + }); + + it("not_().to_have_error passes when value present", _ { + std::expected e{7}; + expect(e).not_().to_have_error(); + }); + + it("to_have_value fails when error present", _ { + std::expected e{std::unexpected(NetworkError{404})}; + expect(e).not_().to_have_value(); + }); + + it("to_have_error passes for network error", _ { + std::expected e{std::unexpected(NetworkError{500})}; + expect(e).to_have_error(); + }); + }); + + context("chaining with value checks", _ { + it("can check value contents after confirming has_value", _ { + std::expected e{123}; + expect(e).to_have_value(); + expect(e.value()).to_equal(123); + }); + + it("can check multiple expectations on same optional", _ { + std::optional opt{3.14}; + expect(opt).to_have_value(); + expect(opt.value()).to_be_greater_than(3.0); + expect(opt.value()).to_be_less_than(4.0); + }); + }); +}); + +CPPSPEC_MAIN(expected_spec); diff --git a/spec/matchers/null_spec.cpp b/spec/matchers/null_spec.cpp new file mode 100644 index 0000000..0a11347 --- /dev/null +++ b/spec/matchers/null_spec.cpp @@ -0,0 +1,57 @@ +#include "cppspec.hpp" + +using namespace CppSpec; + +describe null_spec("to_be_null matcher", $ { + context("with raw pointers", _ { + it("passes when pointer is null", _ { + int* p = nullptr; + expect(p).to_be_null(); + }); + + it("fails when pointer is non-null", _ { + int x = 42; + int* p = &x; + expect(p).not_().to_be_null(); + }); + + it("passes not_().to_be_null() for valid pointer", _ { + double d = 3.14; + expect(&d).not_().to_be_null(); + }); + }); + + context("with void pointer", _ { + it("passes for null void pointer", _ { + void* p = nullptr; + expect(p).to_be_null(); + }); + + it("fails for non-null void pointer", _ { + int x = 0; + void* p = &x; + expect(p).not_().to_be_null(); + }); + }); + + context("to_be_truthy and to_be_falsy (numeric types)", _ { + it("zero int is falsy", _ { + expect(0).to_be_falsy(); + }); + + it("non-zero int is truthy", _ { + expect(1).to_be_truthy(); + expect(-1).to_be_truthy(); + }); + + it("zero double is falsy", _ { + expect(0.0).to_be_falsy(); + }); + + it("non-zero double is truthy", _ { + expect(3.14).to_be_truthy(); + }); + }); +}); + +CPPSPEC_MAIN(null_spec); diff --git a/spec/matchers/numeric_spec.cpp b/spec/matchers/numeric_spec.cpp new file mode 100644 index 0000000..e203d5d --- /dev/null +++ b/spec/matchers/numeric_spec.cpp @@ -0,0 +1,77 @@ +#include "cppspec.hpp" + +using namespace CppSpec; + +describe numeric_spec("numeric comparison matchers", $ { + context("to_be_greater_than", _ { + it("passes when actual > expected (int)", _ { + expect(5).to_be_greater_than(3); + }); + + it("fails when actual == expected", _ { + expect(3).not_().to_be_greater_than(3); + }); + + it("fails when actual < expected", _ { + expect(1).not_().to_be_greater_than(5); + }); + + it("works with doubles", _ { + expect(3.14).to_be_greater_than(3.0); + }); + + it("works with negative numbers", _ { + expect(-1).to_be_greater_than(-5); + }); + + it("works with zero boundary", _ { + expect(0).not_().to_be_greater_than(0); + expect(1).to_be_greater_than(0); + expect(-1).not_().to_be_greater_than(0); + }); + }); + + context("to_be_less_than", _ { + it("passes when actual < expected (int)", _ { + expect(2).to_be_less_than(5); + }); + + it("fails when actual == expected", _ { + expect(5).not_().to_be_less_than(5); + }); + + it("fails when actual > expected", _ { + expect(9).not_().to_be_less_than(4); + }); + + it("works with doubles", _ { + expect(2.71).to_be_less_than(3.14); + }); + + it("works with negative numbers", _ { + expect(-10).to_be_less_than(-1); + }); + + it("works with zero boundary", _ { + expect(0).not_().to_be_less_than(0); + expect(-1).to_be_less_than(0); + expect(1).not_().to_be_less_than(0); + }); + }); + + context("combined usage", _ { + it("value is between two bounds via two expectations", _ { + int v = 7; + expect(v).to_be_greater_than(5); + expect(v).to_be_less_than(10); + }); + + it("float comparison chain", _ { + double pi = 3.14159; + expect(pi).to_be_greater_than(3.0); + expect(pi).to_be_less_than(4.0); + }); + }); +}); + +CPPSPEC_MAIN(numeric_spec); diff --git a/spec/matchers/satisfy_spec.cpp b/spec/matchers/satisfy_spec.cpp new file mode 100644 index 0000000..ec9e93a --- /dev/null +++ b/spec/matchers/satisfy_spec.cpp @@ -0,0 +1,79 @@ +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +describe satisfy_spec("to_satisfy matcher", $ { + context("basic predicate", _ { + it("passes when predicate returns true", _ { + expect(42).to_satisfy([](int x) { return x > 0; }); + }); + + it("fails when predicate returns false", _ { + expect(-1).not_().to_satisfy([](int x) { return x > 0; }); + }); + + it("works with equality predicate", _ { + expect(7).to_satisfy([](int x) { return x == 7; }); + }); + + it("works with modulo check", _ { + expect(6).to_satisfy([](int x) { return x % 2 == 0; }); + }); + + it("works with odd check negated", _ { + expect(7).not_().to_satisfy([](int x) { return x % 2 == 0; }); + }); + }); + + context("with floating point", _ { + it("checks range via predicate", _ { + expect(3.14).to_satisfy([](double x) { return x > 3.0 && x < 3.2; }); + }); + + it("checks NaN", _ { + expect(std::numeric_limits::quiet_NaN()) + .to_satisfy([](double x) { return std::isnan(x); }); + }); + }); + + context("with strings", _ { + it("checks non-empty", _ { + expect(std::string{"hello"}) + .to_satisfy([](const std::string& s) { return !s.empty(); }); + }); + + it("checks prefix", _ { + expect(std::string{"hello world"}) + .to_satisfy([](const std::string& s) { return s.substr(0, 5) == "hello"; }); + }); + + it("negated: fails when predicate is true", _ { + expect(std::string{""}) + .not_().to_satisfy([](const std::string& s) { return !s.empty(); }); + }); + }); + + context("with booleans", _ { + it("true satisfies identity predicate", _ { + expect(true).to_satisfy([](bool b) { return b; }); + }); + + it("false does not satisfy identity predicate", _ { + expect(false).not_().to_satisfy([](bool b) { return b; }); + }); + }); + + int threshold = 10; + + context("with captured variables", _ { + it("captures threshold correctly", _ { + expect(15).to_satisfy([threshold](int x) { return x > threshold; }); + expect(5).not_().to_satisfy([threshold](int x) { return x > threshold; }); + }); + }); +}); + +CPPSPEC_MAIN(satisfy_spec); diff --git a/spec/matchers/string_spec.cpp b/spec/matchers/string_spec.cpp new file mode 100644 index 0000000..a2ddd58 --- /dev/null +++ b/spec/matchers/string_spec.cpp @@ -0,0 +1,119 @@ +#include +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +describe string_spec("string matchers", $ { + context("to_start_with (string)", _ { + it("passes when string starts with prefix", _ { + expect(std::string{"hello world"}).to_start_with("hello"); + }); + + it("passes when prefix equals the full string", _ { + expect(std::string{"hi"}).to_start_with("hi"); + }); + + it("fails when string does not start with prefix", _ { + expect(std::string{"hello"}).not_().to_start_with("world"); + }); + + it("fails on partial mismatch at start", _ { + expect(std::string{"goodbye"}).not_().to_start_with("hello"); + }); + + it("passes with empty prefix", _ { + expect(std::string{"anything"}).to_start_with(""); + }); + }); + + context("to_end_with (string)", _ { + it("passes when string ends with suffix", _ { + expect(std::string{"hello world"}).to_end_with("world"); + }); + + it("passes when suffix equals the full string", _ { + expect(std::string{"hi"}).to_end_with("hi"); + }); + + it("fails when string does not end with suffix", _ { + expect(std::string{"hello"}).not_().to_end_with("world"); + }); + + it("fails on partial suffix mismatch", _ { + expect(std::string{"foobar"}).not_().to_end_with("baz"); + }); + + it("passes with empty suffix", _ { + expect(std::string{"anything"}).to_end_with(""); + }); + }); + + context("to_start_with (container sequences)", _ { + it("vector starts with sub-sequence", _ { + expect(std::vector{1, 2, 3, 4, 5}).to_start_with({1, 2, 3}); + }); + + it("fails when sub-sequence does not match start", _ { + expect(std::vector{1, 2, 3}).not_().to_start_with({2, 3}); + }); + + it("passes for single-element prefix", _ { + expect(std::vector{7, 8, 9}).to_start_with({7}); + }); + }); + + context("to_end_with (container sequences)", _ { + it("vector ends with sub-sequence", _ { + expect(std::vector{1, 2, 3, 4, 5}).to_end_with({3, 4, 5}); + }); + + it("fails when sub-sequence does not match end", _ { + expect(std::vector{1, 2, 3}).not_().to_end_with({1, 2}); + }); + + it("passes for single-element suffix", _ { + expect(std::vector{7, 8, 9}).to_end_with({9}); + }); + }); + + context("to_match (full regex)", _ { + it("matches an exact string pattern", _ { + expect(std::string{"hello"}).to_match("hello"); + }); + + it("matches with wildcard pattern", _ { + expect(std::string{"hello"}).to_match("hel.*"); + }); + + it("matches digit pattern", _ { + expect(std::string{"12345"}).to_match("[0-9]+"); + }); + + it("fails when pattern does not match", _ { + expect(std::string{"hello"}).not_().to_match("world"); + }); + + it("fails when partial match only (regex_match is full)", _ { + expect(std::string{"hello"}).not_().to_match("hel"); + }); + + it("matches case-insensitively with flag", _ { + expect(std::string{"HELLO"}).to_match(std::regex("hello", std::regex::icase)); + }); + }); + + context("string to_contain (char)", _ { + it("finds a character in a string", _ { + expect(std::string{"abcde"}).to_contain('c'); + }); + + it("fails when char absent", _ { + expect(std::string{"abcde"}).not_().to_contain('z'); + }); + }); +}); + +CPPSPEC_MAIN(string_spec); diff --git a/spec/matchers/throw_spec.cpp b/spec/matchers/throw_spec.cpp new file mode 100644 index 0000000..3b91a1e --- /dev/null +++ b/spec/matchers/throw_spec.cpp @@ -0,0 +1,85 @@ +#include +#include + +#include "cppspec.hpp" + +using namespace CppSpec; + +struct MyException : public std::exception { + [[nodiscard]] const char* what() const noexcept override { return "MyException"; } +}; + +struct OtherException : public std::exception { + [[nodiscard]] const char* what() const noexcept override { return "OtherException"; } +}; + +describe throw_spec("to_throw matcher", $ { + context("basic throw detection", _ { + it("passes when function throws std::exception", _ { + std::function f = [] -> void* { throw std::runtime_error("boom"); }; + expect(f).to_throw(); + }); + + it("fails when function does not throw", _ { + std::function f = [] { return 42; }; + expect(f).not_().to_throw(); + }); + + it("passes when lambda throws int", _ { + std::function f = [] -> void* { throw 42; }; + expect(f).template to_throw(); + }); + }); + + context("typed exception matching", _ { + it("catches specific exception type", _ { + std::function f = [] -> void* { throw MyException{}; }; + expect(f).template to_throw(); + }); + + it("catches base class when derived thrown", _ { + std::function f = [] -> void* { throw MyException{}; }; + expect(f).template to_throw(); + }); + + it("does not catch wrong exception type", _ { + std::function f = [] -> void* { throw OtherException{}; }; + expect(f).not_().template to_throw(); + }); + + it("catches std::runtime_error specifically", _ { + std::function f = [] -> void* { throw std::runtime_error("err"); }; + expect(f).template to_throw(); + }); + + it("catches std::logic_error specifically", _ { + std::function f = [] -> void* { throw std::logic_error("logic"); }; + expect(f).template to_throw(); + }); + }); + + context("non-throwing functions", _ { + it("passes not_().to_throw() for a returning function", _ { + std::function f = [] { return 1 + 1; }; + expect(f).not_().to_throw(); + }); + + it("does not throw for a constant function", _ { + std::function f = [] { return 0; }; + expect(f).not_().to_throw(); + }); + }); + + context("functions with side effects before throwing", _ { + it("still detects throw after side effects", _ { + int x = 0; + std::function f = [&x] -> void* { + x = 1; + throw std::runtime_error("after side effect"); + }; + expect(f).to_throw(); + }); + }); +}); + +CPPSPEC_MAIN(throw_spec);