Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
ast (2.4.3)
coderay (1.1.3)
diff-lcs (1.5.1)
diff-lcs (1.6.2)
method_source (1.1.0)
ostruct (0.6.3)
parser (3.3.6.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
prism (1.9.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
racc (1.8.1)
require_all (3.0.0)
rspec (3.13.0)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.2)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.2)
rubocop-ast (1.36.2)
parser (>= 3.3.1.0)
rspec-support (3.13.7)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
yaml (0.1.1)
zeitwerk (2.7.5)

PLATFORMS
arm64-darwin-23
aarch64-linux
ruby

DEPENDENCIES
ostruct (~> 0.6.2)
pry (~> 0.14.1)
require_all (~> 3.0)
rspec (~> 3.12)
rubocop-ast (~> 1.26)
yaml (~> 0.1.0)
zeitwerk (~> 2.6)

BUNDLED WITH
2.5.18
2.5.22
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Rubyzen uses [RuboCop AST](https://github.com/rubocop/rubocop-ast) under the hoo
- Confirming that model classes do not use question-mark in methods, enforcing us to use the Ask pattern.

### Running Lint Rules

You should first start a devcontainer, with `devcontainer open .` run from the `Rubyzen` folder.

To run lint rules, execute RSpec with the path to your rule specifications. The rules will analyze the currently mounted target project:

```bash
Expand Down
46 changes: 33 additions & 13 deletions lib/rubyzen/matchers/be_empty_matcher.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@


RSpec::Matchers.define :be_empty do |custom_message=nil|
RSpec::Matchers.define :be_empty do |custom_message=nil, allowlist: nil, baseline: nil|
include Rubyzen::Matchers::MatcherHelpers

match do |subject_collection|
@custom_message = custom_message
@offenders = []

items = Array(subject_collection) # to handle one or multiple subjects

items.each do |item|
@offenders << element_name(item) if item != nil
end

@offenders.empty?
options = custom_message.is_a?(Hash) ? custom_message : {}
resolved_allowlist = allowlist || options[:allowlist] || options['allowlist']
resolved_baseline = baseline || options[:baseline] || options['baseline']
@custom_message = options[:message] || options['message'] || (custom_message unless custom_message.is_a?(Hash))

@classified_items = classify_items(
subject_collection,
allowlist: resolved_allowlist,
baseline: resolved_baseline
)
@offenders = @classified_items[:violations]
stale_exception_groups = []
stale_exception_groups << 'baseline entries' if @classified_items[:stale_baseline].any?
stale_exception_groups << 'allowlist entries' if @classified_items[:stale_allowlist].any?

@failure_reason = if @classified_items[:violations].any? && stale_exception_groups.any?
"Expected to be empty, but found live violations and stale #{stale_exception_groups.join(' and ')}."
elsif @classified_items[:violations].any?
if resolved_baseline || resolved_allowlist
'Expected to be empty, but found live violations.'
else
'Expected to be empty, but had elements.'
end
elsif stale_exception_groups.any?
"Expected to be empty, but found stale #{stale_exception_groups.join(' and ')}."
end

@classified_items[:violations].empty? &&
@classified_items[:stale_baseline].empty? &&
@classified_items[:stale_allowlist].empty?
end

failure_message do |_|
message_for_failure("Expected to be empty, but had elements.\n#{@offenders.join("\n")}")
message_for_failure(@failure_reason || 'Expected to be empty, but had elements.')
end

failure_message_when_negated do |_|
message_for_failure("Expected not to be empty, but had no elements:\n#{@offenders.join("\n")}")
message_for_failure('Expected not to be empty, but had no elements.')
end
end
42 changes: 12 additions & 30 deletions lib/rubyzen/matchers/be_empty_with_exceptions_matcher.rb
Original file line number Diff line number Diff line change
@@ -1,32 +1,14 @@
RSpec::Matchers.define :be_empty_with_exceptions do |custom_message=nil, allowlist: nil, baseline: nil|
include Rubyzen::Matchers::MatcherHelpers

match do |subject_collection|
@custom_message = custom_message
@classified_items = classify_items(subject_collection, allowlist: allowlist, baseline: baseline)
@offenders = @classified_items[:violations]
stale_exception_groups = []
stale_exception_groups << 'baseline entries' if @classified_items[:stale_baseline].any?
stale_exception_groups << 'allowlist entries' if @classified_items[:stale_allowlist].any?

@failure_reason = if @classified_items[:violations].any? && stale_exception_groups.any?
"Expected to be empty, but found live violations and stale #{stale_exception_groups.join(' and ')}."
elsif @classified_items[:violations].any?
'Expected to be empty, but found live violations.'
elsif stale_exception_groups.any?
"Expected to be empty, but found stale #{stale_exception_groups.join(' and ')}."
end

@classified_items[:violations].empty? &&
@classified_items[:stale_baseline].empty? &&
@classified_items[:stale_allowlist].empty?
end

failure_message do |_|
message_for_failure(@failure_reason || 'Expected to be empty, but had unmatched elements.')
end

failure_message_when_negated do |_|
message_for_failure('Expected not to be empty, but had no matched elements.')
end
match do |subject_collection|
@matcher = be_empty(custom_message, allowlist: allowlist, baseline: baseline)
@matcher.matches?(subject_collection)
end

failure_message do |_|
@matcher.failure_message
end

failure_message_when_negated do |_|
@matcher.failure_message_when_negated
end
end
41 changes: 31 additions & 10 deletions lib/rubyzen/matchers/be_true_matcher.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@


RSpec::Matchers.define :be_true do |custom_message=nil|
RSpec::Matchers.define :be_true do |custom_message=nil, allowlist: nil, baseline: nil|
include Rubyzen::Matchers::MatcherHelpers

match do |subject_collection|
@custom_message = custom_message
options = custom_message.is_a?(Hash) ? custom_message : {}
resolved_allowlist = allowlist || options[:allowlist] || options['allowlist']
resolved_baseline = baseline || options[:baseline] || options['baseline']
@custom_message = options[:message] || options['message'] || (custom_message unless custom_message.is_a?(Hash))
@offenders = []

if block_arg != nil
items = Array(subject_collection) # to handle one or multiple subjects

items.each do |item|
@offenders << element_name(item) unless block_arg.call(item)
end

@offenders.empty?
failing_items = items.filter { |item| !block_arg.call(item) }
@classified_items = classify_items(
failing_items,
allowlist: resolved_allowlist,
baseline: resolved_baseline
)
@offenders = @classified_items[:violations]

stale_exception_groups = []
stale_baseline = @classified_items[:stale_baseline]
stale_allowlist = @classified_items[:stale_allowlist]
stale_exception_groups << 'baseline entries' if stale_baseline.any?
stale_exception_groups << 'allowlist entries' if stale_allowlist.any?

@failure_reason = if @offenders.any? && stale_exception_groups.any?
"Expected to return true for all elements, but found live violations and stale #{stale_exception_groups.join(' and ')}."
elsif @offenders.any?
"Expected to return true for all elements, but returned false for:\n#{@offenders.join("\n")}"
elsif stale_exception_groups.any?
"Expected to return true for all elements, but found stale #{stale_exception_groups.join(' and ')}."
end

@offenders.empty? && stale_baseline.empty? && stale_allowlist.empty?
else
@failure_message = "Expected a block, but got nil."
@failure_reason = "Expected a block, but got nil."
false
end
end

failure_message do |_|
message_for_failure("Expected to return true for all elements, but returned false for:\n#{@offenders.join("\n")}")
message_for_failure(@failure_reason || "Expected to return true for all elements.")
end

failure_message_when_negated do |_|
message_for_failure("Expected to return false for at least one element, but returned true for:\n#{@offenders.join("\n")}")
message_for_failure("Expected to return false for at least one element, but returned true for:\n#{Array(@classified_items&.dig(:baseline)).join("\n")}")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

require_relative '../spec_helper'

RSpec.describe 'be_empty_with_exceptions' do
TestItem = Struct.new(:name, :class_name, :file_path, :line, keyword_init: true)
RSpec.describe 'be_empty' do
let(:test_item_class) { Struct.new(:name, :class_name, :file_path, :line, keyword_init: true) }

let(:root_path) { File.expand_path('../..', __dir__) }

def build_item(name:, class_name:, file_path:, line: 1)
TestItem.new(name: name, class_name: class_name, file_path: file_path, line: line)
test_item_class.new(name: name, class_name: class_name, file_path: file_path, line: line)
end

it 'groups baseline, allowlist, and live violations in the failure output' do
Expand Down
69 changes: 69 additions & 0 deletions sample_project/spec/matchers/be_true_matcher_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

require_relative '../spec_helper'

RSpec.describe 'be_true' do
let(:test_item_class) { Struct.new(:name, :class_name, :file_path, :line, keyword_init: true) }

let(:root_path) { File.expand_path('../..', __dir__) }

def build_item(class_name: 'Questions::Albums::Type', file_path: 'src/questions/albums/type.rb')
test_item_class.new(
name: 'private_album?',
class_name: class_name,
file_path: File.join(root_path, file_path),
line: 10
)
end

it 'fails when a baseline entry is stale' do
matcher = be_true(baseline: ['Questions::Albums::Type']) { |_item| true }

expect(matcher.matches?([build_item])).to be(false)
expect(matcher.failure_message).to include('Expected to return true for all elements, but found stale baseline entries.')
expect(matcher.failure_message).to include('Stale baseline entries:')
expect(matcher.failure_message).to include('Questions::Albums::Type')
end

it 'treats baseline entries as expected failures when they still fail' do
matcher = be_true(baseline: ['Questions::Albums::Type']) { |_item| false }

expect(matcher.matches?([build_item])).to be(true)
end

it 'fails with expect syntax when baseline entry refers to a passing type' do
items = [build_item]
baseline = ['Questions::Albums::Type']
matcher = be_true(baseline: baseline) { |_item| true }

expect(matcher.matches?(items)).to be(false)
expect(matcher.failure_message).to include('Stale baseline entries:')
expect(matcher.failure_message).to include('Questions::Albums::Type')
end

it 'fails with expect syntax when baseline entry refers to a non-existent type' do
items = [build_item]
baseline = ['Questions::Albums::NonExistentTypeThatWasJustDeleted']
matcher = be_true(baseline: baseline) { |_item| true }

expect(matcher.matches?(items)).to be(false)
expect(matcher.failure_message).to include('Stale baseline entries:')
expect(matcher.failure_message).to include('Questions::Albums::NonExistentTypeThatWasJustDeleted')
end

it 'treats allowlisted entries as expected failures when they still fail' do
matcher = be_true(allowlist: ['Questions::Albums::Type']) { |_item| false }

expect(matcher.matches?([build_item])).to be(true)
end

it 'fails when an allowlist entry is stale' do
matcher = be_true(allowlist: ['Questions::Albums::Type']) { |_item| true }

expect(matcher.matches?([build_item])).to be(false)
expect(matcher.failure_message).to include('Expected to return true for all elements, but found stale allowlist entries.')
expect(matcher.failure_message).to include('Stale allowlist entries:')
expect(matcher.failure_message).to include('Questions::Albums::Type')
end

end
Loading