silhouette 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|