You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by il...@apache.org on 2020/11/24 18:08:11 UTC

[lucene-solr] branch jira/solr-15004 updated: Reformat to correct tab length (+ some other IntelliJ magic)

This is an automated email from the ASF dual-hosted git repository.

ilan pushed a commit to branch jira/solr-15004
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/jira/solr-15004 by this push:
     new 8b06bbb  Reformat to correct tab length (+ some other IntelliJ magic)
8b06bbb is described below

commit 8b06bbbd35c4dd8049a9a59a221a8098047b2551
Author: Ilan Ginzburg <ig...@salesforce.com>
AuthorDate: Tue Nov 24 19:05:55 2020 +0100

    Reformat to correct tab length (+ some other IntelliJ magic)
---
 .../impl/AffinityPlacementFactoryTest.java         | 610 ++++++++++-----------
 .../placement/impl/AttributeFetcherForTest.java    | 130 ++---
 .../solr/cluster/placement/impl/Builders.java      | 516 ++++++++---------
 .../placement/impl/ClusterAbstractionsForTest.java | 600 ++++++++++----------
 .../cluster/placement/impl/PluginTestHelper.java   | 116 ++--
 5 files changed, 996 insertions(+), 976 deletions(-)

diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/impl/AffinityPlacementFactoryTest.java b/solr/core/src/test/org/apache/solr/cluster/placement/impl/AffinityPlacementFactoryTest.java
index 5a6911e..3981b09 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/impl/AffinityPlacementFactoryTest.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/impl/AffinityPlacementFactoryTest.java
@@ -41,332 +41,332 @@ import java.util.stream.StreamSupport;
  * Unit test for {@link AffinityPlacementFactory}
  */
 public class AffinityPlacementFactoryTest extends Assert {
-    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
-    private static PlacementPlugin plugin;
-
-    @BeforeClass
-    public static void setupPlugin() {
-        PlacementPluginConfig config = PlacementPluginConfigImpl.createConfigFromProperties(
-                Map.of("minimalFreeDiskGB", 10L, "deprioritizedFreeDiskGB", 50L));
-        plugin = new AffinityPlacementFactory().createPluginInstance(config);
-    }
-
-    @Test
-    public void testBasicPlacementNewCollection() throws Exception {
-        testBasicPlacementInternal(false);
-    }
-
-    @Test
-    public void testBasicPlacementExistingCollection() throws Exception {
-        testBasicPlacementInternal(true);
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static PlacementPlugin plugin;
+
+  @BeforeClass
+  public static void setupPlugin() {
+    PlacementPluginConfig config = PlacementPluginConfigImpl.createConfigFromProperties(
+        Map.of("minimalFreeDiskGB", 10L, "deprioritizedFreeDiskGB", 50L));
+    plugin = new AffinityPlacementFactory().createPluginInstance(config);
+  }
+
+  @Test
+  public void testBasicPlacementNewCollection() throws Exception {
+    testBasicPlacementInternal(false);
+  }
+
+  @Test
+  public void testBasicPlacementExistingCollection() throws Exception {
+    testBasicPlacementInternal(true);
+  }
+
+  @Test
+  public void testBasicPlacementNewCollection2() throws Exception {
+    testBasicInternal2(false);
+  }
+
+  @Test
+  public void testBasicPlacementExistingCollection2() throws Exception {
+    testBasicInternal2(true);
+  }
+
+  private void testBasicInternal2(boolean hasExistingCollection) throws Exception {
+    String collectionName = "testCollection";
+
+    Builders.ClusterBuilder clusterBuilder = Builders.newClusterBuilder().initializeNodes(2);
+    LinkedList<Builders.NodeBuilder> nodeBuilders = clusterBuilder.getNodeBuilders();
+    nodeBuilders.get(0).setCoreCount(1).setFreeDiskGB(100L);
+    nodeBuilders.get(1).setCoreCount(10).setFreeDiskGB(100L);
+
+    Builders.CollectionBuilder collectionBuilder = Builders.newCollectionBuilder(collectionName);
+
+    if (hasExistingCollection) {
+      // Existing collection has replicas for its shards and is visible in the cluster state
+      collectionBuilder.initializeShardsReplicas(1, 1, 0, 0, nodeBuilders);
+      clusterBuilder.addCollection(collectionBuilder);
+    } else {
+      // New collection to create has the shards defined but no replicas and is not present in cluster state
+      collectionBuilder.initializeShardsReplicas(1, 0, 0, 0, List.of());
     }
 
-    @Test
-    public void testBasicPlacementNewCollection2() throws Exception {
-        testBasicInternal2(false);
+    Cluster cluster = clusterBuilder.build();
+    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
+
+    SolrCollection solrCollection = collectionBuilder.build();
+    List<Node> liveNodes = clusterBuilder.buildLiveNodes();
+
+    // Place a new replica for the (only) existing shard of the collection
+    PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
+        Set.of(solrCollection.shards().iterator().next().getShardName()), new HashSet<>(liveNodes),
+        1, 0, 0);
+
+    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, new PlacementPlanFactoryImpl());
+
+    assertEquals(1, pp.getReplicaPlacements().size());
+    ReplicaPlacement rp = pp.getReplicaPlacements().iterator().next();
+    assertEquals(hasExistingCollection ? liveNodes.get(1) : liveNodes.get(0), rp.getNode());
+  }
+
+  /**
+   * When this test places a replica for a new collection, it should pick the node with less cores.<p>
+   * <p>
+   * When it places a replica for an existing collection, it should pick the node with more cores that doesn't already have a replica for the shard.
+   */
+  private void testBasicPlacementInternal(boolean hasExistingCollection) throws Exception {
+    String collectionName = "testCollection";
+
+    Node node1 = new ClusterAbstractionsForTest.NodeImpl("node1");
+    Node node2 = new ClusterAbstractionsForTest.NodeImpl("node2");
+    Set<Node> liveNodes = Set.of(node1, node2);
+
+    ClusterAbstractionsForTest.SolrCollectionImpl solrCollection;
+    // Make sure new collections are not visible in the cluster state and existing ones are
+    final Map<String, SolrCollection> clusterCollections;
+    if (hasExistingCollection) {
+      // An existing collection with a single replica on node 1. Note that new collections already exist by the time the plugin is called, but are empty
+      solrCollection = PluginTestHelper.createCollection(collectionName, Map.of(), 1, 1, 0, 0, Set.of(node1));
+      clusterCollections = Map.of(solrCollection.getName(), solrCollection);
+    } else {
+      // A new collection has the shards defined ok but no replicas
+      solrCollection = PluginTestHelper.createCollection(collectionName, Map.of(), 1, 0, 0, 0, Set.of());
+      clusterCollections = Map.of();
     }
 
-    @Test
-    public void testBasicPlacementExistingCollection2() throws Exception {
-        testBasicInternal2(true);
+    Cluster cluster = new ClusterAbstractionsForTest.ClusterImpl(liveNodes, clusterCollections);
+    // Place a new replica for the (only) existing shard of the collection
+    PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection, Set.of(solrCollection.shards().iterator().next().getShardName()), liveNodes, 1, 0, 0);
+    // More cores on node2
+    Map<Node, Integer> nodeToCoreCount = Map.of(node1, 1, node2, 10);
+    // A lot of free disk on the two nodes
+    final Map<Node, Long> nodeToFreeDisk = Map.of(node1, 100L, node2, 100L);
+    AttributeValues attributeValues = new AttributeValuesImpl(nodeToCoreCount, Map.of(), nodeToFreeDisk, Map.of(), Map.of(), Map.of(), Map.of(), Map.of());
+    AttributeFetcher attributeFetcher = new AttributeFetcherForTest(attributeValues);
+    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
+
+    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+
+
+    assertEquals(1, pp.getReplicaPlacements().size());
+    ReplicaPlacement rp = pp.getReplicaPlacements().iterator().next();
+    assertEquals(hasExistingCollection ? node2 : node1, rp.getNode());
+  }
+
+  @Test
+  public void testAvailabilityZones() throws Exception {
+    String collectionName = "testCollection";
+    int NUM_NODES = 6;
+    Builders.ClusterBuilder clusterBuilder = Builders.newClusterBuilder().initializeNodes(NUM_NODES);
+    for (int i = 0; i < NUM_NODES; i++) {
+      Builders.NodeBuilder nodeBuilder = clusterBuilder.getNodeBuilders().get(i);
+      nodeBuilder.setCoreCount(0);
+      nodeBuilder.setFreeDiskGB(100L);
+      if (i < NUM_NODES / 2) {
+        nodeBuilder.setSysprop(AffinityPlacementFactory.AVAILABILITY_ZONE_SYSPROP, "az1");
+      } else {
+        nodeBuilder.setSysprop(AffinityPlacementFactory.AVAILABILITY_ZONE_SYSPROP, "az2");
+      }
     }
 
-    private void testBasicInternal2(boolean hasExistingCollection) throws Exception {
-        String collectionName = "testCollection";
-
-        Builders.ClusterBuilder clusterBuilder = Builders.newClusterBuilder().initializeNodes(2);
-        LinkedList<Builders.NodeBuilder> nodeBuilders = clusterBuilder.getNodeBuilders();
-        nodeBuilders.get(0).setCoreCount(1).setFreeDiskGB(100L);
-        nodeBuilders.get(1).setCoreCount(10).setFreeDiskGB(100L);
-
-        Builders.CollectionBuilder collectionBuilder = Builders.newCollectionBuilder(collectionName);
-
-        if (hasExistingCollection) {
-            // Existing collection has replicas for its shards and is visible in the cluster state
-            collectionBuilder.initializeShardsReplicas(1, 1, 0, 0, nodeBuilders);
-            clusterBuilder.addCollection(collectionBuilder);
-        } else {
-            // New collection to create has the shards defined but no replicas and is not present in cluster state
-            collectionBuilder.initializeShardsReplicas(1, 0, 0, 0, List.of());
-        }
-
-        Cluster cluster = clusterBuilder.build();
-        AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-
-        SolrCollection solrCollection = collectionBuilder.build();
-        List<Node> liveNodes = clusterBuilder.buildLiveNodes();
+    Builders.CollectionBuilder collectionBuilder = Builders.newCollectionBuilder(collectionName);
+    collectionBuilder.initializeShardsReplicas(2, 0, 0, 0, clusterBuilder.getNodeBuilders());
+    clusterBuilder.addCollection(collectionBuilder);
 
-        // Place a new replica for the (only) existing shard of the collection
-        PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
-                Set.of(solrCollection.shards().iterator().next().getShardName()), new HashSet<>(liveNodes),
-                1, 0, 0);
+    Cluster cluster = clusterBuilder.build();
 
-        PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, new PlacementPlanFactoryImpl());
+    SolrCollection solrCollection = cluster.getCollection(collectionName);
 
-        assertEquals(1, pp.getReplicaPlacements().size());
-        ReplicaPlacement rp = pp.getReplicaPlacements().iterator().next();
-        assertEquals(hasExistingCollection ? liveNodes.get(1) : liveNodes.get(0), rp.getNode());
-    }
-
-    /**
-     * When this test places a replica for a new collection, it should pick the node with less cores.<p>
-     *
-     * When it places a replica for an existing collection, it should pick the node with more cores that doesn't already have a replica for the shard.
-     */
-    private void testBasicPlacementInternal(boolean hasExistingCollection) throws Exception {
-        String collectionName = "testCollection";
-
-        Node node1 = new ClusterAbstractionsForTest.NodeImpl("node1");
-        Node node2 = new ClusterAbstractionsForTest.NodeImpl("node2");
-        Set<Node> liveNodes = Set.of(node1, node2);
-
-        ClusterAbstractionsForTest.SolrCollectionImpl solrCollection;
-        // Make sure new collections are not visible in the cluster state and existing ones are
-        final Map<String, SolrCollection> clusterCollections;
-        if (hasExistingCollection) {
-            // An existing collection with a single replica on node 1. Note that new collections already exist by the time the plugin is called, but are empty
-            solrCollection = PluginTestHelper.createCollection(collectionName, Map.of(), 1, 1, 0, 0, Set.of(node1));
-            clusterCollections = Map.of(solrCollection.getName(), solrCollection);
-        } else {
-            // A new collection has the shards defined ok but no replicas
-            solrCollection = PluginTestHelper.createCollection(collectionName, Map.of(), 1, 0, 0, 0, Set.of());
-            clusterCollections = Map.of();
-        }
-
-        Cluster cluster = new ClusterAbstractionsForTest.ClusterImpl(liveNodes, clusterCollections);
-        // Place a new replica for the (only) existing shard of the collection
-        PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection, Set.of(solrCollection.shards().iterator().next().getShardName()), liveNodes, 1, 0, 0);
-        // More cores on node2
-        Map<Node, Integer> nodeToCoreCount = Map.of(node1, 1, node2, 10);
-        // A lot of free disk on the two nodes
-        final Map<Node, Long> nodeToFreeDisk = Map.of(node1, 100L, node2, 100L);
-        AttributeValues attributeValues = new AttributeValuesImpl(nodeToCoreCount, Map.of(), nodeToFreeDisk, Map.of(), Map.of(), Map.of(), Map.of(), Map.of());
-        AttributeFetcher attributeFetcher = new AttributeFetcherForTest(attributeValues);
-        PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-
-        PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
-
-
-        assertEquals(1, pp.getReplicaPlacements().size());
-        ReplicaPlacement rp = pp.getReplicaPlacements().iterator().next();
-        assertEquals(hasExistingCollection ? node2 : node1, rp.getNode());
-    }
+    PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
+        StreamSupport.stream(solrCollection.shards().spliterator(), false)
+            .map(Shard::getShardName).collect(Collectors.toSet()),
+        cluster.getLiveNodes(), 2, 2, 2);
 
-    @Test
-    public void testAvailabilityZones() throws Exception {
-        String collectionName = "testCollection";
-        int NUM_NODES = 6;
-        Builders.ClusterBuilder clusterBuilder = Builders.newClusterBuilder().initializeNodes(NUM_NODES);
-        for (int i = 0; i < NUM_NODES; i++) {
-            Builders.NodeBuilder nodeBuilder = clusterBuilder.getNodeBuilders().get(i);
-            nodeBuilder.setCoreCount(0);
-            nodeBuilder.setFreeDiskGB(100L);
-            if (i < NUM_NODES / 2) {
-                nodeBuilder.setSysprop(AffinityPlacementFactory.AVAILABILITY_ZONE_SYSPROP, "az1");
-            } else {
-                nodeBuilder.setSysprop(AffinityPlacementFactory.AVAILABILITY_ZONE_SYSPROP, "az2");
-            }
-        }
-
-        Builders.CollectionBuilder collectionBuilder = Builders.newCollectionBuilder(collectionName);
-        collectionBuilder.initializeShardsReplicas(2, 0, 0, 0, clusterBuilder.getNodeBuilders());
-        clusterBuilder.addCollection(collectionBuilder);
-
-        Cluster cluster = clusterBuilder.build();
-
-        SolrCollection solrCollection = cluster.getCollection(collectionName);
-
-        PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
-            StreamSupport.stream(solrCollection.shards().spliterator(), false)
-                 .map(Shard::getShardName).collect(Collectors.toSet()),
-            cluster.getLiveNodes(), 2, 2, 2);
-
-        PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-        AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-        PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
-        // 2 shards, 6 replicas
-        assertEquals(12, pp.getReplicaPlacements().size());
+    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
+    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
+    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+    // 2 shards, 6 replicas
+    assertEquals(12, pp.getReplicaPlacements().size());
 //        List<ReplicaPlacement> placements = new ArrayList<>(pp.getReplicaPlacements());
 //        Collections.sort(placements, Comparator
 //            .comparing((ReplicaPlacement p) -> p.getNode().getName())
 //            .thenComparing((ReplicaPlacement p) -> p.getShardName())
 //            .thenComparing((ReplicaPlacement p) -> p.getReplicaType())
 //        );
-        // shard -> AZ -> replica count
-        Map<Replica.ReplicaType, Map<String, Map<String, AtomicInteger>>> replicas = new HashMap<>();
-        AttributeValues attributeValues = attributeFetcher.fetchAttributes();
-        for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
-            Optional<String> azOptional = attributeValues.getSystemProperty(rp.getNode(), AffinityPlacementFactory.AVAILABILITY_ZONE_SYSPROP);
-            if (!azOptional.isPresent()) {
-                fail("missing AZ sysprop for node " + rp.getNode());
-            }
-            String az = azOptional.get();
-            replicas.computeIfAbsent(rp.getReplicaType(), type -> new HashMap<>())
-                .computeIfAbsent(rp.getShardName(), shard -> new HashMap<>())
-                .computeIfAbsent(az, zone -> new AtomicInteger()).incrementAndGet();
-        }
-        replicas.forEach((type, perTypeReplicas) -> {
-            perTypeReplicas.forEach((shard, azCounts) -> {
-                assertEquals("number of AZs", 2, azCounts.size());
-                azCounts.forEach((az, count) -> {
-                    assertTrue("too few replicas shard=" + shard + ", type=" + type + ", az=" + az,
-                        count.get() >= 1);
-                });
-            });
-        });
+    // shard -> AZ -> replica count
+    Map<Replica.ReplicaType, Map<String, Map<String, AtomicInteger>>> replicas = new HashMap<>();
+    AttributeValues attributeValues = attributeFetcher.fetchAttributes();
+    for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
+      Optional<String> azOptional = attributeValues.getSystemProperty(rp.getNode(), AffinityPlacementFactory.AVAILABILITY_ZONE_SYSPROP);
+      if (!azOptional.isPresent()) {
+        fail("missing AZ sysprop for node " + rp.getNode());
+      }
+      String az = azOptional.get();
+      replicas.computeIfAbsent(rp.getReplicaType(), type -> new HashMap<>())
+          .computeIfAbsent(rp.getShardName(), shard -> new HashMap<>())
+          .computeIfAbsent(az, zone -> new AtomicInteger()).incrementAndGet();
     }
-
-    @Test
-    public void testReplicaType() throws Exception {
-        String collectionName = "testCollection";
-        int NUM_NODES = 6;
-        Builders.ClusterBuilder clusterBuilder = Builders.newClusterBuilder().initializeNodes(NUM_NODES);
-        for (int i = 0; i < NUM_NODES; i++) {
-            Builders.NodeBuilder nodeBuilder = clusterBuilder.getNodeBuilders().get(i);
-            nodeBuilder.setCoreCount(0);
-            nodeBuilder.setFreeDiskGB(100L);
-            if (i < NUM_NODES / 2) {
-                nodeBuilder.setSysprop(AffinityPlacementFactory.REPLICA_TYPE_SYSPROP, "Nrt,Tlog");
-                nodeBuilder.setSysprop("group", "one");
-            } else {
-                nodeBuilder.setSysprop(AffinityPlacementFactory.REPLICA_TYPE_SYSPROP, "Pull, foobar");
-                nodeBuilder.setSysprop("group", "two");
-            }
-        }
-
-        Builders.CollectionBuilder collectionBuilder = Builders.newCollectionBuilder(collectionName);
-        collectionBuilder.initializeShardsReplicas(2, 0, 0, 0, clusterBuilder.getNodeBuilders());
-        clusterBuilder.addCollection(collectionBuilder);
-
-        Cluster cluster = clusterBuilder.build();
-
-        SolrCollection solrCollection = cluster.getCollection(collectionName);
-
-        PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
-            StreamSupport.stream(solrCollection.shards().spliterator(), false)
-                .map(Shard::getShardName).collect(Collectors.toSet()),
-            cluster.getLiveNodes(), 2, 2, 2);
-
-        PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-        AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
-        PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
-        // 2 shards, 6 replicas
-        assertEquals(12, pp.getReplicaPlacements().size());
-        // shard -> group -> replica count
-        Map<Replica.ReplicaType, Map<String, Map<String, AtomicInteger>>> replicas = new HashMap<>();
-        AttributeValues attributeValues = attributeFetcher.fetchAttributes();
-        for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
-            Optional<String> groupOptional = attributeValues.getSystemProperty(rp.getNode(), "group");
-            if (!groupOptional.isPresent()) {
-                fail("missing group sysprop for node " + rp.getNode());
-            }
-            String group = groupOptional.get();
-            if (group.equals("one")) {
-                assertTrue("wrong replica type in group one",
-                    (rp.getReplicaType() == Replica.ReplicaType.NRT) || rp.getReplicaType() == Replica.ReplicaType.TLOG);
-            } else {
-                assertEquals("wrong replica type in group two", Replica.ReplicaType.PULL, rp.getReplicaType());
-            }
-            replicas.computeIfAbsent(rp.getReplicaType(), type -> new HashMap<>())
-                .computeIfAbsent(rp.getShardName(), shard -> new HashMap<>())
-                .computeIfAbsent(group, g -> new AtomicInteger()).incrementAndGet();
-        }
-        replicas.forEach((type, perTypeReplicas) -> {
-            perTypeReplicas.forEach((shard, groupCounts) -> {
-                assertEquals("number of groups", 1, groupCounts.size());
-                groupCounts.forEach((group, count) -> {
-                    assertTrue("too few replicas shard=" + shard + ", type=" + type + ", group=" + group,
-                        count.get() >= 1);
-                });
-            });
+    replicas.forEach((type, perTypeReplicas) -> {
+      perTypeReplicas.forEach((shard, azCounts) -> {
+        assertEquals("number of AZs", 2, azCounts.size());
+        azCounts.forEach((az, count) -> {
+          assertTrue("too few replicas shard=" + shard + ", type=" + type + ", az=" + az,
+              count.get() >= 1);
         });
-
+      });
+    });
+  }
+
+  @Test
+  public void testReplicaType() throws Exception {
+    String collectionName = "testCollection";
+    int NUM_NODES = 6;
+    Builders.ClusterBuilder clusterBuilder = Builders.newClusterBuilder().initializeNodes(NUM_NODES);
+    for (int i = 0; i < NUM_NODES; i++) {
+      Builders.NodeBuilder nodeBuilder = clusterBuilder.getNodeBuilders().get(i);
+      nodeBuilder.setCoreCount(0);
+      nodeBuilder.setFreeDiskGB(100L);
+      if (i < NUM_NODES / 2) {
+        nodeBuilder.setSysprop(AffinityPlacementFactory.REPLICA_TYPE_SYSPROP, "Nrt,Tlog");
+        nodeBuilder.setSysprop("group", "one");
+      } else {
+        nodeBuilder.setSysprop(AffinityPlacementFactory.REPLICA_TYPE_SYSPROP, "Pull, foobar");
+        nodeBuilder.setSysprop("group", "two");
+      }
     }
 
-    @Test
-    //@Ignore
-    public void testScalability() throws Exception {
-        log.info("==== numNodes ====");
-        runTestScalability(1000, 100, 40, 40, 20);
-        runTestScalability(2000, 100, 40, 40, 20);
-        runTestScalability(5000, 100, 40, 40, 20);
-        runTestScalability(10000, 100, 40, 40, 20);
-        runTestScalability(20000, 100, 40, 40, 20);
-        log.info("==== numShards ====");
-        runTestScalability(5000, 100, 40, 40, 20);
-        runTestScalability(5000, 200, 40, 40, 20);
-        runTestScalability(5000, 500, 40, 40, 20);
-        runTestScalability(5000, 1000, 40, 40, 20);
-        runTestScalability(5000, 2000, 40, 40, 20);
-        log.info("==== numReplicas ====");
-        runTestScalability(5000, 100, 100, 0, 0);
-        runTestScalability(5000, 100, 200, 0, 0);
-        runTestScalability(5000, 100, 500, 0, 0);
-        runTestScalability(5000, 100, 1000, 0, 0);
-        runTestScalability(5000, 100, 2000, 0, 0);
+    Builders.CollectionBuilder collectionBuilder = Builders.newCollectionBuilder(collectionName);
+    collectionBuilder.initializeShardsReplicas(2, 0, 0, 0, clusterBuilder.getNodeBuilders());
+    clusterBuilder.addCollection(collectionBuilder);
+
+    Cluster cluster = clusterBuilder.build();
+
+    SolrCollection solrCollection = cluster.getCollection(collectionName);
+
+    PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
+        StreamSupport.stream(solrCollection.shards().spliterator(), false)
+            .map(Shard::getShardName).collect(Collectors.toSet()),
+        cluster.getLiveNodes(), 2, 2, 2);
+
+    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
+    AttributeFetcher attributeFetcher = clusterBuilder.buildAttributeFetcher();
+    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+    // 2 shards, 6 replicas
+    assertEquals(12, pp.getReplicaPlacements().size());
+    // shard -> group -> replica count
+    Map<Replica.ReplicaType, Map<String, Map<String, AtomicInteger>>> replicas = new HashMap<>();
+    AttributeValues attributeValues = attributeFetcher.fetchAttributes();
+    for (ReplicaPlacement rp : pp.getReplicaPlacements()) {
+      Optional<String> groupOptional = attributeValues.getSystemProperty(rp.getNode(), "group");
+      if (!groupOptional.isPresent()) {
+        fail("missing group sysprop for node " + rp.getNode());
+      }
+      String group = groupOptional.get();
+      if (group.equals("one")) {
+        assertTrue("wrong replica type in group one",
+            (rp.getReplicaType() == Replica.ReplicaType.NRT) || rp.getReplicaType() == Replica.ReplicaType.TLOG);
+      } else {
+        assertEquals("wrong replica type in group two", Replica.ReplicaType.PULL, rp.getReplicaType());
+      }
+      replicas.computeIfAbsent(rp.getReplicaType(), type -> new HashMap<>())
+          .computeIfAbsent(rp.getShardName(), shard -> new HashMap<>())
+          .computeIfAbsent(group, g -> new AtomicInteger()).incrementAndGet();
     }
-
-    private void runTestScalability(int numNodes, int numShards,
-                                    int nrtReplicas, int tlogReplicas,
-                                    int pullReplicas) throws Exception {
-
-        int REPLICAS_PER_SHARD = nrtReplicas + tlogReplicas + pullReplicas;
-        int TOTAL_REPLICAS = numShards * REPLICAS_PER_SHARD;
-
-        String collectionName = "testCollection";
-
-        final Set<Node> liveNodes = new HashSet<>();
-        final Map<Node, Long> nodeToFreeDisk = new HashMap<>();
-        final Map<Node, Integer> nodeToCoreCount = new HashMap<>();
-        for (int i = 0; i < numNodes; i++) {
-            Node node = new ClusterAbstractionsForTest.NodeImpl("node_" + i);
-            liveNodes.add(node);
-            nodeToFreeDisk.put(node, Long.valueOf(numNodes));
-            nodeToCoreCount.put(node, 0);
-        }
-        ClusterAbstractionsForTest.SolrCollectionImpl solrCollection =
-            PluginTestHelper.createCollection(collectionName, Map.of(), numShards, 0, 0, 0, Set.of());
-
-        Cluster cluster = new ClusterAbstractionsForTest.ClusterImpl(liveNodes, Map.of());
-        PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
-            // XXX awkward!
-            // StreamSupport.stream(solrCollection.shards().spliterator(), false)
-            //     .map(Shard::getShardName).collect(Collectors.toSet()),
-            solrCollection.getShardNames(),
-            liveNodes, nrtReplicas, tlogReplicas, pullReplicas);
-
-        AttributeValues attributeValues = new AttributeValuesImpl(nodeToCoreCount, Map.of(), nodeToFreeDisk, Map.of(), Map.of(), Map.of(), Map.of(), Map.of());
-        AttributeFetcher attributeFetcher = new AttributeFetcherForTest(attributeValues);
-        PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
-
-        long start = System.nanoTime();
-        PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
-        long end = System.nanoTime();
-        log.info("ComputePlacement: {} nodes, {} shards, {} total replicas, elapsed time {} ms.", numNodes, numShards, TOTAL_REPLICAS, TimeUnit.NANOSECONDS.toMillis(end - start)); //nowarn
-        assertEquals("incorrect number of calculated placements", TOTAL_REPLICAS,
-            pp.getReplicaPlacements().size());
-        // check that replicas are correctly placed
-        Map<Node, AtomicInteger> replicasPerNode = new HashMap<>();
-        Map<Node, Set<String>> shardsPerNode = new HashMap<>();
-        Map<String, AtomicInteger> replicasPerShard = new HashMap<>();
-        Map<Replica.ReplicaType, AtomicInteger> replicasByType = new HashMap<>();
-        for (ReplicaPlacement placement : pp.getReplicaPlacements()) {
-            replicasPerNode.computeIfAbsent(placement.getNode(), n -> new AtomicInteger()).incrementAndGet();
-            shardsPerNode.computeIfAbsent(placement.getNode(), n -> new HashSet<>()).add(placement.getShardName());
-            replicasByType.computeIfAbsent(placement.getReplicaType(), t -> new AtomicInteger()).incrementAndGet();
-            replicasPerShard.computeIfAbsent(placement.getShardName(), s -> new AtomicInteger()).incrementAndGet();
-        }
-        int perNode = TOTAL_REPLICAS > numNodes ? TOTAL_REPLICAS / numNodes : 1;
-        replicasPerNode.forEach((node, count) -> {
-            assertEquals(count.get(), perNode);
-        });
-        shardsPerNode.forEach((node, names) -> {
-            assertEquals(names.size(), perNode);
-        });
-
-        replicasPerShard.forEach((shard, count) -> {
-            assertEquals(count.get(), REPLICAS_PER_SHARD);
+    replicas.forEach((type, perTypeReplicas) -> {
+      perTypeReplicas.forEach((shard, groupCounts) -> {
+        assertEquals("number of groups", 1, groupCounts.size());
+        groupCounts.forEach((group, count) -> {
+          assertTrue("too few replicas shard=" + shard + ", type=" + type + ", group=" + group,
+              count.get() >= 1);
         });
+      });
+    });
+
+  }
+
+  @Test
+  //@Ignore
+  public void testScalability() throws Exception {
+    log.info("==== numNodes ====");
+    runTestScalability(1000, 100, 40, 40, 20);
+    runTestScalability(2000, 100, 40, 40, 20);
+    runTestScalability(5000, 100, 40, 40, 20);
+    runTestScalability(10000, 100, 40, 40, 20);
+    runTestScalability(20000, 100, 40, 40, 20);
+    log.info("==== numShards ====");
+    runTestScalability(5000, 100, 40, 40, 20);
+    runTestScalability(5000, 200, 40, 40, 20);
+    runTestScalability(5000, 500, 40, 40, 20);
+    runTestScalability(5000, 1000, 40, 40, 20);
+    runTestScalability(5000, 2000, 40, 40, 20);
+    log.info("==== numReplicas ====");
+    runTestScalability(5000, 100, 100, 0, 0);
+    runTestScalability(5000, 100, 200, 0, 0);
+    runTestScalability(5000, 100, 500, 0, 0);
+    runTestScalability(5000, 100, 1000, 0, 0);
+    runTestScalability(5000, 100, 2000, 0, 0);
+  }
+
+  private void runTestScalability(int numNodes, int numShards,
+                                  int nrtReplicas, int tlogReplicas,
+                                  int pullReplicas) throws Exception {
+
+    int REPLICAS_PER_SHARD = nrtReplicas + tlogReplicas + pullReplicas;
+    int TOTAL_REPLICAS = numShards * REPLICAS_PER_SHARD;
+
+    String collectionName = "testCollection";
+
+    final Set<Node> liveNodes = new HashSet<>();
+    final Map<Node, Long> nodeToFreeDisk = new HashMap<>();
+    final Map<Node, Integer> nodeToCoreCount = new HashMap<>();
+    for (int i = 0; i < numNodes; i++) {
+      Node node = new ClusterAbstractionsForTest.NodeImpl("node_" + i);
+      liveNodes.add(node);
+      nodeToFreeDisk.put(node, Long.valueOf(numNodes));
+      nodeToCoreCount.put(node, 0);
+    }
+    ClusterAbstractionsForTest.SolrCollectionImpl solrCollection =
+        PluginTestHelper.createCollection(collectionName, Map.of(), numShards, 0, 0, 0, Set.of());
+
+    Cluster cluster = new ClusterAbstractionsForTest.ClusterImpl(liveNodes, Map.of());
+    PlacementRequestImpl placementRequest = new PlacementRequestImpl(solrCollection,
+        // XXX awkward!
+        // StreamSupport.stream(solrCollection.shards().spliterator(), false)
+        //     .map(Shard::getShardName).collect(Collectors.toSet()),
+        solrCollection.getShardNames(),
+        liveNodes, nrtReplicas, tlogReplicas, pullReplicas);
+
+    AttributeValues attributeValues = new AttributeValuesImpl(nodeToCoreCount, Map.of(), nodeToFreeDisk, Map.of(), Map.of(), Map.of(), Map.of(), Map.of());
+    AttributeFetcher attributeFetcher = new AttributeFetcherForTest(attributeValues);
+    PlacementPlanFactory placementPlanFactory = new PlacementPlanFactoryImpl();
+
+    long start = System.nanoTime();
+    PlacementPlan pp = plugin.computePlacement(cluster, placementRequest, attributeFetcher, placementPlanFactory);
+    long end = System.nanoTime();
+    log.info("ComputePlacement: {} nodes, {} shards, {} total replicas, elapsed time {} ms.", numNodes, numShards, TOTAL_REPLICAS, TimeUnit.NANOSECONDS.toMillis(end - start)); //nowarn
+    assertEquals("incorrect number of calculated placements", TOTAL_REPLICAS,
+        pp.getReplicaPlacements().size());
+    // check that replicas are correctly placed
+    Map<Node, AtomicInteger> replicasPerNode = new HashMap<>();
+    Map<Node, Set<String>> shardsPerNode = new HashMap<>();
+    Map<String, AtomicInteger> replicasPerShard = new HashMap<>();
+    Map<Replica.ReplicaType, AtomicInteger> replicasByType = new HashMap<>();
+    for (ReplicaPlacement placement : pp.getReplicaPlacements()) {
+      replicasPerNode.computeIfAbsent(placement.getNode(), n -> new AtomicInteger()).incrementAndGet();
+      shardsPerNode.computeIfAbsent(placement.getNode(), n -> new HashSet<>()).add(placement.getShardName());
+      replicasByType.computeIfAbsent(placement.getReplicaType(), t -> new AtomicInteger()).incrementAndGet();
+      replicasPerShard.computeIfAbsent(placement.getShardName(), s -> new AtomicInteger()).incrementAndGet();
     }
+    int perNode = TOTAL_REPLICAS > numNodes ? TOTAL_REPLICAS / numNodes : 1;
+    replicasPerNode.forEach((node, count) -> {
+      assertEquals(count.get(), perNode);
+    });
+    shardsPerNode.forEach((node, names) -> {
+      assertEquals(names.size(), perNode);
+    });
+
+    replicasPerShard.forEach((shard, count) -> {
+      assertEquals(count.get(), REPLICAS_PER_SHARD);
+    });
+  }
 }
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/impl/AttributeFetcherForTest.java b/solr/core/src/test/org/apache/solr/cluster/placement/impl/AttributeFetcherForTest.java
index 58005f7..f053d1b 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/impl/AttributeFetcherForTest.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/impl/AttributeFetcherForTest.java
@@ -25,69 +25,69 @@ import java.util.Set;
 
 public class AttributeFetcherForTest implements AttributeFetcher {
 
-    private final AttributeValues attributeValues;
-
-    AttributeFetcherForTest(AttributeValues attributeValues) {
-        this.attributeValues = attributeValues;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeCoreCount() {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeDiskType() {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeFreeDisk() {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeTotalDisk() {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeHeapUsage() {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeSystemLoadAverage() {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeSystemProperty(String name) {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestNodeEnvironmentVariable(String name) {
-        throw new UnsupportedOperationException("Not yet implemented...");
-    }
-
-    @Override
-    public AttributeFetcher requestNodeMetric(String metricName, NodeMetricRegistry registry) {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher fetchFrom(Set<Node> nodes) {
-        return this;
-    }
-
-    @Override
-    public AttributeFetcher requestMetric(String scope, String metricName) {
-        throw new UnsupportedOperationException("Not yet implemented...");
-    }
-
-    @Override
-    public AttributeValues fetchAttributes() {
-        return attributeValues;
-    }
+  private final AttributeValues attributeValues;
+
+  AttributeFetcherForTest(AttributeValues attributeValues) {
+    this.attributeValues = attributeValues;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeCoreCount() {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeDiskType() {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeFreeDisk() {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeTotalDisk() {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeHeapUsage() {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeSystemLoadAverage() {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeSystemProperty(String name) {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestNodeEnvironmentVariable(String name) {
+    throw new UnsupportedOperationException("Not yet implemented...");
+  }
+
+  @Override
+  public AttributeFetcher requestNodeMetric(String metricName, NodeMetricRegistry registry) {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher fetchFrom(Set<Node> nodes) {
+    return this;
+  }
+
+  @Override
+  public AttributeFetcher requestMetric(String scope, String metricName) {
+    throw new UnsupportedOperationException("Not yet implemented...");
+  }
+
+  @Override
+  public AttributeValues fetchAttributes() {
+    return attributeValues;
+  }
 }
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/impl/Builders.java b/solr/core/src/test/org/apache/solr/cluster/placement/impl/Builders.java
index bbbbc0e..1d964b2 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/impl/Builders.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/impl/Builders.java
@@ -12,318 +12,318 @@ import java.util.*;
  */
 public class Builders {
 
-    public static ClusterBuilder newClusterBuilder() {
-        return new ClusterBuilder();
+  public static ClusterBuilder newClusterBuilder() {
+    return new ClusterBuilder();
+  }
+
+  public static CollectionBuilder newCollectionBuilder(String collectionName) {
+    return new CollectionBuilder(collectionName);
+  }
+
+  static class ClusterBuilder {
+    private LinkedList<NodeBuilder> nodeBuilders = new LinkedList<>();
+    private LinkedList<CollectionBuilder> collectionBuilders = new LinkedList<>();
+
+    ClusterBuilder initializeNodes(int countNodes) {
+      nodeBuilders = new LinkedList<>();
+      for (int n = 0; n < countNodes; n++) {
+        nodeBuilders.add(new NodeBuilder().setNodeName("node" + n)); // Default name, can be changed
+      }
+      return this;
     }
 
-    public static CollectionBuilder newCollectionBuilder(String collectionName) {
-        return new CollectionBuilder(collectionName);
+    LinkedList<NodeBuilder> getNodeBuilders() {
+      return nodeBuilders;
     }
 
-    static class ClusterBuilder {
-        private LinkedList<NodeBuilder> nodeBuilders = new LinkedList<>();
-        private LinkedList<CollectionBuilder> collectionBuilders = new LinkedList<>();
-
-        ClusterBuilder initializeNodes(int countNodes) {
-            nodeBuilders = new LinkedList<>();
-            for (int n = 0; n < countNodes; n++) {
-                nodeBuilders.add(new NodeBuilder().setNodeName("node" + n)); // Default name, can be changed
-            }
-            return this;
-        }
+    ClusterBuilder addCollection(CollectionBuilder collectionBuilder) {
+      collectionBuilders.add(collectionBuilder);
+      return this;
+    }
 
-        LinkedList<NodeBuilder> getNodeBuilders() {
-            return nodeBuilders;
-        }
+    Cluster build() {
+      // TODO if converting all tests to use builders change ClusterImpl ctor to use list of nodes
+      return new ClusterAbstractionsForTest.ClusterImpl(new HashSet<>(buildLiveNodes()), buildClusterCollections());
+    }
 
-        ClusterBuilder addCollection(CollectionBuilder collectionBuilder) {
-            collectionBuilders.add(collectionBuilder);
-            return this;
-        }
+    List<Node> buildLiveNodes() {
+      List<Node> liveNodes = new LinkedList<>();
+      for (NodeBuilder nodeBuilder : nodeBuilders) {
+        liveNodes.add(nodeBuilder.build());
+      }
 
-        Cluster build() {
-            // TODO if converting all tests to use builders change ClusterImpl ctor to use list of nodes
-            return new ClusterAbstractionsForTest.ClusterImpl(new HashSet<>(buildLiveNodes()), buildClusterCollections());
-        }
+      return liveNodes;
+    }
 
-        List<Node> buildLiveNodes() {
-            List<Node> liveNodes = new LinkedList<>();
-            for (NodeBuilder nodeBuilder : nodeBuilders) {
-                liveNodes.add(nodeBuilder.build());
-            }
+    Map<String, SolrCollection> buildClusterCollections() {
+      Map<String, SolrCollection> clusterCollections = new LinkedHashMap<>();
+      for (CollectionBuilder collectionBuilder : collectionBuilders) {
+        SolrCollection solrCollection = collectionBuilder.build();
+        clusterCollections.put(solrCollection.getName(), solrCollection);
+      }
 
-            return liveNodes;
-        }
+      return clusterCollections;
+    }
 
-        Map<String, SolrCollection> buildClusterCollections() {
-            Map<String, SolrCollection> clusterCollections = new LinkedHashMap<>();
-            for (CollectionBuilder collectionBuilder : collectionBuilders) {
-                SolrCollection solrCollection = collectionBuilder.build();
-                clusterCollections.put(solrCollection.getName(), solrCollection);
-            }
+    AttributeFetcher buildAttributeFetcher() {
+      Map<Node, Integer> nodeToCoreCount = new HashMap<>();
+      Map<Node, Long> nodeToFreeDisk = new HashMap<>();
+      Map<String, Map<Node, String>> sysprops = new HashMap<>();
+      Map<String, Map<Node, Double>> metrics = new HashMap<>();
 
-            return clusterCollections;
-        }
+      // TODO And a few more missing and will be added...
 
-        AttributeFetcher buildAttributeFetcher() {
-            Map<Node, Integer> nodeToCoreCount = new HashMap<>();
-            Map<Node, Long> nodeToFreeDisk = new HashMap<>();
-            Map<String, Map<Node, String>> sysprops = new HashMap<>();
-            Map<String, Map<Node, Double>> metrics = new HashMap<>();
-
-            // TODO And a few more missing and will be added...
-
-            // Slight redoing of work twice (building Node instances) but let's favor readability over tricks (I could think
-            // of many) to reuse the nodes computed in build() or build the AttributeFetcher at the same time.
-            for (NodeBuilder nodeBuilder : nodeBuilders) {
-                Node node = nodeBuilder.build();
-
-                if (nodeBuilder.getCoreCount() != null) {
-                    nodeToCoreCount.put(node, nodeBuilder.getCoreCount());
-                }
-                if (nodeBuilder.getFreeDiskGB() != null) {
-                    nodeToFreeDisk.put(node, nodeBuilder.getFreeDiskGB());
-                }
-                if (nodeBuilder.getSysprops() != null) {
-                    nodeBuilder.getSysprops().forEach((name, value) -> {
-                        sysprops.computeIfAbsent(name, n -> new HashMap<>())
-                            .put(node, value);
-                    });
-                }
-                if (nodeBuilder.getMetrics() != null) {
-                    nodeBuilder.getMetrics().forEach((name, value) -> {
-                        metrics.computeIfAbsent(name, n -> new HashMap<>())
-                            .put(node, value);
-                    });
-                }
-            }
+      // Slight redoing of work twice (building Node instances) but let's favor readability over tricks (I could think
+      // of many) to reuse the nodes computed in build() or build the AttributeFetcher at the same time.
+      for (NodeBuilder nodeBuilder : nodeBuilders) {
+        Node node = nodeBuilder.build();
 
-            AttributeValues attributeValues = new AttributeValuesImpl(nodeToCoreCount, Map.of(), nodeToFreeDisk, Map.of(), Map.of(), Map.of(), sysprops, metrics);
-            return new AttributeFetcherForTest(attributeValues);
+        if (nodeBuilder.getCoreCount() != null) {
+          nodeToCoreCount.put(node, nodeBuilder.getCoreCount());
+        }
+        if (nodeBuilder.getFreeDiskGB() != null) {
+          nodeToFreeDisk.put(node, nodeBuilder.getFreeDiskGB());
+        }
+        if (nodeBuilder.getSysprops() != null) {
+          nodeBuilder.getSysprops().forEach((name, value) -> {
+            sysprops.computeIfAbsent(name, n -> new HashMap<>())
+                .put(node, value);
+          });
+        }
+        if (nodeBuilder.getMetrics() != null) {
+          nodeBuilder.getMetrics().forEach((name, value) -> {
+            metrics.computeIfAbsent(name, n -> new HashMap<>())
+                .put(node, value);
+          });
         }
+      }
+
+      AttributeValues attributeValues = new AttributeValuesImpl(nodeToCoreCount, Map.of(), nodeToFreeDisk, Map.of(), Map.of(), Map.of(), sysprops, metrics);
+      return new AttributeFetcherForTest(attributeValues);
     }
+  }
 
-    static class CollectionBuilder {
-        private final String collectionName;
-        private LinkedList<ShardBuilder> shardBuilders = new LinkedList<>();
-        private Map<String, String> customProperties = new HashMap<>();
+  static class CollectionBuilder {
+    private final String collectionName;
+    private LinkedList<ShardBuilder> shardBuilders = new LinkedList<>();
+    private Map<String, String> customProperties = new HashMap<>();
 
 
-        private CollectionBuilder(String collectionName) {
-            this.collectionName = collectionName;
-        }
+    private CollectionBuilder(String collectionName) {
+      this.collectionName = collectionName;
+    }
 
-        private CollectionBuilder addCustomProperty(String name, String value) {
-            customProperties.put(name, value);
-            return this;
-        }
+    private CollectionBuilder addCustomProperty(String name, String value) {
+      customProperties.put(name, value);
+      return this;
+    }
 
-        /**
-         * Initializes shard and replica builders for the collection based on passed parameters. Replicas are assigned round
-         * robin to the nodes. The shard leader is the first NRT replica of each shard (or first TLOG is no NRT).
-         * Shard and replica configuration can be modified afterwards, the returned builder hierarchy is a convenient starting point.
-         */
-        CollectionBuilder initializeShardsReplicas(int countShards, int countNrtReplicas, int countTlogReplicas,
-                                                   int countPullReplicas, List<NodeBuilder> nodes) {
-            Iterator<NodeBuilder> nodeIterator = nodes.iterator();
-
-            shardBuilders = new LinkedList<>();
-
-            for (int s = 0; s < countShards; s++) {
-                String shardName = "shard" + (s + 1);
-
-                LinkedList<ReplicaBuilder> replicas = new LinkedList<>();
-                ReplicaBuilder leader = null;
-
-                // Iterate on requested counts, NRT then TLOG then PULL. Leader chosen as first NRT (or first TLOG if no NRT)
-                List<Pair<Replica.ReplicaType, Integer>> replicaTypes = List.of(
-                        new Pair<>(Replica.ReplicaType.NRT, countNrtReplicas),
-                        new Pair<>(Replica.ReplicaType.TLOG, countTlogReplicas),
-                        new Pair<>(Replica.ReplicaType.PULL, countPullReplicas));
-
-                for (Pair<Replica.ReplicaType, Integer> tc : replicaTypes) {
-                    Replica.ReplicaType type = tc.first();
-                    int count = tc.second();
-                    String replicaPrefix = collectionName + "_" + shardName + "_replica_" + type.getSuffixChar();
-                    for (int r = 0; r < count; r++) {
-                        String replicaName = replicaPrefix + r;
-                        String coreName = replicaName + "_c";
-                        if (!nodeIterator.hasNext()) {
-                            nodeIterator = nodes.iterator();
-                        }
-                        // If the nodes set is empty, this call will fail
-                        final NodeBuilder node = nodeIterator.next();
-
-                        ReplicaBuilder replicaBuilder = new ReplicaBuilder();
-                        replicaBuilder.setReplicaName(replicaName).setCoreName(coreName).setReplicaType(type)
-                                .setReplicaState(Replica.ReplicaState.ACTIVE).setReplicaNode(node);
-                        replicas.add(replicaBuilder);
-
-                        if (leader == null && type != Replica.ReplicaType.PULL) {
-                            leader = replicaBuilder;
-                        }
-                    }
-                }
-
-                ShardBuilder shardBuilder = new ShardBuilder();
-                shardBuilder.setShardName(shardName).setReplicaBuilders(replicas).setLeader(leader);
-                shardBuilders.add(shardBuilder);
+    /**
+     * Initializes shard and replica builders for the collection based on passed parameters. Replicas are assigned round
+     * robin to the nodes. The shard leader is the first NRT replica of each shard (or first TLOG is no NRT).
+     * Shard and replica configuration can be modified afterwards, the returned builder hierarchy is a convenient starting point.
+     */
+    CollectionBuilder initializeShardsReplicas(int countShards, int countNrtReplicas, int countTlogReplicas,
+                                               int countPullReplicas, List<NodeBuilder> nodes) {
+      Iterator<NodeBuilder> nodeIterator = nodes.iterator();
+
+      shardBuilders = new LinkedList<>();
+
+      for (int s = 0; s < countShards; s++) {
+        String shardName = "shard" + (s + 1);
+
+        LinkedList<ReplicaBuilder> replicas = new LinkedList<>();
+        ReplicaBuilder leader = null;
+
+        // Iterate on requested counts, NRT then TLOG then PULL. Leader chosen as first NRT (or first TLOG if no NRT)
+        List<Pair<Replica.ReplicaType, Integer>> replicaTypes = List.of(
+            new Pair<>(Replica.ReplicaType.NRT, countNrtReplicas),
+            new Pair<>(Replica.ReplicaType.TLOG, countTlogReplicas),
+            new Pair<>(Replica.ReplicaType.PULL, countPullReplicas));
+
+        for (Pair<Replica.ReplicaType, Integer> tc : replicaTypes) {
+          Replica.ReplicaType type = tc.first();
+          int count = tc.second();
+          String replicaPrefix = collectionName + "_" + shardName + "_replica_" + type.getSuffixChar();
+          for (int r = 0; r < count; r++) {
+            String replicaName = replicaPrefix + r;
+            String coreName = replicaName + "_c";
+            if (!nodeIterator.hasNext()) {
+              nodeIterator = nodes.iterator();
             }
+            // If the nodes set is empty, this call will fail
+            final NodeBuilder node = nodeIterator.next();
 
-            return this;
-        }
-
-        SolrCollection build() {
-            ClusterAbstractionsForTest.SolrCollectionImpl solrCollection = new ClusterAbstractionsForTest.SolrCollectionImpl(collectionName, customProperties);
+            ReplicaBuilder replicaBuilder = new ReplicaBuilder();
+            replicaBuilder.setReplicaName(replicaName).setCoreName(coreName).setReplicaType(type)
+                .setReplicaState(Replica.ReplicaState.ACTIVE).setReplicaNode(node);
+            replicas.add(replicaBuilder);
 
-            final LinkedHashMap<String, Shard> shards = new LinkedHashMap<>();
-
-            for (ShardBuilder shardBuilder : shardBuilders) {
-                Shard shard = shardBuilder.build(solrCollection);
-                shards.put(shard.getShardName(), shard);
+            if (leader == null && type != Replica.ReplicaType.PULL) {
+              leader = replicaBuilder;
             }
-
-            solrCollection.setShards(shards);
-            return solrCollection;
+          }
         }
-    }
 
-    static class ShardBuilder {
-        private String shardName;
-        private LinkedList<ReplicaBuilder> replicaBuilders = new LinkedList<>();
-        private ReplicaBuilder leaderReplicaBuilder;
+        ShardBuilder shardBuilder = new ShardBuilder();
+        shardBuilder.setShardName(shardName).setReplicaBuilders(replicas).setLeader(leader);
+        shardBuilders.add(shardBuilder);
+      }
 
-        ShardBuilder setShardName(String shardName) {
-            this.shardName = shardName;
-            return this;
-        }
+      return this;
+    }
 
-        ShardBuilder setReplicaBuilders(LinkedList<ReplicaBuilder> replicaBuilders) {
-            this.replicaBuilders = replicaBuilders;
-            return this;
-        }
+    SolrCollection build() {
+      ClusterAbstractionsForTest.SolrCollectionImpl solrCollection = new ClusterAbstractionsForTest.SolrCollectionImpl(collectionName, customProperties);
 
-        ShardBuilder setLeader(ReplicaBuilder leaderReplicaBuilder) {
-            this.leaderReplicaBuilder = leaderReplicaBuilder;
-            return this;
-        }
+      final LinkedHashMap<String, Shard> shards = new LinkedHashMap<>();
 
-        Shard build(SolrCollection collection) {
-            ClusterAbstractionsForTest.ShardImpl shard = new ClusterAbstractionsForTest.ShardImpl(shardName, collection, Shard.ShardState.ACTIVE);
+      for (ShardBuilder shardBuilder : shardBuilders) {
+        Shard shard = shardBuilder.build(solrCollection);
+        shards.put(shard.getShardName(), shard);
+      }
 
-            final LinkedHashMap<String, Replica> replicas = new LinkedHashMap<>();
-            Replica leader = null;
+      solrCollection.setShards(shards);
+      return solrCollection;
+    }
+  }
 
-            for (ReplicaBuilder replicaBuilder : replicaBuilders) {
-                Replica replica = replicaBuilder.build(shard);
-                replicas.put(replica.getReplicaName(), replica);
+  static class ShardBuilder {
+    private String shardName;
+    private LinkedList<ReplicaBuilder> replicaBuilders = new LinkedList<>();
+    private ReplicaBuilder leaderReplicaBuilder;
 
-                if (leaderReplicaBuilder == replicaBuilder) {
-                    leader = replica;
-                }
-            }
+    ShardBuilder setShardName(String shardName) {
+      this.shardName = shardName;
+      return this;
+    }
 
-            shard.setReplicas(replicas, leader);
-            return shard;
-        }
+    ShardBuilder setReplicaBuilders(LinkedList<ReplicaBuilder> replicaBuilders) {
+      this.replicaBuilders = replicaBuilders;
+      return this;
     }
 
-    static class ReplicaBuilder {
-        private String replicaName;
-        private String coreName;
-        private Replica.ReplicaType replicaType;
-        private Replica.ReplicaState replicaState;
-        private NodeBuilder replicaNode;
+    ShardBuilder setLeader(ReplicaBuilder leaderReplicaBuilder) {
+      this.leaderReplicaBuilder = leaderReplicaBuilder;
+      return this;
+    }
 
-        ReplicaBuilder setReplicaName(String replicaName) {
-            this.replicaName = replicaName;
-            return this;
-        }
+    Shard build(SolrCollection collection) {
+      ClusterAbstractionsForTest.ShardImpl shard = new ClusterAbstractionsForTest.ShardImpl(shardName, collection, Shard.ShardState.ACTIVE);
 
-        ReplicaBuilder setCoreName(String coreName) {
-            this.coreName = coreName;
-            return this;
-        }
+      final LinkedHashMap<String, Replica> replicas = new LinkedHashMap<>();
+      Replica leader = null;
 
-        ReplicaBuilder setReplicaType(Replica.ReplicaType replicaType) {
-            this.replicaType = replicaType;
-            return this;
-        }
+      for (ReplicaBuilder replicaBuilder : replicaBuilders) {
+        Replica replica = replicaBuilder.build(shard);
+        replicas.put(replica.getReplicaName(), replica);
 
-        ReplicaBuilder setReplicaState(Replica.ReplicaState replicaState) {
-            this.replicaState = replicaState;
-            return this;
+        if (leaderReplicaBuilder == replicaBuilder) {
+          leader = replica;
         }
+      }
 
-        ReplicaBuilder setReplicaNode(NodeBuilder replicaNode) {
-            this.replicaNode = replicaNode;
-            return this;
-        }
+      shard.setReplicas(replicas, leader);
+      return shard;
+    }
+  }
+
+  static class ReplicaBuilder {
+    private String replicaName;
+    private String coreName;
+    private Replica.ReplicaType replicaType;
+    private Replica.ReplicaState replicaState;
+    private NodeBuilder replicaNode;
+
+    ReplicaBuilder setReplicaName(String replicaName) {
+      this.replicaName = replicaName;
+      return this;
+    }
 
-        Replica build(Shard shard) {
-            return new ClusterAbstractionsForTest.ReplicaImpl(replicaName, coreName, shard, replicaType, replicaState, replicaNode.build());
-        }
+    ReplicaBuilder setCoreName(String coreName) {
+      this.coreName = coreName;
+      return this;
     }
 
-    static class NodeBuilder {
-        private String nodeName = null;
-        private Integer coreCount = null;
-        private Long freeDiskGB = null;
-        private Map<String, String> sysprops = null;
-        private Map<String, Double> metrics = null;
+    ReplicaBuilder setReplicaType(Replica.ReplicaType replicaType) {
+      this.replicaType = replicaType;
+      return this;
+    }
 
-        NodeBuilder setNodeName(String nodeName) {
-            this.nodeName = nodeName;
-            return this;
-        }
+    ReplicaBuilder setReplicaState(Replica.ReplicaState replicaState) {
+      this.replicaState = replicaState;
+      return this;
+    }
 
-        NodeBuilder setCoreCount(Integer coreCount) {
-            this.coreCount = coreCount;
-            return this;
-        }
+    ReplicaBuilder setReplicaNode(NodeBuilder replicaNode) {
+      this.replicaNode = replicaNode;
+      return this;
+    }
 
-        NodeBuilder setFreeDiskGB(Long freeDiskGB) {
-            this.freeDiskGB = freeDiskGB;
-            return this;
-        }
+    Replica build(Shard shard) {
+      return new ClusterAbstractionsForTest.ReplicaImpl(replicaName, coreName, shard, replicaType, replicaState, replicaNode.build());
+    }
+  }
+
+  static class NodeBuilder {
+    private String nodeName = null;
+    private Integer coreCount = null;
+    private Long freeDiskGB = null;
+    private Map<String, String> sysprops = null;
+    private Map<String, Double> metrics = null;
+
+    NodeBuilder setNodeName(String nodeName) {
+      this.nodeName = nodeName;
+      return this;
+    }
 
-        NodeBuilder setSysprop(String key, String value) {
-            if (sysprops == null) {
-                sysprops = new HashMap<>();
-            }
-            String name = AttributeFetcherImpl.getSystemPropertySnitchTag(key);
-            sysprops.put(name, value);
-            return this;
-        }
+    NodeBuilder setCoreCount(Integer coreCount) {
+      this.coreCount = coreCount;
+      return this;
+    }
 
-        NodeBuilder setMetric(AttributeFetcher.NodeMetricRegistry registry, String key, Double value) {
-            if (metrics == null) {
-                metrics = new HashMap<>();
-            }
-            String name = AttributeFetcherImpl.getMetricSnitchTag(key, registry);
-            metrics.put(name, value);
-            return this;
-        }
+    NodeBuilder setFreeDiskGB(Long freeDiskGB) {
+      this.freeDiskGB = freeDiskGB;
+      return this;
+    }
 
-        Integer getCoreCount() {
-            return coreCount;
-        }
+    NodeBuilder setSysprop(String key, String value) {
+      if (sysprops == null) {
+        sysprops = new HashMap<>();
+      }
+      String name = AttributeFetcherImpl.getSystemPropertySnitchTag(key);
+      sysprops.put(name, value);
+      return this;
+    }
 
-        Long getFreeDiskGB() {
-            return freeDiskGB;
-        }
+    NodeBuilder setMetric(AttributeFetcher.NodeMetricRegistry registry, String key, Double value) {
+      if (metrics == null) {
+        metrics = new HashMap<>();
+      }
+      String name = AttributeFetcherImpl.getMetricSnitchTag(key, registry);
+      metrics.put(name, value);
+      return this;
+    }
 
-        Map<String, String> getSysprops() {
-            return sysprops;
-        }
+    Integer getCoreCount() {
+      return coreCount;
+    }
 
-        Map<String, Double> getMetrics() {
-            return metrics;
-        }
+    Long getFreeDiskGB() {
+      return freeDiskGB;
+    }
 
-        Node build() {
-            // It is ok to build a new instance each time, that instance does the right thing with equals() and hashCode()
-            return new ClusterAbstractionsForTest.NodeImpl(nodeName);
-        }
+    Map<String, String> getSysprops() {
+      return sysprops;
+    }
+
+    Map<String, Double> getMetrics() {
+      return metrics;
+    }
+
+    Node build() {
+      // It is ok to build a new instance each time, that instance does the right thing with equals() and hashCode()
+      return new ClusterAbstractionsForTest.NodeImpl(nodeName);
     }
+  }
 }
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/impl/ClusterAbstractionsForTest.java b/solr/core/src/test/org/apache/solr/cluster/placement/impl/ClusterAbstractionsForTest.java
index da82240..bd14d0d 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/impl/ClusterAbstractionsForTest.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/impl/ClusterAbstractionsForTest.java
@@ -28,295 +28,315 @@ import java.util.stream.Collectors;
  */
 class ClusterAbstractionsForTest {
 
-    static class ClusterImpl implements Cluster {
-        private final Set<Node> liveNodes = new HashSet<>();
-        private final Map<String, SolrCollection> collections = new HashMap<>();
-
-        ClusterImpl(Set<Node> liveNodes, Map<String, SolrCollection> collections) {
-            this.liveNodes.addAll(liveNodes);
-            this.collections.putAll(collections);
-        }
-
-        @Override
-        public Set<Node> getLiveNodes() {
-            return liveNodes;
-        }
-
-        @Override
-        public SolrCollection getCollection(String collectionName) {
-            return collections.get(collectionName);
-        }
-
-        @Override
-        @Nonnull
-        public Iterator<SolrCollection> iterator() {
-            return collections.values().iterator();
-        }
-
-        @Override
-        public Iterable<SolrCollection> collections() {
-            return ClusterImpl.this::iterator;
-        }
-
-        // for unit tests
-
-        ClusterImpl addNode(Node node) {
-            liveNodes.add(node);
-            return this;
-        }
-
-        ClusterImpl removeNode(Node node) {
-            liveNodes.remove(node);
-            return this;
-        }
-
-        ClusterImpl putCollection(SolrCollection collection) {
-            collections.put(collection.getName(), collection);
-            return this;
-        }
-
-        ClusterImpl removeCollection(String name) {
-            collections.remove(name);
-            return this;
-        }
-
-        ClusterImpl removeAllCollections() {
-            collections.clear();
-            return this;
-        }
-    }
-
-
-    static class NodeImpl implements Node {
-        public final String nodeName;
-
-        /**
-         * Transforms a collection of node names into a set of {@link Node} instances.
-         */
-        static Set<Node> getNodes(Collection<String> nodeNames) {
-            return nodeNames.stream().map(NodeImpl::new).collect(Collectors.toSet());
-        }
-
-        NodeImpl(String nodeName) {
-            this.nodeName = nodeName;
-        }
-
-        @Override
-        public String getName() {
-            return nodeName;
-        }
-
-        @Override
-        public String toString() {
-            return getClass().getSimpleName() + "(" + getName() + ")";
-        }
-
-        /**
-         * This class ends up as a key in Maps in {@link org.apache.solr.cluster.placement.AttributeValues}.
-         * It is important to implement this method comparing node names given that new instances of {@link Node} are created
-         * with names equal to existing instances (See {@link Builders.NodeBuilder#build()}).
-         */
-        public boolean equals(Object obj) {
-            if (obj == null) { return false; }
-            if (obj == this) { return true; }
-            if (obj.getClass() != getClass()) { return false; }
-            NodeImpl other = (NodeImpl) obj;
-            return Objects.equals(this.nodeName, other.nodeName);
-        }
-
-        public int hashCode() {
-            return Objects.hashCode(nodeName);
-        }
-    }
-
-
-    static class SolrCollectionImpl implements SolrCollection {
-        private final String collectionName;
-        /** Map from {@link Shard#getShardName()} to {@link Shard} */
-        private Map<String, Shard> shards;
-        private final Map<String, String> customProperties;
-
-        SolrCollectionImpl(String collectionName, Map<String, String> customProperties) {
-            this.collectionName = collectionName;
-            this.customProperties = customProperties;
-        }
-
-        /**
-         * Setting the shards has to happen (in tests) after creating the collection because shards reference the collection
-         */
-        void setShards(Map<String, Shard> shards) {
-            this.shards = shards;
-        }
-
-        Set<String> getShardNames() {
-            return shards.keySet();
-        }
-
-        @Override
-        public String getName() {
-            return collectionName;
-        }
-
-        @Override
-        public Shard getShard(String name) {
-            return shards.get(name);
-        }
-
-        @Override
-        @Nonnull
-        public Iterator<Shard> iterator() {
-            return shards.values().iterator();
-        }
-
-        @Override
-        public Iterable<Shard> shards() {
-            return SolrCollectionImpl.this::iterator;
-        }
-
-        @Override
-        public String getCustomProperty(String customPropertyName) {
-            return customProperties.get(customPropertyName);
-        }
-    }
-
-
-    static class ShardImpl implements Shard {
-        private final String shardName;
-        private final SolrCollection collection;
-        private final ShardState shardState;
-        private Map<String, Replica> replicas;
-        private Replica leader;
-
-        ShardImpl(String shardName, SolrCollection collection, ShardState shardState) {
-            this.shardName = shardName;
-            this.collection = collection;
-            this.shardState = shardState;
-        }
-
-        /**
-         * Setting the replicas has to happen (in tests) after creating the shard because replicas reference the shard
-         */
-        void setReplicas(Map<String, Replica> replicas, Replica leader) {
-            this.replicas = replicas;
-            this.leader = leader;
-        }
-
-        @Override
-        public String getShardName() {
-            return shardName;
-        }
-
-        @Override
-        public SolrCollection getCollection() {
-            return collection;
-        }
-
-        @Override
-        public Replica getReplica(String name) {
-            return replicas.get(name);
-        }
-
-        @Override
-        @Nonnull
-        public Iterator<Replica> iterator() {
-            return replicas.values().iterator();
-        }
-
-        @Override
-        public Iterable<Replica> replicas() {
-            return ShardImpl.this::iterator;
-        }
-
-        @Override
-        public Replica getLeader() {
-            return leader;
-        }
-
-        @Override
-        public ShardState getState() {
-            return shardState;
-        }
-
-        public boolean equals(Object obj) {
-            if (obj == null) { return false; }
-            if (obj == this) { return true; }
-            if (obj.getClass() != getClass()) { return false; }
-            ShardImpl other = (ShardImpl) obj;
-            return Objects.equals(this.shardName, other.shardName)
-                    && Objects.equals(this.collection, other.collection)
-                    && Objects.equals(this.shardState, other.shardState)
-                    && Objects.equals(this.replicas, other.replicas)
-                    && Objects.equals(this.leader, other.leader);
-        }
-
-        public int hashCode() {
-            return Objects.hash(shardName, collection, shardState);
-        }
-    }
-
-
-    static class ReplicaImpl implements Replica {
-        private final String replicaName;
-        private final String coreName;
-        private final Shard shard;
-        private final ReplicaType replicaType;
-        private final ReplicaState replicaState;
-        private final Node node;
-
-        ReplicaImpl(String replicaName, String coreName, Shard shard, ReplicaType replicaType, ReplicaState replicaState, Node node) {
-            this.replicaName = replicaName;
-            this.coreName = coreName;
-            this.shard = shard;
-            this.replicaType = replicaType;
-            this.replicaState = replicaState;
-            this.node = node;
-        }
-
-        @Override
-        public Shard getShard() {
-            return shard;
-        }
-
-        @Override
-        public ReplicaType getType() {
-            return replicaType;
-        }
-
-        @Override
-        public ReplicaState getState() {
-            return replicaState;
-        }
-
-        @Override
-        public String getReplicaName() {
-            return replicaName;
-        }
-
-        @Override
-        public String getCoreName() {
-            return coreName;
-        }
-
-        @Override
-        public Node getNode() {
-            return node;
-        }
-
-        public boolean equals(Object obj) {
-            if (obj == null) { return false; }
-            if (obj == this) { return true; }
-            if (obj.getClass() != getClass()) { return false; }
-            ReplicaImpl other = (ReplicaImpl) obj;
-            return Objects.equals(this.replicaName, other.replicaName)
-                    && Objects.equals(this.coreName, other.coreName)
-                    && Objects.equals(this.shard, other.shard)
-                    && Objects.equals(this.replicaType, other.replicaType)
-                    && Objects.equals(this.replicaState, other.replicaState)
-                    && Objects.equals(this.node, other.node);
-        }
-
-        public int hashCode() {
-            return Objects.hash(replicaName, coreName, shard, replicaType, replicaState, node);
-        }
+  static class ClusterImpl implements Cluster {
+    private final Set<Node> liveNodes = new HashSet<>();
+    private final Map<String, SolrCollection> collections = new HashMap<>();
+
+    ClusterImpl(Set<Node> liveNodes, Map<String, SolrCollection> collections) {
+      this.liveNodes.addAll(liveNodes);
+      this.collections.putAll(collections);
+    }
+
+    @Override
+    public Set<Node> getLiveNodes() {
+      return liveNodes;
+    }
+
+    @Override
+    public SolrCollection getCollection(String collectionName) {
+      return collections.get(collectionName);
+    }
+
+    @Override
+    @Nonnull
+    public Iterator<SolrCollection> iterator() {
+      return collections.values().iterator();
+    }
+
+    @Override
+    public Iterable<SolrCollection> collections() {
+      return ClusterImpl.this::iterator;
+    }
+
+    // for unit tests
+
+    ClusterImpl addNode(Node node) {
+      liveNodes.add(node);
+      return this;
+    }
+
+    ClusterImpl removeNode(Node node) {
+      liveNodes.remove(node);
+      return this;
+    }
+
+    ClusterImpl putCollection(SolrCollection collection) {
+      collections.put(collection.getName(), collection);
+      return this;
+    }
+
+    ClusterImpl removeCollection(String name) {
+      collections.remove(name);
+      return this;
+    }
+
+    ClusterImpl removeAllCollections() {
+      collections.clear();
+      return this;
+    }
+  }
+
+
+  static class NodeImpl implements Node {
+    public final String nodeName;
+
+    /**
+     * Transforms a collection of node names into a set of {@link Node} instances.
+     */
+    static Set<Node> getNodes(Collection<String> nodeNames) {
+      return nodeNames.stream().map(NodeImpl::new).collect(Collectors.toSet());
+    }
+
+    NodeImpl(String nodeName) {
+      this.nodeName = nodeName;
+    }
+
+    @Override
+    public String getName() {
+      return nodeName;
+    }
+
+    @Override
+    public String toString() {
+      return getClass().getSimpleName() + "(" + getName() + ")";
+    }
+
+    /**
+     * This class ends up as a key in Maps in {@link org.apache.solr.cluster.placement.AttributeValues}.
+     * It is important to implement this method comparing node names given that new instances of {@link Node} are created
+     * with names equal to existing instances (See {@link Builders.NodeBuilder#build()}).
+     */
+    public boolean equals(Object obj) {
+      if (obj == null) {
+        return false;
+      }
+      if (obj == this) {
+        return true;
+      }
+      if (obj.getClass() != getClass()) {
+        return false;
+      }
+      NodeImpl other = (NodeImpl) obj;
+      return Objects.equals(this.nodeName, other.nodeName);
+    }
+
+    public int hashCode() {
+      return Objects.hashCode(nodeName);
+    }
+  }
+
+
+  static class SolrCollectionImpl implements SolrCollection {
+    private final String collectionName;
+    /**
+     * Map from {@link Shard#getShardName()} to {@link Shard}
+     */
+    private Map<String, Shard> shards;
+    private final Map<String, String> customProperties;
+
+    SolrCollectionImpl(String collectionName, Map<String, String> customProperties) {
+      this.collectionName = collectionName;
+      this.customProperties = customProperties;
+    }
+
+    /**
+     * Setting the shards has to happen (in tests) after creating the collection because shards reference the collection
+     */
+    void setShards(Map<String, Shard> shards) {
+      this.shards = shards;
+    }
+
+    Set<String> getShardNames() {
+      return shards.keySet();
+    }
+
+    @Override
+    public String getName() {
+      return collectionName;
+    }
+
+    @Override
+    public Shard getShard(String name) {
+      return shards.get(name);
+    }
+
+    @Override
+    @Nonnull
+    public Iterator<Shard> iterator() {
+      return shards.values().iterator();
+    }
+
+    @Override
+    public Iterable<Shard> shards() {
+      return SolrCollectionImpl.this::iterator;
+    }
+
+    @Override
+    public String getCustomProperty(String customPropertyName) {
+      return customProperties.get(customPropertyName);
+    }
+  }
+
+
+  static class ShardImpl implements Shard {
+    private final String shardName;
+    private final SolrCollection collection;
+    private final ShardState shardState;
+    private Map<String, Replica> replicas;
+    private Replica leader;
+
+    ShardImpl(String shardName, SolrCollection collection, ShardState shardState) {
+      this.shardName = shardName;
+      this.collection = collection;
+      this.shardState = shardState;
+    }
+
+    /**
+     * Setting the replicas has to happen (in tests) after creating the shard because replicas reference the shard
+     */
+    void setReplicas(Map<String, Replica> replicas, Replica leader) {
+      this.replicas = replicas;
+      this.leader = leader;
+    }
+
+    @Override
+    public String getShardName() {
+      return shardName;
+    }
+
+    @Override
+    public SolrCollection getCollection() {
+      return collection;
+    }
+
+    @Override
+    public Replica getReplica(String name) {
+      return replicas.get(name);
+    }
+
+    @Override
+    @Nonnull
+    public Iterator<Replica> iterator() {
+      return replicas.values().iterator();
+    }
+
+    @Override
+    public Iterable<Replica> replicas() {
+      return ShardImpl.this::iterator;
+    }
+
+    @Override
+    public Replica getLeader() {
+      return leader;
+    }
+
+    @Override
+    public ShardState getState() {
+      return shardState;
+    }
+
+    public boolean equals(Object obj) {
+      if (obj == null) {
+        return false;
+      }
+      if (obj == this) {
+        return true;
+      }
+      if (obj.getClass() != getClass()) {
+        return false;
+      }
+      ShardImpl other = (ShardImpl) obj;
+      return Objects.equals(this.shardName, other.shardName)
+          && Objects.equals(this.collection, other.collection)
+          && Objects.equals(this.shardState, other.shardState)
+          && Objects.equals(this.replicas, other.replicas)
+          && Objects.equals(this.leader, other.leader);
+    }
+
+    public int hashCode() {
+      return Objects.hash(shardName, collection, shardState);
+    }
+  }
+
+
+  static class ReplicaImpl implements Replica {
+    private final String replicaName;
+    private final String coreName;
+    private final Shard shard;
+    private final ReplicaType replicaType;
+    private final ReplicaState replicaState;
+    private final Node node;
+
+    ReplicaImpl(String replicaName, String coreName, Shard shard, ReplicaType replicaType, ReplicaState replicaState, Node node) {
+      this.replicaName = replicaName;
+      this.coreName = coreName;
+      this.shard = shard;
+      this.replicaType = replicaType;
+      this.replicaState = replicaState;
+      this.node = node;
+    }
+
+    @Override
+    public Shard getShard() {
+      return shard;
+    }
+
+    @Override
+    public ReplicaType getType() {
+      return replicaType;
+    }
+
+    @Override
+    public ReplicaState getState() {
+      return replicaState;
+    }
+
+    @Override
+    public String getReplicaName() {
+      return replicaName;
+    }
+
+    @Override
+    public String getCoreName() {
+      return coreName;
+    }
+
+    @Override
+    public Node getNode() {
+      return node;
+    }
+
+    public boolean equals(Object obj) {
+      if (obj == null) {
+        return false;
+      }
+      if (obj == this) {
+        return true;
+      }
+      if (obj.getClass() != getClass()) {
+        return false;
+      }
+      ReplicaImpl other = (ReplicaImpl) obj;
+      return Objects.equals(this.replicaName, other.replicaName)
+          && Objects.equals(this.coreName, other.coreName)
+          && Objects.equals(this.shard, other.shard)
+          && Objects.equals(this.replicaType, other.replicaType)
+          && Objects.equals(this.replicaState, other.replicaState)
+          && Objects.equals(this.node, other.node);
+    }
+
+    public int hashCode() {
+      return Objects.hash(replicaName, coreName, shard, replicaType, replicaState, node);
     }
+  }
 }
diff --git a/solr/core/src/test/org/apache/solr/cluster/placement/impl/PluginTestHelper.java b/solr/core/src/test/org/apache/solr/cluster/placement/impl/PluginTestHelper.java
index 5b88b9d..61670da 100644
--- a/solr/core/src/test/org/apache/solr/cluster/placement/impl/PluginTestHelper.java
+++ b/solr/core/src/test/org/apache/solr/cluster/placement/impl/PluginTestHelper.java
@@ -29,66 +29,66 @@ import java.util.Set;
 
 public class PluginTestHelper {
 
-    static ClusterAbstractionsForTest.SolrCollectionImpl createCollection(String name, Map<String, String> properties,
-                                           int numShards, int nrtReplicas, int tlogReplicas, int pullReplicas, Set<Node> nodes) {
-        ClusterAbstractionsForTest.SolrCollectionImpl solrCollection = new ClusterAbstractionsForTest.SolrCollectionImpl(name, properties);
-        Map<String, Shard> shards = createShardsAndReplicas(solrCollection, numShards, nrtReplicas, tlogReplicas, pullReplicas, nodes);
-        solrCollection.setShards(shards);
-        return solrCollection;
-    }
+  static ClusterAbstractionsForTest.SolrCollectionImpl createCollection(String name, Map<String, String> properties,
+                                                                        int numShards, int nrtReplicas, int tlogReplicas, int pullReplicas, Set<Node> nodes) {
+    ClusterAbstractionsForTest.SolrCollectionImpl solrCollection = new ClusterAbstractionsForTest.SolrCollectionImpl(name, properties);
+    Map<String, Shard> shards = createShardsAndReplicas(solrCollection, numShards, nrtReplicas, tlogReplicas, pullReplicas, nodes);
+    solrCollection.setShards(shards);
+    return solrCollection;
+  }
+
+  /**
+   * Builds the representation of shards for a collection, based on the number of shards and replicas for each to create.
+   * The replicas are allocated to the provided nodes in a round robin way. The leader is set to the last replica of each shard.
+   */
+  static Map<String, Shard> createShardsAndReplicas(SolrCollection collection, int numShards,
+                                                    int nrtReplicas, int tlogReplicas, int pullReplicas,
+                                                    Set<Node> nodes) {
+    Iterator<Node> nodeIterator = nodes.iterator();
+
+    Map<String, Shard> shards = new HashMap<>();
+
+    for (int s = 0; s < numShards; s++) {
+      // "traditional" shard name
+      String shardName = "shard" + (s + 1);
+
+      ClusterAbstractionsForTest.ShardImpl shard = new ClusterAbstractionsForTest.ShardImpl(shardName, collection, Shard.ShardState.ACTIVE);
+
+      Map<String, Replica> replicas = new HashMap<>();
+
+      Replica leader = null;
+      int totalReplicas = nrtReplicas + tlogReplicas + pullReplicas;
+      for (int r = 0; r < totalReplicas; r++) {
+        Replica.ReplicaType type;
+        if (r < nrtReplicas) {
+          type = Replica.ReplicaType.NRT;
+        } else if (r < nrtReplicas + tlogReplicas) {
+          type = Replica.ReplicaType.TLOG;
+        } else {
+          type = Replica.ReplicaType.PULL;
+        }
+        String replicaName = shardName + "_replica_" + type.getSuffixChar() + r;
+        String coreName = replicaName + "_c";
+        final Node node;
+        if (!nodeIterator.hasNext()) {
+          nodeIterator = nodes.iterator();
+        }
+        // If the nodes set is empty, this call will fail
+        node = nodeIterator.next();
 
-    /**
-     * Builds the representation of shards for a collection, based on the number of shards and replicas for each to create.
-     * The replicas are allocated to the provided nodes in a round robin way. The leader is set to the last replica of each shard.
-     */
-    static Map<String, Shard> createShardsAndReplicas(SolrCollection collection, int numShards,
-                                                      int nrtReplicas, int tlogReplicas, int pullReplicas,
-                                                      Set<Node> nodes) {
-        Iterator<Node> nodeIterator = nodes.iterator();
-
-        Map<String, Shard> shards = new HashMap<>();
-
-        for (int s = 0; s < numShards; s++) {
-            // "traditional" shard name
-            String shardName = "shard" + (s + 1);
-
-            ClusterAbstractionsForTest.ShardImpl shard = new ClusterAbstractionsForTest.ShardImpl(shardName, collection, Shard.ShardState.ACTIVE);
-
-            Map<String, Replica> replicas = new HashMap<>();
-
-            Replica leader = null;
-            int totalReplicas = nrtReplicas + tlogReplicas + pullReplicas;
-            for (int r = 0; r < totalReplicas; r++) {
-                Replica.ReplicaType type;
-                if (r < nrtReplicas) {
-                    type = Replica.ReplicaType.NRT;
-                } else if (r < nrtReplicas + tlogReplicas) {
-                    type = Replica.ReplicaType.TLOG;
-                } else {
-                    type = Replica.ReplicaType.PULL;
-                }
-                String replicaName = shardName + "_replica_" + type.getSuffixChar() + r;
-                String coreName = replicaName + "_c";
-                final Node node;
-                if (!nodeIterator.hasNext()) {
-                    nodeIterator = nodes.iterator();
-                }
-                // If the nodes set is empty, this call will fail
-                node = nodeIterator.next();
-
-                Replica replica = new ClusterAbstractionsForTest.ReplicaImpl(replicaName, coreName, shard, type, Replica.ReplicaState.ACTIVE, node);
-
-                replicas.put(replica.getReplicaName(), replica);
-                if (replica.getType() == Replica.ReplicaType.NRT) {
-                    leader = replica;
-                }
-            }
-
-            shard.setReplicas(replicas, leader);
-
-            shards.put(shard.getShardName(), shard);
+        Replica replica = new ClusterAbstractionsForTest.ReplicaImpl(replicaName, coreName, shard, type, Replica.ReplicaState.ACTIVE, node);
+
+        replicas.put(replica.getReplicaName(), replica);
+        if (replica.getType() == Replica.ReplicaType.NRT) {
+          leader = replica;
         }
+      }
+
+      shard.setReplicas(replicas, leader);
 
-        return shards;
+      shards.put(shard.getShardName(), shard);
     }
+
+    return shards;
+  }
 }