oyencov 0.0.1.pre
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/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: []
|