From 01a2c58ddeb06110c6b54ec4f086bc564f50ad1a Mon Sep 17 00:00:00 2001 From: bercianor Date: Tue, 24 Mar 2026 20:21:58 +0000 Subject: [PATCH 1/9] feat(core): add validationOnly mode to prevent change execution --- .../core/error/PendingChangesException.java | 33 +++ .../error/PendingChangesExceptionTest.java | 88 ++++++++ .../builder/AbstractChangeRunnerBuilder.java | 9 + .../configuration/core/CoreConfigurable.java | 4 + .../configuration/core/CoreConfiguration.java | 15 ++ .../core/operation/OperationFactory.java | 22 +- .../operation/execute/ValidateOperation.java | 150 +++++++++++++ .../core/operation/OperationFactoryTest.java | 177 +++++++++++++++ .../execute/ValidateOperationTest.java | 208 ++++++++++++++++++ .../springboot/SpringbootProperties.java | 10 + .../ValidationOnlyIntegrationTest.java | 197 +++++++++++++++++ .../test/_001__ValidationOnlyTestChange.java | 34 +++ .../META-INF/flamingock/metadata.json | 48 +++- 13 files changed, 982 insertions(+), 13 deletions(-) create mode 100644 core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java create mode 100644 core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java create mode 100644 core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java create mode 100644 core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationFactoryTest.java create mode 100644 core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java create mode 100644 platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java create mode 100644 platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/test/_001__ValidationOnlyTestChange.java diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java new file mode 100644 index 000000000..36aab76b0 --- /dev/null +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.common.core.error; + +/** + * Exception thrown when Flamingock runs in validation-only mode and detects pending changes. + */ +public class PendingChangesException extends FlamingockException { + + private final int pendingCount; + + public PendingChangesException(int pendingCount) { + super("Flamingock validationOnly=true: %d pending change(s) detected. Apply them before running in validation-only mode.", pendingCount); + this.pendingCount = pendingCount; + } + + public int getPendingCount() { + return pendingCount; + } +} diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java new file mode 100644 index 000000000..c7493e3e8 --- /dev/null +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.common.core.error; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PendingChangesExceptionTest { + + @Test + @DisplayName("Should return the pending count passed to the constructor") + void shouldReturnPendingCount() { + // Given + int pendingCount = 5; + + // When + PendingChangesException exception = new PendingChangesException(pendingCount); + + // Then + assertEquals(pendingCount, exception.getPendingCount()); + } + + @Test + @DisplayName("Should include the pending count in the exception message") + void shouldIncludeCountInMessage() { + // Given + int pendingCount = 5; + + // When + PendingChangesException exception = new PendingChangesException(pendingCount); + + // Then + assertTrue(exception.getMessage().contains("5"), + "Message should contain the pending count as a string"); + } + + @Test + @DisplayName("Should be an instance of FlamingockException") + void shouldExtendFlamingockException() { + // Given / When + PendingChangesException exception = new PendingChangesException(3); + + // Then + assertInstanceOf(FlamingockException.class, exception); + } + + @Test + @DisplayName("Should work correctly with zero pending changes") + void shouldWorkWithZeroPendingCount() { + // Given / When + PendingChangesException exception = new PendingChangesException(0); + + // Then + assertEquals(0, exception.getPendingCount()); + assertTrue(exception.getMessage().contains("0")); + } + + @Test + @DisplayName("Should work correctly with a large pending count") + void shouldWorkWithLargePendingCount() { + // Given + int largeCount = 999; + + // When + PendingChangesException exception = new PendingChangesException(largeCount); + + // Then + assertEquals(largeCount, exception.getPendingCount()); + assertTrue(exception.getMessage().contains("999")); + } +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java index 0ebb3eebf..4d54d33a2 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java @@ -359,6 +359,15 @@ public HOLDER setEnabled(boolean enabled) { return getSelf(); } + public HOLDER setValidationOnly(boolean validationOnly) { + coreConfiguration.setValidationOnly(validationOnly); + return getSelf(); + } + + public boolean isValidationOnly() { + return coreConfiguration.isValidationOnly(); + } + @Override public HOLDER setServiceIdentifier(String serviceIdentifier) { coreConfiguration.setServiceIdentifier(serviceIdentifier); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfigurable.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfigurable.java index 6cc0b23e3..7cdc18423 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfigurable.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfigurable.java @@ -38,6 +38,10 @@ public interface CoreConfigurable { void setEnabled(boolean enabled); + void setValidationOnly(boolean validationOnly); + + boolean isValidationOnly(); + void setServiceIdentifier(String serviceIdentifier); void setMetadata(Map metadata); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java index 46d90f86d..d5d4749a5 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java @@ -35,6 +35,11 @@ public class CoreConfiguration implements CoreConfigurable { */ private boolean enabled = true; + /** + * If true, Flamingock will only validate that no pending changes exist without applying them. Default false + */ + private boolean validationOnly = false; + /** * Service identifier. */ @@ -91,6 +96,11 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + @Override + public void setValidationOnly(boolean validationOnly) { + this.validationOnly = validationOnly; + } + @Override public void setServiceIdentifier(String serviceIdentifier) { this.serviceIdentifier = serviceIdentifier; @@ -126,6 +136,11 @@ public boolean isEnabled() { return enabled; } + @Override + public boolean isValidationOnly() { + return validationOnly; + } + @Override public String getServiceIdentifier() { return serviceIdentifier; diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java index 308c2f1dd..7ab2e44d4 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.operation; import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.common.core.operation.OperationType; import io.flamingock.internal.common.core.recovery.Resolution; import io.flamingock.internal.core.builder.args.FlamingockArguments; import io.flamingock.internal.core.configuration.core.CoreConfigurable; @@ -31,6 +32,7 @@ import io.flamingock.internal.core.operation.execute.ExecuteArgs; import io.flamingock.internal.core.operation.execute.ExecuteOperation; import io.flamingock.internal.core.operation.execute.ExecuteResult; +import io.flamingock.internal.core.operation.execute.ValidateOperation; import io.flamingock.internal.core.operation.issue.IssueGetArgs; import io.flamingock.internal.core.operation.issue.IssueGetOperation; import io.flamingock.internal.core.operation.issue.IssueGetResult; @@ -95,9 +97,15 @@ public OperationFactory(RunnerId runnerId, } public RunnableOperation getOperation() { - switch (flamingockArgs.getOperation()) { + OperationType operationType = flamingockArgs.getOperation(); + if (operationType == OperationType.EXECUTE_APPLY && coreConfiguration.isValidationOnly()) { + operationType = OperationType.EXECUTE_VALIDATE; + } + switch (operationType) { case EXECUTE_APPLY: return getExecuteOperation(); + case EXECUTE_VALIDATE: + return getValidateOperation(); case AUDIT_LIST: return getAuditListOperation(); case AUDIT_FIX: @@ -107,7 +115,7 @@ public OperationFactory(RunnerId runnerId, case ISSUE_GET: return getIssueGetOperation(); default: - throw new UnsupportedOperationException(String.format("Operation %s not supported", flamingockArgs.getOperation())); + throw new UnsupportedOperationException(String.format("Operation %s not supported", operationType)); } } @@ -153,6 +161,16 @@ private RunnableOperation getExecuteOperation() { return new RunnableOperation<>(executeOperation, new ExecuteArgs(pipeline)); } + private RunnableOperation getValidateOperation() { + ValidateOperation validateOperation = new ValidateOperation( + runnerId, + executionPlanner, + eventPublisher, + isThrowExceptionIfCannotObtainLock, + finalizer); + return new RunnableOperation<>(validateOperation, new ExecuteArgs(pipeline)); + } + private static OrphanExecutionContext buildExecutionContext(CoreConfigurable configuration) { return new OrphanExecutionContext(StringUtil.hostname(), configuration.getMetadata()); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java new file mode 100644 index 000000000..c1ab6a3f3 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java @@ -0,0 +1,150 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.core.operation.execute; + +import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.error.PendingChangesException; +import io.flamingock.internal.common.core.response.data.ExecuteResponseData; +import io.flamingock.internal.core.event.EventPublisher; +import io.flamingock.internal.core.event.model.impl.PipelineCompletedEvent; +import io.flamingock.internal.core.event.model.impl.PipelineFailedEvent; +import io.flamingock.internal.core.event.model.impl.PipelineStartedEvent; +import io.flamingock.internal.core.operation.Operation; +import io.flamingock.internal.core.operation.result.ExecutionResultBuilder; +import io.flamingock.internal.core.pipeline.execution.ExecutableStage; +import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; +import io.flamingock.internal.core.pipeline.loaded.stage.AbstractLoadedStage; +import io.flamingock.internal.core.plan.ExecutionPlan; +import io.flamingock.internal.core.plan.ExecutionPlanner; +import io.flamingock.internal.core.external.store.lock.LockException; +import io.flamingock.internal.core.task.executable.ExecutableTask; +import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * Validates the pipeline without executing any changes. + * If pending changes exist, throws {@link PendingChangesException}. + */ +public class ValidateOperation implements Operation { + + private static final Logger logger = FlamingockLoggerFactory.getLogger("PipelineRunner"); + + private final RunnerId runnerId; + + private final ExecutionPlanner executionPlanner; + + private final EventPublisher eventPublisher; + + private final boolean throwExceptionIfCannotObtainLock; + + private final Runnable finalizer; + + public ValidateOperation(RunnerId runnerId, + ExecutionPlanner executionPlanner, + EventPublisher eventPublisher, + boolean throwExceptionIfCannotObtainLock, + Runnable finalizer) { + this.runnerId = runnerId; + this.executionPlanner = executionPlanner; + this.eventPublisher = eventPublisher; + this.throwExceptionIfCannotObtainLock = throwExceptionIfCannotObtainLock; + this.finalizer = finalizer; + } + + @Override + public ExecuteResult execute(ExecuteArgs args) { + ExecuteResponseData result; + try { + result = this.validate(args.getPipeline()); + } catch (FlamingockException flamingockException) { + throw flamingockException; + } catch (Throwable throwable) { + throw new FlamingockException(throwable); + } finally { + finalizer.run(); + } + return new ExecuteResult(result); + } + + private static List validateAndGetExecutableStages(LoadedPipeline pipeline) { + pipeline.validate(); + List stages = new ArrayList<>(); + if (pipeline.getSystemStage().isPresent()) { + stages.add(pipeline.getSystemStage().get()); + } + stages.addAll(pipeline.getStages()); + return stages; + } + + private ExecuteResponseData validate(LoadedPipeline pipeline) throws FlamingockException { + List allStages = validateAndGetExecutableStages(pipeline); + int stageCount = allStages.size(); + long changeCount = allStages.stream() + .mapToLong(stage -> stage.getTasks().size()) + .sum(); + logger.info("Flamingock validation started [stages={} changes={}]", stageCount, changeCount); + + eventPublisher.publish(new PipelineStartedEvent()); + ExecutionResultBuilder resultBuilder = new ExecutionResultBuilder().startTimer(); + + do { + List stages = validateAndGetExecutableStages(pipeline); + try (ExecutionPlan execution = executionPlanner.getNextExecution(stages)) { + execution.validate(); + + if (execution.isExecutionRequired()) { + int pendingCount = countPendingTasks(execution); + throw new PendingChangesException(pendingCount); + } else { + break; + } + } catch (LockException exception) { + eventPublisher.publish(new PipelineFailedEvent(exception)); + if (throwExceptionIfCannotObtainLock) { + logger.debug("Required process lock not acquired - ABORTING VALIDATION", exception); + throw exception; + } else { + logger.warn("Process lock not acquired but throwExceptionIfCannotObtainLock=false - CONTINUING WITHOUT LOCK", exception); + } + break; + } + } while (true); + + resultBuilder.stopTimer().noChanges(); + ExecuteResponseData result = resultBuilder.build(); + + logger.info("Flamingock validation completed — no pending changes detected"); + eventPublisher.publish(new PipelineCompletedEvent()); + + return result; + } + + private static int countPendingTasks(ExecutionPlan execution) { + int count = 0; + for (ExecutableStage stage : execution.getPipeline().getExecutableStages()) { + for (ExecutableTask task : stage.getTasks()) { + if (!task.isAlreadyApplied()) { + count++; + } + } + } + return count; + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationFactoryTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationFactoryTest.java new file mode 100644 index 000000000..0ccbaa956 --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationFactoryTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.core.operation; + +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.core.builder.args.FlamingockArguments; +import io.flamingock.internal.core.configuration.core.CoreConfigurable; +import io.flamingock.internal.core.event.EventPublisher; +import io.flamingock.internal.core.external.store.audit.AuditPersistence; +import io.flamingock.internal.core.external.targets.TargetSystemManager; +import io.flamingock.internal.core.operation.execute.ValidateOperation; +import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; +import io.flamingock.internal.core.pipeline.loaded.stage.AbstractLoadedStage; +import io.flamingock.internal.core.plan.ExecutionPlanner; +import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; +import io.flamingock.internal.common.core.operation.OperationType; +import io.flamingock.internal.util.id.RunnerId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +/** + * Tests for OperationFactory — routing logic for creating the appropriate operation. + */ +class OperationFactoryTest { + + @Mock + private FlamingockArguments flamingockArgs; + + @Mock + private LoadedPipeline pipeline; + + @Mock + private AuditPersistence persistence; + + @Mock + private ExecutionPlanner executionPlanner; + + @Mock + private TargetSystemManager targetSystemManager; + + @Mock + private CoreConfigurable coreConfiguration; + + @Mock + private EventPublisher eventPublisher; + + @Mock + private ContextResolver dependencyContext; + + @Mock + private AbstractLoadedStage loadedStage; + + @Mock + private AbstractLoadedTask loadedTask; + + private RunnerId runnerId; + private Runnable noOpFinalizer; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + runnerId = RunnerId.fromString("test-runner@localhost#test-uuid"); + noOpFinalizer = () -> {}; + + // Default pipeline setup so OperationFactory does not NPE on pipeline access + when(pipeline.getSystemStage()).thenReturn(java.util.Optional.empty()); + when(pipeline.getStages()).thenReturn(Collections.singletonList(loadedStage)); + when(loadedStage.getTasks()).thenReturn(Collections.singletonList(loadedTask)); + + // Default coreConfiguration stubs + when(coreConfiguration.getMetadata()).thenReturn(Collections.emptyMap()); + } + + @Test + @DisplayName("validationOnly=true with EXECUTE_APPLY → getOperation() routes to ValidateOperation") + void shouldRouteToValidateOperationWhenValidationOnlyIsTrue() throws Exception { + // Given + when(flamingockArgs.getOperation()).thenReturn(OperationType.EXECUTE_APPLY); + when(coreConfiguration.isValidationOnly()).thenReturn(true); + + OperationFactory factory = new OperationFactory( + runnerId, + flamingockArgs, + pipeline, + persistence, + executionPlanner, + targetSystemManager, + coreConfiguration, + eventPublisher, + dependencyContext, + new HashSet<>(), + true, + noOpFinalizer + ); + + // When + RunnableOperation runnableOperation = factory.getOperation(); + + // Then + assertNotNull(runnableOperation); + Operation innerOperation = extractInnerOperation(runnableOperation); + assertInstanceOf(ValidateOperation.class, innerOperation, + "Expected the factory to route to ValidateOperation when validationOnly=true"); + } + + @Test + @DisplayName("validationOnly=false with EXECUTE_APPLY → getOperation() does NOT route to ValidateOperation") + void shouldNotRouteToValidateOperationWhenValidationOnlyIsFalse() throws Exception { + // Given + when(flamingockArgs.getOperation()).thenReturn(OperationType.EXECUTE_APPLY); + when(coreConfiguration.isValidationOnly()).thenReturn(false); + + OperationFactory factory = new OperationFactory( + runnerId, + flamingockArgs, + pipeline, + persistence, + executionPlanner, + targetSystemManager, + coreConfiguration, + eventPublisher, + dependencyContext, + new HashSet<>(), + true, + noOpFinalizer + ); + + // When + RunnableOperation runnableOperation = factory.getOperation(); + + // Then + assertNotNull(runnableOperation); + Operation innerOperation = extractInnerOperation(runnableOperation); + // When validationOnly=false the standard ExecuteOperation is used, not ValidateOperation + assertNotNull(innerOperation); + // Verify it is NOT a ValidateOperation + boolean isValidateOp = innerOperation instanceof ValidateOperation; + org.junit.jupiter.api.Assertions.assertFalse(isValidateOp, + "Expected the factory NOT to route to ValidateOperation when validationOnly=false"); + } + + // ─────────────────────────── Helpers ─────────────────────────── + + /** + * Uses reflection to extract the private {@code operation} field from a {@link RunnableOperation}. + */ + private static Operation extractInnerOperation(RunnableOperation runnableOperation) + throws NoSuchFieldException, IllegalAccessException { + Field field = RunnableOperation.class.getDeclaredField("operation"); + field.setAccessible(true); + return (Operation) field.get(runnableOperation); + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java new file mode 100644 index 000000000..6ddbaf14b --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java @@ -0,0 +1,208 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.core.operation.execute; + +import io.flamingock.internal.common.core.error.PendingChangesException; +import io.flamingock.internal.common.core.response.data.ExecutionStatus; +import io.flamingock.internal.core.event.EventPublisher; +import io.flamingock.internal.core.pipeline.execution.ExecutablePipeline; +import io.flamingock.internal.core.pipeline.execution.ExecutableStage; +import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; +import io.flamingock.internal.core.pipeline.loaded.stage.AbstractLoadedStage; +import io.flamingock.internal.core.plan.ExecutionPlan; +import io.flamingock.internal.core.plan.ExecutionPlanner; +import io.flamingock.internal.core.task.executable.ExecutableTask; +import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; +import io.flamingock.internal.util.id.RunnerId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for ValidateOperation — validation-only mode that checks for pending changes + * without executing them. + */ +class ValidateOperationTest { + + @Mock + private ExecutionPlanner executionPlanner; + + @Mock + private EventPublisher eventPublisher; + + @Mock + private LoadedPipeline pipeline; + + @Mock + private AbstractLoadedStage loadedStage; + + @Mock + private AbstractLoadedTask loadedTask; + + private ValidateOperation operation; + private RunnerId runnerId; + private Runnable noOpFinalizer; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + runnerId = RunnerId.fromString("test-runner@localhost#test-uuid"); + noOpFinalizer = () -> {}; + + operation = new ValidateOperation( + runnerId, + executionPlanner, + eventPublisher, + true, + noOpFinalizer + ); + } + + @Test + @DisplayName("validationOnly: no pending changes → execute() returns success without throwing") + void shouldReturnSuccessWhenNoPendingChangesExist() throws Exception { + // Given + ExecutionPlan executionPlan = mockNoPendingPlan(); + + when(pipeline.getSystemStage()).thenReturn(java.util.Optional.empty()); + when(pipeline.getStages()).thenReturn(Collections.singletonList(loadedStage)); + when(loadedStage.getTasks()).thenReturn(Collections.singletonList(loadedTask)); + when(executionPlanner.getNextExecution(any())).thenReturn(executionPlan); + + ExecuteArgs args = new ExecuteArgs(pipeline); + + // When + ExecuteResult result = operation.execute(args); + + // Then + assertNotNull(result); + assertNotNull(result.getData()); + // ValidateOperation uses resultBuilder.noChanges() — status is NO_CHANGES when no pending changes + assertEquals(ExecutionStatus.NO_CHANGES, result.getData().getStatus()); + } + + @Test + @DisplayName("validationOnly: pending changes exist → execute() throws PendingChangesException with correct count") + void shouldThrowPendingChangesExceptionWhenPendingChangesExist() throws Exception { + // Given + // Two pending tasks (isAlreadyApplied = false) + ExecutableTask pendingTask1 = mock(ExecutableTask.class); + ExecutableTask pendingTask2 = mock(ExecutableTask.class); + when(pendingTask1.isAlreadyApplied()).thenReturn(false); + when(pendingTask2.isAlreadyApplied()).thenReturn(false); + + List pendingTasks = Arrays.asList(pendingTask1, pendingTask2); + ExecutableStage executableStage = mock(ExecutableStage.class); + doReturn(pendingTasks).when(executableStage).getTasks(); + + ExecutablePipeline executablePipeline = mock(ExecutablePipeline.class); + when(executablePipeline.getExecutableStages()).thenReturn(Collections.singletonList(executableStage)); + + ExecutionPlan executionPlan = mockPendingPlan(executablePipeline); + + when(pipeline.getSystemStage()).thenReturn(java.util.Optional.empty()); + when(pipeline.getStages()).thenReturn(Collections.singletonList(loadedStage)); + when(loadedStage.getTasks()).thenReturn(Arrays.asList(loadedTask, loadedTask)); + when(executionPlanner.getNextExecution(any())).thenReturn(executionPlan); + + ExecuteArgs args = new ExecuteArgs(pipeline); + + // When / Then + PendingChangesException thrown = assertThrows( + PendingChangesException.class, + () -> operation.execute(args) + ); + assertEquals(2, thrown.getPendingCount(), + "Expected 2 pending changes to be counted"); + } + + @Test + @DisplayName("pendingCount counts only non-applied tasks across multiple stages") + void shouldCountOnlyNonAppliedTasks() throws Exception { + // Given + ExecutableTask pendingTask = mock(ExecutableTask.class); + ExecutableTask alreadyAppliedTask = mock(ExecutableTask.class); + when(pendingTask.isAlreadyApplied()).thenReturn(false); + when(alreadyAppliedTask.isAlreadyApplied()).thenReturn(true); + + List mixedTasks = Arrays.asList(pendingTask, alreadyAppliedTask); + ExecutableStage executableStage = mock(ExecutableStage.class); + doReturn(mixedTasks).when(executableStage).getTasks(); + + ExecutablePipeline executablePipeline = mock(ExecutablePipeline.class); + when(executablePipeline.getExecutableStages()).thenReturn(Collections.singletonList(executableStage)); + + ExecutionPlan executionPlan = mockPendingPlan(executablePipeline); + + when(pipeline.getSystemStage()).thenReturn(java.util.Optional.empty()); + when(pipeline.getStages()).thenReturn(Collections.singletonList(loadedStage)); + when(loadedStage.getTasks()).thenReturn(Arrays.asList(loadedTask, loadedTask)); + when(executionPlanner.getNextExecution(any())).thenReturn(executionPlan); + + ExecuteArgs args = new ExecuteArgs(pipeline); + + // When / Then — only 1 pending (the non-applied one) + PendingChangesException thrown = assertThrows( + PendingChangesException.class, + () -> operation.execute(args) + ); + assertEquals(1, thrown.getPendingCount(), + "Expected only 1 pending change (already-applied task should not be counted)"); + } + + // ─────────────────────────── Helpers ─────────────────────────── + + /** + * Creates an ExecutionPlan mock where no execution is required + * (i.e., all changes are already applied). + */ + private ExecutionPlan mockNoPendingPlan() { + ExecutionPlan plan = mock(ExecutionPlan.class); + when(plan.isExecutionRequired()).thenReturn(false); + doNothing().when(plan).validate(); + doNothing().when(plan).close(); + return plan; + } + + /** + * Creates an ExecutionPlan mock where execution is required + * (i.e., there are pending changes) and the pipeline exposes the given executable stages. + */ + private ExecutionPlan mockPendingPlan(ExecutablePipeline executablePipeline) { + ExecutionPlan plan = mock(ExecutionPlan.class); + when(plan.isExecutionRequired()).thenReturn(true); + when(plan.getPipeline()).thenReturn(executablePipeline); + doNothing().when(plan).validate(); + doNothing().when(plan).close(); + return plan; + } +} diff --git a/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProperties.java b/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProperties.java index 477336294..e13dd00dd 100644 --- a/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProperties.java +++ b/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProperties.java @@ -95,6 +95,11 @@ public void setEnabled(boolean enabled) { coreConfiguration.setEnabled(enabled); } + @Override + public void setValidationOnly(boolean validationOnly) { + coreConfiguration.setValidationOnly(validationOnly); + } + @Override public void setServiceIdentifier(String serviceIdentifier) { coreConfiguration.setServiceIdentifier(serviceIdentifier); @@ -131,6 +136,11 @@ public boolean isEnabled() { return coreConfiguration.isEnabled(); } + @Override + public boolean isValidationOnly() { + return coreConfiguration.isValidationOnly(); + } + @Override public String getServiceIdentifier() { return coreConfiguration.getServiceIdentifier(); diff --git a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java new file mode 100644 index 000000000..d9bfebae6 --- /dev/null +++ b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.springboot; + +import io.flamingock.api.external.TargetSystem; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.error.PendingChangesException; +import io.flamingock.internal.core.external.store.CommunityAuditStore; +import io.flamingock.internal.core.external.store.audit.community.CommunityAuditPersistence; +import io.flamingock.internal.core.external.store.lock.LockAcquisition; +import io.flamingock.internal.core.external.store.lock.community.CommunityLockService; +import io.flamingock.internal.util.id.RunnerId; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Spring Boot end-to-end integration tests for the {@code validationOnly} mode. + * + *

Uses {@code INITIALIZING_BEAN} management mode so that Flamingock runs synchronously + * during application context startup — enabling the context to fail fast when pending changes + * are detected. + */ +class ValidationOnlyIntegrationTest { + + private static final String CHANGE_ID = "validation-only-test-change"; + + /** + * Builds an ApplicationContextRunner pre-configured with INITIALIZING_BEAN mode + * and validation-only enabled, wired to a specific audit configuration. + */ + private ApplicationContextRunner contextRunner(Class configClass) { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FlamingockAutoConfiguration.class)) + .withUserConfiguration(configClass) + .withPropertyValues( + "spring.profiles.active=non-cli", + "flamingock.management-mode=INITIALIZING_BEAN", + "flamingock.validation-only=true" + ); + } + + @Test + @DisplayName("validationOnly=true + pending changes → context fails with PendingChangesException") + void whenValidationOnlyAndPendingChanges_thenContextFailsWithPendingChangesException() { + contextRunner(PendingChangesConfiguration.class).run(ctx -> { + assertThat(ctx).hasFailed(); + + Throwable failure = ctx.getStartupFailure(); + Throwable rootCause = getRootCause(failure); + + assertThat(rootCause) + .isInstanceOf(PendingChangesException.class); + }); + } + + @Test + @DisplayName("validationOnly=true + all changes applied → context starts successfully") + void whenValidationOnlyAndAllChangesApplied_thenContextStartsSuccessfully() { + contextRunner(AllChangesAppliedConfiguration.class).run(ctx -> { + assertThat(ctx).hasNotFailed(); + }); + } + + // ─────────────────────────── Helpers ─────────────────────────── + + private static Throwable getRootCause(Throwable throwable) { + Throwable cause = throwable; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + return cause; + } + + /** + * Creates a fully-stubbed {@link CommunityAuditStore} mock. + * Both {@link CommunityAuditPersistence} and {@link CommunityLockService} are mocked + * and wired into the store. + * + * @param auditHistory the audit history to return from {@code getAuditHistory()} + */ + private static CommunityAuditStore buildAuditStoreMock(List auditHistory) { + CommunityAuditPersistence persistence = mock(CommunityAuditPersistence.class); + CommunityLockService lockService = mock(CommunityLockService.class); + + // Stub audit history — determines whether changes are "pending" + when(persistence.getAuditHistory()).thenReturn(auditHistory); + + // Delegate snapshot to the default interface method (builds from getAuditHistory()) + when(persistence.getAuditSnapshotByChangeId()).thenCallRealMethod(); + + // Stub closer — called by both the operation and the runner finalizer + when(persistence.getCloser()).thenReturn(() -> { }); + + // Stub lock acquisition — required when there are pending changes + when(lockService.upsert(any(), any(RunnerId.class), anyLong())) + .thenAnswer(invocation -> new LockAcquisition( + invocation.getArgument(1), + invocation.getArgument(2) + )); + + // Stub lock extension — called by the LockRefreshDaemon thread + when(lockService.extendLock(any(), any(RunnerId.class), anyLong())) + .thenAnswer(invocation -> new LockAcquisition( + invocation.getArgument(1), + invocation.getArgument(2) + )); + + CommunityAuditStore auditStore = mock(CommunityAuditStore.class); + when(auditStore.getPersistence()).thenReturn(persistence); + when(auditStore.getLockService()).thenReturn(lockService); + + return auditStore; + } + + // ─────────────────────────── Spring Configurations ─────────────────────────── + + /** + * Configuration for the "pending changes" scenario. + * The audit history is empty → all pipeline changes are considered pending. + */ + @Configuration + static class PendingChangesConfiguration { + + @Bean + public List targetSystems() { + return new ArrayList<>(); + } + + @Bean + public CommunityAuditStore auditStore() { + return buildAuditStoreMock(Collections.emptyList()); + } + } + + /** + * Configuration for the "all changes applied" scenario. + * The audit history contains an APPLIED entry for the test change → no execution needed. + */ + @Configuration + static class AllChangesAppliedConfiguration { + + @Bean + public List targetSystems() { + return new ArrayList<>(); + } + + @Bean + public CommunityAuditStore auditStore() { + AuditEntry appliedEntry = new AuditEntry( + "exec-id-001", + "test-stage", + CHANGE_ID, + "test", + LocalDateTime.now().minusMinutes(5), + AuditEntry.Status.APPLIED, + AuditEntry.ChangeType.STANDARD_CODE, + "io.flamingock.springboot.test._001__ValidationOnlyTestChange", + "apply", + null, + 100L, + "localhost", + null, + false, + null + ); + return buildAuditStoreMock(Collections.singletonList(appliedEntry)); + } + } +} diff --git a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/test/_001__ValidationOnlyTestChange.java b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/test/_001__ValidationOnlyTestChange.java new file mode 100644 index 000000000..5434131b6 --- /dev/null +++ b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/test/_001__ValidationOnlyTestChange.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.springboot.test; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +/** + * A simple no-op change class used in validation-only integration tests. + * It is never actually executed; it only exists to populate the pipeline for testing. + */ +@Change(id = "validation-only-test-change", transactional = false, author = "test") +@TargetSystem(id = "test-system") +public class _001__ValidationOnlyTestChange { + + @Apply + public void apply() { + // No-op: this change is never executed in validation-only mode + } +} diff --git a/platform-plugins/flamingock-springboot-integration/src/test/resources/META-INF/flamingock/metadata.json b/platform-plugins/flamingock-springboot-integration/src/test/resources/META-INF/flamingock/metadata.json index f3c4a0d76..98e73ffec 100644 --- a/platform-plugins/flamingock-springboot-integration/src/test/resources/META-INF/flamingock/metadata.json +++ b/platform-plugins/flamingock-springboot-integration/src/test/resources/META-INF/flamingock/metadata.json @@ -1,12 +1,38 @@ { - "setup": "DEFAULT", - "stages": [ - { - "name": "test-stage", - "type": "USER", - "sourcesPackage": "io.flamingock.springboot.test", - "tasks": [], - "parallel": false - } - ] -} \ No newline at end of file + "pipeline" : { + "stages" : [ { + "name" : "test-stage", + "type" : "DEFAULT", + "sourcesPackage" : "io.flamingock.springboot.test", + "tasks" : [ { + "type" : "codePreviewChange", + "id" : "validation-only-test-change", + "order" : "001", + "author" : "test", + "source" : "io.flamingock.springboot.test._001__ValidationOnlyTestChange", + "runAlways" : false, + "transactionalFlag" : false, + "system" : false, + "targetSystem" : { + "id" : "test-system" + }, + "recovery" : { + "strategy" : "MANUAL_INTERVENTION", + "alwaysRetry" : false + }, + "legacy" : false, + "previewConstructor" : { + "parameterTypes" : [ ] + }, + "applyPreviewMethod" : { + "name" : "apply", + "parameterTypes" : [ ] + }, + "standard" : true, + "sortable" : true + } ] + } ] + }, + "properties" : { }, + "pipelineFile" : "" +} From 8b552e4e4b6e2afaa522f88f5481f5a7a8e108ce Mon Sep 17 00:00:00 2001 From: bercianor Date: Wed, 25 Mar 2026 18:45:35 +0000 Subject: [PATCH 2/9] fix: remove pending change count and rename EXECUTE_VALIDATE to VALIDATE --- .../core/error/PendingChangesException.java | 11 +-- .../common/core/operation/OperationType.java | 2 +- .../error/PendingChangesExceptionTest.java | 88 ------------------- .../configuration/core/CoreConfiguration.java | 4 +- .../core/operation/OperationFactory.java | 4 +- .../operation/execute/ValidateOperation.java | 15 +--- .../execute/ValidateOperationTest.java | 36 -------- 7 files changed, 9 insertions(+), 151 deletions(-) delete mode 100644 core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java index 36aab76b0..27e3a7d6a 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java @@ -20,14 +20,7 @@ */ public class PendingChangesException extends FlamingockException { - private final int pendingCount; - - public PendingChangesException(int pendingCount) { - super("Flamingock validationOnly=true: %d pending change(s) detected. Apply them before running in validation-only mode.", pendingCount); - this.pendingCount = pendingCount; - } - - public int getPendingCount() { - return pendingCount; + public PendingChangesException() { + super("Flamingock validationOnly=true: pending changes detected. Apply them before running in validation-only mode."); } } diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/operation/OperationType.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/operation/OperationType.java index 5a5f992c8..b57a3a800 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/operation/OperationType.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/operation/OperationType.java @@ -18,8 +18,8 @@ public enum OperationType { EXECUTE_APPLY, EXECUTE_ROLLBACK, - EXECUTE_VALIDATE, EXECUTE_DRYRUN, + VALIDATE, AUDIT_LIST, AUDIT_FIX, ISSUE_LIST, diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java deleted file mode 100644 index c7493e3e8..000000000 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.common.core.error; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class PendingChangesExceptionTest { - - @Test - @DisplayName("Should return the pending count passed to the constructor") - void shouldReturnPendingCount() { - // Given - int pendingCount = 5; - - // When - PendingChangesException exception = new PendingChangesException(pendingCount); - - // Then - assertEquals(pendingCount, exception.getPendingCount()); - } - - @Test - @DisplayName("Should include the pending count in the exception message") - void shouldIncludeCountInMessage() { - // Given - int pendingCount = 5; - - // When - PendingChangesException exception = new PendingChangesException(pendingCount); - - // Then - assertTrue(exception.getMessage().contains("5"), - "Message should contain the pending count as a string"); - } - - @Test - @DisplayName("Should be an instance of FlamingockException") - void shouldExtendFlamingockException() { - // Given / When - PendingChangesException exception = new PendingChangesException(3); - - // Then - assertInstanceOf(FlamingockException.class, exception); - } - - @Test - @DisplayName("Should work correctly with zero pending changes") - void shouldWorkWithZeroPendingCount() { - // Given / When - PendingChangesException exception = new PendingChangesException(0); - - // Then - assertEquals(0, exception.getPendingCount()); - assertTrue(exception.getMessage().contains("0")); - } - - @Test - @DisplayName("Should work correctly with a large pending count") - void shouldWorkWithLargePendingCount() { - // Given - int largeCount = 999; - - // When - PendingChangesException exception = new PendingChangesException(largeCount); - - // Then - assertEquals(largeCount, exception.getPendingCount()); - assertTrue(exception.getMessage().contains("999")); - } -} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java index d5d4749a5..b2dc6715a 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java @@ -36,7 +36,9 @@ public class CoreConfiguration implements CoreConfigurable { private boolean enabled = true; /** - * If true, Flamingock will only validate that no pending changes exist without applying them. Default false + * If true, Flamingock will only validate that no pending changes exist without applying them + * When Flamingock runs through the CLI, the CLI operation takes precedence over this flag + * Default false */ private boolean validationOnly = false; diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java index 7ab2e44d4..e817356b6 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java @@ -99,12 +99,12 @@ public OperationFactory(RunnerId runnerId, public RunnableOperation getOperation() { OperationType operationType = flamingockArgs.getOperation(); if (operationType == OperationType.EXECUTE_APPLY && coreConfiguration.isValidationOnly()) { - operationType = OperationType.EXECUTE_VALIDATE; + operationType = OperationType.VALIDATE; } switch (operationType) { case EXECUTE_APPLY: return getExecuteOperation(); - case EXECUTE_VALIDATE: + case VALIDATE: return getValidateOperation(); case AUDIT_LIST: return getAuditListOperation(); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java index c1ab6a3f3..b071467a6 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java @@ -110,8 +110,7 @@ private ExecuteResponseData validate(LoadedPipeline pipeline) throws FlamingockE execution.validate(); if (execution.isExecutionRequired()) { - int pendingCount = countPendingTasks(execution); - throw new PendingChangesException(pendingCount); + throw new PendingChangesException(); } else { break; } @@ -135,16 +134,4 @@ private ExecuteResponseData validate(LoadedPipeline pipeline) throws FlamingockE return result; } - - private static int countPendingTasks(ExecutionPlan execution) { - int count = 0; - for (ExecutableStage stage : execution.getPipeline().getExecutableStages()) { - for (ExecutableTask task : stage.getTasks()) { - if (!task.isAlreadyApplied()) { - count++; - } - } - } - return count; - } } diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java index 6ddbaf14b..4e0cc9321 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java @@ -141,42 +141,6 @@ void shouldThrowPendingChangesExceptionWhenPendingChangesExist() throws Exceptio PendingChangesException.class, () -> operation.execute(args) ); - assertEquals(2, thrown.getPendingCount(), - "Expected 2 pending changes to be counted"); - } - - @Test - @DisplayName("pendingCount counts only non-applied tasks across multiple stages") - void shouldCountOnlyNonAppliedTasks() throws Exception { - // Given - ExecutableTask pendingTask = mock(ExecutableTask.class); - ExecutableTask alreadyAppliedTask = mock(ExecutableTask.class); - when(pendingTask.isAlreadyApplied()).thenReturn(false); - when(alreadyAppliedTask.isAlreadyApplied()).thenReturn(true); - - List mixedTasks = Arrays.asList(pendingTask, alreadyAppliedTask); - ExecutableStage executableStage = mock(ExecutableStage.class); - doReturn(mixedTasks).when(executableStage).getTasks(); - - ExecutablePipeline executablePipeline = mock(ExecutablePipeline.class); - when(executablePipeline.getExecutableStages()).thenReturn(Collections.singletonList(executableStage)); - - ExecutionPlan executionPlan = mockPendingPlan(executablePipeline); - - when(pipeline.getSystemStage()).thenReturn(java.util.Optional.empty()); - when(pipeline.getStages()).thenReturn(Collections.singletonList(loadedStage)); - when(loadedStage.getTasks()).thenReturn(Arrays.asList(loadedTask, loadedTask)); - when(executionPlanner.getNextExecution(any())).thenReturn(executionPlan); - - ExecuteArgs args = new ExecuteArgs(pipeline); - - // When / Then — only 1 pending (the non-applied one) - PendingChangesException thrown = assertThrows( - PendingChangesException.class, - () -> operation.execute(args) - ); - assertEquals(1, thrown.getPendingCount(), - "Expected only 1 pending change (already-applied task should not be counted)"); } // ─────────────────────────── Helpers ─────────────────────────── From 1bdc33d31a9239704eb58231256b58167f0d38d5 Mon Sep 17 00:00:00 2001 From: bercianor Date: Wed, 25 Mar 2026 18:47:07 +0000 Subject: [PATCH 3/9] fix: extends ValidateOperation from ExecuteOperation --- .../core/operation/OperationFactory.java | 13 ++- .../operation/execute/ExecuteOperation.java | 10 +- .../operation/execute/ValidateOperation.java | 96 ++----------------- .../execute/ValidateOperationTest.java | 24 +++-- 4 files changed, 40 insertions(+), 103 deletions(-) diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java index e817356b6..a1759942b 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java @@ -162,12 +162,15 @@ private RunnableOperation getExecuteOperation() { } private RunnableOperation getValidateOperation() { + final StageExecutor stageExecutor = new StageExecutor(dependencyContext, nonGuardedTypes, persistence, targetSystemManager, null); ValidateOperation validateOperation = new ValidateOperation( - runnerId, - executionPlanner, - eventPublisher, - isThrowExceptionIfCannotObtainLock, - finalizer); + runnerId, + executionPlanner, + stageExecutor, + buildExecutionContext(coreConfiguration), + eventPublisher, + isThrowExceptionIfCannotObtainLock, + finalizer); return new RunnableOperation<>(validateOperation, new ExecuteArgs(pipeline)); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java index fa252d34a..733aefae0 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.operation.execute; import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.error.PendingChangesException; import io.flamingock.internal.common.core.response.data.ErrorInfo; import io.flamingock.internal.common.core.response.data.ExecuteResponseData; import io.flamingock.internal.common.core.response.data.StageResult; @@ -66,7 +67,7 @@ public class ExecuteOperation implements Operation { private final OrphanExecutionContext orphanExecutionContext; - private final Runnable finalizer; + protected final Runnable finalizer; public ExecuteOperation(RunnerId runnerId, ExecutionPlanner executionPlanner, @@ -89,7 +90,7 @@ public ExecuteOperation(RunnerId runnerId, public ExecuteResult execute(ExecuteArgs args) { ExecuteResponseData result; try { - result = this.execute(args.getPipeline()); + result = this.execute(args.getPipeline(), false); } catch (OperationException operationException) { result = operationException.getResult(); throw operationException; @@ -111,7 +112,7 @@ private static List validateAndGetExecutableStages(LoadedPi return stages; } - private ExecuteResponseData execute(LoadedPipeline pipeline) throws FlamingockException { + protected ExecuteResponseData execute(LoadedPipeline pipeline, Boolean validateOnly) throws FlamingockException { List allStages = validateAndGetExecutableStages(pipeline); int stageCount = allStages.size(); long changeCount = allStages.stream() @@ -130,6 +131,9 @@ private ExecuteResponseData execute(LoadedPipeline pipeline) throws FlamingockEx execution.validate(); if (execution.isExecutionRequired()) { + if (validateOnly) { + throw new PendingChangesException(); + } execution.applyOnEach((executionId, lock, executableStage) -> { StageResult stageResult = runStage(executionId, lock, executableStage); resultBuilder.addStage(stageResult); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java index b071467a6..5fd84d91b 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java @@ -19,119 +19,39 @@ import io.flamingock.internal.common.core.error.PendingChangesException; import io.flamingock.internal.common.core.response.data.ExecuteResponseData; import io.flamingock.internal.core.event.EventPublisher; -import io.flamingock.internal.core.event.model.impl.PipelineCompletedEvent; -import io.flamingock.internal.core.event.model.impl.PipelineFailedEvent; -import io.flamingock.internal.core.event.model.impl.PipelineStartedEvent; -import io.flamingock.internal.core.operation.Operation; -import io.flamingock.internal.core.operation.result.ExecutionResultBuilder; -import io.flamingock.internal.core.pipeline.execution.ExecutableStage; -import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; -import io.flamingock.internal.core.pipeline.loaded.stage.AbstractLoadedStage; -import io.flamingock.internal.core.plan.ExecutionPlan; +import io.flamingock.internal.core.pipeline.execution.OrphanExecutionContext; +import io.flamingock.internal.core.pipeline.execution.StageExecutor; import io.flamingock.internal.core.plan.ExecutionPlanner; -import io.flamingock.internal.core.external.store.lock.LockException; -import io.flamingock.internal.core.task.executable.ExecutableTask; import io.flamingock.internal.util.id.RunnerId; -import io.flamingock.internal.util.log.FlamingockLoggerFactory; -import org.slf4j.Logger; - -import java.util.ArrayList; -import java.util.List; /** * Validates the pipeline without executing any changes. * If pending changes exist, throws {@link PendingChangesException}. */ -public class ValidateOperation implements Operation { - - private static final Logger logger = FlamingockLoggerFactory.getLogger("PipelineRunner"); - - private final RunnerId runnerId; - - private final ExecutionPlanner executionPlanner; - - private final EventPublisher eventPublisher; - - private final boolean throwExceptionIfCannotObtainLock; - - private final Runnable finalizer; +public class ValidateOperation extends ExecuteOperation { public ValidateOperation(RunnerId runnerId, ExecutionPlanner executionPlanner, + StageExecutor stageExecutor, + OrphanExecutionContext orphanExecutionContext, EventPublisher eventPublisher, boolean throwExceptionIfCannotObtainLock, Runnable finalizer) { - this.runnerId = runnerId; - this.executionPlanner = executionPlanner; - this.eventPublisher = eventPublisher; - this.throwExceptionIfCannotObtainLock = throwExceptionIfCannotObtainLock; - this.finalizer = finalizer; + super(runnerId, executionPlanner, stageExecutor, orphanExecutionContext, eventPublisher, throwExceptionIfCannotObtainLock, finalizer); } @Override public ExecuteResult execute(ExecuteArgs args) { ExecuteResponseData result; try { - result = this.validate(args.getPipeline()); + result = super.execute(args.getPipeline(), true); } catch (FlamingockException flamingockException) { throw flamingockException; } catch (Throwable throwable) { throw new FlamingockException(throwable); } finally { - finalizer.run(); + super.finalizer.run(); } return new ExecuteResult(result); } - - private static List validateAndGetExecutableStages(LoadedPipeline pipeline) { - pipeline.validate(); - List stages = new ArrayList<>(); - if (pipeline.getSystemStage().isPresent()) { - stages.add(pipeline.getSystemStage().get()); - } - stages.addAll(pipeline.getStages()); - return stages; - } - - private ExecuteResponseData validate(LoadedPipeline pipeline) throws FlamingockException { - List allStages = validateAndGetExecutableStages(pipeline); - int stageCount = allStages.size(); - long changeCount = allStages.stream() - .mapToLong(stage -> stage.getTasks().size()) - .sum(); - logger.info("Flamingock validation started [stages={} changes={}]", stageCount, changeCount); - - eventPublisher.publish(new PipelineStartedEvent()); - ExecutionResultBuilder resultBuilder = new ExecutionResultBuilder().startTimer(); - - do { - List stages = validateAndGetExecutableStages(pipeline); - try (ExecutionPlan execution = executionPlanner.getNextExecution(stages)) { - execution.validate(); - - if (execution.isExecutionRequired()) { - throw new PendingChangesException(); - } else { - break; - } - } catch (LockException exception) { - eventPublisher.publish(new PipelineFailedEvent(exception)); - if (throwExceptionIfCannotObtainLock) { - logger.debug("Required process lock not acquired - ABORTING VALIDATION", exception); - throw exception; - } else { - logger.warn("Process lock not acquired but throwExceptionIfCannotObtainLock=false - CONTINUING WITHOUT LOCK", exception); - } - break; - } - } while (true); - - resultBuilder.stopTimer().noChanges(); - ExecuteResponseData result = resultBuilder.build(); - - logger.info("Flamingock validation completed — no pending changes detected"); - eventPublisher.publish(new PipelineCompletedEvent()); - - return result; - } } diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java index 4e0cc9321..a6624e6a1 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java @@ -20,6 +20,8 @@ import io.flamingock.internal.core.event.EventPublisher; import io.flamingock.internal.core.pipeline.execution.ExecutablePipeline; import io.flamingock.internal.core.pipeline.execution.ExecutableStage; +import io.flamingock.internal.core.pipeline.execution.OrphanExecutionContext; +import io.flamingock.internal.core.pipeline.execution.StageExecutor; import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; import io.flamingock.internal.core.pipeline.loaded.stage.AbstractLoadedStage; import io.flamingock.internal.core.plan.ExecutionPlan; @@ -36,6 +38,7 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -56,6 +59,9 @@ class ValidateOperationTest { @Mock private ExecutionPlanner executionPlanner; + @Mock + private StageExecutor stageExecutor; + @Mock private EventPublisher eventPublisher; @@ -70,20 +76,24 @@ class ValidateOperationTest { private ValidateOperation operation; private RunnerId runnerId; + private OrphanExecutionContext orphanContext; private Runnable noOpFinalizer; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); runnerId = RunnerId.fromString("test-runner@localhost#test-uuid"); + orphanContext = new OrphanExecutionContext("localhost", new HashMap<>()); noOpFinalizer = () -> {}; operation = new ValidateOperation( - runnerId, - executionPlanner, - eventPublisher, - true, - noOpFinalizer + runnerId, + executionPlanner, + stageExecutor, + orphanContext, + eventPublisher, + true, + noOpFinalizer ); } @@ -106,8 +116,8 @@ void shouldReturnSuccessWhenNoPendingChangesExist() throws Exception { // Then assertNotNull(result); assertNotNull(result.getData()); - // ValidateOperation uses resultBuilder.noChanges() — status is NO_CHANGES when no pending changes - assertEquals(ExecutionStatus.NO_CHANGES, result.getData().getStatus()); + // ValidateOperation uses resultBuilder.noChanges() — status is SUCCESS when no pending changes + assertEquals(ExecutionStatus.SUCCESS, result.getData().getStatus()); } @Test From d4fb6ccdae35fa5be1599f7b03457a3284395da7 Mon Sep 17 00:00:00 2001 From: bercianor Date: Wed, 25 Mar 2026 20:15:50 +0000 Subject: [PATCH 4/9] fix: check if not CLI mode to change operation from apply to validate --- .../io/flamingock/internal/core/operation/OperationFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java index a1759942b..34eefc298 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java @@ -98,7 +98,7 @@ public OperationFactory(RunnerId runnerId, public RunnableOperation getOperation() { OperationType operationType = flamingockArgs.getOperation(); - if (operationType == OperationType.EXECUTE_APPLY && coreConfiguration.isValidationOnly()) { + if (operationType == OperationType.EXECUTE_APPLY && coreConfiguration.isValidationOnly() && !flamingockArgs.isCliMode()) { operationType = OperationType.VALIDATE; } switch (operationType) { From 376b0631afeeb6ec10a5c59f2a755abb0d268911 Mon Sep 17 00:00:00 2001 From: bercianor Date: Thu, 26 Mar 2026 16:25:06 +0000 Subject: [PATCH 5/9] refactor: default operation responsibility and AbstractPipelineTraverseOperation --- .../builder/AbstractChangeRunnerBuilder.java | 6 +- .../builder/args/FlamingockArguments.java | 6 +- .../core/builder/runner/RunnerFactory.java | 2 +- ...=> AbstractPipelineTraverseOperation.java} | 44 +++++---------- ...ionFactory.java => OperationResolver.java} | 39 ++++++------- .../execute/ExecuteApplyOperation.java | 56 +++++++++++++++++++ .../ValidateOperation.java | 7 ++- .../builder/args/FlamingockArgumentsTest.java | 23 ++++---- ...st.java => ExecuteApplyOperationTest.java} | 10 ++-- ...ryTest.java => OperationResolverTest.java} | 19 ++++--- .../{execute => }/ValidateOperationTest.java | 5 +- 11 files changed, 130 insertions(+), 87 deletions(-) rename core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/{execute/ExecuteOperation.java => AbstractPipelineTraverseOperation.java} (86%) rename core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/{OperationFactory.java => OperationResolver.java} (88%) create mode 100644 core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java rename core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/{execute => validate}/ValidateOperation.java (87%) rename core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/{ExecuteOperationTest.java => ExecuteApplyOperationTest.java} (96%) rename core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/{OperationFactoryTest.java => OperationResolverTest.java} (89%) rename core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/{execute => }/ValidateOperationTest.java (96%) diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java index 4d54d33a2..3e6b619bf 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java @@ -31,6 +31,7 @@ import io.flamingock.internal.core.context.SimpleContext; import io.flamingock.internal.core.external.store.AuditStore; import io.flamingock.internal.core.external.store.audit.AuditPersistence; +import io.flamingock.internal.core.operation.OperationResolver; import io.flamingock.internal.core.plan.ExecutionPlanner; import io.flamingock.internal.core.event.CompositeEventPublisher; import io.flamingock.internal.core.event.EventPublisher; @@ -43,7 +44,6 @@ import io.flamingock.internal.core.event.model.IStageFailedEvent; import io.flamingock.internal.core.event.model.IStageIgnoredEvent; import io.flamingock.internal.core.event.model.IStageStartedEvent; -import io.flamingock.internal.core.operation.OperationFactory; import io.flamingock.internal.core.operation.RunnableOperation; import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; import io.flamingock.internal.core.plugin.Plugin; @@ -213,7 +213,7 @@ public final Runner build() { FlamingockArguments flamingockArgs = FlamingockArguments.parse(applicationArgs); - OperationFactory operationFactory = new OperationFactory( + OperationResolver operationResolver = new OperationResolver( runnerId, flamingockArgs, pipeline, @@ -227,7 +227,7 @@ public final Runner build() { coreConfiguration.isThrowExceptionIfCannotObtainLock(), persistence.getCloser() ); - RunnableOperation operation = operationFactory.getOperation(); + RunnableOperation operation = operationResolver.getOperation(); return new RunnerFactory(runnerId, flamingockArgs, operation, persistence.getCloser()).create(); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java index eed899be5..eb63b9230 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java @@ -53,7 +53,7 @@ private FlamingockArguments(boolean cliMode, public static FlamingockArguments parse(String[] args) { if (args == null || args.length == 0) { - return new FlamingockArguments(false, OperationType.EXECUTE_APPLY, false, null, Collections.emptyMap()); + return new FlamingockArguments(false, null, false, null, Collections.emptyMap()); } boolean cliMode = false; @@ -150,8 +150,8 @@ public boolean isCliMode() { return cliMode; } - public OperationType getOperation() { - return operation; + public Optional getOperation() { + return Optional.ofNullable(operation); } public boolean isOperationProvided() { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java index 2b085605a..55ebf9d00 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java @@ -60,7 +60,7 @@ private Runner createCliRunner() { .map(outputFile -> (ResponseChannel) new FileResponseChannel(outputFile, JsonObjectMapper.DEFAULT_INSTANCE)) .orElseGet(NoOpResponseChannel::new); - return new CliRunner(operation, finalizer, channel, flamingockArgs.getOperation()); + return new CliRunner(operation, finalizer, channel, flamingockArgs.getOperation().get()); } private Runner createDefaultRunner() { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java similarity index 86% rename from core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java rename to core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java index 733aefae0..dbb9a9000 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.internal.core.operation.execute; +package io.flamingock.internal.core.operation; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.error.PendingChangesException; @@ -27,8 +27,8 @@ import io.flamingock.internal.core.event.model.impl.StageCompletedEvent; import io.flamingock.internal.core.event.model.impl.StageFailedEvent; import io.flamingock.internal.core.event.model.impl.StageStartedEvent; -import io.flamingock.internal.core.operation.Operation; -import io.flamingock.internal.core.operation.OperationException; +import io.flamingock.internal.core.operation.execute.ExecuteArgs; +import io.flamingock.internal.core.operation.execute.ExecuteResult; import io.flamingock.internal.core.operation.result.ExecutionResultBuilder; import io.flamingock.internal.core.pipeline.execution.ExecutableStage; import io.flamingock.internal.core.pipeline.execution.ExecutionContext; @@ -49,9 +49,9 @@ import java.util.List; /** - * Executes the pipeline and returns structured result data. + * Common execution flow for Apply and Validate operations */ -public class ExecuteOperation implements Operation { +public abstract class AbstractPipelineTraverseOperation implements Operation { private static final Logger logger = FlamingockLoggerFactory.getLogger("PipelineRunner"); @@ -69,13 +69,13 @@ public class ExecuteOperation implements Operation { protected final Runnable finalizer; - public ExecuteOperation(RunnerId runnerId, - ExecutionPlanner executionPlanner, - StageExecutor stageExecutor, - OrphanExecutionContext orphanExecutionContext, - EventPublisher eventPublisher, - boolean throwExceptionIfCannotObtainLock, - Runnable finalizer) { + public AbstractPipelineTraverseOperation(RunnerId runnerId, + ExecutionPlanner executionPlanner, + StageExecutor stageExecutor, + OrphanExecutionContext orphanExecutionContext, + EventPublisher eventPublisher, + boolean throwExceptionIfCannotObtainLock, + Runnable finalizer) { this.runnerId = runnerId; this.executionPlanner = executionPlanner; this.stageExecutor = stageExecutor; @@ -85,23 +85,6 @@ public ExecuteOperation(RunnerId runnerId, this.finalizer = finalizer; } - - @Override - public ExecuteResult execute(ExecuteArgs args) { - ExecuteResponseData result; - try { - result = this.execute(args.getPipeline(), false); - } catch (OperationException operationException) { - result = operationException.getResult(); - throw operationException; - } catch (Throwable throwable) { - throw processAndGetFlamingockException(throwable, null); - } finally { - finalizer.run(); - } - return new ExecuteResult(result); - } - private static List validateAndGetExecutableStages(LoadedPipeline pipeline) { pipeline.validate(); List stages = new ArrayList<>(); @@ -193,7 +176,7 @@ private StageResult startStage(String executionId, Lock lock, ExecutableStage ex return executionOutput.getResult(); } - private FlamingockException processAndGetFlamingockException(Throwable exception, ExecutionResultBuilder resultBuilder) throws FlamingockException { + protected FlamingockException processAndGetFlamingockException(Throwable exception, ExecutionResultBuilder resultBuilder) throws FlamingockException { FlamingockException flamingockException; if (exception instanceof OperationException) { OperationException pipelineException = (OperationException) exception; @@ -212,5 +195,4 @@ private FlamingockException processAndGetFlamingockException(Throwable exception eventPublisher.publish(new PipelineFailedEvent(flamingockException)); return flamingockException; } - } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationResolver.java similarity index 88% rename from core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java rename to core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationResolver.java index 34eefc298..c43be9dab 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationFactory.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/OperationResolver.java @@ -29,16 +29,14 @@ import io.flamingock.internal.core.operation.audit.AuditListArgs; import io.flamingock.internal.core.operation.audit.AuditListOperation; import io.flamingock.internal.core.operation.audit.AuditListResult; -import io.flamingock.internal.core.operation.execute.ExecuteArgs; -import io.flamingock.internal.core.operation.execute.ExecuteOperation; -import io.flamingock.internal.core.operation.execute.ExecuteResult; -import io.flamingock.internal.core.operation.execute.ValidateOperation; +import io.flamingock.internal.core.operation.execute.*; import io.flamingock.internal.core.operation.issue.IssueGetArgs; import io.flamingock.internal.core.operation.issue.IssueGetOperation; import io.flamingock.internal.core.operation.issue.IssueGetResult; import io.flamingock.internal.core.operation.issue.IssueListArgs; import io.flamingock.internal.core.operation.issue.IssueListOperation; import io.flamingock.internal.core.operation.issue.IssueListResult; +import io.flamingock.internal.core.operation.validate.ValidateOperation; import io.flamingock.internal.core.pipeline.execution.OrphanExecutionContext; import io.flamingock.internal.core.pipeline.execution.StageExecutor; import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; @@ -48,7 +46,7 @@ import java.util.Set; -public class OperationFactory { +public class OperationResolver { private static final String ARG_HISTORY = "flamingock.audit.history"; private static final String ARG_SINCE = "flamingock.audit.since"; @@ -70,7 +68,7 @@ public class OperationFactory { private final boolean isThrowExceptionIfCannotObtainLock; private final Runnable finalizer; - public OperationFactory(RunnerId runnerId, + public OperationResolver(RunnerId runnerId, FlamingockArguments flamingockArgs, LoadedPipeline pipeline, AuditPersistence persistence, @@ -97,13 +95,12 @@ public OperationFactory(RunnerId runnerId, } public RunnableOperation getOperation() { - OperationType operationType = flamingockArgs.getOperation(); - if (operationType == OperationType.EXECUTE_APPLY && coreConfiguration.isValidationOnly() && !flamingockArgs.isCliMode()) { - operationType = OperationType.VALIDATE; - } + OperationType operationType = flamingockArgs.getOperation().orElse( + coreConfiguration.isValidationOnly() ? OperationType.VALIDATE : OperationType.EXECUTE_APPLY + ); switch (operationType) { case EXECUTE_APPLY: - return getExecuteOperation(); + return getExecuteApplyOperation(); case VALIDATE: return getValidateOperation(); case AUDIT_LIST: @@ -148,17 +145,17 @@ private RunnableOperation getIssueGetOperation() { return new RunnableOperation<>(issueGetOperation, new IssueGetArgs(changeId, guidance)); } - private RunnableOperation getExecuteOperation() { + private RunnableOperation getExecuteApplyOperation() { final StageExecutor stageExecutor = new StageExecutor(dependencyContext, nonGuardedTypes, persistence, targetSystemManager, null); - ExecuteOperation executeOperation = new ExecuteOperation( - runnerId, - executionPlanner, - stageExecutor, - buildExecutionContext(coreConfiguration), - eventPublisher, - isThrowExceptionIfCannotObtainLock, - finalizer); - return new RunnableOperation<>(executeOperation, new ExecuteArgs(pipeline)); + ExecuteApplyOperation executeApplyOperation = new ExecuteApplyOperation( + runnerId, + executionPlanner, + stageExecutor, + buildExecutionContext(coreConfiguration), + eventPublisher, + isThrowExceptionIfCannotObtainLock, + finalizer); + return new RunnableOperation<>(executeApplyOperation, new ExecuteArgs(pipeline)); } private RunnableOperation getValidateOperation() { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java new file mode 100644 index 000000000..764c8eac9 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.core.operation.execute; + +import io.flamingock.internal.common.core.response.data.ExecuteResponseData; +import io.flamingock.internal.core.event.EventPublisher; +import io.flamingock.internal.core.operation.AbstractPipelineTraverseOperation; +import io.flamingock.internal.core.operation.OperationException; +import io.flamingock.internal.core.pipeline.execution.*; +import io.flamingock.internal.core.plan.ExecutionPlanner; +import io.flamingock.internal.util.id.RunnerId; + +/** + * Executes the pipeline and returns structured result data. + */ +public class ExecuteApplyOperation extends AbstractPipelineTraverseOperation { + + public ExecuteApplyOperation(RunnerId runnerId, + ExecutionPlanner executionPlanner, + StageExecutor stageExecutor, + OrphanExecutionContext orphanExecutionContext, + EventPublisher eventPublisher, + boolean throwExceptionIfCannotObtainLock, + Runnable finalizer) { + super(runnerId, executionPlanner, stageExecutor, orphanExecutionContext, eventPublisher, throwExceptionIfCannotObtainLock, finalizer); + } + + @Override + public ExecuteResult execute(ExecuteArgs args) { + ExecuteResponseData result; + try { + result = this.execute(args.getPipeline(), false); + } catch (OperationException operationException) { + result = operationException.getResult(); + throw operationException; + } catch (Throwable throwable) { + throw processAndGetFlamingockException(throwable, null); + } finally { + finalizer.run(); + } + return new ExecuteResult(result); + } +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/validate/ValidateOperation.java similarity index 87% rename from core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java rename to core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/validate/ValidateOperation.java index 5fd84d91b..fabdfafdf 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/validate/ValidateOperation.java @@ -13,12 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.internal.core.operation.execute; +package io.flamingock.internal.core.operation.validate; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.error.PendingChangesException; import io.flamingock.internal.common.core.response.data.ExecuteResponseData; import io.flamingock.internal.core.event.EventPublisher; +import io.flamingock.internal.core.operation.AbstractPipelineTraverseOperation; +import io.flamingock.internal.core.operation.execute.ExecuteArgs; +import io.flamingock.internal.core.operation.execute.ExecuteResult; import io.flamingock.internal.core.pipeline.execution.OrphanExecutionContext; import io.flamingock.internal.core.pipeline.execution.StageExecutor; import io.flamingock.internal.core.plan.ExecutionPlanner; @@ -28,7 +31,7 @@ * Validates the pipeline without executing any changes. * If pending changes exist, throws {@link PendingChangesException}. */ -public class ValidateOperation extends ExecuteOperation { +public class ValidateOperation extends AbstractPipelineTraverseOperation { public ValidateOperation(RunnerId runnerId, ExecutionPlanner executionPlanner, diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java index c1d617814..ec1900020 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import java.time.LocalDateTime; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -42,7 +43,7 @@ void shouldParseAllDefinedParametersWithEqualsFormat() { assertTrue(arguments.isCliMode()); assertTrue(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); assertTrue(arguments.getOutputFile().isPresent()); assertEquals("/tmp/output.json", arguments.getOutputFile().orElse(null)); assertTrue(arguments.getRemainingArgs().isEmpty()); @@ -59,7 +60,7 @@ void shouldParseAllDefinedParametersWithSpaceFormat() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isCliMode()); - assertEquals(OperationType.EXECUTE_ROLLBACK, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_ROLLBACK, arguments.getOperation().get()); assertEquals("/var/log/flamingock.log", arguments.getOutputFile().orElse(null)); } @@ -84,7 +85,7 @@ void shouldHandleNullArgs() { assertFalse(arguments.isCliMode()); assertFalse(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(Optional.empty(), arguments.getOperation()); assertFalse(arguments.getOutputFile().isPresent()); assertTrue(arguments.getRemainingArgs().isEmpty()); } @@ -95,7 +96,7 @@ void shouldHandleEmptyArgs() { assertFalse(arguments.isCliMode()); assertFalse(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(Optional.empty(), arguments.getOperation()); assertFalse(arguments.getOutputFile().isPresent()); assertTrue(arguments.getRemainingArgs().isEmpty()); } @@ -128,7 +129,7 @@ void shouldTreatBooleanFlagFollowedByAnotherFlagAsTrue() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isCliMode()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } @Test @@ -138,7 +139,7 @@ void shouldReturnDefaultOperationWhenNotProvided() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertFalse(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } @Test @@ -149,7 +150,7 @@ void shouldParseAllValidOperationTypes() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isOperationProvided()); - assertEquals(expectedType, arguments.getOperation(), + assertEquals(expectedType, arguments.getOperation().get(), "Failed to parse operation type: " + expectedType); } } @@ -160,7 +161,7 @@ void shouldParseOperationTypeCaseInsensitively() { FlamingockArguments arguments = FlamingockArguments.parse(args); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } @Test @@ -275,7 +276,7 @@ void shouldHandleMixedFormats() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isCliMode()); - assertEquals(OperationType.EXECUTE_DRYRUN, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_DRYRUN, arguments.getOperation().get()); assertEquals("/output.json", arguments.getOutputFile().orElse(null)); assertEquals("customValue", arguments.getRemainingArgs().get("custom.prop")); } @@ -300,7 +301,7 @@ void shouldReturnIsOperationProvidedTrueWhenOperationExplicitlyPassed() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } @Test @@ -310,7 +311,7 @@ void shouldReturnIsOperationProvidedFalseWhenUsingDefault() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertFalse(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation()); + assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } // ========== Typed Accessor Methods Tests ========== diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ExecuteOperationTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ExecuteApplyOperationTest.java similarity index 96% rename from core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ExecuteOperationTest.java rename to core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ExecuteApplyOperationTest.java index f8842393a..869b62d74 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ExecuteOperationTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ExecuteApplyOperationTest.java @@ -21,8 +21,8 @@ import io.flamingock.internal.common.core.response.data.StageResult; import io.flamingock.internal.common.core.response.data.StageStatus; import io.flamingock.internal.core.event.EventPublisher; +import io.flamingock.internal.core.operation.execute.ExecuteApplyOperation; import io.flamingock.internal.core.operation.execute.ExecuteArgs; -import io.flamingock.internal.core.operation.execute.ExecuteOperation; import io.flamingock.internal.core.operation.execute.ExecuteResult; import io.flamingock.internal.core.pipeline.execution.OrphanExecutionContext; import io.flamingock.internal.core.pipeline.execution.StageExecutionException; @@ -48,9 +48,9 @@ import static org.mockito.Mockito.*; /** - * Tests for ExecuteOperation - executes the pipeline and returns structured result data. + * Tests for ExecuteApplyOperation - executes the pipeline and returns structured result data. */ -class ExecuteOperationTest { +class ExecuteApplyOperationTest { @Mock private ExecutionPlanner executionPlanner; @@ -70,7 +70,7 @@ class ExecuteOperationTest { @Mock private AbstractLoadedTask loadedTask; - private ExecuteOperation operation; + private ExecuteApplyOperation operation; private RunnerId runnerId; private OrphanExecutionContext orphanContext; private Runnable noOpFinalizer; @@ -82,7 +82,7 @@ void setUp() { orphanContext = new OrphanExecutionContext("localhost", new HashMap<>()); noOpFinalizer = () -> {}; - operation = new ExecuteOperation( + operation = new ExecuteApplyOperation( runnerId, executionPlanner, stageExecutor, diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationFactoryTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationResolverTest.java similarity index 89% rename from core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationFactoryTest.java rename to core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationResolverTest.java index 0ccbaa956..503537e4c 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationFactoryTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationResolverTest.java @@ -21,7 +21,7 @@ import io.flamingock.internal.core.event.EventPublisher; import io.flamingock.internal.core.external.store.audit.AuditPersistence; import io.flamingock.internal.core.external.targets.TargetSystemManager; -import io.flamingock.internal.core.operation.execute.ValidateOperation; +import io.flamingock.internal.core.operation.validate.ValidateOperation; import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; import io.flamingock.internal.core.pipeline.loaded.stage.AbstractLoadedStage; import io.flamingock.internal.core.plan.ExecutionPlanner; @@ -37,15 +37,16 @@ import java.lang.reflect.Field; import java.util.Collections; import java.util.HashSet; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.when; /** - * Tests for OperationFactory — routing logic for creating the appropriate operation. + * Tests for OperationResolver — routing logic for creating the appropriate operation. */ -class OperationFactoryTest { +class OperationResolverTest { @Mock private FlamingockArguments flamingockArgs; @@ -86,7 +87,7 @@ void setUp() { runnerId = RunnerId.fromString("test-runner@localhost#test-uuid"); noOpFinalizer = () -> {}; - // Default pipeline setup so OperationFactory does not NPE on pipeline access + // Default pipeline setup so OperationResolver does not NPE on pipeline access when(pipeline.getSystemStage()).thenReturn(java.util.Optional.empty()); when(pipeline.getStages()).thenReturn(Collections.singletonList(loadedStage)); when(loadedStage.getTasks()).thenReturn(Collections.singletonList(loadedTask)); @@ -99,10 +100,10 @@ void setUp() { @DisplayName("validationOnly=true with EXECUTE_APPLY → getOperation() routes to ValidateOperation") void shouldRouteToValidateOperationWhenValidationOnlyIsTrue() throws Exception { // Given - when(flamingockArgs.getOperation()).thenReturn(OperationType.EXECUTE_APPLY); + when(flamingockArgs.getOperation()).thenReturn(Optional.of(OperationType.VALIDATE)); when(coreConfiguration.isValidationOnly()).thenReturn(true); - OperationFactory factory = new OperationFactory( + OperationResolver factory = new OperationResolver( runnerId, flamingockArgs, pipeline, @@ -131,10 +132,10 @@ void shouldRouteToValidateOperationWhenValidationOnlyIsTrue() throws Exception { @DisplayName("validationOnly=false with EXECUTE_APPLY → getOperation() does NOT route to ValidateOperation") void shouldNotRouteToValidateOperationWhenValidationOnlyIsFalse() throws Exception { // Given - when(flamingockArgs.getOperation()).thenReturn(OperationType.EXECUTE_APPLY); + when(flamingockArgs.getOperation()).thenReturn(Optional.of(OperationType.EXECUTE_APPLY)); when(coreConfiguration.isValidationOnly()).thenReturn(false); - OperationFactory factory = new OperationFactory( + OperationResolver factory = new OperationResolver( runnerId, flamingockArgs, pipeline, @@ -155,7 +156,7 @@ void shouldNotRouteToValidateOperationWhenValidationOnlyIsFalse() throws Excepti // Then assertNotNull(runnableOperation); Operation innerOperation = extractInnerOperation(runnableOperation); - // When validationOnly=false the standard ExecuteOperation is used, not ValidateOperation + // When validationOnly=false the standard AbstractPipelineTraverseOperation is used, not ValidateOperation assertNotNull(innerOperation); // Verify it is NOT a ValidateOperation boolean isValidateOp = innerOperation instanceof ValidateOperation; diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ValidateOperationTest.java similarity index 96% rename from core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java rename to core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ValidateOperationTest.java index a6624e6a1..216e3043f 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/execute/ValidateOperationTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/ValidateOperationTest.java @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.internal.core.operation.execute; +package io.flamingock.internal.core.operation; import io.flamingock.internal.common.core.error.PendingChangesException; import io.flamingock.internal.common.core.response.data.ExecutionStatus; import io.flamingock.internal.core.event.EventPublisher; +import io.flamingock.internal.core.operation.execute.ExecuteArgs; +import io.flamingock.internal.core.operation.execute.ExecuteResult; +import io.flamingock.internal.core.operation.validate.ValidateOperation; import io.flamingock.internal.core.pipeline.execution.ExecutablePipeline; import io.flamingock.internal.core.pipeline.execution.ExecutableStage; import io.flamingock.internal.core.pipeline.execution.OrphanExecutionContext; From eeb6d7a6298fb2733353b1f23089ca75191d4b36 Mon Sep 17 00:00:00 2001 From: bercianor Date: Fri, 27 Mar 2026 11:12:49 +0000 Subject: [PATCH 6/9] fix: remove operationProvided, fix a test and an Optional.Get() --- .../builder/args/FlamingockArguments.java | 13 ++------ .../core/builder/runner/CliRunner.java | 10 +++---- .../core/builder/runner/RunnerFactory.java | 2 +- .../builder/args/FlamingockArgumentsTest.java | 30 ++++++++----------- .../core/operation/OperationResolverTest.java | 4 +-- 5 files changed, 23 insertions(+), 36 deletions(-) diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java index eb63b9230..d92328917 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java @@ -35,25 +35,22 @@ public class FlamingockArguments { private final boolean cliMode; private final OperationType operation; - private final boolean operationProvided; private final String outputFile; private final Map remainingArgs; private FlamingockArguments(boolean cliMode, OperationType operation, - boolean operationProvided, String outputFile, Map remainingArgs) { this.cliMode = cliMode; this.operation = operation; - this.operationProvided = operationProvided; this.outputFile = outputFile; this.remainingArgs = Collections.unmodifiableMap(remainingArgs); } public static FlamingockArguments parse(String[] args) { if (args == null || args.length == 0) { - return new FlamingockArguments(false, null, false, null, Collections.emptyMap()); + return new FlamingockArguments(false, null, null, Collections.emptyMap()); } boolean cliMode = false; @@ -105,8 +102,8 @@ public static FlamingockArguments parse(String[] args) { } } - OperationType effectiveOperation = operationProvided ? operation : OperationType.EXECUTE_APPLY; - return new FlamingockArguments(cliMode, effectiveOperation, operationProvided, outputFile, remaining); + OperationType effectiveOperation = operationProvided ? operation : null; + return new FlamingockArguments(cliMode, effectiveOperation, outputFile, remaining); } private static boolean parseBoolean(String key, String value) { @@ -154,10 +151,6 @@ public Optional getOperation() { return Optional.ofNullable(operation); } - public boolean isOperationProvided() { - return operationProvided; - } - public Optional getOutputFile() { return Optional.ofNullable(outputFile); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java index 50d8fd75f..f2680345e 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java @@ -18,8 +18,8 @@ import io.flamingock.internal.common.core.response.ResponseChannel; import io.flamingock.internal.common.core.response.ResponseEnvelope; import io.flamingock.internal.common.core.response.ResponseError; +import io.flamingock.internal.core.builder.args.FlamingockArguments; import io.flamingock.internal.core.operation.AbstractOperationResult; -import io.flamingock.internal.common.core.operation.OperationType; import io.flamingock.internal.core.operation.RunnableOperation; import io.flamingock.internal.util.log.FlamingockLoggerFactory; import org.slf4j.Logger; @@ -35,16 +35,16 @@ public class CliRunner implements Runner { private final RunnableOperation operation; private final Runnable finalizer; private final ResponseChannel channel; - private final OperationType operationType; + private final FlamingockArguments flamingockArgs; public CliRunner(RunnableOperation operation, Runnable finalizer, ResponseChannel channel, - OperationType operationType) { + FlamingockArguments flamingockArgs) { this.operation = operation; this.finalizer = finalizer; this.channel = channel; - this.operationType = operationType; + this.flamingockArgs = flamingockArgs; } @Override @@ -92,7 +92,7 @@ public void run() { private void writeResponse(AbstractOperationResult result, Throwable error, long durationMs) { ResponseEnvelope envelope; - String operationName = operationType.name(); + String operationName = flamingockArgs.getOperation().isPresent() ? flamingockArgs.getOperation().get().name() : "unknown operation"; if (error != null) { envelope = ResponseEnvelope.failure(operationName, ResponseError.from(error), durationMs); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java index 55ebf9d00..f049b2837 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java @@ -60,7 +60,7 @@ private Runner createCliRunner() { .map(outputFile -> (ResponseChannel) new FileResponseChannel(outputFile, JsonObjectMapper.DEFAULT_INSTANCE)) .orElseGet(NoOpResponseChannel::new); - return new CliRunner(operation, finalizer, channel, flamingockArgs.getOperation().get()); + return new CliRunner(operation, finalizer, channel, flamingockArgs); } private Runner createDefaultRunner() { diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java index ec1900020..bbe4afdc8 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/args/FlamingockArgumentsTest.java @@ -42,7 +42,7 @@ void shouldParseAllDefinedParametersWithEqualsFormat() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isCliMode()); - assertTrue(arguments.isOperationProvided()); + assertTrue(arguments.getOperation().isPresent()); assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); assertTrue(arguments.getOutputFile().isPresent()); assertEquals("/tmp/output.json", arguments.getOutputFile().orElse(null)); @@ -60,6 +60,7 @@ void shouldParseAllDefinedParametersWithSpaceFormat() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isCliMode()); + assertTrue(arguments.getOperation().isPresent()); assertEquals(OperationType.EXECUTE_ROLLBACK, arguments.getOperation().get()); assertEquals("/var/log/flamingock.log", arguments.getOutputFile().orElse(null)); } @@ -84,7 +85,7 @@ void shouldHandleNullArgs() { FlamingockArguments arguments = FlamingockArguments.parse(null); assertFalse(arguments.isCliMode()); - assertFalse(arguments.isOperationProvided()); + assertFalse(arguments.getOperation().isPresent()); assertEquals(Optional.empty(), arguments.getOperation()); assertFalse(arguments.getOutputFile().isPresent()); assertTrue(arguments.getRemainingArgs().isEmpty()); @@ -95,7 +96,7 @@ void shouldHandleEmptyArgs() { FlamingockArguments arguments = FlamingockArguments.parse(new String[0]); assertFalse(arguments.isCliMode()); - assertFalse(arguments.isOperationProvided()); + assertFalse(arguments.getOperation().isPresent()); assertEquals(Optional.empty(), arguments.getOperation()); assertFalse(arguments.getOutputFile().isPresent()); assertTrue(arguments.getRemainingArgs().isEmpty()); @@ -129,17 +130,18 @@ void shouldTreatBooleanFlagFollowedByAnotherFlagAsTrue() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isCliMode()); + assertTrue(arguments.getOperation().isPresent()); assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } @Test - void shouldReturnDefaultOperationWhenNotProvided() { + void shouldReturnNullOperationWhenNotProvided() { String[] args = {"--flamingock.cli.mode=true"}; FlamingockArguments arguments = FlamingockArguments.parse(args); - assertFalse(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); + assertFalse(arguments.getOperation().isPresent()); + assertNull(arguments.getOperation().orElse(null)); } @Test @@ -149,7 +151,7 @@ void shouldParseAllValidOperationTypes() { FlamingockArguments arguments = FlamingockArguments.parse(args); - assertTrue(arguments.isOperationProvided()); + assertTrue(arguments.getOperation().isPresent()); assertEquals(expectedType, arguments.getOperation().get(), "Failed to parse operation type: " + expectedType); } @@ -161,6 +163,7 @@ void shouldParseOperationTypeCaseInsensitively() { FlamingockArguments arguments = FlamingockArguments.parse(args); + assertTrue(arguments.getOperation().isPresent()); assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } @@ -276,6 +279,7 @@ void shouldHandleMixedFormats() { FlamingockArguments arguments = FlamingockArguments.parse(args); assertTrue(arguments.isCliMode()); + assertTrue(arguments.getOperation().isPresent()); assertEquals(OperationType.EXECUTE_DRYRUN, arguments.getOperation().get()); assertEquals("/output.json", arguments.getOutputFile().orElse(null)); assertEquals("customValue", arguments.getRemainingArgs().get("custom.prop")); @@ -300,17 +304,7 @@ void shouldReturnIsOperationProvidedTrueWhenOperationExplicitlyPassed() { FlamingockArguments arguments = FlamingockArguments.parse(args); - assertTrue(arguments.isOperationProvided()); - assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); - } - - @Test - void shouldReturnIsOperationProvidedFalseWhenUsingDefault() { - String[] args = {"--flamingock.cli.mode=true"}; - - FlamingockArguments arguments = FlamingockArguments.parse(args); - - assertFalse(arguments.isOperationProvided()); + assertTrue(arguments.getOperation().isPresent()); assertEquals(OperationType.EXECUTE_APPLY, arguments.getOperation().get()); } diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationResolverTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationResolverTest.java index 503537e4c..d07c113f2 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationResolverTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/OperationResolverTest.java @@ -97,10 +97,10 @@ void setUp() { } @Test - @DisplayName("validationOnly=true with EXECUTE_APPLY → getOperation() routes to ValidateOperation") + @DisplayName("validationOnly=true with no operation → getOperation() routes to ValidateOperation") void shouldRouteToValidateOperationWhenValidationOnlyIsTrue() throws Exception { // Given - when(flamingockArgs.getOperation()).thenReturn(Optional.of(OperationType.VALIDATE)); + when(flamingockArgs.getOperation()).thenReturn(Optional.empty()); when(coreConfiguration.isValidationOnly()).thenReturn(true); OperationResolver factory = new OperationResolver( From 18b2194cffc029f504d526e9d7cbd8f93a64989e Mon Sep 17 00:00:00 2001 From: bercianor Date: Fri, 27 Mar 2026 11:19:27 +0000 Subject: [PATCH 7/9] fix: operation name should be without "operation" sufix --- .../io/flamingock/internal/core/builder/runner/CliRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java index f2680345e..3a00b5430 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java @@ -92,7 +92,7 @@ public void run() { private void writeResponse(AbstractOperationResult result, Throwable error, long durationMs) { ResponseEnvelope envelope; - String operationName = flamingockArgs.getOperation().isPresent() ? flamingockArgs.getOperation().get().name() : "unknown operation"; + String operationName = flamingockArgs.getOperation().isPresent() ? flamingockArgs.getOperation().get().name() : "unknown"; if (error != null) { envelope = ResponseEnvelope.failure(operationName, ResponseError.from(error), durationMs); From 2e61fe7e41e7c98bdbf8ca24046ff250b9641ab9 Mon Sep 17 00:00:00 2001 From: bercianor Date: Sat, 28 Mar 2026 15:47:18 +0000 Subject: [PATCH 8/9] fix: some PR comments --- .../core/builder/runner/CliRunner.java | 4 +++- .../AbstractPipelineTraverseOperation.java | 21 +++++++++++++++++-- .../execute/ExecuteApplyOperation.java | 17 ++------------- .../operation/validate/ValidateOperation.java | 18 ++-------------- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java index 3a00b5430..0c694ce20 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java @@ -92,7 +92,9 @@ public void run() { private void writeResponse(AbstractOperationResult result, Throwable error, long durationMs) { ResponseEnvelope envelope; - String operationName = flamingockArgs.getOperation().isPresent() ? flamingockArgs.getOperation().get().name() : "unknown"; + String operationName = flamingockArgs.getOperation() + .map(Enum::name) + .orElse("unknown"); if (error != null) { envelope = ResponseEnvelope.failure(operationName, ResponseError.from(error), durationMs); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java index dbb9a9000..330baa526 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java @@ -85,6 +85,23 @@ public AbstractPipelineTraverseOperation(RunnerId runnerId, this.finalizer = finalizer; } + protected abstract boolean validateOnlyMode(); + + @Override + public ExecuteResult execute(ExecuteArgs args) { + ExecuteResponseData result; + try { + result = this.execute(args.getPipeline()); + } catch (OperationException operationException) { + throw operationException; + } catch (Throwable throwable) { + throw processAndGetFlamingockException(throwable, null); + } finally { + this.finalizer.run(); + } + return new ExecuteResult(result); + } + private static List validateAndGetExecutableStages(LoadedPipeline pipeline) { pipeline.validate(); List stages = new ArrayList<>(); @@ -95,7 +112,7 @@ private static List validateAndGetExecutableStages(LoadedPi return stages; } - protected ExecuteResponseData execute(LoadedPipeline pipeline, Boolean validateOnly) throws FlamingockException { + protected ExecuteResponseData execute(LoadedPipeline pipeline) throws FlamingockException { List allStages = validateAndGetExecutableStages(pipeline); int stageCount = allStages.size(); long changeCount = allStages.stream() @@ -114,7 +131,7 @@ protected ExecuteResponseData execute(LoadedPipeline pipeline, Boolean validateO execution.validate(); if (execution.isExecutionRequired()) { - if (validateOnly) { + if (validateOnlyMode()) { throw new PendingChangesException(); } execution.applyOnEach((executionId, lock, executableStage) -> { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java index 764c8eac9..7cca98b39 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteApplyOperation.java @@ -15,10 +15,8 @@ */ package io.flamingock.internal.core.operation.execute; -import io.flamingock.internal.common.core.response.data.ExecuteResponseData; import io.flamingock.internal.core.event.EventPublisher; import io.flamingock.internal.core.operation.AbstractPipelineTraverseOperation; -import io.flamingock.internal.core.operation.OperationException; import io.flamingock.internal.core.pipeline.execution.*; import io.flamingock.internal.core.plan.ExecutionPlanner; import io.flamingock.internal.util.id.RunnerId; @@ -39,18 +37,7 @@ public ExecuteApplyOperation(RunnerId runnerId, } @Override - public ExecuteResult execute(ExecuteArgs args) { - ExecuteResponseData result; - try { - result = this.execute(args.getPipeline(), false); - } catch (OperationException operationException) { - result = operationException.getResult(); - throw operationException; - } catch (Throwable throwable) { - throw processAndGetFlamingockException(throwable, null); - } finally { - finalizer.run(); - } - return new ExecuteResult(result); + protected boolean validateOnlyMode() { + return false; } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/validate/ValidateOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/validate/ValidateOperation.java index fabdfafdf..e5927eb3e 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/validate/ValidateOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/validate/ValidateOperation.java @@ -15,13 +15,9 @@ */ package io.flamingock.internal.core.operation.validate; -import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.error.PendingChangesException; -import io.flamingock.internal.common.core.response.data.ExecuteResponseData; import io.flamingock.internal.core.event.EventPublisher; import io.flamingock.internal.core.operation.AbstractPipelineTraverseOperation; -import io.flamingock.internal.core.operation.execute.ExecuteArgs; -import io.flamingock.internal.core.operation.execute.ExecuteResult; import io.flamingock.internal.core.pipeline.execution.OrphanExecutionContext; import io.flamingock.internal.core.pipeline.execution.StageExecutor; import io.flamingock.internal.core.plan.ExecutionPlanner; @@ -44,17 +40,7 @@ public ValidateOperation(RunnerId runnerId, } @Override - public ExecuteResult execute(ExecuteArgs args) { - ExecuteResponseData result; - try { - result = super.execute(args.getPipeline(), true); - } catch (FlamingockException flamingockException) { - throw flamingockException; - } catch (Throwable throwable) { - throw new FlamingockException(throwable); - } finally { - super.finalizer.run(); - } - return new ExecuteResult(result); + protected boolean validateOnlyMode() { + return true; } } From addaae55e1e4ff2ed4261d8931f2660032641a31 Mon Sep 17 00:00:00 2001 From: bercianor Date: Sat, 28 Mar 2026 15:49:14 +0000 Subject: [PATCH 9/9] fix: change some protected methods to private again --- .../core/operation/AbstractPipelineTraverseOperation.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java index 330baa526..2903a1a39 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java @@ -112,7 +112,7 @@ private static List validateAndGetExecutableStages(LoadedPi return stages; } - protected ExecuteResponseData execute(LoadedPipeline pipeline) throws FlamingockException { + private ExecuteResponseData execute(LoadedPipeline pipeline) throws FlamingockException { List allStages = validateAndGetExecutableStages(pipeline); int stageCount = allStages.size(); long changeCount = allStages.stream() @@ -193,7 +193,7 @@ private StageResult startStage(String executionId, Lock lock, ExecutableStage ex return executionOutput.getResult(); } - protected FlamingockException processAndGetFlamingockException(Throwable exception, ExecutionResultBuilder resultBuilder) throws FlamingockException { + private FlamingockException processAndGetFlamingockException(Throwable exception, ExecutionResultBuilder resultBuilder) throws FlamingockException { FlamingockException flamingockException; if (exception instanceof OperationException) { OperationException pipelineException = (OperationException) exception;