kokishin 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/.ci/Gemfile.rails_4.0.0 +19 -0
- data/.ci/Gemfile.rails_4.0.0.lock +90 -0
- data/.ci/Gemfile.rails_4.1.0 +18 -0
- data/.ci/Gemfile.rails_4.1.0.lock +93 -0
- data/.ci/Gemfile.rails_4.2.0 +19 -0
- data/.ci/Gemfile.rails_4.2.0.lock +97 -0
- data/.ci/Gemfile.rails_5.0.0 +19 -0
- data/.ci/Gemfile.rails_5.0.0.lock +95 -0
- data/.ci/Gemfile.rails_5.1.0 +19 -0
- data/.ci/Gemfile.rails_5.1.0.lock +97 -0
- data/.ci/Gemfile.rails_5.2.0 +19 -0
- data/.ci/Gemfile.rails_5.2.0.lock +97 -0
- data/.ci/Gemfile.rails_6.0.0 +19 -0
- data/.ci/Gemfile.rails_6.0.0.lock +97 -0
- data/.ci/Gemfile.rails_6.1.0 +19 -0
- data/.ci/Gemfile.rails_6.1.0.lock +96 -0
- data/.ci/Gemfile.rails_7.0.0 +19 -0
- data/.ci/Gemfile.rails_7.0.0.lock +110 -0
- data/.gitignore +9 -0
- data/.gitlab-ci.yml +79 -0
- data/.rspec +1 -0
- data/.rubocop.yml +27 -0
- data/.rubocop_todo.yml +24 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +110 -0
- data/LICENSE.txt +21 -0
- data/README.md +14 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/kokishin.gemspec +32 -0
- data/lib/kokishin/extensions/hash_diff.rb +21 -0
- data/lib/kokishin/matchers/association_matcher.rb +160 -0
- data/lib/kokishin/matchers/method_matcher.rb +53 -0
- data/lib/kokishin/matchers/validate_matcher.rb +90 -0
- data/lib/kokishin/should.rb +24 -0
- data/lib/kokishin/version.rb +7 -0
- data/lib/kokishin.rb +6 -0
- metadata +98 -0
data/kokishin.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'kokishin/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.authors = %w[ bm5k ]
|
9
|
+
spec.bindir = 'exe'
|
10
|
+
spec.description = 'A single home for various custom RSpec matchers.'
|
11
|
+
spec.email = %w[ 226882-BM5k@users.noreply.gitlab.com ]
|
12
|
+
spec.homepage = 'https://gitlab.com/devfu/kokishin'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.name = 'kokishin'
|
15
|
+
spec.require_paths = %w[ lib ]
|
16
|
+
spec.summary = 'RSpec matchers'
|
17
|
+
spec.version = Kokishin::VERSION
|
18
|
+
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
21
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
|
+
end
|
28
|
+
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
|
31
|
+
spec.add_dependency 'rspec-expectations'
|
32
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kokishin
|
4
|
+
module HashDiff
|
5
|
+
|
6
|
+
refine Hash do
|
7
|
+
# Stolen from Rails 4.0.5 since this has been removed!
|
8
|
+
#
|
9
|
+
# Returns a hash that represents the difference between two hashes.
|
10
|
+
#
|
11
|
+
# {1 => 2}.diff(1 => 2) # => {}
|
12
|
+
# {1 => 2}.diff(1 => 3) # => {1 => 2}
|
13
|
+
# {}.diff(1 => 2) # => {1 => 2}
|
14
|
+
# {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
|
15
|
+
def diff other
|
16
|
+
dup.delete_if { |k, v| other[k] == v }.merge!(other.dup.delete_if { |k, _v| key?(k) })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kokishin/extensions/hash_diff'
|
4
|
+
|
5
|
+
using Kokishin::HashDiff
|
6
|
+
|
7
|
+
RSpec::Matchers.define :belong_to do |kind|
|
8
|
+
description do
|
9
|
+
"belong_to #{ kind.inspect }"
|
10
|
+
end
|
11
|
+
|
12
|
+
failure_message do
|
13
|
+
msg = "Expected #{ described_class } to belong_to #{ kind.inspect }"
|
14
|
+
msg << "\n column #{ key.inspect } does not exist in #{ described_class }" if associated? && !has_column?
|
15
|
+
msg
|
16
|
+
end
|
17
|
+
|
18
|
+
failure_message_when_negated do
|
19
|
+
"Expected #{ described_class } not to belong_to #{ kind.inspect }"
|
20
|
+
end
|
21
|
+
|
22
|
+
def association
|
23
|
+
gem_version = Gem::Version.new ActiveRecord::VERSION::STRING
|
24
|
+
|
25
|
+
key = if gem_version >= Gem::Version.new('4.2.0')
|
26
|
+
expected.to_s
|
27
|
+
else
|
28
|
+
expected
|
29
|
+
end
|
30
|
+
|
31
|
+
actual.class.reflections[key]
|
32
|
+
end
|
33
|
+
|
34
|
+
def associated?
|
35
|
+
association && association.macro == :belongs_to
|
36
|
+
end
|
37
|
+
|
38
|
+
def key
|
39
|
+
@key ||= association.foreign_key
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_column?
|
43
|
+
actual.class.column_names.include? key
|
44
|
+
end
|
45
|
+
|
46
|
+
match do |_actual|
|
47
|
+
associated? && has_column?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
RSpec::Matchers.define :have_many do |kind, expected_options|
|
52
|
+
description do
|
53
|
+
msg = "have_many #{ kind.inspect }"
|
54
|
+
msg << ", #{ @expected_options.map { |k, v| ":#{ k } => #{ v }" }.join ',' }" if @expected_options.present?
|
55
|
+
|
56
|
+
@expected_options = nil # for some reason this appears to be cached between runs?
|
57
|
+
|
58
|
+
msg
|
59
|
+
end
|
60
|
+
|
61
|
+
failure_message do
|
62
|
+
"Expected #{ described_class } to have_many #{ kind.inspect } #{ diff }"
|
63
|
+
end
|
64
|
+
|
65
|
+
failure_message_when_negated do
|
66
|
+
"Expected #{ described_class } not to have_many #{ kind.inspect } #{ diff }"
|
67
|
+
end
|
68
|
+
|
69
|
+
def expected_options
|
70
|
+
@expected_options || {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def association
|
74
|
+
@association ||= actual.class.reflect_on_association [ *expected ].first
|
75
|
+
end
|
76
|
+
|
77
|
+
def association_options
|
78
|
+
@association_options ||= association.options.reject { |k, v| k == :extend and v == [] } rescue {}
|
79
|
+
end
|
80
|
+
|
81
|
+
def diff?
|
82
|
+
# if association_options is empty and @expected_options is not, there is a problem
|
83
|
+
# if @expected_options is empty and association_options is not, there is a problem
|
84
|
+
# if neither is empty diff them, if there's a diff, there is a problem
|
85
|
+
|
86
|
+
return true if expected_options.present? ^ association_options.present?
|
87
|
+
return true if association_options.diff(expected_options).present?
|
88
|
+
|
89
|
+
false
|
90
|
+
end
|
91
|
+
|
92
|
+
def diff
|
93
|
+
return '' unless diff?
|
94
|
+
|
95
|
+
<<-MSG
|
96
|
+
\n expected options: #{ expected_options.inspect }
|
97
|
+
\n actual options: #{ association_options.inspect }
|
98
|
+
MSG
|
99
|
+
end
|
100
|
+
|
101
|
+
match do |_actual|
|
102
|
+
@expected_options = expected_options
|
103
|
+
association.try(:macro) == :has_many and not diff?
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
RSpec::Matchers.define :have_one do |kind, expected_options|
|
108
|
+
description do
|
109
|
+
msg = "have_many #{ kind.inspect }"
|
110
|
+
msg << ", #{ @expected_options.map { |k, v| ":#{ k } => #{ v }" }.join ',' }" if @expected_options.present?
|
111
|
+
|
112
|
+
@expected_options = nil # for some reason this appears to be cached between runs?
|
113
|
+
|
114
|
+
msg
|
115
|
+
end
|
116
|
+
|
117
|
+
failure_message do
|
118
|
+
"Expected #{ described_class } to have_many #{ kind.inspect } #{ diff }"
|
119
|
+
end
|
120
|
+
|
121
|
+
failure_message_when_negated do
|
122
|
+
"Expected #{ described_class } not to have_many #{ kind.inspect } #{ diff }"
|
123
|
+
end
|
124
|
+
|
125
|
+
def expected_options
|
126
|
+
@expected_options || {}
|
127
|
+
end
|
128
|
+
|
129
|
+
def association
|
130
|
+
@association ||= actual.class.reflect_on_association [ *expected ].first
|
131
|
+
end
|
132
|
+
|
133
|
+
def association_options
|
134
|
+
@association_options ||= association.options.reject { |k, v| k == :extend and v == [] } rescue {}
|
135
|
+
end
|
136
|
+
|
137
|
+
def diff?
|
138
|
+
# if association_options is empty and @expected_options is not, there is a problem
|
139
|
+
# if @expected_options is empty and association_options is not, there is a problem
|
140
|
+
# if neither is empty diff them, if there's a diff, there is a problem
|
141
|
+
return true if expected_options.present? ^ association_options.present?
|
142
|
+
return true if association_options.diff(expected_options).present?
|
143
|
+
|
144
|
+
false
|
145
|
+
end
|
146
|
+
|
147
|
+
def diff
|
148
|
+
return '' unless diff?
|
149
|
+
|
150
|
+
<<-MSG
|
151
|
+
\n expected options: #{ expected_options.inspect }
|
152
|
+
\n actual options: #{ association_options.inspect }
|
153
|
+
MSG
|
154
|
+
end
|
155
|
+
|
156
|
+
match do |_actual|
|
157
|
+
@expected_options = expected_options
|
158
|
+
association.try(:macro) == :has_one and not diff?
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/expectations'
|
4
|
+
|
5
|
+
RSpec::Matchers.define :have_method do |method_name, include_all = false|
|
6
|
+
match do |actual|
|
7
|
+
@actual = actual
|
8
|
+
@include_all = include_all
|
9
|
+
@method_name = method_name
|
10
|
+
|
11
|
+
if @alias_name
|
12
|
+
responds_to_method? && responds_to_alias? && source_locations_match?
|
13
|
+
else
|
14
|
+
responds_to_method?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
chain :with_alias do |alias_name|
|
19
|
+
@alias_name = alias_name
|
20
|
+
end
|
21
|
+
|
22
|
+
description do
|
23
|
+
if @alias_name
|
24
|
+
"have method ##{ @method_name } with alias ##{ @alias_name }"
|
25
|
+
else
|
26
|
+
"have method ##{ @method_name }"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
failure_message do
|
31
|
+
"should have method ##{ @method_name }, but does not."
|
32
|
+
end
|
33
|
+
|
34
|
+
failure_message_when_negated do
|
35
|
+
"should not have method ##{ @method_name }, but does."
|
36
|
+
end
|
37
|
+
|
38
|
+
def responds_to_alias?
|
39
|
+
subject.respond_to? @alias_name, @include_all
|
40
|
+
end
|
41
|
+
|
42
|
+
def responds_to_method?
|
43
|
+
subject.respond_to? @method_name, @include_all
|
44
|
+
end
|
45
|
+
|
46
|
+
def source_locations_match?
|
47
|
+
location_of(@method_name) == location_of(@alias_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def location_of name
|
51
|
+
@actual.method(name).source_location
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/expectations'
|
4
|
+
|
5
|
+
RSpec::Matchers.define :validate do |_expected|
|
6
|
+
chain(:of) { |attr| @attribute = attr }
|
7
|
+
|
8
|
+
chain(:with) do |*opts|
|
9
|
+
@options = opts.extract_options!
|
10
|
+
@options = opts if @options.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
match do |_actual|
|
14
|
+
if @attribute
|
15
|
+
validator.present? && (@options ? validator.options.should =~ @options : true)
|
16
|
+
else
|
17
|
+
callback.present? && (@options ? check_callback_options : true)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
match_when_negated do |_actual|
|
22
|
+
if @attribute
|
23
|
+
raise "Negating a matcher with options doesn't really make sense!" if @options
|
24
|
+
|
25
|
+
validator.should be_nil
|
26
|
+
else
|
27
|
+
callback.should be_nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def callback
|
32
|
+
actual.class._validate_callbacks.detect { |c| c.filter == expected }
|
33
|
+
end
|
34
|
+
|
35
|
+
def check_callback_options
|
36
|
+
opts = parse_callback_options
|
37
|
+
|
38
|
+
if @options.is_a? Hash
|
39
|
+
raise 'Cannot expect custom procs' if @options.values.any? { |v| v.is_a? Proc }
|
40
|
+
|
41
|
+
@options.each { |k, _| opts[k].should =~ Array(@options[k]) }
|
42
|
+
else
|
43
|
+
opts.keys.should =~ @options
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_callback_options
|
48
|
+
gem_version = Gem::Version.new ActiveModel::VERSION::STRING
|
49
|
+
|
50
|
+
options = if (Gem::Version.new('4.0.0')...Gem::Version.new('4.1.0')).cover?(gem_version)
|
51
|
+
parse_callback_options4
|
52
|
+
else
|
53
|
+
parse_callback_options_default
|
54
|
+
end
|
55
|
+
|
56
|
+
options.reject { |_, v| v.empty? }
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_callback_options_default
|
60
|
+
ifs = callback.instance_variable_get '@if'
|
61
|
+
unlesses = callback.instance_variable_get '@unless'
|
62
|
+
|
63
|
+
if ifs.first.is_a?(Proc)
|
64
|
+
proc_binding = ifs.first.binding
|
65
|
+
if proc_binding.local_variables.include? :context # rails 7.1.0+
|
66
|
+
ons = proc_binding.local_variable_get :context
|
67
|
+
ifs = ifs[1..-1]
|
68
|
+
elsif proc_binding.local_variables.include? :options
|
69
|
+
ons = proc_binding.local_variable_get(:options)[:on]
|
70
|
+
ifs = ifs[1..-1]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
{ if: ifs, on: Array(ons), unless: Array(unlesses) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_callback_options4
|
78
|
+
ifs = [ *callback.options[:if] ]
|
79
|
+
ons = [ *callback.options[:on] ]
|
80
|
+
unlesses = [ *callback.options[:unless] ]
|
81
|
+
|
82
|
+
ifs = ifs[1..-1] if ons.any?
|
83
|
+
|
84
|
+
{ if: ifs, on: ons, unless: unlesses }
|
85
|
+
end
|
86
|
+
|
87
|
+
def validator
|
88
|
+
actual.class.validators_on(@attribute).detect { |v| v.kind == expected }
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Should syntax is the best syntax.
|
4
|
+
require 'rspec/matchers'
|
5
|
+
|
6
|
+
module RSpec
|
7
|
+
|
8
|
+
configure do |config|
|
9
|
+
config.expect_with(:rspec) { |spec| spec.syntax = :should }
|
10
|
+
config.mock_with(:rspec) { |spec| spec.syntax = :should }
|
11
|
+
end
|
12
|
+
|
13
|
+
module Matchers
|
14
|
+
|
15
|
+
# Force generated description to use should syntax
|
16
|
+
def self.generated_description
|
17
|
+
return nil if last_expectation_handler.nil?
|
18
|
+
|
19
|
+
"should #{ last_description }"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/kokishin.rb
ADDED
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kokishin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- bm5k
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-10-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec-expectations
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: A single home for various custom RSpec matchers.
|
28
|
+
email:
|
29
|
+
- 226882-BM5k@users.noreply.gitlab.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".ci/Gemfile.rails_4.0.0"
|
35
|
+
- ".ci/Gemfile.rails_4.0.0.lock"
|
36
|
+
- ".ci/Gemfile.rails_4.1.0"
|
37
|
+
- ".ci/Gemfile.rails_4.1.0.lock"
|
38
|
+
- ".ci/Gemfile.rails_4.2.0"
|
39
|
+
- ".ci/Gemfile.rails_4.2.0.lock"
|
40
|
+
- ".ci/Gemfile.rails_5.0.0"
|
41
|
+
- ".ci/Gemfile.rails_5.0.0.lock"
|
42
|
+
- ".ci/Gemfile.rails_5.1.0"
|
43
|
+
- ".ci/Gemfile.rails_5.1.0.lock"
|
44
|
+
- ".ci/Gemfile.rails_5.2.0"
|
45
|
+
- ".ci/Gemfile.rails_5.2.0.lock"
|
46
|
+
- ".ci/Gemfile.rails_6.0.0"
|
47
|
+
- ".ci/Gemfile.rails_6.0.0.lock"
|
48
|
+
- ".ci/Gemfile.rails_6.1.0"
|
49
|
+
- ".ci/Gemfile.rails_6.1.0.lock"
|
50
|
+
- ".ci/Gemfile.rails_7.0.0"
|
51
|
+
- ".ci/Gemfile.rails_7.0.0.lock"
|
52
|
+
- ".gitignore"
|
53
|
+
- ".gitlab-ci.yml"
|
54
|
+
- ".rspec"
|
55
|
+
- ".rubocop.yml"
|
56
|
+
- ".rubocop_todo.yml"
|
57
|
+
- Gemfile
|
58
|
+
- Gemfile.lock
|
59
|
+
- LICENSE.txt
|
60
|
+
- README.md
|
61
|
+
- Rakefile
|
62
|
+
- bin/console
|
63
|
+
- bin/setup
|
64
|
+
- kokishin.gemspec
|
65
|
+
- lib/kokishin.rb
|
66
|
+
- lib/kokishin/extensions/hash_diff.rb
|
67
|
+
- lib/kokishin/matchers/association_matcher.rb
|
68
|
+
- lib/kokishin/matchers/method_matcher.rb
|
69
|
+
- lib/kokishin/matchers/validate_matcher.rb
|
70
|
+
- lib/kokishin/should.rb
|
71
|
+
- lib/kokishin/version.rb
|
72
|
+
homepage: https://gitlab.com/devfu/kokishin
|
73
|
+
licenses:
|
74
|
+
- MIT
|
75
|
+
metadata:
|
76
|
+
homepage_uri: https://gitlab.com/devfu/kokishin
|
77
|
+
rubygems_mfa_required: 'true'
|
78
|
+
source_code_uri: https://gitlab.com/devfu/kokishin
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.4.10
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: RSpec matchers
|
98
|
+
test_files: []
|