Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UniqueScenarioNamesLinter for enforcing unique scenario names #30

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions lib/cuke_linter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
require 'cuke_linter/linters/test_with_setup_step_after_verification_step_linter'
require 'cuke_linter/linters/test_with_setup_step_as_final_step_linter'
require 'cuke_linter/linters/test_with_too_many_steps_linter'
require 'cuke_linter/linters/unique_scenario_names_linter'
require 'cuke_linter/configuration'
require 'cuke_linter/default_linters'
require 'cuke_linter/gherkin'
Expand Down
3 changes: 2 additions & 1 deletion lib/cuke_linter/default_linters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ module CukeLinter # rubocop:disable Style/Documentation
'TestWithSetupStepAfterActionStepLinter' => TestWithSetupStepAfterActionStepLinter.new,
'TestWithSetupStepAfterVerificationStepLinter' => TestWithSetupStepAfterVerificationStepLinter.new,
'TestWithSetupStepAsFinalStepLinter' => TestWithSetupStepAsFinalStepLinter.new,
'TestWithTooManyStepsLinter' => TestWithTooManyStepsLinter.new }
'TestWithTooManyStepsLinter' => TestWithTooManyStepsLinter.new,
'UniqueScenarioNamesLinter' => UniqueScenarioNamesLinter.new }
Comment on lines +29 to +30
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New linters should not be added to the default list. The list of linters that are active by default can be updated on major releases. Users can activate them explicitly if they want them sooner.

# rubocop:enable Layout/LineLength

end
120 changes: 120 additions & 0 deletions lib/cuke_linter/linters/unique_scenario_names_linter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
module CukeLinter

# A linter that detects non-unique scenario names
class UniqueScenarioNamesLinter < Linter

def initialize
super
@scenario_names = {}
end

def rule(model)
return nil unless valid_model?(model)

feature_file = model.get_ancestor(:feature_file)
return nil unless feature_file

file_path = feature_file.path

case model
when CukeModeler::Rule
check_rule(model, file_path)
when CukeModeler::Scenario, CukeModeler::Outline
# Skip scenarios and outlines inside rules
return nil if model.get_ancestor(:rule)

scope_key = "#{file_path}:feature"
check_scenario_or_outline(model, scope_key)
end
end

def message
@specified_message || 'Scenario names are not unique'
end

private

def create_duplicate_message(name, scope_key, is_outline = false)
original_line = @scenario_names[scope_key][name].first
duplicate_lines = @scenario_names[scope_key][name][1..].join(', ')
prefix = is_outline ? 'Scenario name created by Scenario Outline' : 'Scenario name'
"#{prefix} '#{name}' is not unique.\n " \
"Original name is on line: #{original_line}\n " \
"Duplicate is on: #{duplicate_lines}"
end

def check_rule(model, file_path)
rule_scope_key = "#{file_path}:rule:#{model.name}"

problems = model.scenarios.map { |scenario| check_scenario(scenario, rule_scope_key) }
problems += model.outlines.map { |outline| check_scenario_or_outline(outline, rule_scope_key) }

problems.compact.first
end

def check_scenario_or_outline(model, scope_key)
if model.is_a?(CukeModeler::Outline)
check_scenario_outline(model, scope_key)
else
check_scenario(model, scope_key)
end
end

def check_scenario(model, scope_key)
scenario_name = model.name
record_scenario(scenario_name, scope_key, model.source_line)
return nil unless duplicate_name?(scenario_name, scope_key)

@specified_message = create_duplicate_message(scenario_name, scope_key)
message
end

def check_scenario_outline(model, scope_key)
base_name = model.name
scenario_names = generate_scenario_names(model, base_name)

scenario_names.each do |scenario_name|
record_scenario(scenario_name, scope_key, model.source_line)
end

duplicates = scenario_names.select { |name| duplicate_name?(name, scope_key) }.uniq
return nil unless duplicates.any?

@specified_message = create_duplicate_message(duplicates.first, scope_key, true)
message
end

def generate_scenario_names(model, base_name)
model.examples.flat_map do |example|
header_row = example.rows.first
example.rows[1..].map do |data_row|
interpolate_name(base_name, header_row, data_row)
end
end
end

def interpolate_name(base_name, header_row, data_row)
interpolated_name = base_name.dup
header_row.cells.each_with_index do |header, index|
interpolated_name.gsub!("<#{header.value}>", data_row.cells[index].value.to_s)
end
interpolated_name
end

def record_scenario(scenario_name, scope_key, source_line)
@scenario_names[scope_key] ||= Hash.new { |h, k| h[k] = [] }
@scenario_names[scope_key][scenario_name] << source_line
end

def duplicate_name?(scenario_name, scope_key)
@scenario_names[scope_key][scenario_name].count > 1
end

def valid_model?(model)
model.is_a?(CukeModeler::Scenario) ||
model.is_a?(CukeModeler::Outline) ||
model.is_a?(CukeModeler::Rule)
end

end
end
2 changes: 1 addition & 1 deletion testing/cucumber/features/default_linters.feature
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Feature: Default Linters
| TestWithSetupStepAfterVerificationStepLinter |
| TestWithSetupStepAsFinalStepLinter |
| TestWithTooManyStepsLinter |

| UniqueScenarioNamesLinter |
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment regarding default linters.

Additionally, this test would need to be updated when new default linters are added. The Cucumber features are executable documentation for users, so it is important that they are up to date too, but the RSpec tests should cover everything on their own and prove the functionality of the gem, including the parts that aren't worth mentioning at the higher level of documentation.


Scenario: Registering new linters
Given no linters are currently registered
Expand Down
189 changes: 189 additions & 0 deletions testing/cucumber/features/linters/unique_scenario_names.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
Feature: Unique scenario names
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature file needs another pass. Several of the example features don't demonstrate the use case that their scenario names state.


As a reader of documentation
I want each scenario to have a unique name within its scope, where scenarios in Rules are scoped to their Rule and other scenarios are scoped to the Feature file
So that each scenario clearly describes a specific aspect of the application's functionality.

Scenario: Linting (Good: No Duplicates within Feature including Rules)
Given the following feature:
"""
Feature: Sample Feature

Scenario: Unique Scenario Name 1
Given something

Scenario: Unique Scenario Name 2
Given something else

Scenario Outline: Unique Scenario Outline Name With <input>
Examples:
| input |
| something |
| something else |

Rule: Example Rule
Scenario: Unique Scenario Name within Rule 1
Given a rule specific condition

Scenario: Unique Scenario Name within Rule 2
Given another rule specific condition

Scenario: Unique Scenario Name 1
Given something

Rule: Example Rule 1
Scenario: Duplicate Scenario Name
Given something

Rule: Example Rule 2
Scenario: Duplicate Scenario Name
Given something
"""
And a linter for unique scenario names
When the model is linted
Then no error is reported

Scenario: Linting (Good: Duplicates within different Rules)
Given the following feature:
"""
Feature: Sample Feature with Rules

Rule: Sample Rule 1

Scenario: Duplicate Scenario Name
Given something

Rule: Sample Rule 2

Scenario: Duplicate Scenario Name
Given something
"""
And a linter for unique scenario names
When the model is linted
Then no error is reported

Scenario: Linting (Good: Duplicates within Rule and regular scenario)
Given the following feature:
"""
Feature: Sample Feature with Rules

Scenario: Duplicate Scenario Name
Given something

Rule: Sample Rule

Scenario: Duplicate Scenario Name
Given something
"""
And a linter for unique scenario names
When the model is linted
Then no error is reported

Scenario: Linting (Bad: Duplicates of regular scenario name and one from a Rule)
Given the following feature:
"""
Feature: Sample Feature

Rule: Example Rule
Scenario: Duplicate Scenario Name
Given something

Scenario: Duplicate Scenario Name
Given something
"""
Comment on lines +82 to +93
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'regular' scenario here is not one. Once the Rule keyword is used, all scenarios that come after it will be considered part of that rule or part of a later Rule. Regular scenarios have to come before any usage of Rule in a feature.

And a linter for unique scenario names
When the model is linted
Then the following problems are reported:
| linter | problem | location |
| UniqueScenarioNamesLinter | Scenario name 'Duplicate Scenario Name' is not unique.\n Original name is on line: 4\n Duplicate is on: 7 | path_to_file:3 |

Scenario: Linting (Bad: Duplicates within Feature)
Given the following feature:
"""
Feature: Sample Feature

Scenario: Duplicate Scenario Name
Given something

Scenario: Duplicate Scenario Name
Given something else
"""
And a linter for unique scenario names
When the model is linted
Then the following problems are reported:
| linter | problem | location |
| UniqueScenarioNamesLinter | Scenario name 'Duplicate Scenario Name' is not unique.\n Original name is on line: 3\n Duplicate is on: 6 | path_to_file:6 |

Scenario: Linting (Bad: Duplicates from Scenario Outline)
Given the following feature:
"""
Feature: Sample Feature with Scenario Outline

Scenario Outline: Duplicate Scenario Name With <input>

Examples:
| input |
| something |
| something |
"""
And a linter for unique scenario names
When the model is linted
Then the following problems are reported:
| linter | problem | location |
| UniqueScenarioNamesLinter | Scenario name created by Scenario Outline 'Duplicate Scenario Name With something' is not unique.\n Original name is on line: 3\n Duplicate is on: 3 | path_to_file:3 |

Scenario: Linting (Bad: Duplicates within Rule)
Given the following feature:
"""
Feature: Sample Feature with Rules

Rule: Sample Rule

Scenario: Duplicate Scenario Name
Given something

Scenario: Duplicate Scenario Name
Given something else
"""
And a linter for unique scenario names
When the model is linted
Then the following problems are reported:
| linter | problem | location |
| UniqueScenarioNamesLinter | Scenario name 'Duplicate Scenario Name' is not unique.\n Original name is on line: 5\n Duplicate is on: 8 | path_to_file:3 |

Scenario: Linting (Bad: Duplicates from Scenario Outline without placeholders)
Given the following feature:
"""
Feature: Sample Feature with Scenario Outline without placeholders

Scenario Outline: Duplicate Scenario Name

Examples:
| input |
| Something |
| Something else |
"""
And a linter for unique scenario names
When the model is linted
Then the following problems are reported:
| linter | problem | location |
| UniqueScenarioNamesLinter | Scenario name created by Scenario Outline 'Duplicate Scenario Name' is not unique.\n Original name is on line: 3\n Duplicate is on: 3 | path_to_file:3 |

Scenario: Linting (Bad: No Scenario Name with Different Examples)
Given the following feature:
"""
Feature: Sample Feature with Scenario Outline without a Name

Scenario Outline:
Given I have <input>

Examples:
| input |
| something |
| anything |
"""
And a linter for unique scenario names
When the model is linted
Then the following problems are reported:
| linter | problem | location |
| UniqueScenarioNamesLinter | Scenario name created by Scenario Outline '' is not unique.\n Original name is on line: 3\n Duplicate is on: 3 | path_to_file:3 |
4 changes: 4 additions & 0 deletions testing/cucumber/step_definitions/setup_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
@linter = CukeLinter::TestWithBadNameLinter.new
end

Given(/^a linter for unique scenario names$/) do
@linter = CukeLinter::UniqueScenarioNamesLinter.new
end

Given(/^a linter for tests with too many steps$/) do
@linter = CukeLinter::TestWithTooManyStepsLinter.new
end
Expand Down
Loading