turbulence 1.2.3 → 1.3.0

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.
@@ -1,39 +1,31 @@
1
1
  require 'stringio'
2
2
  require 'flog'
3
3
 
4
- class Ruby19Parser < RubyParser
5
- def process(ruby, file)
6
- ruby.gsub!(/(\w+):\s+/, '"\1" =>')
7
- super(ruby, file)
8
- end
9
- end unless defined?(:Ruby19Parser)
10
-
11
- class Flog19 < Flog
12
- def initialize option = {}
13
- super(option)
14
- @parser = Ruby19Parser.new
15
- end
16
- end
17
-
18
4
  class Turbulence
19
5
  module Calculators
20
6
  class Complexity
21
- class << self
22
- def flogger
23
- @flogger ||= Flog19.new(:continue => true)
24
- end
25
- def for_these_files(files)
26
- files.each do |filename|
27
- yield filename, score_for_file(filename)
28
- end
29
- end
7
+ attr_reader :config, :type
30
8
 
31
- def score_for_file(filename)
32
- flogger.reset
33
- flogger.flog filename
34
- flogger.total_score
9
+ def initialize(config = nil)
10
+ @config = config || Turbulence.config
11
+ @type = :complexity
12
+ end
13
+
14
+ def flogger
15
+ @flogger ||= Flog.new(continue: true)
16
+ end
17
+
18
+ def for_these_files(files)
19
+ files.each do |filename|
20
+ yield filename, score_for_file(filename)
35
21
  end
36
22
  end
23
+
24
+ def score_for_file(filename)
25
+ flogger.reset
26
+ flogger.flog filename
27
+ flogger.total_score
28
+ end
37
29
  end
38
30
  end
39
31
  end
@@ -2,7 +2,8 @@ class Turbulence
2
2
  class ChecksEnvironment
3
3
  class << self
4
4
  def scm_repo?(directory)
5
- Turbulence::Calculators::Churn.scm.is_repo?(directory)
5
+ churn_calculator = Turbulence::Calculators::Churn.new
6
+ churn_calculator.scm.is_repo?(directory)
6
7
  end
7
8
  end
8
9
  end
@@ -0,0 +1,53 @@
1
+ class Turbulence
2
+ class CommandLineInterface
3
+ # I Update a Turbulence::Configuration instance to match the user's
4
+ # expectations (as expressed in ARGV)
5
+ module ConfigParser
6
+ def self.parse_argv_into_config(argv, config)
7
+ option_parser = OptionParser.new do |opts|
8
+ opts.banner = "Usage: bule [options] [dir]"
9
+
10
+ opts.on('--scm p4|git', String, 'scm to use (default: git)') do |s|
11
+ case s
12
+ when "git", "", nil
13
+ when "p4"
14
+ config.scm_name = 'Perforce'
15
+ end
16
+ end
17
+
18
+ opts.on('--churn-range since..until', String, 'commit range to compute file churn') do |s|
19
+ config.commit_range = s
20
+ end
21
+
22
+ opts.on('--churn-mean', 'calculate mean churn instead of cumulative') do
23
+ config.compute_mean = true
24
+ end
25
+
26
+ opts.on('--exclude pattern', String, 'exclude files matching pattern') do |pattern|
27
+ config.exclusion_pattern = pattern
28
+ end
29
+
30
+ opts.on('--treemap', String, 'output treemap graph instead of scatterplot') do |s|
31
+ config.graph_type = "treemap"
32
+ end
33
+
34
+ opts.on('--no-open', 'skip opening the report in a browser') do
35
+ config.no_open = true
36
+ end
37
+
38
+ opts.on('--output DIR', String, 'output directory for reports (default: ./turbulence)') do |dir|
39
+ config.output_dir = dir
40
+ end
41
+
42
+ opts.on_tail("-h", "--help", "Show this message") do
43
+ puts opts
44
+ exit
45
+ end
46
+ end
47
+ option_parser.parse!(argv)
48
+
49
+ config.directory = argv.first unless argv.empty?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,60 +1,42 @@
1
1
  require 'fileutils'
2
2
  require 'launchy'
3
3
  require 'optparse'
4
+ require 'forwardable'
5
+
6
+ require 'turbulence/configuration'
7
+ require 'turbulence/cli_parser'
4
8
  require 'turbulence/scm/git'
5
9
  require 'turbulence/scm/perforce'
6
10
 
7
11
  class Turbulence
8
12
  class CommandLineInterface
9
13
  TURBULENCE_TEMPLATE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "template")
10
- TEMPLATE_FILES = ['turbulence.html',
11
- 'highcharts.js',
12
- 'jquery.min.js',
13
- 'treemap.html'].map do |filename|
14
+ TEMPLATE_FILES = [
15
+ 'turbulence.html',
16
+ 'highcharts.js',
17
+ 'jquery.min.js',
18
+ 'treemap.html',
19
+ ].map do |filename|
14
20
  File.join(TURBULENCE_TEMPLATE_PATH, filename)
15
21
  end
16
22
 
17
- attr_reader :exclusion_pattern
18
- attr_reader :directory
19
23
  def initialize(argv, additional_options = {})
20
- Turbulence::Calculators::Churn.scm = Scm::Git
21
- @graph_type = "turbulence"
22
- OptionParser.new do |opts|
23
- opts.banner = "Usage: bule [options] [dir]"
24
-
25
- opts.on('--scm p4|git', String, 'scm to use (default: git)') do |s|
26
- case s
27
- when "git", "", nil
28
- when "p4"
29
- Turbulence::Calculators::Churn.scm = Scm::Perforce
30
- end
31
- end
32
-
33
- opts.on('--churn-range since..until', String, 'commit range to compute file churn') do |s|
34
- Turbulence::Calculators::Churn.commit_range = s
35
- end
36
-
37
- opts.on('--churn-mean', 'calculate mean churn instead of cummulative') do
38
- Turbulence::Calculators::Churn.compute_mean = true
39
- end
40
-
41
- opts.on('--exclude pattern', String, 'exclude files matching pattern') do |pattern|
42
- @exclusion_pattern = pattern
43
- end
44
-
45
- opts.on('--treemap', String, 'output treemap graph instead of scatterplot') do |s|
46
- @graph_type = "treemap"
47
- end
48
-
49
-
50
- opts.on_tail("-h", "--help", "Show this message") do
51
- puts opts
52
- exit
53
- end
54
- end.parse!(argv)
24
+ ConfigParser.parse_argv_into_config argv, config
25
+ config.output = additional_options.fetch(:output, STDOUT)
26
+ end
55
27
 
56
- @directory = argv.first || Dir.pwd
57
- @output = additional_options.fetch(:output, STDOUT)
28
+ extend Forwardable
29
+ def_delegators :Turbulence, :config
30
+ def_delegators :config, *[
31
+ :directory,
32
+ :graph_type,
33
+ :exclusion_pattern,
34
+ :no_open,
35
+ :output_dir,
36
+ ]
37
+
38
+ def output_path
39
+ output_dir || File.join(Dir.pwd, "turbulence")
58
40
  end
59
41
 
60
42
  def copy_templates_into(directory)
@@ -62,12 +44,12 @@ class Turbulence
62
44
  end
63
45
 
64
46
  def generate_bundle
65
- FileUtils.mkdir_p("turbulence")
47
+ FileUtils.mkdir_p(output_path)
66
48
 
67
- Dir.chdir("turbulence") do
68
- turb = Turbulence.new(directory, @output, @exclusion_pattern)
49
+ Dir.chdir(output_path) do
50
+ turb = Turbulence.new(config)
69
51
 
70
- generator = case @graph_type
52
+ generator = case graph_type
71
53
  when "treemap"
72
54
  Turbulence::Generators::TreeMap.new({})
73
55
  else
@@ -79,7 +61,7 @@ class Turbulence
79
61
  end
80
62
 
81
63
  def open_bundle
82
- Launchy.open("file:///#{directory}/turbulence/#{@graph_type}.html")
64
+ Launchy.open("file:///#{output_path}/#{graph_type}.html")
83
65
  end
84
66
  end
85
67
  end
@@ -0,0 +1,30 @@
1
+ class Turbulence
2
+ class Configuration
3
+ attr_accessor *[
4
+ :directory,
5
+ :scm,
6
+ :scm_name,
7
+ :commit_range,
8
+ :compute_mean,
9
+ :exclusion_pattern,
10
+ :graph_type,
11
+ :output,
12
+ :output_dir,
13
+ :no_open,
14
+ ]
15
+
16
+ def initialize
17
+ @directory = Dir.pwd
18
+ @graph_type = 'turbulence'
19
+ @scm_name = 'Git'
20
+ @output = STDOUT
21
+ @output_dir = nil
22
+ @no_open = false
23
+ end
24
+
25
+ # TODO: drop attr accessor and ivar once it stops getting set via Churn
26
+ def scm
27
+ @scm || Turbulence::Scm.const_get(scm_name)
28
+ end
29
+ end
30
+ end
@@ -4,8 +4,8 @@ class Turbulence
4
4
  attr_reader :metrics_hash, :x_metric, :y_metric
5
5
 
6
6
  def initialize(metrics_hash,
7
- x_metric = Turbulence::Calculators::Churn,
8
- y_metric = Turbulence::Calculators::Complexity)
7
+ x_metric = :churn,
8
+ y_metric = :complexity)
9
9
  @x_metric = x_metric
10
10
  @y_metric = y_metric
11
11
  @metrics_hash = metrics_hash
@@ -4,8 +4,8 @@ class Turbulence
4
4
  attr_reader :metrics_hash, :x_metric, :y_metric
5
5
 
6
6
  def initialize(metrics_hash,
7
- x_metric = Turbulence::Calculators::Churn,
8
- y_metric = Turbulence::Calculators::Complexity)
7
+ x_metric = :churn,
8
+ y_metric = :complexity)
9
9
  @x_metric = x_metric
10
10
  @y_metric = y_metric
11
11
  @metrics_hash = metrics_hash
@@ -8,7 +8,7 @@ class Turbulence
8
8
 
9
9
  def is_repo?(directory)
10
10
  FileUtils.cd(directory) {
11
- return !(`git status 2>&1` =~ /Not a git repository/)
11
+ return !(`git status 2>&1` =~ /not a git repository/i)
12
12
  }
13
13
  end
14
14
  end
@@ -1,3 +1,3 @@
1
1
  class Turbulence
2
- VERSION = "1.2.3"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/turbulence.rb CHANGED
@@ -1,32 +1,47 @@
1
+ require 'turbulence/configuration'
1
2
  require 'turbulence/file_name_mangler'
2
3
  require 'turbulence/command_line_interface'
3
- require 'turbulence/checks_environment'
4
4
  require 'turbulence/calculators/churn'
5
5
  require 'turbulence/calculators/complexity'
6
6
  require 'turbulence/generators/treemap'
7
7
  require 'turbulence/generators/scatterplot'
8
8
 
9
9
  class Turbulence
10
- CODE_DIRECTORIES = ["app/models",
11
- "app/controllers",
12
- "app/helpers",
13
- "app/jobs",
14
- "app/mailers",
15
- "app/validators",
16
- "lib"]
17
- CALCULATORS = [Turbulence::Calculators::Complexity,
18
- Turbulence::Calculators::Churn]
10
+ CODE_DIRECTORIES = [
11
+ "app/models",
12
+ "app/controllers",
13
+ "app/helpers",
14
+ "app/jobs",
15
+ "app/mailers",
16
+ "app/validators",
17
+ "lib",
18
+ ]
19
+ CALCULATORS = [
20
+ Turbulence::Calculators::Complexity,
21
+ Turbulence::Calculators::Churn,
22
+ ]
19
23
 
20
- attr_reader :exclusion_pattern
24
+ # Make a config instance available to anyone who wants one
25
+ def self.config
26
+ @config ||= Configuration.new
27
+ end
28
+
29
+ attr_reader :config
21
30
  attr_reader :metrics
22
31
 
23
- def initialize(directory, output = nil, exclusion_pattern = nil)
24
- @output = output
25
- @metrics = {}
26
- @exclusion_pattern = exclusion_pattern
32
+ extend Forwardable
33
+ def_delegators :config, *[
34
+ :directory,
35
+ :exclusion_pattern,
36
+ :output,
37
+ ]
38
+
39
+ def initialize(config = nil)
40
+ @config = config || Turbulence.config
41
+ @metrics = {}
27
42
 
28
43
  Dir.chdir(directory) do
29
- CALCULATORS.each(&method(:calculate_metrics_with))
44
+ calculators.each(&method(:calculate_metrics_with))
30
45
  end
31
46
  end
32
47
 
@@ -40,14 +55,14 @@ class Turbulence
40
55
 
41
56
  calculator.for_these_files(files_of_interest) do |filename, score|
42
57
  report "."
43
- set_file_metric(filename, calculator, score)
58
+ set_file_metric(filename, calculator.type, score)
44
59
  end
45
60
 
46
61
  report "\n"
47
62
  end
48
63
 
49
64
  def report(this)
50
- @output.print this unless @output.nil?
65
+ output.print this unless output.nil?
51
66
  end
52
67
 
53
68
  def set_file_metric(filename, metric, value)
@@ -60,9 +75,13 @@ class Turbulence
60
75
 
61
76
  private
62
77
  def exclude_files(files)
63
- if not @exclusion_pattern.nil?
64
- files = files.reject { |f| f =~ Regexp.new(@exclusion_pattern) }
78
+ if not exclusion_pattern.nil?
79
+ files = files.reject { |f| f =~ Regexp.new(exclusion_pattern) }
65
80
  end
66
81
  files
67
82
  end
83
+
84
+ def calculators
85
+ CALCULATORS.map { |calc_class| calc_class.new(config) }
86
+ end
68
87
  end
@@ -1,152 +1,142 @@
1
1
  require 'turbulence/calculators/churn'
2
2
 
3
3
  describe Turbulence::Calculators::Churn do
4
- let(:calculator) { Turbulence::Calculators::Churn }
4
+ let(:calculator) { Turbulence::Calculators::Churn.new(config) }
5
+ let(:config) { Turbulence::Configuration.new }
6
+
5
7
  before do
6
- calculator.stub(:scm_log_command) { "" }
8
+ allow(calculator).to receive(:scm_log_command).and_return("")
7
9
  end
8
10
 
9
11
  describe "::for_these_files" do
10
12
  it "yields up the filename and score for each file" do
11
13
  files = ["lib/corey.rb", "lib/chad.rb"]
12
- calculator.stub(:changes_by_ruby_file) {
13
- [
14
- ["lib/corey.rb", 5],
15
- ["lib/chad.rb", 10]
16
- ]
17
- }
14
+ allow(calculator).to receive(:changes_by_ruby_file).and_return([
15
+ ["lib/corey.rb", 5],
16
+ ["lib/chad.rb", 10]
17
+ ])
18
18
  yielded_files = []
19
19
  calculator.for_these_files(files) do |filename, score|
20
20
  yielded_files << [filename, score]
21
21
  end
22
- yielded_files.should =~ [["lib/corey.rb", 5],
23
- ["lib/chad.rb",10]]
22
+ expect(yielded_files).to match_array([["lib/corey.rb", 5], ["lib/chad.rb", 10]])
24
23
  end
25
24
 
26
25
  it "filters the results by the passed-in files" do
27
26
  files = ["lib/corey.rb"]
28
- calculator.stub(:changes_by_ruby_file) {
29
- [
30
- ["lib/corey.rb", 5],
31
- ["lib/chad.rb", 10]
32
- ]
33
- }
27
+ allow(calculator).to receive(:changes_by_ruby_file).and_return([
28
+ ["lib/corey.rb", 5],
29
+ ["lib/chad.rb", 10]
30
+ ])
34
31
  yielded_files = []
35
32
  calculator.for_these_files(files) do |filename, score|
36
33
  yielded_files << [filename, score]
37
34
  end
38
- yielded_files.should =~ [["lib/corey.rb", 5]]
35
+ expect(yielded_files).to match_array([["lib/corey.rb", 5]])
39
36
  end
40
37
  end
41
38
 
42
39
  describe "::scm_log_file_lines" do
43
40
  it "returns just the file lines" do
44
- calculator.stub(:scm_log_command) do
45
- "\n\n\n\n10\t6\tlib/turbulence.rb\n\n\n\n17\t2\tlib/eddies.rb\n"
46
- end
41
+ allow(calculator).to receive(:scm_log_command).and_return(
42
+ "\n\n\n\n10\t6\tlib/turbulence.rb\n\n\n\n17\t2\tlib/eddies.rb\n"
43
+ )
47
44
 
48
- calculator.scm_log_file_lines.should =~ [
45
+ expect(calculator.scm_log_file_lines).to match_array([
49
46
  "10\t6\tlib/turbulence.rb",
50
47
  "17\t2\tlib/eddies.rb"
51
- ]
48
+ ])
52
49
  end
53
50
  end
54
51
 
55
52
  describe "::counted_line_changes_by_file_by_commit" do
56
53
  before do
57
- calculator.stub(:scm_log_file_lines) {
58
- [
59
- "10\t6\tlib/turbulence.rb",
60
- "17\t2\tlib/eddies.rb"
61
- ]
62
- }
54
+ allow(calculator).to receive(:scm_log_file_lines).and_return([
55
+ "10\t6\tlib/turbulence.rb",
56
+ "17\t2\tlib/eddies.rb"
57
+ ])
63
58
  end
64
59
 
65
60
  it "sums up the line changes" do
66
- calculator.counted_line_changes_by_file_by_commit.should =~ [["lib/turbulence.rb", 16], ["lib/eddies.rb", 19]]
61
+ expect(calculator.counted_line_changes_by_file_by_commit).to match_array([["lib/turbulence.rb", 16], ["lib/eddies.rb", 19]])
67
62
  end
68
63
  end
69
-
64
+
70
65
  describe "::changes_by_ruby_file" do
71
66
  before do
72
- calculator.stub(:ruby_files_changed_in_scm) {
73
- [
74
- ['lib/eddies.rb', 4],
75
- ['lib/turbulence.rb', 5],
76
- ['lib/turbulence.rb', 16],
77
- ['lib/eddies.rb', 2],
78
- ['lib/turbulence.rb', 7],
79
- ['lib/eddies.rb', 19],
80
- ['lib/eddies.rb', 28]
81
- ]
82
- }
67
+ allow(calculator).to receive(:ruby_files_changed_in_scm).and_return([
68
+ ['lib/eddies.rb', 4],
69
+ ['lib/turbulence.rb', 5],
70
+ ['lib/turbulence.rb', 16],
71
+ ['lib/eddies.rb', 2],
72
+ ['lib/turbulence.rb', 7],
73
+ ['lib/eddies.rb', 19],
74
+ ['lib/eddies.rb', 28]
75
+ ])
83
76
  end
84
-
77
+
85
78
  it "groups and sums churns, excluding the last" do
86
79
  calculator.compute_mean = false
87
- calculator.changes_by_ruby_file.should =~ [ ['lib/eddies.rb', 25], ['lib/turbulence.rb', 21]]
80
+ expect(calculator.changes_by_ruby_file).to match_array([['lib/eddies.rb', 25], ['lib/turbulence.rb', 21]])
88
81
  end
89
82
 
90
83
  it "interprets a single entry as zero churn" do
91
- calculator.stub(:ruby_files_changed_in_scm) {
92
- [
93
- ['lib/eddies.rb', 4],
94
- ]
95
- }
84
+ allow(calculator).to receive(:ruby_files_changed_in_scm).and_return([
85
+ ['lib/eddies.rb', 4],
86
+ ])
96
87
  calculator.compute_mean = false
97
- calculator.changes_by_ruby_file.should =~ [ ['lib/eddies.rb', 0] ]
88
+ expect(calculator.changes_by_ruby_file).to match_array([['lib/eddies.rb', 0]])
98
89
  end
99
-
90
+
100
91
  it "groups and takes the mean of churns, excluding the last" do
101
92
  calculator.compute_mean = true
102
- calculator.changes_by_ruby_file.should =~ [ ['lib/eddies.rb', 8], ['lib/turbulence.rb', 10]]
93
+ expect(calculator.changes_by_ruby_file).to match_array([['lib/eddies.rb', 8], ['lib/turbulence.rb', 10]])
103
94
  calculator.compute_mean = false
104
95
  end
105
96
  end
106
97
 
107
98
  describe "::calculate_mean_of_churn" do
108
99
  it "handles zero sample size" do
109
- calculator.calculate_mean_of_churn(8,0).should == 8
100
+ expect(calculator.calculate_mean_of_churn(8, 0)).to eq 8
110
101
  end
111
102
 
112
- it "returns original churn for sample size = 1" do
113
- calculator.calculate_mean_of_churn(8,1).should == 8
103
+ it "returns original churn for sample size = 1" do
104
+ expect(calculator.calculate_mean_of_churn(8, 1)).to eq 8
114
105
  end
115
106
 
116
107
  it "returns churn divided by sample size" do
117
- calculator.calculate_mean_of_churn(25,3).should == 8
108
+ expect(calculator.calculate_mean_of_churn(25, 3)).to eq 8
118
109
  end
119
-
120
110
  end
121
111
 
122
112
  context "Full stack tests" do
123
113
  context "when one ruby file is given" do
124
114
  context "with two log entries for file" do
125
115
  before do
126
- calculator.stub(:scm_log_command) do
116
+ allow(calculator).to receive(:scm_log_command).and_return(
127
117
  "\n\n\n\n10\t6\tlib/turbulence.rb\n" +
128
- "\n\n\n\n11\t7\tlib/turbulence.rb\n"
129
- end
118
+ "\n\n\n\n11\t7\tlib/turbulence.rb\n"
119
+ )
130
120
  end
131
121
  it "gives the line change count for the file" do
132
122
  yielded_files = []
133
123
  calculator.for_these_files(["lib/turbulence.rb"]) do |filename, score|
134
124
  yielded_files << [filename, score]
135
125
  end
136
- yielded_files.should =~ [["lib/turbulence.rb", 16]]
126
+ expect(yielded_files).to match_array([["lib/turbulence.rb", 16]])
137
127
  end
138
128
  context "with only one log entry for file" do
139
129
  before do
140
- calculator.stub(:scm_log_command) do
130
+ allow(calculator).to receive(:scm_log_command).and_return(
141
131
  "\n\n\n\n10\t6\tlib/turbulence.rb\n"
142
- end
132
+ )
143
133
  end
144
134
  it "shows zero churn for the file" do
145
135
  yielded_files = []
146
136
  calculator.for_these_files(["lib/turbulence.rb"]) do |filename, score|
147
137
  yielded_files << [filename, score]
148
138
  end
149
- yielded_files.should =~ [["lib/turbulence.rb", 0]]
139
+ expect(yielded_files).to match_array([["lib/turbulence.rb", 0]])
150
140
  end
151
141
  end
152
142
  end
@@ -1,19 +1,18 @@
1
1
  require 'turbulence/calculators/complexity'
2
2
 
3
3
  describe Turbulence::Calculators::Complexity do
4
- let(:calculator) { Turbulence::Calculators::Complexity }
4
+ let(:calculator) { Turbulence::Calculators::Complexity.new(config) }
5
+ let(:config) { Turbulence::Configuration.new }
6
+
5
7
  describe "::for_these_files" do
6
8
  it "yields up the filename and score for each file" do
7
9
  files = ["lib/corey.rb", "lib/chad.rb"]
8
- calculator.stub(:score_for_file) { |filename|
9
- filename.size
10
- }
10
+ allow(calculator).to receive(:score_for_file) { |filename| filename.size }
11
11
  yielded_files = []
12
12
  calculator.for_these_files(files) do |filename, score|
13
13
  yielded_files << [filename, score]
14
14
  end
15
- yielded_files.should =~ [["lib/corey.rb", 12],
16
- ["lib/chad.rb",11]]
15
+ expect(yielded_files).to match_array([["lib/corey.rb", 12], ["lib/chad.rb", 11]])
17
16
  end
18
17
  end
19
18
  end