// Copyright 2010 The Bazel Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.testing.junit.runner.sharding; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.runner.Description; import org.junit.runner.manipulation.Filter; /** * Implements the round-robin sharding strategy. * * <p>This is done by equally dividing up the tests across all the shards * Each test is numbered and the test number is modded with the number of * shards and checked against the shard number to see whether it should run * on a particular shard. * * <p>Equals and hashCode implementations are not necessary for correct * sharding, but are done so that this filter can be compared in tests. */ public final class RoundRobinShardingFilter extends Filter { // VisibleForTesting final Map<Description, Integer> testToShardMap; // VisibleForTesting final int shardIndex; // VisibleForTesting final int totalShards; public RoundRobinShardingFilter(Collection<Description> testDescriptions, int shardIndex, int totalShards) { if (shardIndex < 0 || totalShards <= shardIndex) { throw new IllegalArgumentException(); } this.testToShardMap = buildTestToShardMap(testDescriptions); this.shardIndex = shardIndex; this.totalShards = totalShards; } /** * Given a list of test case descriptions, returns a mapping from each * to its index in the list. */ private static Map<Description, Integer> buildTestToShardMap( Collection<Description> testDescriptions) { Map<Description, Integer> map = new HashMap<>(); // Sorting this list is incredibly important to correctness. Otherwise, // "shuffled" suites would break the sharding protocol. List<Description> sortedDescriptions = new ArrayList<>(testDescriptions); Collections.sort(sortedDescriptions, new DescriptionComparator()); // If we get two descriptions that are equal, the shard number for the second // one will overwrite the shard number for the first. Thus they'll run on the // same shard. int index = 0; for (Description description : sortedDescriptions) { if (!description.isTest()) { throw new IllegalArgumentException("Test suite should not be included in the set of tests " + "to shard: " + description.getDisplayName()); } map.put(description, index); index++; } return Collections.unmodifiableMap(map); } @Override public boolean shouldRun(Description description) { if (description.isSuite()) { return true; } Integer testNumber = testToShardMap.get(description); if (testNumber == null) { throw new IllegalArgumentException("This filter keeps a mapping from each test " + "description to a shard, and the given description was not passed in when " + "filter was constructed: " + description); } return (testNumber % totalShards) == shardIndex; } @Override public String describe() { return "round robin sharding filter"; } // VisibleForTesting static class DescriptionComparator implements Comparator<Description> { @Override public int compare(Description d1, Description d2) { return d1.getDisplayName().compareTo(d2.getDisplayName()); } } }