Skip to content

Instantly share code, notes, and snippets.

@bf4
Last active December 26, 2015 20:29
Show Gist options
  • Save bf4/6592ea3da5ea8e00f396 to your computer and use it in GitHub Desktop.
Save bf4/6592ea3da5ea8e00f396 to your computer and use it in GitHub Desktop.
metric_fu snippets
# from https://github.com/bf4/metric_fu/commit/2485dc9321fd8b8a98e4f07bb333e9167d7cdbb2
@legacy_directory = 'tmp/metric_fu'
@new_directory = '_metric_fu_data'
CLOBBER.include(@new_directory)
SRC_FILES = FileList["#{@legacy_directory}/**/*"]
DEST_FILES = SRC_FILES.pathmap("%{#{@legacy_directory},#{@new_directory}}p")
directory @new_directory
SRC_FILES.zip(DEST_FILES).each do |src, dest|
file dest => [src, @new_directory] do
begin
cp src, dest
rescue Errno::ENOTDIR => e
STDERR.puts "Failed copying #{src} to #{dest} with not a directory #{e.class}"
end
end
end
task :update => DEST_FILES
task :default => :update
# https://github.com/bf4/metric_fu/blob/16447f17db3a2c4199f882e6938732a92be0fe6d/lib/metric_fu/metric_file.rb
MetricFu.lib_require { 'utility' }
MetricFu.lib_require { 'options_hash' }
module MetricFu
def self.MetricFile(metric_file)
case metric_file
when MetricFu::MetricFile then metric_file
else MetricFile.new(metric_file)
end
end
class MetricFile
def self.from_file_pattern(file_pattern)
Dir[file_pattern].sort.map { |filename| new(filename) }
end
attr_reader :filename, :date_parts
def initialize(filename)
@filename = filename
@date_parts = year_month_day_from_filename(filename)
end
def data
@data ||= MetricFu::OptionsHash(MetricFu::Utility.load_yaml_file(filename))
end
def score(metric, score_key)
data.fetch(metric, {})[score_key]
end
def sortable_prefix; end
private
def year_month_day_from_filename(path_to_file_with_date); end
end
end
# https://github.com/bf4/metric_fu/blob/16447f17db3a2c4199f882e6938732a92be0fe6d/lib/metric_fu/options_hash.rb
require 'map'
module MetricFu
def self.OptionsHash(options_hash)
case options_hash
when MetricFu::OptionsHash then OptionsHash
else OptionsHash.new(options_hash)
end
end
class OptionsHash < Map
end
end
# So then I could do
def run(options=OptionsHash.new)
# blah
end
# or
def run(options={})
options = MetricFu::OptionsHash(options)
# blah
end
# SPIKE
def fake_metric(metric_name, has_graph)
Class.new(Metric) do
define_method(:name) do
metric_name
end
def default_run_options
{}
end
define_method(:has_graph?) do
has_graph
end
def enabled
true
end
def activated
true
end
end
end
def fake_generator(metric_name)
Class.new(Generator) do
def self.metric
name.sub('MetricFu::','').sub('Generator','').scan(/[A-Z][a-z]+/).join('_').downcase.intern
end
def emit; @output = '' end
def analyze; end
def to_h; {self.class.metric => {}} end
end
end
MetricWithoutGraph = fake_metric(:metric_without_graph, false)
MetricWithoutGraphGenerator = fake_generator(:metric_without_graph)
MetricWithGraph = fake_metric(:metric_with_graph, true)
MetricWithGraphGenerator = fake_generator(:metric_with_graph)
MetricWithGraphGrapher = Class.new(Grapher) do
def initialize
super
end
def get_metrics(metrics, date)
end
end
MetricWithGraphBluffGrapher = Class.new(MetricWithGraphGrapher) do
def title
'metric_with_graph'
end
def data
[
['metric_with_graph', 0]
]
end
def output_filename
'metric_with_graph.js'
end
end
end
# from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/keys.rb
class Hash
# Destructively, recursively convert all keys to symbols, as long as they respond
# to +to_sym+.
def recursively_symbolize_keys!
keys.each do |key|
value = delete(key)
new_key = key.intern #rescue
self[new_key] = (value.is_a?(Hash) ? value.dup.recursively_symbolize_keys! : value)
end
self
end
end
# see https://github.com/metricfu/metric_fu/blob/master/lib/metric_fu/constantize.rb
# see http://rkh.im/code-reloading
# TODO: This class mostly serves to clean up the base MetricFu module,
# but needs further work
attr_reader :loaded_files
def initialize(lib_root)
@lib_root = lib_root
@loaded_files = []
end
def lib_require(base='',&block)
paths = []
base_path = File.join(@lib_root, base)
Array((yield paths, base_path)).each do |path|
file = File.join(base_path, *Array(path))
require file
if @loaded_files.include?(file)
puts "!!!\tAlready loaded #{file}" if !!(ENV['MF_DEBUG'] =~ /true/i)
else
@loaded_files << file
end
end
end
# TODO: Reduce duplication of directory logic
# https://github.com/bf4/metric_fu/blob/16447f17db3a2c4199f882e6938732a92be0fe6d/lib/metric_fu/loader.rb#L83
# Load specified task task only once
# if and only if rake is required and the task is not yet defined
# to prevent the task from being loaded multiple times
# @param tasks_relative_path [String] 'metric_fu.rake' by default
# @param options [Hash] optional task_name to check if loaded
# @option options [String] :task_name The task_name to load, if not yet loaded
def load_tasks(tasks_relative_path, options={task_name: ''})
if defined?(Rake::Task) and not Rake::Task.task_defined?(options[:task_name])
load File.join(@lib_root, 'tasks', *Array(tasks_relative_path))
end
end
def setup
MetricFu.lib_require { 'options_hash' }
MetricFu.logging_require { 'mf_debugger' }
Object.send :include, MfDebugger
MfDebugger::Logger.debug_on = !!(ENV['MF_DEBUG'] =~ /true/i)
MetricFu.lib_require { 'configuration' }
MetricFu.lib_require { 'metric' }
Dir.glob(File.join(MetricFu.metrics_dir, '**/init.rb')).each{|init_file|require(init_file)}
load_user_configuration
MetricFu.metrics_require { 'generator' }
MetricFu.lib_require { 'reporter' }
MetricFu.reporting_require { 'result' }
load_tasks('metric_fu.rake', task_name: 'metrics:all')
end
def load_user_configuration
file = File.join(Dir.pwd, '.metrics')
load file if File.exist?(file)
end
end
# https://github.com/jscruggs/metric_fu/blob/master/lib/base/md5_tracker.rb
# https://github.com/jscruggs/metric_fu/blob/master/spec/base/md5_tracker_spec.rb
require 'digest/md5'
require 'fileutils'
module MetricFu
class MD5Tracker
@@unchanged_md5s = []
class << self
def md5_dir(path_to_file, base_dir)
File.join(base_dir,
path_to_file.split('/')[0..-2].join('/'))
end
def md5_file(path_to_file, base_dir)
File.join(md5_dir(path_to_file, base_dir),
path_to_file.split('/').last.sub(/\.[a-z]+/, '.md5'))
end
def track(path_to_file, base_dir)
md5 = Digest::MD5.hexdigest(File.read(path_to_file))
FileUtils.mkdir_p(md5_dir(path_to_file, base_dir), :verbose => false)
f = File.new(md5_file(path_to_file, base_dir), "w")
f.puts(md5)
f.close
md5
end
def file_changed?(path_to_file, base_dir)
orig_md5_file = md5_file(path_to_file, base_dir)
return !!track(path_to_file, base_dir) unless File.exist?(orig_md5_file)
current_md5 = ""
file = File.open(orig_md5_file, 'r')
file.each_line { |line| current_md5 << line }
file.close
current_md5.chomp!
new_md5 = Digest::MD5.hexdigest(File.read(path_to_file))
new_md5.chomp!
@@unchanged_md5s << path_to_file if new_md5 == current_md5
return new_md5 != current_md5
end
def file_already_counted?(path_to_file)
return @@unchanged_md5s.include?(path_to_file)
end
end
end
end
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
describe MetricFu::MD5Tracker do
before do
@tmp_dir = File.join(File.dirname(__FILE__), 'tmp')
FileUtils.mkdir_p(@tmp_dir, :verbose => false) unless File.directory?(@tmp_dir)
@file1 = File.new(File.join(@tmp_dir, 'file1.txt'), 'w')
@file2 = File.new(File.join(@tmp_dir, 'file2.txt'), 'w')
end
after do
FileUtils.rm_rf(@tmp_dir, :verbose => false)
end
it "identical files should match" do
@file1.puts("Hello World")
@file1.close
file1_md5 = MD5Tracker.track(@file1.path, @tmp_dir)
@file2.puts("Hello World")
@file2.close
file2_md5 = MD5Tracker.track(@file2.path, @tmp_dir)
file2_md5.should == file1_md5
end
it "different files should not match" do
@file1.puts("Hello World")
@file1.close
file1_md5 = MD5Tracker.track(@file1.path, @tmp_dir)
@file2.puts("Goodbye World")
@file2.close
file2_md5 = MD5Tracker.track(@file2.path, @tmp_dir)
file2_md5.should_not == file1_md5
end
it "file_changed? should detect a change" do
@file2.close
@file1.puts("Hello World")
@file1.close
file1_md5 = MD5Tracker.track(@file1.path, @tmp_dir)
@file1 = File.new(File.join(@tmp_dir, 'file1.txt'), 'w')
@file1.puts("Goodbye World")
@file1.close
MD5Tracker.file_changed?(@file1.path, @tmp_dir).should be_true
end
it "should detect a new file" do
@file2.close
MD5Tracker.file_changed?(@file1.path, @tmp_dir).should be_true
File.exist?(MD5Tracker.md5_file(@file1.path, @tmp_dir)).should be_true
end
end
 I wanted to update y'all on the progress of metric_fu to 5.0 and being pluggable, as well as fish for your thoughts and assistance.

I'm sending this email less complete than I'd ideally like it to be, else I might not send it for even longer :) Please ask questions where unclear. (And I know it is, in parts)

1. Should metric_fu just pass through options to the metric it runs? 
   e.g. https://github.com/metricfu/metric_fu/blob/master/lib/metric_fu/metrics/flog/init.rb#L8
   i.e. align the run_options keys with flog option keys
2. Where should metric config files go? 
   e.g. have a per-metric config  such as config/reek.yml in devtools? how should we work with devtools? https://github.com/rom-rb/devtools 3.  Related to #1 and #2, be able to specify 
     - external config files via the command-line, 
     - the .metrics file location, via the command-line, if necessary (depends on #2) 
     - a global .metrics file in the home directory?  (Windows-friendly)
4. re: metric responsibilities, autoloading, and file/folder structure
  -Once PR 139 is complete, the loading responsibilities of metric_fu will be something like this on require 'metric_fu':
  in the loader.setup load
    1. the logger
    2. the MetricFu::Configuration
    3. the MetricFu::Metric
    4. the generator, perhaps renamed to runner
    5. The Reporter which loads the Formatter
    6. The Result aggregation class (do we even need this?)
    7. The metrics included by metric_fu
    8. Metrics included by external options e.g. see this spike https://gist.github.com/bf4/6592ea3da5ea8e00f396#file-loader_setup-rb-L24
    9. The Rake tasks if Rake is required
5. Responsibilities
  - MetricFu::Metric - superclass of all metrics, including hotspots.  It knows how to enable (run?) the metric, activate its depdendent libraries, if is graphed, loads its grapher
  - MetricFu::Grapher - if metric is graphed, how to graph it
  - MetricFu::Hotspot - how to weight metric
  - template for outputting the metric, html, yaml, json, etc
6. Directory structure and base classes
   lib/metric_fu/metric.rb 
   lib/metric_fu/reporter.rb
   lib/metric_fu/formatter.rb 
   lib/metric_fu/metric_grapher.rb - was known as lib/metric_fu/reporting/graphs/grapher.rb
   lib/metric_fu/graph.rb - (graph generator) was known as lib/metric_fu/metrics/graph.rb
   lib/metric_fu/metric_runner.rb  - now known as lib/metric_fu/metrics/generator.rb
   lib/metric_fu/template.rb  - or perhaps templates/template.rb  - now known as lib/metric_fu/metrics/base_template.rb
   lib/metric_fu/hotspot.rb - not the hotspot metric, but the hotspot analyzer (which each metric also subclasses)
   lib/metric_fu/metrics/example_metric/{config.rb,runner.rb,grapher.rb,hotspot.rb,template.rb(not erb!)}
   misc to be considered
   lib/metric_fu/{logger.rb,io.rb,configuration.rb,environment.rb,formatter.rb,loader.rb,result.rb,utility.rb,gem_run.rb,error.rb}
   remove lib/metric_fu_requires.rb or at least use rubygems see https://gist.github.com/bf4/6731278#file-read_deps_from_gemspec-rb-L3


For more code samples and other notes, please see https://gist.github.com/bf4/6592ea3da5ea8e00f396

# from https://github.com/metricfu/metric_fu/blob/8b534c7/lib/metric_fu.rb#L62
# see https://github.com/metricfu/metric_fu/blob/8b534c7/lib/metric_fu/run.rb
def reset
# TODO Don't like how this method needs to know
# all of these class variables that are defined
# in separate classes.
@configuration = nil
@graph = nil
@result = nil
end
def run(options)
MetricFu::Run.new.run(options)
end
def run_only(metrics_to_run_names, options)
metrics_to_run_names = Array(metrics_to_run_names).map(&:to_s)
MetricFu::Configuration.run do |config|
config.configure_metrics.each do |metric|
metric_name = metric.name.to_s
if metrics_to_run_names.include?(metric_name)
p "Enabling #{metric_name}"
metric.enabled = true
else
p "Disabling #{metric_name}"
metric.enabled = false
end
end
end
run(options)
end
### SPIKE ###
# gem_run.rb
require 'metric_fu_requires'
module MetricFu
class GemRun
attr_reader :output
def initialize(arguments={})
@gem_name = arguments.fetch(:gem_name)
@library_name = arguments.fetch(:metric_name)
@version = arguments.fetch(:version) { MetricFu::MetricVersion.public_send(@library_name.intern) }
@arguments = arguments.fetch(:args)
@output = ''
end
def run
require 'rubygems'
gem @gem_name, @version
handle_argv
@output = capture_stdout do
load Gem.bin_path(@gem_name, @library_name, @version)
end.split(/\r?\n/) #.map(&:strip)
rescue StandardError => e
puts "oops #{e.inspect}"
rescue SystemExit => e
puts "all done #{e.inspect}"
ensure
return self
end
def handle_argv
ARGV.clear
@arguments.split(/\s+/).each do |arg|
ARGV << arg
end
end
# require 'stringio'
# def capture_stdout(&block)
# real_stdout = STDOUT.clone
# $stdout = fake_stdout = StringIO.new
# yield
# ensure
# $stdout = real_stdout
# return fake_stdout.string
# end
# From episode 029 of Ruby Tapas by Avdi Grimm
# https://rubytapas.dpdcart.com/subscriber/post?id=88
def capture_stdout(&block)
old_stdout = STDOUT.clone
pipe_r, pipe_w = IO.pipe
pipe_r.sync = true
output = ""
reader = Thread.new do
begin
loop do
output << pipe_r.readpartial(1024)
end
rescue EOFError
end
end
STDOUT.reopen(pipe_w)
yield
ensure
STDOUT.reopen(old_stdout)
pipe_w.close
reader.join
return output
end
end
end
r = MetricFu::GemRun.new({
gem_name: "cane",
metric_name: "cane",
version: ">= 2.6.1",
args: "--abc-glob '**/*.rb' --abc-max 15 --json",
})
r.run.output.each {|l| p l }
# mf-cane
$LOAD_PATH.unshift(File.expand_path(File.join('..','..','lib'),__FILE__))
require 'metric_fu/gem_run'
MetricFu::GemRun.new(metric_name: 'cane', gem_name: 'cane').run
require 'metric_fu'
def run_options
{:run=>true,
:cane=>false,
:churn=>false,
:flay=>false,
:flog=>false,
:hotspots=>true,
:rails_best_practices=>true,
:rcov=>true,
:reek=>true,
:roodi=>false,
:saikuro=>false,
:stats=>false,
:open=>true}
end
def configure_run
run = MetricFu::Run.new
# configure only
run.send :configure_run, run_options
# full run
# run.run(run_options)
end
def formatters_report
MetricFu.configuration.formatters.clear # Command-line format takes precedence.
Array(MetricFu::Formatter::DEFAULT).each do |format, o|
MetricFu.configuration.configure_formatter(format, o)
end
end
def run_metric(metric)
# result = MetricFu::Result.new
#### MetricFu.result.add(metric)
run_options = MetricFu::Metric.get_metric(metric).run_options
generator_class = MetricFu::Generator.get_generator(metric)
generator = generator_class.new(run_options)
result_hash ||= {}
result_hash.merge!(generator.generate_result)
# emit
# analyze
# to_h
generator.per_file_info(MetricFu.result.per_file_data) if generator.respond_to?(:per_file_info)
# file_data = @matches.first
# => {:file_path=>"lib/metric_fu.rb",
# :code_smells=>
# [{:method=>"MetricFu#current_time",
# :message=>"doesn't depend on instance state",
# :type=>"UtilityFunction"},
# {:method=>"MetricFu#run",
# :message=>"doesn't depend on instance state",
# :type=>"UtilityFunction"},
# {:method=>"MetricFu#run_only",
# :message=>"contains iterators nested 2 deep",
# :type=>"NestedIterators"},
# {:method=>"MetricFu#run_only",
# :message=>"has approx 9 statements",
# :type=>"TooManyStatements"}]}
end
def run_hotspots
analyzer = MetricFu::HotspotAnalyzer.new(MetricFu.result.result_hash)
# @analyzer_tables
# analyzer_columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map(&:columns).flatten
analyzer_columns = analyzer.class::COMMON_COLUMNS + analyzer.class::GRANULARITIES + analyzer.tool_analyzers.map(&:columns).flatten
@analyzer_tables = MetricFu::AnalyzerTables.new(analyzer_columns)
# ["metric",
# "file_path", # GRANULARITIES
# "class_name", # GRANULARITIES
# "method_name", # GRANULARITIES
# "reek__type_name",
# "reek__message",
# "reek__value",
# "reek__value_description",
# "reek__comparable_message"]
# @rankings
require 'metric_fu/metrics/reek/hotspot'
reek_hotspot = MetricFu::ReekHotspot.new # tool_analyzers.first
# 'hotspot_reek_generated_records.yml'
reek_analyzer_tables = reek_hotspot.generate_records(MetricFu.result.result_hash[reek_hotspot.name])
@analyzer_tables.update reek_analyzer_tables
@analyzer_tables.generate_records
# ["MetricFu", "MetricFu#current_time"] => "lib/metric_fu.rb"
# @class_and_method_to_file = { [class_name, method_name] => file_path }
tool_tables = @analyzer_tables.tool_tables
@rankings = MetricFu::HotspotRankings.new(tool_tables)
@rankings.calculate_scores([reek_hotspot], analyzer.class::GRANULARITIES)
analyzed_problems = MetricFu::HotspotAnalyzedProblems.new(@rankings, @analyzer_tables)
analyzer.hotspots # = analyzed_problems.worst_items
end
def yaml_report
MetricFu::Formatter::YAML.new.finish
# write_output(MetricFu.result.as_yaml, @path_or_io)
MetricFu::Formatter::YAML.new(
output: MetricFu.run_path.join("#{MetricFu::Io::FileSystem.directory('data_directory')}/#{MetricFu.report_id}.yml")
).finish
end
def html_report
# MetricFu::Formatter::HTML.new.finish
yaml_report
def output_directory
MetricFu.run_path.join(MetricFu::Io::FileSystem.directory('output_directory'))
end
# save_templatized_result
@template = MetricFu::Formatter::Templates.option('template_class').new
@template.output_directory = self.output_directory
@template.result = MetricFu.result.result_hash
@template.per_file_data = MetricFu.result.per_file_data
@template.formatter = MetricFu::Formatter::HTML.new # self
@template.write
# save_graphs
MetricFu.configuration.graphed_metrics.each {|graphed_metric|
MetricFu.graph.add(graphed_metric, MetricFu.configuration.graph_engine, self.output_directory)
}
MetricFu.graph.generate
end
def smoke_test
configure_run
formatters_report
reporter = MetricFu::Reporter.new(MetricFu.configuration.formatters)
reporter.start # no-op
report_metrics = MetricFu::Metric.enabled_metrics.map(&:name)
metric = report_metrics.first # [:reek, :hotspots]
reporter.start_metric(metric) # no-op
run_metric(metric)
# #<MetricFu::Result:0x00000105309458
# @per_file_data=
# {"lib/metric_fu.rb"=>
# {"67"=>
# [{:type=>:reek,
# :description=>"UtilityFunction - doesn't depend on instance state"}],
# "121"=>
# [{:type=>:reek,
# :description=>"UtilityFunction - doesn't depend on instance state"}],
# "124"=>
# [{:type=>:reek,
# :description=>"NestedIterators - contains iterators nested 2 deep"},
# {:type=>:reek,
# :description=>"TooManyStatements - has approx 9 statements"}]},
# "lib/metric_fu/cli/client.rb"=>
# {""=>
# [{:type=>:reek,
# :description=>"IrresponsibleModule - has no descriptive comment"}]},
# "lib/metric_fu/cli/helper.rb"=>
# {""=>
# [{:type=>:reek,
# :description=>"IrresponsibleModule - has no descriptive comment"},
# {:type=>:reek,
# :description=>"UtilityFunction - doesn't depend on instance state"},
# {:type=>:reek,
# :description=>"NestedIterators - contains iterators nested 2 deep"},
# {:type=>:reek,
# :description=>"TooManyStatements - has approx 8 statements"},
# {:type=>:reek,
# :description=>
# "UncommunicativeVariableName - has the variable name 'p'"}]},
# @result_hash=
# {:reek=>
# {:matches=>
# [{:file_path=>"lib/metric_fu.rb",
# :code_smells=>
# [{:method=>"MetricFu#current_time",
# :message=>"doesn't depend on instance state",
# :type=>"UtilityFunction"},
# {:method=>"MetricFu#run",
# :message=>"doesn't depend on instance state",
# :type=>"UtilityFunction"},
# {:method=>"MetricFu#run_only",
# :message=>"contains iterators nested 2 deep",
# :type=>"NestedIterators"},
# {:method=>"MetricFu#run_only",
# :message=>"has approx 9 statements",
# :type=>"TooManyStatements"}]},
# {:file_path=>"lib/metric_fu/cli/client.rb",
# :code_smells=>
# [{:method=>"MetricFu::Cli::Client",
# :message=>"has no descriptive comment",
# :type=>"IrresponsibleModule"}]},
# reporter.finish_metric(metric)
# MetricFu.result.add(:hotspots)
# MetricFu::Hotspot.new.analyze
# result_hash.yaml
run_hotspots
# reporter.finish
html_report
reporter.display_results
end
smoke_test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment