Skip to content

Commit e802c3b

Browse files
committed
[test] Fix flaky admin operation coverage test with declarative mapping
Refactors the admin operation coverage validation to use a declarative OPERATION_TO_TEST_METHOD map instead of runtime tracking. Introduces an EXCLUDED_OPERATIONS set for deprecated or unused operations and moves validation to @BeforeClass for fail-fast behavior. Removes empty test methods for unused operations and expands documentation on how to add coverage for new operations. Increases assertion timeout from 10s to 30s to reduce test flakiness.
1 parent d4a2ff8 commit e802c3b

File tree

1 file changed

+121
-40
lines changed

1 file changed

+121
-40
lines changed

internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/endToEnd/TestAdminOperationWithPreviousVersion.java

Lines changed: 121 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,38 @@
1717
import com.linkedin.venice.client.store.StatTrackingStoreClient;
1818
import com.linkedin.venice.compression.CompressionStrategy;
1919
import com.linkedin.venice.controller.Admin;
20+
import com.linkedin.venice.controller.kafka.protocol.admin.AbortMigration;
21+
import com.linkedin.venice.controller.kafka.protocol.admin.AddVersion;
2022
import com.linkedin.venice.controller.kafka.protocol.admin.AdminOperation;
23+
import com.linkedin.venice.controller.kafka.protocol.admin.ConfigureActiveActiveReplicationForCluster;
24+
import com.linkedin.venice.controller.kafka.protocol.admin.ConfigureIncrementalPushForCluster;
25+
import com.linkedin.venice.controller.kafka.protocol.admin.ConfigureNativeReplicationForCluster;
26+
import com.linkedin.venice.controller.kafka.protocol.admin.CreateStoragePersona;
27+
import com.linkedin.venice.controller.kafka.protocol.admin.DeleteAllVersions;
28+
import com.linkedin.venice.controller.kafka.protocol.admin.DeleteOldVersion;
29+
import com.linkedin.venice.controller.kafka.protocol.admin.DeleteStoragePersona;
30+
import com.linkedin.venice.controller.kafka.protocol.admin.DeleteStore;
31+
import com.linkedin.venice.controller.kafka.protocol.admin.DeleteUnusedValueSchemas;
32+
import com.linkedin.venice.controller.kafka.protocol.admin.DerivedSchemaCreation;
33+
import com.linkedin.venice.controller.kafka.protocol.admin.DisableStoreRead;
34+
import com.linkedin.venice.controller.kafka.protocol.admin.EnableStoreRead;
35+
import com.linkedin.venice.controller.kafka.protocol.admin.KillOfflinePushJob;
36+
import com.linkedin.venice.controller.kafka.protocol.admin.MetaSystemStoreAutoCreationValidation;
37+
import com.linkedin.venice.controller.kafka.protocol.admin.MetadataSchemaCreation;
38+
import com.linkedin.venice.controller.kafka.protocol.admin.MigrateStore;
39+
import com.linkedin.venice.controller.kafka.protocol.admin.PauseStore;
40+
import com.linkedin.venice.controller.kafka.protocol.admin.PushStatusSystemStoreAutoCreationValidation;
41+
import com.linkedin.venice.controller.kafka.protocol.admin.ResumeStore;
42+
import com.linkedin.venice.controller.kafka.protocol.admin.RollForwardCurrentVersion;
43+
import com.linkedin.venice.controller.kafka.protocol.admin.RollbackCurrentVersion;
44+
import com.linkedin.venice.controller.kafka.protocol.admin.SetStoreCurrentVersion;
45+
import com.linkedin.venice.controller.kafka.protocol.admin.SetStoreOwner;
46+
import com.linkedin.venice.controller.kafka.protocol.admin.SetStorePartitionCount;
47+
import com.linkedin.venice.controller.kafka.protocol.admin.StoreCreation;
48+
import com.linkedin.venice.controller.kafka.protocol.admin.SupersetSchemaCreation;
49+
import com.linkedin.venice.controller.kafka.protocol.admin.UpdateStoragePersona;
50+
import com.linkedin.venice.controller.kafka.protocol.admin.UpdateStore;
51+
import com.linkedin.venice.controller.kafka.protocol.admin.ValueSchemaCreation;
2152
import com.linkedin.venice.controller.kafka.protocol.serializer.AdminOperationSerializer;
2253
import com.linkedin.venice.controllerapi.AdminTopicMetadataResponse;
2354
import com.linkedin.venice.controllerapi.ControllerClient;
@@ -58,8 +89,10 @@
5889
import java.util.Arrays;
5990
import java.util.Collection;
6091
import java.util.Collections;
92+
import java.util.HashMap;
6193
import java.util.HashSet;
6294
import java.util.List;
95+
import java.util.Map;
6396
import java.util.Optional;
6497
import java.util.Properties;
6598
import java.util.Set;
@@ -93,11 +126,17 @@
93126
* </p>
94127
*
95128
* <p>
129+
* <b>Test Coverage Enforcement:</b>
130+
* All admin operations must have test coverage declared in {@link #OPERATION_TO_TEST_METHOD}.
131+
* Coverage is validated in {@code @BeforeClass}, causing immediate failure if any operation is missing a test.
132+
* </p>
133+
*
134+
* <p>
96135
* How to add new test for new operation:
97136
* <ol>
98-
* <li>Create a new test method with the name test<OperationName> </li>
99-
* <li>Use runTestForEntryNames inside that test to provide all OperationNames that we are testing</li>
100-
* <li>Write integration test</li>
137+
* <li>Create a new test method with the name test&lt;OperationName&gt; </li>
138+
* <li>Add the mapping to {@link #OPERATION_TO_TEST_METHOD}: {@code put("OperationName", "testOperationName")}</li>
139+
* <li>In the test method, call {@code runTestForEntryNames(Collections.singletonList("OperationName"), ...)}</li>
101140
* </ol>
102141
* </p>
103142
*/
@@ -119,21 +158,70 @@ public class TestAdminOperationWithPreviousVersion {
119158
static final Schema PREVIOUS_SCHEMA = AdminOperationSerializer.getSchema(PREVIOUS_SCHEMA_ID_FOR_ADMIN_OPERATION);
120159
private static final Set<String> NEW_UNION_ENTRIES = getNewUnionEntries();
121160

161+
/**
162+
* Operations that are excluded from test coverage requirements.
163+
* This includes deprecated, unused, or no-op operations that don't need integration tests.
164+
* <p>
165+
* When adding an operation here, include a comment explaining why it's excluded.
166+
*/
167+
private static final Set<String> EXCLUDED_OPERATIONS = new HashSet<>(
168+
Arrays.asList(
169+
// Deprecated: No longer used in production, was replaced by store-level config
170+
ConfigureNativeReplicationForCluster.class.getSimpleName(),
171+
ConfigureIncrementalPushForCluster.class.getSimpleName()));
172+
/**
173+
* Declarative mapping of which operations are covered by which test methods.
174+
* This is validated in @BeforeClass to ensure all operations have tests before execution begins.
175+
* <p>
176+
* When adding a new admin operation, add it here or to {@link #EXCLUDED_OPERATIONS}, or the test will fail.
177+
*/
178+
private static final Map<String, String> OPERATION_TO_TEST_METHOD = new HashMap<String, String>() {
179+
{
180+
put(StoreCreation.class.getSimpleName(), "testStoreCreation");
181+
put(PushStatusSystemStoreAutoCreationValidation.class.getSimpleName(), "testStoreCreation");
182+
put(MetaSystemStoreAutoCreationValidation.class.getSimpleName(), "testStoreCreation");
183+
put(PauseStore.class.getSimpleName(), "testPauseStore");
184+
put(ResumeStore.class.getSimpleName(), "testPauseStore");
185+
put(DisableStoreRead.class.getSimpleName(), "testDisableStoreRead");
186+
put(EnableStoreRead.class.getSimpleName(), "testDisableStoreRead");
187+
put(ValueSchemaCreation.class.getSimpleName(), "testValueSchemaCreation");
188+
put(DeleteUnusedValueSchemas.class.getSimpleName(), "testValueSchemaCreation");
189+
put(CreateStoragePersona.class.getSimpleName(), "testStoragePersona");
190+
put(UpdateStoragePersona.class.getSimpleName(), "testStoragePersona");
191+
put(DeleteStoragePersona.class.getSimpleName(), "testStoragePersona");
192+
put(RollbackCurrentVersion.class.getSimpleName(), "testRollbackCurrentVersion");
193+
put(RollForwardCurrentVersion.class.getSimpleName(), "testRollbackCurrentVersion");
194+
put(ConfigureActiveActiveReplicationForCluster.class.getSimpleName(), "testConfigureActiveActiveReplication");
195+
put(MigrateStore.class.getSimpleName(), "testMigrateStore");
196+
put(AbortMigration.class.getSimpleName(), "testMigrateStore");
197+
put(KillOfflinePushJob.class.getSimpleName(), "testKillOfflinePushJob");
198+
put(DeleteAllVersions.class.getSimpleName(), "testDeleteAllVersions");
199+
put(SetStoreOwner.class.getSimpleName(), "testSetStoreOwner");
200+
put(SetStorePartitionCount.class.getSimpleName(), "testSetStorePartitionCount");
201+
put(SetStoreCurrentVersion.class.getSimpleName(), "testSetStoreCurrentVersion");
202+
put(UpdateStore.class.getSimpleName(), "testUpdateStore");
203+
put(DeleteStore.class.getSimpleName(), "testDeleteStore");
204+
put(DeleteOldVersion.class.getSimpleName(), "testDeleteOldVersion");
205+
put(DerivedSchemaCreation.class.getSimpleName(), "testDerivedSchemaCreation");
206+
put(AddVersion.class.getSimpleName(), "testAddVersion");
207+
put(MetadataSchemaCreation.class.getSimpleName(), "testMetadataSchemaCreation");
208+
put(SupersetSchemaCreation.class.getSimpleName(), "testSupersetSchemaCreation");
209+
}
210+
};
211+
122212
private VeniceTwoLayerMultiRegionMultiClusterWrapper multiRegionMultiClusterWrapper;
123213
private ControllerClient parentControllerClient;
124214
private String clusterName;
125215
private Admin veniceAdmin;
126216
private List<ControllerClient> childControllerClients;
127217
private VeniceMultiClusterWrapper multiClusterWrapperRegion0;
128-
private int countTestRun;
129-
private Set<String> testedOperations;
130218

131219
@BeforeClass(alwaysRun = true)
132220
public void setUp() throws Exception {
133221
Utils.thisIsLocalhost();
134-
// Reset the test run count to 0 for each test class
135-
countTestRun = 0;
136-
testedOperations = new HashSet<>();
222+
223+
// Validate that all operations have test coverage BEFORE running any tests
224+
validateOperationCoverage();
137225

138226
// Set the cluster name to the first cluster
139227
clusterName = CLUSTER_NAMES[0];
@@ -187,9 +275,6 @@ public void setUp() throws Exception {
187275

188276
@BeforeMethod
189277
void beforeEachTest() {
190-
// Increment the count of test run
191-
countTestRun++;
192-
193278
// Verify the admin protocol version
194279
TestUtils.waitForNonDeterministicAssertion(30, TimeUnit.SECONDS, () -> {
195280
assertEquals(
@@ -202,17 +287,33 @@ void beforeEachTest() {
202287
@AfterClass(alwaysRun = true)
203288
public void cleanUp() {
204289
multiRegionMultiClusterWrapper.close();
290+
}
205291

206-
// If the countTestRun = 1, it means we are running one specific test only, no need to verify for all operations
207-
// However, if you are running all tests, we will verify that all operations are tested
208-
if (countTestRun > 1) {
209-
for (String operationName: getPayloadUnionSchemaNames(LATEST_SCHEMA)) {
210-
assertTrue(
211-
testedOperations.contains(operationName),
212-
"Operation type " + operationName + " was not tested. Please add integration test for " + operationName
213-
+ " in TestAdminOperationWithPreviousVersion and use runTestForEntryNames in that test.");
292+
/**
293+
* Validates that all admin operations have test coverage declared in OPERATION_TO_TEST_METHOD
294+
* or are explicitly excluded in EXCLUDED_OPERATIONS.
295+
* This runs in @BeforeClass, so any missing coverage causes immediate failure before expensive test setup.
296+
*/
297+
private void validateOperationCoverage() {
298+
List<String> allOperations = getPayloadUnionSchemaNames(LATEST_SCHEMA);
299+
List<String> missingOperations = new ArrayList<>();
300+
301+
for (String operationName: allOperations) {
302+
if (!OPERATION_TO_TEST_METHOD.containsKey(operationName) && !EXCLUDED_OPERATIONS.contains(operationName)) {
303+
missingOperations.add(operationName);
214304
}
215305
}
306+
307+
if (!missingOperations.isEmpty()) {
308+
throw new AssertionError(
309+
"The following admin operations do not have test coverage declared in OPERATION_TO_TEST_METHOD "
310+
+ "or EXCLUDED_OPERATIONS: " + missingOperations + "\n\n" + "To fix:\n"
311+
+ "1. Add a test method for the operation (e.g., testMyNewOperation)\n"
312+
+ "2. Add the mapping to OPERATION_TO_TEST_METHOD: put(\"MyNewOperation\", \"testMyNewOperation\")\n"
313+
+ "3. In the test method, call runTestForEntryNames(Collections.singletonList(\"MyNewOperation\"), ...)\n"
314+
+ "\n" + "OR if the operation is deprecated/unused:\n"
315+
+ "1. Add it to EXCLUDED_OPERATIONS with a comment explaining why\n");
316+
}
216317
}
217318

218319
@Test(timeOut = TEST_TIMEOUT)
@@ -290,7 +391,7 @@ public void testKillOfflinePushJob() {
290391

291392
// Check version
292393
for (ControllerClient childControllerClient: childControllerClients) {
293-
TestUtils.waitForNonDeterministicAssertion(10, TimeUnit.SECONDS, false, true, () -> {
394+
TestUtils.waitForNonDeterministicAssertion(30, TimeUnit.SECONDS, false, true, () -> {
294395
StoreResponse storeResponse = childControllerClient.getStore(storeName);
295396
assertFalse(storeResponse.isError());
296397
StoreInfo storeInfo = storeResponse.getStore();
@@ -631,23 +732,6 @@ public void testConfigureActiveActiveReplicationForCluster() {
631732
});
632733
}
633734

634-
@Test(timeOut = TEST_TIMEOUT)
635-
public void testConfigureNativeReplicationForCluster() {
636-
// No usage found for this operation
637-
// Check @code{AdminExecutionTask#handleEnableNativeReplicationForCluster}
638-
runTestForEntryNames(Collections.singletonList("ConfigureNativeReplicationForCluster"), () -> {
639-
// Empty Runnable, does nothing
640-
});
641-
}
642-
643-
@Test(timeOut = TEST_TIMEOUT)
644-
public void testConfigureIncrementalPushForCluster() {
645-
// No usage found for this operation
646-
runTestForEntryNames(Collections.singletonList("ConfigureIncrementalPushForCluster"), () -> {
647-
// Empty Runnable, does nothing
648-
});
649-
}
650-
651735
@Test(timeOut = TEST_TIMEOUT)
652736
public void testMigrateStore() {
653737
runTestForEntryNames(Arrays.asList("MigrateStore", "AbortMigration"), () -> {
@@ -909,9 +993,6 @@ private void createAndPushStore(String srcClusterName, String storeName) throws
909993
* @param testLogic: The logic to run the test
910994
*/
911995
private void runTestForEntryNames(List<String> entryNames, Runnable testLogic) {
912-
// Mark the operation type as tested
913-
testedOperations.addAll(entryNames);
914-
915996
boolean isNewUnionEntry = false;
916997
for (String entryName: entryNames) {
917998
if (NEW_UNION_ENTRIES.contains(entryName)) {

0 commit comments

Comments
 (0)