Skip to content

Commit

Permalink
refactor(set): improve Set class documentation and error handling
Browse files Browse the repository at this point in the history
- Add comprehensive documentation for Set class methods and use cases
- Add new match? method for conditional testing without process exit
- Add missing_subject_block error for better error handling
- Improve code organization and method naming
- Add detailed type documentation for specifications array
- Enhance method documentation with more examples
  • Loading branch information
cyril committed Jan 2, 2025
1 parent 485e11a commit 48ab78e
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 32 deletions.
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:
fix (0.19)
fix (0.20)
defi (~> 3.0.1)
matchi (~> 4.1.1)
spectus (~> 5.0.2)
Expand Down
2 changes: 1 addition & 1 deletion VERSION.semver
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.19
0.20
15 changes: 15 additions & 0 deletions lib/fix/error/missing_subject_block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Fix
module Error
# Error raised when attempting to test a specification without providing a subject block
class MissingSubjectBlock < ::ArgumentError
MISSING_BLOCK_ERROR = "Subject block is required for testing a specification. " \
"Use: test { subject } or match? { subject }"

def initialize
super(MISSING_BLOCK_ERROR)
end
end
end
end
131 changes: 101 additions & 30 deletions lib/fix/set.rb
Original file line number Diff line number Diff line change
@@ -1,69 +1,148 @@
# frozen_string_literal: true

require "English"

require_relative "doc"
require_relative "run"
require_relative "error/missing_subject_block"

module Fix
# Collection of specifications that can be executed as a test suite.
#
# The Set class handles loading, organizing, and executing test specifications.
# It supports both named and anonymous specifications and provides detailed
# test reporting.
# The Set class is a central component in Fix's architecture that handles:
# - Loading and organizing test specifications
# - Managing test execution and isolation
# - Reporting test results
# - Handling process management for test isolation
#
# It supports both named specifications (loaded via Fix[name]) and anonymous
# specifications (created directly via Fix blocks).
#
# @example Running a named specification
# @example Running a simple named specification
# Fix[:Calculator].test { Calculator.new }
#
# @example Running an anonymous specification
# Fix do
# it MUST be_positive
# end.test { 42 }
# @example Running a complex specification with multiple contexts
# Fix[:UserSystem] do
# with(role: "admin") do
# on :access?, :settings do
# it MUST be_true
# end
# end
#
# with(role: "guest") do
# on :access?, :settings do
# it MUST be_false
# end
# end
# end.test { UserSystem.new(role:) }
#
# @example Using match? for conditional testing
# if Fix[:EmailValidator].match? { email }
# puts "Email is valid"
# end
#
# @api private
class Set
# @return [Array] A list of specifications to be tested
attr_reader :specs
# List of specifications to be tested.
# Each specification is an array containing:
# - The test environment
# - The source location (file:line)
# - The requirement (MUST, SHOULD, or MAY)
# - The challenges to apply
#
# @return [Array] List of specifications
attr_reader :expected

class << self
# Load specifications from a constant name.
# Loads specifications from a registered constant name.
#
# This method retrieves previously registered specifications and creates
# a new Set instance ready for testing. It's typically used in conjunction
# with Fix[name] syntax.
#
# @param name [String, Symbol] The constant name of the specifications
# @return [Set] A new Set instance containing the loaded specifications
# @raise [Fix::Error::SpecificationNotFound] If specification doesn't exist
#
# @example
# @example Loading a named specification
# Fix::Set.load(:Calculator)
#
# @example Loading and testing in one go
# Fix::Set.load(:EmailValidator).test { email }
#
# @api public
def load(name)
new(*Doc.fetch(name))
end
end

# Initialize a new Set with given contexts.
# Initialize a new Set with the given contexts.
#
# @param contexts [Array<Fix::Dsl>] The list of specification contexts
# @param contexts [Array<Fix::Dsl>] List of specification contexts
#
# @example
# @example Creating a set with a single context
# Fix::Set.new(calculator_context)
#
# @example Creating a set with multiple contexts
# Fix::Set.new(base_context, admin_context, guest_context)
def initialize(*contexts)
@specs = randomize_specs(Doc.extract_specifications(*contexts))
@expected = randomize_specs(Doc.extract_specifications(*contexts))
end

# Checks if the subject matches all specifications without exiting.
#
# Unlike #test, this method:
# - Returns a boolean instead of exiting
# - Can be used in conditional logic
#
# @yield The block of code to be tested
# @yieldreturn [Object] The result of the code being tested
# @return [Boolean] true if all tests pass, false otherwise
#
# @example Basic usage
# set.match? { Calculator.new } #=> true
#
# @example Conditional usage
# if set.match? { user_input }
# save_to_database(user_input)
# end
#
# @api public
def match?(&subject)
raise Error::MissingSubjectBlock unless subject

expected.all? { |spec| run_spec(*spec, &subject) }
end

# Run the test suite against the provided subject.
# Runs the test suite against the provided subject.
#
# This method:
# - Executes all specifications in random order
# - Runs each test in isolation using process forking
# - Reports results for each specification
# - Exits with failure if any test fails
#
# @yield The block of code to be tested
# @yieldreturn [Object] The result of the code being tested
# @return [Boolean] true if all tests pass
# @raise [SystemExit] When tests fail (exit code: 1)
# @raise [SystemExit] When any test fails (exit code: 1)
#
# @example
# @example Basic usage
# set.test { Calculator.new }
#
# @example Testing with parameters
# set.test { Game.new(south_variant:, north_variant:) }
#
# @api public
def test(&subject)
suite_passed?(&subject) || exit_with_failure
match?(&subject) || exit_with_failure
end

# Returns a string representing the matcher.
#
# @return [String] a human-readable description of the matcher
#
# @api public
def to_s
"fix #{expected.inspect}"
end

private
Expand All @@ -76,14 +155,6 @@ def randomize_specs(specifications)
specifications.shuffle
end

# Checks if all specifications in the suite passed
#
# @yield The subject block to test against
# @return [Boolean] true if all specs passed
def suite_passed?(&subject)
specs.all? { |spec| run_spec(*spec, &subject) }
end

# Runs a single specification in a forked process
#
# @param env [Fix::Dsl] The test environment
Expand Down

0 comments on commit 48ab78e

Please sign in to comment.