silhouette 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = '1.0.0'
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'
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'optparse'
3
- require 'silhouette/processor'
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
- io = File.open(file)
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
@@ -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
@@ -1,16 +1,8 @@
1
+ require 'silhouette/setup'
1
2
 
2
- require "silhouette_ext"
3
-
4
- at_exit {
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 file
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