solargraph_test_coverage 0.2.4 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afacac1a6d557ea7ef39c86081fc7f3aafcb7e09c3a8243679e9d252ba47a2c7
4
- data.tar.gz: 9e929da5ff5873ef3c044edebb4a96575cb47f4bfb9b2d36121082257ac03569
3
+ metadata.gz: 5aba663e3f7dca496c3c527adc1be8024353932ef806cf6f9eed9c70bd44f120
4
+ data.tar.gz: d4a031ffbb4e97f2d13eff2552760d3beae56413b1666e6e84edd1c182919c1e
5
5
  SHA512:
6
- metadata.gz: 58b77e64736847a4d5d6fedb3c67f7b76e6af6056e844926356269c052b8bfe5f29af0c1f76e916f2e26d1ddb5c5628d3d324f53c890ceeff55f289959409b8c
7
- data.tar.gz: 92e5f351f941d99103339f80fb05450e1393cb5929f584cd7d080759450f9cd31422a71d51ba847664b1217b88c761f1f132f346f280106580c8c806ba1c6bd9
6
+ metadata.gz: 71edec840ad30fb0e2852c0f6ad3928b961631f546d02060500001e4c8e6bc0ff9f5bb3f71f4e4261ed00d483d6d4c6c3e25fc7dca35cd970a386b77ba28df16
7
+ data.tar.gz: 1abf9451d0fdcaca5f5f4ec52a6ffb918391bfdc092a09c77778b76dcc137b41c0e17719d7141182a92f472f2505d56f2f9b93fc0547748f681a1f1ba226bd78
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- solargraph_test_coverage (0.2.3)
4
+ solargraph_test_coverage (0.3.0)
5
5
  solargraph (~> 0.40, > 0.40)
6
6
 
7
7
  GEM
@@ -19,7 +19,7 @@ GEM
19
19
  kramdown (~> 2.0)
20
20
  nokogiri (1.12.4-x86_64-darwin)
21
21
  racc (~> 1.4)
22
- parallel (1.20.1)
22
+ parallel (1.21.0)
23
23
  parser (3.0.2.0)
24
24
  ast (~> 2.4.1)
25
25
  racc (1.5.2)
@@ -29,7 +29,7 @@ GEM
29
29
  reverse_markdown (2.0.0)
30
30
  nokogiri
31
31
  rexml (3.2.5)
32
- rubocop (1.20.0)
32
+ rubocop (1.21.0)
33
33
  parallel (~> 1.10)
34
34
  parser (>= 3.0.0.0)
35
35
  rainbow (>= 2.2.2, < 4.0)
@@ -41,7 +41,7 @@ GEM
41
41
  rubocop-ast (1.11.0)
42
42
  parser (>= 3.0.1.1)
43
43
  ruby-progressbar (1.11.0)
44
- solargraph (0.43.0)
44
+ solargraph (0.43.2)
45
45
  backport (~> 1.2)
46
46
  benchmark
47
47
  bundler (>= 1.17.2)
@@ -58,7 +58,7 @@ GEM
58
58
  yard (~> 0.9, >= 0.9.24)
59
59
  thor (1.1.0)
60
60
  tilt (2.0.10)
61
- unicode-display_width (2.0.0)
61
+ unicode-display_width (2.1.0)
62
62
  yard (0.9.26)
63
63
 
64
64
  PLATFORMS
data/README.md CHANGED
@@ -33,13 +33,13 @@ test_coverage:
33
33
  preload_rails: true
34
34
  test_framework: rspec # or minitest
35
35
  coverage:
36
- - line
37
- - branch
38
- - test_failing
39
- - test_missing
36
+ - line
37
+ - branch
38
+ - test_failing
39
+ - test_missing
40
40
  exclude_paths:
41
- - 'app/controller'
42
- - 'concerns'
41
+ - 'app/controller'
42
+ - 'concerns'
43
43
  ```
44
44
 
45
45
 
@@ -51,6 +51,9 @@ And then execute:
51
51
  Or install it yourself as:
52
52
 
53
53
  $ gem install solargraph_test_coverage
54
+
55
+ A note on testing framework:
56
+ Since both Minitest and RSpec are supported, neither are direct dependencies of this gem. Therefore, you have to have them installed separately either via your bundle or via `gem install`.
54
57
 
55
58
  ## Usage
56
59
 
@@ -3,22 +3,15 @@
3
3
  module SolargraphTestCoverage
4
4
  # Adapted from SimpleCov - Small class that turns branch coverage data into something easier to work with
5
5
  class Branch
6
- class << self
7
- #
8
- # Builds an array of Branch objects for every branch in results hash.
9
- #
10
- # @return [Array]
11
- #
12
- def build_from(results)
13
- results.fetch(:branches, {}).flat_map do |condition, branches|
14
- _condition_type, _condition_id, condition_start_line, * = condition
6
+ def self.build_from(results)
7
+ results.fetch(:branches, {}).flat_map do |condition, branches|
8
+ _condition_type, _condition_id, condition_start_line, * = condition
15
9
 
16
- branches.map do |branch_data, hit_count|
17
- type, _id, start_line, _start_col, end_line, _end_col = branch_data
10
+ branches.map do |branch_data, hit_count|
11
+ type, _id, start_line, _start_col, end_line, _end_col = branch_data
18
12
 
19
- new(start_line: start_line, end_line: end_line, coverage: hit_count,
20
- inline: start_line == condition_start_line, type: type)
21
- end
13
+ new(start_line: start_line, end_line: end_line, coverage: hit_count,
14
+ inline: start_line == condition_start_line, type: type)
22
15
  end
23
16
  end
24
17
  end
@@ -33,44 +26,18 @@ module SolargraphTestCoverage
33
26
  @type = type
34
27
  end
35
28
 
36
- #
37
- # Predicate method for @inline instance variable
38
- #
39
- # @return [Boolean]
40
- #
41
29
  def inline?
42
30
  @inline
43
31
  end
44
32
 
45
- #
46
- # Return true if there is relevant count defined > 0
47
- #
48
- # @return [Boolean]
49
- #
50
33
  def covered?
51
34
  coverage.positive?
52
35
  end
53
36
 
54
- #
55
- # The line on which we want to report the coverage
56
- #
57
- # Usually we choose the line above the start of the branch (so that it shows up
58
- # at if/else) because that
59
- # * highlights the condition
60
- # * makes it distinguishable if the first line of the branch is an inline branch
61
- # (see the nested_branches fixture)
62
- #
63
- # @return [Integer]
64
- #
65
37
  def report_line
66
38
  inline? ? start_line : start_line - 1
67
39
  end
68
40
 
69
- #
70
- # Return array with coverage count and badge
71
- #
72
- # @return [Hash]
73
- #
74
41
  def report
75
42
  { type: type, line: report_line }
76
43
  end
@@ -1,82 +1,134 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_literal = true
4
-
5
3
  module SolargraphTestCoverage
6
- class Config
7
- class << self
8
- DEFAULTS = {
9
- 'preload_rails' => true, # can be true or false - performance optimization
10
- 'test_framework' => 'rspec', # can be 'rspec' or 'minitest'
11
- 'coverage' => [ # All diagnostics are enabled by default
12
- 'line', # Specifying an array with fewer diagnostics will overwrite this
13
- 'branch',
14
- 'test_failing',
15
- 'test_missing'
16
- ],
17
- 'exclude_paths' => [ # don't attempt to find/run a spec for files that match these paths
18
- 'app/controller',
19
- 'concerns'
20
- ]
21
- }.freeze
22
-
23
- def preload_rails?
24
- plugin_config['preload_rails']
25
- end
4
+ module Config
5
+ extend self
26
6
 
27
- def exclude_paths
28
- plugin_config['exclude_paths']
29
- end
7
+ DEFAULTS = {
8
+ 'preload_rails' => true, # can be true or false - performance optimization
9
+ 'debug' => false, # can be true or false - shows debug messages when ChildFailedError is raised
10
+ 'test_framework' => 'rspec', # can be 'rspec' or 'minitest'
11
+ 'coverage' => [ # All diagnostics are enabled by default
12
+ 'line', # Specifying an array with fewer diagnostics will overwrite this
13
+ 'branch',
14
+ 'test_failing',
15
+ 'test_missing',
16
+ 'example_failing'
17
+ ],
18
+ 'exclude_paths' => [ # don't attempt to find/run a spec for files that match these paths
19
+ 'app/controller',
20
+ 'concerns'
21
+ ]
22
+ }.freeze
30
23
 
31
- def line_coverage?
32
- plugin_config['coverage'].include? 'line'
33
- end
24
+ def debug?
25
+ plugin_config['debug']
26
+ end
34
27
 
35
- def branch_coverage?
36
- plugin_config['coverage'].include? 'branch'
37
- end
28
+ def preload_rails?
29
+ plugin_config['preload_rails']
30
+ end
38
31
 
39
- def test_failing_coverage?
40
- plugin_config['coverage'].include? 'test_failing'
41
- end
32
+ def exclude_paths
33
+ plugin_config['exclude_paths']
34
+ end
42
35
 
43
- def test_missing_coverage?
44
- plugin_config['coverage'].include? 'test_missing'
45
- end
36
+ def line_coverage?
37
+ plugin_config['coverage'].include? 'line'
38
+ end
39
+
40
+ def branch_coverage?
41
+ plugin_config['coverage'].include? 'branch'
42
+ end
43
+
44
+ def test_failing_coverage?
45
+ plugin_config['coverage'].include? 'test_failing'
46
+ end
47
+
48
+ def test_missing_coverage?
49
+ plugin_config['coverage'].include? 'test_missing'
50
+ end
46
51
 
47
- def test_framework
48
- plugin_config['test_framework']
52
+ def example_failing_coverage?
53
+ plugin_config['coverage'].include? 'example_failing'
54
+ end
55
+
56
+ def test_framework
57
+ plugin_config['test_framework']
58
+ end
59
+
60
+ def test_dir
61
+ case test_framework
62
+ when 'rspec'
63
+ 'spec'
64
+ when 'minitest'
65
+ 'test'
49
66
  end
67
+ end
50
68
 
51
- def test_dir
52
- case test_framework
53
- when 'rspec'
54
- 'spec'
55
- when 'minitest'
56
- 'test'
57
- end
69
+ def test_file_suffix
70
+ case test_framework
71
+ when 'rspec'
72
+ '_spec.rb'
73
+ when 'minitest'
74
+ '_test.rb'
58
75
  end
76
+ end
59
77
 
60
- def test_file_suffix
61
- case test_framework
62
- when 'rspec'
63
- '_spec.rb'
64
- when 'minitest'
65
- '_test.rb'
66
- end
78
+ def require_testing_framework!
79
+ case test_framework
80
+ when 'rspec'
81
+ require 'rspec/core'
82
+ when 'minitest'
83
+ require 'minitest/autorun'
84
+ else
85
+ raise UnknownTestingGem
67
86
  end
87
+ end
68
88
 
69
- private
89
+ # This gives us a nice speed-boost when running test in child process
90
+ #
91
+ # We rescue the LoadError since Solargraph would catch it otherwise,
92
+ # and not load the plugin at all.
93
+ #
94
+ # Adding the spec/ directory to the $LOAD_PATH lets 'require "spec_helper"'
95
+ # commonly found in rails_helper work.
96
+ #
97
+ # If Coverage was started in Rails/Spec helper by SimpleCov,
98
+ # calling Coverage.result after requiring stops and resets it.
99
+ #
100
+ # This is a bit experimental
101
+ #
102
+ def preload_rails!
103
+ return if defined?(Rails) || !File.file?('spec/rails_helper.rb')
70
104
 
71
- def plugin_config
72
- @plugin_config ||= workspace_config.tap do |config|
73
- DEFAULTS.each_key { |key| config[key] = DEFAULTS[key] unless config.key?(key) }
74
- end
75
- end
105
+ $LOAD_PATH.unshift(test_path) unless $LOAD_PATH.include?(test_path)
106
+
107
+ require File.join(test_path, 'rails_helper')
108
+ Coverage.result(stop: true, clear: true) if Coverage.running?
76
109
 
77
- def workspace_config
78
- Solargraph::Workspace::Config.new(Dir.pwd).raw_data.fetch('test_coverage', {})
110
+ true
111
+ rescue LoadError => e
112
+ Solargraph::Logging.logger.warn "LoadError when trying to require 'rails_helper'"
113
+ Solargraph::Logging.logger.warn "[#{e.class}] #{e.message}"
114
+
115
+ false
116
+ end
117
+
118
+ private
119
+
120
+ def plugin_config
121
+ @plugin_config ||= workspace_config.tap do |config|
122
+ DEFAULTS.each_key { |key| config[key] = DEFAULTS[key] unless config.key?(key) }
79
123
  end
80
124
  end
125
+
126
+ def workspace_config
127
+ Solargraph::Workspace::Config.new(Dir.pwd).raw_data.fetch('test_coverage', {})
128
+ end
129
+
130
+ def test_path
131
+ FileHelpers.test_path
132
+ end
81
133
  end
82
134
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolargraphTestCoverage
4
+ module DiagnosticMessages
5
+ def line_coverage_warning(source, line)
6
+ return unless Config.line_coverage?
7
+
8
+ {
9
+ range: range(line, 0, line, source.code.lines[line].length),
10
+ severity: Solargraph::Diagnostics::Severities::WARNING,
11
+ source: 'TestCoverage',
12
+ message: 'Line is missing test coverage'
13
+ }
14
+ end
15
+
16
+ def branch_coverage_warning(source, report)
17
+ return unless Config.branch_coverage?
18
+
19
+ {
20
+ range: range(report[:line] - 1, 0, report[:line] - 1, source.code.lines[report[:line] - 1].length),
21
+ severity: Solargraph::Diagnostics::Severities::WARNING,
22
+ source: 'TestCoverage',
23
+ message: "'#{report[:type].upcase}' branch is missing test coverage"
24
+ }
25
+ end
26
+
27
+ def test_failing_error(source)
28
+ return unless Config.test_failing_coverage?
29
+
30
+ {
31
+ range: range(0, 0, 0, source.code.lines[0].length),
32
+ severity: Solargraph::Diagnostics::Severities::ERROR,
33
+ source: 'TestCoverage',
34
+ message: 'Unit Test is currently failing.'
35
+ }
36
+ end
37
+
38
+ def test_missing_error(source)
39
+ return unless Config.test_missing_coverage?
40
+
41
+ {
42
+ range: range(0, 0, 0, source.code.lines[0].length),
43
+ severity: Solargraph::Diagnostics::Severities::HINT,
44
+ source: 'TestCoverage',
45
+ message: "No test file found at '#{FileHelpers.test_file(source).sub("#{Dir.pwd}/", '')}'"
46
+ }
47
+ end
48
+
49
+ def example_failing_error(example, source)
50
+ return unless Config.example_failing_coverage?
51
+
52
+ {
53
+ range: range(example[:line_number], 0, example[:line_number], source.code.lines[example[:line_number]].length),
54
+ severity: Solargraph::Diagnostics::Severities::ERROR,
55
+ source: 'ExampleStatus',
56
+ message: example[:message]
57
+ }
58
+ end
59
+
60
+ def debug_message(exception, source)
61
+ {
62
+ range: range(0, 0, 0, source.code.lines[0].length),
63
+ severity: Solargraph::Diagnostics::Severities::ERROR,
64
+ source: 'SolargraphTestCoverage Plugin',
65
+ message: "DEBUG: (ChildFailedError) #{exception.message}"
66
+ }
67
+ end
68
+
69
+ private
70
+
71
+ def range(start_line, start_column, end_line, end_column)
72
+ Solargraph::Range.from_to(start_line, start_column, end_line, end_column).to_hash
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # example_status reporter for Solargraph
4
+ module SolargraphTestCoverage
5
+ class ExampleStatusReporter < Solargraph::Diagnostics::Base
6
+ include ReporterHelpers
7
+ include ReporterGuards
8
+ include DiagnosticMessages
9
+
10
+ def diagnose(source, _api_map)
11
+ if source.code.empty? || using_debugger?(source) || !is_test_file?(source) || is_test_support_file?(source)
12
+ return []
13
+ end
14
+
15
+ example_failing_errors(source, run_test(source, source.location.filename))
16
+ rescue ChildFailedError => e
17
+ Config.debug? ? [debug_message(e, source)] : []
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolargraphTestCoverage
4
+ module FileHelpers
5
+ extend self
6
+
7
+ def test_file(source)
8
+ relative_filepath = source.location.filename.sub(Dir.pwd, '').split('/').reject(&:empty?)
9
+
10
+ if relative_filepath.first == Config.test_dir && relative_filepath.last.end_with?(Config.test_file_suffix)
11
+ return source.location.filename
12
+ end
13
+
14
+ relative_filepath[0] = Config.test_dir
15
+ File.join(Dir.pwd, relative_filepath.join('/')).sub('.rb', Config.test_file_suffix)
16
+ end
17
+
18
+ def test_path
19
+ File.join(Dir.pwd, Config.test_dir)
20
+ end
21
+ end
22
+ end
@@ -5,28 +5,39 @@ module SolargraphTestCoverage
5
5
  # When called with a block, runs the content of said block in a new (forked) process
6
6
  # the return value of the process/block can be captured and used in parent process
7
7
  class ForkProcess
8
- #
9
- # Executes block in forked process, and captures returned value of that block
10
- # Returns result of block
11
- #
12
- # @return [Object]
13
- #
14
- def self.run
15
- read, write = IO.pipe
8
+ def self.call(&block)
9
+ new.run(&block)
10
+ end
11
+
12
+ def initialize
13
+ @read, @write = IO.pipe
14
+ end
16
15
 
16
+ def run(&block)
17
17
  pid = fork do
18
- read.close
19
- result = yield
20
- Marshal.dump(result, write)
18
+ @read.close
19
+ Marshal.dump(run_block_with_timeout(&block), @write)
21
20
  exit!(0) # skips exit handlers.
22
21
  end
23
22
 
24
- write.close
25
- result = read.read
23
+ @write.close
24
+ result = @read.read
25
+
26
26
  Process.wait(pid)
27
- raise ChildFailedError if result.nil? || result.empty?
27
+ raise ChildFailedError, "Couldn't read pipe" if result.nil?
28
+
29
+ Marshal.load(result).tap do |r|
30
+ raise ChildFailedError, "Marshal.load(result) returned nil" if r.nil?
31
+ raise ChildFailedError, r.message if r.is_a? Exception
32
+ end
33
+ end
34
+
35
+ private
28
36
 
29
- Marshal.load(result)
37
+ def run_block_with_timeout(&block)
38
+ Timeout.timeout(30000, &block)
39
+ rescue StandardError => e
40
+ e
30
41
  end
31
42
  end
32
43
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolargraphTestCoverage
4
+ # Some guard functions for the diagnostics
5
+ module ReporterGuards
6
+ def has_test_file?(source)
7
+ File.file? FileHelpers.test_file(source)
8
+ end
9
+
10
+ def is_test_file?(source)
11
+ source.location.filename.start_with? FileHelpers.test_path
12
+ end
13
+
14
+ def is_test_support_file?(source)
15
+ is_test_file?(source) && !source.location.filename.end_with?(Config.test_file_suffix)
16
+ end
17
+
18
+ def exclude_file?(source)
19
+ Config.exclude_paths.any? { |path| source.location.filename.sub(Dir.pwd, '').include? path }
20
+ end
21
+
22
+ def using_debugger?(source)
23
+ source.code.include?('binding.pry') ||
24
+ source.code.include?('byebug') ||
25
+ source.code.include?('debugger')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolargraphTestCoverage
4
+ # Some helper functions for the diagnostics
5
+ module ReporterHelpers
6
+ # @return [Hash]
7
+ def run_test(source, test_file)
8
+ ForkProcess.call do
9
+ Coverage.start(lines: true, branches: true)
10
+ runner = TestRunner.with(test_file).run!
11
+
12
+ Coverage.result
13
+ .fetch(source.location.filename, {})
14
+ .merge({ test_status: runner.passed?, failed_examples: runner.failed_examples })
15
+ end
16
+ end
17
+
18
+ def line_warnings(source, results)
19
+ uncovered_lines(results).map { |line| line_coverage_warning(source, line) }
20
+ end
21
+
22
+ def branch_warnings(source, results)
23
+ uncovered_branches(results).map { |branch| branch_coverage_warning(source, branch.report) }
24
+ end
25
+
26
+ def test_passing_error(source, results)
27
+ results[:test_status] ? [] : [test_failing_error(source)]
28
+ end
29
+
30
+ def example_failing_errors(source, results)
31
+ results.fetch(:failed_examples, [])
32
+ .map { |example| example_failing_error(example, source) }
33
+ end
34
+
35
+ # Adapted from SingleCov
36
+ # Coverage returns nil for untestable lines (like 'do', 'end', 'if' keywords)
37
+ # otherwise returns int showing how many times a line was called
38
+ #
39
+ # [nil, 1, 0, 1, 0] -> [3, 5]
40
+ # Returns array of line numbers with 0 coverage
41
+ def uncovered_lines(results)
42
+ return [] unless results[:lines]
43
+
44
+ results[:lines].each_with_index
45
+ .select { |c, _| c&.zero? }
46
+ .map { |_, i| i }
47
+ .compact
48
+ end
49
+
50
+ def uncovered_branches(results)
51
+ Branch.build_from(results).reject(&:covered?)
52
+ end
53
+ end
54
+ end
@@ -3,115 +3,23 @@
3
3
  # test_coverage reporter for Solargraph
4
4
  module SolargraphTestCoverage
5
5
  class TestCoverageReporter < Solargraph::Diagnostics::Base
6
- include Helpers
6
+ include ReporterHelpers
7
+ include ReporterGuards
8
+ include DiagnosticMessages
7
9
 
8
- # LSP Diagnostic method
9
- # @return [Array]
10
- #
11
10
  def diagnose(source, _api_map)
12
- return [] if source.code.empty? || exclude_file?(source.location.filename)
13
- return [test_missing_error(source)] unless File.file?(test_file(source))
11
+ return [] if source.code.empty? || using_debugger?(source) || exclude_file?(source) || is_test_file?(source)
12
+ return [test_missing_error(source)] unless has_test_file?(source)
14
13
 
15
- results = run_test(source)
16
- messages(source, results)
17
- rescue ChildFailedError
18
- []
19
- end
20
-
21
- private
14
+ results = run_test(source, FileHelpers.test_file(source))
22
15
 
23
- # Compiles all diagnostic messages for source file
24
- # @return [Array]
25
- #
26
- def messages(source, results)
27
- messages = [
16
+ [
28
17
  line_warnings(source, results),
29
18
  branch_warnings(source, results),
30
19
  test_passing_error(source, results)
31
- ]
32
-
33
- messages.flatten.compact
34
- end
35
-
36
- # Creates array of warnings for uncovered lines
37
- # @return [Array]
38
- #
39
- def line_warnings(source, results)
40
- uncovered_lines(results).map { |line| line_coverage_warning(source, line) }
41
- end
42
-
43
- # Creates array of warnings for uncovered branches
44
- # @return [Array]
45
- #
46
- def branch_warnings(source, results)
47
- uncovered_branches(results).map { |branch| branch_coverage_warning(source, branch.report) }
48
- end
49
-
50
- # Creates array containing error for failing spec
51
- # @return [Array]
52
- #
53
- def test_passing_error(source, results)
54
- results[:test_status] ? [] : [test_failing_error(source)]
55
- end
56
-
57
- # Creates LSP warning message for missing line coverage
58
- # @return [Hash]
59
- #
60
- def line_coverage_warning(source, line)
61
- return unless Config.line_coverage?
62
-
63
- {
64
- range: range(line, 0, line, source.code.lines[line].length),
65
- severity: Solargraph::Diagnostics::Severities::WARNING,
66
- source: 'TestCoverage',
67
- message: 'Line is missing test coverage'
68
- }
69
- end
70
-
71
- # Creates LSP warning message for missing branch coverage
72
- # Line numbers are off by 1, since Branch Coverage starts counting at 1, not 0
73
- #
74
- # @return [Hash]
75
- #
76
- def branch_coverage_warning(source, report)
77
- return unless Config.branch_coverage?
78
-
79
- {
80
- range: range(report[:line] - 1, 0, report[:line] - 1, source.code.lines[report[:line] - 1].length),
81
- severity: Solargraph::Diagnostics::Severities::WARNING,
82
- source: 'TestCoverage',
83
- message: "'#{report[:type].upcase}' branch is missing test coverage"
84
- }
85
- end
86
-
87
- # Creates LSP error message if test file is failing
88
- # @return [Hash]
89
- #
90
- def test_failing_error(source)
91
- return unless Config.test_failing_coverage?
92
-
93
- {
94
- range: range(0, 0, 0, source.code.lines[0].length),
95
- severity: Solargraph::Diagnostics::Severities::ERROR,
96
- source: 'TestCoverage',
97
- message: 'Unit Test is currently failing.'
98
- }
99
- end
100
-
101
- #
102
- # Creates LSP error message if no test file can be found
103
- #
104
- # @return [Hash]
105
- #
106
- def test_missing_error(source)
107
- return unless Config.test_missing_coverage?
108
-
109
- {
110
- range: range(0, 0, 0, source.code.lines[0].length),
111
- severity: Solargraph::Diagnostics::Severities::ERROR,
112
- source: 'TestCoverage',
113
- message: "No unit test file found at #{test_file(source)}"
114
- }
20
+ ].flatten.compact
21
+ rescue ChildFailedError => e
22
+ Config.debug? ? [debug_message(e, source)] : []
115
23
  end
116
24
  end
117
25
  end
@@ -15,14 +15,15 @@ module SolargraphTestCoverage
15
15
  def initialize(test_file)
16
16
  @test_file = test_file
17
17
  @result = nil
18
+ @output = StringIO.new
18
19
  end
19
20
 
20
21
  def run!
21
- @result = test_framework_runner.run(test_options)
22
+ @result = test_framework_runner.run(test_options, $stderr, @output)
22
23
  self
23
24
  end
24
25
 
25
- def test_options
26
+ def failed_examples
26
27
  raise NotImplementedError
27
28
  end
28
29
 
@@ -30,21 +31,43 @@ module SolargraphTestCoverage
30
31
  raise NotImplementedError
31
32
  end
32
33
 
34
+ private
35
+
36
+ def test_options
37
+ raise NotImplementedError
38
+ end
39
+
33
40
  def test_framework_runner
34
41
  raise NotImplementedError
35
42
  end
43
+
44
+ def output
45
+ return if @output.string.empty?
46
+
47
+ JSON.parse @output.string
48
+ end
36
49
  end
37
50
 
38
51
  # Test Runner Subclass for RSpec
39
52
  class RSpecRunner < TestRunner
40
- def test_options
41
- [@test_file, '-o', '/dev/null']
53
+ def failed_examples
54
+ return unless output
55
+
56
+ output['examples']
57
+ .select { |example| example['status'] == 'failed' }
58
+ .map { |example| { line_number: example['line_number'] - 1, message: example.dig('exception', 'message') } }
42
59
  end
43
60
 
44
61
  def passed?
45
62
  @result&.zero?
46
63
  end
47
64
 
65
+ private
66
+
67
+ def test_options
68
+ [@test_file, '--format', 'json']
69
+ end
70
+
48
71
  def test_framework_runner
49
72
  RSpec::Core::Runner
50
73
  end
@@ -52,14 +75,21 @@ module SolargraphTestCoverage
52
75
 
53
76
  # Test Runner Subclass for Minitest
54
77
  class MinitestRunner < TestRunner
55
- def test_options
56
- [@test_file]
78
+ # TODO
79
+ def failed_examples
80
+ []
57
81
  end
58
82
 
59
83
  def passed?
60
84
  @result
61
85
  end
62
86
 
87
+ private
88
+
89
+ def test_options
90
+ [@test_file]
91
+ end
92
+
63
93
  def test_framework_runner
64
94
  Minitest
65
95
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolargraphTestCoverage
4
- VERSION = '0.2.4'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -3,21 +3,27 @@
3
3
  require 'solargraph_test_coverage/version'
4
4
  require 'solargraph_test_coverage/branch'
5
5
  require 'solargraph_test_coverage/fork_process'
6
- require 'solargraph_test_coverage/helpers'
6
+ require 'solargraph_test_coverage/reporter_helpers'
7
+ require 'solargraph_test_coverage/reporter_guards'
8
+ require 'solargraph_test_coverage/file_helpers'
7
9
  require 'solargraph_test_coverage/config'
8
10
  require 'solargraph_test_coverage/test_runner'
11
+ require 'solargraph_test_coverage/diagnostic_messages'
9
12
  require 'solargraph_test_coverage/test_coverage_reporter'
13
+ require 'solargraph_test_coverage/example_status_reporter'
10
14
 
11
15
  require 'solargraph'
12
16
  require 'coverage'
17
+ require 'timeout'
13
18
 
14
19
  module SolargraphTestCoverage
15
20
  class ChildFailedError < StandardError; end
16
21
 
17
22
  class UnknownTestingGem < StandardError; end
18
23
 
19
- Helpers.require_testing_framework!
20
- Helpers.preload_rails! if Config.preload_rails?
24
+ Config.require_testing_framework!
25
+ Config.preload_rails! if Config.preload_rails?
21
26
 
22
27
  Solargraph::Diagnostics.register 'test_coverage', TestCoverageReporter
28
+ Solargraph::Diagnostics.register 'example_status', ExampleStatusReporter
23
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solargraph_test_coverage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Kolkey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-03 00:00:00.000000000 Z
11
+ date: 2021-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: solargraph
@@ -47,8 +47,12 @@ files:
47
47
  - lib/solargraph_test_coverage.rb
48
48
  - lib/solargraph_test_coverage/branch.rb
49
49
  - lib/solargraph_test_coverage/config.rb
50
+ - lib/solargraph_test_coverage/diagnostic_messages.rb
51
+ - lib/solargraph_test_coverage/example_status_reporter.rb
52
+ - lib/solargraph_test_coverage/file_helpers.rb
50
53
  - lib/solargraph_test_coverage/fork_process.rb
51
- - lib/solargraph_test_coverage/helpers.rb
54
+ - lib/solargraph_test_coverage/reporter_guards.rb
55
+ - lib/solargraph_test_coverage/reporter_helpers.rb
52
56
  - lib/solargraph_test_coverage/test_coverage_reporter.rb
53
57
  - lib/solargraph_test_coverage/test_runner.rb
54
58
  - lib/solargraph_test_coverage/version.rb
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolargraphTestCoverage
4
- # Some helper functions for the diagnostics
5
- module Helpers
6
- # @return [Boolean]
7
- def exclude_file?(source_filename)
8
- return true if source_filename.start_with? Helpers.test_path
9
-
10
- Config.exclude_paths.each { |path| return true if source_filename.sub(Dir.pwd, '').include? path }
11
-
12
- false
13
- end
14
-
15
- # @return [String]
16
- def test_file(source)
17
- relative_filepath = source.location.filename.sub(Dir.pwd, '').split('/').reject(&:empty?)
18
- relative_filepath[0] = Config.test_dir
19
-
20
- File.join(Dir.pwd, relative_filepath.join('/'))
21
- .sub('.rb', Config.test_file_suffix)
22
- end
23
-
24
- # @return [Hash]
25
- def run_test(source)
26
- ForkProcess.run do
27
- Coverage.start(lines: true, branches: true)
28
- runner = TestRunner.with(test_file(source)).run!
29
- Coverage.result.fetch(source.location.filename, {}).merge({ test_status: runner.passed? })
30
- end
31
- end
32
-
33
- # Adapted from SingleCov
34
- # Coverage returns nil for untestable lines (like do, end, if keywords)
35
- # otherwise returns int showing how many times a line was called
36
- #
37
- # [nil, 1, 0, 1, 0] -> [3, 5]
38
- #
39
- # @return [Array]
40
- #
41
- def uncovered_lines(results)
42
- return [] unless results[:lines]
43
-
44
- results[:lines].each_with_index
45
- .select { |c, _| c&.zero? }
46
- .map { |_, i| i }
47
- .compact
48
- end
49
-
50
- # @return [Array]
51
- def uncovered_branches(results)
52
- Branch.build_from(results).reject(&:covered?)
53
- end
54
-
55
- # @return [Hash]
56
- def range(start_line, start_column, end_line, end_column)
57
- Solargraph::Range.from_to(start_line, start_column, end_line, end_column).to_hash
58
- end
59
-
60
- def self.require_testing_framework!
61
- case Config.test_framework
62
- when 'rspec'
63
- require 'rspec/core'
64
- when 'minitest'
65
- require 'minitest/autorun'
66
- else
67
- raise UnknownTestingGem
68
- end
69
- end
70
-
71
- # Only called once, when gem is loaded
72
- # Preloads rails via spec/rails_helper if Rails isn't already defined
73
- # This gives us a nice speed-boost when running test in child process
74
- #
75
- # We rescue the LoadError since Solargraph would catch it otherwise,
76
- # and not load the plugin at all.
77
- #
78
- # Adding the spec/ directory to the $LOAD_PATH lets 'require "spec_helper"'
79
- # commonly found in rails_helper work.
80
- #
81
- # If Coverage was started in Rails/Spec helper by SimpleCov,
82
- # calling Coverage.result after requiring stops and resets it.
83
- #
84
- # This is a bit experimental - I'm not sure if there will be downstream side-effects
85
- #
86
- # @return [Boolean]
87
- #
88
- def self.preload_rails!
89
- return if defined?(Rails) || !File.file?('spec/rails_helper.rb')
90
-
91
- $LOAD_PATH.unshift(test_path) unless $LOAD_PATH.include?(test_path)
92
-
93
- require File.join(test_path, 'rails_helper')
94
- Coverage.result(stop: true, clear: true) if Coverage.running?
95
-
96
- true
97
- rescue LoadError => e
98
- Solargraph::Logging.logger.warn "LoadError when trying to require 'rails_helper'"
99
- Solargraph::Logging.logger.warn "[#{e.class}] #{e.message}"
100
-
101
- false
102
- end
103
-
104
- # @return [String]
105
- def self.test_path
106
- File.join(Dir.pwd, Config.test_dir)
107
- end
108
- end
109
- end