calificador 0.1.0

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