indirect-metric_fu 0.8.1 → 0.8.2

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.
@@ -0,0 +1,53 @@
1
+ module MetricFu
2
+
3
+ TEMPLATE_DIR = File.join(File.dirname(__FILE__), '..', 'templates')
4
+ BASE_DIRECTORY = ENV['CC_BUILD_ARTIFACTS'] || 'tmp/metric_fu'
5
+ RAILS = File.exist?("config/environment.rb")
6
+
7
+ if RAILS
8
+ CODE_DIRS = ['app', 'lib']
9
+ else
10
+ CODE_DIRS = ['lib']
11
+ end
12
+
13
+ module Base
14
+ class Generator
15
+
16
+ def initialize(base_dir, options={})
17
+ @base_dir = base_dir
18
+ end
19
+
20
+ def self.generate_report(base_dir, options={})
21
+ self.new(base_dir, options).generate_report
22
+ end
23
+
24
+ def save_html(content, file='index')
25
+ open("#{@base_dir}/#{file}.html", "w") do |f|
26
+ f.puts content
27
+ end
28
+ end
29
+
30
+ def generate_report
31
+ save_html(generate_html)
32
+ end
33
+
34
+ def generate_html
35
+ analyze
36
+ html = ERB.new(File.read(template_file)).result(binding)
37
+ end
38
+
39
+ def template_file
40
+ File.join(MetricFu::TEMPLATE_DIR, "#{template_name}.html.erb")
41
+ end
42
+
43
+ def link_to_filename(name, line = nil)
44
+ filename = File.expand_path(name)
45
+ if PLATFORM['darwin']
46
+ %{<a href="txmt://open/?url=file://#{filename}&line=#{line}">#{name}</a>}
47
+ else
48
+ %{<a href="file://#{filename}">#{name}</a>}
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,127 +1,87 @@
1
1
  module MetricFu
2
- class Churn
3
- class << self
4
- def generate_report(output_dir, options)
5
- date_range, minimum_churn_count, source_control_type = churn_options(options)
6
-
7
- changes = parse_log_for_changes(source_control_type, date_range)
8
- changes.reject! {|file, change_count| change_count < minimum_churn_count}
9
- write_churn_file(changes, output_dir)
2
+ class Churn < Base::Generator
3
+
4
+ def initialize(base_dir, options={})
5
+ @base_dir = base_dir
6
+ if File.exist?(".git")
7
+ @source_control = Git.new(options[:start_date])
8
+ elsif File.exist?(".svn")
9
+ @source_control = Svn.new(options[:start_date])
10
+ else
11
+ raise "Churning requires a subversion or git repo"
12
+ end
13
+
14
+ @minimum_churn_count = options[:minimum_churn_count] || 5
15
+ end
16
+
17
+ def analyze
18
+ @changes = parse_log_for_changes.reject! {|file, change_count| change_count < @minimum_churn_count}
19
+ end
20
+
21
+ # should be dynamically read from the class
22
+ def template_name
23
+ 'churn'
24
+ end
25
+
26
+ private
27
+
28
+ def parse_log_for_changes
29
+ changes = {}
30
+
31
+ logs = @source_control.get_logs
32
+ logs.each do |line|
33
+ changes[line] ? changes[line] += 1 : changes[line] = 1
10
34
  end
11
-
35
+ changes
36
+ end
37
+
38
+
39
+ class SourceControl
40
+ def initialize(start_date=nil)
41
+ @start_date = start_date
42
+ end
43
+
12
44
  private
13
-
14
- def parse_log_for_changes(source_control_type, date_range)
15
- changes = {}
16
-
17
- logs = get_logs(source_control_type, date_range)
18
- logs.each do |line|
19
- changes[line] ? changes[line] += 1 : changes[line] = 1
45
+ def require_rails_env
46
+ # not sure if the following works because active_support might only be in vendor/rails
47
+ # require 'activesupport'
48
+ require RAILS_ROOT + '/config/environment'
49
+ end
50
+ end
51
+
52
+ class Git < SourceControl
53
+ def get_logs
54
+ `git log #{date_range} --name-only --pretty=format:`.split(/\n/).reject{|line| line == ""}
55
+ end
56
+
57
+ private
58
+ def date_range
59
+ if @start_date
60
+ require_rails_env
61
+ "--after=#{@start_date.call.strftime('%Y-%m-%d')}"
20
62
  end
21
-
22
- changes
23
63
  end
24
-
25
- def get_logs(source_control_type, date_range)
26
- if source_control_type == :git
27
- `git log #{date_range} --name-only --pretty=format:`.split(/\n/).reject{|line| line == ""}
28
- else
29
- `svn log #{date_range} --verbose`.split(/\n/).map { |line| clean_up_svn_line(line) }.compact
64
+
65
+ end
66
+
67
+ class Svn < SourceControl
68
+ def get_logs
69
+ `svn log #{date_range} --verbose`.split(/\n/).map { |line| clean_up_svn_line(line) }.compact
70
+ end
71
+
72
+ private
73
+ def date_range
74
+ if @start_date
75
+ require_rails_env
76
+ "--revision {#{@start_date.call.strftime('%Y-%m-%d')}}:{#{Time.now.strftime('%Y-%m-%d')}}"
30
77
  end
31
78
  end
32
-
79
+
33
80
  def clean_up_svn_line(line)
34
81
  m = line.match(/\W*[A,M]\W+(\/.*)\b/)
35
82
  m ? m[1] : nil
36
83
  end
37
-
38
- def churn_options(options)
39
- if File.exist?(".git")
40
- scm = :git
41
- elsif File.exist?(".svn")
42
- scm = :svn
43
- end
44
- raise "Churning requires a subversion or git repo" unless scm
45
-
46
- if options[:start_date]
47
- require 'activesupport'
48
- if scm == :git
49
- date_range = "--after=#{options[:start_date].call.strftime('%Y-%m-%d')}"
50
- else
51
- date_range = "--revision {#{options[:start_date].call.strftime('%Y-%m-%d')}}:{#{Time.now.strftime('%Y-%m-%d')}}"
52
- end
53
- end
54
-
55
- minimum_churn_count = options[:minimum_churn_count] ? options[:minimum_churn_count] : 5
56
- return date_range, minimum_churn_count, scm
57
- end
58
-
59
- def write_churn_file(changes, output_dir)
60
- FileUtils.mkdir_p(output_dir, :verbose => false) unless File.directory?(output_dir)
61
- File.open("#{output_dir}/index.html", "w+") do |file|
62
- file << CHURN_FILE_BEGINING
63
- changes.to_a.sort {|x,y| y[1] <=> x[1]}.each do |change|
64
- file << "<tr><td>#{change[0]}</td><td class='warning'>#{change[1]}</td></tr>\n"
65
- end
66
- file << CHURN_FILE_END
67
- end
68
- end
69
84
  end
70
-
71
- CHURN_FILE_BEGINING = <<-EOS
72
- <html><head><title>Source Control Churn Results</title></head>
73
- <style>
74
- body {
75
- margin: 20px;
76
- padding: 0;
77
- font-size: 12px;
78
- font-family: bitstream vera sans, verdana, arial, sans serif;
79
- background-color: #efefef;
80
- }
81
-
82
- table {
83
- border-collapse: collapse;
84
- /*border-spacing: 0;*/
85
- border: 1px solid #666;
86
- background-color: #fff;
87
- margin-bottom: 20px;
88
- }
89
-
90
- table, th, th+th, td, td+td {
91
- border: 1px solid #ccc;
92
- }
93
-
94
- table th {
95
- font-size: 12px;
96
- color: #fc0;
97
- padding: 4px 0;
98
- background-color: #336;
99
- }
100
-
101
- th, td {
102
- padding: 4px 10px;
103
- }
104
-
105
- td {
106
- font-size: 13px;
107
- }
108
-
109
- .warning {
110
- background-color: yellow;
111
- }
112
- </style>
113
-
114
- <body>
115
- <h1>Source Control Churn Results</h1>
116
- <table width="100%" border="1">
117
- <tr><th>File Path</th><th>Times Changed</th></tr>
118
- EOS
119
-
120
- CHURN_FILE_END = <<-EOS
121
- </table>
122
- </body>
123
- </html>
124
- EOS
125
-
85
+
126
86
  end
127
87
  end
@@ -0,0 +1,17 @@
1
+ require 'erb'
2
+
3
+ module MetricFu
4
+ class FlayReporter < Base::Generator
5
+
6
+ def analyze
7
+ files_to_flay = MetricFu::CODE_DIRS.map{|dir| Dir[File.join(dir, "**/*.rb")] }
8
+ output = `flay #{files_to_flay.join(" ")}`
9
+ @matches = output.chomp.split("\n\n").map{|m| m.split("\n ") }
10
+ end
11
+
12
+ def template_name
13
+ 'flay'
14
+ end
15
+
16
+ end
17
+ end
@@ -1,10 +1,10 @@
1
1
  module MetricFu::FlogReporter
2
-
2
+
3
3
  SCORE_FORMAT = "%0.2f"
4
-
4
+
5
5
  class InvalidFlog < RuntimeError
6
6
  end
7
-
7
+
8
8
  class Base
9
9
  MODULE_NAME = "([A-Za-z]+)+"
10
10
  METHOD_NAME = "#([a-z0-9]+_?)+\\??\\!?"
@@ -12,7 +12,7 @@ module MetricFu::FlogReporter
12
12
 
13
13
  METHOD_NAME_RE = Regexp.new("#{MODULE_NAME}#{METHOD_NAME}")
14
14
  SCORE_RE = Regexp.new(SCORE)
15
-
15
+
16
16
  METHOD_LINE_RE = Regexp.new("#{MODULE_NAME}#{METHOD_NAME}:\\s\\(#{SCORE}\\)")
17
17
  OPERATOR_LINE_RE = Regexp.new("\\s+(#{SCORE}):\\s(.*)$")
18
18
 
@@ -22,15 +22,6 @@ module MetricFu::FlogReporter
22
22
  return second_value
23
23
  end
24
24
 
25
- def load_css(css_file = nil)
26
- filepath = css_file || File.join(File.dirname(__FILE__), 'flog_reporter.css')
27
- css = ""
28
- file = File.open(filepath, "r")
29
- file.each_line { |line| css << line }
30
- file.close
31
- css
32
- end
33
-
34
25
  def parse(text)
35
26
  score = text[/\w+ = (\d+\.\d+)/, 1]
36
27
  return nil unless score
@@ -50,7 +41,7 @@ module MetricFu::FlogReporter
50
41
  page.scanned_methods.last.operators << Operator.new(score, operator)
51
42
  end
52
43
  end
53
-
44
+
54
45
  page
55
46
  end
56
47
  end
@@ -1,71 +1,39 @@
1
1
  module MetricFu::FlogReporter
2
- class Generator
3
- class << self
4
- def generate_report(base_dir)
5
- flog_hashes = []
6
- Dir.glob("#{base_dir}/**/*.txt").each do |filename|
7
- content = ""
8
- File.open(filename, "r").each_line do |file|
9
- content << file
10
- end
11
-
12
- begin
13
- page = Base.parse(content)
14
- rescue InvalidFlog
15
- puts "Invalid flog for #{filename}"
16
- next
17
- end
2
+ class Generator < MetricFu::Base::Generator
3
+ def generate_report
4
+ flog_hashes = []
5
+ Dir.glob("#{@base_dir}/**/*.txt").each do |filename|
6
+ begin
7
+ page = Base.parse(open(filename, "r") { |f| f.read })
8
+ rescue InvalidFlog
9
+ puts "Invalid flog for #{filename}"
10
+ next
11
+ end
18
12
 
19
- next unless page
13
+ next unless page
20
14
 
21
- if MetricFu::MD5Tracker.file_already_counted?(filename)
22
- flog_hashes << {
23
- :page => page,
24
- :path => filename.sub('.txt', '.html').sub("#{base_dir}/", "")
25
- }
26
- else
27
- flog_hashes << generate_page(filename, page, base_dir)
28
- end
15
+ unless MetricFu::MD5Tracker.file_already_counted?(filename)
16
+ generate_page(filename, page)
29
17
  end
30
-
31
- generate_index(flog_hashes, base_dir)
32
- end
33
-
34
- def generate_page(filename, page, base_dir)
35
- html_file = File.new(filename.gsub(/\.txt/, '.html'), "w")
36
- html_file.puts page.to_html
37
- html_file.close
38
- return { :path => html_file.path.sub("#{base_dir}/", ''),
18
+ flog_hashes << { :path => File.basename(filename, ".txt") + '.html',
39
19
  :page => page }
40
20
  end
41
21
 
42
- def generate_index(flog_hashes, base_dir)
43
- html = "<html><head><title>Flog Reporter</title><style>"
44
- html << Base.load_css
45
- html << "</style></head><body>"
46
- html << "<p><strong>Flogged files</strong></p>\n"
47
- html << "<p>Generated on #{Time.now.localtime} with <a href='http://ruby.sadi.st/Flog.html'>flog</a></p>\n"
48
- html << "<table class='report'>\n"
49
- html << "<tr><th>File</th><th>Total score</th><th>Methods</th><th>Average score</th><th>Highest score</th></tr>"
50
- count = 0
51
- flog_hashes.sort {|x,y| y[:page].highest_score <=> x[:page].highest_score }.each do |flog_hash|
52
- html << <<-EOF
53
- <tr class='#{Base.cycle("light", "dark", count)}'>
54
- <td><a href='#{flog_hash[:path]}'>#{flog_hash[:path].sub('.html', '.rb')}</a></td>
55
- <td class='score'>#{sprintf(SCORE_FORMAT, flog_hash[:page].score)}</td>
56
- <td class='score'>#{flog_hash[:page].scanned_methods.length}</td>
57
- <td class='score'>#{sprintf(SCORE_FORMAT, flog_hash[:page].average_score)}</td>
58
- <td class='score'>#{sprintf(SCORE_FORMAT, flog_hash[:page].highest_score)}</td>
59
- </tr>
60
- EOF
61
- count += 1
62
- end
63
- html << "</table>\n"
64
- html << "</body></html>\n"
65
- index = File.new("#{base_dir}/index.html", "w")
66
- index.puts html
67
- index.close
68
- end
22
+ generate_index(flog_hashes)
23
+ end
24
+
25
+ def generate_page(filename, page)
26
+ save_html(page.to_html, File.basename(filename, ".txt"))
27
+ end
28
+
29
+ # should be dynamically read from the class
30
+ def template_name
31
+ 'flog'
32
+ end
33
+
34
+ def generate_index(flog_hashes)
35
+ html = ERB.new(File.read(template_file)).result(binding)
36
+ save_html(html)
69
37
  end
70
38
  end
71
39
  end
@@ -1,7 +1,7 @@
1
1
  module MetricFu::FlogReporter
2
2
  class Operator
3
3
  attr_accessor :score, :operator
4
-
4
+
5
5
  def initialize(score, operator)
6
6
  @score = score.to_f
7
7
  @operator = operator
@@ -1,24 +1,16 @@
1
1
  module MetricFu::FlogReporter
2
- class Page
2
+ class Page < MetricFu::Base::Generator
3
3
  attr_accessor :score, :scanned_methods
4
-
4
+
5
5
  def initialize(score, scanned_methods = [])
6
6
  @score = score.to_f
7
7
  @scanned_methods = scanned_methods
8
8
  end
9
-
9
+
10
10
  def to_html
11
- output = "<html><head><style>"
12
- output << Base.load_css
13
- output << "</style></head><body>"
14
- output << "Score: #{score}\n"
15
- scanned_methods.each do |sm|
16
- output << sm.to_html
17
- end
18
- output << "</body></html>"
19
- output
11
+ ERB.new(File.read(template_file)).result(binding)
20
12
  end
21
-
13
+
22
14
  def average_score
23
15
  sum = 0
24
16
  scanned_methods.each do |m|
@@ -26,11 +18,16 @@ module MetricFu::FlogReporter
26
18
  end
27
19
  sum / scanned_methods.length
28
20
  end
29
-
21
+
30
22
  def highest_score
31
23
  scanned_methods.inject(0) do |highest, m|
32
24
  m.score > highest ? m.score : highest
33
25
  end
34
26
  end
27
+
28
+ # should be dynamically read from the class
29
+ def template_name
30
+ 'flog_page'
31
+ end
35
32
  end
36
33
  end
@@ -1,13 +1,13 @@
1
1
  module MetricFu::FlogReporter
2
2
  class ScannedMethod
3
3
  attr_accessor :name, :score, :operators
4
-
4
+
5
5
  def initialize(name, score, operators = [])
6
6
  @name = name
7
7
  @score = score.to_f
8
8
  @operators = operators
9
9
  end
10
-
10
+
11
11
  def to_html
12
12
  output = "<p><strong>#{name} (#{score})</strong></p>\n"
13
13
  output << "<table>\n"
@@ -9,11 +9,11 @@ module MetricFu
9
9
  class << self
10
10
  def md5_dir(path_to_file, base_dir)
11
11
  File.join(base_dir,
12
- path_to_file.split('/')[0..-2].join('/'))
12
+ path_to_file.split('/')[0..-2].join('/'))
13
13
  end
14
14
 
15
15
  def md5_file(path_to_file, base_dir)
16
- File.join(md5_dir(path_to_file, base_dir),
16
+ File.join(md5_dir(path_to_file, base_dir),
17
17
  path_to_file.split('/').last.sub(/\.[a-z]+/, '.md5'))
18
18
  end
19
19
 
@@ -25,7 +25,7 @@ module MetricFu
25
25
  f.close
26
26
  md5
27
27
  end
28
-
28
+
29
29
  def file_changed?(path_to_file, base_dir)
30
30
  orig_md5_file = md5_file(path_to_file, base_dir)
31
31
  return !!track(path_to_file, base_dir) unless File.exist?(orig_md5_file)
@@ -35,7 +35,7 @@ module MetricFu
35
35
  file.each_line { |line| current_md5 << line }
36
36
  file.close
37
37
  current_md5.chomp!
38
-
38
+
39
39
  new_md5 = Digest::MD5.hexdigest(File.read(path_to_file))
40
40
  new_md5.chomp!
41
41
 
@@ -43,7 +43,7 @@ module MetricFu
43
43
 
44
44
  return new_md5 != current_md5
45
45
  end
46
-
46
+
47
47
  def file_already_counted?(path_to_file)
48
48
  return @@unchanged_md5s.include?(path_to_file)
49
49
  end
data/lib/metric_fu.rb CHANGED
@@ -1,14 +1,3 @@
1
- module MetricFu
2
- TEMPLATE_DIR = File.join(File.dirname(__FILE__), "templates")
3
- BASE_DIRECTORY = ENV['CC_BUILD_ARTIFACTS'] || 'tmp/metric_fu'
4
- RAILS = File.exist?("config/environment.rb")
5
-
6
- if RAILS
7
- CODE_DIRS = ['app', 'lib']
8
- else
9
- CODE_DIRS = ['lib']
10
- end
11
- end
12
-
1
+ require File.join(File.dirname(__FILE__), 'metric_fu', 'base') #require first because of dependecies
13
2
  require File.join(File.dirname(__FILE__), 'tasks', 'metric_fu')
14
3
  Dir[File.join(File.dirname(__FILE__), 'metric_fu/*.rb')].each{|l| require l }
data/lib/tasks/churn.rake CHANGED
@@ -1,7 +1,7 @@
1
1
  require File.join(File.dirname(__FILE__), '../metric_fu/churn')
2
2
 
3
3
  namespace :metrics do
4
-
4
+
5
5
  desc "Which files change the most"
6
6
  task :churn do
7
7
  churn_dir = File.join(MetricFu::BASE_DIRECTORY, 'churn')
@@ -13,10 +13,11 @@ begin
13
13
 
14
14
  namespace :coverage do
15
15
  rcov_output = COVERAGE_DIR
16
-
16
+
17
17
  desc "Delete aggregate coverage data."
18
18
  task(:clean) { rm_f("rcov_tmp", :verbose => false) }
19
-
19
+
20
+ desc "RCov task to generate report"
20
21
  Spec::Rake::SpecTask.new(:do => :clean) do |t|
21
22
  FileUtils.mkdir_p(MetricFu::BASE_DIRECTORY) unless File.directory?(MetricFu::BASE_DIRECTORY)
22
23
  t.ruby_opts = ['-rtest/unit']
@@ -27,10 +28,10 @@ begin
27
28
  t.rcov_dir = COVERAGE_DIR
28
29
  end
29
30
  end
30
-
31
- desc "Generate RCov report"
31
+
32
+ desc "Generate and open coverage report"
32
33
  task :coverage => ['coverage:do'] do
33
- system("open #{SPEC_HTML_FILE}") if PLATFORM['darwin']
34
+ system("open #{COVERAGE_DIR}/index.html") if PLATFORM['darwin']
34
35
  end
35
36
  end
36
37
  rescue LoadError
@@ -38,5 +39,6 @@ rescue LoadError
38
39
  puts 'running in jruby - rcov tasks not available'
39
40
  else
40
41
  puts 'sudo gem install rcov # if you want the rcov tasks'
42
+
41
43
  end
42
44
  end
@@ -0,0 +1,9 @@
1
+ FLAY_DIR = File.join(MetricFu::BASE_DIRECTORY, 'flay')
2
+
3
+ namespace :metrics do
4
+ desc "Generate code duplication report with flay"
5
+ task :flay do
6
+ MetricFu::FlayReporter.generate_report(FLAY_DIR)
7
+ system("open #{FLAY_DIR}/index.html") if PLATFORM['darwin']
8
+ end
9
+ end
data/lib/tasks/flog.rake CHANGED
@@ -10,40 +10,40 @@ begin
10
10
  end
11
11
 
12
12
  namespace :metrics do
13
-
13
+
14
14
  task :flog => ['flog:all'] do
15
15
  end
16
-
16
+
17
17
  namespace :flog do
18
18
  desc "Delete aggregate flog data."
19
19
  task(:clean) { rm_rf(FLOG_DIR, :verbose => false) }
20
-
20
+
21
21
  desc "Flog code in app/models"
22
22
  task :models do
23
23
  flog "models", "app/models"
24
24
  end
25
25
 
26
- desc "Flog code in app/controllers"
26
+ desc "Flog code in app/controllers"
27
27
  task :controllers do
28
28
  flog "controllers", "app/controllers"
29
29
  end
30
30
 
31
- desc "Flog code in app/helpers"
31
+ desc "Flog code in app/helpers"
32
32
  task :helpers do
33
33
  flog "helpers", "app/helpers"
34
34
  end
35
35
 
36
- desc "Flog code in lib"
36
+ desc "Flog code in lib"
37
37
  task :lib do
38
38
  flog "lib", "lib"
39
- end
39
+ end
40
40
 
41
41
  desc "Generate a flog report from specified directories"
42
42
  task :custom do
43
43
  MetricFu::CODE_DIRS.each { |directory| flog(directory, directory) }
44
44
  MetricFu::FlogReporter::Generator.generate_report(FLOG_DIR)
45
- end
46
-
45
+ end
46
+
47
47
  desc "Generate and open flog report"
48
48
  if MetricFu::RAILS
49
49
  task :all => [:models, :controllers, :helpers, :lib] do
@@ -56,7 +56,7 @@ begin
56
56
  system("open #{FLOG_DIR}/index.html") if PLATFORM['darwin']
57
57
  end
58
58
  end
59
-
59
+
60
60
  end
61
61
 
62
62
  end