Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct references in architecture docs #233

Merged
merged 2 commits into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 33 additions & 29 deletions docs/contributors/architecture/how-rspec-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down Expand Up @@ -114,43 +114,45 @@ First, we will review several concepts in RSpec: [^fn1]

Given the above, RSpec performs the following sequence of events:

<!--prettier-ignore-start -->

1. The developer adds an failing assertion to a test using the following forms
(filling in `<actual value>`, `<matcher>`, `<block>`, and `<args...>` appropriately):
- `expect(<actual value>).to <matcher>(<args...>)`
- `expect { <block> }.to <matcher>(<args...>)`
- `expect(<actual value>).not_to <matcher>(<args...>)`
- `expect { <block> }.not_to <matcher>(<args...>)`
- `expect(<actual value>).to <matcher>(<args...>)`
- `expect { <block> }.to <matcher>(<args...>)`
- `expect(<actual value>).not_to <matcher>(<args...>)`
- `expect { <block> }.not_to <matcher>(<args...>)`
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,
which [runs all tests by surrounding them in a call to `RSpec::Core::Reporter#report`][rspec-reporter-report-call].
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,
Expand Down Expand Up @@ -192,6 +194,8 @@ Given the above, RSpec performs the following sequence of events:
the error and backtrace,
and other pertinent details.

<!--prettier-ignore-end -->

[^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
Expand Down
50 changes: 25 additions & 25 deletions docs/contributors/architecture/how-super-diff-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`.
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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.)
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -165,22 +166,21 @@ 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.

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];
```
Loading