calificador 0.1.0

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