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.
@@ -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