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 +4 -4
- data/Gemfile.lock +5 -5
- data/README.md +9 -6
- data/lib/solargraph_test_coverage/branch.rb +7 -40
- data/lib/solargraph_test_coverage/config.rb +113 -61
- data/lib/solargraph_test_coverage/diagnostic_messages.rb +75 -0
- data/lib/solargraph_test_coverage/example_status_reporter.rb +20 -0
- data/lib/solargraph_test_coverage/file_helpers.rb +22 -0
- data/lib/solargraph_test_coverage/fork_process.rb +26 -15
- data/lib/solargraph_test_coverage/reporter_guards.rb +28 -0
- data/lib/solargraph_test_coverage/reporter_helpers.rb +54 -0
- data/lib/solargraph_test_coverage/test_coverage_reporter.rb +10 -102
- data/lib/solargraph_test_coverage/test_runner.rb +36 -6
- data/lib/solargraph_test_coverage/version.rb +1 -1
- data/lib/solargraph_test_coverage.rb +9 -3
- metadata +7 -3
- data/lib/solargraph_test_coverage/helpers.rb +0 -109
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5aba663e3f7dca496c3c527adc1be8024353932ef806cf6f9eed9c70bd44f120
|
4
|
+
data.tar.gz: d4a031ffbb4e97f2d13eff2552760d3beae56413b1666e6e84edd1c182919c1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
- line
|
37
|
+
- branch
|
38
|
+
- test_failing
|
39
|
+
- test_missing
|
40
40
|
exclude_paths:
|
41
|
-
|
42
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
17
|
-
|
10
|
+
branches.map do |branch_data, hit_count|
|
11
|
+
type, _id, start_line, _start_col, end_line, _end_col = branch_data
|
18
12
|
|
19
|
-
|
20
|
-
|
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
|
-
|
7
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
24
|
+
def debug?
|
25
|
+
plugin_config['debug']
|
26
|
+
end
|
34
27
|
|
35
|
-
|
36
|
-
|
37
|
-
|
28
|
+
def preload_rails?
|
29
|
+
plugin_config['preload_rails']
|
30
|
+
end
|
38
31
|
|
39
|
-
|
40
|
-
|
41
|
-
|
32
|
+
def exclude_paths
|
33
|
+
plugin_config['exclude_paths']
|
34
|
+
end
|
42
35
|
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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?
|
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
|
-
|
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
|
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
|
13
|
-
return [test_missing_error(source)] unless
|
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
|
-
|
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
|
-
|
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
|
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
|
41
|
-
|
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
|
-
|
56
|
-
|
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
|
@@ -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/
|
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
|
-
|
20
|
-
|
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.
|
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-
|
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/
|
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
|