shoulda-matchers 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -3
  4. data/CONTRIBUTING.md +60 -28
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +15 -12
  7. data/NEWS.md +111 -0
  8. data/README.md +94 -6
  9. data/Rakefile +10 -8
  10. data/custom_plan.rb +88 -0
  11. data/gemfiles/4.0.0.gemfile +1 -0
  12. data/gemfiles/4.0.0.gemfile.lock +21 -18
  13. data/gemfiles/4.0.1.gemfile +1 -0
  14. data/gemfiles/4.0.1.gemfile.lock +21 -18
  15. data/gemfiles/4.1.gemfile +1 -0
  16. data/gemfiles/4.1.gemfile.lock +21 -18
  17. data/gemfiles/4.2.gemfile +1 -0
  18. data/gemfiles/4.2.gemfile.lock +24 -21
  19. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -11
  20. data/lib/shoulda/matchers/active_model.rb +10 -1
  21. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +258 -180
  22. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +45 -0
  23. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb +23 -0
  24. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +236 -0
  25. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +62 -0
  26. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +40 -0
  27. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +48 -0
  28. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb +14 -0
  29. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb +14 -0
  30. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +34 -14
  31. data/lib/shoulda/matchers/active_model/helpers.rb +9 -17
  32. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +13 -6
  33. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +13 -2
  34. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +19 -35
  35. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +13 -2
  36. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +12 -2
  37. data/lib/shoulda/matchers/active_model/qualifiers.rb +12 -0
  38. data/lib/shoulda/matchers/active_model/qualifiers/ignore_interference_by_writer.rb +101 -0
  39. data/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +21 -0
  40. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +30 -32
  41. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +5 -8
  42. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +22 -22
  43. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +27 -16
  44. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +58 -15
  45. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +22 -12
  46. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +165 -87
  47. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +7 -9
  48. data/lib/shoulda/matchers/active_model/validation_matcher.rb +111 -49
  49. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +60 -0
  50. data/lib/shoulda/matchers/active_model/validator.rb +71 -52
  51. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +19 -5
  52. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +450 -124
  53. data/lib/shoulda/matchers/util.rb +43 -0
  54. data/lib/shoulda/matchers/util/word_wrap.rb +59 -31
  55. data/lib/shoulda/matchers/version.rb +1 -1
  56. data/script/update_gem_in_all_appraisals +1 -1
  57. data/script/update_gems_in_all_appraisals +1 -1
  58. data/spec/acceptance/multiple_libraries_integration_spec.rb +5 -2
  59. data/spec/acceptance/rails_integration_spec.rb +6 -2
  60. data/spec/spec_helper.rb +1 -3
  61. data/spec/support/acceptance/helpers/step_helpers.rb +4 -1
  62. data/spec/support/tests/current_bundle.rb +21 -7
  63. data/spec/support/unit/active_record/create_table.rb +54 -0
  64. data/spec/support/unit/attribute.rb +47 -0
  65. data/spec/support/unit/capture.rb +6 -0
  66. data/spec/support/unit/change_value.rb +111 -0
  67. data/spec/support/unit/create_model_arguments/basic.rb +135 -0
  68. data/spec/support/unit/create_model_arguments/has_many.rb +15 -0
  69. data/spec/support/unit/create_model_arguments/uniqueness_matcher.rb +74 -0
  70. data/spec/support/unit/helpers/active_record_versions.rb +1 -1
  71. data/spec/support/unit/helpers/class_builder.rb +61 -47
  72. data/spec/support/unit/helpers/database_helpers.rb +5 -3
  73. data/spec/support/unit/helpers/model_builder.rb +77 -97
  74. data/spec/support/unit/helpers/validation_matcher_scenario_helpers.rb +44 -0
  75. data/spec/support/unit/load_environment.rb +12 -0
  76. data/spec/support/unit/matchers/fail_with_message_including_matcher.rb +2 -2
  77. data/spec/support/unit/matchers/fail_with_message_matcher.rb +12 -1
  78. data/spec/support/unit/model_creation_strategies/active_model.rb +111 -0
  79. data/spec/support/unit/model_creation_strategies/active_record.rb +77 -0
  80. data/spec/support/unit/model_creators.rb +19 -0
  81. data/spec/support/unit/model_creators/active_model.rb +39 -0
  82. data/spec/support/unit/model_creators/active_record.rb +43 -0
  83. data/spec/support/unit/model_creators/active_record/has_and_belongs_to_many.rb +95 -0
  84. data/spec/support/unit/model_creators/active_record/has_many.rb +67 -0
  85. data/spec/support/unit/model_creators/active_record/uniqueness_matcher.rb +42 -0
  86. data/spec/support/unit/model_creators/basic.rb +97 -0
  87. data/spec/support/unit/rails_application.rb +1 -1
  88. data/spec/support/unit/record_validating_confirmation_builder.rb +3 -7
  89. data/spec/support/unit/shared_examples/ignoring_interference_by_writer.rb +79 -0
  90. data/spec/support/unit/validation_matcher_scenario.rb +62 -0
  91. data/spec/unit/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +4 -0
  92. data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +575 -140
  93. data/spec/unit/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +115 -15
  94. data/spec/unit/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +42 -4
  95. data/spec/unit/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +92 -6
  96. data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +122 -10
  97. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +306 -58
  98. data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +122 -3
  99. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +805 -131
  100. data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +196 -29
  101. data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +82 -40
  102. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +600 -101
  103. data/spec/unit/shoulda/matchers/util/word_wrap_spec.rb +88 -33
  104. data/spec/unit_spec_helper.rb +10 -22
  105. data/zeus.json +11 -0
  106. metadata +64 -23
  107. data/lib/shoulda/matchers/active_model/strict_validator.rb +0 -51
  108. data/spec/support/unit/shared_examples/numerical_type_submatcher.rb +0 -15
  109. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +0 -288
  110. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +0 -100
  111. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +0 -100
  112. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +0 -100
@@ -30,6 +30,49 @@ module Shoulda
30
30
  indentation = ' ' * width
31
31
  string.split(/[\n\r]/).map { |line| indentation + line }.join("\n")
32
32
  end
33
+
34
+ def self.a_or_an(next_word)
35
+ if next_word =~ /\A[aeiou]/i
36
+ "an #{next_word}"
37
+ else
38
+ "a #{next_word}"
39
+ end
40
+ end
41
+
42
+ def self.inspect_value(value)
43
+ "‹#{value.inspect}›"
44
+ end
45
+
46
+ def self.inspect_values(values)
47
+ values.map { |value| inspect_value(value) }
48
+ end
49
+
50
+ def self.inspect_range(range)
51
+ "#{inspect_value(range.first)} to #{inspect_value(range.last)}"
52
+ end
53
+
54
+ def self.dummy_value_for(column_type, array: false)
55
+ if array
56
+ [dummy_value_for(column_type, array: false)]
57
+ else
58
+ case column_type
59
+ when :integer
60
+ 0
61
+ when :date
62
+ Date.new(2100, 1, 1)
63
+ when :datetime, :timestamp
64
+ DateTime.new(2100, 1, 1)
65
+ when :time
66
+ Time.new(2100, 1, 1)
67
+ when :uuid
68
+ SecureRandom.uuid
69
+ when :boolean
70
+ true
71
+ else
72
+ 'dummy value'
73
+ end
74
+ end
75
+ end
33
76
  end
34
77
  end
35
78
  end
@@ -1,14 +1,15 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- def self.word_wrap(document)
5
- Document.new(document).wrap
4
+ def self.word_wrap(document, options = {})
5
+ Document.new(document, options).wrap
6
6
  end
7
7
 
8
8
  # @private
9
9
  class Document
10
- def initialize(document)
10
+ def initialize(document, indent: 0)
11
11
  @document = document
12
+ @indent = indent
12
13
  end
13
14
 
14
15
  def wrap
@@ -17,7 +18,7 @@ module Shoulda
17
18
 
18
19
  protected
19
20
 
20
- attr_reader :document
21
+ attr_reader :document, :indent
21
22
 
22
23
  private
23
24
 
@@ -27,7 +28,7 @@ module Shoulda
27
28
 
28
29
  def wrapped_paragraphs
29
30
  paragraphs.map do |paragraph|
30
- Paragraph.new(paragraph).wrap
31
+ Paragraph.new(paragraph, indent: indent).wrap
31
32
  end
32
33
  end
33
34
  end
@@ -51,8 +52,9 @@ module Shoulda
51
52
 
52
53
  # @private
53
54
  class Paragraph
54
- def initialize(paragraph)
55
+ def initialize(paragraph, indent: 0)
55
56
  @paragraph = Text.new(paragraph)
57
+ @indent = indent
56
58
  end
57
59
 
58
60
  def wrap
@@ -67,7 +69,7 @@ module Shoulda
67
69
 
68
70
  protected
69
71
 
70
- attr_reader :paragraph
72
+ attr_reader :paragraph, :indent
71
73
 
72
74
  private
73
75
 
@@ -92,11 +94,11 @@ module Shoulda
92
94
  end
93
95
 
94
96
  def wrap_lines(lines)
95
- lines.map { |line| Line.new(line).wrap }
97
+ lines.map { |line| Line.new(line, indent: indent).wrap }
96
98
  end
97
99
 
98
100
  def wrap_generic_paragraph
99
- Line.new(combine_paragraph_into_one_line).wrap
101
+ Line.new(combine_paragraph_into_one_line, indent: indent).wrap
100
102
  end
101
103
 
102
104
  def combine_paragraph_into_one_line
@@ -107,72 +109,98 @@ module Shoulda
107
109
  # @private
108
110
  class Line
109
111
  TERMINAL_WIDTH = 72
112
+ OFFSETS = { left: -1, right: +1 }
110
113
 
111
- def initialize(line)
114
+ def initialize(line, indent: 0)
115
+ @indent = indent
112
116
  @original_line = @line_to_wrap = Text.new(line)
113
- @indentation = nil
117
+ @indentation = ' ' * indent
118
+ @indentation_read = false
114
119
  end
115
120
 
116
121
  def wrap
117
- lines = []
118
-
119
122
  if line_to_wrap.indented?
120
- lines << line_to_wrap
123
+ [line_to_wrap]
121
124
  else
125
+ lines = []
126
+
122
127
  loop do
128
+ @previous_line_to_wrap = line_to_wrap
123
129
  new_line = (indentation || '') + line_to_wrap
124
130
  result = wrap_line(new_line)
125
- lines << result[:fitted_line].rstrip
126
- @indentation ||= read_indentation
131
+ lines << normalize_whitespace(result[:fitted_line])
132
+
133
+ unless @indentation_read
134
+ @indentation = read_indentation
135
+ @indentation_read = true
136
+ end
137
+
127
138
  @line_to_wrap = result[:leftover]
128
139
 
129
- if line_to_wrap.empty? || @original_line == @line_to_wrap
140
+ if line_to_wrap.to_s.empty? || previous_line_to_wrap == line_to_wrap
130
141
  break
131
142
  end
132
143
  end
133
- end
134
144
 
135
- lines
145
+ lines
146
+ end
136
147
  end
137
148
 
138
149
  protected
139
150
 
140
- attr_reader :original_line, :line_to_wrap, :indentation
151
+ attr_reader :indent, :original_line, :line_to_wrap, :indentation,
152
+ :previous_line_to_wrap
141
153
 
142
154
  private
143
155
 
144
156
  def read_indentation
157
+ initial_indentation = ' ' * indent
145
158
  match = line_to_wrap.match_as_list_item
146
159
 
147
160
  if match
148
- ' ' * match[1].length
161
+ initial_indentation + (' ' * match[1].length)
149
162
  else
150
- ''
163
+ initial_indentation
151
164
  end
152
165
  end
153
166
 
154
- def wrap_line(line)
167
+ def wrap_line(line, direction: :left)
168
+ index = nil
169
+
155
170
  if line.length > TERMINAL_WIDTH
156
- index = determine_where_to_break_line(line)
157
- fitted_line = line[0 .. index].rstrip
158
- leftover = line[index + 1 .. -1]
159
- else
171
+ index = determine_where_to_break_line(line, direction: :left)
172
+
173
+ if index == -1
174
+ index = determine_where_to_break_line(line, direction: :right)
175
+ end
176
+ end
177
+
178
+ if index.nil? || index == -1
160
179
  fitted_line = line
161
180
  leftover = ''
181
+ else
182
+ fitted_line = line[0..index].rstrip
183
+ leftover = line[index + 1 .. -1]
162
184
  end
163
185
 
164
186
  { fitted_line: fitted_line, leftover: leftover }
165
187
  end
166
188
 
167
- def determine_where_to_break_line(line)
168
- index = TERMINAL_WIDTH - 1
189
+ def determine_where_to_break_line(line, args)
190
+ direction = args.fetch(:direction)
191
+ index = TERMINAL_WIDTH
192
+ offset = OFFSETS.fetch(direction)
169
193
 
170
- while line[index] !~ /\s/
171
- index -= 1
194
+ while line[index] !~ /\s/ && (0...line.length).cover?(index)
195
+ index += offset
172
196
  end
173
197
 
174
198
  index
175
199
  end
200
+
201
+ def normalize_whitespace(string)
202
+ indentation + string.strip.squeeze(' ')
203
+ end
176
204
  end
177
205
  end
178
206
  end
@@ -1,6 +1,6 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- VERSION = '3.0.1'.freeze
4
+ VERSION = '3.1.0'.freeze
5
5
  end
6
6
  end
@@ -5,7 +5,7 @@ gem="$1"
5
5
 
6
6
  update-gem-for-version() {
7
7
  local version="$1"
8
- (export RBENV_VERSION=$version; bundle update "$gem" && bundle exec appraisal update "$gem")
8
+ (export RBENV_VERSION=$version; bundle update "$gem"; bundle exec appraisal update "$gem")
9
9
  }
10
10
 
11
11
  for version in $SUPPORTED_VERSIONS; do
@@ -4,7 +4,7 @@ SUPPORTED_VERSIONS=$(<script/SUPPORTED_VERSIONS)
4
4
 
5
5
  update-gems-for-version() {
6
6
  local version="$1"
7
- (export RBENV_VERSION=$version; bundle update "${@:2}" && bundle exec appraisal update "${@:2}")
7
+ (export RBENV_VERSION=$version; bundle update "${@:2}"; bundle exec appraisal update "${@:2}")
8
8
  }
9
9
 
10
10
  for version in $SUPPORTED_VERSIONS; do
@@ -25,6 +25,7 @@ describe 'shoulda-matchers integrates with multiple libraries' do
25
25
 
26
26
  add_rspec_file 'spec/models/user_spec.rb', <<-FILE
27
27
  describe User do
28
+ subject { User.new(name: "John Smith") }
28
29
  it { should validate_presence_of(:name) }
29
30
  it { should validate_uniqueness_of(:name) }
30
31
  end
@@ -43,9 +44,11 @@ describe 'shoulda-matchers integrates with multiple libraries' do
43
44
  it 'allows the use of matchers from both libraries' do
44
45
  result = run_rspec_suite
45
46
  expect(result).to have_output('2 examples, 0 failures')
46
- expect(result).to have_output('should require name to be set')
47
47
  expect(result).to have_output(
48
- 'should require case sensitive unique value for name'
48
+ 'should validate that :name cannot be empty/falsy'
49
+ )
50
+ expect(result).to have_output(
51
+ 'should validate that :name is case-sensitively unique'
49
52
  )
50
53
  end
51
54
  end
@@ -124,7 +124,9 @@ describe 'shoulda-matchers integrates with Rails' do
124
124
  result = run_n_unit_test_suite
125
125
 
126
126
  expect(result).to indicate_that_tests_were_run(unit: 1, functional: 1)
127
- expect(result).to have_output('User should require name to be set')
127
+ expect(result).to have_output(
128
+ 'User should validate that :name cannot be empty/falsy'
129
+ )
128
130
  expect(result).to have_output('should respond with 200')
129
131
  end
130
132
 
@@ -146,7 +148,9 @@ describe 'shoulda-matchers integrates with Rails' do
146
148
  result = run_rspec_suite
147
149
 
148
150
  expect(result).to have_output('2 examples, 0 failures')
149
- expect(result).to have_output('should require name to be set')
151
+ expect(result).to have_output(
152
+ 'should validate that :name cannot be empty/falsy'
153
+ )
150
154
  expect(result).to have_output('should respond with 200')
151
155
  end
152
156
  end
@@ -13,9 +13,7 @@ RSpec.configure do |config|
13
13
  c.syntax = :expect
14
14
  end
15
15
 
16
- if config.files_to_run.one?
17
- config.default_formatter = 'doc'
18
- end
16
+ config.default_formatter = 'doc'
19
17
 
20
18
  config.mock_with :rspec
21
19
  end
@@ -18,6 +18,7 @@ module AcceptanceTests
18
18
  end
19
19
 
20
20
  def create_generic_bundler_project
21
+ fs.clean
21
22
  fs.create
22
23
  run_command! 'bundle init'
23
24
  end
@@ -61,7 +62,9 @@ module AcceptanceTests
61
62
  end
62
63
 
63
64
  def create_rails_application
64
- command = "bundle exec rails new #{fs.project_directory} --skip-bundle"
65
+ fs.clean
66
+
67
+ command = "bundle exec rails new #{fs.project_directory} --skip-bundle --no-rc"
65
68
 
66
69
  run_command!(command) do |runner|
67
70
  runner.directory = nil
@@ -8,21 +8,33 @@ module Tests
8
8
  include Singleton
9
9
 
10
10
  def assert_appraisal!
11
- unless appraisal?
11
+ unless appraisal_in_use?
12
12
  message = <<EOT
13
13
 
14
14
 
15
15
  Please run tests starting with `appraisal <appraisal_name>`.
16
- Possible appraisals are: #{possible_appraisals}
16
+ Possible appraisals are: #{available_appraisals}
17
17
 
18
18
  EOT
19
19
  raise AppraisalNotSpecified, message
20
20
  end
21
21
  end
22
22
 
23
+ def appraisal_in_use?
24
+ path.dirname == root.join('gemfiles')
25
+ end
26
+
27
+ def current_or_latest_appraisal
28
+ current_appraisal || latest_appraisal
29
+ end
30
+
31
+ def latest_appraisal
32
+ available_appraisals.sort.last
33
+ end
34
+
23
35
  private
24
36
 
25
- def possible_appraisals
37
+ def available_appraisals
26
38
  appraisals = []
27
39
 
28
40
  Appraisal::File.each do |appraisal|
@@ -32,12 +44,14 @@ EOT
32
44
  appraisals
33
45
  end
34
46
 
35
- def path
36
- Bundler.default_gemfile
47
+ def current_appraisal
48
+ if appraisal_in_use?
49
+ File.basename(path, ".gemfile")
50
+ end
37
51
  end
38
52
 
39
- def appraisal?
40
- path.dirname == root.join('gemfiles')
53
+ def path
54
+ Bundler.default_gemfile
41
55
  end
42
56
 
43
57
  def root
@@ -0,0 +1,54 @@
1
+ module UnitTests
2
+ module ActiveRecord
3
+ class CreateTable
4
+ def self.call(table_name, columns)
5
+ new(table_name, columns).call
6
+ end
7
+
8
+ def initialize(table_name, columns)
9
+ @table_name = table_name
10
+ @columns = columns
11
+ end
12
+
13
+ def call
14
+ if columns.key?(:id) && columns[:id] == false
15
+ columns.delete(:id)
16
+ UnitTests::ModelBuilder.create_table(
17
+ table_name,
18
+ id: false,
19
+ &method(:add_columns_to_table)
20
+ )
21
+ else
22
+ UnitTests::ModelBuilder.create_table(
23
+ table_name,
24
+ &method(:add_columns_to_table)
25
+ )
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ attr_reader :table_name, :columns
32
+
33
+ private
34
+
35
+ def add_columns_to_table(table)
36
+ columns.each do |column_name, column_specification|
37
+ add_column_to_table(table, column_name, column_specification)
38
+ end
39
+ end
40
+
41
+ def add_column_to_table(table, column_name, column_specification)
42
+ if column_specification.is_a?(Hash)
43
+ column_type = column_specification.fetch(:type)
44
+ column_options = column_specification.fetch(:options, {})
45
+ else
46
+ column_type = column_specification
47
+ column_options = {}
48
+ end
49
+
50
+ table.column(column_name, column_type, column_options)
51
+ end
52
+ end
53
+ end
54
+ end