trace2 1.0.1
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/trace2 +6 -0
- data/ext/trace2/extconf.rb +4 -0
- data/lib/trace2.rb +23 -0
- data/lib/trace2/2.5/trace2.so +0 -0
- data/lib/trace2/2.6/trace2.so +0 -0
- data/lib/trace2/2.7/trace2.so +0 -0
- data/lib/trace2/class_lister.rb +29 -0
- data/lib/trace2/class_use.rb +103 -0
- data/lib/trace2/class_use_factory.rb +32 -0
- data/lib/trace2/dot_wrapper.rb +40 -0
- data/lib/trace2/event_processor.rb +75 -0
- data/lib/trace2/executable_runner.rb +42 -0
- data/lib/trace2/filter_parser.rb +66 -0
- data/lib/trace2/graph_generator.rb +42 -0
- data/lib/trace2/option_parser.rb +54 -0
- data/lib/trace2/options.rb +134 -0
- data/lib/trace2/query_use.rb +54 -0
- data/lib/trace2/reporting_tools_factory.rb +50 -0
- data/lib/trace2/runner.rb +65 -0
- data/lib/trace2/trace2.so +0 -0
- data/lib/trace2/version.rb +5 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4f7e6874b292e5edcc36ba88b1f9f13798765d1682852a64b0be5667807a3b15
|
4
|
+
data.tar.gz: a3548111fdb9a4efb25f4303df014a9d3e1dc974f0dae15a517f2bafe505e4da
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 35d7b742f6f9908f9ccc168551f8e7318bcde9d710cac0cb2e5eb1ffbccae0cf93ea4e1b3634e335f1bae412c2f1b0914f84c93cef4a94b0f2fd66e58cca5e6b
|
7
|
+
data.tar.gz: efb0c1e6c3a1fa059ee6d4ae4aa0a528e374f34771d6502782712196f6bfa25c30afdda57c1eecf730690b08ae57d1084325a4d8bff6e568962490a88d7e3f8b
|
data/bin/trace2
ADDED
data/lib/trace2.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
5
|
+
require "trace2/#{Regexp.last_match(1)}/trace2"
|
6
|
+
rescue LoadError
|
7
|
+
require 'trace2/trace2'
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'trace2/class_lister'
|
11
|
+
require 'trace2/class_use'
|
12
|
+
require 'trace2/class_use_factory'
|
13
|
+
require 'trace2/event_processor'
|
14
|
+
require 'trace2/executable_runner'
|
15
|
+
require 'trace2/filter_parser'
|
16
|
+
require 'trace2/option_parser'
|
17
|
+
require 'trace2/options'
|
18
|
+
require 'trace2/query_use'
|
19
|
+
require 'trace2/runner'
|
20
|
+
require 'trace2/version'
|
21
|
+
require 'trace2/graph_generator'
|
22
|
+
require 'trace2/reporting_tools_factory'
|
23
|
+
require 'trace2/dot_wrapper'
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Trace2
|
6
|
+
# Responsable for listing all accessed classes
|
7
|
+
# along with their dependencies
|
8
|
+
class ClassLister
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_accessor :classes_uses
|
12
|
+
|
13
|
+
def_delegators :@trace_point, :enable
|
14
|
+
|
15
|
+
def initialize(event_processor, trace_point = TracePoint)
|
16
|
+
@event_processor = event_processor
|
17
|
+
@classes_uses = []
|
18
|
+
@trace_point = trace_point.new(*@event_processor.events) do |tp|
|
19
|
+
@event_processor.process_event(tp)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def disable
|
24
|
+
@trace_point.disable
|
25
|
+
@event_processor.aggregate_uses
|
26
|
+
@classes_uses = @event_processor.classes_uses
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Registers how a class was used during run time
|
5
|
+
class ClassUse
|
6
|
+
attr_reader :name, :method, :stack_level, :path, :line, :callees, :event
|
7
|
+
attr_accessor :caller_class
|
8
|
+
|
9
|
+
def initialize(params)
|
10
|
+
@name = params[:name]
|
11
|
+
@method = params[:method]
|
12
|
+
@caller_class = params[:caller_class]
|
13
|
+
@stack_level = params[:stack_level]
|
14
|
+
@path = params[:path]
|
15
|
+
@line = params[:line]
|
16
|
+
@callees = params[:callees] || []
|
17
|
+
@event = params[:event]
|
18
|
+
end
|
19
|
+
|
20
|
+
def callers_stack(options = {})
|
21
|
+
curr_class = caller_class
|
22
|
+
callers_stack = []
|
23
|
+
until curr_class.nil?
|
24
|
+
curr_caller = run_options(curr_class, options)
|
25
|
+
callers_stack.push(curr_caller) unless curr_caller.nil?
|
26
|
+
curr_class = curr_class.caller_class
|
27
|
+
end
|
28
|
+
callers_stack
|
29
|
+
end
|
30
|
+
|
31
|
+
def matches_method?(methods_names)
|
32
|
+
methods_names.any? { |method_name| method.match(method_name) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def matches_name?(classes_names)
|
36
|
+
classes_names.any? { |class_name| name.match(class_name) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def matches_path?(paths_patterns)
|
40
|
+
paths_patterns.any? { |path_pattern| path.match(path_pattern) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def matches_bottom_of_stack?(is_bottom)
|
44
|
+
caller_class.nil? == is_bottom
|
45
|
+
end
|
46
|
+
|
47
|
+
def matches_top_of_stack?(is_top)
|
48
|
+
callees.empty? == is_top
|
49
|
+
end
|
50
|
+
|
51
|
+
def matches_caller_class?(caller_attributes)
|
52
|
+
callers_stack.any? do |current_caller|
|
53
|
+
valid_caller?(current_caller, caller_attributes)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_callee(callee)
|
58
|
+
callees << callee
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def run_options(class_use, options)
|
64
|
+
options.reduce(class_use) do |acc_use, option|
|
65
|
+
option_method = "run_#{option.first}"
|
66
|
+
send(option_method, acc_use, option.last) unless acc_use.nil?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def run_selector(class_use, selector)
|
71
|
+
return class_use if selector.nil?
|
72
|
+
|
73
|
+
selector.filter(class_use)
|
74
|
+
end
|
75
|
+
|
76
|
+
def run_compact(class_use, compact)
|
77
|
+
compact ? compact_callers(class_use) : class_use
|
78
|
+
end
|
79
|
+
|
80
|
+
def compact_callers(class_use)
|
81
|
+
class_use.dup.tap do |compacted_use|
|
82
|
+
compacted_use.caller_class = nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def valid_caller?(current_caller, caller_attributes)
|
87
|
+
caller_attributes.all? do |attribute, values|
|
88
|
+
validation = "matches_#{attribute}?"
|
89
|
+
current_caller.send(validation, values)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def respond_to_missing?(method, _)
|
94
|
+
method.match?(/^matches_\S+?\?$/)
|
95
|
+
end
|
96
|
+
|
97
|
+
def method_missing(method, *args, &block)
|
98
|
+
return true if respond_to_missing?(method, args)
|
99
|
+
|
100
|
+
super
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Builds a ClassUse from TracePoint
|
5
|
+
class ClassUseFactory
|
6
|
+
CLASS_POINTER_FORMAT = '0x[0-9abcdef]+'
|
7
|
+
|
8
|
+
def self.build(trace_point:, caller_class:, stack_level:)
|
9
|
+
ClassUse.new(
|
10
|
+
trace_point_params(trace_point)
|
11
|
+
.merge(caller_class: caller_class)
|
12
|
+
.merge(stack_level: stack_level)
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.class_name(trace_point)
|
17
|
+
Trace2::NameFinder.class_name(trace_point.self)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def trace_point_params(trace_point)
|
22
|
+
{
|
23
|
+
name: class_name(trace_point),
|
24
|
+
method: trace_point.callee_id.to_s,
|
25
|
+
path: trace_point.path,
|
26
|
+
line: trace_point.lineno,
|
27
|
+
event: trace_point.event
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# A class to abstract the usage of the dot executable
|
5
|
+
# that comes with graphviz
|
6
|
+
class DotWrapper
|
7
|
+
def initialize(kernel: Kernel)
|
8
|
+
@kernel = kernel
|
9
|
+
end
|
10
|
+
|
11
|
+
def render_graph(input_path, output_path, format)
|
12
|
+
return warn_graphviz_not_installed unless graphviz_installed?
|
13
|
+
|
14
|
+
execute_graph_render(input_path, output_path, format)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
DOT_VERSION = 'dot -V'
|
20
|
+
DOT_RENDER = 'dot %s -T%s -o %s'
|
21
|
+
|
22
|
+
def graphviz_installed?
|
23
|
+
@kernel.system(DOT_VERSION)
|
24
|
+
end
|
25
|
+
|
26
|
+
def warn_graphviz_not_installed
|
27
|
+
@kernel.puts 'Graphviz is not installed on the system. '\
|
28
|
+
'Skipping graph rendering...'
|
29
|
+
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def execute_graph_render(input_path, output_path, format)
|
34
|
+
graph_render_command = format(
|
35
|
+
DOT_RENDER, input_path, format, output_path
|
36
|
+
)
|
37
|
+
@kernel.system(graph_render_command)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Processes a TracePoint event, generating an array
|
5
|
+
# of ClassUse
|
6
|
+
class EventProcessor
|
7
|
+
attr_accessor :classes_uses
|
8
|
+
EVENTS = %i[call b_call].freeze
|
9
|
+
|
10
|
+
def initialize(filter_by, kernel: Kernel, query_use: QueryUse,
|
11
|
+
class_use_factory: ClassUseFactory)
|
12
|
+
@callers_stack = []
|
13
|
+
@classes_uses = []
|
14
|
+
@class_use_factory = class_use_factory
|
15
|
+
@kernel = kernel
|
16
|
+
@selector = query_use.where(filter_by)
|
17
|
+
@stack_level = @kernel.caller.length
|
18
|
+
end
|
19
|
+
|
20
|
+
def aggregate_uses
|
21
|
+
@classes_uses = @callers_stack
|
22
|
+
.map { |caller_class| @selector.filter(caller_class) }
|
23
|
+
.concat(@classes_uses)
|
24
|
+
.reject(&:nil?)
|
25
|
+
@callers_stack = []
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_event(trace_point)
|
29
|
+
@stack_level = @kernel.caller.length
|
30
|
+
|
31
|
+
remove_exited_callers_from_stack
|
32
|
+
current_class_use = build_class_use(trace_point, current_caller)
|
33
|
+
update_top_of_stack(current_class_use)
|
34
|
+
|
35
|
+
@callers_stack.unshift(current_class_use)
|
36
|
+
end
|
37
|
+
|
38
|
+
def events
|
39
|
+
EVENTS
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def remove_exited_callers_from_stack
|
45
|
+
while class_exited?(@callers_stack.first)
|
46
|
+
@classes_uses << @selector.filter(@callers_stack.shift)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_class_use(trace_point, caller_class)
|
51
|
+
@class_use_factory.build(
|
52
|
+
trace_point: trace_point,
|
53
|
+
stack_level: @stack_level,
|
54
|
+
caller_class: caller_class
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_top_of_stack(callee)
|
59
|
+
caller_class = callee.caller_class
|
60
|
+
return if caller_class.nil?
|
61
|
+
|
62
|
+
caller_class.add_callee(callee) unless @selector.filter(callee).nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
def class_exited?(current_class)
|
66
|
+
current_class && current_class.stack_level >= @stack_level
|
67
|
+
end
|
68
|
+
|
69
|
+
def current_caller
|
70
|
+
@callers_stack.drop_while do |possible_caller|
|
71
|
+
possible_caller.nil? || @selector.filter(possible_caller).nil?
|
72
|
+
end.first
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Class that runs the executable whose runtime will be
|
5
|
+
# used to analyze the relationship between classes
|
6
|
+
class ExecutableRunner
|
7
|
+
def initialize(system_path: ENV['PATH'], argv: ARGV)
|
8
|
+
@system_path = system_path
|
9
|
+
@argv = argv
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(executable, args)
|
13
|
+
update_argv(args)
|
14
|
+
executable_path = find_executable(executable)
|
15
|
+
load(executable_path)
|
16
|
+
rescue SyntaxError
|
17
|
+
raise SyntaxError, "#{executable} is not a valid Ruby script"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def find_executable(executable)
|
23
|
+
possible_paths = @system_path.split(':').unshift('.').map do |path|
|
24
|
+
"#{path}/#{executable}"
|
25
|
+
end
|
26
|
+
|
27
|
+
executable_path = possible_paths.find do |path|
|
28
|
+
File.exist?(path)
|
29
|
+
end
|
30
|
+
|
31
|
+
if executable_path.nil?
|
32
|
+
raise ArgumentError, "#{executable} does not exist"
|
33
|
+
end
|
34
|
+
|
35
|
+
executable_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_argv(args)
|
39
|
+
@argv.clear.concat(args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Class that queries ClassUse by parameters
|
5
|
+
# passed as a hash
|
6
|
+
class FilterParser
|
7
|
+
FILTER = 'filter'
|
8
|
+
VALIDATE = 'validate_%s'
|
9
|
+
PARSE = 'parse_%s'
|
10
|
+
|
11
|
+
def parse(filters)
|
12
|
+
@filters = filters
|
13
|
+
@parsed_filter = [filters.length]
|
14
|
+
parse_filters
|
15
|
+
@parsed_filter.map(&:to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse_filters
|
21
|
+
@filters.each do |filter|
|
22
|
+
@parsed_filter.push(filter.length)
|
23
|
+
filter.each do |action, validations|
|
24
|
+
parse_validations(validations)
|
25
|
+
@parsed_filter.push(action.to_s)
|
26
|
+
end
|
27
|
+
@parsed_filter.push(FILTER)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_validations(validations)
|
32
|
+
@parsed_filter.push(validations.length)
|
33
|
+
validations.each do |validation|
|
34
|
+
@parsed_filter.push(validation.length)
|
35
|
+
parse_validation(validation)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_validation(validation)
|
40
|
+
validation.each do |attribute, values|
|
41
|
+
@parsed_filter.push(format(VALIDATE, attribute))
|
42
|
+
current_parser = parse_values_method(values)
|
43
|
+
if respond_to?(current_parser, true)
|
44
|
+
send(current_parser, values)
|
45
|
+
else
|
46
|
+
@parsed_filter.push(1)
|
47
|
+
@parsed_filter.push(values.to_s)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_values_method(values)
|
53
|
+
format(PARSE, values.class.to_s.downcase)
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_array(values)
|
57
|
+
@parsed_filter.push(values.length)
|
58
|
+
@parsed_filter.concat(values)
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_hash(values)
|
62
|
+
@parsed_filter.push(values.keys.length)
|
63
|
+
parse_validation(values)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Generates a graph in .dot format from an array of
|
5
|
+
# ClassUse
|
6
|
+
class GraphGenerator
|
7
|
+
def run(output_path, class_lister)
|
8
|
+
parsed_classes = parse_classes(class_lister.classes_uses)
|
9
|
+
File.write(output_path, base_graph(parsed_classes))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def base_graph(parsed_classes)
|
15
|
+
<<~FILE
|
16
|
+
digraph {
|
17
|
+
#{parsed_classes}
|
18
|
+
}
|
19
|
+
FILE
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_classes(classes_uses)
|
23
|
+
classes_uses.flat_map do |class_use|
|
24
|
+
caller_relationship(class_use) + callee_relationships(class_use)
|
25
|
+
end.uniq.join("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
def caller_relationship(class_use)
|
29
|
+
return [] unless class_use.caller_class
|
30
|
+
|
31
|
+
[to_graph(class_use.caller_class, class_use)]
|
32
|
+
end
|
33
|
+
|
34
|
+
def callee_relationships(class_use)
|
35
|
+
class_use.callees.map { |callee| to_graph(class_use, callee) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_graph(caller_class, callee)
|
39
|
+
"\s\s\"#{caller_class.name}\" -> \"#{callee.name}\""
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module Trace2
|
6
|
+
# Class that parses the options that will be used
|
7
|
+
# by trace2
|
8
|
+
class OptionParser < ::OptionParser
|
9
|
+
attr_reader :options_keys
|
10
|
+
|
11
|
+
def initialize(banner = nil, width = 32, indent = ' ' * 4)
|
12
|
+
@options_keys = {}
|
13
|
+
super(banner, width, indent)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_option(short: nil, long: nil, description: [])
|
17
|
+
@options_keys.merge!(**option_hash(short), **option_hash(long))
|
18
|
+
options = [short, long].compact
|
19
|
+
on(*options, *description) do |option_value|
|
20
|
+
yield option_value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def split_executables(args)
|
25
|
+
second_executable = second_executable_arguments(args)
|
26
|
+
|
27
|
+
[args.shift(args.length - second_executable.length), second_executable]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def second_executable_arguments(args)
|
33
|
+
argument = false
|
34
|
+
args.drop_while do |arg|
|
35
|
+
accepts_argument = @options_keys[arg.to_sym]
|
36
|
+
if accepts_argument.nil? && !argument
|
37
|
+
false
|
38
|
+
else
|
39
|
+
argument = accepts_argument
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def option_hash(option)
|
46
|
+
return {} if option.nil?
|
47
|
+
|
48
|
+
arguments = option.split
|
49
|
+
option_name = arguments.first
|
50
|
+
has_arguments = arguments.length > 1
|
51
|
+
{ option_name.to_sym => has_arguments }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Defines and returns options for Trace2's runner,
|
5
|
+
# given the arguments that were passed
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
class Options
|
8
|
+
def self.parse(args)
|
9
|
+
new.parse(args)
|
10
|
+
end
|
11
|
+
|
12
|
+
# rubocop:disable Metrics/MethodLength
|
13
|
+
def initialize(option_parser: Trace2::OptionParser.new, kernel: Kernel)
|
14
|
+
@options = default_options
|
15
|
+
@kernel = kernel
|
16
|
+
@option_parser = option_parser
|
17
|
+
options_banner
|
18
|
+
help_option
|
19
|
+
version_option
|
20
|
+
filter_option
|
21
|
+
output_path_option
|
22
|
+
type_option
|
23
|
+
format_option
|
24
|
+
manual_option
|
25
|
+
end
|
26
|
+
# rubocop:enable Metrics/MethodLength
|
27
|
+
|
28
|
+
def parse(args)
|
29
|
+
trace2, executable_args = @option_parser.split_executables(args)
|
30
|
+
executable = executable_args.shift
|
31
|
+
|
32
|
+
@option_parser.parse(trace2)
|
33
|
+
|
34
|
+
raise_missing_executable if executable.nil?
|
35
|
+
|
36
|
+
@options.merge(executable: executable, args: executable_args)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def default_options
|
42
|
+
{
|
43
|
+
tools_type: :native,
|
44
|
+
automatic_render: true,
|
45
|
+
graph_format: 'pdf',
|
46
|
+
output_path: 'trace2_report.dot',
|
47
|
+
filter_path: '.trace2.yml'
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def options_banner
|
52
|
+
@option_parser.banner = 'Usage: trace2 [options] ' \
|
53
|
+
'RUBY_EXECUTABLE [executable options]'
|
54
|
+
end
|
55
|
+
|
56
|
+
def help_option
|
57
|
+
@option_parser.add_option(short: '-h',
|
58
|
+
long: '--help',
|
59
|
+
description: 'Display help') do
|
60
|
+
puts @option_parser.to_s
|
61
|
+
exit_runner
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def version_option
|
66
|
+
@option_parser.add_option(short: '-v',
|
67
|
+
long: '--version',
|
68
|
+
description: 'Show trace2 version') do
|
69
|
+
puts VERSION
|
70
|
+
exit_runner
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def filter_option
|
75
|
+
filter_description = 'Specify a filter file. Defaults to .trace2.yml'
|
76
|
+
@option_parser.add_option(description: filter_description,
|
77
|
+
long: '--filter FILTER_PATH') do |filter|
|
78
|
+
@options[:filter_path] = filter
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def output_path_option
|
83
|
+
output_path = ['Output path for the report file. Defaults to',
|
84
|
+
'./trace2_report.yml']
|
85
|
+
@option_parser.add_option(short: '-o OUTPUT_PATH',
|
86
|
+
description: output_path,
|
87
|
+
long: '--output OUTPUT_PATH') do |output|
|
88
|
+
@options[:output_path] = output
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def type_option
|
93
|
+
tools_type = ['Type of the tools that will be used to generate the',
|
94
|
+
'relationship between classes. Possible values:',
|
95
|
+
'ruby or native. Defaults to native.']
|
96
|
+
|
97
|
+
@option_parser.add_option(short: '-t TOOLS_TYPE',
|
98
|
+
description: tools_type,
|
99
|
+
long: '--type TOOLS_TYPE') do |type|
|
100
|
+
@options[:tools_type] = type.to_sym
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def manual_option
|
105
|
+
run_manually = 'Don\'t try to render the relationships ' \
|
106
|
+
'graph automatically'
|
107
|
+
@option_parser.add_option(short: '-m',
|
108
|
+
description: run_manually,
|
109
|
+
long: '--manual') do
|
110
|
+
@options[:automatic_render] = false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def format_option
|
115
|
+
output_format = ['Format that will be used to render the relationship\'s',
|
116
|
+
'graph. Has no effect if the manual option is set.',
|
117
|
+
'Defaults to pdf.']
|
118
|
+
@option_parser.add_option(long: '--format FORMAT',
|
119
|
+
description: output_format) do |format|
|
120
|
+
@options[:graph_format] = format
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def raise_missing_executable
|
125
|
+
raise ArgumentError, 'an executable or ruby script name'\
|
126
|
+
' must be passed as argument'
|
127
|
+
end
|
128
|
+
|
129
|
+
def exit_runner
|
130
|
+
@kernel.exit
|
131
|
+
end
|
132
|
+
end
|
133
|
+
# rubocop:enable Metrics/ClassLength
|
134
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Class that queries ClassUse by parameters
|
5
|
+
# passed as a hash
|
6
|
+
class QueryUse
|
7
|
+
attr_reader :classes_uses
|
8
|
+
|
9
|
+
def initialize(queries)
|
10
|
+
@queries = queries
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.where(queries)
|
14
|
+
QueryUse.new(queries)
|
15
|
+
end
|
16
|
+
|
17
|
+
def filter(class_use)
|
18
|
+
class_use if class_use && valid_use?(class_use)
|
19
|
+
end
|
20
|
+
|
21
|
+
def select(classes_uses)
|
22
|
+
classes_uses.select do |class_use|
|
23
|
+
valid_use?(class_use)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def valid_use?(class_use)
|
30
|
+
@queries.all? do |query|
|
31
|
+
query.all? do |action, validations|
|
32
|
+
send(action, matches_validations?(class_use, validations))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def allow(query_result)
|
38
|
+
query_result
|
39
|
+
end
|
40
|
+
|
41
|
+
def reject(query_result)
|
42
|
+
!query_result
|
43
|
+
end
|
44
|
+
|
45
|
+
def matches_validations?(class_use, validations)
|
46
|
+
validations.any? do |validation|
|
47
|
+
validation.all? do |attribute, values|
|
48
|
+
attribute_match = "matches_#{attribute}?"
|
49
|
+
class_use.send(attribute_match, values)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trace2
|
4
|
+
# Builds a ClassLister and GraphGenerator instance depending on the type
|
5
|
+
# that is passed.
|
6
|
+
class ReportingToolsFactory
|
7
|
+
def initialize(class_lister: ClassLister)
|
8
|
+
@class_lister = class_lister
|
9
|
+
end
|
10
|
+
|
11
|
+
def build(filter, type: :native)
|
12
|
+
@type = type
|
13
|
+
{
|
14
|
+
class_lister: build_class_lister(filter),
|
15
|
+
graph_generator: type_options[:graph_generator]
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
Trace2::NATIVE = {
|
22
|
+
event_processor: Trace2::EventProcessorC,
|
23
|
+
filter_parser: Trace2::FilterParser.new,
|
24
|
+
graph_generator: Trace2::GraphGeneratorC.new
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
Trace2::RUBY = {
|
28
|
+
event_processor: Trace2::EventProcessor,
|
29
|
+
filter_parser: nil,
|
30
|
+
graph_generator: Trace2::GraphGenerator.new
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
def build_class_lister(filter)
|
34
|
+
parsed_filter = build_filter(filter)
|
35
|
+
event_processor = type_options[:event_processor].new(parsed_filter)
|
36
|
+
@class_lister.new(event_processor)
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_filter(unparsed_filter)
|
40
|
+
filter_parser = type_options[:filter_parser]
|
41
|
+
return filter_parser.parse(unparsed_filter) if filter_parser
|
42
|
+
|
43
|
+
unparsed_filter
|
44
|
+
end
|
45
|
+
|
46
|
+
def type_options
|
47
|
+
Object.const_get("Trace2::#{@type.upcase}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Trace2
|
6
|
+
# Base class for trace2's executable
|
7
|
+
class Runner
|
8
|
+
def self.run(args: ARGV, options: Options.new)
|
9
|
+
options_hash = options.parse(args)
|
10
|
+
new(options_hash).run
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@args = options[:args]
|
15
|
+
@executable = options[:executable]
|
16
|
+
@output_path = options.fetch(:output_path)
|
17
|
+
@executable_runner = options[:executable_runner] || ExecutableRunner.new
|
18
|
+
@render_graph_automatically = options.fetch(:automatic_render, false)
|
19
|
+
@graph_format = options[:graph_format]
|
20
|
+
@filter_path = options[:filter_path]
|
21
|
+
@dot_wrapper = options.fetch(:dot_wrapper, DotWrapper.new)
|
22
|
+
build_class_lister(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
at_exit { end_class_listing }
|
27
|
+
class_lister.enable
|
28
|
+
executable_runner.run(executable, args)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :args, :class_lister, :executable, :executable_runner,
|
34
|
+
:output_path, :graph_generator, :render_graph_automatically,
|
35
|
+
:dot_wrapper
|
36
|
+
|
37
|
+
def build_class_lister(options)
|
38
|
+
filter = load_filter
|
39
|
+
tools = options.fetch(:reporting_tools_factory, ReportingToolsFactory.new)
|
40
|
+
.build(filter, type: options[:tools_type])
|
41
|
+
|
42
|
+
@class_lister = tools[:class_lister]
|
43
|
+
@graph_generator = tools[:graph_generator]
|
44
|
+
end
|
45
|
+
|
46
|
+
def load_filter
|
47
|
+
return YAML.load_file(@filter_path) if File.exist?(@filter_path)
|
48
|
+
|
49
|
+
[]
|
50
|
+
end
|
51
|
+
|
52
|
+
def end_class_listing
|
53
|
+
class_lister.disable
|
54
|
+
graph_generator.run(output_path, class_lister)
|
55
|
+
run_graph_rendering
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_graph_rendering
|
59
|
+
return unless render_graph_automatically
|
60
|
+
|
61
|
+
final_file = "#{output_path}.#{@graph_format}"
|
62
|
+
dot_wrapper.render_graph(output_path, final_file, @graph_format)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: trace2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Raphael Montani
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-09-27 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Check the runtime dependencies of your classes
|
14
|
+
email: raphael.muntani@gmail.com
|
15
|
+
executables:
|
16
|
+
- trace2
|
17
|
+
extensions:
|
18
|
+
- ext/trace2/extconf.rb
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- bin/trace2
|
22
|
+
- ext/trace2/extconf.rb
|
23
|
+
- lib/trace2.rb
|
24
|
+
- lib/trace2/2.5/trace2.so
|
25
|
+
- lib/trace2/2.6/trace2.so
|
26
|
+
- lib/trace2/2.7/trace2.so
|
27
|
+
- lib/trace2/class_lister.rb
|
28
|
+
- lib/trace2/class_use.rb
|
29
|
+
- lib/trace2/class_use_factory.rb
|
30
|
+
- lib/trace2/dot_wrapper.rb
|
31
|
+
- lib/trace2/event_processor.rb
|
32
|
+
- lib/trace2/executable_runner.rb
|
33
|
+
- lib/trace2/filter_parser.rb
|
34
|
+
- lib/trace2/graph_generator.rb
|
35
|
+
- lib/trace2/option_parser.rb
|
36
|
+
- lib/trace2/options.rb
|
37
|
+
- lib/trace2/query_use.rb
|
38
|
+
- lib/trace2/reporting_tools_factory.rb
|
39
|
+
- lib/trace2/runner.rb
|
40
|
+
- lib/trace2/trace2.so
|
41
|
+
- lib/trace2/version.rb
|
42
|
+
homepage: https://github.com/rmuntani/trace2
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata:
|
46
|
+
source_code_uri: https://github.com/rmuntani/trace2
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.0.4
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Check the runtime dependencies of your classes
|
66
|
+
test_files: []
|