ashtonw-slather 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +45 -0
  4. data/.travis.yml +19 -0
  5. data/CHANGELOG.md +132 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +206 -0
  9. data/Rakefile +1 -0
  10. data/assets/highlight.pack.js +1 -0
  11. data/assets/list.min.js +1 -0
  12. data/assets/slather.css +316 -0
  13. data/bin/slather +117 -0
  14. data/docs/logo.jpg +0 -0
  15. data/lib/cocoapods_plugin.rb +10 -0
  16. data/lib/slather.rb +20 -0
  17. data/lib/slather/coverage_file.rb +195 -0
  18. data/lib/slather/coverage_service/cobertura_xml_output.rb +183 -0
  19. data/lib/slather/coverage_service/coveralls.rb +186 -0
  20. data/lib/slather/coverage_service/gutter_json_output.rb +50 -0
  21. data/lib/slather/coverage_service/hardcover.rb +61 -0
  22. data/lib/slather/coverage_service/html_output.rb +244 -0
  23. data/lib/slather/coverage_service/simple_output.rb +31 -0
  24. data/lib/slather/coveralls_coverage_file.rb +13 -0
  25. data/lib/slather/project.rb +132 -0
  26. data/lib/slather/version.rb +3 -0
  27. data/slather.gemspec +32 -0
  28. data/spec/fixtures/fixtures.xcodeproj/project.pbxproj +496 -0
  29. data/spec/fixtures/fixtures.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  30. data/spec/fixtures/fixtures.xcodeproj/xcshareddata/xcschemes/fixtures.xcscheme +69 -0
  31. data/spec/fixtures/fixtures/Supporting Files/fixtures-Prefix.pch +9 -0
  32. data/spec/fixtures/fixtures/fixtures.h +16 -0
  33. data/spec/fixtures/fixtures/fixtures.m +23 -0
  34. data/spec/fixtures/fixtures/fixtures_cpp.cpp +9 -0
  35. data/spec/fixtures/fixtures/fixtures_cpp.h +6 -0
  36. data/spec/fixtures/fixtures/fixtures_m.h +5 -0
  37. data/spec/fixtures/fixtures/fixtures_m.m +5 -0
  38. data/spec/fixtures/fixtures/fixtures_mm.h +5 -0
  39. data/spec/fixtures/fixtures/fixtures_mm.mm +5 -0
  40. data/spec/fixtures/fixtures/more_files/Branches.h +15 -0
  41. data/spec/fixtures/fixtures/more_files/Branches.m +45 -0
  42. data/spec/fixtures/fixtures/more_files/Empty.h +13 -0
  43. data/spec/fixtures/fixtures/more_files/Empty.m +13 -0
  44. data/spec/fixtures/fixtures/more_files/peekaview.h +13 -0
  45. data/spec/fixtures/fixtures/more_files/peekaview.m +31 -0
  46. data/spec/fixtures/fixturesTests/BranchesTests.m +38 -0
  47. data/spec/fixtures/fixturesTests/Supporting Files/en.lproj/InfoPlist.strings +2 -0
  48. data/spec/fixtures/fixturesTests/Supporting Files/fixturesTests-Info.plist +22 -0
  49. data/spec/fixtures/fixturesTests/fixturesTests.m +36 -0
  50. data/spec/fixtures/fixturesTests/peekaviewTests.m +34 -0
  51. data/spec/fixtures/fixtures_html/Branches.m.html +261 -0
  52. data/spec/fixtures/fixtures_html/BranchesTests.m.html +228 -0
  53. data/spec/fixtures/fixtures_html/Empty.m.html +30 -0
  54. data/spec/fixtures/fixtures_html/fixtures.m.html +151 -0
  55. data/spec/fixtures/fixtures_html/fixturesTests.m.html +216 -0
  56. data/spec/fixtures/fixtures_html/fixtures_cpp.cpp.html +30 -0
  57. data/spec/fixtures/fixtures_html/fixtures_m.m.html +30 -0
  58. data/spec/fixtures/fixtures_html/fixtures_mm.mm.html +30 -0
  59. data/spec/fixtures/fixtures_html/index.html +134 -0
  60. data/spec/fixtures/fixtures_html/peekaview.m.html +190 -0
  61. data/spec/fixtures/fixtures_html/peekaviewTests.m.html +206 -0
  62. data/spec/fixtures/gutter.json +1 -0
  63. data/spec/slather/cocoapods_plugin_spec.rb +21 -0
  64. data/spec/slather/coverage_file_spec.rb +337 -0
  65. data/spec/slather/coverage_service/cobertura_xml_spec.rb +49 -0
  66. data/spec/slather/coverage_service/coveralls_spec.rb +122 -0
  67. data/spec/slather/coverage_service/gutter_json_spec.rb +28 -0
  68. data/spec/slather/coverage_service/hardcover_spec.rb +87 -0
  69. data/spec/slather/coverage_service/html_output_spec.rb +179 -0
  70. data/spec/slather/coverage_service/simple_output_spec.rb +35 -0
  71. data/spec/slather/fixtures.gcno +0 -0
  72. data/spec/slather/project_spec.rb +288 -0
  73. data/spec/spec_helper.rb +27 -0
  74. metadata +319 -0
@@ -0,0 +1,50 @@
1
+ module Slather
2
+ module CoverageService
3
+ module GutterJsonOutput
4
+
5
+ def coverage_file_class
6
+ Slather::CoverageFile
7
+ end
8
+ private :coverage_file_class
9
+
10
+ def post
11
+ output = { 'meta' => { 'timestamp' => DateTime.now.strftime('%Y-%m-%d %H:%M:%S.%6N') } }
12
+ symbols = {}
13
+
14
+ coverage_files.each do |coverage_file|
15
+ next unless coverage_file.gcov_data
16
+
17
+ filename = coverage_file.source_file_pathname.to_s
18
+ filename = filename.sub(Pathname.pwd.to_s, '').reverse.chomp("/").reverse
19
+
20
+ coverage_file.cleaned_gcov_data.split("\n").each do |line|
21
+ data = line.split(':')
22
+
23
+ line_number = data[1].to_i
24
+ next unless line_number > 0
25
+
26
+ coverage = data[0].strip
27
+
28
+ symbol = { 'line' => line_number,
29
+ 'long_text' => '',
30
+ 'short_text' => coverage }
31
+
32
+ if coverage != '-'
33
+ symbol['background_color'] = coverage.to_i > 0 ? '0x35CC4B' : '0xFC635E'
34
+ end
35
+
36
+ if symbols.has_key?(filename)
37
+ symbols[filename] << symbol
38
+ else
39
+ symbols[filename] = [ symbol ]
40
+ end
41
+ end
42
+ end
43
+
44
+ output['symbols_by_file'] = symbols
45
+ File.open('.gutter.json', 'w') { |file| file.write(output.to_json) }
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,61 @@
1
+ module Slather
2
+ module CoverageService
3
+ module Hardcover
4
+
5
+ def coverage_file_class
6
+ Slather::CoverallsCoverageFile
7
+ end
8
+ private :coverage_file_class
9
+
10
+ def jenkins_job_id
11
+ "#{ENV['JOB_NAME']}/#{ENV['BUILD_NUMBER']}"
12
+ end
13
+ private :jenkins_job_id
14
+
15
+ def hardcover_coverage_data
16
+ if ci_service == :jenkins_ci
17
+ if jenkins_job_id
18
+ {
19
+ :service_job_id => jenkins_job_id,
20
+ :service_name => "jenkins-ci",
21
+ :repo_token => Project.yml["hardcover_repo_token"],
22
+ :source_files => coverage_files.map(&:as_json)
23
+ }.to_json
24
+ else
25
+ raise StandardError, "Environment variables `BUILD_NUMBER` and `JOB_NAME` are not set. Is this running on a Jenkins build?"
26
+ end
27
+ else
28
+ raise StandardError, "No support for ci named #{ci_service}"
29
+ end
30
+ end
31
+ private :hardcover_coverage_data
32
+
33
+ def post
34
+ f = File.open('hardcover_json_file', 'w+')
35
+ begin
36
+ f.write(hardcover_coverage_data)
37
+ f.close
38
+ `curl --form json_file=@#{f.path} #{hardcover_api_jobs_path}`
39
+ rescue StandardError => e
40
+ FileUtils.rm(f)
41
+ raise e
42
+ end
43
+ FileUtils.rm(f)
44
+ end
45
+
46
+ def hardcover_api_jobs_path
47
+ "#{hardcover_base_url}/v1/jobs"
48
+ end
49
+ private :hardcover_api_jobs_path
50
+
51
+ def hardcover_base_url
52
+ url = Project.yml["hardcover_base_url"]
53
+ unless url
54
+ raise "No `hardcover_base_url` configured. Please add it to your `.slather.yml`"
55
+ end
56
+ url
57
+ end
58
+ private :hardcover_base_url
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,244 @@
1
+ require 'nokogiri'
2
+
3
+ module Slather
4
+ module CoverageService
5
+ module HtmlOutput
6
+
7
+ def coverage_file_class
8
+ Slather::CoverageFile
9
+ end
10
+ private :coverage_file_class
11
+
12
+ def directory_path
13
+ is_path_valid = !output_directory.nil? && !output_directory.strip.eql?("")
14
+ is_path_valid ? File.expand_path(output_directory) : "html"
15
+ end
16
+ private :directory_path
17
+
18
+ def post
19
+ create_html_reports(coverage_files)
20
+ generate_reports(@docs)
21
+
22
+ index_html_path = File.join(directory_path, "index.html")
23
+ if show_html
24
+ open_coverage index_html_path
25
+ else
26
+ print_path_coverage index_html_path
27
+ end
28
+ end
29
+
30
+ def print_path_coverage(index_html)
31
+ path = File.expand_path index_html
32
+ puts "\nTo open the html reports, use \n\nopen '#{path}'\n\nor use '--show' flag to open it automatically.\n\n"
33
+ end
34
+
35
+ def open_coverage(index_html)
36
+ path = File.expand_path index_html
37
+ `open '#{path}'` if File.exist?(path)
38
+ end
39
+
40
+ def create_html_reports(coverage_files)
41
+ create_index_html(coverage_files)
42
+ create_htmls_from_files(coverage_files)
43
+ end
44
+
45
+ def generate_reports(reports)
46
+ FileUtils.rm_rf(directory_path) if Dir.exist?(directory_path)
47
+ FileUtils.mkdir_p(directory_path)
48
+
49
+ reports.each do |name, doc|
50
+ html_file = File.join(directory_path, "#{name}.html")
51
+ File.write(html_file, doc.to_html)
52
+ end
53
+ end
54
+
55
+ def create_index_html(coverage_files)
56
+ project_name = File.basename(self.xcodeproj)
57
+ template = generate_html_template(project_name, true, false)
58
+
59
+ total_relevant_lines = 0
60
+ total_tested_lines = 0
61
+ coverage_files.each { |coverage_file|
62
+ total_tested_lines += coverage_file.num_lines_tested
63
+ total_relevant_lines += coverage_file.num_lines_testable
64
+ }
65
+
66
+ builder = Nokogiri::HTML::Builder.with(template.at('#reports')) { |cov|
67
+ cov.h2 "Files for \"#{project_name}\""
68
+
69
+ cov.h4 {
70
+ percentage = (total_tested_lines / total_relevant_lines.to_f) * 100.0
71
+ cov.span "Total Coverage : "
72
+ cov.span '%.2f%%' % percentage, :class => class_for_coverage_percentage(percentage), :id => "total_coverage"
73
+ }
74
+
75
+ cov.input(:class => "search", :placeholder => "Search")
76
+
77
+ cov.table(:class => "coverage_list", :cellspacing => 0, :cellpadding => 0) {
78
+
79
+ cov.thead {
80
+ cov.tr {
81
+ cov.th "%", :class => "col_num sort", "data-sort" => "data_percentage"
82
+ cov.th "File", :class => "sort", "data-sort" => "data_filename"
83
+ cov.th "Lines", :class => "col_percent sort", "data-sort" => "data_lines"
84
+ cov.th "Relevant", :class => "col_percent sort", "data-sort" => "data_relevant"
85
+ cov.th "Covered", :class => "col_percent sort", "data-sort" => "data_covered"
86
+ cov.th "Missed", :class => "col_percent sort", "data-sort" => "data_missed"
87
+ }
88
+ }
89
+
90
+ cov.tbody(:class => "list") {
91
+ coverage_files.each { |coverage_file|
92
+ filename = File.basename(coverage_file.source_file_pathname_relative_to_repo_root)
93
+ filename_link = "#{filename}.html"
94
+
95
+ cov.tr {
96
+ percentage = coverage_file.percentage_lines_tested
97
+
98
+ cov.td { cov.span '%.2f' % percentage, :class => "percentage #{class_for_coverage_percentage(percentage)} data_percentage" }
99
+ cov.td(:class => "data_filename") {
100
+ cov.a filename, :href => filename_link
101
+ }
102
+ cov.td "#{coverage_file.line_coverage_data.count}", :class => "data_lines"
103
+ cov.td "#{coverage_file.num_lines_testable}", :class => "data_relevant"
104
+ cov.td "#{coverage_file.num_lines_tested}", :class => "data_covered"
105
+ cov.td "#{(coverage_file.num_lines_testable - coverage_file.num_lines_tested)}", :class => "data_missed"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ @docs = Hash.new
113
+ @docs[:index] = builder.doc
114
+ end
115
+
116
+ def create_htmls_from_files(coverage_files)
117
+ coverage_files.map { |file| create_html_from_file file }
118
+ end
119
+
120
+ def create_html_from_file(coverage_file)
121
+ filepath = coverage_file.source_file_pathname_relative_to_repo_root
122
+ filename = File.basename(filepath)
123
+ percentage = coverage_file.percentage_lines_tested
124
+
125
+ cleaned_gcov_lines = coverage_file.cleaned_gcov_data.split("\n")
126
+ is_file_empty = (cleaned_gcov_lines.count <= 0)
127
+
128
+ template = generate_html_template(filename, false, is_file_empty)
129
+
130
+ builder = Nokogiri::HTML::Builder.with(template.at('#reports')) { |cov|
131
+ cov.h2(:class => "cov_title") {
132
+ cov.span("Coverage for \"#{filename}\"" + (!is_file_empty ? " : " : ""))
133
+ cov.span("#{'%.2f' % percentage}%", :class => class_for_coverage_percentage(percentage)) unless is_file_empty
134
+ }
135
+
136
+ cov.h4("(#{coverage_file.num_lines_tested} of #{coverage_file.num_lines_testable} relevant lines covered)", :class => "cov_subtitle")
137
+ cov.h4(filepath, :class => "cov_filepath")
138
+
139
+ if is_file_empty
140
+ cov.p "¯\\_(ツ)_/¯"
141
+ next
142
+ end
143
+
144
+ cov.table(:class => "source_code") {
145
+ cleaned_gcov_lines.each do |line|
146
+ data = line.split(':', 3)
147
+
148
+ line_number = data[1].to_i
149
+ next unless line_number > 0
150
+
151
+ coverage_data = data[0].strip
152
+ line_data = [line_number, data[2], hits_for_coverage_data(coverage_data)]
153
+ classes = ["num", "src", "coverage"]
154
+
155
+ cov.tr(:class => class_for_coverage_data(coverage_data)) {
156
+ line_data.each_with_index { |line, idx|
157
+ if idx != 1
158
+ cov.td(line, :class => classes[idx])
159
+ else
160
+ cov.td(:class => classes[idx]) {
161
+ cov.pre { cov.code(line, :class => "objc") }
162
+ }
163
+ end
164
+ }
165
+ }
166
+ end
167
+ }
168
+ }
169
+
170
+ @docs[filename] = builder.doc
171
+ end
172
+
173
+ def generate_html_template(title, is_index, is_file_empty)
174
+ logo_path = File.join(gem_root_path, "docs/logo.jpg")
175
+ css_path = File.join(gem_root_path, "assets/slather.css")
176
+ highlight_js_path = File.join(gem_root_path, "assets/highlight.pack.js")
177
+ list_js_path = File.join(gem_root_path, "assets/list.min.js")
178
+
179
+ builder = Nokogiri::HTML::Builder.new do |doc|
180
+ doc.html {
181
+ doc.head {
182
+ doc.title "#{title} - Slather"
183
+ doc.link :href => css_path, :media => "all", :rel => "stylesheet"
184
+ }
185
+ doc.body {
186
+ doc.header {
187
+ doc.div(:class => "row") {
188
+ doc.a(:href => "index.html") { doc.img(:src => logo_path, :alt => "Slather logo") }
189
+ }
190
+ }
191
+ doc.div(:class => "row") { doc.div(:id => "reports") }
192
+ doc.footer {
193
+ doc.div(:class => "row") {
194
+ doc.p { doc.a("Fork me on Github", :href => "https://github.com/venmo/slather") }
195
+ doc.p("© #{Date.today.year} Slather")
196
+ }
197
+ }
198
+
199
+ if is_index
200
+ doc.script :src => list_js_path
201
+ doc.script "var reports = new List('reports', { valueNames: [ 'data_percentage', 'data_filename', 'data_lines', 'data_relevant', 'data_covered', 'data_missed' ]});"
202
+ else
203
+ unless is_file_empty
204
+ doc.script :src => highlight_js_path
205
+ doc.script "hljs.initHighlightingOnLoad();"
206
+ end
207
+ end
208
+ }
209
+ }
210
+ end
211
+ builder.doc
212
+ end
213
+
214
+ def gem_root_path
215
+ File.expand_path File.join(File.dirname(__dir__), "../..")
216
+ end
217
+
218
+ def class_for_coverage_data(coverage_data)
219
+ case coverage_data
220
+ when /\d/ then "covered"
221
+ when /#/ then "missed"
222
+ else "never"
223
+ end
224
+ end
225
+
226
+ def hits_for_coverage_data(coverage_data)
227
+ case coverage_data
228
+ when /\d/ then (coverage_data.to_i > 0) ? "#{coverage_data}x" : ""
229
+ when /#/ then "!"
230
+ else ""
231
+ end
232
+ end
233
+
234
+ def class_for_coverage_percentage(percentage)
235
+ case
236
+ when percentage > 85 then "cov_high"
237
+ when percentage > 70 then "cov_medium"
238
+ else "cov_low"
239
+ end
240
+ end
241
+
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,31 @@
1
+ module Slather
2
+ module CoverageService
3
+ module SimpleOutput
4
+
5
+ def coverage_file_class
6
+ Slather::CoverageFile
7
+ end
8
+ private :coverage_file_class
9
+
10
+ def post
11
+ total_project_lines = 0
12
+ total_project_lines_tested = 0
13
+ coverage_files.each do |coverage_file|
14
+ # ignore lines that don't count towards coverage (comments, whitespace, etc). These are nil in the array.
15
+
16
+ lines_tested = coverage_file.num_lines_tested
17
+ total_lines = coverage_file.num_lines_testable
18
+ percentage = '%.2f' % [coverage_file.percentage_lines_tested]
19
+
20
+ total_project_lines_tested += lines_tested
21
+ total_project_lines += total_lines
22
+
23
+ puts "#{coverage_file.source_file_pathname_relative_to_repo_root}: #{lines_tested} of #{total_lines} lines (#{percentage}%)"
24
+ end
25
+ total_percentage = '%.2f' % [(total_project_lines_tested / total_project_lines.to_f) * 100.0]
26
+ puts "Test Coverage: #{total_percentage}%"
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module Slather
2
+ class CoverallsCoverageFile < CoverageFile
3
+
4
+ def as_json
5
+ {
6
+ :name => source_file_pathname_relative_to_repo_root.to_s,
7
+ :source => source_data,
8
+ :coverage => line_coverage_data
9
+ }
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,132 @@
1
+ require 'fileutils'
2
+ require 'xcodeproj'
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ module Xcodeproj
7
+ class Project
8
+
9
+ def slather_setup_for_coverage
10
+ build_configurations.each do |build_configuration|
11
+ build_configuration.build_settings["GCC_INSTRUMENT_PROGRAM_FLOW_ARCS"] = "YES"
12
+ build_configuration.build_settings["GCC_GENERATE_TEST_COVERAGE_FILES"] = "YES"
13
+ end
14
+ end
15
+
16
+ end
17
+ end
18
+
19
+ module Slather
20
+ class Project < Xcodeproj::Project
21
+
22
+ attr_accessor :build_directory, :ignore_list, :ci_service, :coverage_service, :coverage_access_token, :source_directory, :output_directory, :xcodeproj, :show_html
23
+
24
+ alias_method :setup_for_coverage, :slather_setup_for_coverage
25
+
26
+ def self.open(xcodeproj)
27
+ proj = super
28
+ proj.configure_from_yml
29
+ proj.xcodeproj = xcodeproj
30
+ proj
31
+ end
32
+
33
+ def derived_data_dir
34
+ File.expand_path('~') + "/Library/Developer/Xcode/DerivedData/"
35
+ end
36
+ private :derived_data_dir
37
+
38
+ def build_directory
39
+ @build_directory || derived_data_dir
40
+ end
41
+
42
+ def coverage_files
43
+ coverage_files = Dir["#{build_directory}/**/*.gcno"].map do |file|
44
+ coverage_file = coverage_file_class.new(self, file)
45
+ # If there's no source file for this gcno, it probably belongs to another project.
46
+ coverage_file.source_file_pathname && !coverage_file.ignored? ? coverage_file : nil
47
+ end.compact
48
+
49
+ if coverage_files.empty?
50
+ raise StandardError, "No coverage files found. Are you sure your project is setup for generating coverage files? Try `slather setup your/project.xcodeproj`"
51
+ else
52
+ dedupe(coverage_files)
53
+ end
54
+ end
55
+ private :coverage_files
56
+
57
+ def dedupe(coverage_files)
58
+ coverage_files.group_by(&:source_file_pathname).values.map { |cf_array| cf_array.max_by(&:percentage_lines_tested) }
59
+ end
60
+ private :dedupe
61
+
62
+ def self.yml_filename
63
+ '.slather.yml'
64
+ end
65
+
66
+ def self.yml
67
+ @yml ||= File.exist?(yml_filename) ? YAML.load_file(yml_filename) : {}
68
+ end
69
+
70
+ def configure_from_yml
71
+ configure_build_directory_from_yml
72
+ configure_ignore_list_from_yml
73
+ configure_ci_service_from_yml
74
+ configure_coverage_access_token_from_yml
75
+ configure_coverage_service_from_yml
76
+ configure_source_directory_from_yml
77
+ configure_output_directory_from_yml
78
+ end
79
+
80
+ def configure_build_directory_from_yml
81
+ self.build_directory = self.class.yml["build_directory"] if self.class.yml["build_directory"] && !@build_directory
82
+ end
83
+
84
+ def configure_source_directory_from_yml
85
+ self.source_directory ||= self.class.yml["source_directory"] if self.class.yml["source_directory"]
86
+ end
87
+
88
+ def configure_output_directory_from_yml
89
+ self.output_directory ||= self.class.yml["output_directory"] if self.class.yml["output_directory"]
90
+ end
91
+
92
+ def configure_ignore_list_from_yml
93
+ self.ignore_list ||= [(self.class.yml["ignore"] || [])].flatten
94
+ end
95
+
96
+ def configure_ci_service_from_yml
97
+ self.ci_service ||= (self.class.yml["ci_service"] || :travis_ci)
98
+ end
99
+
100
+ def ci_service=(service)
101
+ @ci_service = service && service.to_sym
102
+ end
103
+
104
+ def configure_coverage_service_from_yml
105
+ self.coverage_service ||= (self.class.yml["coverage_service"] || :terminal)
106
+ end
107
+
108
+ def configure_coverage_access_token_from_yml
109
+ self.coverage_access_token ||= (ENV["COVERAGE_ACCESS_TOKEN"] || self.class.yml["coverage_access_token"] || "")
110
+ end
111
+
112
+ def coverage_service=(service)
113
+ service = service && service.to_sym
114
+ if service == :coveralls
115
+ extend(Slather::CoverageService::Coveralls)
116
+ elsif service == :hardcover
117
+ extend(Slather::CoverageService::Hardcover)
118
+ elsif service == :terminal
119
+ extend(Slather::CoverageService::SimpleOutput)
120
+ elsif service == :gutter_json
121
+ extend(Slather::CoverageService::GutterJsonOutput)
122
+ elsif service == :cobertura_xml
123
+ extend(Slather::CoverageService::CoberturaXmlOutput)
124
+ elsif service == :html
125
+ extend(Slather::CoverageService::HtmlOutput)
126
+ else
127
+ raise ArgumentError, "`#{coverage_service}` is not a valid coverage service. Try `terminal`, `coveralls`, `gutter_json`, `cobertura_xml` or `html`"
128
+ end
129
+ @coverage_service = service
130
+ end
131
+ end
132
+ end