turbulence 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.travis.yml +6 -0
- data/Gemfile.lock +12 -11
- data/README.md +19 -0
- data/Rakefile +3 -0
- data/lib/turbulence.rb +18 -5
- data/lib/turbulence/calculators/complexity.rb +2 -10
- data/lib/turbulence/command_line_interface.rb +30 -9
- data/lib/turbulence/file_name_mangler.rb +18 -0
- data/lib/turbulence/generators/scatterplot.rb +65 -0
- data/lib/turbulence/generators/treemap.rb +48 -0
- data/lib/turbulence/version.rb +1 -1
- data/spec/turbulence/calculators/complexity_spec.rb +0 -17
- data/spec/turbulence/command_line_interface_spec.rb +6 -2
- data/spec/turbulence/{scatter_splot_generator_spec.rb → generators/scatter_plot_spec.rb} +25 -27
- data/spec/turbulence/generators/treemap_spec.rb +26 -0
- data/template/treemap.html +29 -0
- data/turbulence.gemspec +3 -2
- metadata +119 -59
- data/lib/turbulence/scatter_plot_generator.rb +0 -65
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZWM3NDQzMzE3NGIwODMyNGE0YmJmZjg1N2E1OThhNzY0M2M3NzNhNQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MTM3OTcwYmY3MjhmNTljZmI4M2RhMTA0MjVhOTliYjdiNTNmNDM5OA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MzdhYzRjNGNmMGQwODEwZTk2ZWNiMzRjMmZjMzUyNzgzY2IzNWY5YWZlODFm
|
10
|
+
MmJlYjNjODAwMDE4YmYyNzlkZDM2NTZkNzY2OWU5NjJmNDVmYzE3OWJlN2I4
|
11
|
+
YmIyOWZhMGMwOTFhOWJjZWIxY2MwYWU4ODlkODMxNjZiM2I4ZDk=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ODk3MmQ1MmUwZmIyOTRkMTRhMjkzYjNjM2M0ZmY0NmQ5ZTMxMDg0OTBiZTIw
|
14
|
+
NjQ1NDVjNjk5OTgxMWQyMTNlMzZhMmU3NzdlMmFjNTJjYTJlOGEyYjhhZjNl
|
15
|
+
MTQ0Mzc0ZDk1ZGFiZjBjNzZlNTAxZDk0YWU3MDAwODE1YTliMDA=
|
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
@@ -1,22 +1,23 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
turbulence (1.
|
5
|
-
flog (
|
4
|
+
turbulence (1.2.0)
|
5
|
+
flog (~> 4.1)
|
6
6
|
json (>= 1.4.6)
|
7
|
-
launchy (
|
7
|
+
launchy (>= 2.0.0)
|
8
8
|
|
9
9
|
GEM
|
10
10
|
remote: http://rubygems.org/
|
11
11
|
specs:
|
12
|
-
addressable (2.
|
12
|
+
addressable (2.3.5)
|
13
13
|
diff-lcs (1.1.3)
|
14
|
-
flog (
|
14
|
+
flog (4.1.2)
|
15
15
|
ruby_parser (~> 3.1, > 3.1.0)
|
16
16
|
sexp_processor (~> 4.0)
|
17
|
-
json (1.
|
18
|
-
launchy (2.0
|
19
|
-
addressable (~> 2.
|
17
|
+
json (1.8.0)
|
18
|
+
launchy (2.3.0)
|
19
|
+
addressable (~> 2.3)
|
20
|
+
rake (10.1.0)
|
20
21
|
rspec (2.6.0)
|
21
22
|
rspec-core (~> 2.6.0)
|
22
23
|
rspec-expectations (~> 2.6.0)
|
@@ -25,14 +26,14 @@ GEM
|
|
25
26
|
rspec-expectations (2.6.0)
|
26
27
|
diff-lcs (~> 1.1.2)
|
27
28
|
rspec-mocks (2.6.0)
|
28
|
-
ruby_parser (3.
|
29
|
+
ruby_parser (3.2.2)
|
29
30
|
sexp_processor (~> 4.1)
|
30
|
-
sexp_processor (4.
|
31
|
+
sexp_processor (4.3.0)
|
31
32
|
|
32
33
|
PLATFORMS
|
33
34
|
ruby
|
34
|
-
x86-mingw32
|
35
35
|
|
36
36
|
DEPENDENCIES
|
37
|
+
rake
|
37
38
|
rspec (~> 2.6.0)
|
38
39
|
turbulence!
|
data/README.md
CHANGED
@@ -3,6 +3,25 @@ Hopefully-meaningful Metrics
|
|
3
3
|
|
4
4
|
Based on Michael Feathers' [recent work](http://www.stickyminds.com/sitewide.asp?Function=edetail&ObjectType=COL&ObjectId=16679&tth=DYN&tt=siteemail&iDyn=2) in project churn and complexity.
|
5
5
|
|
6
|
+
Here is how to read the graph (extracted from the above article):
|
7
|
+
|
8
|
+
* The upper right quadrant is particularly important.
|
9
|
+
These files have a high degree of complexity, and they change quite frequently.
|
10
|
+
There are a number of reasons why this can happen.
|
11
|
+
The one to look out for, though, is something I call runaway conditionals.
|
12
|
+
Sometimes a class becomes so complex that refactoring seems too difficult.
|
13
|
+
Developers hack if-then-elses into if-then-elses, and the rat’s nest grows. These classes are particularly ripe for a refactoring investment.
|
14
|
+
|
15
|
+
* The lower left quadrant. is the healthy closure region.
|
16
|
+
Abstractions here have low complexity and don't change much.
|
17
|
+
|
18
|
+
* The upper left is what I call the cowboy region. This is complex code that sprang from someone's head and didn't seem to grow incrementally.
|
19
|
+
|
20
|
+
* The bottom right is very interesting. I call it the fertile ground.
|
21
|
+
It can consist of files that are somewhat configurational, but often there are also files that act as incubators for new abstractions.
|
22
|
+
People add code, it grows, and then they factor outward, extracting new classes. The files churn frequently, but their complexity remains low.
|
23
|
+
|
24
|
+
|
6
25
|
Installation
|
7
26
|
------------
|
8
27
|
|
data/Rakefile
CHANGED
data/lib/turbulence.rb
CHANGED
@@ -1,19 +1,30 @@
|
|
1
|
-
require 'turbulence/
|
1
|
+
require 'turbulence/file_name_mangler'
|
2
2
|
require 'turbulence/command_line_interface'
|
3
3
|
require 'turbulence/checks_environment'
|
4
4
|
require 'turbulence/calculators/churn'
|
5
5
|
require 'turbulence/calculators/complexity'
|
6
|
+
require 'turbulence/generators/treemap'
|
7
|
+
require 'turbulence/generators/scatterplot'
|
6
8
|
|
7
9
|
class Turbulence
|
8
|
-
CODE_DIRECTORIES = ["app/models",
|
9
|
-
|
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
19
|
|
11
20
|
attr_reader :exclusion_pattern
|
12
21
|
attr_reader :metrics
|
22
|
+
|
13
23
|
def initialize(directory, output = nil, exclusion_pattern = nil)
|
14
|
-
@output
|
15
|
-
@metrics
|
24
|
+
@output = output
|
25
|
+
@metrics = {}
|
16
26
|
@exclusion_pattern = exclusion_pattern
|
27
|
+
|
17
28
|
Dir.chdir(directory) do
|
18
29
|
CALCULATORS.each(&method(:calculate_metrics_with))
|
19
30
|
end
|
@@ -26,10 +37,12 @@ class Turbulence
|
|
26
37
|
|
27
38
|
def calculate_metrics_with(calculator)
|
28
39
|
report "calculating metric: #{calculator}\n"
|
40
|
+
|
29
41
|
calculator.for_these_files(files_of_interest) do |filename, score|
|
30
42
|
report "."
|
31
43
|
set_file_metric(filename, calculator, score)
|
32
44
|
end
|
45
|
+
|
33
46
|
report "\n"
|
34
47
|
end
|
35
48
|
|
@@ -29,17 +29,9 @@ class Turbulence
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def score_for_file(filename)
|
32
|
+
flogger.reset
|
32
33
|
flogger.flog filename
|
33
|
-
|
34
|
-
flogger.report(reporter)
|
35
|
-
reporter.score
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
class Reporter < ::StringIO
|
40
|
-
SCORE_LINE_DETECTOR = /^\s+([^:]+).*flog total$/
|
41
|
-
def score
|
42
|
-
Float(string.scan(SCORE_LINE_DETECTOR).flatten.first)
|
34
|
+
flogger.total_score
|
43
35
|
end
|
44
36
|
end
|
45
37
|
end
|
@@ -7,10 +7,13 @@ require 'turbulence/scm/perforce'
|
|
7
7
|
class Turbulence
|
8
8
|
class CommandLineInterface
|
9
9
|
TURBULENCE_TEMPLATE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "template")
|
10
|
-
TEMPLATE_FILES = ['turbulence.html',
|
10
|
+
TEMPLATE_FILES = ['turbulence.html',
|
11
|
+
'highcharts.js',
|
12
|
+
'jquery.min.js',
|
13
|
+
'treemap.html'].map do |filename|
|
11
14
|
File.join(TURBULENCE_TEMPLATE_PATH, filename)
|
12
|
-
|
13
|
-
|
15
|
+
end
|
16
|
+
|
14
17
|
attr_reader :exclusion_pattern
|
15
18
|
attr_reader :directory
|
16
19
|
def initialize(argv)
|
@@ -25,16 +28,24 @@ class Turbulence
|
|
25
28
|
Turbulence::Calculators::Churn.scm = Scm::Perforce
|
26
29
|
end
|
27
30
|
end
|
31
|
+
|
28
32
|
opts.on('--churn-range since..until', String, 'commit range to compute file churn') do |s|
|
29
33
|
Turbulence::Calculators::Churn.commit_range = s
|
30
34
|
end
|
35
|
+
|
31
36
|
opts.on('--churn-mean', 'calculate mean churn instead of cummulative') do
|
32
37
|
Turbulence::Calculators::Churn.compute_mean = true
|
33
38
|
end
|
39
|
+
|
34
40
|
opts.on('--exclude pattern', String, 'exclude files matching pattern') do |pattern|
|
35
41
|
@exclusion_pattern = pattern
|
36
42
|
end
|
37
43
|
|
44
|
+
opts.on('--treemap', String, 'output treemap graph instead of scatterplot') do |s|
|
45
|
+
@graph_type = "treemap"
|
46
|
+
end
|
47
|
+
|
48
|
+
|
38
49
|
opts.on_tail("-h", "--help", "Show this message") do
|
39
50
|
puts opts
|
40
51
|
exit
|
@@ -47,21 +58,31 @@ class Turbulence
|
|
47
58
|
def copy_templates_into(directory)
|
48
59
|
FileUtils.cp TEMPLATE_FILES, directory
|
49
60
|
end
|
50
|
-
private :copy_templates_into
|
51
61
|
|
52
62
|
def generate_bundle
|
53
63
|
FileUtils.mkdir_p("turbulence")
|
64
|
+
|
54
65
|
Dir.chdir("turbulence") do
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
66
|
+
turb = Turbulence.new(directory,STDOUT, @exclusion_pattern)
|
67
|
+
|
68
|
+
generator = case @graph_type
|
69
|
+
when "treemap"
|
70
|
+
Turbulence::Generators::TreeMap.new({})
|
71
|
+
else
|
72
|
+
Turbulence::Generators::ScatterPlot.new({})
|
59
73
|
end
|
74
|
+
|
75
|
+
generator.generate_results(turb.metrics, self)
|
60
76
|
end
|
61
77
|
end
|
62
78
|
|
63
79
|
def open_bundle
|
64
|
-
|
80
|
+
case @graph_type
|
81
|
+
when "treemap"
|
82
|
+
Launchy.open("file://#{directory}/turbulence/treemap.html")
|
83
|
+
else
|
84
|
+
Launchy.open("file://#{directory}/turbulence/turbulence.html")
|
85
|
+
end
|
65
86
|
end
|
66
87
|
end
|
67
88
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Turbulence
|
4
|
+
class FileNameMangler
|
5
|
+
def initialize
|
6
|
+
@current_id = 0
|
7
|
+
@segment_map = { "" => "", "app" => "app", "controllers" => "controllers", "helpers" => "helpers", "lib" => "lib" }
|
8
|
+
end
|
9
|
+
|
10
|
+
def transform(segment)
|
11
|
+
@segment_map[segment] ||= (@current_id += 1)
|
12
|
+
end
|
13
|
+
|
14
|
+
def mangle_name(filename)
|
15
|
+
filename.split('/').map {|seg|transform(seg)}.join('/') + ".rb"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Turbulence
|
2
|
+
module Generators
|
3
|
+
class ScatterPlot
|
4
|
+
attr_reader :metrics_hash, :x_metric, :y_metric
|
5
|
+
|
6
|
+
def initialize(metrics_hash,
|
7
|
+
x_metric = Turbulence::Calculators::Churn,
|
8
|
+
y_metric = Turbulence::Calculators::Complexity)
|
9
|
+
@x_metric = x_metric
|
10
|
+
@y_metric = y_metric
|
11
|
+
@metrics_hash = metrics_hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from(metrics_hash)
|
15
|
+
new(metrics_hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
def mangle
|
19
|
+
mangler = FileNameMangler.new
|
20
|
+
mangled = {}
|
21
|
+
metrics_hash.each_pair { |filename, metrics| mangled[mangler.mangle_name(filename)] = metrics}
|
22
|
+
@metrics_hash = mangled
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_js
|
26
|
+
clean_metrics_from_missing_data
|
27
|
+
directory_series = {}
|
28
|
+
|
29
|
+
grouped_by_directory.each_pair do |directory, metrics_hash|
|
30
|
+
directory_series[directory] = file_metrics_for_directory(metrics_hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
"var directorySeries = #{directory_series.to_json};"
|
34
|
+
end
|
35
|
+
|
36
|
+
def clean_metrics_from_missing_data
|
37
|
+
metrics_hash.reject! do |filename, metrics|
|
38
|
+
metrics[x_metric].nil? || metrics[y_metric].nil?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def grouped_by_directory
|
43
|
+
metrics_hash.group_by do |filename, _|
|
44
|
+
directories = File.dirname(filename).split("/")
|
45
|
+
directories[0..1].join("/")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def file_metrics_for_directory(metrics_hash)
|
50
|
+
metrics_hash.map do |filename, metrics|
|
51
|
+
{ :filename => filename,
|
52
|
+
:x => metrics[x_metric],
|
53
|
+
:y => metrics[y_metric]}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def generate_results(metrics, ci)
|
58
|
+
File.open("cc.js", "w") do |f|
|
59
|
+
ci.copy_templates_into(Dir.pwd)
|
60
|
+
f.write Turbulence::Generators::ScatterPlot.from(metrics).to_js
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Turbulence
|
2
|
+
module Generators
|
3
|
+
class TreeMap
|
4
|
+
attr_reader :metrics_hash, :x_metric, :y_metric
|
5
|
+
|
6
|
+
def initialize(metrics_hash,
|
7
|
+
x_metric = Turbulence::Calculators::Churn,
|
8
|
+
y_metric = Turbulence::Calculators::Complexity)
|
9
|
+
@x_metric = x_metric
|
10
|
+
@y_metric = y_metric
|
11
|
+
@metrics_hash = metrics_hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate_results(metrics, cli)
|
15
|
+
File.open("treemap_data.js", "w") do |f|
|
16
|
+
cli.copy_templates_into(Dir.pwd)
|
17
|
+
f.write Turbulence::Generators::TreeMap.from(metrics).build_js
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_js
|
22
|
+
clean_metrics_from_missing_data
|
23
|
+
|
24
|
+
output = "var treemap_data = [['File', 'Parent', 'Churn (size)', 'Complexity (color)'],\n"
|
25
|
+
output << "['Root', null, 0, 0],\n"
|
26
|
+
|
27
|
+
@metrics_hash.each do |file|
|
28
|
+
output << "['#{file[0]}', 'Root', #{file[1][@x_metric]}, #{file[1][@y_metric]}],\n"
|
29
|
+
end
|
30
|
+
|
31
|
+
output << "];"
|
32
|
+
|
33
|
+
output
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def clean_metrics_from_missing_data
|
38
|
+
@metrics_hash.reject! do |filename, metrics|
|
39
|
+
metrics[@x_metric].nil? || metrics[@y_metric].nil?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.from(metrics_hash)
|
44
|
+
new(metrics_hash)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/turbulence/version.rb
CHANGED
@@ -18,20 +18,3 @@ describe Turbulence::Calculators::Complexity do
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
describe Turbulence::Calculators::Complexity::Reporter do
|
22
|
-
subject { Turbulence::Calculators::Complexity::Reporter.new }
|
23
|
-
it "uses the total value from flog" do
|
24
|
-
flog_output = <<-FLOG_OUTPUT
|
25
|
-
38.7: flog total
|
26
|
-
5.5: flog/method average
|
27
|
-
|
28
|
-
9.3: Turbulence#initialize lib/turbulence.rb:9
|
29
|
-
8.7: Turbulence#churn lib/turbulence.rb:41
|
30
|
-
6.1: Turbulence#complexity lib/turbulence.rb:26
|
31
|
-
FLOG_OUTPUT
|
32
|
-
|
33
|
-
subject.stub(:string) { flog_output }
|
34
|
-
|
35
|
-
subject.score.should == 38.7
|
36
|
-
end
|
37
|
-
end
|
@@ -14,7 +14,11 @@ describe Turbulence::CommandLineInterface do
|
|
14
14
|
end
|
15
15
|
it "bundles the files" do
|
16
16
|
cli.generate_bundle
|
17
|
-
Dir.glob('turbulence/*').sort.should eq(["turbulence/cc.js",
|
17
|
+
Dir.glob('turbulence/*').sort.should eq(["turbulence/cc.js",
|
18
|
+
"turbulence/highcharts.js",
|
19
|
+
"turbulence/jquery.min.js",
|
20
|
+
"turbulence/treemap.html",
|
21
|
+
"turbulence/turbulence.html"])
|
18
22
|
end
|
19
23
|
|
20
24
|
it "passes along exclusion pattern" do
|
@@ -33,7 +37,7 @@ describe Turbulence::CommandLineInterface do
|
|
33
37
|
cli_churn_range.directory.should == 'path/to/compute'
|
34
38
|
Turbulence::Calculators::Churn.commit_range.should == 'f3e1d7a6..830b9d3d9f'
|
35
39
|
end
|
36
|
-
|
40
|
+
|
37
41
|
it "sets churn mean" do
|
38
42
|
cli_churn_mean.directory.should == '.'
|
39
43
|
Turbulence::Calculators::Churn.compute_mean.should be_true
|
@@ -1,49 +1,47 @@
|
|
1
1
|
require 'turbulence'
|
2
2
|
|
3
|
-
describe Turbulence::
|
3
|
+
describe Turbulence::Generators::ScatterPlot do
|
4
4
|
context "with both Metrics" do
|
5
5
|
it "generates JavaScript" do
|
6
|
-
generator = Turbulence::
|
7
|
-
"foo.rb" => {
|
8
|
-
|
9
|
-
Turbulence::Calculators::Complexity => 2
|
10
|
-
}
|
6
|
+
generator = Turbulence::Generators::ScatterPlot.new(
|
7
|
+
"foo.rb" => { Turbulence::Calculators::Churn => 1,
|
8
|
+
Turbulence::Calculators::Complexity => 2 }
|
11
9
|
)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
|
11
|
+
generator.to_js.should =~ /var directorySeries/
|
12
|
+
generator.to_js.should =~ /\"filename\"\:\"foo.rb\"/
|
13
|
+
generator.to_js.should =~ /\"x\":1/
|
14
|
+
generator.to_js.should =~ /\"y\":2/
|
15
|
+
end
|
17
16
|
end
|
18
17
|
|
19
18
|
context "with a missing Metric" do
|
20
19
|
it "generates JavaScript" do
|
21
|
-
generator = Turbulence::
|
22
|
-
"foo.rb" => {
|
23
|
-
Turbulence::Calculators::Churn => 1
|
24
|
-
}
|
20
|
+
generator = Turbulence::Generators::ScatterPlot.new(
|
21
|
+
"foo.rb" => { Turbulence::Calculators::Churn => 1 }
|
25
22
|
)
|
23
|
+
|
26
24
|
generator.to_js.should == 'var directorySeries = {};'
|
27
|
-
end
|
25
|
+
end
|
28
26
|
end
|
29
27
|
|
30
|
-
describe "#clean_metrics_from_missing_data" do
|
31
|
-
let(:spg) {Turbulence::
|
28
|
+
describe "#clean_metrics_from_missing_data" do
|
29
|
+
let(:spg) {Turbulence::Generators::ScatterPlot.new({})}
|
32
30
|
|
33
|
-
it "removes entries with missing churn" do
|
34
|
-
spg.stub(:metrics_hash).and_return("foo.rb" => {
|
31
|
+
it "removes entries with missing churn" do
|
32
|
+
spg.stub(:metrics_hash).and_return("foo.rb" => {
|
35
33
|
Turbulence::Calculators::Complexity => 88.3})
|
36
34
|
spg.clean_metrics_from_missing_data.should == {}
|
37
35
|
end
|
38
36
|
|
39
37
|
it "removes entries with missing complexity" do
|
40
|
-
spg.stub(:metrics_hash).and_return("foo.rb" => {
|
38
|
+
spg.stub(:metrics_hash).and_return("foo.rb" => {
|
41
39
|
Turbulence::Calculators::Churn => 1})
|
42
40
|
spg.clean_metrics_from_missing_data.should == {}
|
43
41
|
end
|
44
42
|
|
45
43
|
it "keeps entries with churn and complexity present" do
|
46
|
-
spg.stub(:metrics_hash).and_return("foo.rb" => {
|
44
|
+
spg.stub(:metrics_hash).and_return("foo.rb" => {
|
47
45
|
Turbulence::Calculators::Churn => 1,
|
48
46
|
Turbulence::Calculators::Complexity => 88.3})
|
49
47
|
spg.clean_metrics_from_missing_data.should_not == {}
|
@@ -51,7 +49,7 @@ describe Turbulence::ScatterPlotGenerator do
|
|
51
49
|
end
|
52
50
|
|
53
51
|
describe "#grouped_by_directory" do
|
54
|
-
let(:spg) {Turbulence::
|
52
|
+
let(:spg) {Turbulence::Generators::ScatterPlot.new("lib/foo/foo.rb" => {
|
55
53
|
Turbulence::Calculators::Churn => 1},
|
56
54
|
"lib/bar.rb" => {
|
57
55
|
Turbulence::Calculators::Churn => 2} )}
|
@@ -60,17 +58,17 @@ describe Turbulence::ScatterPlotGenerator do
|
|
60
58
|
spg.stub(:metrics_hash).and_return("foo.rb" => {
|
61
59
|
Turbulence::Calculators::Churn => 1
|
62
60
|
})
|
63
|
-
spg.grouped_by_directory.should ==
|
61
|
+
spg.grouped_by_directory.should == {"." => [["foo.rb", {Turbulence::Calculators::Churn => 1}]]}
|
64
62
|
end
|
65
63
|
|
66
64
|
it "takes full path into account" do
|
67
|
-
spg.grouped_by_directory.should ==
|
68
|
-
"lib" => [["lib/bar.rb", Turbulence::Calculators::Churn => 2]]}
|
65
|
+
spg.grouped_by_directory.should == {"lib/foo" => [["lib/foo/foo.rb", {Turbulence::Calculators::Churn => 1}]],
|
66
|
+
"lib" => [["lib/bar.rb", {Turbulence::Calculators::Churn => 2}]]}
|
69
67
|
end
|
70
68
|
end
|
71
69
|
|
72
70
|
describe "#file_metrics_for_directory" do
|
73
|
-
let(:spg) {Turbulence::
|
71
|
+
let(:spg) {Turbulence::Generators::ScatterPlot.new({})}
|
74
72
|
it "assigns :filename, :x, :y" do
|
75
73
|
spg.file_metrics_for_directory("lib/foo/foo.rb" => {
|
76
74
|
Turbulence::Calculators::Churn => 1,
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'turbulence'
|
2
|
+
|
3
|
+
describe Turbulence::Generators::TreeMap do
|
4
|
+
context "with both Metrics" do
|
5
|
+
it "generates JavaScript" do
|
6
|
+
generator = Turbulence::Generators::TreeMap.new(
|
7
|
+
"foo.rb" => { Turbulence::Calculators::Churn => 1,
|
8
|
+
Turbulence::Calculators::Complexity => 2 }
|
9
|
+
)
|
10
|
+
|
11
|
+
generator.build_js.should =~ /var treemap_data/
|
12
|
+
generator.build_js.should =~ /\'foo.rb\'/
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "with a missing Metric" do
|
17
|
+
it "generates JavaScript" do
|
18
|
+
generator = Turbulence::Generators::TreeMap.new(
|
19
|
+
"foo.rb" => { Turbulence::Calculators::Churn => 1 }
|
20
|
+
)
|
21
|
+
|
22
|
+
generator.build_js.should == "var treemap_data = [['File', 'Parent', 'Churn (size)', 'Complexity (color)'],\n['Root', null, 0, 0],\n];"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<script src="treemap_data.js"></script>
|
4
|
+
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
|
5
|
+
<script type="text/javascript">
|
6
|
+
google.load("visualization", "1", {packages:["treemap"]});
|
7
|
+
google.setOnLoadCallback(drawChart);
|
8
|
+
function drawChart() {
|
9
|
+
// Create and populate the data table.
|
10
|
+
var data = google.visualization.arrayToDataTable(treemap_data);
|
11
|
+
|
12
|
+
// Create and draw the visualization.
|
13
|
+
var tree = new google.visualization.TreeMap(document.getElementById('chart_div'));
|
14
|
+
tree.draw(data, {
|
15
|
+
minColor: '#0d0',
|
16
|
+
midColor: '#ddd',
|
17
|
+
maxColor: '#f00',
|
18
|
+
headerHeight: 15,
|
19
|
+
fontColor: 'black',
|
20
|
+
showScale: true,
|
21
|
+
showTooltips: true});
|
22
|
+
}
|
23
|
+
</script>
|
24
|
+
</head>
|
25
|
+
|
26
|
+
<body>
|
27
|
+
<div id="chart_div" style="width: 900px; height: 500px;"></div>
|
28
|
+
</body>
|
29
|
+
</html>
|
data/turbulence.gemspec
CHANGED
@@ -9,10 +9,11 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.authors = ["Chad Fowler", "Michael Feathers", "Corey Haines"]
|
10
10
|
s.email = ["chad@chadfowler.com", "mfeathers@obtiva.com", "coreyhaines@gmail.com"]
|
11
11
|
s.homepage = "http://chadfowler.com"
|
12
|
-
s.add_dependency "flog", "
|
12
|
+
s.add_dependency "flog", "~>4.1"
|
13
13
|
s.add_dependency "json", ">= 1.4.6"
|
14
|
-
s.add_dependency "launchy", "
|
14
|
+
s.add_dependency "launchy", ">= 2.0.0"
|
15
15
|
s.add_development_dependency 'rspec', '~> 2.6.0'
|
16
|
+
s.add_development_dependency 'rake'
|
16
17
|
|
17
18
|
s.summary = %q{Automates churn + flog scoring on a git repo for a Ruby project}
|
18
19
|
s.description = %q{Based on this http://www.stickyminds.com/sitewide.asp?Function=edetail&ObjectType=COL&ObjectId=16679&tth=DYN&tt=siteemail&iDyn=2}
|
metadata
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbulence
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
5
|
-
prerelease:
|
4
|
+
version: 1.2.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Chad Fowler
|
@@ -11,28 +10,25 @@ authors:
|
|
11
10
|
autorequire:
|
12
11
|
bindir: bin
|
13
12
|
cert_chain: []
|
14
|
-
date: 2013-
|
13
|
+
date: 2013-09-07 00:00:00.000000000 Z
|
15
14
|
dependencies:
|
16
15
|
- !ruby/object:Gem::Dependency
|
17
16
|
name: flog
|
18
17
|
requirement: !ruby/object:Gem::Requirement
|
19
|
-
none: false
|
20
18
|
requirements:
|
21
|
-
- -
|
19
|
+
- - ~>
|
22
20
|
- !ruby/object:Gem::Version
|
23
|
-
version:
|
21
|
+
version: '4.1'
|
24
22
|
type: :runtime
|
25
23
|
prerelease: false
|
26
24
|
version_requirements: !ruby/object:Gem::Requirement
|
27
|
-
none: false
|
28
25
|
requirements:
|
29
|
-
- -
|
26
|
+
- - ~>
|
30
27
|
- !ruby/object:Gem::Version
|
31
|
-
version:
|
28
|
+
version: '4.1'
|
32
29
|
- !ruby/object:Gem::Dependency
|
33
30
|
name: json
|
34
31
|
requirement: !ruby/object:Gem::Requirement
|
35
|
-
none: false
|
36
32
|
requirements:
|
37
33
|
- - ! '>='
|
38
34
|
- !ruby/object:Gem::Version
|
@@ -40,7 +36,6 @@ dependencies:
|
|
40
36
|
type: :runtime
|
41
37
|
prerelease: false
|
42
38
|
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
none: false
|
44
39
|
requirements:
|
45
40
|
- - ! '>='
|
46
41
|
- !ruby/object:Gem::Version
|
@@ -48,23 +43,20 @@ dependencies:
|
|
48
43
|
- !ruby/object:Gem::Dependency
|
49
44
|
name: launchy
|
50
45
|
requirement: !ruby/object:Gem::Requirement
|
51
|
-
none: false
|
52
46
|
requirements:
|
53
|
-
- -
|
47
|
+
- - ! '>='
|
54
48
|
- !ruby/object:Gem::Version
|
55
49
|
version: 2.0.0
|
56
50
|
type: :runtime
|
57
51
|
prerelease: false
|
58
52
|
version_requirements: !ruby/object:Gem::Requirement
|
59
|
-
none: false
|
60
53
|
requirements:
|
61
|
-
- -
|
54
|
+
- - ! '>='
|
62
55
|
- !ruby/object:Gem::Version
|
63
56
|
version: 2.0.0
|
64
57
|
- !ruby/object:Gem::Dependency
|
65
58
|
name: rspec
|
66
59
|
requirement: !ruby/object:Gem::Requirement
|
67
|
-
none: false
|
68
60
|
requirements:
|
69
61
|
- - ~>
|
70
62
|
- !ruby/object:Gem::Version
|
@@ -72,80 +64,148 @@ dependencies:
|
|
72
64
|
type: :development
|
73
65
|
prerelease: false
|
74
66
|
version_requirements: !ruby/object:Gem::Requirement
|
75
|
-
none: false
|
76
67
|
requirements:
|
77
68
|
- - ~>
|
78
69
|
- !ruby/object:Gem::Version
|
79
70
|
version: 2.6.0
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: rake
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
80
85
|
description: Based on this http://www.stickyminds.com/sitewide.asp?Function=edetail&ObjectType=COL&ObjectId=16679&tth=DYN&tt=siteemail&iDyn=2
|
81
86
|
email:
|
82
87
|
- chad@chadfowler.com
|
83
88
|
- mfeathers@obtiva.com
|
84
89
|
- coreyhaines@gmail.com
|
85
90
|
executables:
|
86
|
-
-
|
91
|
+
- !binary |-
|
92
|
+
YnVsZQ==
|
87
93
|
extensions: []
|
88
94
|
extra_rdoc_files: []
|
89
95
|
files:
|
90
|
-
-
|
91
|
-
|
92
|
-
-
|
93
|
-
|
94
|
-
-
|
95
|
-
|
96
|
-
-
|
97
|
-
|
98
|
-
-
|
99
|
-
|
100
|
-
-
|
101
|
-
|
102
|
-
-
|
103
|
-
|
104
|
-
-
|
105
|
-
|
106
|
-
-
|
107
|
-
|
108
|
-
-
|
109
|
-
|
110
|
-
-
|
111
|
-
|
112
|
-
-
|
113
|
-
|
114
|
-
-
|
115
|
-
|
116
|
-
-
|
117
|
-
|
118
|
-
-
|
96
|
+
- !binary |-
|
97
|
+
LmdpdGlnbm9yZQ==
|
98
|
+
- !binary |-
|
99
|
+
LnRyYXZpcy55bWw=
|
100
|
+
- !binary |-
|
101
|
+
R2VtZmlsZQ==
|
102
|
+
- !binary |-
|
103
|
+
R2VtZmlsZS5sb2Nr
|
104
|
+
- !binary |-
|
105
|
+
UkVBRE1FLm1k
|
106
|
+
- !binary |-
|
107
|
+
UmFrZWZpbGU=
|
108
|
+
- !binary |-
|
109
|
+
YmluL2J1bGU=
|
110
|
+
- !binary |-
|
111
|
+
bGliL3R1cmJ1bGVuY2UucmI=
|
112
|
+
- !binary |-
|
113
|
+
bGliL3R1cmJ1bGVuY2UvY2FsY3VsYXRvcnMvY2h1cm4ucmI=
|
114
|
+
- !binary |-
|
115
|
+
bGliL3R1cmJ1bGVuY2UvY2FsY3VsYXRvcnMvY29tcGxleGl0eS5yYg==
|
116
|
+
- !binary |-
|
117
|
+
bGliL3R1cmJ1bGVuY2UvY2hlY2tzX2Vudmlyb25tZW50LnJi
|
118
|
+
- !binary |-
|
119
|
+
bGliL3R1cmJ1bGVuY2UvY29tbWFuZF9saW5lX2ludGVyZmFjZS5yYg==
|
120
|
+
- !binary |-
|
121
|
+
bGliL3R1cmJ1bGVuY2UvZmlsZV9uYW1lX21hbmdsZXIucmI=
|
122
|
+
- !binary |-
|
123
|
+
bGliL3R1cmJ1bGVuY2UvZ2VuZXJhdG9ycy9zY2F0dGVycGxvdC5yYg==
|
124
|
+
- !binary |-
|
125
|
+
bGliL3R1cmJ1bGVuY2UvZ2VuZXJhdG9ycy90cmVlbWFwLnJi
|
126
|
+
- !binary |-
|
127
|
+
bGliL3R1cmJ1bGVuY2Uvc2NtL2dpdC5yYg==
|
128
|
+
- !binary |-
|
129
|
+
bGliL3R1cmJ1bGVuY2Uvc2NtL3BlcmZvcmNlLnJi
|
130
|
+
- !binary |-
|
131
|
+
bGliL3R1cmJ1bGVuY2UvdmVyc2lvbi5yYg==
|
132
|
+
- !binary |-
|
133
|
+
c3BlYy90dXJidWxlbmNlL2NhbGN1bGF0b3JzL2NodXJuX3NwZWMucmI=
|
134
|
+
- !binary |-
|
135
|
+
c3BlYy90dXJidWxlbmNlL2NhbGN1bGF0b3JzL2NvbXBsZXhpdHlfc3BlYy5y
|
136
|
+
Yg==
|
137
|
+
- !binary |-
|
138
|
+
c3BlYy90dXJidWxlbmNlL2NoZWNrc19lbnZpcm9ub21lbnRfc3BlYy5yYg==
|
139
|
+
- !binary |-
|
140
|
+
c3BlYy90dXJidWxlbmNlL2NvbW1hbmRfbGluZV9pbnRlcmZhY2Vfc3BlYy5y
|
141
|
+
Yg==
|
142
|
+
- !binary |-
|
143
|
+
c3BlYy90dXJidWxlbmNlL2dlbmVyYXRvcnMvc2NhdHRlcl9wbG90X3NwZWMu
|
144
|
+
cmI=
|
145
|
+
- !binary |-
|
146
|
+
c3BlYy90dXJidWxlbmNlL2dlbmVyYXRvcnMvdHJlZW1hcF9zcGVjLnJi
|
147
|
+
- !binary |-
|
148
|
+
c3BlYy90dXJidWxlbmNlL3NjbS9naXRfc3BlYy5yYg==
|
149
|
+
- !binary |-
|
150
|
+
c3BlYy90dXJidWxlbmNlL3NjbS9wZXJmb3JjZV9zcGVjLnJi
|
151
|
+
- !binary |-
|
152
|
+
c3BlYy90dXJidWxlbmNlL3R1cmJ1bGVuY2Vfc3BlYy5yYg==
|
153
|
+
- !binary |-
|
154
|
+
dGVtcGxhdGUvaGlnaGNoYXJ0X3RlbXBsYXRlLmpzLmVyYg==
|
155
|
+
- !binary |-
|
156
|
+
dGVtcGxhdGUvaGlnaGNoYXJ0cy5qcw==
|
157
|
+
- !binary |-
|
158
|
+
dGVtcGxhdGUvanF1ZXJ5Lm1pbi5qcw==
|
159
|
+
- !binary |-
|
160
|
+
dGVtcGxhdGUvdHJlZW1hcC5odG1s
|
161
|
+
- !binary |-
|
162
|
+
dGVtcGxhdGUvdHVyYnVsZW5jZS5odG1s
|
163
|
+
- !binary |-
|
164
|
+
dHVyYnVsZW5jZS5nZW1zcGVj
|
165
|
+
- !binary |-
|
166
|
+
d2luX3Jha2VmaWxlX2xvY2F0aW9uX2ZpeC5yYg==
|
119
167
|
homepage: http://chadfowler.com
|
120
168
|
licenses: []
|
169
|
+
metadata: {}
|
121
170
|
post_install_message:
|
122
171
|
rdoc_options: []
|
123
172
|
require_paths:
|
124
173
|
- lib
|
125
174
|
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
-
none: false
|
127
175
|
requirements:
|
128
176
|
- - ! '>='
|
129
177
|
- !ruby/object:Gem::Version
|
130
178
|
version: 1.8.7
|
131
179
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
-
none: false
|
133
180
|
requirements:
|
134
181
|
- - ! '>='
|
135
182
|
- !ruby/object:Gem::Version
|
136
183
|
version: '0'
|
137
184
|
requirements: []
|
138
185
|
rubyforge_project: turbulence
|
139
|
-
rubygems_version:
|
186
|
+
rubygems_version: 2.0.7
|
140
187
|
signing_key:
|
141
|
-
specification_version:
|
188
|
+
specification_version: 4
|
142
189
|
summary: Automates churn + flog scoring on a git repo for a Ruby project
|
143
190
|
test_files:
|
144
|
-
-
|
145
|
-
|
146
|
-
-
|
147
|
-
|
148
|
-
|
149
|
-
-
|
150
|
-
|
151
|
-
-
|
191
|
+
- !binary |-
|
192
|
+
c3BlYy90dXJidWxlbmNlL2NhbGN1bGF0b3JzL2NodXJuX3NwZWMucmI=
|
193
|
+
- !binary |-
|
194
|
+
c3BlYy90dXJidWxlbmNlL2NhbGN1bGF0b3JzL2NvbXBsZXhpdHlfc3BlYy5y
|
195
|
+
Yg==
|
196
|
+
- !binary |-
|
197
|
+
c3BlYy90dXJidWxlbmNlL2NoZWNrc19lbnZpcm9ub21lbnRfc3BlYy5yYg==
|
198
|
+
- !binary |-
|
199
|
+
c3BlYy90dXJidWxlbmNlL2NvbW1hbmRfbGluZV9pbnRlcmZhY2Vfc3BlYy5y
|
200
|
+
Yg==
|
201
|
+
- !binary |-
|
202
|
+
c3BlYy90dXJidWxlbmNlL2dlbmVyYXRvcnMvc2NhdHRlcl9wbG90X3NwZWMu
|
203
|
+
cmI=
|
204
|
+
- !binary |-
|
205
|
+
c3BlYy90dXJidWxlbmNlL2dlbmVyYXRvcnMvdHJlZW1hcF9zcGVjLnJi
|
206
|
+
- !binary |-
|
207
|
+
c3BlYy90dXJidWxlbmNlL3NjbS9naXRfc3BlYy5yYg==
|
208
|
+
- !binary |-
|
209
|
+
c3BlYy90dXJidWxlbmNlL3NjbS9wZXJmb3JjZV9zcGVjLnJi
|
210
|
+
- !binary |-
|
211
|
+
c3BlYy90dXJidWxlbmNlL3R1cmJ1bGVuY2Vfc3BlYy5yYg==
|
@@ -1,65 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
|
3
|
-
class Turbulence
|
4
|
-
class FileNameMangler
|
5
|
-
def initialize
|
6
|
-
@current_id = 0
|
7
|
-
@segment_map = { "" => "", "app" => "app", "controllers" => "controllers", "helpers" => "helpers", "lib" => "lib" }
|
8
|
-
end
|
9
|
-
|
10
|
-
def transform(segment)
|
11
|
-
@segment_map[segment] ||= (@current_id += 1)
|
12
|
-
end
|
13
|
-
|
14
|
-
def mangle_name(filename)
|
15
|
-
filename.split('/').map {|seg|transform(seg)}.join('/') + ".rb"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
class ScatterPlotGenerator
|
20
|
-
def self.from(metrics_hash)
|
21
|
-
new(metrics_hash)
|
22
|
-
end
|
23
|
-
attr_reader :metrics_hash, :x_metric, :y_metric
|
24
|
-
def initialize(metrics_hash, x_metric = Turbulence::Calculators::Churn, y_metric = Turbulence::Calculators::Complexity)
|
25
|
-
@x_metric = x_metric
|
26
|
-
@y_metric = y_metric
|
27
|
-
@metrics_hash = metrics_hash
|
28
|
-
end
|
29
|
-
|
30
|
-
def mangle
|
31
|
-
mangler = FileNameMangler.new
|
32
|
-
mangled = {}
|
33
|
-
metrics_hash.each_pair { |filename, metrics| mangled[mangler.mangle_name(filename)] = metrics}
|
34
|
-
@metrics_hash = mangled
|
35
|
-
end
|
36
|
-
|
37
|
-
def to_js
|
38
|
-
clean_metrics_from_missing_data
|
39
|
-
directory_series = {}
|
40
|
-
grouped_by_directory.each_pair do |directory, metrics_hash|
|
41
|
-
directory_series[directory] = file_metrics_for_directory(metrics_hash) end
|
42
|
-
|
43
|
-
"var directorySeries = #{directory_series.to_json};"
|
44
|
-
end
|
45
|
-
|
46
|
-
def clean_metrics_from_missing_data
|
47
|
-
metrics_hash.reject! do |filename, metrics|
|
48
|
-
metrics[x_metric].nil? || metrics[y_metric].nil?
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def grouped_by_directory
|
53
|
-
metrics_hash.group_by do |filename, _|
|
54
|
-
directories = File.dirname(filename).split("/")
|
55
|
-
directories[0..1].join("/")
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def file_metrics_for_directory(metrics_hash)
|
60
|
-
metrics_hash.map do |filename, metrics|
|
61
|
-
{:filename => filename, :x => metrics[x_metric], :y => metrics[y_metric]}
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|