calificador 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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