Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Procedural Scheduling activity deletion #1610

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures;

import gov.nasa.ammos.aerie.procedural.scheduling.Goal;
import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure;
import gov.nasa.ammos.aerie.procedural.scheduling.plan.DeletedAnchorStrategy;
import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan;
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;
import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;
import gov.nasa.jpl.aerie.types.ActivityDirectiveId;
import org.jetbrains.annotations.NotNull;

import java.util.Map;

/**
* Creates three activities in a chain of anchors, then deletes one.
* If `whichToDelete` is negative, this leaves all three activities.
* If `rollback` is true, this will roll the edit back before finishing.
*/
@SchedulingProcedure
public record ActivityDeletionGoal(int whichToDelete, DeletedAnchorStrategy anchorStrategy, boolean rollback) implements Goal {
@Override
public void run(@NotNull final EditablePlan plan) {
final var ids = new ActivityDirectiveId[3];

ids[0] = plan.create(
"BiteBanana",
new DirectiveStart.Absolute(Duration.HOUR),
Map.of("biteSize", SerializedValue.of(0))
);
ids[1] = plan.create(
"BiteBanana",
new DirectiveStart.Anchor(ids[0], Duration.HOUR, DirectiveStart.Anchor.AnchorPoint.End),
Map.of("biteSize", SerializedValue.of(1))
);
ids[2] = plan.create(
"BiteBanana",
new DirectiveStart.Anchor(ids[1], Duration.HOUR, DirectiveStart.Anchor.AnchorPoint.Start),
Map.of("biteSize", SerializedValue.of(2))
);

plan.commit();

if (whichToDelete >= 0) {
plan.delete(ids[whichToDelete], anchorStrategy);
}
JoelCourtney marked this conversation as resolved.
Show resolved Hide resolved

if (rollback) plan.rollback();
else plan.commit();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ void executeEDSLAndProcedure() throws IOException {
final var args = Json.createObjectBuilder().add("quantity", 4).build();
hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

final String recurrenceGoalDefinition =
"""
export default function myGoal() {
return Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.PeelBanana({peelDirection: 'fromStem'}),
interval: Temporal.Duration.from({hours:1})
})}""";

hasura.createSchedulingSpecGoal(
"Recurrence Scheduling Test Goal",
recurrenceGoalDefinition,
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: None of these tests appear to excersize deleting and reanchoring activities that were already in the plan, only ones that were created as part of scheduling. Please add tests where activities in the plan prior to scheduling are 1) deleted and 2) reachored so that the new GQL code can be tested. I would especially prefer if these tests involved multiple existing activities being reanchored/have their start times adjusted to unique offsets to fully test the modifyActivityDirecitves behavior, since it's chaining sub-mutations.

Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package gov.nasa.jpl.aerie.e2e.procedural.scheduling;

import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId;
import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.json.Json;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class DeletionTests extends ProceduralSchedulingSetup {
private GoalInvocationId procedureId;

@BeforeEach
void localBeforeEach() throws IOException {
try (final var gateway = new GatewayRequests(playwright)) {
int procedureJarId = gateway.uploadJarFile("build/libs/ActivityDeletionGoal.jar");
// Add Scheduling Procedure
procedureId = hasura.createSchedulingSpecProcedure(
"Test Scheduling Procedure",
procedureJarId,
specId,
0
);
}
}

@AfterEach
void localAfterEach() throws IOException {
hasura.deleteSchedulingGoal(procedureId.goalId());
}

@Test
void createsThreeActivities() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", -1)
.add("anchorStrategy", "Error")
.add("rollback", false)
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(3, activities.size());

final AtomicReference<Integer> id1 = new AtomicReference<>();
final AtomicReference<Integer> id2 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null);
if (result) id1.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id1.get());
if (result) id2.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id2.get())
));
}

@Test
void deletesLast() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 2)
.add("anchorStrategy", "Error")
.add("rollback", false)
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(2, activities.size());

final AtomicReference<Integer> id1 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null);
if (result) id1.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id1.get())
));
}

@Test
void deletesMiddleCascade() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 1)
.add("anchorStrategy", "Cascade")
.add("rollback", false)
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(1, activities.size());

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null)
));
}

@Test
void deletesMiddleAnchorToParent() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 1)
.add("anchorStrategy", "PreserveTree")
.add("rollback", false)
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(2, activities.size());

final AtomicReference<Integer> id1 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null);
if (result) id1.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana")
&& Objects.equals(it.anchorId(), id1.get())
&& Objects.equals(it.startOffset(), "02:00:00")
));
}

@Test
void deletesFirstCascade() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 0)
.add("anchorStrategy", "Cascade")
.add("rollback", false)
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(0, activities.size());
}

@Test
void deletesFirstReAnchorToPlan() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 0)
.add("anchorStrategy", "PreserveTree")
.add("rollback", false)
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(2, activities.size());

final AtomicReference<Integer> id2 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana")
&& Objects.equals(it.anchorId(), null)
&& Objects.equals(it.startOffset(), "02:00:00");
if (result) id2.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id2.get())
));
}

@Test
void anchorResetOnRollback() throws IOException {
final var args = Json
.createObjectBuilder()
.add("whichToDelete", 1)
.add("anchorStrategy", "PreserveTree")
.add("rollback", true)
.build();

hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args);

hasura.awaitScheduling(specId);

final var plan = hasura.getPlan(planId);
final var activities = plan.activityDirectives();

assertEquals(3, activities.size());

final AtomicReference<Integer> id1 = new AtomicReference<>();
final AtomicReference<Integer> id2 = new AtomicReference<>();
assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null);
if (result) id1.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> {
final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id1.get());
if (result) id2.set(it.id());
return result;
}
));

assertTrue(activities.stream().anyMatch(
it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id2.get())
));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package gov.nasa.jpl.aerie.e2e.procedural.scheduling;

import com.microsoft.playwright.Playwright;
import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId;
import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests;
import gov.nasa.jpl.aerie.e2e.utils.HasuraRequests;
import org.junit.jupiter.api.AfterAll;
Expand All @@ -26,13 +25,7 @@ public abstract class ProceduralSchedulingSetup {

// Cross-Test Constants
protected final String planStartTimestamp = "2023-01-01T00:00:00+00:00";
protected final String recurrenceGoalDefinition =
"""
export default function myGoal() {
return Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.PeelBanana({peelDirection: 'fromStem'}),
interval: Temporal.Duration.from({hours:1})
})}""";

JoelCourtney marked this conversation as resolved.
Show resolved Hide resolved

@BeforeAll
void beforeAll() {
Expand Down
15 changes: 13 additions & 2 deletions e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/Plan.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@ public record Plan(
int revision,
List<ActivityDirective> activityDirectives
) {
public record ActivityDirective(int id, int planId, String type, String startOffset, JsonObject arguments, String name) {
public record ActivityDirective(
int id,
int planId,
String type,
String startOffset,
JsonObject arguments,
String name,
Integer anchorId,
boolean anchoredToStart
) {
public static ActivityDirective fromJSON(JsonObject json){
return new ActivityDirective(
json.getInt("id"),
json.getInt("plan_id"),
json.getString("type"),
json.getString("startOffset"),
json.getJsonObject("arguments"),
json.getString("name")
json.getString("name"),
json.isNull("anchorId") ? null : json.getInt("anchorId"),
json.getBoolean("anchoredToStart")
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,8 @@ query GetPlan($id: Int!) {
startOffset: start_offset
type
name
anchorId: anchor_id
anchoredToStart: anchored_to_start
}
constraint_specification {
constraint_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public boolean validateGrowingDuration() {
@EffectModel
@ControllableDuration(parameterName = "growingDuration")
public void run(final Mission mission) {
final var rate = this.quantity() / (double) this.growingDuration().in(Duration.SECONDS);
final var rate = this.quantity() / (double) this.growingDuration().ratioOver(Duration.SECOND);
mission.fruit.rate.add(rate);
delay(this.growingDuration());
mission.fruit.rate.add(-rate);
Expand Down
Loading
Loading