single_cov 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fc4bd21262f4b962189d8bb21f2032e5f32c6b42
4
+ data.tar.gz: 96832f5c76759152715afc37a7250037e054e7ae
5
+ SHA512:
6
+ metadata.gz: 949aa6f89ff4d1cfe868fdee4b778f686971b1bca13092b5c099abdf6b282fa7465bb97a00f56daba40082a62348d48c693be3fed117b4ce582991228421b252
7
+ data.tar.gz: 6971f210c32e0e54e13bf8e3adc9980056d66ef3368cec0b1894620691197f32834681094a46b557615f5cb71327e77604ce20882dbef647d24ae1bff7dece5c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2013 Michael Grosser <michael@grosser.it>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ module SingleCov
2
+ VERSION = "0.1.0"
3
+ end
data/lib/single_cov.rb ADDED
@@ -0,0 +1,212 @@
1
+ module SingleCov
2
+ COVERAGES = []
3
+ MAX_OUTPUT = 40
4
+
5
+ class << self
6
+ def not_covered!
7
+ end
8
+
9
+ def covered!(file: nil, uncovered: 0)
10
+ file = guess_and_check_covered_file(file)
11
+ COVERAGES << [file, uncovered]
12
+ end
13
+
14
+ def all_covered?(result)
15
+ errors = COVERAGES.map do |file, expected_uncovered|
16
+ if coverage = result[File.expand_path(file)]
17
+ uncovered_lines = coverage.each_with_index.map { |c, i| "#{file}:#{i+1}" if c == 0 }.compact
18
+ next if uncovered_lines.size == expected_uncovered
19
+ warn_about_bad_coverage(file, expected_uncovered, uncovered_lines)
20
+ else
21
+ warn_about_no_coverage(file)
22
+ end
23
+ end.compact
24
+
25
+ return true if errors.empty?
26
+
27
+ errors = errors.join("\n").split("\n") # unify arrays with multiline strings
28
+ errors[MAX_OUTPUT..-1] = "... coverage output truncated" if errors.size >= MAX_OUTPUT
29
+ warn errors
30
+
31
+ errors.all? { |l| l.end_with?('?') } # ok if we just have warnings
32
+ end
33
+
34
+ def assert_used(tests: default_tests)
35
+ bad = tests.select do |file|
36
+ File.read(file) !~ /SingleCov.(not_)?covered\!/
37
+ end
38
+ unless bad.empty?
39
+ raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
40
+ end
41
+ end
42
+
43
+ def assert_tested(files: Dir['{app,lib}/**/*.rb'], tests: default_tests, untested: [])
44
+ missing = files - tests.map { |t| file_under_test(t) }
45
+ fixed = untested - missing
46
+ missing -= untested
47
+
48
+ if fixed.any?
49
+ raise "Remove #{fixed.inspect} from untested!"
50
+ elsif missing.any?
51
+ raise missing.map { |f| "missing test for #{f}" }.join("\n")
52
+ end
53
+ end
54
+
55
+ def setup(framework)
56
+ if defined?(SimpleCov)
57
+ raise "Load SimpleCov after SingleCov"
58
+ end
59
+
60
+ case framework
61
+ when :minitest
62
+ minitest_should_not_be_running!
63
+ return if minitest_running_subset_of_tests?
64
+ start_coverage_recording
65
+
66
+ override_at_exit do |status, _exception|
67
+ exit 1 if status == 0 && !SingleCov.all_covered?(coverage_results)
68
+ end
69
+ else
70
+ raise "Unsupported framework #{framework.inspect}"
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def default_tests
77
+ Dir["{test,spec}/**/*_{test,spec}.rb"]
78
+ end
79
+
80
+ # do not ask for coverage when SimpleCov already does or it conflicts
81
+ def coverage_results
82
+ if defined?(SimpleCov)
83
+ SimpleCov.instance_variable_get(:@result).original_result
84
+ else
85
+ Coverage.result
86
+ end
87
+ end
88
+
89
+ # start recording before classes are loaded or nothing can be recorded
90
+ # SimpleCov might start coverage again, but that does not hurt ...
91
+ def start_coverage_recording
92
+ require 'coverage'
93
+ Coverage.start
94
+ end
95
+
96
+ # not running rake or a whole folder
97
+ # TODO make this better ...
98
+ def running_single_file?
99
+ !defined?(Rake)
100
+ end
101
+
102
+ # we cannot insert our hooks when minitest is already running
103
+ def minitest_should_not_be_running!
104
+ if defined?(Minitest) && Minitest.class_variable_get(:@@installed_at_exit)
105
+ raise "Load minitest after setting up SingleCov"
106
+ end
107
+ end
108
+
109
+ # do not record or verify when only running selected tests since it would be missing data
110
+ def minitest_running_subset_of_tests?
111
+ (ARGV & ['-n', '--name', '-l', '--line']).any?
112
+ end
113
+
114
+ # code stolen from SimpleCov
115
+ def override_at_exit
116
+ at_exit do
117
+ exit_status = if $! # was an exception thrown?
118
+ # if it was a SystemExit, use the accompanying status
119
+ # otherwise set a non-zero status representing termination by
120
+ # some other exception (see github issue 41)
121
+ $!.is_a?(SystemExit) ? $!.status : 1
122
+ else
123
+ # Store the exit status of the test run since it goes away
124
+ # after calling the at_exit proc...
125
+ 0
126
+ end
127
+
128
+ yield exit_status, $!
129
+
130
+ # Force exit with stored status (see github issue #5)
131
+ # unless it's nil or 0 (see github issue #281)
132
+ Kernel.exit exit_status if exit_status && exit_status > 0
133
+ end
134
+ end
135
+
136
+ def guess_and_check_covered_file(file)
137
+ if file && file.start_with?("/")
138
+ raise "Use paths relative to rails root."
139
+ end
140
+
141
+ if file
142
+ raise "#{file} does not exist and cannot be covered." unless File.exist?(file)
143
+ else
144
+ file = file_under_test(caller[1])
145
+ unless File.exist?(file)
146
+ raise "Tried to guess covered file as #{file}, but it does not exist.\nUse `SingleCov.covered file: 'target_file.rb'` to set covered file location."
147
+ end
148
+ end
149
+
150
+ file
151
+ end
152
+
153
+ def warn_about_bad_coverage(file, expected_uncovered, uncovered_lines)
154
+ details = "(#{uncovered_lines.size} current vs #{expected_uncovered} configured)"
155
+ if expected_uncovered > uncovered_lines.size
156
+ if running_single_file?
157
+ "#{file} has less uncovered lines #{details}, decrement configured uncovered?"
158
+ end
159
+ else
160
+ [
161
+ "#{file} new uncovered lines introduced #{details}",
162
+ "Lines missing coverage:",
163
+ *uncovered_lines
164
+ ].join("\n")
165
+ end
166
+ end
167
+
168
+ def warn_about_no_coverage(file)
169
+ if $LOADED_FEATURES.include?(File.expand_path(file))
170
+ # we cannot enforce $LOADED_FEATURES during covered! since it would fail when multiple files are loaded
171
+ "#{file} was expected to be covered, but already loaded before tests started."
172
+ else
173
+ "#{file} was expected to be covered, but never loaded."
174
+ end
175
+ end
176
+
177
+ def file_under_test(file)
178
+ file = file.dup
179
+
180
+ # remove project root
181
+ file.sub!("#{Bundler.root}/", '')
182
+
183
+ # remove caller junk to get nice error messages when something fails
184
+ file.sub!(/\.rb\b.*/, '.rb')
185
+
186
+ # preserve subfolders like foobar/test/xxx_test.rb -> foobar/lib/xxx_test.rb
187
+ subfolder, file_part = file.split(%r{(?:^|/)(?:test|spec)/}, 2)
188
+ unless file_part
189
+ raise "#{file} includes neither 'test' nor 'spec' folder ... unable to resolve"
190
+ end
191
+
192
+ # rails things live in app
193
+ file_part[0...0] = if file_part =~ /^(?:models|serializers|helpers|controllers|mailers|views)\//
194
+ "app/"
195
+ elsif file_part.start_with?("lib/") # don't add lib twice
196
+ ""
197
+ else # everything else lives in lib
198
+ "lib/"
199
+ end
200
+
201
+ # remove test extension
202
+ unless file_part.sub!(/_(?:test|spec)\.rb\b.*/, '.rb')
203
+ raise "Unable to remove test extension from #{file} ... _test.rb and _spec.rb are supported"
204
+ end
205
+
206
+ # put back the subfolder
207
+ file_part[0...0] = "#{subfolder}/" unless subfolder.empty?
208
+
209
+ file_part
210
+ end
211
+ end
212
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: single_cov
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-17 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: michael@grosser.it
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - MIT-LICENSE
20
+ - lib/single_cov.rb
21
+ - lib/single_cov/version.rb
22
+ homepage: https://github.com/grosser/single_cov
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 2.4.5.1
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: Actionable code coverage.
46
+ test_files: []