silhouette 1.0.0 → 2.0.0
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.
- data/Manifest.txt +10 -1
- data/Rakefile +4 -2
- data/bin/silhouette +2 -29
- data/bin/silhouette_coverage +70 -0
- data/bin/silrun +83 -0
- data/lib/silhouette.rb +5 -13
- data/lib/silhouette/converter.rb +102 -0
- data/lib/silhouette/coverage.rb +604 -0
- data/lib/silhouette/default.css +68 -0
- data/lib/silhouette/emitters.rb +176 -0
- data/lib/silhouette/finder.rb +138 -0
- data/lib/silhouette/light.css +63 -0
- data/lib/silhouette/process.rb +3 -0
- data/lib/silhouette/processor.rb +8 -279
- data/lib/silhouette/setup.rb +102 -0
- data/silhouette_ext.c +66 -23
- data/test/test.rb +8 -1
- metadata +54 -22
- data/test/silhouette.out +0 -0
data/Manifest.txt
CHANGED
@@ -1,10 +1,19 @@
|
|
1
1
|
bin/silhouette
|
2
|
+
bin/silhouette_coverage
|
3
|
+
bin/silrun
|
2
4
|
extconf.rb
|
3
5
|
lib/silhouette/processor.rb
|
6
|
+
lib/silhouette/converter.rb
|
7
|
+
lib/silhouette/coverage.rb
|
8
|
+
lib/silhouette/emitters.rb
|
9
|
+
lib/silhouette/process.rb
|
10
|
+
lib/silhouette/setup.rb
|
11
|
+
lib/silhouette/finder.rb
|
12
|
+
lib/silhouette/default.css
|
13
|
+
lib/silhouette/light.css
|
4
14
|
lib/silhouette.rb
|
5
15
|
Manifest.txt
|
6
16
|
Rakefile
|
7
17
|
README
|
8
18
|
silhouette_ext.c
|
9
|
-
test/silhouette.out
|
10
19
|
test/test.rb
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ $VERBOSE = nil
|
|
6
6
|
|
7
7
|
spec = Gem::Specification.new do |s|
|
8
8
|
s.name = 'silhouette'
|
9
|
-
s.version = '
|
9
|
+
s.version = '2.0.0'
|
10
10
|
s.summary = 'A 2 stage profiler'
|
11
11
|
s.author = 'Evan Webb'
|
12
12
|
s.email = 'evan@fallingsnow.net'
|
@@ -14,9 +14,11 @@ spec = Gem::Specification.new do |s|
|
|
14
14
|
s.has_rdoc = true
|
15
15
|
s.files = File.read('Manifest.txt').split($/)
|
16
16
|
s.require_path = 'lib'
|
17
|
-
s.executables = ['silhouette']
|
17
|
+
s.executables = ['silhouette', 'silhouette_coverage', 'silrun']
|
18
18
|
s.default_executable = 'silhouette'
|
19
19
|
s.extensions = ['extconf.rb']
|
20
|
+
s.add_dependency 'builder'
|
21
|
+
s.add_dependency 'syntax'
|
20
22
|
end
|
21
23
|
|
22
24
|
desc 'Build Gem'
|
data/bin/silhouette
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'optparse'
|
3
|
-
require 'silhouette/
|
3
|
+
require 'silhouette/process'
|
4
4
|
|
5
5
|
output = nil
|
6
6
|
max = nil
|
@@ -16,25 +16,6 @@ load = false
|
|
16
16
|
STDOUT.sync = true
|
17
17
|
|
18
18
|
opt = OptionParser.new do |opt|
|
19
|
-
=begin
|
20
|
-
opt.on("-o FILE", "Where to output data") { |output| }
|
21
|
-
opt.on("-c", "--combine", "Combine multiple data files.") do
|
22
|
-
data = Hash.new
|
23
|
-
ARGV.each do |file|
|
24
|
-
print "#{file}: "
|
25
|
-
rp = Silhouette.new(file)
|
26
|
-
rp.data = data
|
27
|
-
rp.parse
|
28
|
-
puts "done."
|
29
|
-
end
|
30
|
-
|
31
|
-
File.open(output, "w") do |f|
|
32
|
-
f << Marshal.dump(data)
|
33
|
-
end
|
34
|
-
puts "Data saved to #{output}. #{data.keys.size} data points."
|
35
|
-
exit
|
36
|
-
end
|
37
|
-
=end
|
38
19
|
opt.on("-m", "--max MAX", "Only show the top N call sites") do |m|
|
39
20
|
max = m.to_i
|
40
21
|
end
|
@@ -86,19 +67,11 @@ if load
|
|
86
67
|
exit
|
87
68
|
end
|
88
69
|
|
89
|
-
|
90
70
|
unless file = ARGV.shift
|
91
71
|
STDERR.puts "Please specify a file to process."
|
92
72
|
end
|
93
73
|
|
94
|
-
|
95
|
-
|
96
|
-
if gzip
|
97
|
-
require 'zlib'
|
98
|
-
io = Zlib::GzipReader.new(io)
|
99
|
-
end
|
100
|
-
|
101
|
-
emit = Silhouette.find_emitter(io)
|
74
|
+
emit = Silhouette.find_emitter(file)
|
102
75
|
|
103
76
|
if ascii
|
104
77
|
if long
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'silhouette/process'
|
3
|
+
require 'silhouette/coverage'
|
4
|
+
require 'optparse'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
cfg = OpenStruct.new
|
8
|
+
|
9
|
+
opt = OptionParser.new do |o|
|
10
|
+
o.on "-x", "--xml FILE", "Output as XML to FILE" do |file|
|
11
|
+
cfg.xml = file
|
12
|
+
end
|
13
|
+
|
14
|
+
o.on "-h", "--html DIR", "Ouput as HTML to DIR" do |file|
|
15
|
+
cfg.html = file
|
16
|
+
end
|
17
|
+
|
18
|
+
o.on "-c", "--compact", "Use the compact output for the text mode" do
|
19
|
+
cfg.compact = true
|
20
|
+
end
|
21
|
+
|
22
|
+
o.on "-s", "--stats", "Output stats only" do
|
23
|
+
cfg.stats = true
|
24
|
+
end
|
25
|
+
|
26
|
+
o.on "-m", "--match MATCH", "Only generate coverage info for files matching MATCH" do |m|
|
27
|
+
cfg.match = m
|
28
|
+
end
|
29
|
+
|
30
|
+
o.on "-l", "--light", "Use the light colored CSS definitions." do
|
31
|
+
cfg.light = true
|
32
|
+
end
|
33
|
+
|
34
|
+
o.on "-I PATH", "Add path to includes." do |m|
|
35
|
+
$:.unshift m
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
opt.parse!
|
40
|
+
file = ARGV.shift
|
41
|
+
|
42
|
+
emit = Silhouette.find_emitter(file)
|
43
|
+
|
44
|
+
cov = Silhouette::CoverageProcessor.new
|
45
|
+
if cfg.match
|
46
|
+
cov.match_files = Regexp.new(cfg.match)
|
47
|
+
end
|
48
|
+
|
49
|
+
if cfg.light
|
50
|
+
cov.css = "light.css"
|
51
|
+
end
|
52
|
+
emit.processor = cov
|
53
|
+
emit.parse
|
54
|
+
|
55
|
+
if cfg.xml
|
56
|
+
File.open(cfg.xml, "w") do |fd|
|
57
|
+
fd << cov.to_xml
|
58
|
+
end
|
59
|
+
elsif cfg.html
|
60
|
+
unless File.exists? cfg.html
|
61
|
+
Dir.mkdir cfg.html
|
62
|
+
end
|
63
|
+
cov.to_html cfg.html
|
64
|
+
else
|
65
|
+
if cfg.stats
|
66
|
+
print cov.stats
|
67
|
+
else
|
68
|
+
print cov.to_ascii(cfg.compact)
|
69
|
+
end
|
70
|
+
end
|
data/bin/silrun
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'silhouette/setup'
|
4
|
+
require 'optparse'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
options = Silhouette::Options.new
|
8
|
+
options.import_env
|
9
|
+
|
10
|
+
cfg = OpenStruct.new
|
11
|
+
|
12
|
+
opts = OptionParser.new do |o|
|
13
|
+
o.on "-n", "--no-compression", "Don't compress data files." do
|
14
|
+
options.compress = false
|
15
|
+
end
|
16
|
+
|
17
|
+
o.on "-c", "--coverage", "Only generate coverage information." do
|
18
|
+
options.coverage = true
|
19
|
+
end
|
20
|
+
|
21
|
+
o.on "-s", "--send HOST:PORT",
|
22
|
+
"Send information via TCP to this address." do |h|
|
23
|
+
host, port = h.split(":")
|
24
|
+
require 'socket'
|
25
|
+
begin
|
26
|
+
sock = TCPSocket.new(host, port.to_i)
|
27
|
+
rescue Object => e
|
28
|
+
puts "Unable to connect to #{h}: #{e.message} (#{e.class})"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
options.file = sock
|
32
|
+
options.location = h
|
33
|
+
end
|
34
|
+
|
35
|
+
o.on "-f", "--file PATH", "Output data to PATH" do |h|
|
36
|
+
options.file = h
|
37
|
+
end
|
38
|
+
|
39
|
+
o.on "-a", "--all", "Generate as much information as possible." do
|
40
|
+
options.all = true
|
41
|
+
end
|
42
|
+
|
43
|
+
o.on "-r", "--require LIB", "Require this library." do |r|
|
44
|
+
require r
|
45
|
+
end
|
46
|
+
|
47
|
+
o.on "-I", "--include PATH", "Prepend this include path." do |i|
|
48
|
+
$:.unshift i
|
49
|
+
end
|
50
|
+
|
51
|
+
o.on "-d", "--debug", "Turn debug on" do
|
52
|
+
$DEBUG = true
|
53
|
+
end
|
54
|
+
|
55
|
+
o.on "-w", "--warn", "Turn warnings on" do
|
56
|
+
$VERBOSE = true
|
57
|
+
end
|
58
|
+
|
59
|
+
o.on "-h", "--help" do
|
60
|
+
puts o
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
|
64
|
+
#o.on "-t", "--test", "The file should be run using test/unit" do
|
65
|
+
# cfg.is_test = true
|
66
|
+
#end
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.parse!
|
70
|
+
|
71
|
+
if ARGV.empty?
|
72
|
+
puts "No file to run!"
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
|
76
|
+
options.describe = true
|
77
|
+
|
78
|
+
args, file = options.setup_args
|
79
|
+
|
80
|
+
STDERR.puts "Logging profile information to #{file}"
|
81
|
+
Silhouette.start_profile *args
|
82
|
+
|
83
|
+
require ARGV.first
|
data/lib/silhouette.rb
CHANGED
@@ -1,16 +1,8 @@
|
|
1
|
+
require 'silhouette/setup'
|
1
2
|
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
STDERR.puts "Flushing profile information..."
|
6
|
-
Silhouette.stop_profile
|
7
|
-
}
|
8
|
-
|
9
|
-
if ENV["SILHOUETTE_FILE"]
|
10
|
-
file = ENV["SILHOUETTE_FILE"]
|
11
|
-
else
|
12
|
-
file = "silhouette.out"
|
13
|
-
end
|
3
|
+
opts = Silhouette::Options.new
|
4
|
+
opts.import_env
|
5
|
+
args, file = opts.setup_args
|
14
6
|
|
15
7
|
STDERR.puts "Logging profile information to #{file}"
|
16
|
-
Silhouette.start_profile
|
8
|
+
Silhouette.start_profile *args
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Silhouette
|
2
|
+
|
3
|
+
# NOTE: This uses IO#write instead of IO#puts
|
4
|
+
# because IO#write is faster as it does a quick test
|
5
|
+
# to see if the argument is already a string and just
|
6
|
+
# writes it if it is. IO#puts calls respond_to? on
|
7
|
+
# all arguments to see if they are strings, which is
|
8
|
+
# a lot slower if you do this 20,000 times.
|
9
|
+
class ASCIIConverter < Processor
|
10
|
+
def initialize(file)
|
11
|
+
@io = File.open(file, "w")
|
12
|
+
end
|
13
|
+
|
14
|
+
def process_start(*args)
|
15
|
+
@io.write "! #{args.join(' ')}\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_end(*args)
|
19
|
+
@io.write "@ #{args.join(' ')}\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
def process_method(*args)
|
23
|
+
@io.write "& #{args.join(' ')}\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
def process_file(*args)
|
27
|
+
@io.write "* #{args.join(' ')}\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_call(*args)
|
31
|
+
@io.write "c #{args.join(' ')}\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_return(*args)
|
35
|
+
@io.write "r #{args.join(' ')}\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_line(*args)
|
39
|
+
@io.write "l #{args.join(' ')}\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
def close
|
43
|
+
@io.close
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class ASCIIConverterLong < ASCIIConverter
|
48
|
+
|
49
|
+
def initialize(file)
|
50
|
+
@methods = Hash.new
|
51
|
+
@files = Hash.new
|
52
|
+
@last_method = nil
|
53
|
+
@last_series = nil
|
54
|
+
@skip_return = false
|
55
|
+
super(file)
|
56
|
+
end
|
57
|
+
def process_method(idx, klass, kind, meth)
|
58
|
+
@methods[idx] = [klass, kind, meth].to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
def process_file(idx, file)
|
62
|
+
@files[idx] = file
|
63
|
+
end
|
64
|
+
|
65
|
+
def process_call(thread, meth, file, line, clock)
|
66
|
+
@io.puts "c #{thread} #{@methods[meth]} #{@files[file]} #{line} #{clock}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def process_return(thread, meth, file, line, clock)
|
70
|
+
@io.puts "r #{thread} #{@methods[meth]} #{clock}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_line(thread, meth, file, line, clock)
|
74
|
+
@io.puts "l #{thread} #{@files[file]} #{line} #{clock}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def process_call_rep(thread, meth, file, line, clock)
|
78
|
+
if @last_method == [thread, meth, file, line] and @last_series
|
79
|
+
@last_series += 1
|
80
|
+
@skip_return = true
|
81
|
+
else
|
82
|
+
@io.puts "cal #{thread} #{@methods[meth]} #{meth} #{@files[file]} #{line} #{clock}"
|
83
|
+
end
|
84
|
+
|
85
|
+
@last_method = [thread, meth, file, line]
|
86
|
+
end
|
87
|
+
|
88
|
+
def process_return_rep(thread, meth, file, line, clock)
|
89
|
+
if @last_method == [thread, meth, file, line]
|
90
|
+
@last_series = 1 unless @last_series
|
91
|
+
elsif @last_series
|
92
|
+
p [thread, meth, @methods[meth]]
|
93
|
+
p @last_method
|
94
|
+
@io.puts "rep #{@last_series}"
|
95
|
+
@last_series = nil
|
96
|
+
@skip_return = false
|
97
|
+
end
|
98
|
+
return if @skip_return
|
99
|
+
@io.puts "ret #{thread} #{@methods[meth]} #{clock}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,604 @@
|
|
1
|
+
require "builder"
|
2
|
+
require 'pathname'
|
3
|
+
require 'silhouette/finder'
|
4
|
+
|
5
|
+
module Silhouette
|
6
|
+
|
7
|
+
class SourceFile
|
8
|
+
def initialize(path, covered_lines)
|
9
|
+
@path = path
|
10
|
+
@coverage = covered_lines
|
11
|
+
@in_rdoc_comment = false
|
12
|
+
@lines = []
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :path
|
16
|
+
|
17
|
+
def update_coverage
|
18
|
+
@lines = File.readlines @path
|
19
|
+
i = 0
|
20
|
+
@lines.each do |line|
|
21
|
+
i += 1
|
22
|
+
next if @coverage[i]
|
23
|
+
if code = noop_line(line)
|
24
|
+
@coverage[i] = code
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@largest = @coverage.compact.max do |a,b|
|
29
|
+
a.to_i <=> b.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
@convertor = Silhouette::MethodFinder.for_syntax "ruby"
|
33
|
+
@syn_lines = @convertor.convert @lines.join(""), false
|
34
|
+
|
35
|
+
@methods = @convertor.methods
|
36
|
+
@missed_methods = []
|
37
|
+
@missed_methods_start = []
|
38
|
+
|
39
|
+
@method_starts = []
|
40
|
+
|
41
|
+
# Calculate the LOC and missed lines per method.
|
42
|
+
|
43
|
+
@methods.each do |mp|
|
44
|
+
next unless mp.first_line and mp.last_line
|
45
|
+
(mp.first_line + 1).upto(mp.last_line - 1) do |line|
|
46
|
+
code = @coverage[line]
|
47
|
+
mp.loc += 1 unless code and code < 0
|
48
|
+
mp.misses += 1 unless code
|
49
|
+
end
|
50
|
+
|
51
|
+
@method_starts[mp.first_line] = mp
|
52
|
+
|
53
|
+
if mp.loc > 0 and mp.misses == mp.loc
|
54
|
+
@missed_methods << mp
|
55
|
+
@missed_methods_start[mp.first_line] = mp
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
NOOP_PATTERNS = [
|
61
|
+
/^\s*begin/,
|
62
|
+
/^\s*else/,
|
63
|
+
/^\s*#/,
|
64
|
+
/^\s*ensure/,
|
65
|
+
/^\s*end/,
|
66
|
+
/^\s*rescue/,
|
67
|
+
/^\s*[}\)\]]/,
|
68
|
+
/^\scase/ # You can have a case with no condition.
|
69
|
+
]
|
70
|
+
|
71
|
+
def noop_line(line)
|
72
|
+
if /^=begin/.match(line)
|
73
|
+
@in_rdoc_comment = true
|
74
|
+
return -1
|
75
|
+
elsif /^=end/.match(line)
|
76
|
+
@in_rdoc_comment = false
|
77
|
+
return -1
|
78
|
+
elsif @in_rdoc_comment
|
79
|
+
return -1
|
80
|
+
elsif /^\s*$/.match(line)
|
81
|
+
return -2
|
82
|
+
else
|
83
|
+
return -1 if NOOP_PATTERNS.any? { |m| m.match(line) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_ascii
|
88
|
+
output = ""
|
89
|
+
@lines.each_with_index do |line, i|
|
90
|
+
if count = @coverage[i+1]
|
91
|
+
output << "* "
|
92
|
+
else
|
93
|
+
output << " "
|
94
|
+
end
|
95
|
+
output << line
|
96
|
+
end
|
97
|
+
return output
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_ascii_compact
|
101
|
+
output = ""
|
102
|
+
@lines.each_with_index do |line, idx|
|
103
|
+
lineno = idx + 1
|
104
|
+
count = @coverage[lineno]
|
105
|
+
unless count
|
106
|
+
output << ("%4s " % [lineno])
|
107
|
+
output << line
|
108
|
+
end
|
109
|
+
end
|
110
|
+
return output
|
111
|
+
end
|
112
|
+
|
113
|
+
def misses
|
114
|
+
miss = @lines.size - @coverage.compact.size
|
115
|
+
end
|
116
|
+
|
117
|
+
def non_executable
|
118
|
+
@coverage.find_all { |i| i.to_i < 0 }.size
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_xml(b)
|
122
|
+
b.file(:path => @path, :total => @lines.size, :missed => misses) do
|
123
|
+
@coverage.each_with_index do |count, idx|
|
124
|
+
next if idx == 0
|
125
|
+
count = 0 unless count
|
126
|
+
b.line(:times => count, :number => idx)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def loc
|
132
|
+
loc = total - non_executable
|
133
|
+
end
|
134
|
+
|
135
|
+
def loc_percentage
|
136
|
+
(((loc - misses) / loc.to_f) * 100).to_i
|
137
|
+
end
|
138
|
+
|
139
|
+
def total
|
140
|
+
@lines.size
|
141
|
+
end
|
142
|
+
|
143
|
+
def percent
|
144
|
+
(((total - misses) / total.to_f) * 100).to_i
|
145
|
+
end
|
146
|
+
|
147
|
+
GREEN = "#2bc339"
|
148
|
+
RED = "#fd3333"
|
149
|
+
|
150
|
+
def html_method_summary(b)
|
151
|
+
b.table do
|
152
|
+
b.tr do
|
153
|
+
b.th("Method")
|
154
|
+
b.th("LOC")
|
155
|
+
b.th("Misses")
|
156
|
+
b.th("Coverage", :colspan => 2)
|
157
|
+
end
|
158
|
+
|
159
|
+
color = DGRAY
|
160
|
+
|
161
|
+
@methods.each do |mp|
|
162
|
+
color = (color == DGRAY ? LGRAY: DGRAY)
|
163
|
+
b.tr(:bgcolor => color) do
|
164
|
+
klass = mp.klass
|
165
|
+
klass = "Object" if klass.empty?
|
166
|
+
b.td(:align => "left") do
|
167
|
+
html = @path.to_s.gsub("/","--") + ".html"
|
168
|
+
b.a(klass + "#" + mp.name, :href => "#{html}#line#{mp.first_line}")
|
169
|
+
end
|
170
|
+
b.td(mp.loc)
|
171
|
+
b.td(mp.misses)
|
172
|
+
if mp.loc == 0
|
173
|
+
prec = 100
|
174
|
+
else
|
175
|
+
prec = (((mp.loc - mp.misses) / mp.loc.to_f) * 100).to_i
|
176
|
+
end
|
177
|
+
b.td do
|
178
|
+
b.text! "#{prec}%"
|
179
|
+
end
|
180
|
+
b.td do
|
181
|
+
percentage_bar(prec, b)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def html_summary(b)
|
189
|
+
b.td(total, :align => "right")
|
190
|
+
b.td(:align => "right") do
|
191
|
+
if misses == 0
|
192
|
+
color = GREEN
|
193
|
+
else
|
194
|
+
color = RED
|
195
|
+
end
|
196
|
+
b.p(misses, :style => "color:#{color}")
|
197
|
+
end
|
198
|
+
b.td(:align => "right") do
|
199
|
+
meth_misses = @missed_methods.size
|
200
|
+
if meth_misses == 0
|
201
|
+
color = GREEN
|
202
|
+
else
|
203
|
+
color = RED
|
204
|
+
end
|
205
|
+
b.p(meth_misses, :style => "color:#{color}")
|
206
|
+
end
|
207
|
+
loc_percentage = (((loc - misses) / loc.to_f) * 100).to_i
|
208
|
+
b.td(loc, :align => "right")
|
209
|
+
b.td { percentage_bar(loc_percentage, b) }
|
210
|
+
b.td("#{percent}%", :align => "right")
|
211
|
+
b.td { percentage_bar(percent, b) }
|
212
|
+
end
|
213
|
+
|
214
|
+
def percentage_bar(percent, b)
|
215
|
+
b.table(:width => 100, :height => 15, :cellspacing => 0,
|
216
|
+
:cellpadding => 0, :bgcolor => RED) do
|
217
|
+
b.tr do
|
218
|
+
b.td do
|
219
|
+
b.table(:width => "#{percent}%", :height => 15, :bgcolor => GREEN) do
|
220
|
+
b.tr { b.td { } }
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def html_header(b)
|
228
|
+
color = "#a8b1f9"
|
229
|
+
b.table(:width => "100%") do
|
230
|
+
b.tr do
|
231
|
+
b.td do
|
232
|
+
b.text! @path.to_s
|
233
|
+
b.a("[MAIN]", :href => "index.html")
|
234
|
+
end
|
235
|
+
b.td("Total Lines: #{@lines.size}", :bgcolor => color)
|
236
|
+
if misses == 0
|
237
|
+
ok = "green"
|
238
|
+
else
|
239
|
+
ok = "#fd3333"
|
240
|
+
end
|
241
|
+
b.td("Missed Lines: #{misses}", :bgcolor => ok)
|
242
|
+
b.td("Non-Code Lines: #{non_executable}", :bgcolor => color)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
DGRAY = "#d4d4d4"
|
248
|
+
LGRAY = "#f4f4f4"
|
249
|
+
|
250
|
+
def calculate_hotness(count)
|
251
|
+
return DGRAY if @largest == 0
|
252
|
+
count = 0 if Symbol === count
|
253
|
+
return "white" unless count
|
254
|
+
prec = (count / @largest.to_f)
|
255
|
+
r = [2 * (1 - prec), 1].min * 255
|
256
|
+
g = [2 * prec, 1].min * 255
|
257
|
+
b = 0
|
258
|
+
|
259
|
+
"#%02X%02X%02X" % [g, r, b]
|
260
|
+
end
|
261
|
+
|
262
|
+
def to_html(b)
|
263
|
+
html_header(b)
|
264
|
+
b.table(:width => "800", :class => "code",
|
265
|
+
:cellspacing => 0, :cellpadding => 2) do
|
266
|
+
i = 0
|
267
|
+
missed_until = nil
|
268
|
+
good_until = nil
|
269
|
+
|
270
|
+
@syn_lines.each do |line|
|
271
|
+
i += 1
|
272
|
+
b.tr do
|
273
|
+
count = @coverage[i]
|
274
|
+
|
275
|
+
klass = "sourceLine"
|
276
|
+
lcClass = "lineCount"
|
277
|
+
ccClass = "coverageCount"
|
278
|
+
|
279
|
+
tooltip = ""
|
280
|
+
|
281
|
+
# mp = @missed_methods_start[i]
|
282
|
+
if cmp = @method_starts[i]
|
283
|
+
if cmp.misses == 0
|
284
|
+
gmp = cmp
|
285
|
+
elsif cmp.misses == cmp.loc
|
286
|
+
mp = cmp
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
if mp
|
291
|
+
ccClass = "coverageMissing"
|
292
|
+
klass = "sourceLineHighlight"
|
293
|
+
missed_until = mp.last_line
|
294
|
+
count = ""
|
295
|
+
tooltip = "Method was never entered."
|
296
|
+
elsif missed_until
|
297
|
+
ccClass = "coverageMissing"
|
298
|
+
missed_until = nil if missed_until == i
|
299
|
+
count = ""
|
300
|
+
elsif gmp
|
301
|
+
ccClass = "coverageMethod"
|
302
|
+
klass = "sourceLineGoodHighlight"
|
303
|
+
good_until = gmp.last_line
|
304
|
+
tooltip = "Method was completely covered."
|
305
|
+
count = ""
|
306
|
+
elsif good_until
|
307
|
+
ccClass = "coverageMethod"
|
308
|
+
good_until = nil if good_until == i
|
309
|
+
elsif cmp
|
310
|
+
ccClass = "coverageHit"
|
311
|
+
klass = "sourceLinePartialHighlight"
|
312
|
+
tooltip = "Method has #{cmp.coverage}% coverage."
|
313
|
+
count = ""
|
314
|
+
elsif count == -1 or count == -2
|
315
|
+
lcClass = ccClass = "lineNonCode"
|
316
|
+
count = ""
|
317
|
+
elsif count
|
318
|
+
lcClass = ccClass = "coverageHit"
|
319
|
+
else
|
320
|
+
klass = "sourceLineHighlight"
|
321
|
+
ccClass = "coverageMissed"
|
322
|
+
tooltip = "This line was never run."
|
323
|
+
end
|
324
|
+
|
325
|
+
b.td(:class => lcClass, :align => "right", :width => 20) do
|
326
|
+
b.a(i, :name => "line#{i}")
|
327
|
+
end
|
328
|
+
|
329
|
+
# Don't show less than 0 counts (they are special)
|
330
|
+
count = "" if count and Numeric === count and count < 0
|
331
|
+
|
332
|
+
b.td(count, :class => ccClass, :align => "right", :width => 20)
|
333
|
+
|
334
|
+
b.td(:class => klass) do
|
335
|
+
b.a(:title => tooltip) do
|
336
|
+
b.pre(:class => klass) do
|
337
|
+
b << line.rstrip
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
=begin
|
342
|
+
if color == RED
|
343
|
+
color = DGRAY
|
344
|
+
b.td(:bgcolor => color, :style => "border: medium solid red") do
|
345
|
+
b << "<pre>#{line.rstrip}</pre>"
|
346
|
+
end
|
347
|
+
hotness = "white"
|
348
|
+
else
|
349
|
+
# Calculate the HOTNESS of the line.
|
350
|
+
|
351
|
+
b.td(:bgcolor => color) do
|
352
|
+
b << "<pre>#{line.rstrip}</pre>"
|
353
|
+
end
|
354
|
+
hotness = calculate_hotness count
|
355
|
+
end
|
356
|
+
if count and count > 0
|
357
|
+
b.td(count, :bgcolor => hotness, :width => 65)
|
358
|
+
end
|
359
|
+
=end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
class CoverageProcessor < Processor
|
367
|
+
def initialize
|
368
|
+
@methods = Hash.new
|
369
|
+
@files = Hash.new
|
370
|
+
@coverage = Hash.new { |h,k| h[k] = [] }
|
371
|
+
@process_all = false
|
372
|
+
@total_lines = 0
|
373
|
+
@total_missed = 0
|
374
|
+
@match_files = nil
|
375
|
+
@css = "default.css"
|
376
|
+
@method_hits = Hash.new { |h,k| h[k] = 0 }
|
377
|
+
end
|
378
|
+
|
379
|
+
attr_accessor :process_all, :match_files, :css
|
380
|
+
|
381
|
+
def process?(file)
|
382
|
+
return true if @process_all
|
383
|
+
return false if file == "(eval)"
|
384
|
+
|
385
|
+
if @match_files
|
386
|
+
return @match_files.match(file.to_s)
|
387
|
+
end
|
388
|
+
|
389
|
+
if file[0] == ?/
|
390
|
+
return false
|
391
|
+
end
|
392
|
+
|
393
|
+
return true
|
394
|
+
end
|
395
|
+
|
396
|
+
def add_line(file, line)
|
397
|
+
fc = @coverage[file]
|
398
|
+
if fc[line]
|
399
|
+
fc[line] += 1
|
400
|
+
else
|
401
|
+
fc[line] = 1
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def process_call(thread, meth, file, line, clock)
|
406
|
+
return unless @files.keys.include? file
|
407
|
+
@method_hits[meth] += 1
|
408
|
+
end
|
409
|
+
|
410
|
+
def process_method(idx, klass, kind, meth)
|
411
|
+
@methods[idx] = [klass, kind, meth].to_s
|
412
|
+
end
|
413
|
+
|
414
|
+
def process_file(idx, file)
|
415
|
+
return unless process? file
|
416
|
+
@files[idx] = file
|
417
|
+
end
|
418
|
+
|
419
|
+
def process_line(thread, meth, file, line, clock)
|
420
|
+
return unless @files.keys.include? file
|
421
|
+
add_line file, line
|
422
|
+
end
|
423
|
+
|
424
|
+
attr_reader :coverage, :files
|
425
|
+
|
426
|
+
def find_in_paths(file)
|
427
|
+
$:.each do |path|
|
428
|
+
cp = File.join(path, file)
|
429
|
+
return cp if File.exists? cp
|
430
|
+
end
|
431
|
+
|
432
|
+
return file
|
433
|
+
end
|
434
|
+
|
435
|
+
def processed_files
|
436
|
+
indexs = @coverage.keys.sort do |a,b|
|
437
|
+
@files[a].to_s <=> @files[b].to_s
|
438
|
+
end
|
439
|
+
|
440
|
+
indexs.each do |idx|
|
441
|
+
hits = @coverage[idx]
|
442
|
+
file = @files[idx]
|
443
|
+
if file
|
444
|
+
unless File.exists? file
|
445
|
+
file = find_in_paths(file)
|
446
|
+
end
|
447
|
+
path = Pathname.new(file)
|
448
|
+
yield(path.cleanpath, hits)
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def num_files
|
454
|
+
@coverage.find_all { |i,h| @files[i] }.size
|
455
|
+
end
|
456
|
+
|
457
|
+
WONLY = /^\s*(end)?\s*$/
|
458
|
+
|
459
|
+
def each_file
|
460
|
+
processed_files do |file, hits|
|
461
|
+
sf = SourceFile.new(file, hits)
|
462
|
+
sf.update_coverage
|
463
|
+
@total_lines += sf.loc
|
464
|
+
@total_missed += sf.misses
|
465
|
+
yield sf
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
def to_ascii(compact=false)
|
470
|
+
output = ""
|
471
|
+
processed_files do |file, hits|
|
472
|
+
sf = SourceFile.new(file, hits)
|
473
|
+
sf.update_coverage
|
474
|
+
output << "================ #{file} (#{sf.total} / #{sf.loc} / #{sf.misses} / #{sf.loc_percentage}%)\n"
|
475
|
+
if compact
|
476
|
+
output << sf.to_ascii_compact
|
477
|
+
else
|
478
|
+
output << sf.to_ascii
|
479
|
+
end
|
480
|
+
end
|
481
|
+
return output
|
482
|
+
end
|
483
|
+
|
484
|
+
def stats
|
485
|
+
output = ""
|
486
|
+
output << "Total files: #{num_files}\n"
|
487
|
+
output << "\n"
|
488
|
+
total_lines = 0
|
489
|
+
total_missed = 0
|
490
|
+
each_file do |sf|
|
491
|
+
output << "#{sf.path}: #{sf.total}, #{sf.loc}, #{sf.misses}, #{sf.loc_percentage}%\n"
|
492
|
+
end
|
493
|
+
output << "\nTotal LOC: #{total_lines}\n"
|
494
|
+
output << "Total Missed: #{total_missed}\n"
|
495
|
+
output << "Overall Coverage: #{overall_percent.to_i}%\n"
|
496
|
+
end
|
497
|
+
|
498
|
+
def to_xml
|
499
|
+
output = ""
|
500
|
+
xm = Builder::XmlMarkup.new(:target=>output, :indent=>2)
|
501
|
+
xm.coverage(:pwd => Dir.getwd, :time => Time.now.to_i) do
|
502
|
+
each_file do |sf|
|
503
|
+
sf.to_xml xm
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
return output
|
508
|
+
end
|
509
|
+
|
510
|
+
def overall_percent
|
511
|
+
((@total_lines - @total_missed) / @total_lines.to_f) * 100
|
512
|
+
end
|
513
|
+
|
514
|
+
def to_html(dir)
|
515
|
+
dir = Pathname.new(dir)
|
516
|
+
paths = []
|
517
|
+
STDOUT.sync = true
|
518
|
+
print "Writing out html (#{num_files} total): "
|
519
|
+
i = 1
|
520
|
+
each_file do |sf|
|
521
|
+
print "\b\b\b\b\b #{"%3d" % [(i / num_files.to_f) * 100]}%"
|
522
|
+
path = dir + "#{sf.path.to_s.gsub("/","--")}.html"
|
523
|
+
sum = dir + "#{sf.path.to_s.gsub("/","--")}-summary.html"
|
524
|
+
|
525
|
+
paths << [path, sum, sf]
|
526
|
+
path.open("w") do |fd|
|
527
|
+
xm = Builder::XmlMarkup.new(:target=>fd, :indent=>2)
|
528
|
+
xm.html do
|
529
|
+
xm.head do
|
530
|
+
xm.link :rel => "stylesheet", :type => "text/css", :href => "default.css"
|
531
|
+
end
|
532
|
+
xm.body do
|
533
|
+
sf.to_html xm
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
sum.open("w") do |fd|
|
538
|
+
xm = Builder::XmlMarkup.new(:target => fd, :indent => 2)
|
539
|
+
xm.html do
|
540
|
+
xm.body do
|
541
|
+
sf.html_method_summary xm
|
542
|
+
end
|
543
|
+
end
|
544
|
+
end
|
545
|
+
i += 1
|
546
|
+
end
|
547
|
+
|
548
|
+
css = dir + "default.css"
|
549
|
+
unless css.exist?
|
550
|
+
css.open("w") do |fd|
|
551
|
+
fd << File.read(File.join(File.dirname(__FILE__), @css))
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
color2 = "#dedede"
|
556
|
+
color1 = "#a8b1f9"
|
557
|
+
|
558
|
+
cur_color = color2
|
559
|
+
|
560
|
+
index = dir + "index.html"
|
561
|
+
index.open("w") do |fd|
|
562
|
+
xm = Builder::XmlMarkup.new(:target => fd, :indent => 2)
|
563
|
+
xm.html do
|
564
|
+
xm.body do
|
565
|
+
xm.h3 "Code Coverage Information"
|
566
|
+
xm.h5 do
|
567
|
+
xm.text!("Total Coverage: ")
|
568
|
+
xm.b("#{overall_percent.to_i}%")
|
569
|
+
end
|
570
|
+
xm.h5 do
|
571
|
+
xm.text!("Number of Files: ")
|
572
|
+
xm.b(num_files)
|
573
|
+
end
|
574
|
+
|
575
|
+
xm.table(:cellpadding => 3, :cellspacing => 1) do
|
576
|
+
xm.tr do
|
577
|
+
xm.th("File")
|
578
|
+
xm.th("Total")
|
579
|
+
xm.th("Missed")
|
580
|
+
xm.th("Methods Missed")
|
581
|
+
xm.th("LOC")
|
582
|
+
xm.th("LOC %")
|
583
|
+
xm.th("Coverage")
|
584
|
+
xm.th("Coverage %")
|
585
|
+
end
|
586
|
+
paths.each do |path, sum, sf|
|
587
|
+
cur_color = (cur_color == color1 ? color2 : color1)
|
588
|
+
xm.tr(:bgcolor => cur_color) do
|
589
|
+
xm.td do
|
590
|
+
xm.a(sf.path, :href => path.basename)
|
591
|
+
xm.a("[M]", :href => sum.basename)
|
592
|
+
end
|
593
|
+
sf.html_summary(xm)
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
puts "\b\b\b\b\b done."
|
602
|
+
end
|
603
|
+
end
|
604
|
+
end
|