cuke_linter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis.yml +14 -0
  4. data/CHANGELOG.md +19 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +148 -0
  8. data/Rakefile +23 -0
  9. data/appveyor.yml +24 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/cuke_linter.gemspec +34 -0
  13. data/environments/common_env.rb +11 -0
  14. data/environments/cucumber_env.rb +4 -0
  15. data/environments/rspec_env.rb +41 -0
  16. data/exe/cuke_linter +6 -0
  17. data/lib/cuke_linter/formatters/pretty_formatter.rb +48 -0
  18. data/lib/cuke_linter/linters/feature_without_scenarios_linter.rb +22 -0
  19. data/lib/cuke_linter/version.rb +3 -0
  20. data/lib/cuke_linter.rb +71 -0
  21. data/testing/cucumber/features/command_line.feature +12 -0
  22. data/testing/cucumber/features/formatters/pretty_formatter.feature +24 -0
  23. data/testing/cucumber/features/linters/default_linters.feature +7 -0
  24. data/testing/cucumber/features/linters/feature_without_scenarios.feature +12 -0
  25. data/testing/cucumber/step_definitions/action_steps.rb +13 -0
  26. data/testing/cucumber/step_definitions/setup_steps.rb +30 -0
  27. data/testing/cucumber/step_definitions/verification_steps.rb +18 -0
  28. data/testing/file_helper.rb +28 -0
  29. data/testing/formatter_factory.rb +15 -0
  30. data/testing/linter_factory.rb +22 -0
  31. data/testing/model_factory.rb +39 -0
  32. data/testing/rspec/spec/integration/cuke_linter_integration_spec.rb +112 -0
  33. data/testing/rspec/spec/integration/formatters/formatter_integration_specs.rb +5 -0
  34. data/testing/rspec/spec/integration/formatters/pretty_formatter_integration_spec.rb +8 -0
  35. data/testing/rspec/spec/integration/linters/feature_without_scenarios_linter_integration_spec.rb +8 -0
  36. data/testing/rspec/spec/integration/linters/linter_integration_specs.rb +7 -0
  37. data/testing/rspec/spec/unit/cuke_linter_unit_spec.rb +83 -0
  38. data/testing/rspec/spec/unit/formatters/formatter_unit_specs.rb +11 -0
  39. data/testing/rspec/spec/unit/formatters/pretty_formatter_unit_spec.rb +75 -0
  40. data/testing/rspec/spec/unit/linters/feature_without_scenarios_linter_unit_spec.rb +72 -0
  41. data/testing/rspec/spec/unit/linters/linter_unit_specs.rb +38 -0
  42. metadata +183 -0
@@ -0,0 +1,18 @@
1
+ Then(/^a linting report will be made for all features$/) do
2
+ expect(@output).to match(/\d+ issues found/)
3
+ end
4
+
5
+ Then(/^the resulting output is the following:$/) do |text|
6
+ expect(@results).to eq(text)
7
+ end
8
+
9
+ Then(/^an error is reported$/) do |table|
10
+ table.hashes.each do |error_record|
11
+ expect(@results).to include({ problem: error_record['problem'],
12
+ location: error_record['location'].sub('<path_to_file>', @model.get_ancestor(:feature_file).path) })
13
+ end
14
+ end
15
+
16
+ Then(/^the following linters are registered by default$/) do |linter_names|
17
+ expect(CukeLinter.registered_linters.keys).to eq(linter_names.raw.flatten)
18
+ end
@@ -0,0 +1,28 @@
1
+ require 'tmpdir'
2
+
3
+
4
+ module CukeLinter
5
+ module FileHelper
6
+
7
+ class << self
8
+
9
+ def created_directories
10
+ @created_directories ||= []
11
+ end
12
+
13
+ def create_directory(options = {})
14
+ options[:name] ||= 'test_directory'
15
+ options[:directory] ||= Dir.mktmpdir
16
+
17
+ path = "#{options[:directory]}/#{options[:name]}"
18
+
19
+ Dir::mkdir(path)
20
+ created_directories << options[:directory]
21
+
22
+ path
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ module CukeLinter
2
+ module FormatterFactory
3
+
4
+ def self.generate_fake_formatter(name: 'FakeFormater')
5
+ formatter = Object.new
6
+
7
+ formatter.define_singleton_method('format') do |data|
8
+ data.reduce("#{name}: ") { |final, lint_error| final << "#{lint_error[:problem]}: #{lint_error[:location]}\n" }
9
+ end
10
+
11
+ formatter
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ module CukeLinter
2
+ module LinterFactory
3
+
4
+ def self.generate_fake_linter(name: 'FakeLinter')
5
+ linter = Object.new
6
+
7
+ linter.define_singleton_method('lint') do |model|
8
+ location = model.respond_to?(:source_line) ? "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" :
9
+ model.path
10
+ [{ problem: "#{name} problem",
11
+ location: location }]
12
+ end
13
+
14
+ linter.define_singleton_method('name') do
15
+ name
16
+ end
17
+
18
+ linter
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ module CukeLinter
2
+ class TestModel < CukeModeler::Model
3
+
4
+ include CukeModeler::Sourceable
5
+
6
+ end
7
+ end
8
+
9
+
10
+ module CukeLinter
11
+ module ModelFactory
12
+
13
+ def self.generate_feature_model(source_text: 'Feature:', parent_file_path: 'path_to_file')
14
+ fake_file_model = CukeModeler::FeatureFile.new
15
+ fake_file_model.path = parent_file_path
16
+
17
+ model = CukeModeler::Feature.new(source_text)
18
+ model.parent_model = fake_file_model
19
+
20
+ model
21
+ end
22
+
23
+ def self.generate_lintable_model(parent_file_path: 'path_to_file', source_line: '1', children: [])
24
+ fake_file_model = CukeModeler::FeatureFile.new
25
+ fake_file_model.path = parent_file_path
26
+
27
+ model = CukeLinter::TestModel.new
28
+ model.parent_model = fake_file_model
29
+ model.source_line = source_line
30
+
31
+ model.define_singleton_method('children') do
32
+ children
33
+ end
34
+
35
+ model
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,112 @@
1
+ require_relative '../../../../environments/rspec_env'
2
+
3
+ RSpec.describe CukeLinter do
4
+
5
+ let(:test_model_tree) { CukeLinter::ModelFactory.generate_lintable_model }
6
+ let(:test_linters) { [CukeLinter::LinterFactory.generate_fake_linter] }
7
+ let(:test_formatters) { [[CukeLinter::FormatterFactory.generate_fake_formatter, "#{CukeLinter::FileHelper::create_directory}/junk_output_file.txt"]] }
8
+ let(:linting_options) { { model_tree: test_model_tree, linters: test_linters, formatters: test_formatters } }
9
+
10
+
11
+ it 'returns the un-formatted linting data when linting' do
12
+ results = subject.lint(linting_options)
13
+
14
+ expect(results).to eq([{ linter: 'FakeLinter', location: 'path_to_file:1', problem: 'FakeLinter problem' }])
15
+ end
16
+
17
+ it 'uses evey formatter provided' do
18
+ linting_options[:formatters] = [[CukeLinter::FormatterFactory.generate_fake_formatter(name: 'Formatter1')],
19
+ [CukeLinter::FormatterFactory.generate_fake_formatter(name: 'Formatter2')]]
20
+
21
+ expect { subject.lint(linting_options) }.to output("Formatter1: FakeLinter problem: path_to_file:1\nFormatter2: FakeLinter problem: path_to_file:1\n").to_stdout
22
+ end
23
+
24
+ it "uses the 'pretty' formatter if none are provided" do
25
+ linting_options.delete(:formatters)
26
+
27
+ expect { subject.lint(linting_options) }.to output(['FakeLinter',
28
+ ' FakeLinter problem',
29
+ ' path_to_file:1',
30
+ '',
31
+ '1 issues found',
32
+ ''].join("\n")).to_stdout
33
+ end
34
+
35
+ it 'outputs formatted linting data to the provided output location' do
36
+ output_path = "#{CukeLinter::FileHelper::create_directory}/output.txt"
37
+ linting_options[:formatters] = [[CukeLinter::FormatterFactory.generate_fake_formatter(name: 'Formatter1'),
38
+ output_path]]
39
+
40
+ expect { subject.lint(linting_options) }.to_not output.to_stdout
41
+ expect(File.read(output_path)).to eq("Formatter1: FakeLinter problem: path_to_file:1\n")
42
+ end
43
+
44
+ it 'outputs formatted data to STDOUT if not location is provided' do
45
+ linting_options[:formatters] = [[CukeLinter::FormatterFactory.generate_fake_formatter(name: 'Formatter1')]]
46
+
47
+ expect { subject.lint(linting_options) }.to output("Formatter1: FakeLinter problem: path_to_file:1\n").to_stdout
48
+ end
49
+
50
+ it 'lints every model in the model tree' do
51
+ child_model = CukeLinter::ModelFactory.generate_lintable_model(source_line: 3)
52
+ parent_model = CukeLinter::ModelFactory.generate_lintable_model(source_line: 5, children: [child_model])
53
+ linting_options[:model_tree] = parent_model
54
+
55
+ results = subject.lint(linting_options)
56
+
57
+ expect(results).to match_array([{ linter: 'FakeLinter', location: 'path_to_file:3', problem: 'FakeLinter problem' },
58
+ { linter: 'FakeLinter', location: 'path_to_file:5', problem: 'FakeLinter problem' }])
59
+ end
60
+
61
+ it 'models the current directory if a model tree is not provided' do
62
+ test_dir = CukeLinter::FileHelper::create_directory
63
+ File.write("#{test_dir}/test_feature.feature", "Feature:")
64
+ linting_options.delete(:model_tree)
65
+
66
+ Dir.chdir(test_dir) do
67
+ @results = subject.lint(linting_options)
68
+ end
69
+
70
+ # There should be 3 models to lint: the directory, the file, and the feature
71
+ expect(@results.count).to eq(3)
72
+ end
73
+
74
+ it 'uses evey linter provided' do
75
+ linting_options[:linters] = [CukeLinter::LinterFactory.generate_fake_linter(name: 'FakeLinter1'),
76
+ CukeLinter::LinterFactory.generate_fake_linter(name: 'FakeLinter2')]
77
+
78
+ results = subject.lint(linting_options)
79
+
80
+ expect(results).to match_array([{ linter: 'FakeLinter1', location: 'path_to_file:1', problem: 'FakeLinter1 problem' },
81
+ { linter: 'FakeLinter2', location: 'path_to_file:1', problem: 'FakeLinter2 problem' }])
82
+ end
83
+
84
+ it 'uses all registered linters if none are provided', :linter_registration do
85
+ CukeLinter.register_linter(linter: CukeLinter::LinterFactory.generate_fake_linter(name: 'FakeLinter1'), name: 'FakeLinter1')
86
+ CukeLinter.register_linter(linter: CukeLinter::LinterFactory.generate_fake_linter(name: 'FakeLinter2'), name: 'FakeLinter2')
87
+ CukeLinter.register_linter(linter: CukeLinter::LinterFactory.generate_fake_linter(name: 'FakeLinter3'), name: 'FakeLinter3')
88
+ linting_options.delete(:linters)
89
+
90
+ results = subject.lint(linting_options)
91
+
92
+ expect(results).to match_array([{ linter: 'FakeLinter1', location: 'path_to_file:1', problem: 'FakeLinter1 problem' },
93
+ { linter: 'FakeLinter2', location: 'path_to_file:1', problem: 'FakeLinter2 problem' },
94
+ { linter: 'FakeLinter3', location: 'path_to_file:1', problem: 'FakeLinter3 problem' }])
95
+ end
96
+
97
+ it 'includes the name of the linter in the linting data' do
98
+ linting_options[:linters] = [CukeLinter::LinterFactory.generate_fake_linter(name: 'FakeLinter1'),
99
+ CukeLinter::LinterFactory.generate_fake_linter(name: 'FakeLinter2')]
100
+
101
+ results = subject.lint(linting_options)
102
+
103
+ expect(results).to match_array([{ linter: 'FakeLinter1', location: 'path_to_file:1', problem: 'FakeLinter1 problem' },
104
+ { linter: 'FakeLinter2', location: 'path_to_file:1', problem: 'FakeLinter2 problem' }])
105
+ end
106
+
107
+ it 'has a default set of registered linters' do
108
+ expect(subject.registered_linters.keys).to include('FeatureWithoutScenariosLinter')
109
+ expect(subject.registered_linters['FeatureWithoutScenariosLinter']).to be_a(CukeLinter::FeatureWithoutScenariosLinter)
110
+ end
111
+
112
+ end
@@ -0,0 +1,5 @@
1
+ shared_examples_for 'a formatter at the integration level' do
2
+
3
+ # No defined behavior yet
4
+
5
+ end
@@ -0,0 +1,8 @@
1
+ require_relative '../../../../../environments/rspec_env'
2
+
3
+
4
+ RSpec.describe CukeLinter::PrettyFormatter do
5
+
6
+ it_should_behave_like 'a formatter at the integration level'
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ require_relative '../../../../../environments/rspec_env'
2
+
3
+
4
+ RSpec.describe CukeLinter::FeatureWithoutScenariosLinter do
5
+
6
+ it_should_behave_like 'a linter at the integration level'
7
+
8
+ end
@@ -0,0 +1,7 @@
1
+ shared_examples_for 'a linter at the integration level' do
2
+
3
+ # it 'inherits from the base linter' do
4
+ # expect(subject).to be_a(CukeLinter::BaseLinter)
5
+ # end
6
+
7
+ end
@@ -0,0 +1,83 @@
1
+ require_relative '../../../../environments/rspec_env'
2
+
3
+
4
+ RSpec.describe 'the gem' do
5
+
6
+ let(:gemspec) { eval(File.read "#{__dir__}/../../../../cuke_linter.gemspec") }
7
+
8
+ it 'has an executable' do
9
+ expect(gemspec.executables).to include('cuke_linter')
10
+ end
11
+
12
+ it 'validates cleanly' do
13
+ mock_ui = Gem::MockGemUi.new
14
+ Gem::DefaultUserInteraction.use_ui(mock_ui) { gemspec.validate }
15
+
16
+ expect(mock_ui.error).to_not match(/warn/i)
17
+ end
18
+
19
+ end
20
+
21
+
22
+ RSpec.describe CukeLinter do
23
+
24
+ it "has a version number" do
25
+ expect(CukeLinter::VERSION).not_to be nil
26
+ end
27
+
28
+ it 'can lint' do
29
+ expect(CukeLinter).to respond_to(:lint)
30
+ end
31
+
32
+ it 'lints the (optionally) given model tree using the (optionally) provided set of linters and formats the output with the (optionally) provided formatters' do
33
+ expect(CukeLinter.method(:lint).arity).to eq(-1)
34
+ expect(CukeLinter.method(:lint).parameters).to match_array([[:key, :model_tree],
35
+ [:key, :linters],
36
+ [:key, :formatters]])
37
+ end
38
+
39
+ it 'can register a linter' do
40
+ expect(CukeLinter).to respond_to(:register_linter)
41
+ end
42
+
43
+ it 'can unregister a linter' do
44
+ expect(CukeLinter).to respond_to(:unregister_linter)
45
+ end
46
+
47
+ it 'registers a linter by name' do
48
+ expect(CukeLinter.method(:register_linter).arity).to eq(1)
49
+ expect(CukeLinter.method(:register_linter).parameters).to match_array([[:keyreq, :linter],
50
+ [:keyreq, :name]])
51
+ end
52
+
53
+ it 'unregisters a linter by name' do
54
+ expect(CukeLinter.method(:unregister_linter).arity).to eq(1)
55
+ expect(CukeLinter.method(:unregister_linter).parameters).to match_array([[:req, :name]])
56
+ end
57
+
58
+ it 'knows its currently registered linters' do
59
+ expect(CukeLinter).to respond_to(:registered_linters)
60
+ end
61
+
62
+ it 'correctly registers, unregisters, and tracks linters', :linter_registration do
63
+ CukeLinter.clear_registered_linters
64
+ CukeLinter.register_linter(name: 'foo', linter: :linter_1)
65
+ CukeLinter.register_linter(name: 'bar', linter: :linter_2)
66
+ CukeLinter.register_linter(name: 'baz', linter: :linter_3)
67
+
68
+ CukeLinter.unregister_linter('bar')
69
+
70
+ expect(CukeLinter.registered_linters).to eq({ 'foo' => :linter_1,
71
+ 'baz' => :linter_3, })
72
+ end
73
+
74
+ it 'can clear all of its currently registered linters', :linter_registration do
75
+ expect(CukeLinter).to respond_to(:clear_registered_linters)
76
+
77
+ CukeLinter.register_linter(name: 'some_linter', linter: :the_linter)
78
+ CukeLinter.clear_registered_linters
79
+
80
+ expect(CukeLinter.registered_linters).to eq({})
81
+ end
82
+
83
+ end
@@ -0,0 +1,11 @@
1
+ shared_examples_for 'a formatter at the unit level' do
2
+
3
+ it 'can format' do
4
+ expect(subject).to respond_to(:format)
5
+ end
6
+
7
+ it 'formats linting data' do
8
+ expect(subject.method(:format).arity).to eq(1)
9
+ end
10
+
11
+ end
@@ -0,0 +1,75 @@
1
+ require_relative '../../../../../environments/rspec_env'
2
+
3
+
4
+ RSpec.describe CukeLinter::PrettyFormatter do
5
+
6
+ it_should_behave_like 'a formatter at the unit level'
7
+
8
+
9
+ it 'formats linting data as pretty text' do
10
+ linting_data = [{ linter: 'SomeLinter',
11
+ problem: 'Some problem',
12
+ location: 'path/to/the_file:1' },
13
+ { linter: 'SomeOtherLinter',
14
+ problem: 'Some other problem',
15
+ location: 'path/to/the_file:1' }]
16
+
17
+ results = subject.format(linting_data)
18
+
19
+ expect(results).to eq(['SomeLinter',
20
+ ' Some problem',
21
+ ' path/to/the_file:1',
22
+ 'SomeOtherLinter',
23
+ ' Some other problem',
24
+ ' path/to/the_file:1',
25
+ '',
26
+ '2 issues found'].join("\n"))
27
+ end
28
+
29
+ it 'groups data by linter and problem' do
30
+ linting_data = [{ linter: 'SomeLinter',
31
+ problem: 'Some problem',
32
+ location: 'path/to/the_file:1' },
33
+ { linter: 'SomeOtherLinter',
34
+ problem: 'Some other problem',
35
+ location: 'path/to/the_file:1' },
36
+ { linter: 'SomeLinter',
37
+ problem: 'Some problem',
38
+ location: 'path/to/the_file:11' }]
39
+
40
+ results = subject.format(linting_data)
41
+
42
+ expect(results).to eq(['SomeLinter',
43
+ ' Some problem',
44
+ ' path/to/the_file:1',
45
+ ' path/to/the_file:11',
46
+ 'SomeOtherLinter',
47
+ ' Some other problem',
48
+ ' path/to/the_file:1',
49
+ '',
50
+ '3 issues found'].join("\n"))
51
+ end
52
+
53
+ it 'orders violations by line number' do
54
+ linting_data = [{ linter: 'SomeLinter',
55
+ problem: 'Some problem',
56
+ location: 'path/to/the_file:2' },
57
+ { linter: 'SomeLinter',
58
+ problem: 'Some problem',
59
+ location: 'path/to/the_file:3' },
60
+ { linter: 'SomeLinter',
61
+ problem: 'Some problem',
62
+ location: 'path/to/the_file:1' }]
63
+
64
+ results = subject.format(linting_data)
65
+
66
+ expect(results).to eq(['SomeLinter',
67
+ ' Some problem',
68
+ ' path/to/the_file:1',
69
+ ' path/to/the_file:2',
70
+ ' path/to/the_file:3',
71
+ '',
72
+ '3 issues found'].join("\n"))
73
+ end
74
+
75
+ end
@@ -0,0 +1,72 @@
1
+ require_relative '../../../../../environments/rspec_env'
2
+
3
+
4
+ RSpec.describe CukeLinter::FeatureWithoutScenariosLinter do
5
+
6
+ let(:good_data) do
7
+ model = CukeLinter::ModelFactory.generate_feature_model
8
+ model.tests = ['totally_a_test']
9
+
10
+ model
11
+ end
12
+
13
+ let(:bad_data) do
14
+ model = CukeLinter::ModelFactory.generate_feature_model
15
+ model.tests = []
16
+
17
+ model
18
+ end
19
+
20
+
21
+ it_should_behave_like 'a linter at the unit level'
22
+
23
+
24
+ it 'has a name' do
25
+ expect(subject.name).to eq('FeatureWithoutScenariosLinter')
26
+ end
27
+
28
+ describe 'linting' do
29
+
30
+ context 'a feature with no scenarios' do
31
+
32
+ let(:test_model) do
33
+ model = CukeLinter::ModelFactory.generate_feature_model(parent_file_path: 'path_to_file')
34
+ model.tests = []
35
+
36
+ model
37
+ end
38
+
39
+ it 'records a problem' do
40
+ results = subject.lint(test_model)
41
+
42
+ expect(results.first[:problem]).to eq('Feature has no scenarios')
43
+ end
44
+
45
+ it 'records the location of the problem' do
46
+ model_1 = CukeLinter::ModelFactory.generate_feature_model(parent_file_path: 'path_to_file')
47
+ model_1.tests = []
48
+ model_1.source_line = 1
49
+ model_2 = CukeLinter::ModelFactory.generate_feature_model(parent_file_path: 'path_to_file')
50
+ model_2.tests = []
51
+ model_2.source_line = 3
52
+
53
+ results = subject.lint(model_1)
54
+ expect(results.first[:location]).to eq('path_to_file:1')
55
+
56
+ results = subject.lint(model_2)
57
+ expect(results.first[:location]).to eq('path_to_file:3')
58
+ end
59
+
60
+ end
61
+
62
+ context 'a non-feature model' do
63
+
64
+ it 'returns an empty set of results' do
65
+ results = subject.lint(CukeModeler::Model.new)
66
+
67
+ expect(results).to eq([])
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,38 @@
1
+ shared_examples_for 'a linter at the unit level' do
2
+
3
+ it 'is named' do
4
+ expect(subject).to respond_to(:name)
5
+ expect(subject.name).to be_a_kind_of(String)
6
+ expect(subject.name).to_not be_empty
7
+ end
8
+
9
+ it 'can lint' do
10
+ expect(subject).to respond_to(:lint)
11
+ end
12
+
13
+ it 'lints a model' do
14
+ expect(subject.method(:lint).arity).to eq(1)
15
+ end
16
+
17
+ context 'with good data' do
18
+
19
+ it 'returns an empty set of results' do
20
+ expect(subject.lint(good_data)).to eq([])
21
+ end
22
+
23
+ end
24
+
25
+ context 'with bad data' do
26
+
27
+ it 'returns a set of detected problems' do
28
+ expect(subject.lint(bad_data)).to_not be_empty
29
+ end
30
+
31
+ it 'includes the problems and their locations in its results' do
32
+ results = subject.lint(bad_data)
33
+
34
+ results.each { |result| expect(result.keys).to match_array([:problem, :location]) }
35
+ end
36
+
37
+ end
38
+ end