treye-semaphore_test_boosters 2.5.1
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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +105 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +226 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config.reek +16 -0
- data/exe/cucumber_booster +5 -0
- data/exe/ex_unit_booster +5 -0
- data/exe/go_test_booster +5 -0
- data/exe/minitest_booster +5 -0
- data/exe/rspec_booster +5 -0
- data/lib/test_boosters.rb +28 -0
- data/lib/test_boosters/boosters/base.rb +81 -0
- data/lib/test_boosters/boosters/cucumber.rb +37 -0
- data/lib/test_boosters/boosters/ex_unit.rb +17 -0
- data/lib/test_boosters/boosters/go_test.rb +17 -0
- data/lib/test_boosters/boosters/minitest.rb +21 -0
- data/lib/test_boosters/boosters/rspec.rb +49 -0
- data/lib/test_boosters/cli_parser.rb +49 -0
- data/lib/test_boosters/files/distributor.rb +45 -0
- data/lib/test_boosters/files/leftover_files.rb +42 -0
- data/lib/test_boosters/files/split_configuration.rb +68 -0
- data/lib/test_boosters/insights_uploader.rb +24 -0
- data/lib/test_boosters/job.rb +40 -0
- data/lib/test_boosters/logger.rb +19 -0
- data/lib/test_boosters/project_info.rb +32 -0
- data/lib/test_boosters/shell.rb +51 -0
- data/lib/test_boosters/version.rb +3 -0
- data/rspec_formatters/semaphore_rspec3_json_formatter.rb +55 -0
- data/test_boosters.gemspec +32 -0
- metadata +195 -0
data/exe/ex_unit_booster
ADDED
data/exe/go_test_booster
ADDED
data/exe/rspec_booster
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "optparse"
|
3
|
+
require "json"
|
4
|
+
require "cucumber_booster_config"
|
5
|
+
|
6
|
+
module TestBoosters
|
7
|
+
require "test_boosters/version"
|
8
|
+
|
9
|
+
require "test_boosters/cli_parser"
|
10
|
+
require "test_boosters/logger"
|
11
|
+
require "test_boosters/shell"
|
12
|
+
require "test_boosters/insights_uploader"
|
13
|
+
require "test_boosters/project_info"
|
14
|
+
require "test_boosters/job"
|
15
|
+
|
16
|
+
require "test_boosters/files/distributor"
|
17
|
+
require "test_boosters/files/leftover_files"
|
18
|
+
require "test_boosters/files/split_configuration"
|
19
|
+
|
20
|
+
require "test_boosters/boosters/base"
|
21
|
+
require "test_boosters/boosters/rspec"
|
22
|
+
require "test_boosters/boosters/cucumber"
|
23
|
+
require "test_boosters/boosters/go_test"
|
24
|
+
require "test_boosters/boosters/ex_unit"
|
25
|
+
require "test_boosters/boosters/minitest"
|
26
|
+
|
27
|
+
ROOT_PATH = File.absolute_path(File.dirname(__FILE__) + "/..")
|
28
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Boosters
|
3
|
+
class Base
|
4
|
+
|
5
|
+
def initialize(file_pattern, split_configuration_path, command)
|
6
|
+
@command = command
|
7
|
+
@file_pattern = file_pattern
|
8
|
+
@split_configuration_path = split_configuration_path
|
9
|
+
end
|
10
|
+
|
11
|
+
# :reek:TooManyStatements
|
12
|
+
def run
|
13
|
+
display_header
|
14
|
+
|
15
|
+
before_job # execute some activities when the before the job starts
|
16
|
+
|
17
|
+
distribution.display_info
|
18
|
+
|
19
|
+
known, leftover = distribution.files_for(job_index)
|
20
|
+
|
21
|
+
if cli_options[:dry_run]
|
22
|
+
show_files_for_dry_run("known", known)
|
23
|
+
show_files_for_dry_run("leftover", leftover)
|
24
|
+
return 0
|
25
|
+
end
|
26
|
+
|
27
|
+
exit_status = TestBoosters::Job.run(@command, known, leftover)
|
28
|
+
|
29
|
+
after_job # execute some activities when the job finishes
|
30
|
+
|
31
|
+
exit_status
|
32
|
+
end
|
33
|
+
|
34
|
+
def show_files_for_dry_run(label, files)
|
35
|
+
if files.empty?
|
36
|
+
puts "[DRY RUN] No #{label} files."
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "\n[DRY RUN] Running tests for #{label} files:"
|
41
|
+
puts files.map { |file| "- #{file}" }.join("\n")
|
42
|
+
end
|
43
|
+
|
44
|
+
def before_job
|
45
|
+
# Do nothing
|
46
|
+
end
|
47
|
+
|
48
|
+
def after_job
|
49
|
+
# Do nothing
|
50
|
+
end
|
51
|
+
|
52
|
+
def display_header
|
53
|
+
version = "Test Booster v#{TestBoosters::VERSION}"
|
54
|
+
job_info = "Job #{job_index + 1} out of #{job_count}"
|
55
|
+
|
56
|
+
TestBoosters::Shell.display_title("#{version} - #{job_info}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def distribution
|
60
|
+
@distribution ||= TestBoosters::Files::Distributor.new(@split_configuration_path,
|
61
|
+
@file_pattern,
|
62
|
+
job_count)
|
63
|
+
end
|
64
|
+
|
65
|
+
def job_index
|
66
|
+
@job_index ||= cli_options[:job_index] - 1
|
67
|
+
end
|
68
|
+
|
69
|
+
def job_count
|
70
|
+
@job_count ||= cli_options[:job_count]
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def cli_options
|
76
|
+
@cli_options ||= TestBoosters::CliParser.parse
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Boosters
|
3
|
+
class Cucumber < Base
|
4
|
+
|
5
|
+
FILE_PATTERN = "features/**/*.feature".freeze
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super(FILE_PATTERN, split_configuration_path, "bundle exec cucumber")
|
9
|
+
end
|
10
|
+
|
11
|
+
def before_job
|
12
|
+
CucumberBoosterConfig::Injection.new(Dir.pwd, report_path).run
|
13
|
+
end
|
14
|
+
|
15
|
+
def after_job
|
16
|
+
TestBoosters::InsightsUploader.upload("cucumber", report_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def display_header
|
20
|
+
super
|
21
|
+
|
22
|
+
TestBoosters::ProjectInfo.display_ruby_version
|
23
|
+
TestBoosters::ProjectInfo.display_bundler_version
|
24
|
+
TestBoosters::ProjectInfo.display_cucumber_version
|
25
|
+
end
|
26
|
+
|
27
|
+
def report_path
|
28
|
+
@report_path ||= ENV["REPORT_PATH"] || "#{ENV["HOME"]}/cucumber_report.json"
|
29
|
+
end
|
30
|
+
|
31
|
+
def split_configuration_path
|
32
|
+
ENV["CUCUMBER_SPLIT_CONFIGURATION_PATH"] || "#{ENV["HOME"]}/cucumber_split_configuration.json"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Boosters
|
3
|
+
class ExUnit < Base
|
4
|
+
|
5
|
+
FILE_PATTERN = "test/**/*_test.exs".freeze
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super(FILE_PATTERN, split_configuration_path, "mix test")
|
9
|
+
end
|
10
|
+
|
11
|
+
def split_configuration_path
|
12
|
+
ENV["EX_UNIT_SPLIT_CONFIGURATION_PATH"] || "#{ENV["HOME"]}/ex_unit_split_configuration.json"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Boosters
|
3
|
+
class GoTest < Base
|
4
|
+
|
5
|
+
FILE_PATTERN = "**/*/*_test.go".freeze
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super(FILE_PATTERN, split_configuration_path, "go test")
|
9
|
+
end
|
10
|
+
|
11
|
+
def split_configuration_path
|
12
|
+
ENV["GO_TEST_SPLIT_CONFIGURATION_PATH"] || "#{ENV["HOME"]}/go_test_split_configuration.json"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Boosters
|
3
|
+
class Minitest < Base
|
4
|
+
|
5
|
+
FILE_PATTERN = "test/**/*_test.rb".freeze
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super(FILE_PATTERN, split_configuration_path, command)
|
9
|
+
end
|
10
|
+
|
11
|
+
def command
|
12
|
+
"ruby -e 'ARGV.each { |f| require \"./\#{f}\" }'"
|
13
|
+
end
|
14
|
+
|
15
|
+
def split_configuration_path
|
16
|
+
ENV["MINITEST_SPLIT_CONFIGURATION_PATH"] || "#{ENV["HOME"]}/minitest_split_configuration.json"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Boosters
|
3
|
+
class Rspec < Base
|
4
|
+
def initialize
|
5
|
+
super(file_pattern, split_configuration_path, command)
|
6
|
+
end
|
7
|
+
|
8
|
+
def display_header
|
9
|
+
super
|
10
|
+
|
11
|
+
TestBoosters::ProjectInfo.display_ruby_version
|
12
|
+
TestBoosters::ProjectInfo.display_bundler_version
|
13
|
+
TestBoosters::ProjectInfo.display_rspec_version
|
14
|
+
end
|
15
|
+
|
16
|
+
def after_job
|
17
|
+
TestBoosters::InsightsUploader.upload("rspec", report_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def command
|
21
|
+
@command ||= "bundle exec rspec #{rspec_options}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def rspec_options
|
25
|
+
@rspec_options ||= begin
|
26
|
+
output_formatter = ENV.fetch("TB_RSPEC_FORMATTER", "documentation")
|
27
|
+
# rubocop:disable LineLength
|
28
|
+
"#{ENV["TB_RSPEC_OPTIONS"]} --format #{output_formatter} --require #{formatter_path} --format SemaphoreFormatter --out #{report_path}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def report_path
|
33
|
+
@report_path ||= ENV["REPORT_PATH"] || "#{ENV["HOME"]}/rspec_report.json"
|
34
|
+
end
|
35
|
+
|
36
|
+
def split_configuration_path
|
37
|
+
ENV["RSPEC_SPLIT_CONFIGURATION_PATH"] || "#{ENV["HOME"]}/rspec_split_configuration.json"
|
38
|
+
end
|
39
|
+
|
40
|
+
def formatter_path
|
41
|
+
@formatter_path ||= File.join(::TestBoosters::ROOT_PATH, "rspec_formatters/semaphore_rspec3_json_formatter.rb")
|
42
|
+
end
|
43
|
+
|
44
|
+
def file_pattern
|
45
|
+
ENV["TEST_BOOSTERS_RSPEC_TEST_FILE_PATTERN"] || "spec/**/*_spec.rb"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module CliParser
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# :reek:TooManyStatements
|
6
|
+
# :reek:NestedIterators
|
7
|
+
# :reek:DuplicateMethodCall
|
8
|
+
def parse
|
9
|
+
options = {}
|
10
|
+
|
11
|
+
parser = OptionParser.new do |opts|
|
12
|
+
opts.on(
|
13
|
+
"--thread INDEX",
|
14
|
+
"[DEPRECATED] Use the '--job' option instead"
|
15
|
+
) do |parameter|
|
16
|
+
puts "[DEPRECATION WARNING] The '--thread' parameter is deprecated. Please use '--job' instead."
|
17
|
+
|
18
|
+
options.merge!(parse_job_params(parameter))
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on(
|
22
|
+
"--job INDEX",
|
23
|
+
"The job index and number of total jobs. e.g. --job 4/32"
|
24
|
+
) do |parameter|
|
25
|
+
options.merge!(parse_job_params(parameter))
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on(
|
29
|
+
"--dry-run",
|
30
|
+
"Only print the files that will be run for this job index"
|
31
|
+
) do |parameter|
|
32
|
+
options.merge!(:dry_run => parameter)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
parser.parse!
|
37
|
+
|
38
|
+
options
|
39
|
+
end
|
40
|
+
|
41
|
+
# parses input like '1/32' and outputs { :job_index => 1, :job_count => 32 }
|
42
|
+
def parse_job_params(input_parameter)
|
43
|
+
job_index, job_count, _rest = input_parameter.split("/")
|
44
|
+
|
45
|
+
{ :job_index => job_index.to_i, :job_count => job_count.to_i }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Files
|
3
|
+
|
4
|
+
#
|
5
|
+
# Distributes test files based on split configuration, file pattern, and their file size
|
6
|
+
#
|
7
|
+
class Distributor
|
8
|
+
|
9
|
+
def initialize(split_configuration_path, file_pattern, job_count)
|
10
|
+
@split_configuration_path = split_configuration_path
|
11
|
+
@file_pattern = file_pattern
|
12
|
+
@job_count = job_count
|
13
|
+
end
|
14
|
+
|
15
|
+
def display_info
|
16
|
+
puts "Split configuration present: #{split_configuration.present? ? "yes" : "no"}"
|
17
|
+
puts "Split configuration valid: #{split_configuration.valid? ? "yes" : "no"}"
|
18
|
+
puts "Split configuration file count: #{split_configuration.all_files.size}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def files_for(job_index)
|
22
|
+
known = all_files & split_configuration.files_for_job(job_index)
|
23
|
+
leftover = leftover_files.select(:index => job_index, :total => @job_count)
|
24
|
+
|
25
|
+
[known, leftover]
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_files
|
29
|
+
@all_files ||= Dir[@file_pattern].sort
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def leftover_files
|
35
|
+
@leftover_files ||= TestBoosters::Files::LeftoverFiles.new(all_files - split_configuration.all_files)
|
36
|
+
end
|
37
|
+
|
38
|
+
def split_configuration
|
39
|
+
@split_configuration ||= TestBoosters::Files::SplitConfiguration.new(@split_configuration_path)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Files
|
3
|
+
class LeftoverFiles
|
4
|
+
|
5
|
+
attr_reader :files
|
6
|
+
|
7
|
+
def initialize(files)
|
8
|
+
@files = files
|
9
|
+
end
|
10
|
+
|
11
|
+
def select(options = {})
|
12
|
+
index = options.fetch(:index)
|
13
|
+
total = options.fetch(:total)
|
14
|
+
|
15
|
+
file_distribution(total)[index]
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def file_distribution(job_count)
|
21
|
+
# create N empty boxes
|
22
|
+
jobs = Array.new(job_count) { [] }
|
23
|
+
|
24
|
+
# distribute files in Round Robin fashion
|
25
|
+
sorted_files_by_file_size.each.with_index do |file, index|
|
26
|
+
jobs[index % job_count] << file
|
27
|
+
end
|
28
|
+
|
29
|
+
jobs
|
30
|
+
end
|
31
|
+
|
32
|
+
def sorted_files_by_file_size
|
33
|
+
@sorted_files_by_file_size ||= existing_files.sort_by { |file| -File.size(file) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def existing_files
|
37
|
+
@existing_files ||= @files.select { |file| File.file?(file) }
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module TestBoosters
|
2
|
+
module Files
|
3
|
+
class SplitConfiguration
|
4
|
+
|
5
|
+
Job = Struct.new(:files)
|
6
|
+
|
7
|
+
def initialize(path)
|
8
|
+
@path = path
|
9
|
+
@valid = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def present?
|
13
|
+
File.exist?(@path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
jobs # try to load data into memory
|
18
|
+
|
19
|
+
@valid
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_files
|
23
|
+
@all_files ||= jobs.map(&:files).flatten.sort
|
24
|
+
end
|
25
|
+
|
26
|
+
def files_for_job(job_index)
|
27
|
+
job = jobs[job_index]
|
28
|
+
|
29
|
+
job ? job.files : []
|
30
|
+
end
|
31
|
+
|
32
|
+
def jobs
|
33
|
+
@jobs ||= present? ? load_data : []
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# :reek:TooManyStatements
|
39
|
+
def load_data
|
40
|
+
@valid = false
|
41
|
+
|
42
|
+
content = JSON.parse(File.read(@path)).map do |raw_job|
|
43
|
+
files = raw_job.fetch("files").sort
|
44
|
+
|
45
|
+
TestBoosters::Files::SplitConfiguration::Job.new(files)
|
46
|
+
end
|
47
|
+
|
48
|
+
@valid = true
|
49
|
+
|
50
|
+
content
|
51
|
+
rescue TypeError, KeyError => ex
|
52
|
+
log_error("Split Configuration has invalid structure", ex)
|
53
|
+
|
54
|
+
[]
|
55
|
+
rescue JSON::ParserError => ex
|
56
|
+
log_error("Split Configuration is not parsable", ex)
|
57
|
+
|
58
|
+
[]
|
59
|
+
end
|
60
|
+
|
61
|
+
def log_error(message, exception)
|
62
|
+
TestBoosters::Logger.error(message)
|
63
|
+
TestBoosters::Logger.error(exception.inspect)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|