calificador 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/lib/calificador.rb +44 -0
- data/lib/calificador/assertor.rb +134 -0
- data/lib/calificador/build/attribute.rb +20 -0
- data/lib/calificador/build/attribute_container.rb +103 -0
- data/lib/calificador/build/attribute_evaluator.rb +85 -0
- data/lib/calificador/build/factory.rb +132 -0
- data/lib/calificador/build/trait.rb +20 -0
- data/lib/calificador/key.rb +59 -0
- data/lib/calificador/minitest/minitest_patches.rb +27 -0
- data/lib/calificador/spec/basic_context.rb +353 -0
- data/lib/calificador/spec/class_method_context.rb +42 -0
- data/lib/calificador/spec/condition_context.rb +10 -0
- data/lib/calificador/spec/examine_context.rb +29 -0
- data/lib/calificador/spec/instance_method_context.rb +38 -0
- data/lib/calificador/spec/test_environment.rb +141 -0
- data/lib/calificador/spec/test_method.rb +64 -0
- data/lib/calificador/spec/test_root.rb +44 -0
- data/lib/calificador/spec/type_context.rb +29 -0
- data/lib/calificador/spec/value_override.rb +37 -0
- data/lib/calificador/test.rb +12 -0
- data/lib/calificador/test_mixin.rb +264 -0
- data/lib/calificador/util/call_formatter.rb +41 -0
- data/lib/calificador/util/class_mixin.rb +14 -0
- data/lib/calificador/util/core_extensions.rb +135 -0
- data/lib/calificador/util/missing.rb +26 -0
- data/lib/calificador/util/nil.rb +59 -0
- data/lib/calificador/version.rb +5 -0
- metadata +257 -0
@@ -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,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
|