Skip to content

Commit

Permalink
fix pause computation in inject start date
Browse files Browse the repository at this point in the history
Signed-off-by: Antoine MAZEAS <[email protected]>
  • Loading branch information
antoinemzs committed Jan 29, 2025
1 parent d910eb9 commit 5444f43
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.openbas.migration;

import java.sql.Connection;
import java.sql.Statement;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.stereotype.Component;

@Component
public class V3_62__make_exercise_pause_tz_aware extends BaseJavaMigration {

@Override
public void migrate(Context context) throws Exception {
Connection connection = context.getConnection();
Statement statement = connection.createStatement();

statement.execute(
"""
ALTER TABLE exercises ADD COLUMN exercise_pause_date_tempwithtz TIMESTAMP WITH TIME ZONE;
UPDATE exercises SET exercise_pause_date_tempwithtz = exercise_pause_date;
ALTER TABLE exercises DROP COLUMN exercise_pause_date;
ALTER TABLE exercises RENAME COLUMN exercise_pause_date_tempwithtz TO exercise_pause_date;
""");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.openbas.database.model;

import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;

import io.openbas.database.raw.RawExercise;
import io.openbas.database.repository.ExerciseRepository;
import io.openbas.utils.fixtures.ExerciseFixture;
import io.openbas.utils.fixtures.composers.ExerciseComposer;
import jakarta.persistence.EntityManager;
import java.time.Instant;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@TestInstance(PER_CLASS)
@Transactional
public class ExerciseTest {
@Autowired private ExerciseComposer exerciseComposer;
@Autowired private ExerciseRepository exerciseRepository;
@Autowired private EntityManager entityManager;

private final Instant exerciseStartTime = Instant.parse("2012-11-21T04:00:00Z");

@Test
@DisplayName("Given a persisted exercise, current pause from raw query is correctly persisted.")
public void GivenAnExercise_CurrentPauseFromRawQueryIsCorrectlyPersisted() {
Instant expectedCurrentPauseTime = Instant.parse("2012-11-21T04:05:00Z");
ExerciseComposer.Composer wrapper =
exerciseComposer
.forExercise(ExerciseFixture.createDefaultAttackExercise(exerciseStartTime));
wrapper.get().setCurrentPause(expectedCurrentPauseTime); // current pause at T+5 minutes

Exercise expected = wrapper.persist().get();

// reset JPA
entityManager.flush();
entityManager.clear();

RawExercise dbExercise = exerciseRepository.rawDetailsById(expected.getId());

Assertions.assertTrue(
expected.getCurrentPause().isPresent(),
"Current pause should be present for expected exercise");
Assertions.assertEquals(expected.getCurrentPause().get(), dbExercise.getExercise_pause_date());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.openbas.database.model;

import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;

import io.openbas.utils.fixtures.ExerciseFixture;
import io.openbas.utils.fixtures.InjectFixture;
import io.openbas.utils.fixtures.PauseFixture;
import io.openbas.utils.fixtures.composers.ExerciseComposer;
import io.openbas.utils.fixtures.composers.InjectComposer;
import io.openbas.utils.fixtures.composers.PauseComposer;
import java.time.Instant;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@TestInstance(PER_CLASS)
@Transactional
public class InjectTest {

@Autowired private ExerciseComposer exerciseComposer;
@Autowired private InjectComposer injectComposer;
@Autowired private PauseComposer pauseComposer;

@Nested
@DisplayName("Given valid exercise")
public class GivenValidExerciseWithTwoPauses {

private final Instant exerciseStartTime = Instant.parse("2012-11-21T04:00:00Z");

public ExerciseComposer.Composer getExerciseComposer() {
return exerciseComposer
.forExercise(ExerciseFixture.createRunningAttackExercise(exerciseStartTime))
.withInject(
injectComposer.forInject(
InjectFixture.getDefaultInjectWithDuration(600L))); // run time 04:10
}

@Nested
@DisplayName("With two pauses, one of which starts after original inject time")
public class WithTwoPauses {

private final Instant firstPauseStartTime = Instant.parse("2012-11-21T04:02:00Z");
private final Instant secondPauseStartTime = Instant.parse("2012-11-21T04:15:00Z");

@Test
@DisplayName(
"When the inject was affected by both pauses its starting date should account for both pauses")
public void WhenInjectEffectivelyWasPausedTwice_InjectDateAccountsForBothPauses() {
Exercise exercise =
getExerciseComposer()
.withPause(
pauseComposer.forPause(
// first pause duration brings wakeup close to second pause start
// so that inject does not run in between
PauseFixture.createPause(firstPauseStartTime, 600L))) // wakeup 04:12
.withPause(
pauseComposer.forPause(
PauseFixture.createPause(secondPauseStartTime, 3600L))) // wakeup 05:15
.get();
Instant expected_instant = Instant.parse("2012-11-21T05:20:00Z");

Inject inject = exercise.getInjects().getFirst();

Assertions.assertTrue(inject.getDate().isPresent(), "Inject has no date.");
Assertions.assertEquals(expected_instant, inject.getDate().get());
}

@Test
@DisplayName(
"When the inject was affected by first pause only its starting date should account for first pause only")
public void WhenInjectEffectivelyWasPausedOnce_InjectDateAccountsForSinglePause() {
Exercise exercise =
getExerciseComposer()
.withPause(
pauseComposer.forPause(
// first pause is short enough so that inject runs before second pause
// the pause will effectively delay inject by a single minute
PauseFixture.createPause(
firstPauseStartTime, 15L))) // wakeup 04:02:15, effective 04:03:00
.withPause(
pauseComposer.forPause(
PauseFixture.createPause(secondPauseStartTime, 3600L))) // wakeup 05:15
.get();
Instant expected_instant = Instant.parse("2012-11-21T04:11:00Z");

Inject inject = exercise.getInjects().getFirst();

Assertions.assertTrue(inject.getDate().isPresent(), "Inject has no date.");
Assertions.assertEquals(expected_instant, inject.getDate().get());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ public static Inject getDefaultInject() {
return inject;
}

public static Inject getDefaultInjectWithDuration(long duration) {
Inject inject = createInjectWithDefaultTitle();
inject.setEnabled(true);
inject.setDependsDuration(duration);
return inject;
}

public static Inject getInjectForEmailContract(InjectorContract injectorContract) {
return createInject(injectorContract, INJECT_EMAIL_NAME);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.openbas.utils.fixtures;

import io.openbas.database.model.Pause;
import java.time.Instant;

public class PauseFixture {
public static Pause createPause(Instant start, long duration) {
Pause pause = new Pause();
pause.setDuration(duration);
pause.setDate(start);
return pause;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class Composer extends InnerComposerBase<Exercise> {
private final List<TagComposer.Composer> tagComposers = new ArrayList<>();
private final List<DocumentComposer.Composer> documentComposers = new ArrayList<>();
private final List<VariableComposer.Composer> variableComposers = new ArrayList<>();
private final List<PauseComposer.Composer> pauseComposers = new ArrayList<>();

public Composer(Exercise exercise) {
this.exercise = exercise;
Expand All @@ -39,6 +40,7 @@ public Composer withVariable(VariableComposer.Composer variableComposer) {
public Composer withInject(InjectComposer.Composer injectComposer) {
injectComposers.add(injectComposer);
List<Inject> injects = exercise.getInjects();
injectComposer.get().setExercise(exercise);
injects.add(injectComposer.get());
this.exercise.setInjects(injects);
return this;
Expand Down Expand Up @@ -113,6 +115,14 @@ public Composer withDocument(DocumentComposer.Composer documentComposer) {
return this;
}

public Composer withPause(PauseComposer.Composer pauseComposer) {
this.pauseComposers.add(pauseComposer);
List<Pause> tempPauses = this.exercise.getPauses();
tempPauses.add(pauseComposer.get());
this.exercise.setPauses(tempPauses);
return this;
}

public Composer withId(String id) {
this.exercise.setId(id);
return this;
Expand All @@ -128,6 +138,7 @@ public Composer persist() {
this.tagComposers.forEach(TagComposer.Composer::persist);
this.documentComposers.forEach(DocumentComposer.Composer::persist);
this.variableComposers.forEach(VariableComposer.Composer::persist);
this.pauseComposers.forEach(PauseComposer.Composer::persist);
exerciseRepository.save(exercise);
return this;
}
Expand All @@ -143,6 +154,7 @@ public Composer delete() {
this.teamComposers.forEach(TeamComposer.Composer::delete);
this.categoryComposers.forEach(LessonsCategoryComposer.Composer::delete);
this.articleComposers.forEach(ArticleComposer.Composer::delete);
this.pauseComposers.forEach(PauseComposer.Composer::delete);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.openbas.utils.fixtures.composers;

import io.openbas.database.model.Pause;
import io.openbas.database.repository.PauseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class PauseComposer extends ComposerBase<Pause> {
@Autowired private PauseRepository pauseRepository;

public class Composer extends InnerComposerBase<Pause> {
private final Pause pause;

public Composer(Pause pause) {
this.pause = pause;
}

@Override
public Composer persist() {
pauseRepository.save(pause);
return this;
}

@Override
public Composer delete() {
pauseRepository.delete(pause);
return this;
}

@Override
public Pause get() {
return this.pause;
}
}

public Composer forPause(Pause pause) {
this.generatedItems.add(pause);
return new Composer(pause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,31 +119,33 @@ public static Instant computeInjectDate(
Instant source, int speed, Long dependsDuration, Exercise exercise) {
// Compute origin execution date
long duration = ofNullable(dependsDuration).orElse(0L) / speed;
Instant dependingStart = source;
Instant standardExecutionDate = dependingStart.plusSeconds(duration);
Instant standardExecutionDate = source.plusSeconds(duration);
// Compute execution dates with previous terminated pauses
Instant afterPausesExecutionDate = standardExecutionDate;
List<Pause> sortedPauses = exercise.getPauses();
sortedPauses.sort(
(pause0, pause1) ->
pause0.getDate().equals(pause1.getDate())
? 0
: pause0.getDate().isBefore(pause1.getDate()) ? -1 : 1);
long previousPauseDelay = 0L;
if (exercise != null) {
previousPauseDelay =
exercise.getPauses().stream()
.filter(pause -> pause.getDate().isBefore(standardExecutionDate))
.mapToLong(pause -> pause.getDuration().orElse(0L))
.sum();
for (Pause pause : sortedPauses) {
if (pause.getDate().isAfter(afterPausesExecutionDate)) {
break;
}
previousPauseDelay += pause.getDuration().orElse(0L);
afterPausesExecutionDate = standardExecutionDate.plusSeconds(previousPauseDelay);
}
Instant afterPausesExecutionDate = standardExecutionDate.plusSeconds(previousPauseDelay);

// Add current pause duration in date computation if needed
long currentPauseDelay = 0L;
if (exercise != null) {
currentPauseDelay =
exercise
.getCurrentPause()
.map(
last ->
last.isBefore(afterPausesExecutionDate)
? between(last, now()).getSeconds()
: 0L)
.orElse(0L);
}
long currentPauseDelay;
Instant finalAfterPausesExecutionDate = afterPausesExecutionDate;
currentPauseDelay =
exercise
.getCurrentPause()
.filter(pauseTime -> pauseTime.isBefore(finalAfterPausesExecutionDate))
.map(pauseTime -> between(pauseTime, now()).getSeconds())
.orElse(0L);
long globalPauseDelay = previousPauseDelay + currentPauseDelay;
long minuteAlignModulo = globalPauseDelay % 60;
long alignedPauseDelay =
Expand Down

0 comments on commit 5444f43

Please sign in to comment.