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.RolloutGroup;
|
||||
|
||||
/**
|
||||
* Evaluates if the {@link RolloutGroup#getErrorConditionExp()} is reached.
|
||||
*/
|
||||
@Slf4j
|
||||
public class ThresholdRolloutGroupErrorCondition
|
||||
implements RolloutGroupConditionEvaluator<RolloutGroup.RolloutGroupErrorCondition> {
|
||||
|
||||
@@ -445,10 +445,11 @@ public class JpaRolloutExecutor implements RolloutExecutor {
|
||||
if (isError) {
|
||||
log.info("Rollout {} {} has error, calling error action", rollout.getName(), rollout.getId());
|
||||
callErrorAction(rollout, rolloutGroup);
|
||||
} else {
|
||||
// not in error so check finished state, do we need to start the next group?
|
||||
checkSuccessCondition(rollout, rolloutGroup, evalProxy, rolloutGroup.getSuccessCondition());
|
||||
if (!(rolloutGroup == lastGroup && rolloutGroup.isDynamic()) && isRolloutGroupComplete(rollout, rolloutGroup)) {
|
||||
} else {// not in error so check success condition and group completed
|
||||
// 'success' is either group completed or success condition reached - execute 'success' Action
|
||||
final boolean groupCompleted = !(rolloutGroup == lastGroup && rolloutGroup.isDynamic()) && isRolloutGroupComplete(rollout, rolloutGroup);
|
||||
checkSuccessCondition(rollout, rolloutGroup, evalProxy, rolloutGroup.getSuccessCondition(), groupCompleted);
|
||||
if (groupCompleted) {
|
||||
rolloutGroup.setStatus(RolloutGroupStatus.FINISHED);
|
||||
rolloutGroupRepository.save(rolloutGroup);
|
||||
}
|
||||
@@ -466,11 +467,9 @@ public class JpaRolloutExecutor implements RolloutExecutor {
|
||||
}
|
||||
|
||||
private long countTargetsFrom(final JpaRolloutGroup rolloutGroup) {
|
||||
if (rolloutGroup.isDynamic()) {
|
||||
return countByActionsInRolloutGroup(rolloutGroup.getId());
|
||||
} else {
|
||||
return rolloutGroupManagement.countTargetsOfRolloutsGroup(rolloutGroup.getId());
|
||||
}
|
||||
// Use action-based count for all groups: deleting an action removes the target from the count,
|
||||
// keeping totalTargets consistent with the actual denominator used by condition evaluators.
|
||||
return countByActionsInRolloutGroup(rolloutGroup.getId());
|
||||
}
|
||||
|
||||
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,
|
||||
final RolloutGroupSuccessCondition successCondition) {
|
||||
final RolloutGroupSuccessCondition successCondition, final boolean groupCompleted) {
|
||||
log.trace("Checking finish condition {} on rolloutgroup {}", successCondition, rolloutGroup);
|
||||
try {
|
||||
final boolean isFinished = evaluationManager
|
||||
if (groupCompleted || evaluationManager
|
||||
.getSuccessConditionEvaluator(successCondition)
|
||||
.eval(rollout, evalProxy, rolloutGroup.getSuccessConditionExp());
|
||||
if (isFinished) {
|
||||
log.debug("Rollout group {} is finished, starting next group", rolloutGroup);
|
||||
.eval(rollout, evalProxy, rolloutGroup.getSuccessConditionExp())) {
|
||||
log.debug("Rollout group {} fulfills SuccessCondition or is Finished, executing Success Action", rolloutGroup);
|
||||
evaluationManager.getSuccessActionEvaluator(rolloutGroup.getSuccessAction()).exec(rollout, rolloutGroup);
|
||||
} else {
|
||||
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);
|
||||
changeStatusForRunningActions(rolloutOne, Status.ERROR, 2);
|
||||
changeStatusForRunningActions(rolloutOne, Status.FINISHED, 3);
|
||||
changeStatusForRunningActions(rolloutOne, Status.FINISHED, 1);
|
||||
rolloutHandler.handleAll();
|
||||
// verify: 40% error and 60% finished -> should not move to next group
|
||||
// because successCondition 80%
|
||||
// verify: 2 RUNNING remain → group not complete → success action not triggered regardless of thresholds
|
||||
final List<RolloutGroup> rolloutGruops = rolloutGroupManagement.findByRollout(rolloutOne.getId(), PAGE)
|
||||
.getContent();
|
||||
final Map<TotalTargetCountStatus.Status, Long> expectedTargetCountStatus = createInitStatusMap();
|
||||
@@ -1675,11 +1674,11 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
rolloutHandler.handleAll();
|
||||
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
||||
Status.CANCELED);
|
||||
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.SCHEDULED, false, amountTargetsInGroup2,
|
||||
Status.SCHEDULED);
|
||||
// group 1 complete (all CANCELED) → success action fires immediately → group 2 starts in same cycle
|
||||
assertRolloutGroup(rolloutGroupIds.get(1), RolloutGroupStatus.RUNNING, false, amountTargetsInGroup2,
|
||||
Status.RUNNING);
|
||||
|
||||
// verify actions of second rule are directly in RUNNING state, since
|
||||
// confirmation is not required for this group
|
||||
// verify state is stable across another scheduler cycle
|
||||
rolloutHandler.handleAll();
|
||||
assertRolloutGroup(rolloutGroupIds.get(0), RolloutGroupStatus.FINISHED, true, amountTargetsInGroup1,
|
||||
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