diff --git a/docs/contributors/architecture/how-rspec-works.md b/docs/contributors/architecture/how-rspec-works.md index e2c7240b..6c79e381 100644 --- a/docs/contributors/architecture/how-rspec-works.md +++ b/docs/contributors/architecture/how-rspec-works.md @@ -86,7 +86,7 @@ First, we will review several concepts in RSpec: [^fn1] The default formatter is "progress", [set in the configuration object][rspec-default-formatter-set], which maps to an instance of [`RSpec::Core::Formatters::ProgressFormatter`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/progress_formatter.rb). -- [Notifications](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb) +- **[Notifications](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb)** represent events that occur while running tests, such as "these tests failed" or "this test was skipped". @@ -114,12 +114,14 @@ First, we will review several concepts in RSpec: [^fn1] Given the above, RSpec performs the following sequence of events: + + 1. The developer adds an failing assertion to a test using the following forms (filling in ``, ``, ``, and `` appropriately): - - `expect().to ()` - - `expect { }.to ()` - - `expect().not_to ()` - - `expect { }.not_to ()` + - `expect().to ()` + - `expect { }.to ()` + - `expect().not_to ()` + - `expect { }.not_to ()` 1. The developer runs the test using the `rspec` executable. 1. The `rspec` executable [calls `RSpec::Core::Runner.invoke`][rspec-core-runner-call]. 1. Skipping a few steps, `RSpec::Core::Runner#run_specs` is called, @@ -127,30 +129,30 @@ Given the above, RSpec performs the following sequence of events: 1. Skipping a few more steps, [`RSpec::Core::Example#run` is called to run the current example][rspec-core-example-run-call]. 1. From here one of two paths is followed depending on whether the assertion is positive (`.to`) or negative (`.not_to`). - - If the assertion is positive: - 1. Within the test, - after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`, - [the `to` method calls `RSpec::Expectations::PositiveExpectationHandler.handle_matcher`][rspec-positive-expectation-handler-handle-matcher-call]. - 1. The matcher is then used to know - whether the assertion passes or fails: - `PositiveExpectationHandler` - [calls the `matches?` method on the matcher][rspec-positive-expectation-handler-matcher-matches]. - 1. Assuming that `matches?` returns false, - `PositiveExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][rspec-expectation-helper-handle-failure-call-positive], - telling it to get the positive failure message from the matcher - by calling `failure_message`. - - If the assertion is negative: - 1. Within the test, - after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`, - [the `not_to` method calls `RSpec::Expectations::NegativeExpectationHandler.handle_matcher`][rspec-negative-expectation-handler-handle-matcher-call]. - 1. The matcher is then used to know - whether the assertion passes or fails: - `NegativeExpectationHandler`, - [calls the `does_not_match?` method on the matcher][rspec-negative-expectation-handler-matcher-does-not-match]. - 1. Assuming that `does_not_match?` returns false, - `NegativeExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][via `NegativeExpectationHandler`][rspec-expectation-helper-handle-failure-call-negative], - telling it to get the negative failure message from the matcher - by calling `failure_message_when_negated`. + - If the assertion is positive: + 1. Within the test, + after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`, + [the `to` method calls `RSpec::Expectations::PositiveExpectationHandler.handle_matcher`][rspec-positive-expectation-handler-handle-matcher-call]. + 1. The matcher is then used to know + whether the assertion passes or fails: + `PositiveExpectationHandler` + [calls the `matches?` method on the matcher][rspec-positive-expectation-handler-matcher-matches]. + 1. Assuming that `matches?` returns false, + `PositiveExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][rspec-expectation-helper-handle-failure-call-positive], + telling it to get the positive failure message from the matcher + by calling `failure_message`. + - If the assertion is negative: + 1. Within the test, + after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`, + [the `not_to` method calls `RSpec::Expectations::NegativeExpectationHandler.handle_matcher`][rspec-negative-expectation-handler-handle-matcher-call]. + 1. The matcher is then used to know + whether the assertion passes or fails: + `NegativeExpectationHandler`, + [calls the `does_not_match?` method on the matcher][rspec-negative-expectation-handler-matcher-does-not-match]. + 1. Assuming that `does_not_match?` returns false, + `NegativeExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][via `NegativeExpectationHandler`][rspec-expectation-helper-handle-failure-call-negative], + telling it to get the negative failure message from the matcher + by calling `failure_message_when_negated`. 1. `RSpec::Expectations::ExpectationHelper.handle_failure` [calls `RSpec::Expectations.fail_with`][rspec-expectations-fail-with-call]. 1. `RSpec::Expectations.fail_with` [creates a diff using `RSpec::Matchers::MultiMatcherDiff`, wraps it in an exception, @@ -192,6 +194,8 @@ Given the above, RSpec performs the following sequence of events: the error and backtrace, and other pertinent details. + + [^fn1]: Note that the analysis of the RSpec source code in this document is accurate as of RSpec v3.13.0, released February 4, 2024. [rspec-exp-syntax]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/syntax.rb#L73 diff --git a/docs/contributors/architecture/how-super-diff-works.md b/docs/contributors/architecture/how-super-diff-works.md index 6a86ba85..2b6935df 100644 --- a/docs/contributors/architecture/how-super-diff-works.md +++ b/docs/contributors/architecture/how-super-diff-works.md @@ -2,17 +2,17 @@ ## SuperDiff's cast of characters -- An **inspection tree builder** (or, casually, an _inspector_) +- An **inspection tree builder** makes use of an **inspection tree** to generate a multi-line textual representation of an object, - similar to PrettyPrinter in Ruby or AwesomePrint, + similar to PrettyPrinter in the Ruby standard library or the AwesomePrint gem, but more appropriate for showing within a diff. - An **operation tree builder** makes a comparison between two objects (the "expected" vs. the "actual") - and generates an **operation tree** to represent the differences. -- An operation tree is made up of **operations**, + and generates an operation tree to represent the differences. +- An **operation tree** is made up of **operations**, which designate differences in the inner parts of the two objects. - Those differences can be of type _delete_, _insert_, or _change_. + Those differences can be of type _delete_, _insert_, _change_, or _noop_. Since objects can be nested, some operations can have children operations themselves, hence the tree. @@ -33,9 +33,9 @@ As described in ["How RSpec works"](./how-rspec-works.md#what-rspec-does), when an assertion in a test fails — -which happens when a matcher whose `matches?` method returns `false` +which happens when a matcher whose `#matches?` method returns `false` is passed to `expect(...).to`, -or when a matcher whose `does_not_match?` method returns `true` +or when a matcher whose `#does_not_match?` method returns `true` is passed to `expect(...).not_to` — RSpec will call the `RSpec::Expectations::ExpectationHelper#handle_failure` method, which will call `RSpec::Expectations.fail_with`. @@ -60,7 +60,7 @@ in order to integrate fully with RSpec. the gem needs to provide intelligent diffing for all kinds of built-in matchers. Many matchers in RSpec are marked as non-diffable — - their `diffable?` method returns `false` — + their `#diffable?` method returns `false` — causing RSpec to not show a diff after the matcher's failure message in the failure output. The `contain_exactly` matcher is one such example. @@ -88,7 +88,7 @@ Here are all of the places that SuperDiff patches RSpec: (to turn off syntax highlighting for code, as it interferes with the previous patches) - `RSpec::Support::ObjectFormatter` - (to use SuperDiff's object inspectors) + (to use SuperDiff's object inspection logic) - `RSpec::Matchers::ExpectedsForMultipleDiffs` (to add a key above the diff, add spacing around the diff, @@ -109,10 +109,11 @@ Once a test fails and RSpec delegates to SuperDiff's differ, this sequence of events occurs: -1. `SuperDiff::Differs::Main.call` is called with a pair of values: `expected` and `actual`. - This method looks for a differ that is suitable for the pair - among a set of defaults and the list of differs registered via SuperDiff's configuration. - It does this by calling `.applies_to?` on each, +1. `SuperDiff.diff` is called with a pair of values: `expected` and `actual`. + This method delegates to `SuperDiff::Core::DifferDispatcher.call`, + which looks for a differ via `SuperDiff.configuration` + which is suitable for the pair. + It does this by calling `.applies_to?` on each one, passing the `expected` and `actual`; the first differ for whom this method returns `true` wins. (This is a common pattern throughout the codebase.) @@ -121,15 +122,15 @@ this sequence of events occurs: although this is sometimes overridden. 1. Once a differ is found, its `.call` method is called. - Since all differs inherit from `SuperDiff::Differs::Base`, + Since all differs inherit from `SuperDiff::Core::AbstractDiffer`, `.call` always builds an operation tree, but the type of operation tree to build — or, more specifically, the operation tree builder subclass — is determined by the differ itself, - via the `operation_tree_builder_class` method. + via the `#operation_tree_builder_class` method. For instance, - `SuperDiff::Differs::Array` uses a `SuperDiff::OperationTreeBuilder::Array`, - `SuperDiff::Differs::Hash` uses a `SuperDiff::OperationTreeBuilder::Hash`, + `SuperDiff::Basic::Differs::Array` uses a `SuperDiff::Basic::OperationTreeBuilders::Array`, + `SuperDiff::Basic::Differs::Hash` uses a `SuperDiff::Basic::OperationTreeBuilders::Hash`, etc. 1. Once the differ has an operation tree builder, the differ calls `.call` on it @@ -140,15 +141,15 @@ this sequence of events occurs: find the differences between them, and represent those differences as operations. An operation may be one of four types: - `insert`, `delete`, `change`, or `noop`. + `:insert`, `:delete`, `:change`, or `:noop`. In the case of collections — which covers most types of values — the diff is performed recursively. This means that just as collections can have multiple levels, so too can operation trees. 1. Once the differ has an operation tree, - it then calls `to_diff` on it. - This method is defined in `SuperDiff::OperationTrees::Base`, + it then calls `#to_diff` on it. + This method is defined in `SuperDiff::Core::AbstractOperationTree`, and it starts by first flattening the tree. 1. This means that we need an operation tree flattener class. Like differs, @@ -165,12 +166,11 @@ this sequence of events occurs: 1. Once the operation tree has been flattened, then if the user has configured the gem to do so, a step is performed to look for unchanged lines - (that is, operations of type `noop`) + (that is, operations of type `:noop`) and _elide_ them — collapse them in such a way that the surrounding context is still visible. 1. Once a set of elided lines is obtained, - the operation tree runs them through a formatter — - so called `TieredLinesFormatter` — + the operation tree runs them through `SuperDiff::Core::TieredLinesFormatter`, which will add the `-`s and `+`s along with splashes of color to create the final format you see at the very end. @@ -178,9 +178,9 @@ In summary: ```mermaid graph TB - DiffersMain["Differs::Main"] -- Differs --> Differ; + DifferDispatcher -- Configured differs --> Differ; Differ -- Operation tree builder --> OperationTree[Operation tree]; OperationTree -- Operation tree flattener --> Lines; Lines -- Tiered lines elider --> ElidedLines[Elided lines]; - ElidedLines -- Tiered lines formatter --> FinalDiff[Final diff]; + ElidedLines -- Tiered lines formatter --> FinalDiff[Diff string]; ```