indirect-metric_fu 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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