calificador 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -16
  3. data/TODO.md +16 -0
  4. data/calificador.gemspec +54 -0
  5. data/lib/calificador.rb +8 -4
  6. data/lib/calificador/assert.rb +15 -0
  7. data/lib/calificador/assertor.rb +79 -35
  8. data/lib/calificador/build/attribute_evaluator.rb +34 -30
  9. data/lib/calificador/build/basic_factory.rb +195 -0
  10. data/lib/calificador/build/mock_factory.rb +151 -0
  11. data/lib/calificador/build/object_factory.rb +85 -0
  12. data/lib/calificador/build/trait.rb +0 -20
  13. data/lib/calificador/context/basic_context.rb +406 -0
  14. data/lib/calificador/context/class_method_context.rb +0 -0
  15. data/lib/calificador/{spec → context}/condition_context.rb +1 -3
  16. data/lib/calificador/{spec/type_context.rb → context/instance_context.rb} +5 -10
  17. data/lib/calificador/context/operation_context.rb +27 -0
  18. data/lib/calificador/context/override/argument_override.rb +73 -0
  19. data/lib/calificador/context/override/basic_override.rb +14 -0
  20. data/lib/calificador/context/override/factory_override.rb +31 -0
  21. data/lib/calificador/context/override/property_override.rb +61 -0
  22. data/lib/calificador/context/test_environment.rb +283 -0
  23. data/lib/calificador/{spec → context}/test_method.rb +2 -31
  24. data/lib/calificador/{spec → context}/test_root.rb +3 -15
  25. data/lib/calificador/{spec/examine_context.rb → context/type_context.rb} +7 -10
  26. data/lib/calificador/key.rb +27 -15
  27. data/lib/calificador/minitest/minitest_patches.rb +0 -2
  28. data/lib/calificador/test.rb +1 -3
  29. data/lib/calificador/test_mixin.rb +143 -139
  30. data/lib/calificador/util/call_formatter.rb +5 -5
  31. data/lib/calificador/util/core_extensions.rb +104 -79
  32. data/lib/calificador/util/proxy_object.rb +63 -0
  33. data/lib/calificador/version.rb +1 -1
  34. metadata +22 -42
  35. data/lib/calificador/build/attribute_container.rb +0 -103
  36. data/lib/calificador/build/factory.rb +0 -132
  37. data/lib/calificador/spec/basic_context.rb +0 -353
  38. data/lib/calificador/spec/class_method_context.rb +0 -42
  39. data/lib/calificador/spec/instance_method_context.rb +0 -38
  40. data/lib/calificador/spec/test_environment.rb +0 -141
  41. data/lib/calificador/spec/value_override.rb +0 -37
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calificador
4
+ module Context
5
+ module Override
6
+ # Argument override
7
+ class ArgumentOverride < BasicOverride
8
+ # Configuration proxy to configure overrides
9
+ class ConfigProxy < Util::ProxyObject
10
+ def initialize(override:)
11
+ super()
12
+
13
+ @override = override
14
+ end
15
+
16
+ protected
17
+
18
+ def __respond_to_missing?(name:, include_all:)
19
+ METHOD_PATTERN =~ name || super
20
+ end
21
+
22
+ def __method_missing(name:, arguments:, keywords:, block:)
23
+ name = name.to_sym
24
+
25
+ if METHOD_PATTERN =~ name
26
+ ::Kernel.raise ::ArgumentError, "Property method '#{name}' cannot have arguments" unless arguments.empty?
27
+
28
+ @override.add_attribute(name: name, value: block) if block
29
+
30
+ ArgumentProxy.new(override: @override, name: name)
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ class ArgumentProxy < Util::ProxyObject
38
+ def initialize(override:, name:)
39
+ super()
40
+
41
+ @override = override
42
+ @name = name
43
+ end
44
+
45
+ def [](index, &block)
46
+ @override.add_attribute(name: index, value: block)
47
+ end
48
+ end
49
+
50
+ attr_reader :attributes
51
+
52
+ def initialize(attributes: {})
53
+ super()
54
+
55
+ @attributes = attributes.dup
56
+ end
57
+
58
+ def add_attribute(name:, value:)
59
+ @attributes[name] = value
60
+ end
61
+
62
+ def config(&block)
63
+ ConfigProxy.new(override: self).instance_exec(&block)
64
+ self
65
+ end
66
+
67
+ def apply(context:)
68
+ context.merge_operation_arguments(@attributes)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calificador
4
+ module Context
5
+ module Override
6
+ # Base class for overrides
7
+ class BasicOverride
8
+ def apply(context:)
9
+ raise NotImplementedError, "Subclasses must implement"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calificador
4
+ module Context
5
+ module Override
6
+ # Factory override
7
+ class FactoryOverride < BasicOverride
8
+ attr_reader :key, :function
9
+
10
+ def initialize(key:, function:)
11
+ raise ArgumentError, "Key must be a #{Key}, not '#{key}' (#{key.class})" unless key.is_a?(Key)
12
+
13
+ unless function.is_a?(Proc)
14
+ raise ArgumentError, "Function must be a #{Proc}, not '#{function}' (#{function.class})"
15
+ end
16
+
17
+ super()
18
+
19
+ @key = key
20
+ @function = function
21
+ end
22
+
23
+ def apply(context:)
24
+ key = @key.with_default(context.subject_key)
25
+ factory = context.override_factory(key: key)
26
+ factory.init_with = @function
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calificador
4
+ module Context
5
+ module Override
6
+ # Property override
7
+ class PropertyOverride < BasicOverride
8
+ # Configuration proxy to configure overrides
9
+ class ConfigProxy < Util::ProxyObject
10
+ def initialize(override:)
11
+ super()
12
+
13
+ @override = override
14
+ end
15
+
16
+ protected
17
+
18
+ def __respond_to_missing?(name:, include_all:)
19
+ METHOD_PATTERN =~ name
20
+ end
21
+
22
+ def __method_missing(name:, arguments:, keywords:, block:)
23
+ ::Kernel.raise ::ArgumentError, "Property method '#{name}' cannot have arguments" unless arguments.empty?
24
+
25
+ unless block
26
+ ::Kernel.raise ::ArgumentError, "Property method '#{name}' must have a block for the property value"
27
+ end
28
+
29
+ @override.add_attribute(name: name, value: block)
30
+ end
31
+ end
32
+
33
+ attr_reader :key, :attributes
34
+
35
+ def initialize(key:, attributes: {})
36
+ raise ArgumentError, "Key must be a #{Key}, not '#{key}' (#{key.class})" unless key.is_a?(Key)
37
+
38
+ super()
39
+
40
+ @key = key
41
+ @attributes = attributes.dup
42
+ end
43
+
44
+ def add_attribute(name:, value:)
45
+ @attributes[name] = value
46
+ end
47
+
48
+ def config(&block)
49
+ ConfigProxy.new(override: self).instance_exec(&block)
50
+ self
51
+ end
52
+
53
+ def apply(context:)
54
+ key = @key.with_default(context.subject_key)
55
+ factory = context.override_factory(key: key)
56
+ factory.add_overrides(attributes)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Calificador
6
+ module Context
7
+ # Environment to run test method
8
+ class TestEnvironment < BasicContext
9
+ # Placeholder for default values in method calls
10
+ class DefaultValue
11
+ include Singleton
12
+
13
+ def to_s
14
+ "<default>"
15
+ end
16
+
17
+ alias_method :inspect, :to_s
18
+ end
19
+
20
+ DEFAULT_VALUE = DefaultValue.instance
21
+
22
+ class Proxy < Util::ProxyObject
23
+ extend ::Forwardable
24
+
25
+ def initialize(environment:)
26
+ super()
27
+
28
+ @environment = environment
29
+ @test_instance = environment.test_instance
30
+ end
31
+
32
+ def assert(*arguments, &body)
33
+ if arguments.empty?
34
+ @environment.assert do
35
+ instance_exec(&body)
36
+ end
37
+ else
38
+ @test_instance.assert(*arguments, &body)
39
+ end
40
+ end
41
+
42
+ ruby2_keywords :assert
43
+
44
+ def refute(*arguments, &body)
45
+ if arguments.empty?
46
+ @environment.refute do
47
+ instance_exec(&body)
48
+ end
49
+ else
50
+ @test_instance.refute(*arguments, &body)
51
+ end
52
+ end
53
+
54
+ ruby2_keywords :refute
55
+
56
+ def_delegator :@environment, :subject
57
+ def_delegator :@environment, :create
58
+ def_delegator :@environment, :properties
59
+ def_delegator :@environment, :arguments
60
+
61
+ def _
62
+ Context::TestEnvironment::DEFAULT_VALUE
63
+ end
64
+
65
+ def call(*arguments, **keywords, &block)
66
+ @environment.call_operation(*arguments, **keywords, &block)
67
+ end
68
+
69
+ def result
70
+ @environment.result
71
+ end
72
+
73
+ protected
74
+
75
+ def __respond_to_missing?(name:, include_all:)
76
+ @environment.operation_name ||
77
+ !@environment.lookup_named_factory(name: name).nil? ||
78
+ @test_instance.respond_to?(name)
79
+ end
80
+
81
+ def __method_missing(name:, arguments:, keywords:, block:)
82
+ if name == @environment.operation_name
83
+ @environment.call_operation(*arguments, **keywords, &block)
84
+ else
85
+ factory = @environment.lookup_named_factory(name: name)
86
+
87
+ if factory
88
+ @environment.create_object(key: factory.key)
89
+ elsif @test_instance.respond_to?(name)
90
+ @test_instance.send(name, *arguments, **keywords, &block)
91
+ else
92
+ super
93
+ end
94
+ end
95
+ end
96
+
97
+ def singleton_method_added(name) # rubocop:disable Lint/MissingSuper
98
+ ::Kernel.raise "Adding methods (#{name}) inside test methods is not supported"
99
+ end
100
+ end
101
+
102
+ attr_reader :test_instance, :proxy
103
+
104
+ def initialize(parent:, test_instance:, overrides: [])
105
+ raise "Parent must be a #{TestMethod}" unless parent.is_a?(TestMethod)
106
+
107
+ super(
108
+ parent: parent,
109
+ subject_key: parent.subject_key,
110
+ description: parent.method_name.to_s,
111
+ overrides: overrides
112
+ )
113
+
114
+ @test_instance = test_instance
115
+ @subject = MISSING
116
+ @result = MISSING
117
+ @created_objects = {}
118
+ @current_assertor = nil
119
+ @proxy = Proxy.new(environment: self)
120
+ end
121
+
122
+ def subject(value = MISSING)
123
+ if value.equal?(MISSING)
124
+ if @subject.equal?(MISSING)
125
+ @result = MISSING
126
+ @subject = create_subject(environment: self)
127
+ end
128
+ else
129
+ @result = MISSING
130
+ @subject = value
131
+ end
132
+
133
+ @subject
134
+ end
135
+
136
+ def call_operation(*arguments, **keywords, &block)
137
+ effective_arguments, effective_options, effective_block = collect_arguments(
138
+ arguments: arguments,
139
+ keywords: keywords,
140
+ block: block
141
+ )
142
+
143
+ @result = if effective_arguments.empty?
144
+ if effective_options.empty?
145
+ subject.__send__(operation_name, &block)
146
+ else
147
+ subject.__send__(operation_name, **effective_options, &effective_block)
148
+ end
149
+ else
150
+ subject.__send__(operation_name, *effective_arguments, **effective_options, &effective_block)
151
+ end
152
+ end
153
+
154
+ def result
155
+ raise StandardError, "Method under test was not called yet, so there is no result" if @result == MISSING
156
+
157
+ @result
158
+ end
159
+
160
+ def create(type, trait = Key::NO_TRAIT)
161
+ create_object(key: Key[type, trait])
162
+ end
163
+
164
+ def assert(&block)
165
+ @current_assertor&.__check_triggered
166
+ @current_assertor = Assertor.new(handler: @test_instance, block: block)
167
+ end
168
+
169
+ def refute(&block)
170
+ @current_assertor&.__check_triggered
171
+ @current_assertor = Assertor.new(handler: @test_instance, negated: true, block: block)
172
+ end
173
+
174
+ def arguments(&block)
175
+ raise "Cannot override properties after method under test has been called" unless @result == EMPTY
176
+
177
+ super.then do |override|
178
+ override.apply(context: self)
179
+ end
180
+ end
181
+
182
+ def properties(type = nil, trait = Key::DEFAULT_TRAIT, &block)
183
+ raise "Cannot override properties after objects have been created" unless @created_objects.empty?
184
+
185
+ super.then do |override|
186
+ override.apply(context: self)
187
+ end
188
+ end
189
+
190
+ def create_object(key:)
191
+ @created_objects.fetch(key) do
192
+ factory = lookup_factory(key: key)
193
+
194
+ @created_objects[key] = if factory
195
+ factory.create(environment: self)
196
+ else
197
+ raise(KeyError, "No factory found for #{key}") if key.trait?
198
+
199
+ if key.type.include?(Singleton)
200
+ key.type.instance
201
+ elsif key.type.is_a?(Class)
202
+ method = key.type.method(:new)
203
+
204
+ if method.required_arguments?
205
+ raise KeyError, "Class #{key} has no default constructor, cannot create without factory"
206
+ end
207
+
208
+ key.type.send(:new)
209
+ elsif key.type.is_a?(Module)
210
+ key.type
211
+ else
212
+ raise(KeyError, "Cannot create object for #{key} without factory")
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ def done(error:)
219
+ @current_assertor&.__check_triggered unless error
220
+ @current_assertor = nil
221
+ end
222
+
223
+ def run_test
224
+ if parent.expected_to_fail
225
+ passed = begin
226
+ @proxy.instance_exec(&parent.body)
227
+ true
228
+ rescue ::Minitest::Assertion => e
229
+ @test_instance.pass(e.message)
230
+ false
231
+ end
232
+
233
+ @test_instance.flunk("Expected test to fail") if passed
234
+ else
235
+ @proxy.instance_exec(&parent.body)
236
+ end
237
+
238
+ done(error: false)
239
+ rescue StandardError
240
+ done(error: true)
241
+ raise
242
+ end
243
+
244
+ def to_s
245
+ "#{self.class.name}(#{@test_instance.name})"
246
+ end
247
+
248
+ protected
249
+
250
+ def collect_arguments(arguments:, keywords:, block:)
251
+ default_arguments = operation_arguments
252
+
253
+ arguments = arguments.each_with_index.map do |argument, index|
254
+ if argument.equal?(DEFAULT_VALUE)
255
+ unless default_arguments.key?(index)
256
+ raise "Please provide a default value for positional argument ##{index} of '#{operation_name}'"
257
+ end
258
+
259
+ config = default_arguments[index]
260
+ argument = proxy.instance_exec(&config)
261
+ end
262
+
263
+ argument
264
+ end
265
+
266
+ keywords = keywords.map do |name, value|
267
+ if value.equal?(DEFAULT_VALUE)
268
+ unless default_arguments.key?(name)
269
+ raise "Please provide a default value for keyword argument '#{name}' of '#{operation_name}'"
270
+ end
271
+
272
+ config = default_arguments[name]
273
+ value = proxy.instance_exec(&config)
274
+ end
275
+
276
+ [name, value]
277
+ end.to_h
278
+
279
+ [arguments, keywords, block]
280
+ end
281
+ end
282
+ end
283
+ end