turbulence 1.1.0 → 1.2.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.
- 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
|