1717import com .linkedin .venice .client .store .StatTrackingStoreClient ;
1818import com .linkedin .venice .compression .CompressionStrategy ;
1919import 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 ;
2022import 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 ;
2152import com .linkedin .venice .controller .kafka .protocol .serializer .AdminOperationSerializer ;
2253import com .linkedin .venice .controllerapi .AdminTopicMetadataResponse ;
2354import com .linkedin .venice .controllerapi .ControllerClient ;
5889import java .util .Arrays ;
5990import java .util .Collection ;
6091import java .util .Collections ;
92+ import java .util .HashMap ;
6193import java .util .HashSet ;
6294import java .util .List ;
95+ import java .util .Map ;
6396import java .util .Optional ;
6497import java .util .Properties ;
6598import java .util .Set ;
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< OperationName> </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