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.
- checksums.yaml +4 -4
- data/.travis.yml +12 -1
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +1 -1
- data/README.md +71 -24
- data/Rakefile +3 -1
- data/TODO.md +5 -0
- data/knapsack_report.json +9 -12
- data/lib/knapsack.rb +25 -6
- data/lib/knapsack/adapters/{base.rb → base_adapter.rb} +18 -8
- data/lib/knapsack/adapters/{rspec.rb → rspec_adapter.rb} +13 -6
- data/lib/knapsack/allocator.rb +65 -0
- data/lib/knapsack/distributors/base_distributor.rb +67 -0
- data/lib/knapsack/distributors/leftover_distributor.rb +49 -0
- data/lib/knapsack/distributors/report_distributor.rb +76 -0
- data/lib/knapsack/presenter.rb +38 -5
- data/lib/knapsack/report.rb +27 -12
- data/lib/knapsack/task_loader.rb +11 -0
- data/lib/knapsack/tracker.rb +30 -13
- data/lib/knapsack/version.rb +1 -1
- data/lib/tasks/knapsack.rake +21 -0
- data/spec/knapsack/adapters/base_adapter_spec.rb +91 -0
- data/spec/knapsack/adapters/rspec_adapter_spec.rb +29 -0
- data/spec/knapsack/allocator_spec.rb +57 -0
- data/spec/knapsack/distributors/base_distributor_spec.rb +85 -0
- data/spec/knapsack/distributors/leftover_distributor_spec.rb +110 -0
- data/spec/knapsack/distributors/report_distributor_spec.rb +152 -0
- data/spec/knapsack/presenter_spec.rb +83 -0
- data/spec/knapsack/report_spec.rb +73 -0
- data/spec/knapsack/task_loader_spec.rb +10 -0
- data/spec/knapsack/tracker_spec.rb +84 -20
- data/spec/knapsack_spec.rb +23 -2
- data/spec/spec_helper.rb +16 -0
- data/spec_examples/fast/1_spec.rb +1 -4
- data/spec_examples/fast/2_spec.rb +1 -4
- data/spec_examples/fast/3_spec.rb +1 -4
- data/spec_examples/fast/4_spec.rb +1 -4
- data/spec_examples/fast/5_spec.rb +1 -4
- data/spec_examples/fast/6_spec.rb +1 -4
- data/spec_examples/leftover/1_spec.rb +4 -0
- data/spec_examples/leftover/a_spec.rb +8 -0
- data/spec_examples/slow/a_spec.rb +1 -3
- data/spec_examples/slow/b_spec.rb +3 -5
- data/spec_examples/slow/c_spec.rb +1 -3
- data/spec_examples/spec_helper.rb +5 -2
- metadata +32 -7
- data/spec_examples/slow/d_spec.rb +0 -7
- data/spec_examples/slow/e_spec.rb +0 -7
- 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
|
data/lib/knapsack/presenter.rb
CHANGED
@@ -5,19 +5,52 @@ module Knapsack
|
|
5
5
|
class Presenter
|
6
6
|
class << self
|
7
7
|
def report_yml
|
8
|
-
Knapsack.tracker.
|
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.
|
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
|
-
"
|
20
|
+
"\nKnapsack global time execution for specs: #{Knapsack.tracker.global_time}s"
|
17
21
|
end
|
18
22
|
|
19
|
-
def
|
20
|
-
"
|
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
|
data/lib/knapsack/report.rb
CHANGED
@@ -1,20 +1,35 @@
|
|
1
1
|
module Knapsack
|
2
2
|
class Report
|
3
|
-
|
3
|
+
include Singleton
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
end
|
5
|
+
def config(opts={})
|
6
|
+
@config ||= default_config
|
7
|
+
@config.merge!(opts)
|
8
|
+
end
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
data/lib/knapsack/tracker.rb
CHANGED
@@ -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, :
|
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
|
-
|
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
|
-
@
|
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
|
-
@
|
58
|
-
@
|
74
|
+
@spec_files_with_time[spec_path] ||= 0
|
75
|
+
@spec_files_with_time[spec_path] += @execution_time
|
59
76
|
end
|
60
77
|
|
61
|
-
def
|
62
|
-
@
|
78
|
+
def report_distributor
|
79
|
+
@report_distributor ||= Knapsack::Distributors::ReportDistributor.new
|
63
80
|
end
|
64
81
|
end
|
65
82
|
end
|
data/lib/knapsack/version.rb
CHANGED
@@ -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
|