-
Notifications
You must be signed in to change notification settings - Fork 9
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,7 +33,7 @@ Feature: Default Linters | |
| TestWithSetupStepAfterVerificationStepLinter | | ||
| TestWithSetupStepAsFinalStepLinter | | ||
| TestWithTooManyStepsLinter | | ||
|
||
| UniqueScenarioNamesLinter | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
Feature: Unique scenario names | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 'regular' scenario here is not one. Once the |
||
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 | |
There was a problem hiding this comment.
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.