turbulence 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/Gemfile.lock +11 -1
- data/bin/bule +3 -29
- data/lib/turbulence.rb +20 -42
- data/lib/turbulence/calculators/churn.rb +40 -0
- data/lib/turbulence/calculators/complexity.rb +33 -0
- data/lib/turbulence/command_line_interface.rb +37 -0
- data/lib/turbulence/scatter_plot_generator.rb +10 -12
- data/lib/turbulence/version.rb +1 -1
- data/spec/turbulence/calculators/churn_spec.rb +103 -0
- data/spec/turbulence/calculators/complexity_spec.rb +37 -0
- data/template/turbulence.html +8 -7
- metadata +11 -5
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
turbulence (0.0.
|
4
|
+
turbulence (0.0.3)
|
5
5
|
flog (= 2.5.0)
|
6
6
|
json (= 1.4.6)
|
7
7
|
launchy (~> 0.4.0)
|
@@ -10,6 +10,7 @@ GEM
|
|
10
10
|
remote: http://rubygems.org/
|
11
11
|
specs:
|
12
12
|
configuration (1.2.0)
|
13
|
+
diff-lcs (1.1.2)
|
13
14
|
flog (2.5.0)
|
14
15
|
ruby_parser (~> 2.0)
|
15
16
|
sexp_processor (~> 3.0)
|
@@ -18,6 +19,14 @@ GEM
|
|
18
19
|
configuration (>= 0.0.5)
|
19
20
|
rake (>= 0.8.1)
|
20
21
|
rake (0.8.7)
|
22
|
+
rspec (2.5.0)
|
23
|
+
rspec-core (~> 2.5.0)
|
24
|
+
rspec-expectations (~> 2.5.0)
|
25
|
+
rspec-mocks (~> 2.5.0)
|
26
|
+
rspec-core (2.5.1)
|
27
|
+
rspec-expectations (2.5.0)
|
28
|
+
diff-lcs (~> 1.1.2)
|
29
|
+
rspec-mocks (2.5.0)
|
21
30
|
ruby_parser (2.0.6)
|
22
31
|
sexp_processor (~> 3.0)
|
23
32
|
sexp_processor (3.0.5)
|
@@ -29,4 +38,5 @@ DEPENDENCIES
|
|
29
38
|
flog
|
30
39
|
json
|
31
40
|
launchy (~> 0.4.0)
|
41
|
+
rspec
|
32
42
|
turbulence!
|
data/bin/bule
CHANGED
@@ -1,32 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'turbulence'
|
3
|
-
require 'turbulence/scatter_plot_generator'
|
4
|
-
require 'fileutils'
|
5
|
-
require 'launchy'
|
6
|
-
TURBULENCE_PATH = File.join(File.absolute_path(File.dirname(__FILE__)), "..")
|
7
3
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def copy_templates_into(directory)
|
13
|
-
FileUtils.cp path_to_template('turbulence.html'), directory
|
14
|
-
FileUtils.cp path_to_template('highcharts.js'), directory
|
15
|
-
FileUtils.cp path_to_template('jquery.min.js'), directory
|
16
|
-
end
|
17
|
-
|
18
|
-
def generate_bundle_for(git_repo)
|
19
|
-
FileUtils.mkdir_p("turbulence")
|
20
|
-
Dir.chdir("turbulence") do
|
21
|
-
copy_templates_into(Dir.pwd)
|
22
|
-
File.open("cc.js", "w") do |f|
|
23
|
-
f.write Turbulence::ScatterPlotGenerator.from(Turbulence.new(git_repo).metrics)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def open_bundle(directory)
|
29
|
-
Launchy.open("file://#{directory}/turbulence/turbulence.html")
|
30
|
-
end
|
31
|
-
generate_bundle_for Dir.pwd
|
32
|
-
open_bundle Dir.pwd
|
4
|
+
cli = Turbulence::CommandLineInterface.new(Dir.pwd)
|
5
|
+
cli.generate_bundle
|
6
|
+
cli.open_bundle
|
data/lib/turbulence.rb
CHANGED
@@ -1,13 +1,9 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
1
|
+
require 'turbulence/scatter_plot_generator'
|
2
|
+
require 'turbulence/command_line_interface'
|
3
|
+
require 'turbulence/calculators/churn'
|
4
|
+
require 'turbulence/calculators/complexity'
|
3
5
|
|
4
6
|
class Turbulence
|
5
|
-
class Reporter < StringIO
|
6
|
-
def average
|
7
|
-
Float(string.scan(/^\s+([^:]+).*total$/).flatten.first)
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
7
|
attr_reader :dir
|
12
8
|
attr_reader :metrics
|
13
9
|
def initialize(dir)
|
@@ -19,52 +15,34 @@ class Turbulence
|
|
19
15
|
end
|
20
16
|
end
|
21
17
|
|
22
|
-
def
|
18
|
+
def files_of_interest
|
23
19
|
files = ["app/models", "app/controllers", "app/helpers", "lib"].map{|base_dir| "#{base_dir}/**/*\.rb"}
|
24
20
|
@ruby_files ||= Dir[*files]
|
25
21
|
end
|
26
22
|
|
27
|
-
def
|
28
|
-
|
29
|
-
files.each do |count, filename|
|
30
|
-
print "."
|
31
|
-
metrics_for(filename)[:churn] = Integer(count)
|
32
|
-
end
|
23
|
+
def complexity
|
24
|
+
calculate_metrics Turbulence::Calculators::Complexity
|
33
25
|
end
|
34
26
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
print "."
|
27
|
+
def churn
|
28
|
+
calculate_metrics Turbulence::Calculators::Churn
|
29
|
+
end
|
39
30
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
metrics_for(filename)[:complexity] = reporter.average
|
45
|
-
rescue SyntaxError, Racc::ParseError => e
|
46
|
-
puts "\nError flogging: #{filename}\n"
|
47
|
-
end
|
31
|
+
def calculate_metrics(calculator)
|
32
|
+
puts "calculating metric: #{calculator}"
|
33
|
+
calculator.for_these_files(files_of_interest) do |filename, score|
|
34
|
+
set_file_metric(filename, calculator, score)
|
48
35
|
end
|
36
|
+
puts "\n"
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_file_metric(filename, metric, value)
|
40
|
+
print "."
|
41
|
+
metrics_for(filename)[metric] = value
|
49
42
|
end
|
50
43
|
|
51
44
|
def metrics_for(filename)
|
52
45
|
@metrics[filename] ||= {}
|
53
46
|
end
|
54
47
|
|
55
|
-
private
|
56
|
-
def changes_by_ruby_file
|
57
|
-
line_changes_by_file.select do |count, filename|
|
58
|
-
filename =~ /\.rb$/ && File.exist?(filename)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def line_changes_by_file
|
63
|
-
`git log --all -M -C --numstat --format="%n"`.each_line.reject{|line| line =~ /^\n$/}.map do |line|
|
64
|
-
adds, deletes, filename = line.chomp.split(/\t/)
|
65
|
-
[filename, adds.to_i + deletes.to_i]
|
66
|
-
end.group_by(&:first).map do |filename, stats|
|
67
|
-
[stats.map(&:last).tap{|list| list.pop}.inject(0){|n, i| n + i}, filename]
|
68
|
-
end
|
69
|
-
end
|
70
48
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Turbulence
|
2
|
+
module Calculators
|
3
|
+
class Churn
|
4
|
+
class << self
|
5
|
+
def for_these_files(files)
|
6
|
+
changes_by_ruby_file.select do |count, filename|
|
7
|
+
yield filename, count if files.include?(filename)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def changes_by_ruby_file
|
12
|
+
ruby_files_changed_in_git.group_by(&:first).map do |filename, stats|
|
13
|
+
[stats.map(&:last).tap{|list| list.pop}.inject(0){|n, i| n + i}, filename]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def counted_line_changes_by_file_by_commit
|
18
|
+
git_log_file_lines.map do |line|
|
19
|
+
adds, deletes, filename = line.split(/\t/)
|
20
|
+
[filename, adds.to_i + deletes.to_i]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def ruby_files_changed_in_git
|
25
|
+
counted_line_changes_by_file_by_commit.select do |filename, count|
|
26
|
+
filename =~ /\.rb$/ && File.exist?(filename)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def git_log_file_lines
|
31
|
+
git_log_command.each_line.reject{|line| line =~ /^\n$/}.map(&:chomp)
|
32
|
+
end
|
33
|
+
|
34
|
+
def git_log_command
|
35
|
+
`git log --all -M -C --numstat --format="%n"`
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'flog'
|
3
|
+
class Turbulence
|
4
|
+
module Calculators
|
5
|
+
class Complexity
|
6
|
+
class << self
|
7
|
+
def flogger
|
8
|
+
@flogger ||= Flog.new
|
9
|
+
end
|
10
|
+
def for_these_files(files)
|
11
|
+
files.each do |filename|
|
12
|
+
yield filename, score_for_file(filename)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def score_for_file(filename)
|
17
|
+
flogger.flog filename
|
18
|
+
reporter = Reporter.new
|
19
|
+
flogger.report(reporter)
|
20
|
+
reporter.score
|
21
|
+
rescue SyntaxError, Racc::ParseError
|
22
|
+
STDERR.puts "\nError flogging: #{filename}\n"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Reporter < ::StringIO
|
27
|
+
def score
|
28
|
+
Float(string.scan(/^\s+([^:]+).*total$/).flatten.first)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'launchy'
|
3
|
+
|
4
|
+
class Turbulence
|
5
|
+
class CommandLineInterface
|
6
|
+
TURBULENCE_PATH = File.join(File.absolute_path(File.dirname(__FILE__)), "..", "..")
|
7
|
+
|
8
|
+
attr_reader :directory
|
9
|
+
def initialize(directory)
|
10
|
+
@directory = directory
|
11
|
+
end
|
12
|
+
|
13
|
+
def path_to_template(filename)
|
14
|
+
File.join(TURBULENCE_PATH, "template", filename)
|
15
|
+
end
|
16
|
+
|
17
|
+
def copy_templates_into(directory)
|
18
|
+
['turbulence.html', 'highcharts.js', 'jquery.min.js'].each do |filename|
|
19
|
+
FileUtils.cp path_to_template(filename), directory
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_bundle
|
24
|
+
FileUtils.mkdir_p("turbulence")
|
25
|
+
Dir.chdir("turbulence") do
|
26
|
+
copy_templates_into(Dir.pwd)
|
27
|
+
File.open("cc.js", "w") do |f|
|
28
|
+
f.write Turbulence::ScatterPlotGenerator.from(Turbulence.new(directory).metrics).to_js
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def open_bundle
|
34
|
+
Launchy.open("file://#{directory}/turbulence/turbulence.html")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -2,14 +2,14 @@ require 'json'
|
|
2
2
|
class Turbulence
|
3
3
|
class ScatterPlotGenerator
|
4
4
|
def self.from(metrics_hash)
|
5
|
+
new(metrics_hash)
|
6
|
+
end
|
7
|
+
attr_reader :metrics_hash
|
8
|
+
def initialize(metrics_hash)
|
9
|
+
@metrics_hash = metrics_hash
|
10
|
+
end
|
5
11
|
|
6
|
-
|
7
|
-
{:filename => filename, :x => metrics[:churn], :y => metrics[:complexity]}
|
8
|
-
end.reject do |metrics|
|
9
|
-
metrics[:x].nil? || metrics[:y].nil?
|
10
|
-
end.to_json
|
11
|
-
series = ["var turbulenceGraphData = #{data_in_json_format};"]
|
12
|
-
|
12
|
+
def to_js
|
13
13
|
grouped_by_directory = metrics_hash.group_by do |filename, _|
|
14
14
|
directories = File.dirname(filename).split("/")
|
15
15
|
directories[0..1].join("/")
|
@@ -18,16 +18,14 @@ class Turbulence
|
|
18
18
|
directory_series = {}
|
19
19
|
grouped_by_directory.each_pair do |directory, metrics_hash|
|
20
20
|
data_in_json_format = metrics_hash.map do |filename, metrics|
|
21
|
-
{:filename => filename, :x => metrics[
|
21
|
+
{:filename => filename, :x => metrics[Turbulence::Calculators::Churn], :y => metrics[Turbulence::Calculators::Complexity]}
|
22
22
|
end.reject do |metrics|
|
23
|
-
|
23
|
+
metrics[:x].nil? || metrics[:y].nil?
|
24
24
|
end
|
25
25
|
directory_series[directory] = data_in_json_format
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
series.join("\n")
|
28
|
+
"var directorySeries = #{directory_series.to_json};"
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
data/lib/turbulence/version.rb
CHANGED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'turbulence/calculators/churn'
|
2
|
+
|
3
|
+
describe Turbulence::Calculators::Churn do
|
4
|
+
let(:calculator) { Turbulence::Calculators::Churn }
|
5
|
+
before do
|
6
|
+
calculator.stub(:git_log_command) { "" }
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "::for_these_files" do
|
10
|
+
it "yields up the filename and score for each file" do
|
11
|
+
files = ["lib/corey.rb", "lib/chad.rb"]
|
12
|
+
calculator.stub(:changes_by_ruby_file) {
|
13
|
+
[
|
14
|
+
[5, "lib/corey.rb"],
|
15
|
+
[10, "lib/chad.rb"]
|
16
|
+
]
|
17
|
+
}
|
18
|
+
yielded_files = []
|
19
|
+
calculator.for_these_files(files) do |filename, score|
|
20
|
+
yielded_files << [filename, score]
|
21
|
+
end
|
22
|
+
yielded_files.should =~ [["lib/corey.rb", 5],
|
23
|
+
["lib/chad.rb",10]]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "filters the results by the passed-in files" do
|
27
|
+
files = ["lib/corey.rb"]
|
28
|
+
calculator.stub(:changes_by_ruby_file) {
|
29
|
+
[
|
30
|
+
[5, "lib/corey.rb"],
|
31
|
+
[10, "lib/chad.rb"]
|
32
|
+
]
|
33
|
+
}
|
34
|
+
yielded_files = []
|
35
|
+
calculator.for_these_files(files) do |filename, score|
|
36
|
+
yielded_files << [filename, score]
|
37
|
+
end
|
38
|
+
yielded_files.should =~ [["lib/corey.rb", 5]]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "::git_log_file_lines" do
|
43
|
+
it "returns just the file lines" do
|
44
|
+
calculator.stub(:git_log_command) do
|
45
|
+
"\n\n\n\n10\t6\tlib/turbulence.rb\n\n\n\n17\t2\tlib/eddies.rb\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
calculator.git_log_file_lines.should =~ [
|
49
|
+
"10\t6\tlib/turbulence.rb",
|
50
|
+
"17\t2\tlib/eddies.rb"
|
51
|
+
]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "::counted_line_changes_by_file_by_commit" do
|
56
|
+
before do
|
57
|
+
calculator.stub(:git_log_file_lines) {
|
58
|
+
[
|
59
|
+
"10\t6\tlib/turbulence.rb",
|
60
|
+
"17\t2\tlib/eddies.rb"
|
61
|
+
]
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
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]]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "Full stack tests" do
|
71
|
+
context "when one ruby file is given" do
|
72
|
+
context "with two log entries for file" do
|
73
|
+
before do
|
74
|
+
calculator.stub(:git_log_command) do
|
75
|
+
"\n\n\n\n10\t6\tlib/turbulence.rb\n" +
|
76
|
+
"\n\n\n\n11\t7\tlib/turbulence.rb\n"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
it "gives the line change count for the file" do
|
80
|
+
yielded_files = []
|
81
|
+
calculator.for_these_files(["lib/turbulence.rb"]) do |filename, score|
|
82
|
+
yielded_files << [filename, score]
|
83
|
+
end
|
84
|
+
yielded_files.should =~ [["lib/turbulence.rb", 16]]
|
85
|
+
end
|
86
|
+
context "with only one log entry for file" do
|
87
|
+
before do
|
88
|
+
calculator.stub(:git_log_command) do
|
89
|
+
"\n\n\n\n10\t6\tlib/turbulence.rb\n"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
it "shows zero churn for the file" do
|
93
|
+
yielded_files = []
|
94
|
+
calculator.for_these_files(["lib/turbulence.rb"]) do |filename, score|
|
95
|
+
yielded_files << [filename, score]
|
96
|
+
end
|
97
|
+
yielded_files.should =~ [["lib/turbulence.rb", 0]]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'turbulence/calculators/complexity'
|
2
|
+
|
3
|
+
describe Turbulence::Calculators::Complexity do
|
4
|
+
let(:calculator) { Turbulence::Calculators::Complexity }
|
5
|
+
describe "::for_these_files" do
|
6
|
+
it "yields up the filename and score for each file" do
|
7
|
+
files = ["lib/corey.rb", "lib/chad.rb"]
|
8
|
+
calculator.stub(:score_for_file) { |filename|
|
9
|
+
filename.size
|
10
|
+
}
|
11
|
+
yielded_files = []
|
12
|
+
calculator.for_these_files(files) do |filename, score|
|
13
|
+
yielded_files << [filename, score]
|
14
|
+
end
|
15
|
+
yielded_files.should =~ [["lib/corey.rb", 12],
|
16
|
+
["lib/chad.rb",11]]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
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
|
data/template/turbulence.html
CHANGED
@@ -22,18 +22,19 @@ $(document).ready(function() {
|
|
22
22
|
enabled: true,
|
23
23
|
text: 'Churn'
|
24
24
|
},
|
25
|
-
startOnTick:
|
26
|
-
endOnTick:
|
27
|
-
|
28
|
-
|
25
|
+
startOnTick: false,
|
26
|
+
endOnTick: false,
|
27
|
+
labels: {enabled:false},
|
28
|
+
lineWidth: 0,
|
29
|
+
tickWidth: 0
|
29
30
|
},
|
30
31
|
yAxis: {
|
31
32
|
title: {
|
32
33
|
text: 'Complexity'
|
33
34
|
},
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
startOnTick: false,
|
36
|
+
endOnTick: false,
|
37
|
+
labels: {enabled:false}
|
37
38
|
},
|
38
39
|
tooltip: {
|
39
40
|
formatter: function() {
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 4
|
9
|
+
version: 0.0.4
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Chad Fowler
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2011-02-
|
19
|
+
date: 2011-02-28 00:00:00 -07:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
@@ -84,8 +84,13 @@ files:
|
|
84
84
|
- Rakefile
|
85
85
|
- bin/bule
|
86
86
|
- lib/turbulence.rb
|
87
|
+
- lib/turbulence/calculators/churn.rb
|
88
|
+
- lib/turbulence/calculators/complexity.rb
|
89
|
+
- lib/turbulence/command_line_interface.rb
|
87
90
|
- lib/turbulence/scatter_plot_generator.rb
|
88
91
|
- lib/turbulence/version.rb
|
92
|
+
- spec/turbulence/calculators/churn_spec.rb
|
93
|
+
- spec/turbulence/calculators/complexity_spec.rb
|
89
94
|
- template/highchart_template.js.erb
|
90
95
|
- template/highcharts.js
|
91
96
|
- template/jquery.min.js
|
@@ -123,5 +128,6 @@ rubygems_version: 1.3.7
|
|
123
128
|
signing_key:
|
124
129
|
specification_version: 3
|
125
130
|
summary: Automates churn + flog scoring on a git repo for a Ruby project
|
126
|
-
test_files:
|
127
|
-
|
131
|
+
test_files:
|
132
|
+
- spec/turbulence/calculators/churn_spec.rb
|
133
|
+
- spec/turbulence/calculators/complexity_spec.rb
|