knapsack 0.0.3 → 0.1.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +12 -1
  3. data/CHANGELOG.md +11 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +71 -24
  6. data/Rakefile +3 -1
  7. data/TODO.md +5 -0
  8. data/knapsack_report.json +9 -12
  9. data/lib/knapsack.rb +25 -6
  10. data/lib/knapsack/adapters/{base.rb → base_adapter.rb} +18 -8
  11. data/lib/knapsack/adapters/{rspec.rb → rspec_adapter.rb} +13 -6
  12. data/lib/knapsack/allocator.rb +65 -0
  13. data/lib/knapsack/distributors/base_distributor.rb +67 -0
  14. data/lib/knapsack/distributors/leftover_distributor.rb +49 -0
  15. data/lib/knapsack/distributors/report_distributor.rb +76 -0
  16. data/lib/knapsack/presenter.rb +38 -5
  17. data/lib/knapsack/report.rb +27 -12
  18. data/lib/knapsack/task_loader.rb +11 -0
  19. data/lib/knapsack/tracker.rb +30 -13
  20. data/lib/knapsack/version.rb +1 -1
  21. data/lib/tasks/knapsack.rake +21 -0
  22. data/spec/knapsack/adapters/base_adapter_spec.rb +91 -0
  23. data/spec/knapsack/adapters/rspec_adapter_spec.rb +29 -0
  24. data/spec/knapsack/allocator_spec.rb +57 -0
  25. data/spec/knapsack/distributors/base_distributor_spec.rb +85 -0
  26. data/spec/knapsack/distributors/leftover_distributor_spec.rb +110 -0
  27. data/spec/knapsack/distributors/report_distributor_spec.rb +152 -0
  28. data/spec/knapsack/presenter_spec.rb +83 -0
  29. data/spec/knapsack/report_spec.rb +73 -0
  30. data/spec/knapsack/task_loader_spec.rb +10 -0
  31. data/spec/knapsack/tracker_spec.rb +84 -20
  32. data/spec/knapsack_spec.rb +23 -2
  33. data/spec/spec_helper.rb +16 -0
  34. data/spec_examples/fast/1_spec.rb +1 -4
  35. data/spec_examples/fast/2_spec.rb +1 -4
  36. data/spec_examples/fast/3_spec.rb +1 -4
  37. data/spec_examples/fast/4_spec.rb +1 -4
  38. data/spec_examples/fast/5_spec.rb +1 -4
  39. data/spec_examples/fast/6_spec.rb +1 -4
  40. data/spec_examples/leftover/1_spec.rb +4 -0
  41. data/spec_examples/leftover/a_spec.rb +8 -0
  42. data/spec_examples/slow/a_spec.rb +1 -3
  43. data/spec_examples/slow/b_spec.rb +3 -5
  44. data/spec_examples/slow/c_spec.rb +1 -3
  45. data/spec_examples/spec_helper.rb +5 -2
  46. metadata +32 -7
  47. data/spec_examples/slow/d_spec.rb +0 -7
  48. data/spec_examples/slow/e_spec.rb +0 -7
  49. data/spec_examples/slow/f_spec.rb +0 -7
@@ -0,0 +1,49 @@
1
+ module Knapsack
2
+ module Distributors
3
+ class LeftoverDistributor < BaseDistributor
4
+ attr_reader :spec_pattern
5
+
6
+ def report_specs
7
+ @report_specs ||= @report.keys
8
+ end
9
+
10
+ def all_specs
11
+ @all_specs ||= Dir[spec_pattern]
12
+ end
13
+
14
+ def leftover_specs
15
+ @leftover_specs ||= all_specs - report_specs
16
+ end
17
+
18
+ private
19
+
20
+ def post_initialize(args={})
21
+ @spec_pattern = args[:spec_pattern] || default_spec_pattern
22
+ end
23
+
24
+ def default_spec_pattern
25
+ 'spec/**/*_spec.rb'
26
+ end
27
+
28
+ def post_assign_spec_files_to_node
29
+ leftover_specs.each do |spec_file|
30
+ node_specs[@node_index] << spec_file
31
+ update_node_index
32
+ end
33
+ end
34
+
35
+ def post_specs_for_node(node_index)
36
+ spec_files = node_specs[node_index]
37
+ return unless spec_files
38
+ spec_files
39
+ end
40
+
41
+ def default_node_specs
42
+ @node_specs = []
43
+ ci_node_total.times do |index|
44
+ @node_specs[index] = []
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ module Knapsack
2
+ module Distributors
3
+ class ReportDistributor < BaseDistributor
4
+ def sorted_report
5
+ @sorted_report ||= report.sort_by{|k,v| v}.reverse
6
+ end
7
+
8
+ def total_time_execution
9
+ @total_time_execution ||= report.values.reduce(0, :+).to_f
10
+ end
11
+
12
+ def node_time_execution
13
+ @node_time_execution ||= total_time_execution / ci_node_total
14
+ end
15
+
16
+ private
17
+
18
+ def post_assign_spec_files_to_node
19
+ assign_slow_spec_files
20
+ assign_remaining_spec_files
21
+ end
22
+
23
+ def post_specs_for_node(node_index)
24
+ node_spec = node_specs[node_index]
25
+ return unless node_spec
26
+ node_spec[:spec_files_with_time].map(&:first)
27
+ end
28
+
29
+ def default_node_specs
30
+ @node_specs = []
31
+ ci_node_total.times do |index|
32
+ @node_specs << {
33
+ node_index: index,
34
+ time_left: node_time_execution,
35
+ spec_files_with_time: []
36
+ }
37
+ end
38
+ end
39
+
40
+ def assign_slow_spec_files
41
+ @not_assigned_spec_files = []
42
+ @node_index = 0
43
+ sorted_report.each do |spec_file_with_time|
44
+ assign_slow_spec_file(spec_file_with_time)
45
+ update_node_index
46
+ end
47
+ end
48
+
49
+ def assign_slow_spec_file(spec_file_with_time)
50
+ time = spec_file_with_time[1]
51
+ time_left = node_specs[@node_index][:time_left] - time
52
+
53
+ if time_left >= 0 or node_specs[@node_index][:spec_files_with_time].empty?
54
+ node_specs[@node_index][:time_left] -= time
55
+ node_specs[@node_index][:spec_files_with_time] << spec_file_with_time
56
+ else
57
+ @not_assigned_spec_files << spec_file_with_time
58
+ end
59
+ end
60
+
61
+ def assign_remaining_spec_files
62
+ @not_assigned_spec_files.each do |spec_file_with_time|
63
+ index = node_with_max_time_left
64
+ time = spec_file_with_time[1]
65
+ node_specs[index][:time_left] -= time
66
+ node_specs[index][:spec_files_with_time] << spec_file_with_time
67
+ end
68
+ end
69
+
70
+ def node_with_max_time_left
71
+ node_spec = node_specs.max { |a,b| a[:time_left] <=> b[:time_left] }
72
+ node_spec[:node_index]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -5,19 +5,52 @@ module Knapsack
5
5
  class Presenter
6
6
  class << self
7
7
  def report_yml
8
- Knapsack.tracker.files.to_yaml
8
+ Knapsack.tracker.spec_files_with_time.to_yaml
9
9
  end
10
10
 
11
11
  def report_json
12
- JSON.pretty_generate(Knapsack.tracker.files)
12
+ JSON.pretty_generate(Knapsack.tracker.spec_files_with_time)
13
+ end
14
+
15
+ def report_details
16
+ "Knapsack report was generated. Preview:\n" + Presenter.report_json
13
17
  end
14
18
 
15
19
  def global_time
16
- "Knapsack global time execution for specs: #{Knapsack.tracker.global_time}s"
20
+ "\nKnapsack global time execution for specs: #{Knapsack.tracker.global_time}s"
17
21
  end
18
22
 
19
- def report_details
20
- "Knapsack report was generated. Preview:\n" + Presenter.report_json
23
+ def time_offset
24
+ "Time offset: #{Knapsack.tracker.config[:time_offset_in_seconds]}s"
25
+ end
26
+
27
+ def max_allowed_node_time_execution
28
+ "Max allowed node time execution: #{Knapsack.tracker.max_node_time_execution}s"
29
+ end
30
+
31
+ def exceeded_time
32
+ "Exceeded time: #{Knapsack.tracker.exceeded_time}s"
33
+ end
34
+
35
+ def time_offset_warning
36
+ str = %{\n========= Knapsack Time Offset Warning ==========
37
+ #{Presenter.time_offset}
38
+ #{Presenter.max_allowed_node_time_execution}
39
+ #{Presenter.exceeded_time}
40
+ }
41
+ if Knapsack.tracker.time_exceeded?
42
+ str << %{
43
+ Specs on this CI node took more than time offset.
44
+ Please regenerate your knapsack report.
45
+ If that didn't help then split your heavy test file
46
+ or bump time_offset_in_seconds setting.}
47
+ else
48
+ str << %{
49
+ Global time execution for this CI node is fine.
50
+ Happy testing!}
51
+ end
52
+ str << "\n=================================================\n"
53
+ str
21
54
  end
22
55
  end
23
56
  end
@@ -1,20 +1,35 @@
1
1
  module Knapsack
2
2
  class Report
3
- REPORT_PATH = 'knapsack_report.json'
3
+ include Singleton
4
4
 
5
- class << self
6
- def save
7
- File.open(REPORT_PATH, 'w+') do |f|
8
- f.write(Presenter.report_json)
9
- end
10
- end
5
+ def config(opts={})
6
+ @config ||= default_config
7
+ @config.merge!(opts)
8
+ end
11
9
 
12
- def open
13
- report = File.read(REPORT_PATH)
14
- JSON.parse(report)
15
- rescue Errno::ENOENT
16
- raise "Knapsack report file doesn't exist. Please generate report first!"
10
+ def save
11
+ File.open(config[:report_path], 'w+') do |f|
12
+ f.write(report_json)
17
13
  end
18
14
  end
15
+
16
+ def open
17
+ report = File.read(config[:report_path])
18
+ JSON.parse(report)
19
+ rescue Errno::ENOENT
20
+ raise "Knapsack report file doesn't exist. Please generate report first!"
21
+ end
22
+
23
+ private
24
+
25
+ def default_config
26
+ {
27
+ report_path: 'knapsack_report.json'
28
+ }
29
+ end
30
+
31
+ def report_json
32
+ Presenter.report_json
33
+ end
19
34
  end
20
35
  end
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+
3
+ module Knapsack
4
+ class TaskLoader
5
+ include ::Rake::DSL
6
+
7
+ def load_tasks
8
+ Dir.glob("#{Knapsack.root}/lib/tasks/*.rake").each { |r| import r }
9
+ end
10
+ end
11
+ end
@@ -1,20 +1,14 @@
1
- require 'singleton'
2
-
3
1
  module Knapsack
4
2
  class Tracker
5
3
  include Singleton
6
4
 
7
- attr_reader :global_time, :files
5
+ attr_reader :global_time, :spec_files_with_time
8
6
  attr_writer :spec_path
9
7
 
10
8
  def initialize
11
9
  set_defaults
12
10
  end
13
11
 
14
- def generate_report?
15
- ENV['KNAPSACK_GENERATE_REPORT'] || false
16
- end
17
-
18
12
  def config(opts={})
19
13
  @config ||= default_config
20
14
  @config.merge!(opts)
@@ -35,18 +29,41 @@ module Knapsack
35
29
  @execution_time
36
30
  end
37
31
 
32
+ def spec_path
33
+ raise("spec_path needs to be set by Knapsack Adapter's bind method") unless @spec_path
34
+ @spec_path.sub(/^\.\//, '')
35
+ end
36
+
37
+ def time_exceeded?
38
+ global_time > max_node_time_execution
39
+ end
40
+
41
+ def max_node_time_execution
42
+ report_distributor.node_time_execution + config[:time_offset_in_seconds]
43
+ end
44
+
45
+ def exceeded_time
46
+ global_time - max_node_time_execution
47
+ end
48
+
38
49
  private
39
50
 
40
51
  def default_config
41
52
  {
42
53
  enable_time_offset_warning: true,
43
- time_offset_warning: 30,
54
+ time_offset_in_seconds: 30,
55
+ generate_report: generate_report
44
56
  }
45
57
  end
46
58
 
59
+ def generate_report
60
+ ENV['KNAPSACK_GENERATE_REPORT'] || false
61
+ end
62
+
47
63
  def set_defaults
48
64
  @global_time = 0
49
- @files = {}
65
+ @spec_files_with_time = {}
66
+ @spec_path = nil
50
67
  end
51
68
 
52
69
  def update_global_time
@@ -54,12 +71,12 @@ module Knapsack
54
71
  end
55
72
 
56
73
  def update_spec_file_time
57
- @files[spec_path] ||= 0
58
- @files[spec_path] += @execution_time
74
+ @spec_files_with_time[spec_path] ||= 0
75
+ @spec_files_with_time[spec_path] += @execution_time
59
76
  end
60
77
 
61
- def spec_path
62
- @spec_path || raise("spec_path needs to be set by Knapsack Adapter's bind method")
78
+ def report_distributor
79
+ @report_distributor ||= Knapsack::Distributors::ReportDistributor.new
63
80
  end
64
81
  end
65
82
  end
@@ -1,3 +1,3 @@
1
1
  module Knapsack
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,21 @@
1
+ require 'knapsack'
2
+
3
+ namespace :knapsack do
4
+ task :rspec do
5
+ allocator = Knapsack::Allocator.new
6
+
7
+ puts
8
+ puts 'Report specs:'
9
+ puts allocator.report_node_specs
10
+ puts
11
+ puts 'Leftover specs:'
12
+ puts allocator.leftover_node_specs
13
+ puts
14
+
15
+ custom_spec_dir = allocator.custom_spec_dir
16
+ default_path = custom_spec_dir ? "--default-path #{custom_spec_dir}" : nil
17
+ cmd = %Q[bundle exec rspec #{default_path} -- #{allocator.stringify_node_specs}]
18
+
19
+ exec(cmd)
20
+ end
21
+ end
@@ -0,0 +1,91 @@
1
+ describe Knapsack::Adapters::BaseAdapter do
2
+ describe '.bind' do
3
+ let(:adapter) { instance_double(described_class) }
4
+
5
+ subject { described_class.bind }
6
+
7
+ before do
8
+ expect(described_class).to receive(:new).and_return(adapter)
9
+ expect(adapter).to receive(:bind)
10
+ end
11
+
12
+ it { should eql adapter }
13
+ end
14
+
15
+ describe '#bind' do
16
+ let(:tracker) { instance_double(Knapsack::Tracker) }
17
+
18
+ before do
19
+ allow(subject).to receive(:tracker).and_return(tracker)
20
+ end
21
+
22
+ context 'when generate report' do
23
+ before do
24
+ expect(tracker).to receive(:config).and_return({ generate_report: true })
25
+ end
26
+
27
+ it do
28
+ expect(subject).to receive(:bind_time_tracker)
29
+ expect(subject).to receive(:bind_report_generator)
30
+ expect(subject).not_to receive(:bind_time_offset_warning)
31
+ subject.bind
32
+ end
33
+ end
34
+
35
+ context 'when enable time offset warning' do
36
+ before do
37
+ expect(tracker).to receive(:config).twice.and_return({
38
+ generate_report: false,
39
+ enable_time_offset_warning: true
40
+ })
41
+ end
42
+
43
+ it do
44
+ expect(subject).to receive(:bind_time_tracker)
45
+ expect(subject).to receive(:bind_time_offset_warning)
46
+ expect(subject).not_to receive(:bind_report_generator)
47
+ subject.bind
48
+ end
49
+ end
50
+
51
+ context 'when adapter is off' do
52
+ before do
53
+ expect(tracker).to receive(:config).twice.and_return({
54
+ generate_report: false,
55
+ enable_time_offset_warning: false
56
+ })
57
+ end
58
+
59
+ it do
60
+ expect(subject).not_to receive(:bind_time_tracker)
61
+ expect(subject).not_to receive(:bind_report_generator)
62
+ expect(subject).not_to receive(:bind_time_offset_warning)
63
+ subject.bind
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '#bind_time_tracker' do
69
+ it do
70
+ expect {
71
+ subject.bind_time_tracker
72
+ }.to raise_error(NotImplementedError)
73
+ end
74
+ end
75
+
76
+ describe '#bind_report_generator' do
77
+ it do
78
+ expect {
79
+ subject.bind_report_generator
80
+ }.to raise_error(NotImplementedError)
81
+ end
82
+ end
83
+
84
+ describe '#bind_time_offset_warning' do
85
+ it do
86
+ expect {
87
+ subject.bind_time_offset_warning
88
+ }.to raise_error(NotImplementedError)
89
+ end
90
+ end
91
+ end