ashtonw-slather 1.8.2
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 +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +45 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +132 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +206 -0
- data/Rakefile +1 -0
- data/assets/highlight.pack.js +1 -0
- data/assets/list.min.js +1 -0
- data/assets/slather.css +316 -0
- data/bin/slather +117 -0
- data/docs/logo.jpg +0 -0
- data/lib/cocoapods_plugin.rb +10 -0
- data/lib/slather.rb +20 -0
- data/lib/slather/coverage_file.rb +195 -0
- data/lib/slather/coverage_service/cobertura_xml_output.rb +183 -0
- data/lib/slather/coverage_service/coveralls.rb +186 -0
- data/lib/slather/coverage_service/gutter_json_output.rb +50 -0
- data/lib/slather/coverage_service/hardcover.rb +61 -0
- data/lib/slather/coverage_service/html_output.rb +244 -0
- data/lib/slather/coverage_service/simple_output.rb +31 -0
- data/lib/slather/coveralls_coverage_file.rb +13 -0
- data/lib/slather/project.rb +132 -0
- data/lib/slather/version.rb +3 -0
- data/slather.gemspec +32 -0
- data/spec/fixtures/fixtures.xcodeproj/project.pbxproj +496 -0
- data/spec/fixtures/fixtures.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- data/spec/fixtures/fixtures.xcodeproj/xcshareddata/xcschemes/fixtures.xcscheme +69 -0
- data/spec/fixtures/fixtures/Supporting Files/fixtures-Prefix.pch +9 -0
- data/spec/fixtures/fixtures/fixtures.h +16 -0
- data/spec/fixtures/fixtures/fixtures.m +23 -0
- data/spec/fixtures/fixtures/fixtures_cpp.cpp +9 -0
- data/spec/fixtures/fixtures/fixtures_cpp.h +6 -0
- data/spec/fixtures/fixtures/fixtures_m.h +5 -0
- data/spec/fixtures/fixtures/fixtures_m.m +5 -0
- data/spec/fixtures/fixtures/fixtures_mm.h +5 -0
- data/spec/fixtures/fixtures/fixtures_mm.mm +5 -0
- data/spec/fixtures/fixtures/more_files/Branches.h +15 -0
- data/spec/fixtures/fixtures/more_files/Branches.m +45 -0
- data/spec/fixtures/fixtures/more_files/Empty.h +13 -0
- data/spec/fixtures/fixtures/more_files/Empty.m +13 -0
- data/spec/fixtures/fixtures/more_files/peekaview.h +13 -0
- data/spec/fixtures/fixtures/more_files/peekaview.m +31 -0
- data/spec/fixtures/fixturesTests/BranchesTests.m +38 -0
- data/spec/fixtures/fixturesTests/Supporting Files/en.lproj/InfoPlist.strings +2 -0
- data/spec/fixtures/fixturesTests/Supporting Files/fixturesTests-Info.plist +22 -0
- data/spec/fixtures/fixturesTests/fixturesTests.m +36 -0
- data/spec/fixtures/fixturesTests/peekaviewTests.m +34 -0
- data/spec/fixtures/fixtures_html/Branches.m.html +261 -0
- data/spec/fixtures/fixtures_html/BranchesTests.m.html +228 -0
- data/spec/fixtures/fixtures_html/Empty.m.html +30 -0
- data/spec/fixtures/fixtures_html/fixtures.m.html +151 -0
- data/spec/fixtures/fixtures_html/fixturesTests.m.html +216 -0
- data/spec/fixtures/fixtures_html/fixtures_cpp.cpp.html +30 -0
- data/spec/fixtures/fixtures_html/fixtures_m.m.html +30 -0
- data/spec/fixtures/fixtures_html/fixtures_mm.mm.html +30 -0
- data/spec/fixtures/fixtures_html/index.html +134 -0
- data/spec/fixtures/fixtures_html/peekaview.m.html +190 -0
- data/spec/fixtures/fixtures_html/peekaviewTests.m.html +206 -0
- data/spec/fixtures/gutter.json +1 -0
- data/spec/slather/cocoapods_plugin_spec.rb +21 -0
- data/spec/slather/coverage_file_spec.rb +337 -0
- data/spec/slather/coverage_service/cobertura_xml_spec.rb +49 -0
- data/spec/slather/coverage_service/coveralls_spec.rb +122 -0
- data/spec/slather/coverage_service/gutter_json_spec.rb +28 -0
- data/spec/slather/coverage_service/hardcover_spec.rb +87 -0
- data/spec/slather/coverage_service/html_output_spec.rb +179 -0
- data/spec/slather/coverage_service/simple_output_spec.rb +35 -0
- data/spec/slather/fixtures.gcno +0 -0
- data/spec/slather/project_spec.rb +288 -0
- data/spec/spec_helper.rb +27 -0
- 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,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
|