Fix Error/Success Conditions not evaluated case. Fix 'Cancel' final s… (#3110)
* Fix Error/Success Conditions not evaluated case. Fix 'Cancel' final status not mapped - now mapped to Error. Fix Delete Action does not update properly group count and percentage evaluation. Signed-off-by: vasilchev <vasil.ilchev@bosch.com> * Add ThreshholdRolloutGroupSuccessCondition Signed-off-by: vasilchev <vasil.ilchev@bosch.com> * Cancel Action not included in ERROR/SUCCESS. Trigger SuccessAction on SuccessCondition/GroupFINISHED. Signed-off-by: vasilchev <vasil.ilchev@bosch.com> * Review Findings add comments to tests Signed-off-by: vasilchev <vasil.ilchev@bosch.com> --------- Signed-off-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
@@ -15,9 +15,6 @@ import org.eclipse.hawkbit.repository.model.Action;
|
|||||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||||
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates if the {@link RolloutGroup#getErrorConditionExp()} is reached.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ThresholdRolloutGroupErrorCondition
|
public class ThresholdRolloutGroupErrorCondition
|
||||||
implements RolloutGroupConditionEvaluator<RolloutGroup.RolloutGroupErrorCondition> {
|
implements RolloutGroupConditionEvaluator<RolloutGroup.RolloutGroupErrorCondition> {
|
||||||
|
|||||||
@@ -445,10 +445,11 @@ public class JpaRolloutExecutor implements RolloutExecutor {
|
|||||||
if (isError) {
|
if (isError) {
|
||||||
log.info("Rollout {} {} has error, calling error action", rollout.getName(), rollout.getId());
|
log.info("Rollout {} {} has error, calling error action", rollout.getName(), rollout.getId());
|
||||||
callErrorAction(rollout, rolloutGroup);
|
callErrorAction(rollout, rolloutGroup);
|
||||||
} else {
|
} else {// not in error so check success condition and group completed
|
||||||
// not in error so check finished state, do we need to start the next group?
|
// 'success' is either group completed or success condition reached - execute 'success' Action
|
||||||
checkSuccessCondition(rollout, rolloutGroup, evalProxy, rolloutGroup.getSuccessCondition());
|
final boolean groupCompleted = !(rolloutGroup == lastGroup && rolloutGroup.isDynamic()) && isRolloutGroupComplete(rollout, rolloutGroup);
|
||||||
if (!(rolloutGroup == lastGroup && rolloutGroup.isDynamic()) && isRolloutGroupComplete(rollout, rolloutGroup)) {
|
checkSuccessCondition(rollout, rolloutGroup, evalProxy, rolloutGroup.getSuccessCondition(), groupCompleted);
|
||||||
|
if (groupCompleted) {
|
||||||
rolloutGroup.setStatus(RolloutGroupStatus.FINISHED);
|
rolloutGroup.setStatus(RolloutGroupStatus.FINISHED);
|
||||||
rolloutGroupRepository.save(rolloutGroup);
|
rolloutGroupRepository.save(rolloutGroup);
|
||||||
}
|
}
|
||||||
@@ -466,11 +467,9 @@ public class JpaRolloutExecutor implements RolloutExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private long countTargetsFrom(final JpaRolloutGroup rolloutGroup) {
|
private long countTargetsFrom(final JpaRolloutGroup rolloutGroup) {
|
||||||
if (rolloutGroup.isDynamic()) {
|
// Use action-based count for all groups: deleting an action removes the target from the count,
|
||||||
return countByActionsInRolloutGroup(rolloutGroup.getId());
|
// keeping totalTargets consistent with the actual denominator used by condition evaluators.
|
||||||
} else {
|
return countByActionsInRolloutGroup(rolloutGroup.getId());
|
||||||
return rolloutGroupManagement.countTargetsOfRolloutsGroup(rolloutGroup.getId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void callErrorAction(final Rollout rollout, final RolloutGroup rolloutGroup) {
|
private void callErrorAction(final Rollout rollout, final RolloutGroup rolloutGroup) {
|
||||||
@@ -507,14 +506,13 @@ public class JpaRolloutExecutor implements RolloutExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkSuccessCondition(final Rollout rollout, final RolloutGroup rolloutGroup, final RolloutGroup evalProxy,
|
private void checkSuccessCondition(final Rollout rollout, final RolloutGroup rolloutGroup, final RolloutGroup evalProxy,
|
||||||
final RolloutGroupSuccessCondition successCondition) {
|
final RolloutGroupSuccessCondition successCondition, final boolean groupCompleted) {
|
||||||
log.trace("Checking finish condition {} on rolloutgroup {}", successCondition, rolloutGroup);
|
log.trace("Checking finish condition {} on rolloutgroup {}", successCondition, rolloutGroup);
|
||||||
try {
|
try {
|
||||||
final boolean isFinished = evaluationManager
|
if (groupCompleted || evaluationManager
|
||||||
.getSuccessConditionEvaluator(successCondition)
|
.getSuccessConditionEvaluator(successCondition)
|
||||||
.eval(rollout, evalProxy, rolloutGroup.getSuccessConditionExp());
|
.eval(rollout, evalProxy, rolloutGroup.getSuccessConditionExp())) {
|
||||||
if (isFinished) {
|
log.debug("Rollout group {} fulfills SuccessCondition or is Finished, executing Success Action", rolloutGroup);
|
||||||
log.debug("Rollout group {} is finished, starting next group", rolloutGroup);
|
|
||||||
evaluationManager.getSuccessActionEvaluator(rolloutGroup.getSuccessAction()).exec(rollout, rolloutGroup);
|
evaluationManager.getSuccessActionEvaluator(rolloutGroup.getSuccessAction()).exec(rollout, rolloutGroup);
|
||||||
} else {
|
} else {
|
||||||
log.debug("Rollout group {} is still running", rolloutGroup);
|
log.debug("Rollout group {} is still running", rolloutGroup);
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Contributors to the Eclipse Foundation
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made
|
||||||
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.eclipse.hawkbit.repository.jpa.management;
|
||||||
|
|
||||||
|
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.callAs;
|
||||||
|
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action.ActionStatusCreate;
|
||||||
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
||||||
|
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus;
|
||||||
|
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessAction;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.domain.Sort.Direction;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for rollout group condition evaluation — group cascade behavior.
|
||||||
|
*
|
||||||
|
* Every test uses a 2-group rollout so both groups' final state is verified:
|
||||||
|
* - group 1: the group under test
|
||||||
|
* - group 2: verifies cascade (SCHEDULED = did not start, RUNNING = was triggered)
|
||||||
|
*
|
||||||
|
* Success-action tests use PAUSE: PauseRolloutGroupSuccessAction only fires when a
|
||||||
|
* scheduled child group exists, making success-action execution observable as
|
||||||
|
* RolloutStatus.PAUSED without advancing to group 2.
|
||||||
|
*
|
||||||
|
* Decision-log rules under test:
|
||||||
|
* - Success Action fires on SuccessCondition OR GroupFINISHED
|
||||||
|
* - CANCEL not mapped to Error (ignored in condition evaluation)
|
||||||
|
* - Target's Action count used as Group Target count
|
||||||
|
*/
|
||||||
|
@TestPropertySource(properties = { "hawkbit.server.repository.dynamicRolloutsMinInvolvePeriodMS=-1" })
|
||||||
|
class RolloutGroupConditionTest extends AbstractJpaIntegrationTest {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void reset() {
|
||||||
|
this.approvalStrategy.setApprovalNeeded(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// CANCELED ignored; group completion triggers success action
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Test
|
||||||
|
void canceledActionIgnoredGroupCompletionTriggersSuccessAction() {
|
||||||
|
final String targetPrefix = "cancelTarget";
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSetLocked("cancelTargetDs");
|
||||||
|
testdataFactory.createTargets(targetPrefix, 10);
|
||||||
|
|
||||||
|
final Rollout rollout = rolloutWith2Groups(
|
||||||
|
"cancelTargetRollout", targetPrefix, ds, "100", RolloutGroupSuccessAction.PAUSE, "10");
|
||||||
|
final List<RolloutGroup> groups = getGroups(rollout);
|
||||||
|
|
||||||
|
rolloutManagement.start(rollout.getId());
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
final List<JpaAction> running = assertAndGetRunning(rollout, 5).getContent();
|
||||||
|
running.subList(0, 4).forEach(this::finishAction);
|
||||||
|
setCanceled(running.get(4));
|
||||||
|
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
assertGroup(groups.get(0), false, RolloutGroupStatus.FINISHED, 5);
|
||||||
|
assertGroup(groups.get(1), false, RolloutGroupStatus.SCHEDULED, 5);
|
||||||
|
assertRollout(rollout, false, RolloutStatus.PAUSED, 2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Test
|
||||||
|
void groupCompleteNeitherConditionFulfilledTriggersSuccessAction() {
|
||||||
|
final String targetPrefix = "subthreshold";
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSetLocked("subthresholdDs");
|
||||||
|
testdataFactory.createTargets(targetPrefix, 10);
|
||||||
|
|
||||||
|
final Rollout rollout = rolloutWith2Groups(
|
||||||
|
"subthreshRollout", targetPrefix, ds, "100", RolloutGroupSuccessAction.PAUSE, "50");
|
||||||
|
final List<RolloutGroup> groups = getGroups(rollout);
|
||||||
|
|
||||||
|
rolloutManagement.start(rollout.getId());
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
final List<JpaAction> running = assertAndGetRunning(rollout, 5).getContent();
|
||||||
|
// 3/5 SUCCESS, 2/5 ERRORS, neither condition fulfilled (S:100, E:50)
|
||||||
|
// but Group Finishes 5/5 -> execute SuccessAction#PAUSE
|
||||||
|
running.subList(0, 3).forEach(this::finishAction);
|
||||||
|
running.subList(3, 5).forEach(this::reportError);
|
||||||
|
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
assertGroup(groups.get(0), false, RolloutGroupStatus.FINISHED, 5);
|
||||||
|
assertGroup(groups.get(1), false, RolloutGroupStatus.SCHEDULED, 5);
|
||||||
|
assertRollout(rollout, false, RolloutStatus.PAUSED, 2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Test
|
||||||
|
void successConditionMetBeforeGroupFinishedTriggersSuccessAction() {
|
||||||
|
final String targetPrefix = "scBefore";
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSetLocked("scBeforeDs");
|
||||||
|
testdataFactory.createTargets(targetPrefix, 10);
|
||||||
|
|
||||||
|
final Rollout rollout = rolloutWith2Groups(
|
||||||
|
"scBeforeRollout", targetPrefix, ds, "60", RolloutGroupSuccessAction.PAUSE, "90");
|
||||||
|
final List<RolloutGroup> groups = getGroups(rollout);
|
||||||
|
|
||||||
|
rolloutManagement.start(rollout.getId());
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
final List<JpaAction> group1Actions = assertAndGetRunning(rollout, 5).getContent();
|
||||||
|
// 3/5 SUCCESS, 2/5 RUNNING -> fulfill 60% SuccessCondition => Trigger SuccessAction#PAUSE
|
||||||
|
group1Actions.subList(0, 3).forEach(this::finishAction);
|
||||||
|
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
assertGroup(groups.get(0), false, RolloutGroupStatus.RUNNING, 5);
|
||||||
|
assertGroup(groups.get(1), false, RolloutGroupStatus.SCHEDULED, 5);
|
||||||
|
assertRollout(rollout, false, RolloutStatus.PAUSED, 2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Test
|
||||||
|
void deletedActionUpdatesGroupCountAndSuccessFires() {
|
||||||
|
final String targetPrefix = "denomSuccess";
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSetLocked("denomSuccessDs");
|
||||||
|
testdataFactory.createTargets(targetPrefix, 10);
|
||||||
|
|
||||||
|
final Rollout rollout = rolloutWith2Groups(
|
||||||
|
"denomSuccessRollout", targetPrefix, ds, "100", RolloutGroupSuccessAction.PAUSE, "10");
|
||||||
|
final List<RolloutGroup> groups = getGroups(rollout);
|
||||||
|
|
||||||
|
rolloutManagement.start(rollout.getId());
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
final List<JpaAction> running = assertAndGetRunning(rollout, 5).getContent();
|
||||||
|
// 4/5 SUCCESS, 1 DELETE -> 4/4 SUCCESS fulfill SuccessCondition 100% => Trigger SuccessAction#PAUSE
|
||||||
|
actionRepository.deleteById(running.get(4).getId());
|
||||||
|
running.subList(0, 4).forEach(this::finishAction);
|
||||||
|
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
assertGroup(groups.get(0), false, RolloutGroupStatus.FINISHED, 4);
|
||||||
|
assertGroup(groups.get(1), false, RolloutGroupStatus.SCHEDULED, 5);
|
||||||
|
assertRollout(rollout, false, RolloutStatus.PAUSED, 2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Test
|
||||||
|
void deletedActionUpdatesErrorDenominator() {
|
||||||
|
final String targetPrefix = "denomError";
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSetLocked("denomErrorDs");
|
||||||
|
testdataFactory.createTargets(targetPrefix, 10);
|
||||||
|
|
||||||
|
final Rollout rollout = rolloutWith2Groups(
|
||||||
|
"denomErrorRollout", targetPrefix, ds, "100", RolloutGroupSuccessAction.PAUSE, "23");
|
||||||
|
final List<RolloutGroup> groups = getGroups(rollout);
|
||||||
|
|
||||||
|
rolloutManagement.start(rollout.getId());
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
final List<JpaAction> running = assertAndGetRunning(rollout, 5).getContent();
|
||||||
|
// 3/5 SUCCESS, 1 DELETE -> 3/4 SUCCESS, 1/4 ERROR fulfill ErrorCondition >23% => Trigger ErrorAction#PAUSE
|
||||||
|
actionRepository.deleteById(running.get(4).getId());
|
||||||
|
running.subList(0, 3).forEach(this::finishAction);
|
||||||
|
reportError(running.get(3));
|
||||||
|
|
||||||
|
rolloutHandler.handleAll();
|
||||||
|
|
||||||
|
assertGroup(groups.get(0), false, RolloutGroupStatus.ERROR, 4);
|
||||||
|
assertGroup(groups.get(1), false, RolloutGroupStatus.SCHEDULED, 5);
|
||||||
|
assertRollout(rollout, false, RolloutStatus.PAUSED, 2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rollout rolloutWith2Groups(
|
||||||
|
final String name, final String targetPrefix, final DistributionSet ds,
|
||||||
|
final String successCondition, final RolloutGroupSuccessAction successAction,
|
||||||
|
final String errorCondition) throws Exception {
|
||||||
|
return callAs(
|
||||||
|
withUser(name + "User", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"),
|
||||||
|
() -> testdataFactory.createRolloutByVariables(name, name, 2,
|
||||||
|
"controllerid==" + targetPrefix + "*", ds,
|
||||||
|
successCondition, successAction, errorCondition, null, null, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RolloutGroup> getGroups(final Rollout rollout) {
|
||||||
|
return rolloutGroupManagement.findByRollout(
|
||||||
|
rollout.getId(), new OffsetBasedPageRequest(0, 10, Sort.by(Direction.ASC, "id"))).getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCanceled(final JpaAction action) {
|
||||||
|
action.setStatus(Action.Status.CANCELED);
|
||||||
|
action.setActive(false);
|
||||||
|
actionRepository.save(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportError(final Action action) {
|
||||||
|
controllerManagement.addUpdateActionStatus(
|
||||||
|
ActionStatusCreate.builder().actionId(action.getId()).status(Action.Status.ERROR).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1004,10 +1004,9 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
|
|
||||||
rolloutOne = reloadRollout(rolloutOne);
|
rolloutOne = reloadRollout(rolloutOne);
|
||||||
changeStatusForRunningActions(rolloutOne, Status.ERROR, 2);
|
changeStatusForRunningActions(rolloutOne, Status.ERROR, 2);
|
||||||
changeStatusForRunningActions(rolloutOne, Status.FINISHED, 3);
|
changeStatusForRunningActions(rolloutOne, Status.FINISHED, 1);
|
||||||
rolloutHandler.handleAll();
|
rolloutHandler.handleAll();
|
||||||
// verify: 40% error and 60% finished -> should not move to next group
|
// verify: 2 RUNNING remain → group not complete → success action not triggered regardless of thresholds
|
||||||
// because successCondition 80%
|
|
||||||
final List<RolloutGroup> rolloutGruops = rolloutGroupManagement.findByRollout(rolloutOne.getId(), PAGE)
|
final List<RolloutGroup> rolloutGruops = rolloutGroupManagement.findByRollout(rolloutOne.getId(), PAGE)
|
||||||
.getContent();
|
.getContent();
|
||||||
final Map<TotalTargetCountStatus.Status, Long> expectedTargetCountStatus = createInitStatusMap();
|
final Map<TotalTargetCountStatus.Status, Long> expectedTargetCountStatus = createInitStatusMap();
|
||||||
@@ -1675,11 +1674,11 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
|||||||
rolloutHandler.handleAll();
|
rolloutHandler.handleAll();
|
||||||
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
||||||
Status.CANCELED);
|
Status.CANCELED);
|
||||||
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.SCHEDULED, false, amountTargetsInGroup2,
|
// group 1 complete (all CANCELED) → success action fires immediately → group 2 starts in same cycle
|
||||||
Status.SCHEDULED);
|
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.RUNNING, false, amountTargetsInGroup2,
|
||||||
|
Status.RUNNING);
|
||||||
|
|
||||||
// verify actions of second rule are directly in RUNNING state, since
|
// verify state is stable across another scheduler cycle
|
||||||
// confirmation is not required for this group
|
|
||||||
rolloutHandler.handleAll();
|
rolloutHandler.handleAll();
|
||||||
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
||||||
Status.CANCELED);
|
Status.CANCELED);
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Contributors to the Eclipse Foundation
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made
|
||||||
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.eclipse.hawkbit.repository.jpa.rollout.condition;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||||
|
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ThresholdRolloutGroupErrorConditionTest {
|
||||||
|
|
||||||
|
private static final long ROLLOUT_ID = 1L;
|
||||||
|
private static final long GROUP_ID = 10L;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ActionRepository actionRepository;
|
||||||
|
@Mock
|
||||||
|
private Rollout rollout;
|
||||||
|
@Mock
|
||||||
|
private RolloutGroup rolloutGroup;
|
||||||
|
|
||||||
|
private ThresholdRolloutGroupErrorCondition condition;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
condition = new ThresholdRolloutGroupErrorCondition(actionRepository);
|
||||||
|
when(rollout.getId()).thenReturn(ROLLOUT_ID);
|
||||||
|
when(rolloutGroup.getId()).thenReturn(GROUP_ID);
|
||||||
|
when(rolloutGroup.getTotalTargets()).thenReturn(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void errorStatusExceedsThreshold() {
|
||||||
|
// 2 ERROR / 5 = 40% > 10%
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.ERROR)).thenReturn(2L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "10")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void errorStatusBelowThreshold() {
|
||||||
|
// 1 ERROR / 5 = 20% < 50%
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.ERROR)).thenReturn(1L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "50")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noErrorsDoesNotFire() {
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.ERROR)).thenReturn(0L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "10")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exactlyAtThresholdDoesNotFire() {
|
||||||
|
// strict >: 1/5 = 20%, threshold=20% → not exceeded
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.ERROR)).thenReturn(1L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "20")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zeroTotalTargetsDoesNotFire() {
|
||||||
|
when(rolloutGroup.getTotalTargets()).thenReturn(0);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "10")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidThresholdExpressionDoesNotFire() {
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.ERROR)).thenReturn(1L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "notANumber")).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Contributors to the Eclipse Foundation
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made
|
||||||
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.eclipse.hawkbit.repository.jpa.rollout.condition;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||||
|
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ThresholdRolloutGroupSuccessConditionTest {
|
||||||
|
|
||||||
|
private static final long ROLLOUT_ID = 1L;
|
||||||
|
private static final long GROUP_ID = 10L;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ActionRepository actionRepository;
|
||||||
|
@Mock
|
||||||
|
private Rollout rollout;
|
||||||
|
@Mock
|
||||||
|
private RolloutGroup rolloutGroup;
|
||||||
|
|
||||||
|
private ThresholdRolloutGroupSuccessCondition condition;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
condition = new ThresholdRolloutGroupSuccessCondition(actionRepository);
|
||||||
|
when(rollout.getId()).thenReturn(ROLLOUT_ID);
|
||||||
|
when(rolloutGroup.getId()).thenReturn(GROUP_ID);
|
||||||
|
when(rolloutGroup.getTotalTargets()).thenReturn(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void finishedRatioMeetsThreshold() {
|
||||||
|
// 4 FINISHED / 5 = 80% >= 80%
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.FINISHED)).thenReturn(4L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "80")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void finishedRatioBelowThreshold() {
|
||||||
|
// 3 FINISHED / 5 = 60% < 80%
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.FINISHED)).thenReturn(3L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "80")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exactlyAtThresholdFires() {
|
||||||
|
// uses >=: 1/5 = 20%, threshold=20% → fires (contrast: error uses >)
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.FINISHED)).thenReturn(1L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "20")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zeroTotalTargetsReturnsTrue() {
|
||||||
|
// opposite of error condition: no targets = group considered done
|
||||||
|
when(rolloutGroup.getTotalTargets()).thenReturn(0);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "100")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downloadOnlyUsesDownloadedStatus() {
|
||||||
|
when(rollout.getActionType()).thenReturn(Action.ActionType.DOWNLOAD_ONLY);
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.DOWNLOADED)).thenReturn(5L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "100")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downloadOnlyDoesNotCountFinishedStatus() {
|
||||||
|
when(rollout.getActionType()).thenReturn(Action.ActionType.DOWNLOAD_ONLY);
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.DOWNLOADED)).thenReturn(0L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "100")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noFinishedActionsDoesNotFire() {
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.FINISHED)).thenReturn(0L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "10")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidThresholdExpressionReturnsFalse() {
|
||||||
|
when(actionRepository.countByRolloutIdAndRolloutGroupIdAndStatus(ROLLOUT_ID, GROUP_ID, Action.Status.FINISHED)).thenReturn(5L);
|
||||||
|
assertThat(condition.eval(rollout, rolloutGroup, "notANumber")).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user