generated from mattbrictson/gem
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
--shard
flag for splitting tests across parallel CI jobs (#14)
* Add `--shard` flag for splitting tests across parallel CI jobs With this commit, mighty_test can now distribute test files evenly across parallel CI jobs, using the `--shard` option. The _shard_ nomenclature has been borrowed from similar features in [Jest](https://jestjs.io/docs/cli#--shard) and [Playwright](https://playwright.dev/docs/test-sharding). ```sh # Run the 1st group of tests out of 4 total groups bin/mt --shard 1/4 ``` Test files are shuffled before dividing into shards. To ensure the shuffle is consistent across CI nodes, the SHA of the git commit being tested is used as the random seed. If the SHA cannot be determined, a hard-coded seed is used. * Ensure CI environment doesn't pollute unit test
- Loading branch information
1 parent
8fa73df
commit 0d7f124
Showing
7 changed files
with
154 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
module MightyTest | ||
class Sharder | ||
DEFAULT_SEED = 123_456_789 | ||
|
||
def self.from_argv(value, env: ENV) | ||
index, total = value.to_s.match(%r{\A(\d+)/(\d+)\z})&.captures&.map(&:to_i) | ||
raise ArgumentError, "shard: value must be in the form INDEX/TOTAL (e.g. 2/8)" if total.nil? | ||
|
||
git_sha = env.values_at("GITHUB_SHA", "CIRCLE_SHA1").find { |sha| !sha.to_s.strip.empty? } | ||
seed = git_sha&.unpack1("l_") | ||
|
||
new(index:, total:, seed:) | ||
end | ||
|
||
attr_reader :index, :total, :seed | ||
|
||
def initialize(index:, total:, seed: nil) | ||
raise ArgumentError, "shard: total shards must be a number greater than 0" unless total > 0 | ||
|
||
valid_group = index > 0 && index <= total | ||
raise ArgumentError, "shard: shard index must be > 0 and <= #{total}" unless valid_group | ||
|
||
@index = index | ||
@total = total | ||
@seed = seed || DEFAULT_SEED | ||
end | ||
|
||
def shard(*test_paths) | ||
random = Random.new(seed) | ||
shuffled_paths = test_paths.flatten.shuffle(random:) | ||
slices = shuffled_paths.each_slice(total) | ||
slices.filter_map { |slice| slice[index - 1] } | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
require "test_helper" | ||
|
||
module MightyTest | ||
class SharderTest < Minitest::Test | ||
def test_it_parses_the_shard_value | ||
sharder = Sharder.from_argv("2/7") | ||
|
||
assert_equal(2, sharder.index) | ||
assert_equal(7, sharder.total) | ||
end | ||
|
||
def test_it_raises_an_exception_on_an_invalid_format | ||
error = assert_raises(ArgumentError) do | ||
Sharder.from_argv("a/9") | ||
end | ||
|
||
assert_includes(error.message, "value must be in the form INDEX/TOTAL") | ||
end | ||
|
||
def test_it_raises_an_exception_on_an_invalid_index_value | ||
error = assert_raises(ArgumentError) do | ||
Sharder.from_argv("9/5") | ||
end | ||
|
||
assert_includes(error.message, "index must be > 0 and <= 5") | ||
end | ||
|
||
def test_it_raises_an_exception_on_an_invalid_total_value | ||
error = assert_raises(ArgumentError) do | ||
Sharder.from_argv("1/0") | ||
end | ||
|
||
assert_includes(error.message, "total shards must be a number greater than 0") | ||
end | ||
|
||
def test_it_has_a_default_hardcoded_seed | ||
sharder = Sharder.from_argv("1/2", env: {}) | ||
assert_equal(123_456_789, sharder.seed) | ||
end | ||
|
||
def test_it_derives_a_seed_value_from_the_github_actions_env_var | ||
sharder = Sharder.from_argv("1/2", env: { "GITHUB_SHA" => "b94d6d86a2281d690eafd7bb3282c7032999e85f" }) | ||
assert_equal(3_906_982_861_516_061_026, sharder.seed) | ||
end | ||
|
||
def test_it_derives_a_seed_value_from_the_circle_ci_env_var | ||
sharder = Sharder.from_argv("1/2", env: { "CIRCLE_SHA1" => "189733eff795bd1ea7c586a5234a717f82e58b64" }) | ||
assert_equal(7_378_359_859_579_271_217, sharder.seed) | ||
end | ||
|
||
def test_for_a_given_seed_it_generates_a_stable_shuffled_result | ||
sharder = Sharder.new(index: 1, total: 2, seed: 678) | ||
result = sharder.shard(%w[a b c d e f]) | ||
|
||
assert_equal(%w[f e c], result) | ||
end | ||
|
||
def test_it_divdes_items_into_roughly_equally_sized_shards | ||
all = %w[a b c d e f g h i j k l m n o p q r] | ||
shards = (1..4).map do |index| | ||
Sharder.new(index:, total: 4).shard(all) | ||
end | ||
|
||
shards.each do |shard| | ||
assert_includes [4, 5], shard.length | ||
end | ||
|
||
assert_equal all, shards.flatten.sort | ||
end | ||
end | ||
end |