delfos 0.0.1.pre.beta
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/lib/delfos.rb +58 -0
- data/lib/delfos/common_path.rb +60 -0
- data/lib/delfos/distance/calculation.rb +133 -0
- data/lib/delfos/distance/relation.rb +119 -0
- data/lib/delfos/execution_chain.rb +74 -0
- data/lib/delfos/method_logging.rb +71 -0
- data/lib/delfos/method_logging/args.rb +53 -0
- data/lib/delfos/method_logging/code_location.rb +156 -0
- data/lib/delfos/method_logging/klass_determination.rb +16 -0
- data/lib/delfos/neo4j/distance_update.rb +73 -0
- data/lib/delfos/neo4j/execution_persistence.rb +63 -0
- data/lib/delfos/neo4j/informer.rb +110 -0
- data/lib/delfos/neo4j/query_execution.rb +38 -0
- data/lib/delfos/patching.rb +105 -0
- data/lib/delfos/patching_unstubbing_spec_helper.rb +44 -0
- data/lib/delfos/perform_patching.rb +16 -0
- data/lib/delfos/remove_patching.rb +10 -0
- data/lib/delfos/version.rb +4 -0
- metadata +161 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "pathname"
|
3
|
+
require "forwardable"
|
4
|
+
require "binding_of_caller"
|
5
|
+
require_relative "common_path"
|
6
|
+
require_relative "method_logging/klass_determination"
|
7
|
+
require_relative "method_logging/code_location"
|
8
|
+
require_relative "method_logging/args"
|
9
|
+
require_relative "execution_chain"
|
10
|
+
|
11
|
+
module Delfos
|
12
|
+
class << self
|
13
|
+
attr_accessor :logger, :application_directories
|
14
|
+
end
|
15
|
+
|
16
|
+
class ApplicationDirectoriesNotDefined < StandardError
|
17
|
+
def initialize(*_args)
|
18
|
+
super "Please set Delfos.application_directories"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module MethodLogging
|
23
|
+
class << self
|
24
|
+
def check_setup!
|
25
|
+
raise Delfos::ApplicationDirectoriesNotDefined unless Delfos.application_directories
|
26
|
+
end
|
27
|
+
|
28
|
+
def log(called_object,
|
29
|
+
args, keyword_args, _block,
|
30
|
+
class_method,
|
31
|
+
stack, call_site_binding,
|
32
|
+
called_method)
|
33
|
+
check_setup!
|
34
|
+
|
35
|
+
call_site = CodeLocation.from_call_site(stack, call_site_binding)
|
36
|
+
Delfos::ExecutionChain.push(call_site)
|
37
|
+
|
38
|
+
return unless call_site
|
39
|
+
|
40
|
+
args = Args.new(args.dup, keyword_args.dup)
|
41
|
+
|
42
|
+
called_code = CodeLocation.from_called(called_object, called_method, class_method)
|
43
|
+
|
44
|
+
Delfos.logger.debug(args, call_site, called_code)
|
45
|
+
end
|
46
|
+
|
47
|
+
def include_any_path_in_logging?(paths)
|
48
|
+
paths.inject(false) do |result, path|
|
49
|
+
result || include_file_in_logging?(path)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def exclude_from_logging?(method)
|
54
|
+
file, _line_number = method.source_location
|
55
|
+
return true unless file
|
56
|
+
|
57
|
+
exclude_file_from_logging?(File.expand_path(file))
|
58
|
+
end
|
59
|
+
|
60
|
+
def include_file_in_logging?(file)
|
61
|
+
!exclude_file_from_logging?(file)
|
62
|
+
end
|
63
|
+
|
64
|
+
def exclude_file_from_logging?(file)
|
65
|
+
check_setup!
|
66
|
+
path = Pathname.new(File.expand_path file)
|
67
|
+
!CommonPath.included_in?(path, Delfos.application_directories)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "../../delfos"
|
3
|
+
require_relative "../common_path"
|
4
|
+
require_relative "klass_determination"
|
5
|
+
|
6
|
+
module Delfos
|
7
|
+
module MethodLogging
|
8
|
+
class Args
|
9
|
+
include KlassDetermination
|
10
|
+
|
11
|
+
def initialize(args, keyword_args)
|
12
|
+
@raw_args = args
|
13
|
+
@raw_keyword_args = keyword_args
|
14
|
+
end
|
15
|
+
|
16
|
+
def args
|
17
|
+
@args ||= calculate_args(@raw_args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def calculate_args(args)
|
21
|
+
klass_file_locations(args).select do |_klass, locations|
|
22
|
+
Delfos::MethodLogging.include_any_path_in_logging?(locations)
|
23
|
+
end.map(&:first)
|
24
|
+
end
|
25
|
+
|
26
|
+
def keyword_args
|
27
|
+
@keyword_args ||= calculate_args(@raw_keyword_args.values)
|
28
|
+
end
|
29
|
+
|
30
|
+
def klass_locations(klass)
|
31
|
+
files = method_sources(klass)
|
32
|
+
|
33
|
+
files.flatten.compact.uniq
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def klass_file_locations(args)
|
39
|
+
klasses(args).each_with_object({}) { |k, result| result[k] = klass_locations(k) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def klasses(args)
|
43
|
+
args.map do |k|
|
44
|
+
klass_for(k)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_sources(klass)
|
49
|
+
(Delfos::Patching.added_methods[klass.to_s] || {}).values.map(&:first)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "klass_determination"
|
3
|
+
|
4
|
+
module Delfos
|
5
|
+
module MethodLogging
|
6
|
+
class CodeLocation
|
7
|
+
include KlassDetermination
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def from_call_site(stack, call_site_binding)
|
11
|
+
CallSiteParsing.new(stack, call_site_binding).perform
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_called(object, called_method, class_method)
|
15
|
+
file, line_number = called_method.source_location
|
16
|
+
return unless file && line_number
|
17
|
+
|
18
|
+
new(object, called_method.name.to_s, class_method, file, line_number)
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_type_from(class_method)
|
22
|
+
class_method ? "ClassMethod" : "InstanceMethod"
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_definition_for(klass, class_method, method_name)
|
26
|
+
key = key_from(class_method, method_name)
|
27
|
+
|
28
|
+
Delfos::Patching.method_definition_for(klass, key)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def key_from(class_method, name)
|
34
|
+
"#{method_type_from(class_method)}_#{name}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :object, :method_name, :class_method, :method_type, :file, :line_number
|
39
|
+
|
40
|
+
def initialize(object, method_name, class_method, file, line_number)
|
41
|
+
@object = object
|
42
|
+
@method_name = method_name
|
43
|
+
@class_method = class_method
|
44
|
+
@method_type = self.class.method_type_from class_method
|
45
|
+
@line_number = line_number.to_i
|
46
|
+
@file = file
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def file
|
51
|
+
file = @file.to_s
|
52
|
+
|
53
|
+
if file
|
54
|
+
Delfos.application_directories.map do |d|
|
55
|
+
file = relative_path(file, d)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
file
|
60
|
+
end
|
61
|
+
|
62
|
+
def relative_path(file, dir)
|
63
|
+
match = dir.to_s.split("/")[0..-2].join("/")
|
64
|
+
|
65
|
+
if file[match]
|
66
|
+
file = file.gsub(match, "").
|
67
|
+
gsub(%r{^/}, "")
|
68
|
+
end
|
69
|
+
|
70
|
+
file
|
71
|
+
end
|
72
|
+
|
73
|
+
def klass
|
74
|
+
klass_for(object)
|
75
|
+
end
|
76
|
+
|
77
|
+
def klass_name
|
78
|
+
name = klass.name || "__AnonymousClass"
|
79
|
+
name.tr ":", "_"
|
80
|
+
end
|
81
|
+
|
82
|
+
def method_definition_file
|
83
|
+
method_definition[0].to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def method_definition_line
|
87
|
+
method_definition[1].to_i
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def method_key
|
93
|
+
"#{method_type}_#{method_name}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def method_definition
|
97
|
+
(@method_definition ||= self.class.method_definition_for(klass, class_method, method_name)) || {}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class CallSiteParsing
|
102
|
+
# This magic number is determined based on the specific implementation now
|
103
|
+
# E.g. if the line
|
104
|
+
# where we call this `call_site_binding.of_caller(stack_index + STACK_OFFSET).eval('self')`
|
105
|
+
# is to be extracted into another method we will get a failing test and have to increment
|
106
|
+
# the value
|
107
|
+
STACK_OFFSET = 5
|
108
|
+
|
109
|
+
attr_reader :stack, :call_site_binding
|
110
|
+
|
111
|
+
def initialize(stack, call_site_binding)
|
112
|
+
@stack = stack
|
113
|
+
@call_site_binding = call_site_binding
|
114
|
+
end
|
115
|
+
|
116
|
+
def perform
|
117
|
+
file, line_number, method_name = method_details
|
118
|
+
return unless current && file && line_number && method_name
|
119
|
+
|
120
|
+
CodeLocation.new(object, method_name.to_s, class_method, file, line_number)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def class_method
|
126
|
+
object.is_a? Module
|
127
|
+
end
|
128
|
+
|
129
|
+
def current
|
130
|
+
stack.detect do |s|
|
131
|
+
file = s.split(":")[0]
|
132
|
+
Delfos::MethodLogging.include_file_in_logging?(file)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def object
|
137
|
+
@object ||= call_site_binding.of_caller(stack_index + STACK_OFFSET).receiver
|
138
|
+
end
|
139
|
+
|
140
|
+
def stack_index
|
141
|
+
stack.index { |c| c == current }
|
142
|
+
end
|
143
|
+
|
144
|
+
def method_details
|
145
|
+
return unless current
|
146
|
+
file, line_number, rest = current.split(":")
|
147
|
+
method_name = rest[/`.*'$/]
|
148
|
+
return unless method_name && file && line_number
|
149
|
+
|
150
|
+
method_name.delete!("`").delete!("'")
|
151
|
+
|
152
|
+
[file, line_number.to_i, method_name]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "../distance/calculation"
|
3
|
+
require_relative "query_execution"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Delfos
|
7
|
+
module Neo4j
|
8
|
+
class DistanceUpdate
|
9
|
+
def perform
|
10
|
+
query = <<-QUERY
|
11
|
+
MATCH
|
12
|
+
(klass) - [:OWNS] -> (method),
|
13
|
+
(method) - [:CONTAINS] -> (call_site),
|
14
|
+
(call_site) - [:CALLS] -> (called),
|
15
|
+
(called_klass) - [:OWNS] -> (called)
|
16
|
+
|
17
|
+
RETURN
|
18
|
+
head(labels(klass)),
|
19
|
+
call_site, id(call_site),
|
20
|
+
method,
|
21
|
+
called, id(called),
|
22
|
+
head(labels(called_klass))
|
23
|
+
QUERY
|
24
|
+
|
25
|
+
results = Delfos::Neo4j::QueryExecution.execute(query)
|
26
|
+
|
27
|
+
update(results)
|
28
|
+
end
|
29
|
+
|
30
|
+
def determine_full_path(f)
|
31
|
+
f = Pathname.new f
|
32
|
+
return f.realpath if File.exist?(f)
|
33
|
+
|
34
|
+
Delfos.application_directories.map do |d|
|
35
|
+
path = Pathname.new(d + f.to_s.gsub(%r{[^/]*/}, ""))
|
36
|
+
path if path.exist?
|
37
|
+
end.compact.first
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def update(results)
|
43
|
+
results.map do |klass, call_site, call_site_id, meth, called, called_id, called_klass|
|
44
|
+
start = determine_full_path call_site["file"]
|
45
|
+
finish = determine_full_path called["file"]
|
46
|
+
|
47
|
+
calc = Delfos::Distance::Calculation.new(start, finish)
|
48
|
+
|
49
|
+
perform_query(calc, call_site_id, called_id)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def perform_query(calc, call_site_id, called_id)
|
54
|
+
Delfos::Neo4j::QueryExecution.execute <<-QUERY
|
55
|
+
START call_site = node(#{call_site_id}),
|
56
|
+
called = node(#{called_id})
|
57
|
+
|
58
|
+
MERGE (call_site) - #{rel_for(calc)} -> (called)
|
59
|
+
QUERY
|
60
|
+
end
|
61
|
+
|
62
|
+
def rel_for(calc)
|
63
|
+
<<-REL
|
64
|
+
[:EFFERENT_COUPLING{
|
65
|
+
distance: #{calc.sum_traversals},
|
66
|
+
possible_distance: #{calc.sum_possible_traversals}
|
67
|
+
}
|
68
|
+
]
|
69
|
+
REL
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative "query_execution"
|
2
|
+
|
3
|
+
module Delfos
|
4
|
+
module Neo4j
|
5
|
+
class ExecutionPersistence
|
6
|
+
def self.save!(other)
|
7
|
+
new(other.call_sites, other.execution_count).save!
|
8
|
+
end
|
9
|
+
|
10
|
+
def save!
|
11
|
+
Neo4j::QueryExecution.execute query
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def initialize(call_sites, execution_count)
|
17
|
+
@call_sites = call_sites
|
18
|
+
|
19
|
+
@execution_count = execution_count
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :call_sites, :execution_count
|
23
|
+
|
24
|
+
def query
|
25
|
+
call_sites.compact.map.with_index do |c, i|
|
26
|
+
call_site_query(c, i)
|
27
|
+
end.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def call_site_query(cs, i)
|
31
|
+
<<-QUERY
|
32
|
+
#{method_query(cs, i)}
|
33
|
+
|
34
|
+
MERGE
|
35
|
+
(m#{i})
|
36
|
+
|
37
|
+
-[:CONTAINS]->
|
38
|
+
|
39
|
+
(cs#{i}:CallSite {file: "#{cs.file}", line_number: #{cs.line_number}})
|
40
|
+
|
41
|
+
#{execution_chain_query(cs, i)}
|
42
|
+
QUERY
|
43
|
+
end
|
44
|
+
|
45
|
+
def execution_chain_query(cs, i)
|
46
|
+
<<-QUERY
|
47
|
+
MERGE (e#{i}:ExecutionChain{number: #{execution_count}})
|
48
|
+
|
49
|
+
MERGE e#{i}-[:STEP{number: #{i + 1}}]-> (cs#{i})
|
50
|
+
QUERY
|
51
|
+
end
|
52
|
+
|
53
|
+
def method_query(cs, i)
|
54
|
+
<<-QUERY
|
55
|
+
MERGE (k#{i}:#{cs.klass})
|
56
|
+
- [:OWNS] ->
|
57
|
+
|
58
|
+
(m#{i} :#{cs.method_type} {name: "#{cs.method_name}", file: "#{cs.method_definition_file}", line_number: #{cs.method_definition_line}})
|
59
|
+
QUERY
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "delfos"
|
3
|
+
require_relative "query_execution"
|
4
|
+
|
5
|
+
module Delfos
|
6
|
+
module Neo4j
|
7
|
+
class Informer
|
8
|
+
def debug(args, call_site, called_code)
|
9
|
+
execute_query(args, call_site, called_code)
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute_query(*args)
|
13
|
+
query = query_for(*args)
|
14
|
+
|
15
|
+
QueryExecution.execute(query)
|
16
|
+
end
|
17
|
+
|
18
|
+
def query_for(args, call_site, called_code)
|
19
|
+
assign_query_variables(args, call_site, called_code)
|
20
|
+
|
21
|
+
klasses_query = query_variables.map do |klass, name|
|
22
|
+
"MERGE (#{name}:#{klass})"
|
23
|
+
end.join("\n")
|
24
|
+
|
25
|
+
<<-QUERY
|
26
|
+
#{klasses_query}
|
27
|
+
|
28
|
+
#{merge_query(call_site, called_code)}
|
29
|
+
#{args_query args}
|
30
|
+
QUERY
|
31
|
+
end
|
32
|
+
|
33
|
+
def assign_query_variables(args, call_site, called_code)
|
34
|
+
query_variables.assign(call_site.klass, "k")
|
35
|
+
query_variables.assign(called_code.klass, "k")
|
36
|
+
|
37
|
+
(args.args + args.keyword_args).uniq.each do |k|
|
38
|
+
query_variables.assign(k, "k")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def merge_query(call_site, called_code)
|
43
|
+
<<-MERGE_QUERY
|
44
|
+
#{method_definition call_site, "m1"}
|
45
|
+
|
46
|
+
MERGE (m1) - [:CONTAINS] -> (cs:CallSite{file: "#{call_site.file}", line_number: #{call_site.line_number}})
|
47
|
+
|
48
|
+
#{method_definition called_code, "m2"}
|
49
|
+
|
50
|
+
MERGE (cs) - [:CALLS] -> m2
|
51
|
+
MERGE_QUERY
|
52
|
+
end
|
53
|
+
|
54
|
+
def method_definition(code, id)
|
55
|
+
<<-METHOD
|
56
|
+
MERGE (#{query_variable(code.klass)}) - [:OWNS] -> #{method_node(code, id)}
|
57
|
+
METHOD
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_node(code, id)
|
61
|
+
if code.method_definition_file.length > 0 && code.method_definition_line > 0
|
62
|
+
<<-NODE
|
63
|
+
(#{id}:#{code.method_type}{name: "#{code.method_name}", file: #{code.method_definition_file.inspect}, line_number: #{code.method_definition_line}})
|
64
|
+
NODE
|
65
|
+
else
|
66
|
+
<<-NODE
|
67
|
+
(#{id}:#{code.method_type}{name: "#{code.method_name}"})
|
68
|
+
NODE
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def args_query(args)
|
73
|
+
(args.args + args.keyword_args).map do |k|
|
74
|
+
name = query_variable(k)
|
75
|
+
"MERGE (cs) - [:ARG] -> (#{name})"
|
76
|
+
end.join("\n")
|
77
|
+
end
|
78
|
+
|
79
|
+
def query_variable(k)
|
80
|
+
query_variables[k.to_s]
|
81
|
+
end
|
82
|
+
|
83
|
+
def query_variables
|
84
|
+
@query_variables ||= QueryVariables.new
|
85
|
+
end
|
86
|
+
|
87
|
+
def code_execution_query
|
88
|
+
Delfos::Patching.method_chain
|
89
|
+
end
|
90
|
+
|
91
|
+
class QueryVariables < Hash
|
92
|
+
def initialize(*args)
|
93
|
+
super(*args)
|
94
|
+
@counters = Hash.new(1)
|
95
|
+
end
|
96
|
+
|
97
|
+
def assign(klass, prefix)
|
98
|
+
klass = klass.to_s
|
99
|
+
val = self[klass]
|
100
|
+
return val if val
|
101
|
+
|
102
|
+
"#{prefix}#{@counters[prefix]}".tap do |v|
|
103
|
+
self[klass] = v
|
104
|
+
@counters[prefix] += 1
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|