Skip to content

Commit

Permalink
docs(all): improve documentation and add Ruby 3.1 support
Browse files Browse the repository at this point in the history
- Add comprehensive API documentation with examples

- Clarify different result types and states

- Add integration guide and usage examples

- Document support for Ruby 3.1
  • Loading branch information
cyril committed Dec 31, 2024
1 parent b7efde1 commit 5d45b72
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 216 deletions.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jobs:
strategy:
matrix:
ruby:
- 3.1
- 3.2
- 3.3
- 3.4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2
ruby-version: 3.1
bundler-cache: true

- name: Run the RuboCop task
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ inherit_mode:

AllCops:
NewCops: enable
TargetRubyVersion: 3.2
TargetRubyVersion: 3.1

Exclude:
- test.rb
Expand Down
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.3
3.1.6
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
expresenter (1.5.0)
expresenter (1.5.1)

GEM
remote: https://rubygems.org/
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2021-2024 Cyril Kato
Copyright (c) 2021-2025 Cyril Kato

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
224 changes: 113 additions & 111 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@

[![Version](https://img.shields.io/github/v/tag/fixrb/expresenter?label=Version&logo=github)](https://github.com/fixrb/expresenter/tags)
[![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/expresenter/main)
[![Ruby](https://github.com/fixrb/expresenter/workflows/Ruby/badge.svg?branch=main)](https://github.com/fixrb/expresenter/actions?query=workflow%3Aruby+branch%3Amain)
[![RuboCop](https://github.com/fixrb/expresenter/workflows/RuboCop/badge.svg?branch=main)](https://github.com/fixrb/expresenter/actions?query=workflow%3Arubocop+branch%3Amain)
[![License](https://img.shields.io/github/license/fixrb/expresenter?label=License&logo=github)](https://github.com/fixrb/expresenter/raw/main/LICENSE.md)

> A Ruby gem for presenting test expectation results with rich formatting and requirement level support. Perfect for test frameworks and assertion libraries that need flexible result reporting with support for MUST/SHOULD/MAY requirement levels.
## Features

- Rich result formatting with colored output
- Support for MUST/SHOULD/MAY requirement levels
- Multiple result classification: success, warning, info, failure, and error
- Emoji support for visual result indication (✅, ⚠️, 💡, ❌, 💥)
- Flexible negation support for negative assertions
- Detailed error reporting with custom messages
- Rich test result presentation with:
- Colored output for different result types (success, warning, info, failure, error)
- Single-character indicators for compact output (".", "W", "I", "F", "E")
- Emoji support for visual feedback (✅, ⚠️, 💡, ❌, 💥)
- ANSI-colored formatted messages with bold titles
- Support for RFC 2119 requirement levels (MUST/SHOULD/MAY)
- Comprehensive result classification:
- Success: Test passed as expected (green)
- Warning: Non-critical issues, typically for SHOULD/MAY requirements (yellow)
- Info: Additional information about passing tests (blue)
- Failure: Test failed but no exception occurred (purple)
- Error: Unexpected exceptions during test execution (red)
- Built-in support for negative assertions
- Detailed error reporting with captured exceptions
- Clean integration with test frameworks via simple API

## Installation

Expand All @@ -39,133 +46,128 @@ gem install expresenter

## Usage

Assuming that an expectation is an assertion that is either `true` or `false`,
qualifying it with `MUST`, `SHOULD` and `MAY`, we can draw up several scenarios:

| Requirement levels | **MUST** | **SHOULD** | **MAY** |
| ------------------------- | -------- | ---------- | ------- |
| Implemented & Matched | `true` | `true` | `true` |
| Implemented & Not matched | `false` | `true` | `false` |
| Implemented & Exception | `false` | `false` | `false` |
| Not implemented | `false` | `false` | `true` |

Then,

* for a `true` assertion, a `Expresenter::Pass` instance can be returned;
* for a `false` assertion, a `Expresenter::Fail` exception can be raised.

Both class share a same `Common` interface.

Passed expectations can be classified as:

* ✅ success
* ⚠️ warning
* 💡 info

Failed expectations can be classified as:

* ❌ failure
* 💥 error

### Instantiation

The following parameters are required to instantiate the result:

* `actual`: Returned value by the challenged subject.
* `definition`: A readable string of the matcher and any expected values.
* `error`: Any possible raised exception.
* `got`: The result of the boolean comparison between the actual value and the expected value through the matcher.
* `negate`: Evaluated to a negative assertion?
* `level`: The requirement level (`:MUST`, `:SHOULD` or `:MAY`).

#### Examples

A passed expectation:
### Basic Example

```ruby
result = Expresenter.call(true).new(actual: "FOO", definition: 'eq "foo"', error: nil, got: true, negate: true, level: :MUST)

result.failed? # => false
result.failure? # => false
result.info? # => false
result.warning? # => false
result.to_sym # => :success
result.char # => "."
result.emoji # => "✅"
result.passed? # => true
result.negate? # => true
result.error? # => false
result.success? # => true
result.definition # => "eq \"foo\""
result.summary # => "expected \"FOO\" not to eq \"foo\""
result.colored_char # => "\e[32m.\e[0m"
result.colored_string # => "\e[32m\e[1mSuccess\e[22m: expected \"FOO\" not to eq \"foo\".\e[0m"
result.message # => "Success: expected \"FOO\" not to eq \"foo\"."
result.to_s # => "Success: expected \"FOO\" not to eq \"foo\"."
result.titre # => "Success"
# Create a successful test result
result = Expresenter.call(true).new(
actual: "foo",
definition: 'eq "foo"',
error: nil,
got: true,
negate: false,
level: :MUST
)

result.passed? # => true
result.to_sym # => :success
result.char # => "."
result.emoji # => "✅"
result.to_s # => 'Success: expected "foo" to eq "foo".'
```

A failed expectation:
### Handling Different Result Types

```ruby
result = Expresenter.call(false).new(actual: "foo", definition: "eq 42", error: Exception.new("BOOM"), got: true, negate: true, level: :MUST)

result.failed? # => true
result.failure? # => false
result.info? # => false
result.warning? # => false
result.to_sym # => :error
result.char # => "E"
result.emoji # => "💥"
result.passed? # => false
result.negate? # => true
result.error? # => true
result.success? # => true
result.definition # => "eq 42"
result.summary # => "BOOM"
result.colored_char # => "\e[31mE\e[0m"
result.colored_string # => "\e[31m\e[1mException\e[22m: BOOM.\e[0m"
result.message # => "Exception: BOOM."
result.to_s # => "Exception: BOOM."
result.titre # => "Exception"
# Warning example (non-critical requirement)
warning = Expresenter.call(true).new(
actual: "foo",
definition: "be_optimized",
error: nil,
got: false, # triggers warning state
negate: false,
level: :SHOULD
)

warning.warning? # => true
warning.char # => "W"
warning.emoji # => "⚠️"

# Failure example with exception
begin
Expresenter.call(false).with(
actual: 42,
definition: "eq 43",
error: nil,
got: false,
negate: false,
level: :MUST
)
rescue Expresenter::Fail => e
e.failure? # => true
e.char # => "F"
e.emoji # => "❌"
e.to_s # => "Failure: expected 42 to eq 43."
end
```

### Return or Raise
### Using Requirement Levels

To return the results which pass, and to raise the results which fail, the `with` method is available.
Expresenter supports RFC 2119 requirement levels:

In this example, the result passes, the instance is therefore returned:
- `:MUST` - Critical requirements that must be satisfied
- `:SHOULD` - Recommended requirements that should be satisfied when possible
- `:MAY` - Optional requirements that may be satisfied

```ruby
Expresenter.call(true).with(actual: "FOO", definition: 'eq "foo"', error: nil, got: true, negate: true, level: :MUST) # => Expresenter::Pass(actual: "FOO", definition: "eq \"foo\"", error: nil, got: true, negate: true, level: :MUST)
# SHOULD requirement with warning
result = Expresenter.call(true).new(
actual: response,
definition: "have_fast_response_time",
error: nil,
got: false,
negate: false,
level: :SHOULD
)

result.warning? # => true
```

In this example, the result fails, so the exception is raised:
### Working with Negative Assertions

```ruby
Expresenter.call(false).with(actual: "foo", definition: "eq 40", error: Exception.new("BOOM"), got: true, negate: true, level: :MUST)
# Negative assertion example
result = Expresenter.call(true).new(
actual: "foo",
definition: 'eq "bar"',
error: nil,
got: true,
negate: true, # indicates negative assertion
level: :MUST
)

result.negate? # => true
result.to_s # => 'Success: expected "foo" not to eq "bar".'
```

> Traceback (most recent call last):
> 4: from ./bin/console:7:in `<main>'
> 3: from (irb):42
> 2: from (irb):43:in `rescue in irb_binding'
> 1: from /Users/cyril/github/fixrb/expresenter/lib/expresenter/fail.rb:25:in `with'
> Expresenter::Fail (Exception: BOOM.)
## Integration

Expresenter can be easily integrated into test frameworks and assertion libraries. It provides a simple API for creating and handling test results with rich formatting and requirement levels.

### More Examples
Example integration with a test framework:

A full list of unit tests can be viewed (and executed) here:
[./test.rb](https://github.com/fixrb/expresenter/blob/main/test.rb)
```ruby
def assert(actual, matcher, level: :MUST)
result = matcher.match(actual)

Expresenter.call(result.success?).with(
actual: actual,
definition: matcher.description,
error: result.error,
got: result.matched?,
negate: false,
level: level
)
end
```

## Contact
## Development

* Home page: https://github.com/fixrb/expresenter
* Bugs/issues: https://github.com/fixrb/expresenter/issues
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

## Versioning
## Contributing

__Expresenter__ follows [Semantic Versioning 2.0](https://semver.org/).
Bug reports and pull requests are welcome on GitHub at https://github.com/fixrb/expresenter. This project is intended to be a safe, welcoming space for collaboration.

## License

Expand Down
2 changes: 1 addition & 1 deletion VERSION.semver
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.5.0
1.5.1
2 changes: 1 addition & 1 deletion expresenter.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
spec.homepage = "https://github.com/fixrb/expresenter"
spec.license = "MIT"
spec.files = Dir["LICENSE.md", "README.md", "lib/**/*"]
spec.required_ruby_version = ">= 3.2.0"
spec.required_ruby_version = ">= 3.1.0"

spec.metadata["rubygems_mfa_required"] = "true"
end
55 changes: 49 additions & 6 deletions lib/expresenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,58 @@

# Namespace for the Expresenter library.
#
# @example A passed expectation result presenter.
# Expresenter.call(true).with(actual: "FOO", definition: 'eql "foo"', error: nil, got: true, negate: true, level: :MUST) # => Expresenter::Pass(actual: "FOO", definition: "eql \"foo\"", error: nil, got: true, negate: true, level: :MUST)
# The Expresenter library provides a flexible way to present test expectation results with rich
# formatting and requirement level support. It is designed to work with test frameworks and
# assertion libraries that need detailed result reporting.
#
# Each expectation result can be categorized as:
# - Success: The test passed as expected
# - Warning: A non-critical test failure (typically for :SHOULD or :MAY requirements)
# - Info: Additional information about the test
# - Failure: A critical test failure
# - Error: An unexpected error occurred during the test
#
# @example Creating a passing expectation result
# result = Expresenter.call(true).with(
# actual: "FOO",
# definition: 'eql "foo"',
# error: nil,
# got: true,
# negate: true,
# level: :MUST
# )
# result.passed? # => true
# result.to_s # => "Success: expected \"FOO\" not to eql \"foo\"."
#
# @example Creating a failing expectation result
# # This will raise an Expresenter::Fail exception
# Expresenter.call(false).with(
# actual: "foo",
# definition: "eq 42",
# error: Exception.new("Test failed"),
# got: false,
# negate: false,
# level: :MUST
# )
#
module Expresenter
# @param is_passed [Boolean] The value of an assertion.
# Factory method that returns the appropriate result class based on the assertion outcome.
#
# @param is_passed [Boolean] The value of the assertion. True indicates a passing test,
# false indicates a failing test.
#
# @return [Class<Pass>, Class<Fail>] Returns the Pass class for passing tests or
# the Fail class for failing tests. These classes can then be instantiated with
# detailed test information using #with.
#
# @example Getting a Pass class for a successful test
# result_class = Expresenter.call(true)
# result_class # => Expresenter::Pass
#
# @return [Class<Pass>, Class<Fail>] The class of the result.
# @example Getting a Fail class for a failed test
# result_class = Expresenter.call(false)
# result_class # => Expresenter::Fail
#
# @example Get the pass class result.
# call(true) # => Pass
def self.call(is_passed)
is_passed ? Pass : Fail
end
Expand Down
Loading

0 comments on commit 5d45b72

Please sign in to comment.