slather 1.7.1 → 1.8

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.
data/lib/slather.rb CHANGED
@@ -4,8 +4,10 @@ require 'slather/coverage_file'
4
4
  require 'slather/coveralls_coverage_file'
5
5
  require 'slather/coverage_service/cobertura_xml_output'
6
6
  require 'slather/coverage_service/coveralls'
7
+ require 'slather/coverage_service/hardcover'
7
8
  require 'slather/coverage_service/gutter_json_output'
8
9
  require 'slather/coverage_service/simple_output'
10
+ require 'slather/coverage_service/html_output'
9
11
 
10
12
  module Slather
11
13
 
@@ -71,7 +71,7 @@ module Slather
71
71
  end
72
72
 
73
73
  def cleaned_gcov_data
74
- data = gcov_data.gsub(/^function(.*) called [0-9]+ returned [0-9]+% blocks executed(.*)$\r?\n/, '')
74
+ data = gcov_data.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').gsub(/^function(.*) called [0-9]+ returned [0-9]+% blocks executed(.*)$\r?\n/, '')
75
75
  data.gsub(/^branch(.*)$\r?\n/, '')
76
76
  end
77
77
 
@@ -11,22 +11,54 @@ module Slather
11
11
  ENV['TRAVIS_JOB_ID']
12
12
  end
13
13
  private :travis_job_id
14
-
14
+
15
15
  def circleci_job_id
16
16
  ENV['CIRCLE_BUILD_NUM']
17
17
  end
18
18
  private :circleci_job_id
19
-
19
+
20
20
  def circleci_pull_request
21
- ENV['CI_PULL_REQUEST']
21
+ ENV['CIRCLE_PR_NUMBER'] || ENV['CI_PULL_REQUEST'] || ""
22
22
  end
23
23
  private :circleci_pull_request
24
-
24
+
25
+ def jenkins_job_id
26
+ ENV['BUILD_ID']
27
+ end
28
+ private :jenkins_job_id
29
+
30
+ def jenkins_branch_name
31
+ branch_name = ENV['GIT_BRANCH']
32
+ if branch_name.include? 'origin/'
33
+ branch_name[7...branch_name.length]
34
+ else
35
+ branch_name
36
+ end
37
+ end
38
+ private :jenkins_branch_name
39
+
40
+ def jenkins_git_info
41
+ {
42
+ head: {
43
+ id: ENV['sha1'],
44
+ author_name: ENV['ghprbActualCommitAuthor'],
45
+ message: ENV['ghprbPullTitle']
46
+ },
47
+ branch: jenkins_branch_name
48
+ }
49
+ end
50
+ private :jenkins_git_info
51
+
52
+ def circleci_build_url
53
+ "https://circleci.com/gh/" + ENV['CIRCLE_PROJECT_USERNAME'] || "" + "/" + ENV['CIRCLE_PROJECT_REPONAME'] || "" + "/" + ENV['CIRCLE_BUILD_NUM'] || ""
54
+ end
55
+ private :circleci_build_url
56
+
25
57
  def circleci_git_info
26
58
  {
27
59
  :head => {
28
60
  :id => (ENV['CIRCLE_SHA1'] || ""),
29
- :author_name => (ENV['CIRCLE_USERNAME'] || ""),
61
+ :author_name => (ENV['CIRCLE_PR_USERNAME'] || ENV['CIRCLE_USERNAME'] || ""),
30
62
  :message => (`git log --format=%s -n 1 HEAD`.chomp || "")
31
63
  },
32
64
  :branch => (ENV['CIRCLE_BRANCH'] || "")
@@ -47,7 +79,7 @@ module Slather
47
79
  {
48
80
  :service_job_id => travis_job_id,
49
81
  :service_name => "travis-pro",
50
- :repo_token => ci_access_token,
82
+ :repo_token => coverage_access_token,
51
83
  :source_files => coverage_files.map(&:as_json)
52
84
  }.to_json
53
85
  end
@@ -59,19 +91,32 @@ module Slather
59
91
  coveralls_hash = {
60
92
  :service_job_id => circleci_job_id,
61
93
  :service_name => "circleci",
62
- :repo_token => ci_access_token,
94
+ :repo_token => coverage_access_token,
63
95
  :source_files => coverage_files.map(&:as_json),
64
- :git => circleci_git_info
96
+ :git => circleci_git_info,
97
+ :service_build_url => circleci_build_url
65
98
  }
66
-
99
+
67
100
  if circleci_pull_request != nil && circleci_pull_request.length > 0
68
101
  coveralls_hash[:service_pull_request] = circleci_pull_request.split("/").last
69
102
  end
70
-
103
+
71
104
  coveralls_hash.to_json
72
105
  else
73
106
  raise StandardError, "Environment variable `CIRCLE_BUILD_NUM` not set. Is this running on a circleci build?"
74
107
  end
108
+ elsif ci_service == :jenkins
109
+ if jenkins_job_id
110
+ {
111
+ service_job_id: jenkins_job_id,
112
+ service_name: "jenkins",
113
+ repo_token: coverage_access_token,
114
+ source_files: coverage_files.map(&:as_json),
115
+ git: jenkins_git_info
116
+ }.to_json
117
+ else
118
+ raise StandardError, "Environment variable `BUILD_ID` not set. Is this running on a jenkins build?"
119
+ end
75
120
  else
76
121
  raise StandardError, "No support for ci named #{ci_service}"
77
122
  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
@@ -19,13 +19,14 @@ end
19
19
  module Slather
20
20
  class Project < Xcodeproj::Project
21
21
 
22
- attr_accessor :build_directory, :ignore_list, :ci_service, :coverage_service, :ci_access_token, :source_directory, :output_directory
22
+ attr_accessor :build_directory, :ignore_list, :ci_service, :coverage_service, :coverage_access_token, :source_directory, :output_directory, :xcodeproj, :show_html
23
23
 
24
24
  alias_method :setup_for_coverage, :slather_setup_for_coverage
25
25
 
26
26
  def self.open(xcodeproj)
27
27
  proj = super
28
28
  proj.configure_from_yml
29
+ proj.xcodeproj = xcodeproj
29
30
  proj
30
31
  end
31
32
 
@@ -70,7 +71,7 @@ module Slather
70
71
  configure_build_directory_from_yml
71
72
  configure_ignore_list_from_yml
72
73
  configure_ci_service_from_yml
73
- configure_ci_access_token_from_yml
74
+ configure_coverage_access_token_from_yml
74
75
  configure_coverage_service_from_yml
75
76
  configure_source_directory_from_yml
76
77
  configure_output_directory_from_yml
@@ -104,26 +105,28 @@ module Slather
104
105
  self.coverage_service ||= (self.class.yml["coverage_service"] || :terminal)
105
106
  end
106
107
 
107
- def configure_ci_access_token_from_yml
108
- self.ci_access_token ||= (self.class.yml["ci_access_token"] || "")
108
+ def configure_coverage_access_token_from_yml
109
+ self.coverage_access_token ||= (ENV["COVERAGE_ACCESS_TOKEN"] || self.class.yml["coverage_access_token"] || "")
109
110
  end
110
111
 
111
112
  def coverage_service=(service)
112
113
  service = service && service.to_sym
113
114
  if service == :coveralls
114
115
  extend(Slather::CoverageService::Coveralls)
116
+ elsif service == :hardcover
117
+ extend(Slather::CoverageService::Hardcover)
115
118
  elsif service == :terminal
116
119
  extend(Slather::CoverageService::SimpleOutput)
117
120
  elsif service == :gutter_json
118
121
  extend(Slather::CoverageService::GutterJsonOutput)
119
122
  elsif service == :cobertura_xml
120
123
  extend(Slather::CoverageService::CoberturaXmlOutput)
124
+ elsif service == :html
125
+ extend(Slather::CoverageService::HtmlOutput)
121
126
  else
122
- raise ArgumentError, "`#{coverage_service}` is not a valid coverage service. Try `terminal`, `coveralls`, `gutter_json` or `cobertura_xml`"
127
+ raise ArgumentError, "`#{coverage_service}` is not a valid coverage service. Try `terminal`, `coveralls`, `gutter_json`, `cobertura_xml` or `html`"
123
128
  end
124
129
  @coverage_service = service
125
130
  end
126
-
127
131
  end
128
132
  end
129
-