calificador 0.1.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.
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Calificador::Util::CoreExtensions
4
+
5
+ module Calificador
6
+ module Build
7
+ # Factory calss
8
+ class Factory < AttributeContainer
9
+ # Configuration derived factories
10
+ class Dsl < AttributeContainer::Dsl
11
+ def trait(name, description = nil, &block)
12
+ factory = Build::Factory.new(
13
+ context: @delegate.context,
14
+ parent: @delegate,
15
+ key: @delegate.key.with(name),
16
+ name: [@delegate.name, name].compact.join("_"),
17
+ source_location: block.source_location,
18
+ description: description || __default_trait_description(trait: name)
19
+ )
20
+
21
+ factory.dsl_config(&block)
22
+ @delegate.context.add_factory(factory)
23
+ end
24
+
25
+ protected
26
+
27
+ def __default_trait_description(trait:)
28
+ trait.to_s.gsub("_", " ").chomp
29
+ end
30
+ end
31
+
32
+ attr_reader :key, :context, :name, :source_location
33
+ attr_accessor :init_with
34
+
35
+ def initialize(context:, parent: nil, key:, name:, description: nil, source_location:, values: nil)
36
+ super(parent: parent, description: description)
37
+
38
+ raise "Parent factory must have same type" unless parent.nil? || parent.key.type == key.type
39
+
40
+ @context = context
41
+ @key = key
42
+ @name = name.to_sym
43
+ @source_location = source_location
44
+ @values = values&.dup || {}
45
+ end
46
+
47
+ def create(context:)
48
+ evaluator = AttributeEvaluator.new(context: context)
49
+
50
+ collect_attributes_and_values(evaluator: evaluator)
51
+
52
+ before_create(evaluator: evaluator)
53
+
54
+ object = create_object(evaluator: evaluator)
55
+
56
+ set_properties(object: object, evaluator: evaluator)
57
+
58
+ after_create(evaluator: evaluator, object: object)
59
+
60
+ object
61
+ end
62
+
63
+ def add_values(values)
64
+ @values.merge!(values)
65
+ end
66
+
67
+ def setup(test_class:); end
68
+
69
+ protected
70
+
71
+ def collect_attributes_and_values(evaluator:)
72
+ @parent&.collect_attributes_and_values(evaluator: evaluator)
73
+ evaluator.add_attributes(@attributes.values)
74
+ evaluator.add_values(@values)
75
+ end
76
+
77
+ def before_create(evaluator:)
78
+ @parent&.before_create(evaluator: evaluator)
79
+ evaluator.evaluate(&@before_create) unless @before_create.nil?
80
+ end
81
+
82
+ def after_create(evaluator:, object:)
83
+ @parent&.after_create(evaluator: evaluator, object: object)
84
+ evaluator.evaluate(object, &@after_create) unless @after_create.nil?
85
+ end
86
+
87
+ def nearest_init_with
88
+ @init_with || @parent&.nearest_init_with
89
+ end
90
+
91
+ def create_object(evaluator:)
92
+ init_with = nearest_init_with
93
+
94
+ if init_with.nil?
95
+ call_initializer(evaluator: evaluator)
96
+ else
97
+ evaluator.evaluate(&init_with)
98
+ end
99
+ end
100
+
101
+ def set_properties(object:, evaluator:)
102
+ evaluator.attributes.each_value do |attribute|
103
+ object.send(:"#{attribute.name}=", evaluator.value(name: attribute.name)) if attribute.type == :property
104
+ end
105
+
106
+ evaluator.values.each do |name, value| # rubocop:disable Style/HashEachMethods
107
+ object.send(:"#{name}=", value) unless evaluator.attribute?(name: name)
108
+ end
109
+ end
110
+
111
+ def call_initializer(evaluator:)
112
+ parameters = []
113
+ options = {}
114
+
115
+ @key.type.instance_method(:initialize).parameters.each do |type, name|
116
+ case type
117
+ when :req
118
+ parameters << evaluator.value(name: name)
119
+ when :opt
120
+ parameters << evaluator.value(name: name) if evaluator.attribute?(name: name)
121
+ when :keyreq
122
+ options[name] = evaluator.value(name: name)
123
+ when :key
124
+ options[name] = evaluator.value(name: name) if evaluator.attribute?(name: name)
125
+ end
126
+ end
127
+
128
+ @key.type.new(*parameters, **options)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calificador
4
+ module Build
5
+ # Trait description
6
+ class Trait < AttributeContainer
7
+ # Configuration proxy for traits
8
+ class Dsl < AttributeContainer::Dsl
9
+ end
10
+
11
+ attr_reader :name
12
+
13
+ def initialize(parent:, name:, description: nil)
14
+ super(parent: parent, description: description)
15
+
16
+ @name = name.to_sym
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+
5
+ using Calificador::Util::CoreExtensions
6
+
7
+ module Calificador
8
+ # Test subject key
9
+ class Key
10
+ DEFAULT_TRAIT = :"<default>"
11
+ INHERITED_TRAIT = :"<inherited>"
12
+
13
+ class << self
14
+ def [](type, trait = DEFAULT_TRAIT)
15
+ new(type: type, trait: trait)
16
+ end
17
+ end
18
+
19
+ attr_reader :type, :trait
20
+
21
+ def initialize(type:, trait: DEFAULT_TRAIT)
22
+ raise ArgumentError, "Illegal trait value #{trait}" if trait == INHERITED_TRAIT
23
+
24
+ @type = type
25
+ @trait = trait || DEFAULT_TRAIT
26
+ end
27
+
28
+ def hash
29
+ (@type.hash * 31) + @trait.hash
30
+ end
31
+
32
+ def ==(other)
33
+ (@type == other.type) && (@trait == other.trait)
34
+ end
35
+
36
+ alias_method :eql?, :==
37
+
38
+ def to_s
39
+ trait == DEFAULT_TRAIT ? type.to_s : "#{type} (#{trait})"
40
+ end
41
+
42
+ alias_method :inspect, :to_s
43
+
44
+ def with(trait)
45
+ case trait
46
+ when INHERITED_TRAIT
47
+ self
48
+ when nil, DEFAULT_TRAIT
49
+ @trait == DEFAULT_TRAIT ? self : Key.new(type: @type, trait: DEFAULT_TRAIT)
50
+ else
51
+ trait == @trait ? self : Key.new(type: @type, trait: trait)
52
+ end
53
+ end
54
+
55
+ def trait?
56
+ @trait != DEFAULT_TRAIT
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/test"
4
+
5
+ using Calificador::Util::CoreExtensions
6
+
7
+ module Calificador
8
+ module Minitest
9
+ # Patches to minitest classes
10
+ module MinitestPatches
11
+ # Patches to Minitest::Assertion
12
+ module AssertionMethods
13
+ def location
14
+ last_before_assertion = ""
15
+
16
+ backtrace.reverse_each do |s|
17
+ break if s =~ %r{assertor|in .(assert|refute|flunk|pass|fail|raise|must|wont)}
18
+
19
+ last_before_assertion = s
20
+ end
21
+
22
+ last_before_assertion.sub(%r{:in .*$}, "")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Calificador::Util::CoreExtensions
4
+
5
+ module Calificador
6
+ module Spec
7
+ class BasicContext
8
+ class Dsl
9
+ def initialize(delegate:)
10
+ @delegate = delegate
11
+ end
12
+
13
+ def factory(type, description = nil, name: nil, &block)
14
+ key = Key[type]
15
+ name ||= BasicContext.default_factory_name(test_class: @delegate.test_class, key: key)
16
+
17
+ factory = Build::Factory.new(
18
+ context: @delegate,
19
+ key: key,
20
+ name: name,
21
+ description: description,
22
+ source_location: block.source_location
23
+ )
24
+
25
+ factory.dsl_config(&block)
26
+ @delegate.add_factory(factory)
27
+ end
28
+
29
+ def examine(subject_type, description = nil, trait: Key::DEFAULT_TRAIT, **values, &block)
30
+ subject_key = Key[subject_type, trait == Key::INHERITED_TRAIT ? Key::DEFAULT_TRAIT : trait]
31
+ overrides = BasicContext.collect_overrides(values: values, default_key: subject_key)
32
+
33
+ description ||= BasicContext.default_examine_description(
34
+ test_class: @delegate.test_class,
35
+ subject_key: subject_key,
36
+ values: values
37
+ )
38
+
39
+ context = Spec::ExamineContext.new(
40
+ parent: @delegate,
41
+ subject_key: subject_key,
42
+ description: description,
43
+ overrides: overrides
44
+ )
45
+
46
+ context.dsl_config(&block)
47
+
48
+ @delegate.add_context(context, &block)
49
+ end
50
+
51
+ def method(method, description = nil, &block)
52
+ context = Spec::InstanceMethodContext.new(
53
+ parent: @delegate,
54
+ method: method,
55
+ description: description || "\##{method}"
56
+ )
57
+
58
+ context.dsl_config(&block)
59
+
60
+ @delegate.add_context(context, &block)
61
+ end
62
+
63
+ def class_method(method, description = nil, &block)
64
+ context = Spec::ClassMethodContext.new(
65
+ parent: @delegate,
66
+ method: method,
67
+ description: description || ".#{method}"
68
+ )
69
+
70
+ context.dsl_config(&block)
71
+
72
+ @delegate.add_context(context, &block)
73
+ end
74
+
75
+ def must(description, trait: Key::INHERITED_TRAIT, **values, &block)
76
+ subject_key = @delegate.subject_key.with(trait)
77
+ overrides = BasicContext.collect_overrides(values: values, default_key: subject_key)
78
+
79
+ context = Spec::TestMethod.new(
80
+ parent: @delegate,
81
+ subject_key: subject_key,
82
+ description: "must #{description}",
83
+ overrides: overrides,
84
+ expected_to_fail: false,
85
+ body: block
86
+ )
87
+
88
+ @delegate.add_context(context)
89
+ end
90
+
91
+ def must_fail(description, trait:, **values, &block)
92
+ subject_key = @delegate.subject_key.with(trait)
93
+ overrides = BasicContext.collect_overrides(values: values, default_key: subject_key)
94
+
95
+ context = Spec::TestMethod.new(
96
+ parent: @delegate,
97
+ subject_key: subject_key,
98
+ description: "must fail #{description}",
99
+ overrides: overrides,
100
+ expected_to_fail: true,
101
+ body: block
102
+ )
103
+
104
+ @delegate.add_context(context)
105
+ end
106
+
107
+ def with(description, trait: Key::INHERITED_TRAIT, **values, &block)
108
+ __condition(conjunction: "with", description: description, trait: trait, values: values, &block)
109
+ end
110
+
111
+ def without(description, trait: Key::INHERITED_TRAIT, **values, &block)
112
+ __condition(conjunction: "without", description: description, trait: trait, values: values, &block)
113
+ end
114
+
115
+ def where(description, trait: Key::INHERITED_TRAIT, **values, &block)
116
+ __condition(conjunction: "where", description: "description", trait: trait, values: values, &block)
117
+ end
118
+
119
+ protected
120
+
121
+ def __condition(conjunction:, description:, trait: Key::INHERITED_TRAIT, values: {}, &block)
122
+ if description.is_a?(Symbol) && trait == Key::INHERITED_TRAIT
123
+ trait = description
124
+ description = nil
125
+ end
126
+
127
+ subject_key = @delegate.subject_key.with(trait)
128
+ overrides = BasicContext.collect_overrides(values: values, default_key: subject_key)
129
+
130
+ description ||= __default_description(key: subject_key)
131
+
132
+ context = Spec::ConditionContext.new(
133
+ parent: @delegate,
134
+ subject_key: subject_key,
135
+ description: description,
136
+ overrides: overrides
137
+ )
138
+
139
+ context.dsl_config(&block)
140
+
141
+ @delegate.add_context(context, &block)
142
+ end
143
+
144
+ def __default_description(key:)
145
+ raise "Please provide a description or a trait" if key.trait == Key::INHERITED_TRAIT
146
+
147
+ factory = @delegate.lookup_factory(key: key)
148
+
149
+ raise "No factory defined for #{key}" if factory.nil?
150
+
151
+ factory.description
152
+ end
153
+ end
154
+
155
+ class << self
156
+ def default_factory_name(test_class:, key:)
157
+ name = key.type.name.delete_prefix(test_class.parent_prefix).gsub("::", "").snake_case
158
+
159
+ name = "#{name}_#{key.trait}" if key.trait?
160
+
161
+ name.to_sym
162
+ end
163
+
164
+ def default_examine_description(test_class:, subject_key:, values:)
165
+ description = StringIO.new
166
+ description << subject_key.type.name.delete_prefix(test_class.parent_prefix)
167
+
168
+ description << " (" << subject_key.trait.to_s.gsub("_", " ") << ")" if subject_key.trait?
169
+
170
+ append_values_description(description: description, values: values)
171
+
172
+ description.string
173
+ end
174
+
175
+ def collect_overrides(values:, default_key:)
176
+ result = Hash.new { |hash, key| hash[key] = ValueOverride.new(key: key) }
177
+ default_values = {}
178
+
179
+ values.each do |key, value|
180
+ case key
181
+ when Symbol
182
+ default_values[key] = value
183
+ when Class
184
+ key = Key[key]
185
+ result[key] = result[key].merge_values(value)
186
+ when Key
187
+ result[key] = result[key].merge_values(value)
188
+ else
189
+ raise ArgumentError, "Illegal override key '#{key}'"
190
+ end
191
+ end
192
+
193
+ result[default_key] = result[default_key].merge_values(default_values) unless default_values.empty?
194
+ result.default_proc = nil
195
+
196
+ result
197
+ end
198
+
199
+ protected
200
+
201
+ def append_values_description(description:, values:)
202
+ unless values.empty?
203
+ values.each_with_index do |(name, value), index|
204
+ description << case index
205
+ when 0
206
+ " where "
207
+ when values.size - 1
208
+ " and "
209
+ else
210
+ ", "
211
+ end
212
+
213
+ description << name
214
+
215
+ description << case value
216
+ when NilClass
217
+ "is not set"
218
+ when TrueClass
219
+ "is true"
220
+ when FalseClass
221
+ "is false"
222
+ else
223
+ "is set"
224
+ end
225
+ end
226
+ end
227
+
228
+ description
229
+ end
230
+ end
231
+
232
+ attr_reader :description, :parent, :overrides
233
+
234
+ def initialize(parent:, subject_key:, description:, overrides:)
235
+ @parent = parent
236
+ @subject_key = subject_key
237
+ @description = description
238
+
239
+ @children = []
240
+
241
+ factories = overrides.map do |key, override|
242
+ parent_factory = parent.lookup_factory(key: key)
243
+
244
+ factory = Build::Factory.new(
245
+ parent: parent_factory,
246
+ context: self,
247
+ key: key,
248
+ name: parent_factory&.name || BasicContext.default_factory_name(test_class: test_class, key: key),
249
+ source_location: Kernel.caller_locations(0, 1).first
250
+ )
251
+
252
+ factory.add_values(override.values)
253
+
254
+ factory
255
+ end
256
+
257
+ @factories = factories.map { |f| [f.key, f] }.to_h
258
+ @named_factories = factories.map { |f| [f.name, f] }.to_h
259
+ end
260
+
261
+ def setup; end
262
+
263
+ def subtree_root?
264
+ false
265
+ end
266
+
267
+ def add_context(context, &block)
268
+ @children << context
269
+
270
+ context.setup
271
+ end
272
+
273
+ def test_class
274
+ parent&.test_class || raise(StandardError, "No parent context defines a test class")
275
+ end
276
+
277
+ def subject_key
278
+ @subject_key || (parent.subject_key unless subtree_root? || parent.nil?)
279
+ end
280
+
281
+ def context_path(subtree: true)
282
+ add_context_to_path([], subtree: subtree).freeze
283
+ end
284
+
285
+ def full_description
286
+ context_path.reduce(StringIO.new) do |description, context|
287
+ description << " " if description.length.positive? && context.separate_description_by_space?
288
+ description << context.description
289
+ end.string
290
+ end
291
+
292
+ def root
293
+ result = self
294
+ result = result.parent until result.parent.nil?
295
+ result
296
+ end
297
+
298
+ def add_factory(factory)
299
+ raise KeyError, "Factory for type #{factory.key.type} already defined" if @factories.key?(factory.key.type)
300
+ raise KeyError, "Factory with name #{factory.name} already defined" if @named_factories.key?(factory.name)
301
+
302
+ @factories[factory.key] = factory
303
+ @named_factories[factory.name] = factory
304
+
305
+ test_class.__define_factory_method(factory: factory)
306
+ end
307
+
308
+ def factories
309
+ @factories.dup.freeze
310
+ end
311
+
312
+ def named_factories
313
+ @named_factories.dup.freeze
314
+ end
315
+
316
+ def lookup_factory(key:)
317
+ @factories[key] || @parent&.lookup_factory(key: key)
318
+ end
319
+
320
+ def lookup_named_factory(name:)
321
+ @named_factories[name] || @parent&.lookup_named_factory(name: name)
322
+ end
323
+
324
+ def create_subject(environment:, subject_key:)
325
+ @parent.create_subject(environment: environment, subject_key: subject_key)
326
+ end
327
+
328
+ def create_result(subject:, arguments:, options:, block:)
329
+ @parent.create_result(subject: subject, arguments: arguments, options: options, block: block)
330
+ end
331
+
332
+ protected
333
+
334
+ def effective_overrides
335
+ context_path.each_with_object({}) do |context, overrides|
336
+ overrides.merge!(context.overrides) do |_key, existing_override, additional_override|
337
+ existing_override.merge(additional_override)
338
+ end
339
+ end
340
+ end
341
+
342
+ def add_context_to_path(path, subtree: true)
343
+ @parent.add_context_to_path(path, subtree: subtree) unless @parent.nil? || (subtree && subtree_root?)
344
+
345
+ path << self
346
+ end
347
+
348
+ def separate_description_by_space?
349
+ true
350
+ end
351
+ end
352
+ end
353
+ end