2018-03-28

simplecovコードリーディング

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>
このエントリーをはてなブックマークに追加