fix 0.18.2 → 0.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +2 -2
  3. data/README.md +384 -84
  4. data/lib/fix/builder.rb +101 -0
  5. data/lib/fix/doc.rb +59 -0
  6. data/lib/fix/dsl.rb +139 -0
  7. data/lib/fix/error/invalid_specification_name.rb +12 -0
  8. data/lib/fix/error/missing_specification_block.rb +14 -0
  9. data/lib/fix/error/specification_not_found.rb +12 -0
  10. data/lib/fix/matcher.rb +76 -0
  11. data/lib/fix/requirement.rb +119 -0
  12. data/lib/fix/run.rb +88 -0
  13. data/lib/fix/set.rb +67 -0
  14. data/lib/fix.rb +85 -20
  15. data/lib/kernel.rb +49 -0
  16. metadata +41 -153
  17. data/.gitignore +0 -11
  18. data/.rubocop.yml +0 -1
  19. data/.rubocop_todo.yml +0 -25
  20. data/.travis.yml +0 -28
  21. data/.yardopts +0 -1
  22. data/CODE_OF_CONDUCT.md +0 -13
  23. data/Gemfile +0 -5
  24. data/Rakefile +0 -23
  25. data/VERSION.semver +0 -1
  26. data/bin/console +0 -8
  27. data/bin/setup +0 -6
  28. data/checksum/fix-0.0.1.pre.gem.sha512 +0 -1
  29. data/checksum/fix-0.1.0.gem.sha512 +0 -1
  30. data/checksum/fix-0.1.0.pre.gem.sha512 +0 -1
  31. data/checksum/fix-0.10.0.gem.sha512 +0 -1
  32. data/checksum/fix-0.11.0.gem.sha512 +0 -1
  33. data/checksum/fix-0.11.1.gem.sha512 +0 -1
  34. data/checksum/fix-0.12.0.gem.sha512 +0 -1
  35. data/checksum/fix-0.12.1.gem.sha512 +0 -1
  36. data/checksum/fix-0.12.2.gem.sha512 +0 -1
  37. data/checksum/fix-0.12.3.gem.sha512 +0 -1
  38. data/checksum/fix-0.13.0.gem.sha512 +0 -1
  39. data/checksum/fix-0.14.0.gem.sha512 +0 -1
  40. data/checksum/fix-0.14.1.gem.sha512 +0 -1
  41. data/checksum/fix-0.15.0.gem.sha512 +0 -1
  42. data/checksum/fix-0.15.2.gem.sha512 +0 -1
  43. data/checksum/fix-0.16.0.gem.sha512 +0 -1
  44. data/checksum/fix-0.17.0.gem.sha512 +0 -1
  45. data/checksum/fix-0.17.1.gem.sha512 +0 -1
  46. data/checksum/fix-0.17.2.gem.sha512 +0 -1
  47. data/checksum/fix-0.18.0.gem.sha512 +0 -1
  48. data/checksum/fix-0.18.1.gem.sha512 +0 -1
  49. data/checksum/fix-0.2.0.gem.sha512 +0 -1
  50. data/checksum/fix-0.3.0.gem.sha512 +0 -1
  51. data/checksum/fix-0.4.0.gem.sha512 +0 -1
  52. data/checksum/fix-0.5.0.gem.sha512 +0 -1
  53. data/checksum/fix-0.6.0.gem.sha512 +0 -1
  54. data/checksum/fix-0.6.1.gem.sha512 +0 -1
  55. data/checksum/fix-0.7.0.gem.sha512 +0 -1
  56. data/checksum/fix-0.8.0.gem.sha512 +0 -1
  57. data/checksum/fix-0.9.0.gem.sha512 +0 -1
  58. data/checksum/fix-0.9.1.gem.sha512 +0 -1
  59. data/fix.gemspec +0 -29
  60. data/lib/fix/it.rb +0 -41
  61. data/lib/fix/on.rb +0 -139
  62. data/lib/fix/report.rb +0 -120
  63. data/lib/fix/test.rb +0 -89
  64. data/pkg_checksum +0 -12
data/lib/fix/doc.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error/invalid_specification_name"
4
+
5
+ module Fix
6
+ # Module for storing and managing specification documents.
7
+ #
8
+ # This module acts as a registry for specification classes and handles
9
+ # the extraction of test specifications from context objects.
10
+ #
11
+ # @api private
12
+ module Doc
13
+ # Retrieves the contexts array for a named specification.
14
+ #
15
+ # @param name [String, Symbol] The constant name of the specification
16
+ # @return [Array<Fix::Dsl>] Array of context classes for the specification
17
+ # @raise [NameError] If specification constant is not found
18
+ def self.fetch(name)
19
+ const_get("#{name}::CONTEXTS")
20
+ end
21
+
22
+ # Extracts test specifications from a list of context classes.
23
+ # Each specification consists of an environment and its associated test data.
24
+ #
25
+ # @param contexts [Array<Fix::Dsl>] List of context classes to process
26
+ # @return [Array<Array>] Array of arrays where each sub-array contains:
27
+ # - [0] environment: The test environment instance
28
+ # - [1] location: The test file location (as "path:line")
29
+ # - [2] requirement: The test requirement (MUST, SHOULD, or MAY)
30
+ # - [3] challenges: Array of test challenges to execute
31
+ def self.extract_specifications(*contexts)
32
+ contexts.flat_map do |context|
33
+ extract_context_specifications(context)
34
+ end
35
+ end
36
+
37
+ # Registers a new specification class under the given name.
38
+ #
39
+ # @param name [String, Symbol] Name to register the specification under
40
+ # @param klass [Class] The specification class to register
41
+ # @raise [Fix::Error::InvalidSpecificationName] If name is not a valid constant name
42
+ # @return [void]
43
+ def self.spec_set(name, klass)
44
+ const_set(name, klass)
45
+ rescue ::NameError => _e
46
+ raise Error::InvalidSpecificationName, name
47
+ end
48
+
49
+ # @private
50
+ def self.extract_context_specifications(context)
51
+ env = context.new
52
+ env.public_methods(false).map do |public_method|
53
+ [env] + env.public_send(public_method)
54
+ end
55
+ end
56
+
57
+ private_class_method :extract_context_specifications
58
+ end
59
+ end
data/lib/fix/dsl.rb ADDED
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "defi/method"
4
+
5
+ require_relative "matcher"
6
+ require_relative "requirement"
7
+
8
+ module Fix
9
+ # Abstract class for handling the domain-specific language.
10
+ #
11
+ # @api private
12
+ class Dsl
13
+ extend Matcher
14
+ extend Requirement
15
+
16
+ # Sets a user-defined property.
17
+ #
18
+ # @example
19
+ # require "fix"
20
+ #
21
+ # Fix do
22
+ # let(:name) { "Bob" }
23
+ # end
24
+ #
25
+ # @param name [String, Symbol] The name of the property.
26
+ # @yield The block that defines the property's value
27
+ # @yieldreturn [Object] The value to be returned by the property
28
+ #
29
+ # @return [Symbol] A private method that defines the block content.
30
+ #
31
+ # @api public
32
+ def self.let(name, &)
33
+ private define_method(name, &)
34
+ end
35
+
36
+ # Defines an example group with user-defined properties that describes a
37
+ # unit to be tested.
38
+ #
39
+ # @example
40
+ # require "fix"
41
+ #
42
+ # Fix do
43
+ # with password: "secret" do
44
+ # it MUST be true
45
+ # end
46
+ # end
47
+ #
48
+ # @param kwargs [Hash] The list of properties to define in this context
49
+ # @yield The block that defines the specs for this context
50
+ # @yieldreturn [void]
51
+ #
52
+ # @return [Class] A new class representing this context
53
+ #
54
+ # @api public
55
+ def self.with(**kwargs, &)
56
+ klass = ::Class.new(self)
57
+ klass.const_get(:CONTEXTS) << klass
58
+ kwargs.each { |name, value| klass.let(name) { value } }
59
+ klass.instance_eval(&)
60
+ klass
61
+ end
62
+
63
+ # Defines an example group that describes a unit to be tested.
64
+ #
65
+ # @example
66
+ # require "fix"
67
+ #
68
+ # Fix do
69
+ # on :+, 2 do
70
+ # it MUST be 42
71
+ # end
72
+ # end
73
+ #
74
+ # @param method_name [String, Symbol] The method to send to the subject
75
+ # @param args [Array] Positional arguments to pass to the method
76
+ # @param kwargs [Hash] Keyword arguments to pass to the method
77
+ # @yield The block containing the specifications for this context
78
+ # @yieldreturn [void]
79
+ #
80
+ # @return [Class] A new class representing this context
81
+ #
82
+ # @api public
83
+ def self.on(method_name, *args, **kwargs, &block)
84
+ klass = ::Class.new(self)
85
+ klass.const_get(:CONTEXTS) << klass
86
+
87
+ const_name = :"MethodContext_#{block.object_id}"
88
+ const_set(const_name, klass)
89
+
90
+ klass.define_singleton_method(:challenges) do
91
+ challenge = ::Defi::Method.new(method_name, *args, **kwargs)
92
+ super() + [challenge]
93
+ end
94
+
95
+ klass.instance_eval(&block)
96
+ klass
97
+ end
98
+
99
+ # Defines a concrete spec definition.
100
+ #
101
+ # @example
102
+ # require "fix"
103
+ #
104
+ # Fix { it MUST be 42 }
105
+ #
106
+ # Fix do
107
+ # it { MUST be 42 }
108
+ # end
109
+ #
110
+ # @param requirement [Object, nil] The requirement to test
111
+ # @yield A block defining the requirement if not provided directly
112
+ # @yieldreturn [Object] The requirement definition
113
+ #
114
+ # @return [Symbol] Name of the generated test method
115
+ #
116
+ # @raise [ArgumentError] If neither or both requirement and block are provided
117
+ #
118
+ # @api public
119
+ def self.it(requirement = nil, &block)
120
+ raise ::ArgumentError, "Must provide either requirement or block, not both" if requirement && block
121
+ raise ::ArgumentError, "Must provide either requirement or block" unless requirement || block
122
+
123
+ location = caller_locations(1, 1).fetch(0)
124
+ location = [location.path, location.lineno].join(":")
125
+
126
+ test_method_name = :"test_#{(requirement || block).object_id}"
127
+ define_method(test_method_name) do
128
+ [location, requirement || singleton_class.class_eval(&block), self.class.challenges]
129
+ end
130
+ end
131
+
132
+ # The list of challenges to be addressed to the object to be tested.
133
+ #
134
+ # @return [Array<Defi::Method>] A list of challenges.
135
+ def self.challenges
136
+ []
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fix
4
+ module Error
5
+ # Error raised when an invalid specification name is provided during declaration
6
+ class InvalidSpecificationName < ::NameError
7
+ def initialize(name)
8
+ super("Invalid specification name '#{name}'. Specification names must be valid Ruby constants.")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fix
4
+ module Error
5
+ # Error raised when attempting to build a specification without a block
6
+ class MissingSpecificationBlock < ::ArgumentError
7
+ MISSING_BLOCK_ERROR = "Block is required for building a specification"
8
+
9
+ def initialize
10
+ super(MISSING_BLOCK_ERROR)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fix
4
+ module Error
5
+ # Error raised when a specification cannot be found at runtime
6
+ class SpecificationNotFound < ::NameError
7
+ def initialize(name)
8
+ super("Specification '#{name}' not found. Make sure it's defined before running the test.")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "matchi"
4
+
5
+ module Fix
6
+ # Collection of expectation matchers.
7
+ # Provides a comprehensive set of matchers for testing different aspects of objects.
8
+ #
9
+ # The following matchers are available:
10
+ #
11
+ # Basic Comparison:
12
+ # - eq(expected) # Checks equality using eql?
13
+ # it MUST eq(42)
14
+ # it MUST eq("hello")
15
+ # - eql(expected) # Alias for eq
16
+ # - be(expected) # Checks exact object identity using equal?
17
+ # string = "test"
18
+ # it MUST be(string) # Passes only if it's the same object
19
+ # - equal(expected) # Alias for be
20
+ #
21
+ # Type Checking:
22
+ # - be_an_instance_of(class) # Checks exact class match
23
+ # it MUST be_an_instance_of(Array)
24
+ # - be_a_kind_of(class) # Checks class inheritance and module inclusion
25
+ # it MUST be_a_kind_of(Enumerable)
26
+ #
27
+ # State & Changes:
28
+ # - change(object, method) # Base for checking state changes
29
+ # .by(n) # Exact change by n
30
+ # it MUST change(user, :points).by(5)
31
+ # .by_at_least(n) # Minimum change by n
32
+ # it MUST change(counter, :value).by_at_least(10)
33
+ # .by_at_most(n) # Maximum change by n
34
+ # it MUST change(account, :balance).by_at_most(100)
35
+ # .from(old).to(new) # Change from old to new value
36
+ # it MUST change(user, :status).from("pending").to("active")
37
+ # .to(new) # Change to new value
38
+ # it MUST change(post, :title).to("Updated")
39
+ #
40
+ # Value Testing:
41
+ # - be_within(delta).of(value) # Checks numeric value within delta
42
+ # it MUST be_within(0.1).of(3.14)
43
+ # - match(regex) # Tests against regular expression
44
+ # it MUST match(/^\d{3}-\d{2}-\d{4}$/) # SSN format
45
+ # - satisfy { |value| ... } # Custom matcher with block
46
+ # it MUST satisfy { |num| num.even? && num > 0 }
47
+ #
48
+ # Exceptions:
49
+ # - raise_exception(class) # Checks if code raises exception
50
+ # it MUST raise_exception(ArgumentError)
51
+ # it MUST raise_exception(CustomError, "specific message")
52
+ #
53
+ # State Testing:
54
+ # - be_true # Tests for true
55
+ # it MUST be_true # Only passes for true, not truthy values
56
+ # - be_false # Tests for false
57
+ # it MUST be_false # Only passes for false, not falsey values
58
+ # - be_nil # Tests for nil
59
+ # it MUST be_nil
60
+ #
61
+ # Predicate Matchers:
62
+ # - be_* # Matches object.*? method
63
+ # it MUST be_empty # Calls empty?
64
+ # it MUST be_valid # Calls valid?
65
+ # it MUST be_frozen # Calls frozen?
66
+ # - have_* # Matches object.has_*? method
67
+ # it MUST have_key(:id) # Calls has_key?
68
+ # it MUST have_errors # Calls has_errors?
69
+ #
70
+ # @note All matchers can be used with MUST, MUST_NOT, SHOULD, SHOULD_NOT, and MAY
71
+ # @see https://github.com/fixrb/matchi for more details about the matchers
72
+ # @api private
73
+ module Matcher
74
+ include Matchi
75
+ end
76
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spectus/requirement/optional"
4
+ require "spectus/requirement/recommended"
5
+ require "spectus/requirement/required"
6
+
7
+ module Fix
8
+ # Implements requirement levels as defined in RFC 2119.
9
+ # Provides methods for specifying different levels of requirements
10
+ # in test specifications: MUST, SHOULD, and MAY.
11
+ #
12
+ # @api private
13
+ module Requirement
14
+ # rubocop:disable Naming/MethodName
15
+
16
+ # This method means that the definition is an absolute requirement of the
17
+ # specification.
18
+ #
19
+ # @example Test exact equality
20
+ # it MUST eq(42)
21
+ #
22
+ # @example Test type matching
23
+ # it MUST be_an_instance_of(User)
24
+ #
25
+ # @example Test state changes
26
+ # it MUST change(user, :status).from("pending").to("active")
27
+ #
28
+ # @param matcher [#match?] The matcher that defines the required condition
29
+ # @return [::Spectus::Requirement::Required] An absolute requirement level instance
30
+ #
31
+ # @api public
32
+ def MUST(matcher)
33
+ ::Spectus::Requirement::Required.new(negate: false, matcher:)
34
+ end
35
+
36
+ # This method means that the definition is an absolute prohibition of the
37
+ # specification.
38
+ #
39
+ # @example Test prohibited state
40
+ # it MUST_NOT be_nil
41
+ #
42
+ # @example Test prohibited type
43
+ # it MUST_NOT be_a_kind_of(AdminUser)
44
+ #
45
+ # @example Test prohibited exception
46
+ # it MUST_NOT raise_exception(SecurityError)
47
+ #
48
+ # @param matcher [#match?] The matcher that defines the prohibited condition
49
+ # @return [::Spectus::Requirement::Required] An absolute prohibition level instance
50
+ #
51
+ # @api public
52
+ def MUST_NOT(matcher)
53
+ ::Spectus::Requirement::Required.new(negate: true, matcher:)
54
+ end
55
+
56
+ # This method means that there may exist valid reasons in particular
57
+ # circumstances to ignore this requirement, but the implications must be
58
+ # understood and carefully weighed.
59
+ #
60
+ # @example Test numeric boundaries
61
+ # it SHOULD be_within(0.1).of(expected_value)
62
+ #
63
+ # @example Test pattern matching
64
+ # it SHOULD match(/^[A-Z][a-z]+$/)
65
+ #
66
+ # @example Test custom condition
67
+ # it SHOULD satisfy { |obj| obj.valid? && obj.complete? }
68
+ #
69
+ # @param matcher [#match?] The matcher that defines the recommended condition
70
+ # @return [::Spectus::Requirement::Recommended] A recommended requirement level instance
71
+ #
72
+ # @api public
73
+ def SHOULD(matcher)
74
+ ::Spectus::Requirement::Recommended.new(negate: false, matcher:)
75
+ end
76
+
77
+ # This method means that there may exist valid reasons in particular
78
+ # circumstances when the behavior is acceptable, but the implications should be
79
+ # understood and weighed carefully.
80
+ #
81
+ # @example Test state changes to avoid
82
+ # it SHOULD_NOT change(object, :state)
83
+ #
84
+ # @example Test predicate conditions to avoid
85
+ # it SHOULD_NOT be_empty
86
+ # it SHOULD_NOT have_errors
87
+ #
88
+ # @param matcher [#match?] The matcher that defines the discouraged condition
89
+ # @return [::Spectus::Requirement::Recommended] A discouraged requirement level instance
90
+ #
91
+ # @api public
92
+ def SHOULD_NOT(matcher)
93
+ ::Spectus::Requirement::Recommended.new(negate: true, matcher:)
94
+ end
95
+
96
+ # This method means that the item is truly optional. Implementations may
97
+ # include this feature if it enhances their product, and must be prepared to
98
+ # interoperate with implementations that include or omit this feature.
99
+ #
100
+ # @example Test optional functionality
101
+ # it MAY respond_to(:cache_key)
102
+ #
103
+ # @example Test optional state
104
+ # it MAY be_frozen
105
+ #
106
+ # @example Test optional predicates
107
+ # it MAY have_attachments
108
+ #
109
+ # @param matcher [#match?] The matcher that defines the optional condition
110
+ # @return [::Spectus::Requirement::Optional] An optional requirement level instance
111
+ #
112
+ # @api public
113
+ def MAY(matcher)
114
+ ::Spectus::Requirement::Optional.new(negate: false, matcher:)
115
+ end
116
+
117
+ # rubocop:enable Naming/MethodName
118
+ end
119
+ end
data/lib/fix/run.rb ADDED
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "expresenter/fail"
4
+
5
+ module Fix
6
+ # Executes a test specification by running a subject against a set of challenges
7
+ # and requirements.
8
+ #
9
+ # The Run class orchestrates test execution by:
10
+ # 1. Evaluating the test subject in the proper environment
11
+ # 2. Applying a series of method challenges to the result
12
+ # 3. Verifying the final value against the requirement
13
+ #
14
+ # @example Running a simple test
15
+ # run = Run.new(env, requirement)
16
+ # run.test { MyClass.new }
17
+ #
18
+ # @example Running with method challenges
19
+ # run = Run.new(env, requirement, challenge1, challenge2)
20
+ # run.test { MyClass.new } # Will call methods defined in challenges
21
+ #
22
+ # @api private
23
+ class Run
24
+ # The test environment containing defined variables and methods
25
+ # @return [::Fix::Dsl] A context instance
26
+ attr_reader :environment
27
+
28
+ # The specification requirement to validate against
29
+ # @return [::Spectus::Requirement::Base] An expectation
30
+ attr_reader :requirement
31
+
32
+ # The list of method calls to apply to the subject
33
+ # @return [Array<::Defi::Method>] A list of challenges
34
+ attr_reader :challenges
35
+
36
+ # Initializes a new test run with the given environment and challenges.
37
+ #
38
+ # @param environment [::Fix::Dsl] Context instance with test setup
39
+ # @param requirement [::Spectus::Requirement::Base] Expectation to verify
40
+ # @param challenges [Array<::Defi::Method>] Method calls to apply
41
+ #
42
+ # @example
43
+ # Run.new(test_env, must_be_positive, increment_method)
44
+ def initialize(environment, requirement, *challenges)
45
+ @environment = environment
46
+ @requirement = requirement
47
+ @challenges = challenges
48
+ end
49
+
50
+ # Verifies if the subject meets the requirement after applying all challenges.
51
+ #
52
+ # @param subject [Proc] The block of code to be tested
53
+ #
54
+ # @raise [::Expresenter::Fail] When the test specification fails
55
+ # @return [::Expresenter::Pass] When the test specification passes
56
+ #
57
+ # @example Basic testing
58
+ # run.test { 42 }
59
+ #
60
+ # @example Testing with subject modification
61
+ # run.test { User.new(name: "John") }
62
+ #
63
+ # @see https://github.com/fixrb/expresenter
64
+ def test(&subject)
65
+ requirement.call { actual_value(&subject) }
66
+ rescue ::Expresenter::Fail => e
67
+ e
68
+ end
69
+
70
+ private
71
+
72
+ # Computes the final value to test by applying all challenges to the subject.
73
+ #
74
+ # @param subject [Proc] The initial test subject
75
+ # @return [#object_id] The final value after applying all challenges
76
+ #
77
+ # @example Internal process
78
+ # # If challenges are [:upcase, :reverse]
79
+ # # and subject returns "hello"
80
+ # # actual_value will return "OLLEH"
81
+ def actual_value(&subject)
82
+ initial_value = environment.instance_eval(&subject)
83
+ challenges.inject(initial_value) do |obj, challenge|
84
+ challenge.to(obj).call
85
+ end
86
+ end
87
+ end
88
+ end
data/lib/fix/set.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ require_relative "doc"
6
+ require_relative "run"
7
+
8
+ module Fix
9
+ # Collection of specifications.
10
+ #
11
+ # @api private
12
+ class Set
13
+ # @return [Array] A list of specifications.
14
+ attr_reader :specs
15
+
16
+ # Load specifications from a constant name.
17
+ #
18
+ # @param name [String, Symbol] The constant name of the specifications.
19
+ # @return [Set] A new Set instance containing the loaded specifications.
20
+ #
21
+ # @api public
22
+ def self.load(name)
23
+ new(*Doc.fetch(name))
24
+ end
25
+
26
+ # Initialize a new Set with given contexts.
27
+ #
28
+ # @param contexts [Array<::Fix::Dsl>] The list of contexts document.
29
+ def initialize(*contexts)
30
+ @specs = Doc.extract_specifications(*contexts).shuffle
31
+ end
32
+
33
+ # Run the test suite against the provided subject.
34
+ #
35
+ # @yield The block of code to be tested
36
+ # @yieldreturn [Object] The result of the code being tested
37
+ # @return [Boolean] true if all tests pass, exits with false otherwise
38
+ # @raise [::SystemExit] The test set failed!
39
+ #
40
+ # @api public
41
+ def test(&)
42
+ suite_passed?(&) || ::Kernel.exit(false)
43
+ end
44
+
45
+ private
46
+
47
+ def suite_passed?(&subject)
48
+ specs.all? { |spec| run_spec(*spec, &subject) }
49
+ end
50
+
51
+ def run_spec(env, location, requirement, challenges, &subject)
52
+ ::Process.fork { lab(env, location, requirement, challenges, &subject) }
53
+ ::Process.wait
54
+ $CHILD_STATUS.success?
55
+ end
56
+
57
+ def lab(env, location, requirement, challenges, &)
58
+ result = Run.new(env, requirement, *challenges).test(&)
59
+ report!(location, result)
60
+ ::Kernel.exit(result.passed?)
61
+ end
62
+
63
+ def report!(path, result)
64
+ puts "#{path} #{result.colored_string}"
65
+ end
66
+ end
67
+ end