scrutinizer-ocular 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +12 -0
- data/lib/scrutinizer/ocular.rb +48 -0
- data/lib/scrutinizer/ocular/api_client.rb +80 -0
- data/lib/scrutinizer/ocular/formatter.rb +76 -0
- data/lib/scrutinizer/ocular/output.rb +27 -0
- data/lib/scrutinizer/ocular/repository_introspector.rb +64 -0
- data/lib/scrutinizer/ocular/version.rb +5 -0
- data/res/cacert.pem +3866 -0
- data/scrutinizer-ocular.gemspec +31 -0
- data/spec/scrutinizer/ocular/api_client_spec.rb +19 -0
- data/spec/scrutinizer/ocular/fixtures/foo.rb +4 -0
- data/spec/scrutinizer/ocular/formatter_spec.rb +60 -0
- data/spec/scrutinizer/ocular/repository_introspector_spec.rb +50 -0
- data/spec/spec_helper.rb +71 -0
- metadata +135 -0
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
vendor/
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Johannes M. Schmitt
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Scrutinizer Ocular
|
2
|
+
|
3
|
+
Uploads Ruby code coverage data to [scrutinizer-ci.com](https://scrutinizer-ci.com). Internally, it relies on SimpleCov.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'scrutinizer-ocular'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install scrutinizer-ocular
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
You need to initialize ocular at the very top of your ``test_helper.rb`` or ``spec_helper.rb`` file
|
23
|
+
before loading any of your code:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# test_helper.rb or spec_helper.rb
|
27
|
+
require 'scrutinizer/ocular'
|
28
|
+
Scrutinizer::Ocular.watch!
|
29
|
+
```
|
30
|
+
|
31
|
+
When running your tests on a private repository, make sure you have your access token set:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
SCRUTINIZER_ACCESS_TOKEN=abc123 bundle exec rspec spec
|
35
|
+
```
|
36
|
+
|
37
|
+
When you [create an access token](https://scrutinizer-ci.com/profile/applications), make sure to select ``READ`` permission only.
|
38
|
+
Generally, it's a good idea to set-up a dedicated user for reporting code coverage only.
|
39
|
+
|
40
|
+
## Advanced Use-Cases
|
41
|
+
|
42
|
+
### Defining the SimpleCov Profile
|
43
|
+
If you would like to have SimpleCov use a specific profile, you can pass it to the watch method:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
Scrutinizer::Ocular.watch! 'rails'
|
47
|
+
```
|
48
|
+
|
49
|
+
### Adding additional Formatters
|
50
|
+
If you would like to run other formatters apart from Scrutinizer's Formatter, you can add these easily:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
require 'simplecov'
|
54
|
+
require 'scrutinizer/ocular'
|
55
|
+
|
56
|
+
# To avoid uploading coverage when running tests locally, you can use
|
57
|
+
# Scrutinizer::Ocular.should_run? and add the formatter conditionally.
|
58
|
+
|
59
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
60
|
+
SimpleCov::Formatter::HTMLFormatter,
|
61
|
+
Scrutinizer::Ocular::UploadFormatter
|
62
|
+
]
|
63
|
+
SimpleCov.start
|
64
|
+
```
|
65
|
+
|
66
|
+
### Merging Coverage from Parallelized Runs
|
67
|
+
Scrutinizer supports merging coverage from parallelized runs natively. All you need to do is
|
68
|
+
[adjust your .scrutinizer.yml](https://scrutinizer-ci.com/docs/tools/external-code-coverage/)
|
69
|
+
to tell us how many submissions you are going to make; no further changes are necessary.
|
70
|
+
|
71
|
+
## Credits
|
72
|
+
Parts of this code were inspired by the Ruby Coveralls implementation and were received under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "scrutinizer/ocular/version"
|
2
|
+
|
3
|
+
module Scrutinizer
|
4
|
+
module Ocular
|
5
|
+
extend self
|
6
|
+
|
7
|
+
attr_accessor :enabled
|
8
|
+
@enabled = false
|
9
|
+
|
10
|
+
def watch!(profile = nil, &block)
|
11
|
+
if self.should_run?
|
12
|
+
setup!
|
13
|
+
start! profile, &block
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup!
|
18
|
+
require 'simplecov'
|
19
|
+
require "scrutinizer/ocular/formatter"
|
20
|
+
|
21
|
+
::SimpleCov.formatter = create_formatter
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_formatter
|
25
|
+
if ENV['SCRUTINIZER_CC_FILE']
|
26
|
+
return Scrutinizer::Ocular::LocalOutputFormatter
|
27
|
+
end
|
28
|
+
|
29
|
+
Scrutinizer::Ocular::UploadingFormatter
|
30
|
+
end
|
31
|
+
|
32
|
+
def start!(profile=nil, &block)
|
33
|
+
::SimpleCov.add_filter 'vendor'
|
34
|
+
|
35
|
+
if profile
|
36
|
+
::SimpleCov.start(profile)
|
37
|
+
elsif block
|
38
|
+
::SimpleCov.start(profile) { instance_eval(block) }
|
39
|
+
else
|
40
|
+
::SimpleCov.start
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def should_run?
|
45
|
+
ENV["CI"] || ENV["JENKINS_URL"] || ENV["SCRUTINIZER_COVERAGE"] || ENV['SCRUTINIZER_CC_FILE'] || @enabled
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
require 'json'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Scrutinizer
|
7
|
+
module Ocular
|
8
|
+
class UploadFailed < StandardError
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ApiClient
|
17
|
+
def initialize(api_url, repository_name, revision, parents, access_token)
|
18
|
+
@api_url = api_url
|
19
|
+
@repository_name = repository_name
|
20
|
+
@revision = revision
|
21
|
+
@parents = parents
|
22
|
+
@access_token = access_token
|
23
|
+
@http = create_http_service(@api_url)
|
24
|
+
|
25
|
+
disable_net_blockers!
|
26
|
+
end
|
27
|
+
|
28
|
+
def upload(format, data)
|
29
|
+
uri = URI.parse(@api_url + '/repositories/' + @repository_name + '/data/code-coverage')
|
30
|
+
|
31
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
32
|
+
request.add_field('Content-Type', 'application/json')
|
33
|
+
request.body = {
|
34
|
+
:revision => @revision,
|
35
|
+
:parents => @parents,
|
36
|
+
:coverage => {
|
37
|
+
:format => format,
|
38
|
+
:data => Base64.encode64(data)
|
39
|
+
}
|
40
|
+
}.to_json
|
41
|
+
|
42
|
+
response = @http.request(request)
|
43
|
+
|
44
|
+
if response.code.to_i < 200 || response.code.to_i >= 300
|
45
|
+
raise UploadFailed.new(response), "Upload failed with status #{response.code}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def create_http_service(url)
|
52
|
+
uri = URI(url)
|
53
|
+
pem_file = File.absolute_path(File.dirname(__FILE__) + '/../../../res/cacert.pem')
|
54
|
+
pem = File.read(pem_file)
|
55
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
56
|
+
http.use_ssl = true
|
57
|
+
http.cert = OpenSSL::X509::Certificate.new(pem)
|
58
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
59
|
+
|
60
|
+
http
|
61
|
+
end
|
62
|
+
|
63
|
+
def disable_net_blockers!
|
64
|
+
host = URI(@api_url).host
|
65
|
+
|
66
|
+
if defined?(WebMock)
|
67
|
+
allow = WebMock::Config.instance.allow || []
|
68
|
+
WebMock::Config.instance.allow = [*allow].push host
|
69
|
+
end
|
70
|
+
|
71
|
+
if defined?(VCR)
|
72
|
+
VCR.send(VCR.version.major < 2 ? :config : :configure) do |c|
|
73
|
+
c.ignore_hosts host
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'scrutinizer/ocular/api_client'
|
2
|
+
require 'scrutinizer/ocular/repository_introspector'
|
3
|
+
require 'scrutinizer/ocular/output'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Scrutinizer
|
7
|
+
module Ocular
|
8
|
+
class Serializer
|
9
|
+
def serialize(result)
|
10
|
+
files = []
|
11
|
+
result.files.each do |file|
|
12
|
+
files << {
|
13
|
+
:name => file.filename,
|
14
|
+
:lines => file.coverage
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
files.to_json
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class LocalOutputFormatter
|
23
|
+
def initialize
|
24
|
+
@output_file = ENV['SCRUTINIZER_CC_FILE'] || "./coverage.json"
|
25
|
+
@serializer = Serializer.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def format(result)
|
29
|
+
File.open(@output_file, 'w') do |file|
|
30
|
+
file.write(@serializer.serialize(result))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class UploadingFormatter
|
36
|
+
attr_accessor :output
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
introspector = RepositoryIntrospector.new(Dir.pwd)
|
40
|
+
|
41
|
+
api_url = ENV['SCRUTINIZER_HOST'] || "https://scrutinizer-ci.com/api"
|
42
|
+
access_token = ENV['SCRUTINIZER_ACCESS_TOKEN'] || nil
|
43
|
+
@repository = ENV['SCRUTINIZER_REPOSITORY'] || introspector.get_repository_name
|
44
|
+
@revision = ENV['SCRUTINIZER_REVISION'] || introspector.get_current_revision
|
45
|
+
|
46
|
+
@api_client = ApiClient.new(
|
47
|
+
api_url,
|
48
|
+
@repository,
|
49
|
+
@revision,
|
50
|
+
introspector.get_current_parents,
|
51
|
+
access_token
|
52
|
+
)
|
53
|
+
|
54
|
+
@serializer = Serializer.new
|
55
|
+
@output = StdoutOutput.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def format(result)
|
59
|
+
begin
|
60
|
+
@output.write("Uploading code coverage for '#{@repository}' and revision '#{@revision}'... ")
|
61
|
+
@api_client.upload("rb-cc", @serializer.serialize(result))
|
62
|
+
@output.write("Done!\n")
|
63
|
+
rescue UploadFailed => e
|
64
|
+
@output.write("Failed\n")
|
65
|
+
|
66
|
+
if e.response.code.to_i == 401 || e.response.code.to_i == 403
|
67
|
+
@output.write("Please make sure to set an access token via the environment variable 'SCRUTINIZER_ACCESS_TOKEN'\n")
|
68
|
+
@output.write("You can obtain access tokens with 'READ' permission on https://scrutinizer-ci.com/profile/applications")
|
69
|
+
else
|
70
|
+
@output.write(e.response.body)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Scrutinizer
|
2
|
+
module Ocular
|
3
|
+
class NullOutput
|
4
|
+
def write(message)
|
5
|
+
# Do nothing
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class MemorizedOutput
|
10
|
+
attr_reader :output
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@output = ''
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(message)
|
17
|
+
@output += message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class StdoutOutput
|
22
|
+
def write(message)
|
23
|
+
print(message)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Scrutinizer
|
4
|
+
module Ocular
|
5
|
+
class RepositoryIntrospector
|
6
|
+
def initialize(dir)
|
7
|
+
@dir = dir
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_repository_name()
|
11
|
+
stdout, status = Open3.capture2('git remote -v', :chdir => @dir)
|
12
|
+
|
13
|
+
raise 'Repository name could not be determined' unless status.exitstatus == 0
|
14
|
+
|
15
|
+
output = stdout.to_s
|
16
|
+
patterns = [
|
17
|
+
/^origin\s+(?:git@|(?:git|https?):\/\/)([^:\/]+)(?:\/|:)([^\/]+)\/([^\/\s]+?)(?:\.git)?(?:\s|\n)/,
|
18
|
+
/^[^\s]+\s+(?:git@|(?:git|https?):\/\/)([^:\/]+)(?:\/|:)([^\/]+)\/([^\/\s]+?)(?:\.git)?(?:\s|\n)/,
|
19
|
+
]
|
20
|
+
|
21
|
+
patterns.each { |pattern|
|
22
|
+
if output =~ pattern
|
23
|
+
return get_repository_type($1) + '/' + $2 + '/' + $3
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
raise "Could not determine repository. Please set the 'SCRUTINIZER_REPOSITORY' environment variable"
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_current_parents
|
31
|
+
stdout, status = Open3.capture2('git log --pretty=%P -n1 HEAD', :chdir => @dir)
|
32
|
+
|
33
|
+
raise 'Parents could not be determined' unless status.exitstatus == 0
|
34
|
+
|
35
|
+
output = stdout.to_s.strip
|
36
|
+
if output.empty?
|
37
|
+
return []
|
38
|
+
end
|
39
|
+
|
40
|
+
output.split(' ')
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_current_revision
|
44
|
+
stdout, status = Open3.capture2('git rev-parse HEAD', :chdir => @dir)
|
45
|
+
|
46
|
+
raise 'Revision could not be determined' unless status.exitstatus == 0
|
47
|
+
|
48
|
+
stdout.to_s.strip
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def get_repository_type(host)
|
53
|
+
if host == "github.com"
|
54
|
+
return "g"
|
55
|
+
elsif host == "bitbucket.org"
|
56
|
+
return "b"
|
57
|
+
end
|
58
|
+
|
59
|
+
raise "Unknown host " + host
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|