simplecov (0.16.1)のコードリーディングをしました。
SimpleCov.startは以下のように定義されており、Coverage.startを呼び出します。
module SimpleCov
class << self
attr_accessor :running
attr_accessor :pid
def start(profile = nil, &block)
if SimpleCov.usable?
load_profile(profile) if profile
configure(&block) if block_given?
@result = nil
self.running = true
self.pid = Process.pid
Coverage.start
else
warn "WARNING: SimpleCov is activated, but you're not running Ruby 1.9+ - no coverage analysis will happen"
warn "Starting with SimpleCov 1.0.0, even no-op compatibility with Ruby <= 1.8 will be entirely dropped."
false
end
end
lib/simplecov.rb内ではsimplecov/defaultsも読み込まれます。
require "simplecov/defaults" unless ENV["SIMPLECOV_NO_DEFAULTS"]
simplecov/defaults.rbはSimpleCovに関する各種設定を行います。 Kernel.at_exitでプロセス終了時に処理を実行します。
SimpleCov.configure do
formatter SimpleCov::Formatter::HTMLFormatter
load_profile "bundler_filter"
# Exclude files outside of SimpleCov.root
load_profile "root_filter"
end
# Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
at_exit do
# If we are in a different process than called start, don't interfere.
next if SimpleCov.pid != Process.pid
SimpleCov.set_exit_exception
SimpleCov.run_exit_tasks!
end
# Autoload config from ~/.simplecov if present
require "simplecov/load_global_config"
# Autoload config from .simplecov if present
# Recurse upwards until we find .simplecov or reach the root directory
config_path = Pathname.new(SimpleCov.root)
loop do
filename = config_path.join(".simplecov")
if filename.exist?
begin
load filename
rescue LoadError, StandardError
$stderr.puts "Warning: Error occurred while trying to load #{filename}. " \
"Error message: #{$!.message}"
end
break
end
config_path, = config_path.split
break if config_path.root?
end
SimpleCov.run_exit_tasks!は以下のように定義されており、SimpleCov.at_exit.callを呼び出します。
def run_exit_tasks!
exit_status = SimpleCov.exit_status_from_exception
SimpleCov.at_exit.call
exit_status = SimpleCov.process_result(SimpleCov.result, exit_status)
# Force exit with stored status (see github issue #5)
# unless it's nil or 0 (see github issue #281)
Kernel.exit exit_status if exit_status && exit_status > 0
end
SimpleCov.at_exitはSimpleCov::Configuration.at_exitで、SimpleCov.result.format!を呼び出すprocになります。
module SimpleCov
module Configuration # rubocop:disable ModuleLength
def at_exit(&block)
return proc {} unless running || block_given?
@at_exit = block if block_given?
@at_exit ||= proc { SimpleCov.result.format! }
end
SimpleCov.resultはCoverage.resultとloadされていないファイルをマージして返します。
def add_not_loaded_files(result)
if tracked_files
result = result.dup
Dir[tracked_files].each do |file|
absolute = File.expand_path(file)
result[absolute] ||= LinesClassifier.new.classify(File.foreach(absolute))
end
end
result
end
def result
return @result if result?
# Collect our coverage result
if running
@result = SimpleCov::Result.new add_not_loaded_files(Coverage.result)
end
# If we're using merging of results, store the current result
# first (if there is one), then merge the results and return those
if use_merging
SimpleCov::ResultMerger.store_result(@result) if result?
@result = SimpleCov::ResultMerger.merged_result
end
@result
ensure
self.running = false
end
Simplecov::Result.format!はformatterのformatメソッドを呼び出します。simplecov/defaults.rbを読み込んでいる場合、formatterはSimpleCov::Formatter::HTMLFormatterになります。
module SimpleCov
class Result
# ...
def initialize(original_result)
@original_result = original_result.freeze
@files = SimpleCov::FileList.new(original_result.map do |filename, coverage|
SimpleCov::SourceFile.new(filename, coverage) if File.file?(filename)
end.compact.sort_by(&:filename))
filter!
end
def format!
SimpleCov.formatter.new.format(self)
end
HTMLFormatter#formatは以下のように定義されています。simplecov-htmlのgemのpublicディレクトリのファイル群をassetの出力パスにコピーし、index.htmlにファイルの内容を書き出します。
module SimpleCov
module Formatter
class HTMLFormatter
def format(result)
Dir[File.join(File.dirname(__FILE__), "../public/*")].each do |path|
FileUtils.cp_r(path, asset_output_path)
end
File.open(File.join(output_path, "index.html"), "wb") do |file|
file.puts template("layout").result(binding)
end
puts output_message(result)
end
templateはERBのインスタンスでlayout.erbのファイルをHTMLFormatterのインスタンスの#format呼び出し時のbindingでレンダリングしています。
# Returns the an erb instance for the template of given name
def template(name)
ERB.new(File.read(File.join(File.dirname(__FILE__), "../views/", "#{name}.erb")))
end
layout.erbは以下のように定義されています。
<!DOCTYPE html>
<html xmlns='http://www.w3.org/1999/xhtml'>
<head>
# ...
</head>
<body>
<div id="loading">
<img src="<%= assets_path('loading.gif') %>" alt="loading"/>
</div>
<div id="wrapper" style="display:none;">
<div class="timestamp">Generated <%= timeago(Time.now) %></div>
<ul class="group_tabs"></ul>
<div id="content">
<%= formatted_file_list("All Files", result.source_files) %>
<% result.groups.each do |name, files| %>
<%= formatted_file_list(name, files) %>
<% end %>
</div>
<div id="footer">
Generated by <a href="http://github.com/colszowka/simplecov">simplecov</a> v<%= SimpleCov::VERSION %>
and simplecov-html v<%= SimpleCov::Formatter::HTMLFormatter::VERSION %><br/>
using <%= result.command_name %>
</div>
<div class="source_files">
<% result.source_files.each do |source_file| %>
<%= formatted_source_file(source_file) %>
<% end %>
</div>
</div>
</body>
</html>
formatted_file_listはtemplateメソッドを使ってfile_list.erbのテンプレートをレンダリングします。source_filesの引数は一見使って無さそうに見えますが、bindingを使ってfile_list.erbのレンダリング時に利用しています。
def formatted_file_list(title, source_files)
title_id = title.gsub(/^[^a-zA-Z]+/, "").gsub(/[^a-zA-Z0-9\-\_]/, "")
# Silence a warning by using the following variable to assign to itself:
# "warning: possibly useless use of a variable in void context"
# The variable is used by ERB via binding.
title_id = title_id
template("file_list").result(binding)
end
#formatted_source_fileはsource_fileのレンダリングをします。この部分はjQueryのhide関数で非表示になっており、colorboxを使ってクリック時にポップアップでソースコードを表示する処理を実現しています。
<div class="source_table" id="<%= id source_file %>">
<div class="header">
<h2><%= shortened_filename source_file %></h2>
<h3><span class="<%= coverage_css_class(source_file.covered_percent) %>"><%= source_file.covered_percent.round(2).to_s %> %</span> covered</h3>
<div>
<b><%= source_file.lines_of_code %></b> relevant lines.
<span class="green"><b><%= source_file.covered_lines.count %></b> lines covered</span> and
<span class="red"><b><%= source_file.missed_lines.count %></b> lines missed.</span>
</div>
</div>
<pre>
<ol>
<% source_file.lines.each do |line| %>
<li class="<%= line.status %>" data-hits="<%= line.coverage ? line.coverage : '' %>" data-linenumber="<%= line.number %>">
<% if line.covered? %><span class="hits"><%= line.coverage %></span><% end %>
<% if line.skipped? %><span class="hits">skipped</span><% end %>
<code class="ruby"><%= CGI.escapeHTML(line.src.chomp) %></code>
</li>
<% end %>
</ol>
</pre>
</div>