duck_testing 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/.rspec +3 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +7 -0
- data/Guardfile +19 -0
- data/LICENSE +22 -0
- data/README.md +75 -0
- data/Rakefile +8 -0
- data/duck_testing.gemspec +28 -0
- data/lib/duck_testing.rb +19 -0
- data/lib/duck_testing/errors.rb +4 -0
- data/lib/duck_testing/method_call_data.rb +19 -0
- data/lib/duck_testing/reporter/base.rb +17 -0
- data/lib/duck_testing/reporter/raise_error.rb +30 -0
- data/lib/duck_testing/tester.rb +50 -0
- data/lib/duck_testing/type/base.rb +13 -0
- data/lib/duck_testing/type/class_instance.rb +24 -0
- data/lib/duck_testing/type/constant.rb +26 -0
- data/lib/duck_testing/type/duck_type.rb +24 -0
- data/lib/duck_testing/type/hash.rb +60 -0
- data/lib/duck_testing/type/order_dependent_array.rb +27 -0
- data/lib/duck_testing/type/order_independent_array.rb +27 -0
- data/lib/duck_testing/version.rb +3 -0
- data/lib/duck_testing/violation.rb +41 -0
- data/lib/duck_testing/yard.rb +51 -0
- data/lib/duck_testing/yard/builder.rb +70 -0
- data/lib/duck_testing/yard/class_object.rb +20 -0
- data/lib/duck_testing/yard/code_object.rb +22 -0
- data/lib/duck_testing/yard/method_object.rb +87 -0
- data/lib/duck_testing/yard/method_parameter.rb +49 -0
- data/lib/duck_testing/yard/parser.rb +30 -0
- data/sample/.gitignore +35 -0
- data/sample/.rspec +3 -0
- data/sample/Gemfile +5 -0
- data/sample/lib/concern.rb +9 -0
- data/sample/lib/sample.rb +14 -0
- data/sample/spec/sample_spec.rb +33 -0
- data/sample/spec/spec_helper.rb +4 -0
- data/spec/.rubocop.yml +8 -0
- data/spec/class_type_integration_spec.rb +98 -0
- data/spec/constant_type_integration_spec.rb +90 -0
- data/spec/duck_testing/method_call_data_spec.rb +24 -0
- data/spec/duck_testing/reporter/raise_error_spec.rb +35 -0
- data/spec/duck_testing/tester_spec.rb +73 -0
- data/spec/duck_testing/violation_spec.rb +58 -0
- data/spec/duck_testing/yard/builder_spec.rb +179 -0
- data/spec/duck_testing/yard/parser_spec.rb +38 -0
- data/spec/duck_type_integration_spec.rb +89 -0
- data/spec/hash_type_integration_spec.rb +112 -0
- data/spec/order_dependent_array_type_integration_spec.rb +121 -0
- data/spec/order_independent_array_type_integration_spec.rb +104 -0
- data/spec/spec_helper.rb +78 -0
- metadata +212 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require "duck_testing/type/base"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
module Type
|
5
|
+
class DuckType < Base
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param object [Object]
|
13
|
+
# @return [Boolean]
|
14
|
+
def match?(object)
|
15
|
+
object.respond_to?(name)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
def to_s
|
20
|
+
"##{name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "duck_testing/type/base"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
module Type
|
5
|
+
class Hash < Base
|
6
|
+
attr_reader :key_types, :value_types
|
7
|
+
|
8
|
+
# @param key_types [Array<DuckTesting::Type::Base>]
|
9
|
+
# @param value_types [Array<DuckTesting::Type::Base>]
|
10
|
+
def initialize(key_types, value_types)
|
11
|
+
@key_types = key_types
|
12
|
+
@value_types = value_types
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param object [Object]
|
16
|
+
# @return [Boolean]
|
17
|
+
def match?(object)
|
18
|
+
return false unless object.is_a?(::Hash)
|
19
|
+
match_keys?(object) && match_values?(object)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
def to_s
|
24
|
+
"Hash{#{key_types_to_s} => #{value_types_to_s}}"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# @param hash [Hash]
|
30
|
+
# @return [Boolean]
|
31
|
+
def match_keys?(hash)
|
32
|
+
hash.keys.all? do |key|
|
33
|
+
key_types.all? do |type|
|
34
|
+
type.match?(key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param hash [Hash]
|
40
|
+
# @return [Boolean]
|
41
|
+
def match_values?(hash)
|
42
|
+
hash.values.all? do |key|
|
43
|
+
value_types.all? do |type|
|
44
|
+
type.match?(key)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
def key_types_to_s
|
51
|
+
key_types.map(&:to_s).join(", ")
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String]
|
55
|
+
def value_types_to_s
|
56
|
+
value_types.map(&:to_s).join(", ")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "duck_testing/type/base"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
module Type
|
5
|
+
class OrderDependentArray < Base
|
6
|
+
attr_reader :types
|
7
|
+
|
8
|
+
def initialize(*types)
|
9
|
+
@types = types
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param [Object] array
|
13
|
+
# @return [Boolean]
|
14
|
+
def match?(array)
|
15
|
+
return false unless array.is_a?(Array) && array.size == types.size
|
16
|
+
array.zip(types).all? do |array_element, type|
|
17
|
+
type.match?(array_element)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
def to_s
|
23
|
+
"Array<(#{types.map(&:to_s).join(', ')})>"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "duck_testing/type/base"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
module Type
|
5
|
+
class OrderIndependentArray < Base
|
6
|
+
attr_reader :types
|
7
|
+
|
8
|
+
def initialize(*types)
|
9
|
+
@types = types
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param [Object] array
|
13
|
+
# @return [Boolean]
|
14
|
+
def match?(array)
|
15
|
+
return false unless array.is_a?(Array)
|
16
|
+
array.all? do |array_element|
|
17
|
+
types.any? { |type| type.match?(array_element) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
def to_s
|
23
|
+
"Array<#{types.map(&:to_s).join(', ')}>"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
class Violation
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegators :@call_data, :method_expr
|
8
|
+
|
9
|
+
attr_reader :param
|
10
|
+
|
11
|
+
# @param call_data [DuckTesting::MethodCallData]
|
12
|
+
# @param param [Object] the given object.
|
13
|
+
# @param expected_types [Array<DuckTesting::Type::Base>] the expected object types.
|
14
|
+
# @param param_or_return [Symbol] the report type, `:param` or `:return`
|
15
|
+
def initialize(call_data: nil, param: nil, expected_types: nil, param_or_return: nil)
|
16
|
+
@call_data = call_data
|
17
|
+
@param = param
|
18
|
+
@expected_types = expected_types
|
19
|
+
@param_or_return = param_or_return
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Boolean]
|
23
|
+
def param?
|
24
|
+
param_or_return == :param
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Boolean]
|
28
|
+
def return?
|
29
|
+
param_or_return == :return
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [String]
|
33
|
+
def expected
|
34
|
+
expected_types.map(&:to_s).join(", ")
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :call_data, :expected_types, :param_or_return
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "duck_testing/yard/builder"
|
2
|
+
require "duck_testing/yard/parser"
|
3
|
+
|
4
|
+
module DuckTesting
|
5
|
+
module YARD
|
6
|
+
# The default glob of files to be parsed.
|
7
|
+
DEFAULT_PATH_GLOB = ["{lib,app}/**/*.rb"]
|
8
|
+
|
9
|
+
# Parses a path or set of paths then prepend them into corresponding classes..
|
10
|
+
#
|
11
|
+
# @param paths [String, Array<String>] a path, glob or list of paths to parse
|
12
|
+
# @param excluded [Array<String, Regexp>] a list of excluded path matchers
|
13
|
+
# @return [void]
|
14
|
+
def apply(paths: DEFAULT_PATH_GLOB, excluded: [])
|
15
|
+
Parser.parse(paths, excluded).each do |class_object|
|
16
|
+
builder = Builder.new(class_object)
|
17
|
+
begin
|
18
|
+
klass = Object.const_get(class_object.path)
|
19
|
+
rescue NameError
|
20
|
+
next
|
21
|
+
end
|
22
|
+
klass.send(:prepend, builder.build(:instance))
|
23
|
+
klass.singleton_class.send(:prepend, builder.build(:class))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param types [Array<String>]
|
28
|
+
# @return [Array<DuckTesting::Type::Base>]
|
29
|
+
def expected_types(types)
|
30
|
+
types.map do |type|
|
31
|
+
if type == "Boolean"
|
32
|
+
[
|
33
|
+
DuckTesting::Type::Constant.new(true),
|
34
|
+
DuckTesting::Type::Constant.new(false),
|
35
|
+
]
|
36
|
+
elsif DuckTesting::Type::Constant::CONSTANTS.include?(type)
|
37
|
+
DuckTesting::Type::Constant.new(type)
|
38
|
+
elsif type == "void"
|
39
|
+
nil
|
40
|
+
elsif type.start_with?("Array")
|
41
|
+
# TODO: Support specifing types of array elements.
|
42
|
+
DuckTesting::Type::ClassInstance.new(Array)
|
43
|
+
else
|
44
|
+
DuckTesting::Type::ClassInstance.new(Object.const_get(type))
|
45
|
+
end
|
46
|
+
end.flatten.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
module_function :apply, :expected_types
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module DuckTesting
|
2
|
+
module YARD
|
3
|
+
class Builder
|
4
|
+
# @param class_object [DuckTesting::YARD::ClassObject]
|
5
|
+
def initialize(class_object)
|
6
|
+
@class_object = class_object
|
7
|
+
end
|
8
|
+
|
9
|
+
# Build duck testing for the `@class_object`.
|
10
|
+
#
|
11
|
+
# @param scope [Symbol] `:instance` or `:class`
|
12
|
+
# @return [Module] duck testing module of `class_object` in the `scope`.
|
13
|
+
def build(scope = :instance)
|
14
|
+
@building_module = prepare_module
|
15
|
+
@class_object.method_objects.each do |method_object|
|
16
|
+
next unless method_object.scope == scope
|
17
|
+
handle_method_object(method_object)
|
18
|
+
end
|
19
|
+
@building_module
|
20
|
+
ensure
|
21
|
+
remove_instance_variable :@building_module
|
22
|
+
end
|
23
|
+
|
24
|
+
# rubocop:disable Lint/NestedMethodDefinition, Metrics/AbcSize
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def prepare_module
|
29
|
+
Module.new do
|
30
|
+
private
|
31
|
+
|
32
|
+
def __dk_test_normal_parameters(tester, method_object, parameters)
|
33
|
+
parameters.each_with_index do |parameter, index|
|
34
|
+
tester.test_param(
|
35
|
+
parameter,
|
36
|
+
method_object.method_parameters[index].expected_types
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def __dk_test_keyword_parameters(tester, method_object, hash_parameter)
|
42
|
+
method_object.keyword_parameters.each do |method_parameter|
|
43
|
+
next unless hash_parameter.key?(method_parameter.key_name)
|
44
|
+
tester.test_param(
|
45
|
+
hash_parameter[method_parameter.key_name],
|
46
|
+
method_parameter.expected_types
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param method_object [DuckTesting::YARD::MethodObject]
|
54
|
+
def handle_method_object(method_object)
|
55
|
+
@building_module.module_eval do
|
56
|
+
define_method method_object.name do |*parameters|
|
57
|
+
tester = DuckTesting::Tester.new(self, method_object.name)
|
58
|
+
if method_object.keyword_parameters.any? && parameters.last.is_a?(Hash)
|
59
|
+
__dk_test_normal_parameters(tester, method_object, parameters[0...-1])
|
60
|
+
__dk_test_keyword_parameters(tester, method_object, parameters.last)
|
61
|
+
else
|
62
|
+
__dk_test_normal_parameters(tester, method_object, parameters)
|
63
|
+
end
|
64
|
+
tester.test_return(super(*parameters), method_object.expected_return_types)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "duck_testing/yard/code_object"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
module YARD
|
5
|
+
# Encapsulate `YARD::CodeObjects::ClassObject`.
|
6
|
+
class ClassObject < CodeObject
|
7
|
+
# @return [Array<DuckTesting::YARD::MethodObject>]
|
8
|
+
def method_objects
|
9
|
+
@method_objects ||= yard_object.meths.map do |method_object|
|
10
|
+
MethodObject.new(method_object)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
def verbose_name
|
16
|
+
@verbose_name ||= yard_object.path.gsub(/::/, "")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
module YARD
|
5
|
+
class CodeObject
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :yard_object
|
9
|
+
|
10
|
+
# @!attribute [r] path
|
11
|
+
# Represents the unique path of the object.
|
12
|
+
#
|
13
|
+
# @return [String]
|
14
|
+
def_delegators :@yard_object, "path"
|
15
|
+
|
16
|
+
# @param yard_object [YARD::CodeObjects::Base]
|
17
|
+
def initialize(yard_object)
|
18
|
+
@yard_object = yard_object
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "duck_testing/yard/code_object"
|
2
|
+
|
3
|
+
module DuckTesting
|
4
|
+
module YARD
|
5
|
+
# Encapsulate `YARD::CodeObjects::MethodObject`.
|
6
|
+
class MethodObject < CodeObject
|
7
|
+
# @!attribute [r] name
|
8
|
+
# Returns the name of the object.
|
9
|
+
#
|
10
|
+
# @return [String]
|
11
|
+
# @!attribute [r] scope
|
12
|
+
# Returns the scope of the object.
|
13
|
+
#
|
14
|
+
# @return [Symbol]
|
15
|
+
def_delegators :@yard_object, "name", "scope"
|
16
|
+
|
17
|
+
# @return [String]
|
18
|
+
def signature
|
19
|
+
"#{name}(#{parameters_signature})"
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Array<DuckTesting::YARD::MethodParameter>]
|
23
|
+
def method_parameters
|
24
|
+
@method_parameters ||= begin
|
25
|
+
if yard_object.parameters.any?
|
26
|
+
yard_object.parameters.map do |name, default|
|
27
|
+
MethodParameter.new(self, name, default, get_parameter_tag(name))
|
28
|
+
end
|
29
|
+
elsif parameter_tags.any?
|
30
|
+
# Maybe the method is defined using a DSL and its signature is
|
31
|
+
# declared with "@!method" directive.
|
32
|
+
parameter_tags.map do |tag|
|
33
|
+
MethodParameter.new(self, tag.name, nil, tag)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
# The method does not take any arguments.
|
37
|
+
[]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Array<DuckTesting::YARD::MethodParameter>]
|
43
|
+
def keyword_parameters
|
44
|
+
method_parameters.select(&:keyword?)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [YARD::Tags::Tag]
|
48
|
+
def return_tag
|
49
|
+
@return_tag ||= yard_object.tags.find { |tag| tag.tag_name == "return" }
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Array<DuckTesting::Type::Base>]
|
53
|
+
def expected_return_types
|
54
|
+
return_tag ? DuckTesting::YARD.expected_types(return_tag.types) : []
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Boolean]
|
58
|
+
def public_instance_method?
|
59
|
+
yard_object.scope == :instance
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Boolean]
|
63
|
+
def public_class_method?
|
64
|
+
yard_object.scope == :class
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# @return [String]
|
70
|
+
def parameters_signature
|
71
|
+
method_parameters.map(&:to_s).join(", ")
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [YARD::Tags::Tag]
|
75
|
+
def get_parameter_tag(name)
|
76
|
+
parameter_tags.find do |tag|
|
77
|
+
name == (name.end_with?(":") ? "#{tag.name}:" : tag.name)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Array<YARD::Tags::Tag>]
|
82
|
+
def parameter_tags
|
83
|
+
@parameter_tags ||= yard_object.tags.select { |tag| tag.tag_name == "param" }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|