debt_ceiling 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb989ae976353da509970f25093e1ae303ec8b6f
4
- data.tar.gz: a473a4ba56137d62578d03522e1519f11ab4d982
3
+ metadata.gz: 1121dd0d368a02bfad9d6f2fd21a15707c8089d8
4
+ data.tar.gz: 7dca6e9d0b291318e5e67b99a5aa96245a416c7e
5
5
  SHA512:
6
- metadata.gz: 1d4243bb0c4569c7d42d37de59102d36dc29211647f087295fe4b5807f5493c416203f54734a295a744b8b0adc65672263dedabaa2f38594ea4f67e214e6f288
7
- data.tar.gz: a4820d63a6f400e833c7015d85751aa539a58ae2c89d979d087b17eb350f397b1c2382cba9105cd5c8297b1d4b7adcb02d1e9929127c9038dad905c77cf04e74
6
+ metadata.gz: bf8aee5e06f3ec57e6215bd2e3a8d88471f2185b053cf4acb76a82ed175c6b5163f869f02dd6b30766f81cf68483fb01082dafc1ae91b9594139496c0bc34339
7
+ data.tar.gz: a3b9a6ae68acf71df05fca377df940dc5a3b85c1ed4dcaf188a681d3aeff177582c262ad3b6b6429fdf133f99f5c26ea0be16e8215196bbfe65b407188e5729d
data/README.md CHANGED
@@ -1,8 +1,76 @@
1
+ [![Debt Ceiling Chat](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/bglusman/debt_ceiling)
2
+
1
3
  #DebtCeiling
2
4
 
3
- ###
4
- Work in progress, trying to use some automatic heuristic plus manual mechanisms to help visibility and tracking of technical debt.
5
5
 
6
- Current plan is to configure/customize the weight given to heuristic grade
7
- based first on a simple DSL in a .debt_ceiling file in the project's home directory, and if additional customization is desired, pass a path to
8
- `extension_file_path` command in the DSL file to a file defining DebtCeiling::Debt like the one in examples directory, and replace/augment it's methods with your own additional calculation per file.
6
+ ### Work in progress, trying to use some automatic heuristic plus manual mechanisms to help visibility and tracking of technical debt.
7
+
8
+ Current features include:
9
+ * configuring points per [RubyCritic](https://github.com/whitesmith/rubycritic) grade per file line
10
+ * Whitelisting/blacklisting files by filename
11
+ * Modifying or replacing default calculation on a per file basis
12
+ * Reporting the single greatest source of debt based on your definitions
13
+ * Reporting total debt for the git repo based on your definitions
14
+ * Running the binary from a test suite to fail if debt ceiling is exceeded
15
+ * Running the binary from a test suite to fail if debt deadline is missed
16
+
17
+ These features are largely are demonstrated/discussed in [examples/.debt_ceiling](https://github.com/bglusman/debt_ceiling/blob/master/examples/.debt_ceiling.example) which demonstrates configuring debt ceiling
18
+
19
+ Additional customization is supported via two method hooks in the debt class, which debt_ceiling will load from a provided extension_file_path in the main config file, which should look like the [example file](https://github.com/bglusman/debt_ceiling/blob/master/examples/debt.rb.example)
20
+
21
+ You can configure/customize the debt calculated using a few simple commands in a .debt_ceiling file in the project's home directory
22
+
23
+ ```
24
+ set_debt_ceiling 500
25
+ #exceeding this will fail a test, if you run debt_ceiling binary from test suite
26
+ debt_reduction_target_and_date 100, 'Jan 1 2015'
27
+ #exceeding this will fail after the target date (parsed by Chronic)
28
+
29
+ #set the multipliers per line of code in a file with each letter grade
30
+ b_cost_per_line 10
31
+ c_cost_per_line 20
32
+ d_cost_per_line 40
33
+ f_cost_per_line 100
34
+
35
+ #load custom debt calculations (see examples/debt.rb) from this path
36
+ extension_file_path "./debt.rb"
37
+
38
+ #only count debt scores for files matching these strings (converted to regexes)
39
+ whitelist_matching %w(app lib)
40
+
41
+ #or.... exclude debt scores for files matching these strings (obviously mutually exclusive, raises error if both present)
42
+ #blacklist_matching %w(schema.rb routes.rb)
43
+ ```
44
+
45
+ As mentioned/linked above, additional customization is supported.
46
+
47
+ As shown in example file, pass a path to `extension_file_path` command pointing to a file defining DebtCeiling::Debt like the one in examples directory, and define its methods for your own additional calculation per file.
48
+
49
+ Right now it lacks all tests... feel free to open a PR!
50
+
51
+ I'll try and add test coverage where it makes sense as API matures.
52
+
53
+ ## TODO: planned features/ideas
54
+
55
+ todo_cost 500 #cost per comment matching /TODO/
56
+
57
+ debt_per_reference_to deprecated_regex, 500 # help transition away from deprecated APIs
58
+
59
+ points_per_regex_match REGEX, 500 # seems like an alias for above maybe, nix?
60
+
61
+
62
+ ### Other ideas:
63
+
64
+ rubocop/cane integration debt for style violations
65
+
66
+ every line over x ideal file size is y points of debt
67
+
68
+ multipliers for important files
69
+
70
+ include one of the JS complexity/debt analysis libraries below, or another if anyone had another suggestion:
71
+
72
+ * https://github.com/es-analysis/plato
73
+
74
+ * https://github.com/dpnishant/jsprime
75
+
76
+ * https://github.com/mozilla/doctorjs
data/bin/debt_ceiling CHANGED
@@ -21,4 +21,4 @@ end
21
21
  extension_path = DebtCeiling.current_extension_file_path
22
22
  load extension_path if File.exists?(extension_path)
23
23
 
24
- DebtCeiling.enforce(ARGV[0] ? ARGV[0] : ".")
24
+ DebtCeiling.calculate(ARGV[0] ? ARGV[0] : ".")
data/debt_ceiling.gemspec CHANGED
@@ -3,23 +3,19 @@ lib = File.expand_path('../lib/', __FILE__)
3
3
  $:.unshift lib unless $:.include?(lib)
4
4
  require "debt_ceiling/version"
5
5
 
6
- # module ::Gem
7
- # post_install {|gem_installer| puts 'in hook'; DebtCeiling.post_install(gem_installer) }
8
- # end
9
-
10
6
  Gem::Specification.new do |s|
11
7
  s.name = "debt_ceiling"
12
8
  s.version = DebtCeiling::VERSION
13
9
  s.platform = Gem::Platform::RUBY
14
10
  s.authors = ["Brian Glusman"]
15
11
  s.email = ["bglusman@shutterstock.com"]
16
- s.homepage = "https://github.com/bglusman/DebtCeiling"
12
+ s.homepage = "https://github.com/bglusman/debt_ceiling"
17
13
  s.summary = "DebtCeiling helps you track Tech Debt"
18
14
  s.rubyforge_project = "debt_ceiling"
19
15
  s.extensions = ["Rakefile"]
20
16
 
21
17
  s.description = <<-DESC
22
- Quantify and add visibility/history to (some half-baked standin for) Technical Debt
18
+ Get a grip on your technical debt
23
19
  DESC
24
20
 
25
21
 
@@ -29,7 +25,8 @@ Gem::Specification.new do |s|
29
25
  s.require_paths = ["lib"]
30
26
 
31
27
  s.add_runtime_dependency "rubycritic", "~> 1.1.1"
32
- s.add_runtime_dependency "pry"
33
- s.add_development_dependency "rake"
34
- s.add_development_dependency "minitest"
28
+ s.add_runtime_dependency "chronic", "~> 0.10"
29
+ s.add_development_dependency "pry", '~> 0.10'
30
+ s.add_development_dependency "rake", '~> 10.3'
31
+ s.add_development_dependency "minitest", '~> 5.4'
35
32
  end
@@ -1,36 +1,19 @@
1
- #TODO example gradings
2
- # debt_ceiling 15000
1
+ set_debt_ceiling 500
2
+ #exceeding this will fail a test, if you run debt_ceiling binary from test suite
3
+ debt_reduction_target_and_date 100, 'Jan 1 2015'
4
+ #exceeding this will fail after the target date (parsed by Chronic)
3
5
 
6
+ #set the multipliers per line of code in a file with each letter grade
4
7
  b_cost_per_line 10
5
8
  c_cost_per_line 20
6
9
  d_cost_per_line 40
7
10
  f_cost_per_line 100
8
- # todo_cost 500
9
11
 
10
- # debt_per_reference_to blah
12
+ #load custom debt calculations (see examples/debt.rb) from this path
13
+ extension_file_path "./debt.rb"
11
14
 
12
- # points_per_regex_match REGEX, points
13
-
14
- #rubocop/cane integration debt for style violations
15
-
16
- #every line over x ideal file size is y points of debt
17
-
18
- #whitelist/blacklist API: TODO
19
-
20
- #multipliers for important files
21
-
22
- #ditch/augment module eval and use below or something
23
-
24
- # class DebtRule
25
- # def measure_file(filename, metrics)
26
- # cost = 0
27
-
28
- # cost += 0 if metrics.rubycritic.grade == :a
29
- # cost += 10 if metrics.rubycritic.grade == :b
30
-
31
- # churn = measure_churn(filename)
32
- # cost += whatever if churn > something
33
- # end
34
-
35
- # def measure_churn...
36
- # end
15
+ #only count debt scores for files matching these strings (converted to regexes)
16
+ whitelist_matching %w(app lib)
17
+ #or
18
+ #exclude debt scores for files matching these strings (commented as mutually exclusive)
19
+ #blacklist_matching %w(schema.rb routes.rb)
@@ -10,10 +10,11 @@ module DebtCeiling
10
10
  end
11
11
 
12
12
  #OR do your own calcuation first, and add defaults on top of that
13
- def augment_custom_debt
13
+ def augment_debt
14
14
  #output additional cost beyond defaults based on #file_attributes
15
15
  end
16
16
 
17
- #augment_custom_debt is only called if measure_debt is undefined or returns nil/false
17
+ #augment_debt is only called if measure_debt is undefined or returns nil/false
18
+ #for this instance of debt (which is 1:1 mapping of Rubycritic::AnalysedModule at the moment)
18
19
  end
19
20
  end
@@ -1,31 +1,54 @@
1
1
  module DebtCeiling
2
2
  class Debt
3
- attr_reader :file_attributes
3
+ DoNotWhitelistAndBlacklistSimulateneously = Class.new(StandardError)
4
+
5
+ attr_reader :file_attributes, :path, :analysed_module, :module_name, :linecount
4
6
  attr_accessor :debt_amount
5
7
  def initialize(file_attributes)
6
- @file_attributes = file_attributes
7
- default_measure_debt
8
+ @file_attributes = file_attributes
9
+ @path = file_attributes.path
10
+ @analysed_module = file_attributes.analysed_module
11
+ @module_name = file_attributes.name
12
+ @linecount = file_attributes.linecount
13
+ default_measure_debt if valid_debt?
8
14
  end
9
15
 
10
16
  def default_measure_debt
11
- if self.respond_to?(:measure_file)
17
+ if self.respond_to?(:measure_debt)
12
18
  cost = self.public_send(:measure_debt)
13
19
  end
14
20
  if !cost
15
- cost = if self.respond_to?(:augment_custom_debt)
16
- self.public_send(:augment_custom_debt) || 0
21
+ cost = if self.respond_to?(:augment_debt)
22
+ self.public_send(:augment_debt) || 0
17
23
  else
18
24
  0
19
25
  end
20
- letter_grade = file_attributes.analyzed_module.rating.to_s.downcase
26
+ letter_grade = file_attributes.analysed_module.rating.to_s.downcase
21
27
  cost_per_line = DebtCeiling.public_send("#{letter_grade}_current_cost_per_line")
22
28
  cost += file_attributes.linecount * cost_per_line
23
29
  end
24
30
  self.debt_amount = cost
25
31
  end
26
32
 
33
+ def valid_debt?
34
+ black_empty = DebtCeiling.blacklist.empty?
35
+ white_empty = DebtCeiling.whitelist.empty?
36
+ raise DoNotWhitelistAndBlacklistSimulateneously if (!black_empty && !white_empty)
37
+ (black_empty && white_empty) ||
38
+ (black_empty && self.class.whitelist_includes?(self)) ||
39
+ (white_empty && !self.class.blacklist_includes?(self))
40
+ end
41
+
42
+ def self.whitelist_includes?(debt)
43
+ DebtCeiling.whitelist.detect {|filename| filename.match debt.path }
44
+ end
45
+
46
+ def self.blacklist_includes?(debt)
47
+ DebtCeiling.blacklist.detect {|filename| filename.match debt.path }
48
+ end
49
+
27
50
  def to_i
28
- debt_amount
51
+ debt_amount.to_i
29
52
  end
30
53
 
31
54
  def +(other)
@@ -1,3 +1,3 @@
1
1
  module DebtCeiling
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/debt_ceiling.rb CHANGED
@@ -1,35 +1,16 @@
1
- require 'ripper'
2
- require_relative 'debt_ceiling/enforcement'
1
+ require_relative 'debt_ceiling/accounting'
3
2
  require_relative 'debt_ceiling/debt'
4
-
5
-
3
+ require 'chronic'
6
4
 
7
5
  module DebtCeiling
8
6
  extend self
9
7
 
10
- def debt_ceiling(pattern, message=nil, options={})
11
- if pattern.kind_of?(String)
12
- rule = Rule.new(pattern, message, options)
13
- DebtCeiling::Enforcement.add(rule)
14
- else
15
- DebtCeiling::Enforcement.add(self.send(pattern))
16
- end
17
- end
18
-
19
- def enforce(dir=".")
20
- DebtCeiling::Enforcement.process_directory(dir, :stdout)
8
+ def calculate(dir=".")
9
+ @debt = DebtCeiling::Accounting.calculate(dir)
10
+ evaluate
21
11
  end
22
12
 
23
- def validate_files(files)
24
- files.reduce(error: false, output:"") do |results, file|
25
- error, output = DebtCeiling::Enforcement.process_file(file)
26
- results[:error] ||= error
27
- results[:output] += output
28
- results
29
- end
30
- end
31
-
32
- @extension_file_path = "#{Dir.pwd}/debt_rule.rb"
13
+ @extension_file_path = "#{Dir.pwd}/debt.rb"
33
14
  def extension_file_path(path)
34
15
  @extension_file_path = path
35
16
  end
@@ -38,12 +19,43 @@ module DebtCeiling
38
19
  @extension_file_path
39
20
  end
40
21
 
41
- GRADES = [:a, :b, :c, :d, :f]
42
- GRADE_DEFAULTS = [0, 10, 20, 40, 100]
43
- GRADE_MAP = GRADES.zip(0..4).to_h
44
- GRADES.each do |grade|
45
- default_index = GRADE_MAP[grade]
46
- instance_variable_set "@#{grade}_cost_per_line", GRADE_DEFAULTS[default_index]
22
+ def blacklist_matching(matchers)
23
+ @blacklist = matchers.map {|matcher| Regexp.new(matcher)}
24
+ end
25
+
26
+ def whitelist_matching(matchers)
27
+ @whitelist = matchers.map {|matcher| Regexp.new(matcher)}
28
+ end
29
+
30
+ def set_debt_ceiling(value)
31
+ @ceiling_amount = value
32
+ end
33
+
34
+ def debt_reduction_target_and_date(target_value, date_to_parse)
35
+ @reduction_target = target_value
36
+ @reduction_date = Chronic.parse(date_to_parse)
37
+ end
38
+
39
+ def evaluate
40
+ if ceiling_amount && ceiling_amount <= debt
41
+ fail_test
42
+ elsif reduction_target && reduction_target <= debt &&
43
+ Time.now > reduction_date
44
+ fail_test
45
+ end
46
+ end
47
+
48
+ def fail_test
49
+ Kernel.exit 1
50
+ end
51
+
52
+ attr_reader :blacklist, :whitelist, :ceiling_amount, :reduction_date, :reduction_target, :debt
53
+ @blacklist = []
54
+ @whitelist = []
55
+
56
+ GRADE_MAP = {a: 0, b: 10, c: 20, d: 40, f: 100} #arbitrary default grades for now
57
+ GRADE_MAP.keys.each do |grade|
58
+ instance_variable_set "@#{grade}_cost_per_line", GRADE_MAP[grade]
47
59
  define_method("#{grade}_current_cost_per_line") do
48
60
  instance_variable_get "@#{grade}_cost_per_line"
49
61
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: debt_ceiling
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Glusman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-12 00:00:00.000000000 Z
11
+ date: 2014-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubycritic
@@ -25,49 +25,63 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.1.1
27
27
  - !ruby/object:Gem::Dependency
28
- name: pry
28
+ name: chronic
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '0.10'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '0.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - ">="
59
+ - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '0'
61
+ version: '10.3'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - ">="
66
+ - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '0'
68
+ version: '10.3'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: minitest
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - ">="
73
+ - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '0'
75
+ version: '5.4'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - ">="
80
+ - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '0'
82
+ version: '5.4'
69
83
  description: |2
70
- Quantify and add visibility/history to (some half-baked standin for) Technical Debt
84
+ Get a grip on your technical debt
71
85
  email:
72
86
  - bglusman@shutterstock.com
73
87
  executables:
@@ -87,11 +101,10 @@ files:
87
101
  - examples/debt.rb.example
88
102
  - lib/debt_ceiling.rb
89
103
  - lib/debt_ceiling/debt.rb
90
- - lib/debt_ceiling/enforcement.rb
91
104
  - lib/debt_ceiling/version.rb
92
105
  - test/debt_ceiling_test.rb
93
106
  - test/test_helper.rb
94
- homepage: https://github.com/bglusman/DebtCeiling
107
+ homepage: https://github.com/bglusman/debt_ceiling
95
108
  licenses: []
96
109
  metadata: {}
97
110
  post_install_message:
@@ -1,25 +0,0 @@
1
- require 'rubycritic'
2
- require 'ostruct'
3
- module DebtCeiling
4
- class Enforcement
5
- class << self
6
- attr_reader :rules
7
- def add(rule)
8
- @rules ||= []
9
- @rules << rule
10
- end
11
-
12
- def process_directory(path, output=:destructured)
13
- modules = Rubycritic::Orchestrator.new.critique([path])
14
- debts = modules.map do |mod|
15
- file_attributes = OpenStruct.new
16
- file_attributes.linecount = `wc -l #{mod.path}`.match(/\d+/)[0].to_i
17
- file_attributes.path = mod.path
18
- file_attributes.analyzed_module = mod
19
- debt_rule = Debt.new(file_attributes)
20
- end
21
- puts "Current total tech debt: #{debts.map(&:to_i).reduce(&:+)}"
22
- end
23
- end
24
- end
25
- end