knapsack 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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