From 48ab78ea23721b072559bc43f8a391817834c676 Mon Sep 17 00:00:00 2001 From: Cyril Kato Date: Thu, 2 Jan 2025 22:38:54 +0100 Subject: [PATCH] refactor(set): improve Set class documentation and error handling - 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 --- Gemfile.lock | 2 +- VERSION.semver | 2 +- lib/fix/error/missing_subject_block.rb | 15 +++ lib/fix/set.rb | 131 +++++++++++++++++++------ 4 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 lib/fix/error/missing_subject_block.rb diff --git a/Gemfile.lock b/Gemfile.lock index ef04a1d..3b96463 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/VERSION.semver b/VERSION.semver index caa4836..9f4eca2 100644 --- a/VERSION.semver +++ b/VERSION.semver @@ -1 +1 @@ -0.19 +0.20 diff --git a/lib/fix/error/missing_subject_block.rb b/lib/fix/error/missing_subject_block.rb new file mode 100644 index 0000000..dc5c694 --- /dev/null +++ b/lib/fix/error/missing_subject_block.rb @@ -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 diff --git a/lib/fix/set.rb b/lib/fix/set.rb index 89e1572..240d221 100644 --- a/lib/fix/set.rb +++ b/lib/fix/set.rb @@ -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] The list of specification contexts + # @param contexts [Array] 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 @@ -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