rcov 0.8.1.2.0 → 0.9.3

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.
Files changed (68) hide show
  1. data/BLURB +2 -40
  2. data/LICENSE +2 -5
  3. data/Rakefile +32 -106
  4. data/THANKS +14 -0
  5. data/bin/rcov +277 -1090
  6. data/doc/readme_for_api.markdown +22 -0
  7. data/doc/readme_for_emacs.markdown +52 -0
  8. data/doc/readme_for_rake.markdown +51 -0
  9. data/doc/readme_for_vim.markdown +34 -0
  10. data/{rcov.el → editor-extensions/rcov.el} +0 -0
  11. data/{rcov.vim → editor-extensions/rcov.vim} +0 -0
  12. data/ext/rcovrt/1.8/callsite.c +216 -0
  13. data/ext/rcovrt/1.8/rcovrt.c +287 -0
  14. data/ext/rcovrt/1.9/callsite.c +234 -0
  15. data/ext/rcovrt/1.9/rcovrt.c +264 -0
  16. data/ext/rcovrt/extconf.rb +12 -2
  17. data/lib/rcov.rb +13 -968
  18. data/lib/rcov/call_site_analyzer.rb +225 -0
  19. data/lib/rcov/code_coverage_analyzer.rb +268 -0
  20. data/lib/rcov/coverage_info.rb +36 -0
  21. data/lib/rcov/differential_analyzer.rb +116 -0
  22. data/lib/rcov/file_statistics.rb +334 -0
  23. data/lib/rcov/formatters.rb +13 -0
  24. data/lib/rcov/formatters/base_formatter.rb +173 -0
  25. data/lib/rcov/formatters/failure_report.rb +15 -0
  26. data/lib/rcov/formatters/full_text_report.rb +48 -0
  27. data/lib/rcov/formatters/html_coverage.rb +274 -0
  28. data/lib/rcov/formatters/html_erb_template.rb +62 -0
  29. data/lib/rcov/formatters/text_coverage_diff.rb +193 -0
  30. data/lib/rcov/formatters/text_report.rb +32 -0
  31. data/lib/rcov/formatters/text_summary.rb +11 -0
  32. data/lib/rcov/lowlevel.rb +16 -17
  33. data/lib/rcov/rcovtask.rb +21 -22
  34. data/lib/rcov/templates/detail.html.erb +64 -0
  35. data/lib/rcov/templates/index.html.erb +93 -0
  36. data/lib/rcov/templates/jquery-1.3.2.min.js +19 -0
  37. data/lib/rcov/templates/jquery.tablesorter.min.js +15 -0
  38. data/lib/rcov/templates/print.css +12 -0
  39. data/lib/rcov/templates/rcov.js +42 -0
  40. data/lib/rcov/templates/screen.css +270 -0
  41. data/lib/rcov/version.rb +5 -8
  42. data/setup.rb +5 -2
  43. data/test/{sample_01.rb → assets/sample_01.rb} +0 -0
  44. data/test/{sample_02.rb → assets/sample_02.rb} +0 -0
  45. data/test/{sample_03.rb → assets/sample_03.rb} +0 -0
  46. data/test/{sample_04.rb → assets/sample_04.rb} +0 -0
  47. data/test/{sample_05-new.rb → assets/sample_05-new.rb} +0 -0
  48. data/test/{sample_05-old.rb → assets/sample_05-old.rb} +0 -0
  49. data/test/{sample_05.rb → assets/sample_05.rb} +0 -0
  50. data/test/{test_CallSiteAnalyzer.rb → call_site_analyzer_test.rb} +57 -81
  51. data/test/{test_CodeCoverageAnalyzer.rb → code_coverage_analyzer_test.rb} +71 -35
  52. data/test/{test_FileStatistics.rb → file_statistics_test.rb} +34 -36
  53. data/test/{test_functional.rb → functional_test.rb} +21 -35
  54. metadata +91 -69
  55. data/CHANGES +0 -177
  56. data/LEGAL +0 -36
  57. data/README.API +0 -42
  58. data/README.emacs +0 -64
  59. data/README.en +0 -130
  60. data/README.rake +0 -62
  61. data/README.rant +0 -68
  62. data/README.vim +0 -47
  63. data/Rantfile +0 -76
  64. data/ext/rcovrt/callsite.c +0 -242
  65. data/ext/rcovrt/rcovrt.c +0 -329
  66. data/lib/rcov/rant.rb +0 -87
  67. data/lib/rcov/report.rb +0 -1236
  68. data/mingw-rbconfig.rb +0 -174
@@ -0,0 +1,13 @@
1
+ require 'rcov/formatters/html_erb_template'
2
+ require 'rcov/formatters/base_formatter'
3
+ require 'rcov/formatters/text_summary'
4
+ require 'rcov/formatters/text_report'
5
+ require 'rcov/formatters/text_coverage_diff'
6
+ require 'rcov/formatters/full_text_report'
7
+ require 'rcov/formatters/html_coverage'
8
+ require 'rcov/formatters/failure_report'
9
+
10
+ module Rcov
11
+ module Formatters
12
+ end
13
+ end
@@ -0,0 +1,173 @@
1
+ module Rcov
2
+ class BaseFormatter # :nodoc:
3
+ require 'pathname'
4
+
5
+ if RUBY_PLATFORM =~ /java/
6
+ ignore_files = [ /\btc_[^.]*.rb/, /_test\.rb\z/, /\btest\//, /\bvendor\//, /\A#{Regexp.escape(__FILE__)}\z/]
7
+ else
8
+ require 'mkmf'
9
+ ignore_files = [/\A#{Regexp.escape(Pathname.new(::Config::CONFIG['libdir']).cleanpath.to_s)}/, /\btc_[^.]*.rb/, /_test\.rb\z/, /\btest\//, /\bvendor\//, /\A#{Regexp.escape(__FILE__)}\z/]
10
+ end
11
+
12
+ DEFAULT_OPTS = { :ignore => ignore_files, :sort => :name, :sort_reverse => false,
13
+ :output_threshold => 101, :dont_ignore => [], :callsite_analyzer => nil, \
14
+ :comments_run_by_default => false }
15
+
16
+ def initialize(opts = {})
17
+ options = DEFAULT_OPTS.clone.update(opts)
18
+ @failure_threshold = options[:failure_threshold]
19
+ @files = {}
20
+ @ignore_files = options[:ignore]
21
+ @dont_ignore_files = options[:dont_ignore]
22
+ @sort_criterium = case options[:sort]
23
+ when :loc then lambda{|fname, finfo| finfo.num_code_lines}
24
+ when :coverage then lambda{|fname, finfo| finfo.code_coverage}
25
+ else lambda { |fname, finfo| fname }
26
+ end
27
+ @sort_reverse = options[:sort_reverse]
28
+ @output_threshold = options[:output_threshold]
29
+ @callsite_analyzer = options[:callsite_analyzer]
30
+ @comments_run_by_default = options[:comments_run_by_default]
31
+ @callsite_index = nil
32
+
33
+ @mangle_filename = Hash.new{|h,base|
34
+ h[base] = Pathname.new(base).cleanpath.to_s.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/[\\\/]/, "-") + ".html"
35
+ }
36
+ end
37
+
38
+ def add_file(filename, lines, coverage, counts)
39
+ old_filename = filename
40
+ filename = normalize_filename(filename)
41
+ SCRIPT_LINES__[filename] = SCRIPT_LINES__[old_filename]
42
+ if @ignore_files.any?{|x| x === filename} &&
43
+ !@dont_ignore_files.any?{|x| x === filename}
44
+ return nil
45
+ end
46
+ if @files[filename]
47
+ @files[filename].merge(lines, coverage, counts)
48
+ else
49
+ @files[filename] = FileStatistics.new(filename, lines, counts,
50
+ @comments_run_by_default)
51
+ end
52
+ end
53
+
54
+ def normalize_filename(filename)
55
+ File.expand_path(filename).gsub(/^#{Regexp.escape(Dir.getwd)}\//, '')
56
+ end
57
+
58
+ def mangle_filename(base)
59
+ @mangle_filename[base]
60
+ end
61
+
62
+ def each_file_pair_sorted(&b)
63
+ return sorted_file_pairs unless block_given?
64
+ sorted_file_pairs.each(&b)
65
+ end
66
+
67
+ def sorted_file_pairs
68
+ pairs = @files.sort_by do |fname, finfo|
69
+ @sort_criterium.call(fname, finfo)
70
+ end.select{|_, finfo| 100 * finfo.code_coverage < @output_threshold}
71
+ @sort_reverse ? pairs.reverse : pairs
72
+ end
73
+
74
+ def total_coverage
75
+ lines = 0
76
+ total = 0.0
77
+ @files.each do |k,f|
78
+ total += f.num_lines * f.total_coverage
79
+ lines += f.num_lines
80
+ end
81
+ return 0 if lines == 0
82
+ total / lines
83
+ end
84
+
85
+ def code_coverage
86
+ lines = 0
87
+ total = 0.0
88
+ @files.each do |k,f|
89
+ total += f.num_code_lines * f.code_coverage
90
+ lines += f.num_code_lines
91
+ end
92
+ return 0 if lines == 0
93
+ total / lines
94
+ end
95
+
96
+ def num_code_lines
97
+ lines = 0
98
+ @files.each{|k, f| lines += f.num_code_lines }
99
+ lines
100
+ end
101
+
102
+ def num_lines
103
+ lines = 0
104
+ @files.each{|k, f| lines += f.num_lines }
105
+ lines
106
+ end
107
+
108
+ private
109
+
110
+ def cross_references_for(filename, lineno)
111
+ return nil unless @callsite_analyzer
112
+ @callsite_index ||= build_callsite_index
113
+ @callsite_index[normalize_filename(filename)][lineno]
114
+ end
115
+
116
+ def reverse_cross_references_for(filename, lineno)
117
+ return nil unless @callsite_analyzer
118
+ @callsite_reverse_index ||= build_reverse_callsite_index
119
+ @callsite_reverse_index[normalize_filename(filename)][lineno]
120
+ end
121
+
122
+ def build_callsite_index
123
+ index = Hash.new{|h,k| h[k] = {}}
124
+ @callsite_analyzer.analyzed_classes.each do |classname|
125
+ @callsite_analyzer.analyzed_methods(classname).each do |methname|
126
+ defsite = @callsite_analyzer.defsite(classname, methname)
127
+ index[normalize_filename(defsite.file)][defsite.line] =
128
+ @callsite_analyzer.callsites(classname, methname)
129
+ end
130
+ end
131
+ index
132
+ end
133
+
134
+ def build_reverse_callsite_index
135
+ index = Hash.new{|h,k| h[k] = {}}
136
+ @callsite_analyzer.analyzed_classes.each do |classname|
137
+ @callsite_analyzer.analyzed_methods(classname).each do |methname|
138
+ callsites = @callsite_analyzer.callsites(classname, methname)
139
+ defsite = @callsite_analyzer.defsite(classname, methname)
140
+ callsites.each_pair do |callsite, count|
141
+ next unless callsite.file
142
+ fname = normalize_filename(callsite.file)
143
+ (index[fname][callsite.line] ||= []) << [classname, methname, defsite, count]
144
+ end
145
+ end
146
+ end
147
+ index
148
+ end
149
+
150
+ class XRefHelper < Struct.new(:file, :line, :klass, :mid, :count) # :nodoc:
151
+ end
152
+
153
+ def _get_defsites(ref_blocks, filename, lineno, linetext, label, &format_call_ref)
154
+ if @do_cross_references and
155
+ (rev_xref = reverse_cross_references_for(filename, lineno))
156
+ refs = rev_xref.map do |classname, methodname, defsite, count|
157
+ XRefHelper.new(defsite.file, defsite.line, classname, methodname, count)
158
+ end.sort_by{|r| r.count}.reverse
159
+ ref_blocks << [refs, label, format_call_ref]
160
+ end
161
+ end
162
+
163
+ def _get_callsites(ref_blocks, filename, lineno, linetext, label, &format_called_ref)
164
+ if @do_callsites and
165
+ (refs = cross_references_for(filename, lineno))
166
+ refs = refs.sort_by{|k,count| count}.map do |ref, count|
167
+ XRefHelper.new(ref.file, ref.line, ref.calling_class, ref.calling_method, count)
168
+ end.reverse
169
+ ref_blocks << [refs, label, format_called_ref]
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,15 @@
1
+ module Rcov
2
+ class FailureReport < TextSummary # :nodoc:
3
+ def execute
4
+ puts summary
5
+ coverage = code_coverage * 100
6
+ if coverage < @failure_threshold
7
+ puts "You failed to satisfy the coverage theshold of #{@failure_threshold}%"
8
+ exit(1)
9
+ end
10
+ if (coverage - @failure_threshold) > 3
11
+ puts "Your coverage has significantly increased over your threshold of #{@failure_threshold}. Please increase it."
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ module Rcov
2
+ class FullTextReport < BaseFormatter # :nodoc:
3
+ DEFAULT_OPTS = {:textmode => :coverage}
4
+
5
+ def initialize(opts = {})
6
+ options = DEFAULT_OPTS.clone.update(opts)
7
+ @textmode = options[:textmode]
8
+ @color = options[:color]
9
+ super(options)
10
+ end
11
+
12
+ def execute
13
+ each_file_pair_sorted do |filename, fileinfo|
14
+ puts "=" * 80
15
+ puts filename
16
+ puts "=" * 80
17
+ lines = SCRIPT_LINES__[filename]
18
+
19
+ unless lines
20
+ # try to get the source code from the global code coverage
21
+ # analyzer
22
+ re = /#{Regexp.escape(filename)}\z/
23
+ if $rcov_code_coverage_analyzer and
24
+ (data = $rcov_code_coverage_analyzer.data_matching(re))
25
+ lines = data[0]
26
+ end
27
+ end
28
+
29
+ (lines || []).each_with_index do |line, i|
30
+ case @textmode
31
+ when :counts
32
+ puts "%-70s| %6d" % [line.chomp[0,70], fileinfo.counts[i]]
33
+ when :gcc
34
+ puts "%s:%d:%s" % [filename, i+1, line.chomp] unless fileinfo.coverage[i]
35
+ when :coverage
36
+ if @color
37
+ prefix = fileinfo.coverage[i] ? "\e[32;40m" : "\e[31;40m"
38
+ puts "#{prefix}%s\e[37;40m" % line.chomp
39
+ else
40
+ prefix = fileinfo.coverage[i] ? " " : "!! "
41
+ puts "#{prefix}#{line}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,274 @@
1
+ module Rcov
2
+ class HTMLCoverage < BaseFormatter # :nodoc:
3
+ require 'fileutils'
4
+
5
+ DEFAULT_OPTS = {:color => false, :fsr => 30, :destdir => "coverage",
6
+ :callsites => false, :cross_references => false,
7
+ :charset => nil }
8
+
9
+ def initialize(opts = {})
10
+ options = DEFAULT_OPTS.clone.update(opts)
11
+ super(options)
12
+ @dest = options[:destdir]
13
+ @css = options[:css]
14
+ @color = options[:color]
15
+ @fsr = options[:fsr]
16
+ @do_callsites = options[:callsites]
17
+ @do_cross_references = options[:cross_references]
18
+ @span_class_index = 0
19
+ @charset = options[:charset]
20
+ end
21
+
22
+ def execute
23
+ return if @files.empty?
24
+ FileUtils.mkdir_p @dest
25
+
26
+ # Copy collaterals
27
+ ['screen.css','print.css','rcov.js','jquery-1.3.2.min.js','jquery.tablesorter.min.js'].each do |_file|
28
+ _src = File.expand_path("#{File.dirname(__FILE__)}/../templates/#{_file}")
29
+ FileUtils.cp(_src, File.join(@dest, "#{_file}"))
30
+ end
31
+
32
+ # Copy custom CSS, if any
33
+ if @css
34
+ begin
35
+ _src = File.expand_path("#{@dest}/../#{@css}")
36
+ FileUtils.cp(_src, File.join(@dest, "custom.css"))
37
+ rescue
38
+ @css = nil
39
+ end
40
+ end
41
+
42
+ create_index(File.join(@dest, "index.html"))
43
+
44
+ each_file_pair_sorted do |filename, fileinfo|
45
+ create_file(File.join(@dest, mangle_filename(filename)), fileinfo)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ class SummaryFileInfo # :nodoc:
52
+ def initialize(obj)
53
+ @o = obj
54
+ end
55
+
56
+ def num_lines
57
+ @o.num_lines
58
+ end
59
+
60
+ def num_code_lines
61
+ @o.num_code_lines
62
+ end
63
+
64
+ def code_coverage
65
+ @o.code_coverage
66
+ end
67
+
68
+ def code_coverage_for_report
69
+ code_coverage * 100
70
+ end
71
+
72
+ def total_coverage
73
+ @o.total_coverage
74
+ end
75
+
76
+ def total_coverage_for_report
77
+ total_coverage * 100
78
+ end
79
+
80
+ def name
81
+ "TOTAL"
82
+ end
83
+ end
84
+
85
+ def create_index(destname)
86
+
87
+ doc = Rcov::Formatters::HtmlErbTemplate.new('index.html.erb',
88
+ :project_name => project_name,
89
+ :generated_on => Time.now,
90
+ :css => @css,
91
+ :rcov => Rcov,
92
+ :formatter => self,
93
+ :output_threshold => @output_threshold,
94
+ :total => SummaryFileInfo.new(self),
95
+ :files => each_file_pair_sorted.map{|k,v| v}
96
+ )
97
+ File.open(destname, "w") { |f| f.puts doc.render }
98
+ end
99
+
100
+ def create_file(destfile, fileinfo)
101
+ doc = Rcov::Formatters::HtmlErbTemplate.new('detail.html.erb',
102
+ :project_name => project_name,
103
+ :page_title => fileinfo.name,
104
+ :css => @css,
105
+ :generated_on => Time.now,
106
+ :rcov => Rcov,
107
+ :formatter => self,
108
+ :output_threshold => @output_threshold,
109
+ :fileinfo => fileinfo
110
+ )
111
+ File.open(destfile, "w") { |f| f.puts doc.render }
112
+ end
113
+
114
+ private
115
+
116
+ def project_name
117
+ Dir.pwd.split('/')[-1].split(/[^a-zA-Z0-9]/).map{|i| i.gsub(/[^a-zA-Z0-9]/,'').capitalize} * " " || ""
118
+ end
119
+
120
+ end
121
+
122
+ class HTMLProfiling < HTMLCoverage # :nodoc:
123
+ DEFAULT_OPTS = {:destdir => "profiling"}
124
+ def initialize(opts = {})
125
+ options = DEFAULT_OPTS.clone.update(opts)
126
+ super(options)
127
+ @max_cache = {}
128
+ @median_cache = {}
129
+ end
130
+
131
+ def default_title
132
+ "Bogo-profile information"
133
+ end
134
+
135
+ def default_color
136
+ if @color
137
+ "rgb(179,205,255)"
138
+ else
139
+ "rgb(255, 255, 255)"
140
+ end
141
+ end
142
+
143
+ def output_color_table?
144
+ false
145
+ end
146
+
147
+ def span_class(sourceinfo, marked, count)
148
+ full_scale_range = @fsr # dB
149
+ nz_count = sourceinfo.counts.select{|x| x && x != 0}
150
+ nz_count << 1 # avoid div by 0
151
+ max = @max_cache[sourceinfo] ||= nz_count.max
152
+ #avg = @median_cache[sourceinfo] ||= 1.0 *
153
+ # nz_count.inject{|a,b| a+b} / nz_count.size
154
+ median = @median_cache[sourceinfo] ||= 1.0 * nz_count.sort[nz_count.size/2]
155
+ max ||= 2
156
+ max = 2 if max == 1
157
+ if marked == true
158
+ count = 1 if !count || count == 0
159
+ idx = 50 + 1.0 * (500/full_scale_range) * Math.log(count/median) / Math.log(10)
160
+ idx = idx.to_i
161
+ idx = 0 if idx < 0
162
+ idx = 100 if idx > 100
163
+ "run#{idx}"
164
+ else
165
+ nil
166
+ end
167
+ end
168
+ end
169
+
170
+ class RubyAnnotation < BaseFormatter # :nodoc:
171
+ DEFAULT_OPTS = { :destdir => "coverage" }
172
+ def initialize(opts = {})
173
+ options = DEFAULT_OPTS.clone.update(opts)
174
+ super(options)
175
+ @dest = options[:destdir]
176
+ @do_callsites = true
177
+ @do_cross_references = true
178
+
179
+ @mangle_filename = Hash.new{ |h,base|
180
+ h[base] = Pathname.new(base).cleanpath.to_s.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/[\\\/]/, "-") + ".rb"
181
+ }
182
+ end
183
+
184
+ def execute
185
+ return if @files.empty?
186
+ FileUtils.mkdir_p @dest
187
+ each_file_pair_sorted do |filename, fileinfo|
188
+ create_file(File.join(@dest, mangle_filename(filename)), fileinfo)
189
+ end
190
+ end
191
+
192
+ private
193
+
194
+ def format_lines(file)
195
+ result = ""
196
+ format_line = "%#{file.num_lines.to_s.size}d"
197
+ file.num_lines.times do |i|
198
+ line = file.lines[i].chomp
199
+ marked = file.coverage[i]
200
+ count = file.counts[i]
201
+ result << create_cross_refs(file.name, i+1, line, marked) + "\n"
202
+ end
203
+ result
204
+ end
205
+
206
+ def create_cross_refs(filename, lineno, linetext, marked)
207
+ return linetext unless @callsite_analyzer && @do_callsites
208
+ ref_blocks = []
209
+ _get_defsites(ref_blocks, filename, lineno, linetext, ">>") do |ref|
210
+ if ref.file
211
+ ref.file.sub!(%r!^./!, '')
212
+ where = "at #{mangle_filename(ref.file)}:#{ref.line}"
213
+ else
214
+ where = "(C extension/core)"
215
+ end
216
+ "#{ref.klass}##{ref.mid} " + where + ""
217
+ end
218
+ _get_callsites(ref_blocks, filename, lineno, linetext, "<<") do |ref| # "
219
+ ref.file.sub!(%r!^./!, '')
220
+ "#{mangle_filename(ref.file||'C code')}:#{ref.line} " + "in #{ref.klass}##{ref.mid}"
221
+ end
222
+
223
+ create_cross_reference_block(linetext, ref_blocks, marked)
224
+ end
225
+
226
+ def create_cross_reference_block(linetext, ref_blocks, marked)
227
+ codelen = 75
228
+ if ref_blocks.empty?
229
+ if marked
230
+ return "%-#{codelen}s #o" % linetext
231
+ else
232
+ return linetext
233
+ end
234
+ end
235
+ ret = ""
236
+ @cross_ref_idx ||= 0
237
+ @known_files ||= sorted_file_pairs.map{|fname, finfo| normalize_filename(fname)}
238
+ ret << "%-#{codelen}s # " % linetext
239
+ ref_blocks.each do |refs, toplabel, label_proc|
240
+ unless !toplabel || toplabel.empty?
241
+ ret << toplabel << " "
242
+ end
243
+ refs.each do |dst|
244
+ dstfile = normalize_filename(dst.file) if dst.file
245
+ dstline = dst.line
246
+ label = label_proc.call(dst)
247
+ if dst.file && @known_files.include?(dstfile)
248
+ ret << "[[" << label << "]], "
249
+ else
250
+ ret << label << ", "
251
+ end
252
+ end
253
+ end
254
+ ret
255
+ end
256
+
257
+ def create_file(destfile, fileinfo)
258
+ #body = format_lines(fileinfo)
259
+ #File.open(destfile, "w") do |f|
260
+ #f.puts body
261
+ #f.puts footer(fileinfo)
262
+ #end
263
+ end
264
+
265
+ def footer(fileinfo)
266
+ s = "# Total lines : %d\n" % fileinfo.num_lines
267
+ s << "# Lines of code : %d\n" % fileinfo.num_code_lines
268
+ s << "# Total coverage : %3.1f%%\n" % [ fileinfo.total_coverage*100 ]
269
+ s << "# Code coverage : %3.1f%%\n\n" % [ fileinfo.code_coverage*100 ]
270
+ # prevents false positives on Emacs
271
+ s << "# Local " "Variables:\n" "# mode: " "rcov-xref\n" "# End:\n"
272
+ end
273
+ end
274
+ end