slather 1.7.1 → 1.8

Sign up to get free protection for your applications and to get access to all the features.
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
-