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,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+
5
+ using Calificador::Util::CoreExtensions
6
+
7
+ module Calificador
8
+ # Mixin for unit tests
9
+ module TestMixin
10
+ Key = Calificador::Key
11
+
12
+ class << self
13
+ def included(includer)
14
+ includer.extend(ClassMethods)
15
+ end
16
+
17
+ def prepended(prepender)
18
+ prepender.extend(ClassMethods)
19
+ end
20
+ end
21
+
22
+ def __run_test(test_method:)
23
+ stack_marker = "calificador_test_#{SecureRandom.uuid.gsub("-", "_")}"
24
+
25
+ __register_current_run(stack_marker: stack_marker, test_method: test_method)
26
+
27
+ begin
28
+ instance_eval(<<~METHOD, stack_marker, 1)
29
+ test_method.run_test(test: self)
30
+ METHOD
31
+ ensure
32
+ __unregister_current_run(stack_marker: stack_marker)
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ TestRun = Struct.new(:test_instance, :test_method, keyword_init: true)
39
+
40
+ def __register_current_run(stack_marker:, test_method:)
41
+ @__calificador_current_test_run = TestRun.new(test_instance: self, test_method: test_method)
42
+ Calificador::Test.__register_current_run(stack_marker: stack_marker, test_run: @__calificador_current_test_run)
43
+ end
44
+
45
+ def __unregister_current_run(stack_marker:)
46
+ if !instance_variable_defined?(:@__calificador_current_test_run) || @__calificador_current_test_run.nil?
47
+ raise StandardError, "No current test run registered"
48
+ end
49
+
50
+ Calificador::Test.__unregister_current_run(stack_marker: stack_marker)
51
+ @__calificador_current_test_run = nil
52
+ end
53
+
54
+ def __current_test_run
55
+ if !instance_variable_defined?(:@__calificador_current_test_run) || @__calificador_current_test_run.nil?
56
+ raise StandardError, "No current test run registered"
57
+ end
58
+
59
+ @__calificador_current_test_run
60
+ end
61
+
62
+ # Class methods for unit tests
63
+ module ClassMethods
64
+ def run_all_tests(reporter: MiniTest::CompositeReporter.new)
65
+ runnable_methods.each do |method|
66
+ run_one_method(self, method, reporter)
67
+ end
68
+ end
69
+
70
+ def examines(type = MISSING, trait: MISSING)
71
+ self.__subject_type = type unless type.equal?(MISSING)
72
+ self.__subject_trait = trait unless trait.equal?(MISSING)
73
+
74
+ Key[__subject_type, __subject_trait]
75
+ end
76
+
77
+ def factory(type, description = nil, name: nil, &block)
78
+ __root_context.dsl_config do
79
+ factory(type, description, name: nil, &block)
80
+ end
81
+ end
82
+
83
+ def examine(subject_type, description = nil, trait: Key::INHERITED_TRAIT, **values, &block)
84
+ __root_context.dsl_config do
85
+ examine(subject_type, description, trait: trait, **values, &block)
86
+ end
87
+ end
88
+
89
+ def method(method, description = nil, **values, &block)
90
+ __root_context.dsl_config do
91
+ method(method, description, &block)
92
+ end
93
+ end
94
+
95
+ def class_method(method, description = nil, **values, &block)
96
+ __root_context.dsl_config do
97
+ class_method(method, description, &block)
98
+ end
99
+ end
100
+
101
+ def must(description, trait: Key::INHERITED_TRAIT, **values, &block)
102
+ __root_context.dsl_config do
103
+ must(description, trait: trait, **values, &block)
104
+ end
105
+ end
106
+
107
+ def must_fail(description, trait: Key::INHERITED_TRAIT, **values, &block)
108
+ __root_context.dsl_config do
109
+ must_fail(description, trait: trait, **values, &block)
110
+ end
111
+ end
112
+
113
+ def with(description, trait: Key::INHERITED_TRAIT, **values, &block)
114
+ __root_context.dsl_config do
115
+ with(description, trait: trait, **values, &block)
116
+ end
117
+ end
118
+
119
+ def without(description, trait: Key::INHERITED_TRAIT, **values, &block)
120
+ __root_context.dsl_config do
121
+ without(description, trait: trait, **values, &block)
122
+ end
123
+ end
124
+
125
+ def where(description, trait: Key::INHERITED_TRAIT, **values, &block)
126
+ __root_context.dsl_config do
127
+ where(description, trait: trait, **values, &block)
128
+ end
129
+ end
130
+
131
+ def body(&block)
132
+ class_eval(&block)
133
+ end
134
+
135
+ def __subject_type
136
+ if !instance_variable_defined?(:@__calificador_subject_type) || @__calificador_subject_type.nil?
137
+ type_name = name.gsub(%r{(?<=\w)Test\z}, "")
138
+
139
+ if Kernel.const_defined?(type_name)
140
+ @__calificador_subject_type = Kernel.const_get(type_name)
141
+ else
142
+ raise StandardError, "Cannot determine test subject type from test class name '#{name}'"
143
+ end
144
+ end
145
+
146
+ @__calificador_subject_type
147
+ end
148
+
149
+ def __subject_type=(type)
150
+ if instance_variable_defined?(:@__calificador_subject_type) && !@__calificador_subject_type.nil?
151
+ raise StandardError, "Cannot redefine test subject type"
152
+ end
153
+
154
+ @__calificador_subject_type = type
155
+ end
156
+
157
+ def __subject_trait
158
+ if !instance_variable_defined?(:@__calificador_subject_trait) || @__calificador_subject_trait.nil?
159
+ @__calificador_subject_trait = Key::DEFAULT_TRAIT
160
+ end
161
+
162
+ @__calificador_subject_trait
163
+ end
164
+
165
+ def __subject_trait=(trait)
166
+ if instance_variable_defined?(:@__calificador_subject_trait) && !@__calificador_subject_trait.nil?
167
+ raise StandardError, "Cannot redefine test subject trait"
168
+ end
169
+
170
+ @__calificador_subject_trait = trait
171
+ end
172
+
173
+ def __root_context
174
+ if !instance_variable_defined?(:@__calificador_root_context) || @__calificador_root_context.nil?
175
+ description = __subject_type.name.delete_prefix(parent_prefix)
176
+ description = "#{description} {#{__subject_trait}}" unless __subject_trait.equal?(Key::DEFAULT_TRAIT)
177
+
178
+ @__calificador_root_context = Spec::TestRoot.new(
179
+ test_class: self,
180
+ subject_key: Key[__subject_type, __subject_trait],
181
+ description: description
182
+ )
183
+ end
184
+
185
+ @__calificador_root_context
186
+ end
187
+
188
+ def __register_current_run(stack_marker:, test_run:)
189
+ __calificador_test_lock.synchronize do
190
+ raise KeyError, "Test run #{stack_marker} already registered" if __calificador_test_runs.key?(stack_marker)
191
+
192
+ __calificador_test_runs[stack_marker] = test_run
193
+ end
194
+ end
195
+
196
+ def __unregister_current_run(stack_marker:)
197
+ __calificador_test_lock.synchronize do
198
+ if __calificador_test_runs.delete(stack_marker).nil?
199
+ raise KeyError, "Could not unregister test #{stack_marker}"
200
+ end
201
+ end
202
+ end
203
+
204
+ def __current_test_run
205
+ __calificador_test_lock.synchronize do
206
+ location = ::Kernel.caller_locations.find do |l|
207
+ %r{\Acalificador_test_[a-zA-Z0-9_]+\z} =~ l.path
208
+ end
209
+
210
+ raise StandardError, "Could not find current test run in call stack" unless location
211
+
212
+ __calificador_test_runs.fetch(location.path) do
213
+ raise KeyError, "No test run registered for #{location.path}"
214
+ end
215
+ end
216
+ end
217
+
218
+ def __factory_methods
219
+ @__calificador_factory_methods = Set.new unless instance_variable_defined?(:@__calificador_factory_methods)
220
+
221
+ @__calificador_factory_methods.dup.freeze
222
+ end
223
+
224
+ def __define_factory_method(factory:)
225
+ @__calificador_factory_methods = Set.new unless instance_variable_defined?(:@__calificador_factory_methods)
226
+
227
+ if @__calificador_factory_methods.add?(factory.name)
228
+ factory_method_name = if factory.name.to_s.start_with?("test_")
229
+ "create_#{factory.name}"
230
+ else
231
+ factory.name
232
+ end
233
+
234
+ if method_defined?(factory_method_name, true)
235
+ raise "Cannot define factory method #{factory_method_name}, method already exists in #{self.class}"
236
+ end
237
+
238
+ type = factory.key.type # rubocop:disable Lint/UselessAssignment
239
+ trait = factory.key.trait # rubocop:disable Lint/UselessAssignment
240
+
241
+ class_eval(<<~METHOD, factory.source_location.first, factory.source_location.last)
242
+ define_method(factory_method_name) do
243
+ __current_test_run.test_method.create(type: type, trait: trait)
244
+ end
245
+ METHOD
246
+ end
247
+ end
248
+
249
+ protected
250
+
251
+ def __calificador_test_runs
252
+ @__calificador_test_runs = {} unless instance_variable_defined?(:@__calificador_test_runs)
253
+
254
+ @__calificador_test_runs
255
+ end
256
+
257
+ def __calificador_test_lock
258
+ @__calificador_test_lock = Mutex.new unless instance_variable_defined?(:@__calificador_test_lock)
259
+
260
+ @__calificador_test_lock
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pp"
4
+
5
+ module Calificador
6
+ module Util
7
+ class CallFormatter
8
+ def method(method:, arguments: [], options: {})
9
+ info = ::StringIO.new
10
+ info << method
11
+
12
+ unless arguments.empty? && options.empty?
13
+ info << "("
14
+
15
+ arguments.each_with_index do |argument, i|
16
+ info << ", " unless i.zero?
17
+ append_value(value: argument, out: info)
18
+ end
19
+
20
+ options.each_with_index do |(name, value), i|
21
+ info << ", " unless i.zero? && arguments.empty?
22
+ info << name << ": "
23
+ append_value(value: value, out: info)
24
+ end
25
+
26
+ info << ")"
27
+ end
28
+
29
+ info.string
30
+ end
31
+
32
+ def value(value:)
33
+ append_value(value: value, out: StringIO.new).string
34
+ end
35
+
36
+ def append_value(value:, out:)
37
+ PP.singleline_pp(value, out)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calificador
4
+ module Util
5
+ # Patches for Class objects
6
+ module ClassMixin
7
+ def with(trait)
8
+ Key[self, trait]
9
+ end
10
+
11
+ alias + with
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "docile"
4
+ require "ostruct"
5
+
6
+ module Calificador
7
+ module Util
8
+ # Extensions to core classes
9
+ module CoreExtensions
10
+ module_function
11
+
12
+ def map_call_arguments(signature:, arguments:, options:)
13
+ min_argument_count = 0
14
+ max_argument_count = 0
15
+ option_names = Set.new
16
+
17
+ signature.each do |type, name|
18
+ case type
19
+ when :req
20
+ min_argument_count += 1
21
+ max_argument_count += 1
22
+ when :opt
23
+ max_argument_count += 1
24
+ when :rest
25
+ max_argument_count = nil
26
+ when :keyreq
27
+ raise ArgumentError, "Required option #{name} missing for #{self} #{signature}" unless options.key?(name)
28
+
29
+ option_names << name
30
+ when :key
31
+ option_names << name
32
+ when :keyrest
33
+ option_names += options.keys
34
+ when :block
35
+ # ignore
36
+ else
37
+ raise ArgumentError, "Illegal parameter type #{type} for #{self} #{signature}"
38
+ end
39
+ end
40
+
41
+ argument_count = arguments.size
42
+ argument_count = max_argument_count if max_argument_count && argument_count > max_argument_count
43
+
44
+ raise ArgumentError, "Not enough parameters to call proc with #{signature}" if argument_count < min_argument_count
45
+
46
+ arguments = arguments[0...argument_count]
47
+ options = options.slice(*option_names)
48
+
49
+ [arguments, options]
50
+ end
51
+
52
+ refine Object do
53
+ def to_bool
54
+ self ? true : false
55
+ end
56
+
57
+ def dsl_config(&block)
58
+ target = self
59
+
60
+ if block
61
+ dsl = self.class.const_get(:Dsl).new(delegate: target)
62
+ Docile.dsl_eval(dsl, &block)
63
+ end
64
+
65
+ target
66
+ end
67
+ end
68
+
69
+ refine String do
70
+ def snake_case
71
+ gsub(%r{(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])}, "_").downcase
72
+ end
73
+
74
+ def camel_case
75
+ gsub(%r{(?:_+|^)([a-z])}) do
76
+ Regexp.last_match(1).upcase
77
+ end
78
+ end
79
+ end
80
+
81
+ refine Module do
82
+ def parent_module
83
+ @__calificador_parent_module ||= Nil[parent_name&.then { |m| const_get(m) }]
84
+ @__calificador_parent_module.unmask_nil
85
+ end
86
+
87
+ def parent_prefix
88
+ parent_module ? "#{parent_module}::" : ""
89
+ end
90
+
91
+ def base_name
92
+ @__calificador_base_name ||= if %r{(?:^|::)(?<base_name>(?:[^:]|:[^:])+)\z} =~ name
93
+ %r{\A#<.+>\z} =~ base_name ? Nil.instance : base_name
94
+ else
95
+ Nil.instance
96
+ end
97
+
98
+ @__calificador_base_name.unmask_nil
99
+ end
100
+
101
+ def parent_name
102
+ @__calificador_parent_name ||= if %r{(?<parent_name>\A.*)::} =~ name
103
+ %r{#<} =~ parent_name ? Nil.instance : parent_name
104
+ else
105
+ Nil.instance
106
+ end
107
+
108
+ @__calificador_parent_name.unmask_nil
109
+ end
110
+ end
111
+
112
+ [Method, Proc].each do |callable|
113
+ refine callable do
114
+ def map_call_arguments(*arguments, **options)
115
+ CoreExtensions.map_call_arguments(signature: parameters, arguments: arguments, options: options)
116
+ end
117
+
118
+ def invoke(*arguments, **options, &block)
119
+ arguments, options = map_call_arguments(*arguments, **options)
120
+ call(*arguments, **options, &block)
121
+ end
122
+
123
+ def invoke_with_target(target, *arguments, **options)
124
+ arguments, options = map_call_arguments(*arguments, **options)
125
+ target.instance_exec(*arguments, **options, &self)
126
+ end
127
+
128
+ def source_location_info
129
+ source_location ? source_location.join(":") : nil
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end