lib/test_prof.rb



# frozen_string_literal: true

require "fileutils"
require "test_prof/version"
require "test_prof/logging"
require "test_prof/utils"

# Ruby applications tests profiling tools.
#
# Contains tools to anylyze factories usage, integrate with Ruby profilers,
# profile your examples using ActiveSupport notifications (if any) and
# statically analyze your code with custom Rubocop cops.
#
# Example usage:
#
#   require 'test_prof'
#
#   # Activate a tool by providing environment variable, e.g.
#   TEST_RUBY_PROF=1 rspec ...
#
#   # or manually in your code
#   TestProf::RubyProf.run
#
# See other modules for more examples.
module TestProf
  class << self
    include Logging

    def config
      @config ||= Configuration.new
    end

    def configure
      yield config
    end

    # Avoid issues with wrong time due to monkey-patches (e.g. timecop)
    # See https://github.com/rspec/rspec-core/blob/v3.6.0/lib/rspec/core.rb#L147
    #
    # We also want to handle Timecop specificaly
    # See https://github.com/travisjeffery/timecop/blob/master/lib/timecop/time_extensions.rb#L11
    if Time.respond_to?(:now_without_mock_time)
      define_method(:now, &::Time.method(:now_without_mock_time))
    else
      define_method(:now, &::Time.method(:now))
    end

    # Require gem and shows a custom
    # message if it fails to load
    def require(gem_name, msg)
      Kernel.require gem_name
      block_given? ? yield : true
    rescue LoadError
      log :error, msg
      false
    end

    # Run block only if provided env var is present and
    # equal to the provided value (if any).
    # Contains workaround for applications using Spring.
    def activate(env_var, val = nil)
      if defined?(::Spring)
        ::Spring.after_fork { activate!(env_var, val) { yield } }
      else
        activate!(env_var, val) { yield }
      end
    end

    # Return absolute path to asset
    def asset_path(filename)
      ::File.expand_path(filename, ::File.join(::File.dirname(__FILE__), "..", "assets"))
    end

    # Return a path to store artifact
    def artifact_path(filename)
      FileUtils.mkdir_p(config.output_dir)

      with_timestamps(
        ::File.join(
          config.output_dir,
          filename
        )
      )
    end

    private

    def activate!(env_var, val)
      yield if ENV[env_var] && (val.nil? || ENV[env_var] == val)
    end

    def with_timestamps(path)
      return path unless config.timestamps?
      timestamps = "-#{now.to_i}"
      "#{path.sub(/\.\w+$/, '')}#{timestamps}#{::File.extname(path)}"
    end
  end

  # TestProf configuration
  class Configuration
    attr_accessor :output,      # IO to write output messages.
                  :color,       # Whether to colorize output or not
                  :output_dir,  # Directory to store artifacts
                  :timestamps   # Whethere to use timestamped names for artifacts

    def initialize
      @output = $stdout
      @color = true
      @output_dir = "tmp/test_prof"
      @timestamps = false
    end

    def color?
      color == true
    end

    def timestamps?
      timestamps == true
    end
  end
end

require "test_prof/ruby_prof"
require "test_prof/stack_prof"
require "test_prof/event_prof"
require "test_prof/factory_doctor"
require "test_prof/factory_prof"
require "test_prof/rspec_stamp"
require "test_prof/tag_prof"