oyencov 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/oyencov +18 -0
- data/lib/oyencov/api_connection.rb +53 -0
- data/lib/oyencov/background.rb +81 -0
- data/lib/oyencov/cli.rb +97 -0
- data/lib/oyencov/configuration.rb +62 -0
- data/lib/oyencov/controller_tracking.rb +22 -0
- data/lib/oyencov/coverage_peek_delta.rb +83 -0
- data/lib/oyencov/method_range_parser.rb +101 -0
- data/lib/oyencov/railtie.rb +26 -0
- data/lib/oyencov/simplecov_resultset_translator.rb +54 -0
- data/lib/oyencov/test_report_merger.rb +71 -0
- data/lib/oyencov/test_reporting.rb +28 -0
- data/lib/oyencov.rb +15 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 829c5d959c386d08c7d81c836251104461d0875fc4fb6e04d1963cd32301235b
|
4
|
+
data.tar.gz: 4f14f7447a5278cc84b89d7c6bcd9d4fa97c6ea3f12f4d898e89b042a5de880d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 11a0471aae67aa816d1840f1882eaa206be0ad339ab40fae872567985579a8e84dbae85573b5466083450849b33c30e9ee7a136c48d7ad08d1976849ce46290f
|
7
|
+
data.tar.gz: f896668dcbdf7deb0cb392da50f410fca1a0361f20181844fd99753df1ebd83ea3d5d870a9e3790cce60debe8fef09b88430a58149b40ac016c95c7244a3c23e
|
data/bin/oyencov
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
unless File.exist?('./Gemfile')
|
4
|
+
abort 'Please run oyencov from the root of the project.'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
begin
|
9
|
+
require 'bundler'
|
10
|
+
Bundler.setup
|
11
|
+
rescue StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
here = File.expand_path(File.dirname __FILE__)
|
15
|
+
$LOAD_PATH << "#{here}/../lib"
|
16
|
+
|
17
|
+
require "oyencov/cli"
|
18
|
+
OyenCov::CLI.start(ARGV)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "faraday"
|
2
|
+
require "singleton"
|
3
|
+
|
4
|
+
module OyenCov
|
5
|
+
class APIConnection < Faraday::Connection
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super({
|
10
|
+
url: (ENV["OYENCOV_API_URL"] || "https://telemetry-api.oyencov.com"),
|
11
|
+
headers: {
|
12
|
+
"Authorization" => "Bearer #{ENV["OYENCOV_API_KEY"]}",
|
13
|
+
"Content-Type" => "application/json",
|
14
|
+
"User-Agent" => "oyencov-ruby 0.0.1"
|
15
|
+
}
|
16
|
+
}) do |f|
|
17
|
+
f.request :json
|
18
|
+
f.response :json
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Used in `background.rb` to determine whether to start background
|
23
|
+
def get_data_submission_clearance
|
24
|
+
attempts = 3
|
25
|
+
begin
|
26
|
+
response = get("/v1/data_submission_clearance")
|
27
|
+
rescue Faraday::Error => e
|
28
|
+
if ENV["OYENCOV_DEBUG"]
|
29
|
+
warn(e)
|
30
|
+
end
|
31
|
+
|
32
|
+
if attempts > 0
|
33
|
+
attempts -= 1
|
34
|
+
sleep(5)
|
35
|
+
retry
|
36
|
+
end
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
response
|
41
|
+
end
|
42
|
+
|
43
|
+
def post_runtime_report(body)
|
44
|
+
post("/v1/runtime_reports", body)
|
45
|
+
rescue Faraday::Error
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def post_test_report(body)
|
50
|
+
post("/v1/test_reports", body)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "singleton"
|
3
|
+
require_relative "api_connection"
|
4
|
+
require_relative "coverage_peek_delta"
|
5
|
+
|
6
|
+
# Bootstrap the thread that starts Coverage module data collection.
|
7
|
+
#
|
8
|
+
# Every 60 secs or so:
|
9
|
+
# 1. Get the coverage peek_result delta
|
10
|
+
# 2. Parse source code and determine which method being run
|
11
|
+
# 3. Get controller actions' hits
|
12
|
+
# 4. Call the reporter
|
13
|
+
#
|
14
|
+
# Most of the codes here are inspired by danmayer's coverband gem.
|
15
|
+
#
|
16
|
+
module OyenCov
|
17
|
+
class Background
|
18
|
+
@loop_interval = 60 # seconds, can be set from server
|
19
|
+
@semaphore = Mutex.new
|
20
|
+
@thread = nil
|
21
|
+
@reporter = nil
|
22
|
+
@api_conn = OyenCov::APIConnection.instance
|
23
|
+
@config = OyenCov.config
|
24
|
+
|
25
|
+
def self.start
|
26
|
+
puts "Hello #{Rails.env}"
|
27
|
+
|
28
|
+
# Start `Coverage` as soon as possible before other codes are loaded
|
29
|
+
CoveragePeekDelta.start
|
30
|
+
|
31
|
+
@thread = Thread.new {
|
32
|
+
# Check with backend to get parameters
|
33
|
+
sleep(3)
|
34
|
+
clearance = @api_conn.get_data_submission_clearance
|
35
|
+
|
36
|
+
if clearance.nil?
|
37
|
+
puts "Unable to obtain oyencov submission clearance. Stopping OyenCov background thread."
|
38
|
+
Thread.stop
|
39
|
+
end
|
40
|
+
|
41
|
+
if ENV["OYENCOV_DEBUG"]
|
42
|
+
puts(clearance.body)
|
43
|
+
end
|
44
|
+
|
45
|
+
@config.mode == "production" && loop do
|
46
|
+
sleep(@loop_interval + 3 - rand(6))
|
47
|
+
new_method_hits = CoveragePeekDelta.snapshot_delta
|
48
|
+
new_controller_hits = ControllerTracking.snapshot_and_reset!
|
49
|
+
|
50
|
+
puts new_method_hits
|
51
|
+
|
52
|
+
runtime_report = {
|
53
|
+
git_commit_sha: @config.release,
|
54
|
+
controller_action_hits: new_controller_hits,
|
55
|
+
method_hits: new_method_hits
|
56
|
+
}
|
57
|
+
response = @api_conn.post_runtime_report(runtime_report)
|
58
|
+
|
59
|
+
if response && response.body["status"] == "ok"
|
60
|
+
puts "[OyenOnsen] POST runtime_report ok."
|
61
|
+
else
|
62
|
+
warn "[OyenOnsen] POST runtime_report failed. Stopping background thread."
|
63
|
+
Thread.stop
|
64
|
+
end
|
65
|
+
end # loop
|
66
|
+
}
|
67
|
+
|
68
|
+
@thread.run
|
69
|
+
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# If production/staging etc, we can exit without further processing.
|
74
|
+
# For `test`, persist controller report.
|
75
|
+
def self.stop
|
76
|
+
@thread.stop
|
77
|
+
end
|
78
|
+
|
79
|
+
private_class_method
|
80
|
+
end
|
81
|
+
end
|
data/lib/oyencov/cli.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require "thor"
|
2
|
+
require_relative "./api_connection"
|
3
|
+
require_relative "./test_report_merger"
|
4
|
+
require_relative "./simplecov_resultset_translator"
|
5
|
+
|
6
|
+
# Bootstrapped from `bin/oyencov`
|
7
|
+
#
|
8
|
+
#
|
9
|
+
module OyenCov
|
10
|
+
class CLI < Thor
|
11
|
+
desc "translate_simplecov coverage/.resultset.json", "err"
|
12
|
+
long_desc <<~TEXT
|
13
|
+
If you have parallel test jobs, this command is to be run right after
|
14
|
+
finishing the tests, and before artifacts are uploaded.
|
15
|
+
|
16
|
+
Assumes the --coverage-dir path contains both the simplecov .resultset.json,
|
17
|
+
and the oyencov-resultset.json. It parses the .resultset.json file
|
18
|
+
generated by simplecov, and append the translated information into the
|
19
|
+
pre-existing oyencov-resultset.json.
|
20
|
+
TEXT
|
21
|
+
# option :coverage_dir, default: "coverage"
|
22
|
+
option :simplecov_json_path, default: "coverage/.resultset.json"
|
23
|
+
option :oyencov_json_path, default: "coverage/oyencov-resultset.json"
|
24
|
+
option :dry_run, type: :boolean, default: false
|
25
|
+
def translate_simplecov
|
26
|
+
oyencov_json_path = Dir.pwd + "/" + options[:oyencov_json_path]
|
27
|
+
simplecov_json_path = Dir.pwd + "/" + options[:simplecov_json_path]
|
28
|
+
|
29
|
+
# Find existing resultset files
|
30
|
+
if File.exist?(oyencov_json_path)
|
31
|
+
oyencov_json = File.read(oyencov_json_path)
|
32
|
+
else
|
33
|
+
warn("Could not find existing oyencov-resultset.json at #{oyencov_json_path}")
|
34
|
+
exit(1)
|
35
|
+
end
|
36
|
+
|
37
|
+
warn "Starting to translate simplecov"
|
38
|
+
|
39
|
+
if File.exist?(simplecov_json_path)
|
40
|
+
simplecov_translated_json = OyenCov::SimplecovResultsetTranslator.translate(simplecov_json_path)
|
41
|
+
else
|
42
|
+
warn("Could not find existing simplecov's .resultset.json at #{simplecov_json_path}")
|
43
|
+
exit(1)
|
44
|
+
end
|
45
|
+
|
46
|
+
warn "AFTER translate simplecov"
|
47
|
+
|
48
|
+
# Attempt merging
|
49
|
+
oyencov_resultset = JSON.parse(oyencov_json)
|
50
|
+
oyencov_resultset = oyencov_resultset.merge("method_hits" => simplecov_translated_json)
|
51
|
+
|
52
|
+
# Persist, or dry run stdout?
|
53
|
+
new_oyencov_resultset_json = JSON.pretty_generate(oyencov_resultset)
|
54
|
+
if options[:dry_run]
|
55
|
+
puts new_oyencov_resultset_json
|
56
|
+
else
|
57
|
+
File.write(oyencov_json_path, new_oyencov_resultset_json)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "submit tmp/coverage-jsons-*/oyencov-resultset.json",
|
62
|
+
"submits the oyencov resultsets data"
|
63
|
+
option :files, type: :array, required: true
|
64
|
+
option :git_commit_sha, required: true
|
65
|
+
option :token
|
66
|
+
def submit
|
67
|
+
resultset_files = options[:files]
|
68
|
+
ENV["OYENCOV_DEBUG"] && if resultset_files.any?
|
69
|
+
puts "Found #{resultset_files.join(", ")}"
|
70
|
+
else
|
71
|
+
puts "No resultset files found"
|
72
|
+
exit 1
|
73
|
+
end
|
74
|
+
|
75
|
+
collated_report = OyenCov::TestReportMerger
|
76
|
+
.collate_job_reports(options[:files])
|
77
|
+
.merge({
|
78
|
+
"git_commit_sha" => options[:git_commit_sha]
|
79
|
+
})
|
80
|
+
|
81
|
+
# puts JSON.pretty_generate(collated_report)
|
82
|
+
|
83
|
+
# Add metadaata
|
84
|
+
|
85
|
+
ENV["OYENCOV_API_KEY"] ||= options[:token]
|
86
|
+
unless ENV["OYENCOV_API_KEY"]
|
87
|
+
warn "API token not set. Unable to submit."
|
88
|
+
exit(1)
|
89
|
+
end
|
90
|
+
|
91
|
+
connection = OyenCov::APIConnection.instance
|
92
|
+
post_response = connection.post_test_report(collated_report)
|
93
|
+
|
94
|
+
puts post_response.body
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# We encourage configuring OyenOnsen through environment variables.
|
2
|
+
#
|
3
|
+
# But some can be set through config/ if they are meant to be uniform across environments.
|
4
|
+
module OyenCov
|
5
|
+
class Configuration
|
6
|
+
ENV_PARAMETERS = %w[
|
7
|
+
API_KEY
|
8
|
+
API_URL
|
9
|
+
MODE
|
10
|
+
RELEASE
|
11
|
+
TEST_REPORTING_DIR
|
12
|
+
TEST_RESULTSET_PATH
|
13
|
+
PROGRAM_NAME
|
14
|
+
]
|
15
|
+
|
16
|
+
attr :api_key, :api_url, :mode, :including_file_paths, :excluding_file_paths, :release, :test_reporting_dir, :test_resultset_path, :program_name
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
reset_to_defaults
|
20
|
+
ENV_PARAMETERS.each do |key|
|
21
|
+
if (envvar_value = ENV["OYENONSEN_#{key}"])
|
22
|
+
instance_variable_set(
|
23
|
+
:"@#{key.downcase}", envvar_value
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset_to_defaults
|
30
|
+
@api_key = nil
|
31
|
+
@api_url = "https://telemetry-api.oyencov.com"
|
32
|
+
@mode = ENV["RAILS_ENV"]
|
33
|
+
@including_file_paths = %w[app lib]
|
34
|
+
@excluding_file_paths = []
|
35
|
+
@release = suggest_release
|
36
|
+
@test_reporting_dir = "coverage/"
|
37
|
+
@test_resultset_path = "coverage/oyencov-resultset.json"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Lots of ideas came from sentry-ruby, thanks to nate berkopec.
|
43
|
+
def suggest_release
|
44
|
+
release = `git rev-parse HEAD ||:`.strip
|
45
|
+
|
46
|
+
if release == "" || release.nil?
|
47
|
+
[".source_version", "REVISION"].each do |version_clue|
|
48
|
+
if File.exist?(Rails.root.join(version_clue))
|
49
|
+
release = File.read(Rails.root.join(version_clue)).strip
|
50
|
+
return release
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
release
|
56
|
+
end
|
57
|
+
|
58
|
+
# We need to know if this is rails, sidekiq, rake task etc
|
59
|
+
def suggest_program_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This module is merely the container data structure.
|
2
|
+
#
|
3
|
+
# The ACTUAL code that tracks controller is in `railtie.rb`
|
4
|
+
module OyenCov
|
5
|
+
module ControllerTracking
|
6
|
+
@hits = {}
|
7
|
+
|
8
|
+
def self.bump(controller_action_name)
|
9
|
+
if @hits[controller_action_name]
|
10
|
+
@hits[controller_action_name] += 1
|
11
|
+
else
|
12
|
+
@hits[controller_action_name] = 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.snapshot_and_reset!
|
17
|
+
current_hits = @hits
|
18
|
+
@hits = {}
|
19
|
+
current_hits
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "coverage"
|
2
|
+
require_relative "method_range_parser"
|
3
|
+
|
4
|
+
# `CoveragePeekDelta` utility is meant to take...
|
5
|
+
#
|
6
|
+
# This class won't be governing the state, that will be handled by `OyenCov::Background`. However this will help strip the project path from the hash keys for cleaner reporting.
|
7
|
+
module OyenCov
|
8
|
+
module CoveragePeekDelta
|
9
|
+
PWD = Dir.pwd
|
10
|
+
|
11
|
+
@@previous_method_hits = {}
|
12
|
+
|
13
|
+
# We go a bit softer here. If there are other libraries starting
|
14
|
+
# coverage then don't throw exception.
|
15
|
+
def self.start
|
16
|
+
reset!
|
17
|
+
|
18
|
+
unless Coverage.running?
|
19
|
+
Coverage.start
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# 1. Filter only the keys with PWD.
|
24
|
+
# 2. `transform_keys` and remove the PWD
|
25
|
+
# 3. Use MRP to get the impt lines.
|
26
|
+
#
|
27
|
+
# @return [Hash] Method name => line num executions diff from last time
|
28
|
+
def self.snapshot_delta
|
29
|
+
current_peek = Coverage.peek_result
|
30
|
+
|
31
|
+
if ENV["OYENONSEN_DEBUG"]
|
32
|
+
$stdout.puts "current_peek size = #{current_peek.size}, keys like: #{current_peek.keys[0, 3]}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Filter into project
|
36
|
+
filtered = current_peek.select do |k, _|
|
37
|
+
/^#{PWD}/o.match?(k)
|
38
|
+
end.transform_keys do |k|
|
39
|
+
k.gsub(/#{PWD}\//o, "")
|
40
|
+
end
|
41
|
+
|
42
|
+
if ENV["OYENONSEN_DEBUG"]
|
43
|
+
$stdout.puts "filtered size = #{filtered.size}, keys like: #{filtered.keys[0, 3]}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Filter inside project to just the paths
|
47
|
+
filtered = filtered.select do |k, _|
|
48
|
+
/^(app|lib)/.match?(k)
|
49
|
+
end
|
50
|
+
|
51
|
+
if ENV["OYENONSEN_DEBUG"]
|
52
|
+
$stdout.puts "filtered size = #{filtered.size}, keys like: #{filtered.keys[0, 3]}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Find the method ranges, set
|
56
|
+
current_method_hits = {}
|
57
|
+
filtered.each_pair do |fpath, line_hits|
|
58
|
+
MethodRangeParser[fpath]&.each_pair do |method_name, line_num|
|
59
|
+
# puts [method_name, line_num, line_hits[line_num]]
|
60
|
+
next if line_num.nil? || line_hits[line_num].nil?
|
61
|
+
current_method_hits[method_name] = line_hits[line_num]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Compare and delta
|
66
|
+
new_method_hits = {}
|
67
|
+
current_method_hits.each_pair do |method_name, counter|
|
68
|
+
if counter.nil?; puts method_name; end
|
69
|
+
new_hits = counter - (@@previous_method_hits[method_name] || 0)
|
70
|
+
if new_hits > 0
|
71
|
+
new_method_hits[method_name] = new_hits
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@@previous_method_hits = current_method_hits
|
76
|
+
new_method_hits
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.reset!
|
80
|
+
@@previous_method_hits = {}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "parser/current"
|
2
|
+
|
3
|
+
# This module helps scanning source code files and get the definition line
|
4
|
+
# ranges, so we can count how many times a method has been executed.
|
5
|
+
module OyenCov
|
6
|
+
class MethodRangeParser < Hash
|
7
|
+
@@parsed_files = {}
|
8
|
+
|
9
|
+
def self.parsed_files
|
10
|
+
@@parsed_files
|
11
|
+
end
|
12
|
+
|
13
|
+
# Check cache
|
14
|
+
def self.[](filepath)
|
15
|
+
@filepath = filepath
|
16
|
+
@@parsed_files[@filepath] ||= parse_file(@filepath)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Considerations:
|
22
|
+
# - Some .rb files do not have valid syntax, we can rescue them. However parser
|
23
|
+
# stills stderr
|
24
|
+
#
|
25
|
+
# @return [Hash<String, >] Hash of methods to their children starting line count. The line count can be used to read how often the method is executed from `Coverage.peek_result`
|
26
|
+
private_class_method def self.parse_file(filepath)
|
27
|
+
traverse_ast(Parser::CurrentRuby.parse(File.read(filepath)))
|
28
|
+
.to_h
|
29
|
+
.select do |k, v|
|
30
|
+
/\.|\#/.match?(k)
|
31
|
+
end.transform_keys do |k|
|
32
|
+
k.gsub(/^::/, "")
|
33
|
+
end
|
34
|
+
rescue Parser::SyntaxError
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
|
38
|
+
private_class_method def self.declaration_name(node)
|
39
|
+
case node.type
|
40
|
+
when :begin then ""
|
41
|
+
when :defs then ".#{node.children[1]}"
|
42
|
+
when :def then "##{node.children[0]}"
|
43
|
+
when :class, :module # traverse
|
44
|
+
current_name_constant_node = node.children[0]
|
45
|
+
full_constant_name = ""
|
46
|
+
until current_name_constant_node.children[0].nil?
|
47
|
+
full_constant_name = "::#{current_name_constant_node.children[1]}#{full_constant_name}"
|
48
|
+
current_name_constant_node = current_name_constant_node.children[0]
|
49
|
+
end
|
50
|
+
"::#{current_name_constant_node.children[1]}#{full_constant_name}"
|
51
|
+
else "Unsupported AST node type: #{node.type}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Integer]
|
56
|
+
private_class_method def self.definition_line_num(node)
|
57
|
+
definition_lines = node.children.find do |i|
|
58
|
+
Parser::AST::Node === i && i.type == :begin
|
59
|
+
end || node.children[-1]
|
60
|
+
|
61
|
+
if definition_lines.nil?
|
62
|
+
nil
|
63
|
+
else
|
64
|
+
definition_lines.loc.first_line
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# this be recursion
|
69
|
+
# return array of ["node_type/namespace::nam#node_name" => [startline, endline]
|
70
|
+
private_class_method def self.traverse_ast(node)
|
71
|
+
unless Parser::AST::Node === node
|
72
|
+
return nil
|
73
|
+
end
|
74
|
+
|
75
|
+
unless %i[begin module class defs def].include?(node.type)
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
|
79
|
+
node_children = node.children.find do |i|
|
80
|
+
Parser::AST::Node === i && i.type == :begin
|
81
|
+
end&.children || [node.children[-1]]
|
82
|
+
|
83
|
+
ownself_name = declaration_name(node)
|
84
|
+
ownself_range = definition_line_num(node)
|
85
|
+
|
86
|
+
children_name_range = []
|
87
|
+
|
88
|
+
node_children&.each do |cnode|
|
89
|
+
if Array === traverse_ast(cnode)
|
90
|
+
children_name_range += traverse_ast(cnode)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
children_name_range.map! do |cnode|
|
95
|
+
["#{ownself_name}#{cnode[0]}", cnode[1]]
|
96
|
+
end
|
97
|
+
|
98
|
+
[[ownself_name, ownself_range]] + children_name_range
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative "background"
|
2
|
+
require_relative "controller_tracking"
|
3
|
+
require_relative "test_reporting"
|
4
|
+
|
5
|
+
module OyenCov
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
initializer "oyencov.configure" do
|
8
|
+
# puts "lib/oyencov/railtie.rb initializer oyencov.configure"
|
9
|
+
OyenCov::Background.start
|
10
|
+
end
|
11
|
+
|
12
|
+
config.after_initialize do
|
13
|
+
# puts "lib/oyencov/railtie.rb config.after_initialize"
|
14
|
+
ActiveSupport::Notifications.subscribe("start_processing.action_controller") do |name, start, finish, id, payload|
|
15
|
+
# puts(payload)
|
16
|
+
ControllerTracking.bump("#{payload[:controller]}##{payload[:action]}")
|
17
|
+
end
|
18
|
+
|
19
|
+
if OyenCov.config.mode == "test"
|
20
|
+
at_exit do
|
21
|
+
OyenCov::TestReporting.persist_controller_actions!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative "./method_range_parser"
|
2
|
+
require_relative "./test_report_merger"
|
3
|
+
|
4
|
+
# This is meant to be run at the end of the fan-out jobs, and the output
|
5
|
+
# artefacts are meant to be persisted for the next job in the workflow.
|
6
|
+
#
|
7
|
+
# It's possible the full codebase isn't pulled in the final summarising
|
8
|
+
# job. User can opt to just `gem install oyencov` and do the collation
|
9
|
+
# and submission with just the gem.
|
10
|
+
#
|
11
|
+
# The code should be similar to `CoveragePeekDelta`.
|
12
|
+
#
|
13
|
+
# A simplecov .resultset.json file looks like this:
|
14
|
+
#
|
15
|
+
# ```
|
16
|
+
# $test_tool (e.g. Minitest, RSpec) # can disregard
|
17
|
+
# "coverage" # constant string
|
18
|
+
# $rb_filepath # root relative paths
|
19
|
+
# [null, 1, 2, null, ...] # raw `Coverage.result` output
|
20
|
+
# ```
|
21
|
+
#
|
22
|
+
module OyenCov
|
23
|
+
module SimplecovResultsetTranslator
|
24
|
+
PWD = Dir.pwd
|
25
|
+
|
26
|
+
# @param [String] Root-relative path to the .resultset.json
|
27
|
+
# @return [String] JSON file
|
28
|
+
def self.translate(resultset_json_path, persist: false)
|
29
|
+
# Open up the JSON
|
30
|
+
resultset = JSON.parse(File.read(resultset_json_path))
|
31
|
+
|
32
|
+
# binding.irb
|
33
|
+
|
34
|
+
# Loop through all the files
|
35
|
+
# Set {"method" => runs, ...}
|
36
|
+
all_methods_hits = resultset[resultset.keys[0]]["coverage"].each_pair.map do |file_path, file_attr|
|
37
|
+
# file_path = file_path.gsub(/#{PWD}\//o, "")
|
38
|
+
line_hits = file_attr["lines"]
|
39
|
+
methods_hits = MethodRangeParser[file_path]&.each_pair&.map do |method_name, line_num|
|
40
|
+
next if line_num.nil? || line_hits[line_num].nil?
|
41
|
+
[method_name, line_hits[line_num]]
|
42
|
+
end&.compact.to_h
|
43
|
+
# methods_hits
|
44
|
+
end.reduce(:merge)
|
45
|
+
|
46
|
+
# Persist to existing oyencov report?
|
47
|
+
if persist
|
48
|
+
OyenCov::TestReportMerger.create_or_append!(method_hits: all_methods_hits)
|
49
|
+
end
|
50
|
+
|
51
|
+
all_methods_hits
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
# Currently Oyencov only tracks
|
4
|
+
#
|
5
|
+
# Simplecov report collation is not handled here.
|
6
|
+
module OyenCov
|
7
|
+
module TestReportMerger
|
8
|
+
def self.create_or_append!(hash_to_merge)
|
9
|
+
resultset_path = OyenCov.config.test_resultset_path
|
10
|
+
|
11
|
+
# Load
|
12
|
+
hash_content = if File.exist?(resultset_path)
|
13
|
+
JSON.parse(File.read(resultset_path))
|
14
|
+
else
|
15
|
+
{}
|
16
|
+
end
|
17
|
+
|
18
|
+
hash_content.merge!(hash_to_merge)
|
19
|
+
|
20
|
+
# Persist
|
21
|
+
File.write(resultset_path, JSON.generate(hash_content))
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [String]
|
25
|
+
# @param [String]
|
26
|
+
# @return [Hash]
|
27
|
+
def self.collate_job_reports(filepath_glob, save_to = nil)
|
28
|
+
# Read and parse their JSONs
|
29
|
+
job_reports_files = Dir.glob(filepath_glob)
|
30
|
+
# binding.irb
|
31
|
+
return if job_reports_files.count == 0
|
32
|
+
|
33
|
+
job_reports = job_reports_files.map do |f|
|
34
|
+
JSON.parse(File.read(f))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add them up
|
38
|
+
collated_report = job_reports.reduce({
|
39
|
+
"controller_action_hits" => {},
|
40
|
+
"method_hits" => {}
|
41
|
+
}) do |i, j|
|
42
|
+
ij = {}
|
43
|
+
i.keys.each do |metric|
|
44
|
+
unless j[metric]
|
45
|
+
ij[metric] = i[metric]
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
case metric
|
50
|
+
when "controller_action_hits", "method_hits"
|
51
|
+
ij[metric] = add_hashes(i[metric], j[metric])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ij
|
56
|
+
end
|
57
|
+
|
58
|
+
# Persist to filesystem as JSON
|
59
|
+
if !!save_to
|
60
|
+
end
|
61
|
+
|
62
|
+
collated_report
|
63
|
+
end
|
64
|
+
|
65
|
+
private_class_method def self.add_hashes(i, j)
|
66
|
+
(i.keys | j.keys).map do |key|
|
67
|
+
[key, (i[key] || 0) + (j[key] || 0)]
|
68
|
+
end.to_h
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
# For use in CI only, one-off export
|
4
|
+
#
|
5
|
+
module OyenCov
|
6
|
+
class TestReporting
|
7
|
+
def self.persist_controller_actions!
|
8
|
+
controller_action_hits = OyenCov::ControllerTracking.snapshot_and_reset!
|
9
|
+
|
10
|
+
# Persist report file
|
11
|
+
test_report_dir = File.expand_path(OyenCov.config.test_reporting_dir)
|
12
|
+
FileUtils.mkdir_p(test_report_dir)
|
13
|
+
test_report_path = OyenCov.config.test_resultset_path
|
14
|
+
|
15
|
+
report_content = {
|
16
|
+
controller_action_hits:
|
17
|
+
}
|
18
|
+
|
19
|
+
report_content_json = JSON.generate(report_content)
|
20
|
+
|
21
|
+
File.open(test_report_path, "w+") do |f|
|
22
|
+
f.puts(report_content_json)
|
23
|
+
end
|
24
|
+
|
25
|
+
puts "[OyenCov] Saved to #{test_report_path}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/oyencov.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative "oyencov/configuration"
|
2
|
+
require_relative "oyencov/simplecov_resultset_translator"
|
3
|
+
|
4
|
+
# For now, support only Rails. We bootstrap from Railtie.
|
5
|
+
module OyenCov
|
6
|
+
VERSION = "0.0.1.pre"
|
7
|
+
|
8
|
+
def self.config
|
9
|
+
@config ||= OyenCov::Configuration.new
|
10
|
+
end
|
11
|
+
|
12
|
+
if defined?(Rails::Railtie) && ENV["OYENONSEN_API_KEY"]
|
13
|
+
require_relative "oyencov/railtie"
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oyencov
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anonoz Chong
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-08-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: standard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: faraday
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: parser
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2'
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '4.0'
|
93
|
+
type: :runtime
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '2'
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '4.0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: thor
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
description: Runtime and test reporters.
|
118
|
+
email: anonoz@oyencov.com
|
119
|
+
executables:
|
120
|
+
- oyencov
|
121
|
+
extensions: []
|
122
|
+
extra_rdoc_files: []
|
123
|
+
files:
|
124
|
+
- bin/oyencov
|
125
|
+
- lib/oyencov.rb
|
126
|
+
- lib/oyencov/api_connection.rb
|
127
|
+
- lib/oyencov/background.rb
|
128
|
+
- lib/oyencov/cli.rb
|
129
|
+
- lib/oyencov/configuration.rb
|
130
|
+
- lib/oyencov/controller_tracking.rb
|
131
|
+
- lib/oyencov/coverage_peek_delta.rb
|
132
|
+
- lib/oyencov/method_range_parser.rb
|
133
|
+
- lib/oyencov/railtie.rb
|
134
|
+
- lib/oyencov/simplecov_resultset_translator.rb
|
135
|
+
- lib/oyencov/test_report_merger.rb
|
136
|
+
- lib/oyencov/test_reporting.rb
|
137
|
+
homepage: https://www.oyencov.com
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: 3.1.0
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">"
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: 1.3.1
|
155
|
+
requirements: []
|
156
|
+
rubygems_version: 3.3.7
|
157
|
+
signing_key:
|
158
|
+
specification_version: 4
|
159
|
+
summary: Client-side telemetry
|
160
|
+
test_files: []
|