knapsack 0.5.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +16 -16
- data/CHANGELOG.md +6 -0
- data/README.md +162 -61
- data/TODO.md +1 -1
- data/bin/print_header.sh +5 -0
- data/knapsack.gemspec +4 -2
- data/{knapsack_report.json → knapsack_rspec_report.json} +0 -0
- data/lib/knapsack.rb +5 -2
- data/lib/knapsack/adapters/base_adapter.rb +17 -1
- data/lib/knapsack/adapters/cucumber_adapter.rb +40 -0
- data/lib/knapsack/adapters/rspec_adapter.rb +5 -2
- data/lib/knapsack/allocator.rb +10 -11
- data/lib/knapsack/allocator_builder.rb +33 -0
- data/lib/knapsack/config/env.rb +30 -0
- data/lib/knapsack/config/tracker.rb +19 -0
- data/lib/knapsack/distributors/base_distributor.rb +18 -28
- data/lib/knapsack/distributors/leftover_distributor.rb +14 -14
- data/lib/knapsack/distributors/report_distributor.rb +33 -33
- data/lib/knapsack/presenter.rb +4 -4
- data/lib/knapsack/report.rb +13 -11
- data/lib/knapsack/tracker.rb +20 -15
- data/lib/knapsack/version.rb +1 -1
- data/lib/tasks/knapsack_cucumber.rake +19 -0
- data/lib/tasks/knapsack_rspec.rake +19 -0
- data/spec/knapsack/adapters/cucumber_adapter_spec.rb +69 -0
- data/spec/knapsack/adapters/rspec_adapter_spec.rb +26 -18
- data/spec/knapsack/allocator_builder_spec.rb +88 -0
- data/spec/knapsack/allocator_spec.rb +26 -27
- data/spec/knapsack/{config_spec.rb → config/env_spec.rb} +9 -32
- data/spec/knapsack/config/tracker_spec.rb +24 -0
- data/spec/knapsack/distributors/base_distributor_spec.rb +26 -39
- data/spec/knapsack/distributors/leftover_distributor_spec.rb +49 -31
- data/spec/knapsack/distributors/report_distributor_spec.rb +35 -41
- data/spec/knapsack/presenter_spec.rb +5 -5
- data/spec/knapsack/report_spec.rb +5 -9
- data/spec/knapsack/task_loader_spec.rb +4 -2
- data/spec/knapsack/tracker_spec.rb +14 -14
- data/spec/support/mocks/cucumber.rb +12 -0
- data/spec/support/shared_examples/adapter.rb +0 -4
- data/spec_examples/leftover/1_spec.rb +1 -1
- data/spec_examples/leftover/a_spec.rb +1 -1
- data/spec_examples/spec_helper.rb +1 -1
- metadata +54 -11
- data/lib/knapsack/config.rb +0 -40
- data/lib/tasks/knapsack.rake +0 -20
File without changes
|
data/lib/knapsack.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
require_relative 'knapsack/version'
|
3
|
-
require_relative 'knapsack/config'
|
3
|
+
require_relative 'knapsack/config/env'
|
4
|
+
require_relative 'knapsack/config/tracker'
|
4
5
|
require_relative 'knapsack/logger'
|
5
6
|
require_relative 'knapsack/tracker'
|
6
7
|
require_relative 'knapsack/presenter'
|
7
8
|
require_relative 'knapsack/report'
|
8
9
|
require_relative 'knapsack/allocator'
|
10
|
+
require_relative 'knapsack/allocator_builder'
|
9
11
|
require_relative 'knapsack/task_loader'
|
10
12
|
require_relative 'knapsack/distributors/base_distributor'
|
11
13
|
require_relative 'knapsack/distributors/report_distributor'
|
12
14
|
require_relative 'knapsack/distributors/leftover_distributor'
|
13
15
|
require_relative 'knapsack/adapters/base_adapter'
|
14
16
|
require_relative 'knapsack/adapters/rspec_adapter'
|
17
|
+
require_relative 'knapsack/adapters/cucumber_adapter'
|
15
18
|
|
16
19
|
module Knapsack
|
17
20
|
class << self
|
@@ -26,7 +29,7 @@ module Knapsack
|
|
26
29
|
end
|
27
30
|
|
28
31
|
def root
|
29
|
-
File.expand_path
|
32
|
+
File.expand_path('../..', __FILE__)
|
30
33
|
end
|
31
34
|
|
32
35
|
def load_tasks
|
@@ -1,6 +1,10 @@
|
|
1
1
|
module Knapsack
|
2
2
|
module Adapters
|
3
3
|
class BaseAdapter
|
4
|
+
# Just examples, please overwrite constants in subclasses
|
5
|
+
TEST_DIR_PATTERN = 'test/**/*_test.rb'
|
6
|
+
REPORT_PATH = 'knapsack_base_report.json'
|
7
|
+
|
4
8
|
def self.bind
|
5
9
|
adapter = new
|
6
10
|
adapter.bind
|
@@ -8,6 +12,8 @@ module Knapsack
|
|
8
12
|
end
|
9
13
|
|
10
14
|
def bind
|
15
|
+
update_report_config
|
16
|
+
|
11
17
|
if tracker.config[:generate_report]
|
12
18
|
Knapsack.logger.info 'Knapsack report generator started!'
|
13
19
|
bind_time_tracker
|
@@ -33,11 +39,21 @@ module Knapsack
|
|
33
39
|
raise NotImplementedError
|
34
40
|
end
|
35
41
|
|
36
|
-
|
42
|
+
private
|
37
43
|
|
38
44
|
def tracker
|
39
45
|
Knapsack.tracker
|
40
46
|
end
|
47
|
+
|
48
|
+
def update_report_config
|
49
|
+
current_test_file_pattern = Knapsack.report.config[:test_file_pattern]
|
50
|
+
current_report_path = Knapsack.report.config[:report_path]
|
51
|
+
|
52
|
+
Knapsack.report.config({
|
53
|
+
test_file_pattern: Knapsack::Config::Env.test_file_pattern || current_test_file_pattern || self.class::TEST_DIR_PATTERN,
|
54
|
+
report_path: Knapsack::Config::Env.report_path || current_report_path || self.class::REPORT_PATH
|
55
|
+
})
|
56
|
+
end
|
41
57
|
end
|
42
58
|
end
|
43
59
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Knapsack
|
2
|
+
module Adapters
|
3
|
+
class CucumberAdapter < BaseAdapter
|
4
|
+
TEST_DIR_PATTERN = 'features/**/*.feature'
|
5
|
+
REPORT_PATH = 'knapsack_cucumber_report.json'
|
6
|
+
|
7
|
+
def bind_time_tracker
|
8
|
+
Around do |scenario, block|
|
9
|
+
Knapsack.tracker.test_path = scenario.file
|
10
|
+
Knapsack.tracker.start_timer
|
11
|
+
block.call
|
12
|
+
Knapsack.tracker.stop_timer
|
13
|
+
end
|
14
|
+
|
15
|
+
::Kernel.at_exit do
|
16
|
+
Knapsack.logger.info(Presenter.global_time)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def bind_report_generator
|
21
|
+
::Kernel.at_exit do
|
22
|
+
Knapsack.report.save
|
23
|
+
Knapsack.logger.info(Presenter.report_details)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def bind_time_offset_warning
|
28
|
+
::Kernel.at_exit do
|
29
|
+
Knapsack.logger.warn(Presenter.time_offset_warning)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def Around(*tag_expressions, &proc)
|
36
|
+
::Cucumber::RbSupport::RbDsl.register_rb_hook('around', tag_expressions, proc)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module Knapsack
|
2
2
|
module Adapters
|
3
3
|
class RspecAdapter < BaseAdapter
|
4
|
+
TEST_DIR_PATTERN = 'spec/**/*_spec.rb'
|
5
|
+
REPORT_PATH = 'knapsack_rspec_report.json'
|
6
|
+
|
4
7
|
def bind_time_tracker
|
5
8
|
::RSpec.configure do |config|
|
6
9
|
config.before(:each) do
|
@@ -10,7 +13,7 @@ module Knapsack
|
|
10
13
|
else
|
11
14
|
example.metadata
|
12
15
|
end
|
13
|
-
Knapsack.tracker.
|
16
|
+
Knapsack.tracker.test_path = RspecAdapter.test_path(current_example_group)
|
14
17
|
Knapsack.tracker.start_timer
|
15
18
|
end
|
16
19
|
|
@@ -41,7 +44,7 @@ module Knapsack
|
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
44
|
-
def self.
|
47
|
+
def self.test_path(example_group)
|
45
48
|
until example_group[:parent_example_group].nil?
|
46
49
|
example_group = example_group[:parent_example_group]
|
47
50
|
end
|
data/lib/knapsack/allocator.rb
CHANGED
@@ -1,29 +1,28 @@
|
|
1
1
|
module Knapsack
|
2
2
|
class Allocator
|
3
3
|
def initialize(args={})
|
4
|
-
@spec_pattern = args[:spec_pattern] || Config.spec_pattern
|
5
4
|
@report_distributor = Knapsack::Distributors::ReportDistributor.new(args)
|
6
5
|
@leftover_distributor = Knapsack::Distributors::LeftoverDistributor.new(args)
|
7
6
|
end
|
8
7
|
|
9
|
-
def
|
10
|
-
@
|
8
|
+
def report_node_tests
|
9
|
+
@report_node_tests ||= @report_distributor.tests_for_current_node
|
11
10
|
end
|
12
11
|
|
13
|
-
def
|
14
|
-
@
|
12
|
+
def leftover_node_tests
|
13
|
+
@leftover_node_tests ||= @leftover_distributor.tests_for_current_node
|
15
14
|
end
|
16
15
|
|
17
|
-
def
|
18
|
-
@
|
16
|
+
def node_tests
|
17
|
+
@node_tests ||= report_node_tests + leftover_node_tests
|
19
18
|
end
|
20
19
|
|
21
|
-
def
|
22
|
-
|
20
|
+
def stringify_node_tests
|
21
|
+
node_tests.join(' ')
|
23
22
|
end
|
24
23
|
|
25
|
-
def
|
26
|
-
@
|
24
|
+
def test_dir
|
25
|
+
@report_distributor.test_file_pattern.gsub(/^(.*?)\//).first
|
27
26
|
end
|
28
27
|
end
|
29
28
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Knapsack
|
2
|
+
class AllocatorBuilder
|
3
|
+
def initialize(adapter_class)
|
4
|
+
@adapter_class = adapter_class
|
5
|
+
set_report_path
|
6
|
+
end
|
7
|
+
|
8
|
+
def allocator
|
9
|
+
Knapsack::Allocator.new({
|
10
|
+
report: Knapsack.report.open,
|
11
|
+
test_file_pattern: test_file_pattern,
|
12
|
+
ci_node_total: Knapsack::Config::Env.ci_node_total,
|
13
|
+
ci_node_index: Knapsack::Config::Env.ci_node_index
|
14
|
+
})
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def set_report_path
|
20
|
+
Knapsack.report.config({
|
21
|
+
report_path: report_path
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
def report_path
|
26
|
+
Knapsack::Config::Env.report_path || @adapter_class::REPORT_PATH
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_file_pattern
|
30
|
+
Knapsack::Config::Env.test_file_pattern || @adapter_class::TEST_DIR_PATTERN
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Knapsack
|
2
|
+
module Config
|
3
|
+
class Env
|
4
|
+
class << self
|
5
|
+
def report_path
|
6
|
+
ENV['KNAPSACK_REPORT_PATH']
|
7
|
+
end
|
8
|
+
|
9
|
+
def ci_node_total
|
10
|
+
ENV['CI_NODE_TOTAL'] || ENV['CIRCLE_NODE_TOTAL'] || ENV['SEMAPHORE_THREAD_COUNT'] || 1
|
11
|
+
end
|
12
|
+
|
13
|
+
def ci_node_index
|
14
|
+
ENV['CI_NODE_INDEX'] || ENV['CIRCLE_NODE_INDEX'] || semaphore_current_thread || 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_file_pattern
|
18
|
+
ENV['KNAPSACK_TEST_FILE_PATTERN']
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def semaphore_current_thread
|
24
|
+
index = ENV['SEMAPHORE_CURRENT_THREAD']
|
25
|
+
index.to_i - 1 if index
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Knapsack
|
2
|
+
module Config
|
3
|
+
class Tracker
|
4
|
+
class << self
|
5
|
+
def enable_time_offset_warning
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def time_offset_in_seconds
|
10
|
+
30
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate_report
|
14
|
+
ENV['KNAPSACK_GENERATE_REPORT'] || false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module Knapsack
|
2
2
|
module Distributors
|
3
3
|
class BaseDistributor
|
4
|
-
attr_reader :report, :
|
4
|
+
attr_reader :report, :node_tests, :test_file_pattern
|
5
5
|
|
6
6
|
def initialize(args={})
|
7
|
-
@report = args[:report] ||
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
7
|
+
@report = args[:report] || raise('Missing report')
|
8
|
+
@test_file_pattern = args[:test_file_pattern] || raise('Missing test_file_pattern')
|
9
|
+
@ci_node_total = args[:ci_node_total] || raise('Missing ci_node_total')
|
10
|
+
@ci_node_index = args[:ci_node_index] || raise('Missing ci_node_index')
|
11
11
|
end
|
12
12
|
|
13
13
|
def ci_node_total
|
@@ -18,47 +18,37 @@ module Knapsack
|
|
18
18
|
@ci_node_index.to_i
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
|
21
|
+
def tests_for_current_node
|
22
|
+
tests_for_node(ci_node_index)
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
25
|
+
def tests_for_node(node_index)
|
26
|
+
assign_test_files_to_node
|
27
|
+
post_tests_for_node(node_index)
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
30
|
+
def assign_test_files_to_node
|
31
|
+
default_node_tests
|
32
|
+
post_assign_test_files_to_node
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
36
|
-
@
|
35
|
+
def all_tests
|
36
|
+
@all_tests ||= Dir[test_file_pattern]
|
37
37
|
end
|
38
38
|
|
39
39
|
protected
|
40
40
|
|
41
|
-
def
|
41
|
+
def post_tests_for_node(node_index)
|
42
42
|
raise NotImplementedError
|
43
43
|
end
|
44
44
|
|
45
|
-
def
|
45
|
+
def post_assign_test_files_to_node
|
46
46
|
raise NotImplementedError
|
47
47
|
end
|
48
48
|
|
49
|
-
def
|
49
|
+
def default_node_tests
|
50
50
|
raise NotImplementedError
|
51
51
|
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def config
|
56
|
-
Knapsack::Config
|
57
|
-
end
|
58
|
-
|
59
|
-
def default_report
|
60
|
-
Knapsack.report.open
|
61
|
-
end
|
62
52
|
end
|
63
53
|
end
|
64
54
|
end
|
@@ -1,35 +1,35 @@
|
|
1
1
|
module Knapsack
|
2
2
|
module Distributors
|
3
3
|
class LeftoverDistributor < BaseDistributor
|
4
|
-
def
|
5
|
-
@
|
4
|
+
def report_tests
|
5
|
+
@report_tests ||= report.keys
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
9
|
-
@
|
8
|
+
def leftover_tests
|
9
|
+
@leftover_tests ||= all_tests - report_tests
|
10
10
|
end
|
11
11
|
|
12
12
|
private
|
13
13
|
|
14
|
-
def
|
14
|
+
def post_assign_test_files_to_node
|
15
15
|
node_index = 0
|
16
|
-
|
17
|
-
|
16
|
+
leftover_tests.each do |test_file|
|
17
|
+
node_tests[node_index] << test_file
|
18
18
|
node_index += 1
|
19
19
|
node_index %= ci_node_total
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
|
25
|
-
return unless
|
26
|
-
|
23
|
+
def post_tests_for_node(node_index)
|
24
|
+
test_files = node_tests[node_index]
|
25
|
+
return unless test_files
|
26
|
+
test_files
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
30
|
-
@
|
29
|
+
def default_node_tests
|
30
|
+
@node_tests = []
|
31
31
|
ci_node_total.times do |index|
|
32
|
-
@
|
32
|
+
@node_tests[index] = []
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
@@ -2,15 +2,15 @@ module Knapsack
|
|
2
2
|
module Distributors
|
3
3
|
class ReportDistributor < BaseDistributor
|
4
4
|
def sorted_report
|
5
|
-
@sorted_report ||= report.sort_by { |
|
5
|
+
@sorted_report ||= report.sort_by { |test_path, time| time }.reverse
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
9
|
-
@
|
8
|
+
def sorted_report_with_existing_tests
|
9
|
+
@sorted_report_with_existing_tests ||= sorted_report.select { |test_path, time| all_tests.include?(test_path) }
|
10
10
|
end
|
11
11
|
|
12
12
|
def total_time_execution
|
13
|
-
@total_time_execution ||=
|
13
|
+
@total_time_execution ||= sorted_report_with_existing_tests.map(&:last).reduce(0, :+).to_f
|
14
14
|
end
|
15
15
|
|
16
16
|
def node_time_execution
|
@@ -19,62 +19,62 @@ module Knapsack
|
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
22
|
+
def post_assign_test_files_to_node
|
23
|
+
assign_slow_test_files
|
24
|
+
assign_remaining_test_files
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
28
|
-
|
29
|
-
return unless
|
30
|
-
|
27
|
+
def post_tests_for_node(node_index)
|
28
|
+
node_test = node_tests[node_index]
|
29
|
+
return unless node_test
|
30
|
+
node_test[:test_files_with_time].map(&:first)
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
34
|
-
@
|
33
|
+
def default_node_tests
|
34
|
+
@node_tests = []
|
35
35
|
ci_node_total.times do |index|
|
36
|
-
@
|
36
|
+
@node_tests << {
|
37
37
|
node_index: index,
|
38
38
|
time_left: node_time_execution,
|
39
|
-
|
39
|
+
test_files_with_time: []
|
40
40
|
}
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
def
|
45
|
-
@
|
44
|
+
def assign_slow_test_files
|
45
|
+
@not_assigned_test_files = []
|
46
46
|
node_index = 0
|
47
|
-
|
48
|
-
|
47
|
+
sorted_report_with_existing_tests.each do |test_file_with_time|
|
48
|
+
assign_slow_test_file(node_index, test_file_with_time)
|
49
49
|
node_index += 1
|
50
50
|
node_index %= ci_node_total
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
def
|
55
|
-
time =
|
56
|
-
time_left =
|
54
|
+
def assign_slow_test_file(node_index, test_file_with_time)
|
55
|
+
time = test_file_with_time[1]
|
56
|
+
time_left = node_tests[node_index][:time_left] - time
|
57
57
|
|
58
|
-
if time_left >= 0 or
|
59
|
-
|
60
|
-
|
58
|
+
if time_left >= 0 or node_tests[node_index][:test_files_with_time].empty?
|
59
|
+
node_tests[node_index][:time_left] -= time
|
60
|
+
node_tests[node_index][:test_files_with_time] << test_file_with_time
|
61
61
|
else
|
62
|
-
@
|
62
|
+
@not_assigned_test_files << test_file_with_time
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
66
|
-
def
|
67
|
-
@
|
66
|
+
def assign_remaining_test_files
|
67
|
+
@not_assigned_test_files.each do |test_file_with_time|
|
68
68
|
index = node_with_max_time_left
|
69
|
-
time =
|
70
|
-
|
71
|
-
|
69
|
+
time = test_file_with_time[1]
|
70
|
+
node_tests[index][:time_left] -= time
|
71
|
+
node_tests[index][:test_files_with_time] << test_file_with_time
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
75
|
def node_with_max_time_left
|
76
|
-
|
77
|
-
|
76
|
+
node_test = node_tests.max { |a,b| a[:time_left] <=> b[:time_left] }
|
77
|
+
node_test[:node_index]
|
78
78
|
end
|
79
79
|
end
|
80
80
|
end
|