fix 0.18.2 → 0.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.md +2 -2
- data/README.md +384 -84
- data/lib/fix/builder.rb +101 -0
- data/lib/fix/doc.rb +59 -0
- data/lib/fix/dsl.rb +139 -0
- data/lib/fix/error/invalid_specification_name.rb +12 -0
- data/lib/fix/error/missing_specification_block.rb +14 -0
- data/lib/fix/error/specification_not_found.rb +12 -0
- data/lib/fix/matcher.rb +76 -0
- data/lib/fix/requirement.rb +119 -0
- data/lib/fix/run.rb +88 -0
- data/lib/fix/set.rb +67 -0
- data/lib/fix.rb +85 -20
- data/lib/kernel.rb +49 -0
- metadata +41 -153
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -1
- data/.rubocop_todo.yml +0 -25
- data/.travis.yml +0 -28
- data/.yardopts +0 -1
- data/CODE_OF_CONDUCT.md +0 -13
- data/Gemfile +0 -5
- data/Rakefile +0 -23
- data/VERSION.semver +0 -1
- data/bin/console +0 -8
- data/bin/setup +0 -6
- data/checksum/fix-0.0.1.pre.gem.sha512 +0 -1
- data/checksum/fix-0.1.0.gem.sha512 +0 -1
- data/checksum/fix-0.1.0.pre.gem.sha512 +0 -1
- data/checksum/fix-0.10.0.gem.sha512 +0 -1
- data/checksum/fix-0.11.0.gem.sha512 +0 -1
- data/checksum/fix-0.11.1.gem.sha512 +0 -1
- data/checksum/fix-0.12.0.gem.sha512 +0 -1
- data/checksum/fix-0.12.1.gem.sha512 +0 -1
- data/checksum/fix-0.12.2.gem.sha512 +0 -1
- data/checksum/fix-0.12.3.gem.sha512 +0 -1
- data/checksum/fix-0.13.0.gem.sha512 +0 -1
- data/checksum/fix-0.14.0.gem.sha512 +0 -1
- data/checksum/fix-0.14.1.gem.sha512 +0 -1
- data/checksum/fix-0.15.0.gem.sha512 +0 -1
- data/checksum/fix-0.15.2.gem.sha512 +0 -1
- data/checksum/fix-0.16.0.gem.sha512 +0 -1
- data/checksum/fix-0.17.0.gem.sha512 +0 -1
- data/checksum/fix-0.17.1.gem.sha512 +0 -1
- data/checksum/fix-0.17.2.gem.sha512 +0 -1
- data/checksum/fix-0.18.0.gem.sha512 +0 -1
- data/checksum/fix-0.18.1.gem.sha512 +0 -1
- data/checksum/fix-0.2.0.gem.sha512 +0 -1
- data/checksum/fix-0.3.0.gem.sha512 +0 -1
- data/checksum/fix-0.4.0.gem.sha512 +0 -1
- data/checksum/fix-0.5.0.gem.sha512 +0 -1
- data/checksum/fix-0.6.0.gem.sha512 +0 -1
- data/checksum/fix-0.6.1.gem.sha512 +0 -1
- data/checksum/fix-0.7.0.gem.sha512 +0 -1
- data/checksum/fix-0.8.0.gem.sha512 +0 -1
- data/checksum/fix-0.9.0.gem.sha512 +0 -1
- data/checksum/fix-0.9.1.gem.sha512 +0 -1
- data/fix.gemspec +0 -29
- data/lib/fix/it.rb +0 -41
- data/lib/fix/on.rb +0 -139
- data/lib/fix/report.rb +0 -120
- data/lib/fix/test.rb +0 -89
- 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
|
data/lib/fix/matcher.rb
ADDED
@@ -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
|