diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45697a5882..a2df13e1c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,7 +78,7 @@ jobs: - name: Assemble run: ./gradlew assemble --parallel - name: Build scheduling procedure jars for testing - run: ./gradlew procedural:examples:foo-procedures:buildAllSchedulingProcedureJars + run: ./gradlew e2e-tests:buildAllSchedulingProcedureJars - name: Start Services run: | docker compose -f ./e2e-tests/docker-compose-test.yml up -d --build diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 4ff48fdab6..d7d5c9c359 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -1,6 +1,9 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + plugins { id 'java-library' id 'jacoco' + id 'com.gradleup.shadow' version '8.3.0' } java { @@ -54,12 +57,17 @@ task e2eTest(type: Test) { } dependencies { - testImplementation project(":procedural:timeline") + annotationProcessor project(':procedural:processor') + + implementation project(":procedural:scheduling") + implementation project(":procedural:timeline") + implementation project(':merlin-sdk') + implementation project(':type-utils') + implementation project(':contrib') + testImplementation project(":procedural:remote") testImplementation "com.zaxxer:HikariCP:5.1.0" testImplementation("org.postgresql:postgresql:42.6.0") - testImplementation project(':merlin-driver') - testImplementation project(':type-utils') testImplementation 'com.microsoft.playwright:playwright:1.37.0' @@ -69,3 +77,65 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' } + +tasks.register('buildAllSchedulingProcedureJars') { + group = 'SchedulingProcedureJars' + + dependsOn "generateSchedulingProcedureJarTasks" + dependsOn { + tasks.findAll { task -> task.name.startsWith('buildSchedulingProcedureJar_') } + } +} + +tasks.create("generateSchedulingProcedureJarTasks") { + group = 'SchedulingProcedureJars' + + final proceduresDir = findFirstMatchingBuildDir("generated/procedures") + + if (proceduresDir == null) { + println "No procedures folder found" + return + } + println "Generating jar tasks for the following procedures directory: ${proceduresDir}" + + final files = file(proceduresDir).listFiles() + if (files.length == 0) { + println "No procedures available within folder ${proceduresDir}" + return + } + + files.toList().each { file -> + final nameWithoutExtension = file.name.replace(".java", "") + final taskName = "buildSchedulingProcedureJar_${nameWithoutExtension}" + + println "Generating ${taskName} task, which will build ${nameWithoutExtension}.jar" + + tasks.create(taskName, ShadowJar) { + group = 'SchedulingProcedureJars' + configurations = [project.configurations.compileClasspath] + from sourceSets.main.output + archiveBaseName = "" // clear + archiveClassifier.set(nameWithoutExtension) // set output jar name + manifest { + attributes 'Main-Class': getMainClassFromGeneratedFile(file) + } + minimize() + } + } +} + +private String findFirstMatchingBuildDir(String pattern) { + String found = null + final generatedDir = file("build/generated/sources") + generatedDir.mkdirs() + generatedDir.eachDirRecurse { dir -> if (dir.path.contains(pattern)) found = dir.path } + return found +} + +private static String getMainClassFromGeneratedFile(File file) { + final fileString = file.toString() + final prefix = "build/generated/sources/annotationProcessor/java/main/" + final index = fileString.indexOf(prefix) + prefix.length() + final trimmed = fileString.substring(index).replace(".java", "") + return trimmed.replace("/", ".") +} diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/package-info.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/package-info.java new file mode 100644 index 0000000000..c77565fa77 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/package-info.java @@ -0,0 +1,5 @@ +@WithMappers(BasicValueMappers.class) +package gov.nasa.jpl.aerie.e2e.procedural.scheduling; + +import gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithMappers; diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoal.java new file mode 100644 index 0000000000..7085d0f640 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/DumbRecurrenceGoal.java @@ -0,0 +1,40 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +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.NewDirective; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * Waits 24hrs into the plan, then places `quantity` number of BiteBanana activities, + * one every 6hrs. + */ +@SchedulingProcedure +public record DumbRecurrenceGoal(int quantity) implements Goal { + @Override + public void run(@NotNull final EditablePlan plan) { + final var firstTime = Duration.hours(24); + final var step = Duration.hours(6); + + var currentTime = firstTime; + for (var i = 0; i < quantity; i++) { + plan.create( + new NewDirective( + new AnyDirective(Map.of("biteSize", SerializedValue.of(1))), + "It's a bite banana activity", + "BiteBanana", + new DirectiveStart.Absolute(currentTime) + ) + ); + currentTime = currentTime.plus(step); + } + plan.commit(); + } +} diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalProfileGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalProfileGoal.java new file mode 100644 index 0000000000..52ad2f0a30 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ExternalProfileGoal.java @@ -0,0 +1,28 @@ +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.EditablePlan; +import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Booleans; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +@SchedulingProcedure +public record ExternalProfileGoal() implements Goal { + @Override + public void run(@NotNull final EditablePlan plan) { + final var myBoolean = plan.resource("/my_boolean", Booleans.deserializer()); + for (final var interval: myBoolean.highlightTrue()) { + plan.create( + "BiteBanana", + new DirectiveStart.Absolute(interval.start), + Map.of("biteSize", SerializedValue.of(1)) + ); + } + + plan.commit(); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/EdslSchedulingTests.java similarity index 86% rename from e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java rename to e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/EdslSchedulingTests.java index 681a68e067..f9e384ee1a 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/EdslSchedulingTests.java @@ -3,7 +3,6 @@ import com.microsoft.playwright.Playwright; import gov.nasa.jpl.aerie.e2e.types.ExternalDataset.ProfileInput; import gov.nasa.jpl.aerie.e2e.types.ExternalDataset.ProfileInput.ProfileSegmentInput; -import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; import gov.nasa.jpl.aerie.e2e.types.Plan; import gov.nasa.jpl.aerie.e2e.types.ProfileSegment; import gov.nasa.jpl.aerie.e2e.types.SchedulingRequest.SchedulingStatus; @@ -29,7 +28,6 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; -import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -38,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.fail; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class SchedulingTests { +class EdslSchedulingTests { // Requests private Playwright playwright; private HasuraRequests hasura; @@ -168,10 +166,10 @@ private ArrayList insertAnchorActivities() throws IOException { .add("growingDuration", 10800000L) // 3h .build()); anchors.add(id1); - Integer id2 = hasura.insertActivityDirective(planId, "GrowBanana", "5h", - Json.createObjectBuilder() - .add("growingDuration", 10800000L) // 3h - .build()); + hasura.insertActivityDirective(planId, "GrowBanana", "5h", + Json.createObjectBuilder() + .add("growingDuration", 10800000L) // 3h + .build()); anchors.add(id1); Integer id3 = hasura.insertActivityDirective(planId, "GrowBanana", "10h", Json.createObjectBuilder() @@ -278,7 +276,7 @@ void schedulingCoexistenceGoal() throws IOException { @Test void schedulingCoexistenceGoalWithAnchor() throws IOException { // Setup: Add Goal and Activities - ArrayList anchors = insertAnchorActivities(); + insertAnchorActivities(); hasura.createSchedulingSpecGoal("Coexistence Scheduling Test Goal", coexistenceGoalWithAnchorsDefinition, schedulingSpecId, 0); @@ -1003,175 +1001,4 @@ void schedulingIgnoreDisabledGoals() throws IOException { } } - @Nested - class ProceduralSchedulingTests { - private int modelId; - private int planId; - private int specId; - private int procedureJarId; - private GoalInvocationId procedureId; - - @BeforeEach - void beforeEach() throws IOException, InterruptedException { - try (final var gateway = new GatewayRequests(playwright)) { - modelId = hasura.createMissionModel( - gateway.uploadJarFile(), - "Banananation (e2e tests)", - "aerie_e2e_tests", - "Proc Scheduling Tests"); - - procedureJarId = gateway.uploadJarFile("../procedural/examples/foo-procedures/build/libs/SampleProcedure.jar"); - } - // Insert the Plan - planId = hasura.createPlan( - modelId, - "Proc Sched Plan - Proc Scheduling Tests", - "48:00:00", - planStartTimestamp); - specId = hasura.getSchedulingSpecId(planId); - - // Add Scheduling Procedure - procedureId = hasura.createSchedulingSpecProcedure( - "Test Scheduling Procedure", - procedureJarId, - specId, - 0); - } - - @AfterEach - void afterEach() throws IOException { - hasura.deleteSchedulingGoal(procedureId.goalId()); - hasura.deletePlan(planId); - hasura.deleteMissionModel(modelId); - } - - /** - * Upload a procedure jar and add to spec - */ - @Test - void proceduralUploadWorks() throws IOException { - final var ids = hasura.getSchedulingSpecGoalIds(specId); - - assertEquals(1, ids.size()); - assertEquals(procedureId.goalId(), ids.getFirst()); - } - - /** - * Run a spec with one procedure in it with required params but no args set - * Should fail scheduling run - */ - @Test - void executeSchedulingRunWithoutArguments() throws IOException { - final var resp = hasura.awaitFailingScheduling(specId); - final var message = resp.reason().getString("message"); - assertTrue(message.contains("java.lang.RuntimeException: Record missing key Component[name=quantity")); - } - - /** - * Run a spec with one procedure in it - */ - @Test - void executeSchedulingRunWithArguments() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); - - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); - - final var resp = hasura.awaitScheduling(specId); - - final var plan = hasura.getPlan(planId); - final var activities = plan.activityDirectives(); - - assertEquals(2, activities.size()); - - assertTrue(activities.stream().anyMatch( - $ -> Objects.equals($.type(), "BiteBanana") && Objects.equals($.startOffset(), "24:00:00") - )); - - assertTrue(activities.stream().anyMatch( - $ -> Objects.equals($.type(), "BiteBanana") && Objects.equals($.startOffset(), "30:00:00") - )); - } - - /** - * Run a spec with two invocations of the same procedure in it - */ - @Test - void executeMultipleInvocationsOfSameProcedure() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); - - final var secondInvocationId = hasura.insertGoalInvocation(procedureId.goalId(), specId); - hasura.updateSchedulingSpecGoalArguments(secondInvocationId.invocationId(), args); - - final var resp = hasura.awaitScheduling(specId); - - final var plan = hasura.getPlan(planId); - final var activities = plan.activityDirectives(); - - assertEquals(4, activities.size()); - } - - /** - * Run a spec with two procedures in it - */ - @Test - void executeMultipleProcedures() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); - - final var secondProcedure = hasura.createSchedulingSpecProcedure( - "Test Scheduling Procedure 2", - procedureJarId, - specId, - 1); - - hasura.updateSchedulingSpecGoalArguments(secondProcedure.invocationId(), args); - - final var resp = hasura.awaitScheduling(specId); - - final var plan = hasura.getPlan(planId); - final var activities = plan.activityDirectives(); - - assertEquals(4, activities.size()); - } - - /** - * Run a spec with one EDSL goal and one procedure - */ - @Test - void executeEDSLAndProcedure() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 4).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); - - final int recurrenceGoalId = hasura.createSchedulingSpecGoal( - "Recurrence Scheduling Test Goal", - recurrenceGoalDefinition, - specId, - 1).goalId(); - - final var resp = hasura.awaitScheduling(specId); - - final var plan = hasura.getPlan(planId); - final var activities = plan.activityDirectives(); - - assertEquals(52, activities.size()); - } - - /** - * Run a spec with one procedure and make sure the activity names are saved to the database - */ - @Test - void saveActivityName() throws IOException { - final var args = Json.createObjectBuilder().add("quantity", 2).build(); - hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); - - final var resp = hasura.awaitScheduling(specId); - - final var plan = hasura.getPlan(planId); - final var activities = plan.activityDirectives(); - - assertEquals(2, activities.size()); - assertEquals("It's a bite banana activity", activities.getFirst().name()); - } - } } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ExternalDatasetsTest.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ExternalDatasetsTest.java index 880bcb891d..b60bd96c4c 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ExternalDatasetsTest.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ExternalDatasetsTest.java @@ -28,12 +28,13 @@ public class ExternalDatasetsTest { // Cross-Test Constants private final String datasetOffset = "06:00:00"; - private final ProfileInput myBooleanProfile = + public static final ProfileInput myBooleanProfile = new ProfileInput( "/my_boolean", "discrete", ValueSchema.VALUE_SCHEMA_BOOLEAN, List.of( + // 3600000000L is 1hr in microseconds new ProfileSegmentInput(3600000000L, JsonValue.FALSE), new ProfileSegmentInput(3600000000L, JsonValue.NULL), new ProfileSegmentInput(3600000000L, JsonValue.TRUE), diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java new file mode 100644 index 0000000000..71fb6aae76 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java @@ -0,0 +1,167 @@ +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BasicTests extends ProceduralSchedulingSetup { + private int procedureJarId; + private GoalInvocationId procedureId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + procedureJarId = gateway.uploadJarFile("build/libs/DumbRecurrenceGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(procedureId.goalId()); + } + + /** + * Upload a procedure jar and add to spec + */ + @Test + void proceduralUploadWorks() throws IOException { + final var ids = hasura.getSchedulingSpecGoalIds(specId); + + assertEquals(1, ids.size()); + assertEquals(procedureId.goalId(), ids.getFirst()); + } + + /** + * Run a spec with one procedure in it with required params but no args set + * Should fail scheduling run + */ + @Test + void executeSchedulingRunWithoutArguments() throws IOException { + final var resp = hasura.awaitFailingScheduling(specId); + final var message = resp.reason().getString("message"); + assertTrue(message.contains("java.lang.RuntimeException: Record missing key Component[name=quantity")); + } + + /** + * Run a spec with one procedure in it + */ + @Test + void executeSchedulingRunWithArguments() throws IOException { + final var args = Json.createObjectBuilder().add("quantity", 2).build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(2, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.startOffset(), "24:00:00") + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.startOffset(), "30:00:00") + )); + } + + /** + * Run a spec with two invocations of the same procedure in it + */ + @Test + void executeMultipleInvocationsOfSameProcedure() throws IOException { + final var args = Json.createObjectBuilder().add("quantity", 2).build(); + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + final var secondInvocationId = hasura.insertGoalInvocation(procedureId.goalId(), specId); + hasura.updateSchedulingSpecGoalArguments(secondInvocationId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(4, activities.size()); + } + + /** + * Run a spec with two procedures in it + */ + @Test + void executeMultipleProcedures() throws IOException { + final var args = Json.createObjectBuilder().add("quantity", 2).build(); + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + final var secondProcedure = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure 2", + procedureJarId, + specId, + 1); + + hasura.updateSchedulingSpecGoalArguments(secondProcedure.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(4, activities.size()); + } + + /** + * Run a spec with one EDSL goal and one procedure + */ + @Test + void executeEDSLAndProcedure() throws IOException { + final var args = Json.createObjectBuilder().add("quantity", 4).build(); + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.createSchedulingSpecGoal( + "Recurrence Scheduling Test Goal", + recurrenceGoalDefinition, + specId, + 1); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(52, activities.size()); + } + + /** + * Run a spec with one procedure and make sure the activity names are saved to the database + */ + @Test + void saveActivityName() throws IOException { + final var args = Json.createObjectBuilder().add("quantity", 2).build(); + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(2, activities.size()); + assertEquals("It's a bite banana activity", activities.getFirst().name()); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalProfilesTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalProfilesTests.java new file mode 100644 index 0000000000..7f8061b653 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ExternalProfilesTests.java @@ -0,0 +1,60 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling; + +import gov.nasa.jpl.aerie.e2e.ExternalDatasetsTest; +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 java.io.IOException; +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ExternalProfilesTests extends ProceduralSchedulingSetup { + private GoalInvocationId procedureId; + private int datasetId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + int procedureJarId = gateway.uploadJarFile("build/libs/ExternalProfileGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + + datasetId = hasura.insertExternalDataset( + planId, + "2023-001T01:00:00.000", + List.of(ExternalDatasetsTest.myBooleanProfile) + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(procedureId.goalId()); + hasura.deleteExternalDataset(planId, datasetId); + } + + @Test + void testQueryExternalProfiles() throws IOException { + 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.startOffset(), "03:00:00") + )); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java new file mode 100644 index 0000000000..c1ef5853b1 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java @@ -0,0 +1,77 @@ +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; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; + +import java.io.IOException; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class ProceduralSchedulingSetup { + + // Requests + protected Playwright playwright; + protected HasuraRequests hasura; + + // Per-Test Data + protected int modelId; + protected int planId; + protected int specId; + + // 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}) + })}"""; + + @BeforeAll + void beforeAll() { + // Setup Requests + playwright = Playwright.create(); + hasura = new HasuraRequests(playwright); + } + + @AfterAll + void afterAll() { + // Cleanup Requests + hasura.close(); + playwright.close(); + } + + @BeforeEach + void beforeEach() throws IOException, InterruptedException { + try (final var gateway = new GatewayRequests(playwright)) { + modelId = hasura.createMissionModel( + gateway.uploadJarFile(), + "Banananation (e2e tests)", + "aerie_e2e_tests", + "Proc Scheduling Tests for subclass: %s".formatted(this.getClass().getSimpleName())); + + + } + // Insert the Plan + planId = hasura.createPlan( + modelId, + "Proc Sched Plan - Proc Scheduling Tests for subclass: %s".formatted(this.getClass().getSimpleName()), + "48:00:00", + planStartTimestamp); + + specId = hasura.getSchedulingSpecId(planId); + } + + @AfterEach + void afterEach() throws IOException { + hasura.deletePlan(planId); + hasura.deleteMissionModel(modelId); + } +} diff --git a/procedural/constraints/src/test/java/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedPlan.java b/procedural/constraints/src/test/java/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedPlan.java index 430457a978..18fb2ee459 100644 --- a/procedural/constraints/src/test/java/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedPlan.java +++ b/procedural/constraints/src/test/java/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedPlan.java @@ -2,6 +2,8 @@ import gov.nasa.ammos.aerie.procedural.timeline.Interval; import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives; +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment; import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; @@ -11,6 +13,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; +import java.util.List; public class NotImplementedPlan implements Plan { @NotNull @@ -39,4 +42,13 @@ public Directives directives( { throw new NotImplementedError(); } + + @NotNull + @Override + public > TL resource( + @NotNull final String name, + @NotNull final Function1>, ? extends TL> deserializer) + { + throw new NotImplementedError(); + } } diff --git a/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/GeneratorTest.kt b/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/GeneratorTest.kt index 91c10bac81..afadf14cf6 100644 --- a/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/GeneratorTest.kt +++ b/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/GeneratorTest.kt @@ -2,7 +2,7 @@ package gov.nasa.ammos.aerie.procedural.constraints import gov.nasa.ammos.aerie.procedural.timeline.Interval import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Numbers -import gov.nasa.ammos.aerie.procedural.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults @@ -26,7 +26,7 @@ class GeneratorTest: GeneratorConstraint() { fun testGenerator() { val plan = NotImplementedPlan() val simResults = object : NotImplementedSimulationResults() { - override fun > resource( + override fun > resource( name: String, deserializer: (List>) -> TL ): TL { diff --git a/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt b/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt index 6833e30051..4b783efd21 100644 --- a/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt +++ b/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt @@ -2,7 +2,7 @@ package gov.nasa.ammos.aerie.procedural.constraints import gov.nasa.ammos.aerie.procedural.timeline.Interval import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances -import gov.nasa.ammos.aerie.procedural.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue @@ -10,7 +10,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue open class NotImplementedSimulationResults: SimulationResults { override fun isStale(): Boolean = TODO() override fun simBounds(): Interval = TODO() - override fun > resource( + override fun > resource( name: String, deserializer: (List>) -> TL ): TL = TODO() diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/Plan.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/Plan.kt index b120212c21..dfdf77287c 100644 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/Plan.kt +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/Plan.kt @@ -5,6 +5,8 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Duration import gov.nasa.ammos.aerie.procedural.timeline.Interval import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps +import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import java.time.Instant /** An interface for querying plan information and simulation results. */ @@ -31,4 +33,12 @@ interface Plan { fun directives(type: String) = directives(type, AnyDirective.deserializer()) /** Queries all activity directives, deserializing them as [AnyDirective]. **/ fun directives() = directives(null, AnyDirective.deserializer()) + + /** + * Query a resource profile from the external datasets associated with this plan. + * + * @param deserializer constructor of the profile, converting [SerializedValue] + * @param name string name of the resource + */ + fun > resource(name: String, deserializer: (List>) -> TL): TL } diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulatedPlan.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulatedPlan.kt deleted file mode 100644 index cf23624821..0000000000 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulatedPlan.kt +++ /dev/null @@ -1,7 +0,0 @@ -package gov.nasa.ammos.aerie.procedural.timeline.plan - -/** A connection to Aerie's database for a particular simulation result. */ -data class SimulatedPlan( - private val plan: Plan, - private val simResults: SimulationResults -): Plan by plan, SimulationResults by simResults diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt index 2ddcb70190..58896179b1 100644 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt @@ -4,8 +4,8 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue import gov.nasa.ammos.aerie.procedural.timeline.Interval import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyInstance -import gov.nasa.ammos.aerie.procedural.timeline.ops.coalesce.CoalesceSegmentsOp import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps /** An interface for querying plan information and simulation results. */ interface SimulationResults { @@ -16,12 +16,12 @@ interface SimulationResults { fun simBounds(): Interval /** - * Query a resource profile from the database + * Query a resource profile from this simulation dataset. * * @param deserializer constructor of the profile, converting [SerializedValue] * @param name string name of the resource */ - fun > resource(name: String, deserializer: (List>) -> TL): TL + fun > resource(name: String, deserializer: (List>) -> TL): TL /** * Query activity instances. diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java index 42dde11280..9495c91961 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java @@ -9,6 +9,7 @@ import gov.nasa.jpl.aerie.scheduler.model.ActivityType; import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivity; import gov.nasa.jpl.aerie.scheduler.plan.InMemoryEditablePlan; import gov.nasa.jpl.aerie.scheduler.plan.SchedulerToProcedurePlanAdapter; @@ -35,7 +36,15 @@ public Procedure(final PlanningHorizon planningHorizon, Path jarPath, Map missionModel, Function lookupActivityType, SimulationFacade simulationFacade, DirectiveIdGenerator idGenerator) { + public void run( + final Problem problem, + final Evaluation eval, + final Plan plan, + final MissionModel missionModel, + final Function lookupActivityType, + final SimulationFacade simulationFacade, + final DirectiveIdGenerator idGenerator + ) { final ProcedureMapper procedureMapper; try { procedureMapper = ProcedureLoader.loadProcedure(jarPath); @@ -47,7 +56,9 @@ public void run(Evaluation eval, Plan plan, MissionModel missionModel, Functi final var planAdapter = new SchedulerToProcedurePlanAdapter( plan, - planHorizon + planHorizon, + problem.getDiscreteExternalProfiles(), + problem.getRealExternalProfiles() ); final var editablePlan = new InMemoryEditablePlan( diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index cc9a5f14c9..7167327a96 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -323,7 +323,7 @@ private void satisfyGoal(Goal goal) throws SchedulingInterruptedException{ satisfyOptionGoal(optionGoal); } else if (goal instanceof Procedure procedure) { if (!analysisOnly) { - procedure.run(plan.getEvaluation(), plan, problem.getMissionModel(), this.problem::getActivityType, this.simulationFacade, this.idGenerator); + procedure.run(problem, plan.getEvaluation(), plan, problem.getMissionModel(), this.problem::getActivityType, this.simulationFacade, this.idGenerator); } } else { satisfyGoalGeneral(goal); diff --git a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt index c3da7cc682..1b3cdca4c5 100644 --- a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt +++ b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt @@ -1,17 +1,16 @@ package gov.nasa.jpl.aerie.scheduler.plan -import gov.nasa.jpl.aerie.merlin.driver.engine.ProfileSegment -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue import gov.nasa.ammos.aerie.procedural.timeline.Interval import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances -import gov.nasa.ammos.aerie.procedural.timeline.util.duration.rangeTo -import gov.nasa.ammos.aerie.procedural.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment -import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Activity import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Instance import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults +import gov.nasa.ammos.aerie.procedural.timeline.util.duration.rangeTo +import gov.nasa.jpl.aerie.merlin.driver.engine.ProfileSegment +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 gov.nasa.jpl.aerie.types.ActivityInstanceId import java.time.Instant @@ -46,7 +45,7 @@ class MerlinToProcedureSimulationResultsAdapter( } } - override fun > resource(name: String, deserializer: (List>) -> TL): TL { + override fun > resource(name: String, deserializer: (List>) -> TL): TL { val profile = if (results.discreteProfiles.containsKey(name)) convertProfileWithoutGaps(results.discreteProfiles[name]!!.segments) { it } else if (results.realProfiles.containsKey(name)) convertProfileWithoutGaps(results.realProfiles[name]!!.segments) { diff --git a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt index 71a1b78263..638ea9f979 100644 --- a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt +++ b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt @@ -2,11 +2,17 @@ package gov.nasa.jpl.aerie.scheduler.plan import gov.nasa.ammos.aerie.procedural.timeline.Interval import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives +import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps +import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Directive import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart.Anchor.AnchorPoint.Companion.anchorToStart import gov.nasa.ammos.aerie.procedural.timeline.util.duration.minus import gov.nasa.ammos.aerie.procedural.timeline.util.duration.plus +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile +import gov.nasa.jpl.aerie.constraints.model.LinearProfile +import gov.nasa.jpl.aerie.constraints.time.Interval as ConstraintsInterval +import gov.nasa.jpl.aerie.constraints.time.Segment as ConstraintsSegment import gov.nasa.jpl.aerie.merlin.protocol.types.Duration import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon @@ -17,6 +23,8 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan as SchedulerPlan data class SchedulerToProcedurePlanAdapter( private val schedulerPlan: SchedulerPlan, private val planningHorizon: PlanningHorizon, + private val discreteExternalResources: Map, + private val realExternalResources: Map, ): TimelinePlan, SchedulerPlan by schedulerPlan { override fun totalBounds() = Interval.between(Duration.ZERO, planningHorizon.aerieHorizonDuration) @@ -47,4 +55,38 @@ data class SchedulerToProcedurePlanAdapter( return Directives(result) } + override fun > resource( + name: String, + deserializer: (List>) -> TL + ): TL { + fun constraintsToProceduralInterval(i: ConstraintsInterval) = Interval( + i.start, + i.end, + if (i.includesStart()) Interval.Inclusivity.Inclusive else Interval.Inclusivity.Exclusive, + if (i.includesEnd()) Interval.Inclusivity.Inclusive else Interval.Inclusivity.Exclusive + ) + fun constraintsToProceduralSegment(seg: ConstraintsSegment) = Segment( + constraintsToProceduralInterval(seg.interval), + seg.value + ) + val segments = when (name) { + in discreteExternalResources -> discreteExternalResources[name]!! + .asIterable().map(::constraintsToProceduralSegment) + .toList() + in realExternalResources -> realExternalResources[name]!! + .asIterable() + .map(::constraintsToProceduralSegment) + .map { it.withNewValue(SerializedValue.of( + mapOf( + "initial" to SerializedValue.of(it.value.valueAt(it.interval.start)), + "rate" to SerializedValue.of(it.value.rate) + ) + ))} + .toList() + else -> throw IllegalArgumentException("External profile not found: $name") + } + + return deserializer(segments) + } + }