Skip to content

Commit

Permalink
Merge pull request #1169 from NASA-AMMOS/fix/get-constraints-loads-co…
Browse files Browse the repository at this point in the history
…rrect-sim

Make Check Constraints Load Correct Sim Dataset
  • Loading branch information
Mythicaeda authored Oct 3, 2023
2 parents 1f51a50 + a16e883 commit 323cf5b
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 15 deletions.
21 changes: 21 additions & 0 deletions e2e-tests/src/tests/bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as urls from '../utilities/urls.js';
test.describe('Merlin Bindings', () => {
let mission_model_id: number;
let plan_id: number;
let second_plan_id: number;
let admin: User = {
username: "Admin_User",
default_role: "aerie_admin",
Expand Down Expand Up @@ -50,11 +51,16 @@ test.describe('Merlin Bindings', () => {
duration : time.getIntervalFromDoyRange(plan_start_timestamp, plan_end_timestamp)
};
plan_id = await req.createPlan(request, plan_input, admin.session);

// Insert the Second Plan
plan_input.name = plan_input.name +' (2)';
second_plan_id = await req.createPlan(request, plan_input, admin.session);
});
test.afterAll(async ({ request }) => {
// Remove Model and Plan
await req.deleteMissionModel(request, mission_model_id);
await req.deletePlan(request, plan_id);
await req.deletePlan(request, second_plan_id);

// Remove Users
await req.deleteUser(request, admin.username);
Expand Down Expand Up @@ -201,6 +207,21 @@ test.describe('Merlin Bindings', () => {
cause: "no simulation datasets found for plan id " + plan_id
});

// Returns a 404 if given an invalid simulation dataset id
// message is "Simulation Dataset with id `1` does not belong to Plan with id `1`
const invalidSimDatasetId = (await awaitSimulation(request, second_plan_id)).simulationDatasetId;
response = await request.post(`${urls.MERLIN_URL}/constraintViolations`, {
data: {
action: {name: "check_constraints"},
input: {planId: plan_id, simulationDatasetId: invalidSimDatasetId},
request_query: "",
session_variables: admin.session}});
expect(response.status()).toEqual(404);
expect((await response.json())).toEqual({
message: "input mismatch exception",
cause: "Simulation Dataset with id `" + invalidSimDatasetId + "` does not belong to Plan with id `"+plan_id+"`"
});

// Simulation already tested; run one
// "status" is not "failed"
response = await request.post(`${urls.MERLIN_URL}/getSimulationResults`, {
Expand Down
108 changes: 108 additions & 0 deletions e2e-tests/src/tests/constraints-load-old-sim-results.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { expect, test } from '@playwright/test';
import req, { awaitSimulation } from '../utilities/requests.js';

test.describe.serial('Check Constraints Against Specific Sim Datasets', () => {
const constraintName = 'fruit_equal_peel';
const activity_offset_hours = 1;

const plan_start_timestamp = "2023-01-01T00:00:00+00:00";

let mission_model_id: number;
let plan_id: number;
let constraint_id: number;
let violation: Violation;
let activity_id: number;

test.beforeAll(async ({request}) => {
let rd = Math.random() * 100000;
let jar_id = await req.uploadJarFile(request);
// Add Mission Model
const model: MissionModelInsertInput = {
jar_id,
mission: 'aerie_e2e_tests',
name: 'Banananation (e2e tests)',
version: rd + "",
};
mission_model_id = await req.createMissionModel(request, model);

// Add Plan
const plan_input: CreatePlanInput = {
model_id: mission_model_id,
name: 'test_plan' + rd,
start_time: plan_start_timestamp,
duration: "24:00:00"
};
plan_id = await req.createPlan(request, plan_input);

// Add Activity
const activityToInsert: ActivityInsertInput = {
arguments: {
biteSize: 2,
},
plan_id: plan_id,
type: 'BiteBanana',
start_offset: activity_offset_hours + 'h',
};
activity_id = await req.insertActivity(request, activityToInsert);

// Add Constraint
const constraint: ConstraintInsertInput = {
name: constraintName,
definition: 'export default (): Constraint => Real.Resource("/fruit").equal(Real.Resource("/peel"))',
description: '',
model_id: null,
plan_id,
};
constraint_id = await req.insertConstraint(request, constraint);
});

test.afterAll(async ({request}) => {
// Delete Constraint
await req.deleteConstraint(request, constraint_id);

// Deleting Plan and Model cascades the rest of the cleanup
await req.deleteMissionModel(request, mission_model_id);
await req.deletePlan(request, plan_id);
});

test('Constraints Loads Specified Outdated Sim Dataset', async ({request}) => {
// Simulate
const oldSimDatasetId = (await awaitSimulation(request, plan_id)).simulationDatasetId;

// Invalidate Results and Resim
await req.deleteActivity(request, plan_id, activity_id);
const newSimulationDatasetId = (await awaitSimulation(request, plan_id)).simulationDatasetId;

// Assert No Violations on Newest Results
const newConstraintResults = await req.checkConstraints(request, plan_id, newSimulationDatasetId);
expect(newConstraintResults).toHaveLength(0);

// Assert One Violation on Old Results
const oldConstraintResults = await req.checkConstraints(request, plan_id, oldSimDatasetId);
expect(oldConstraintResults).toHaveLength(1);

// Assert the Results to be as expected
const result = oldConstraintResults.pop();
expect(result.constraintName).toEqual(constraintName);
expect(result.constraintId).toEqual(constraint_id);

// Resources
expect(result.resourceIds).toHaveLength(2);
expect(result.resourceIds).toContain('/fruit');
expect(result.resourceIds).toContain('/peel');

// Violation
expect(result.violations).toHaveLength(1);

// Violation Window
const plan_duration_micro = 24 * 60 * 60 * 1000 * 1000;
const activity_offset_micro = activity_offset_hours * 60 * 60 * 1000 * 1000;

violation = result.violations[0];
expect(violation.windows[0].start).toEqual(activity_offset_micro);
expect(violation.windows[0].end).toEqual(plan_duration_micro);

// Gaps
expect(result.gaps).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package gov.nasa.jpl.aerie.merlin.server.exceptions;

import gov.nasa.jpl.aerie.merlin.server.models.PlanId;
import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId;

public class SimulationDatasetMismatchException extends Exception {
public final SimulationDatasetId simDatasetId;
public final PlanId planId;

public SimulationDatasetMismatchException(final PlanId pid, final SimulationDatasetId sid) {
super("Simulation Dataset with id `" + sid.id() + "` does not belong to Plan with id `"+pid.id()+"`");
this.planId = pid;
this.simDatasetId = sid;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException;
import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanDatasetException;
import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanException;
import gov.nasa.jpl.aerie.merlin.server.exceptions.SimulationDatasetMismatchException;
import gov.nasa.jpl.aerie.merlin.server.services.ConstraintAction;
import gov.nasa.jpl.aerie.merlin.server.models.HasuraAction;
import gov.nasa.jpl.aerie.merlin.server.models.PlanId;
Expand Down Expand Up @@ -250,6 +251,8 @@ private void getConstraintViolations(final Context ctx) {
ctx.status(404).result(ExceptionSerializers.serializeNoSuchPlanException(ex).toString());
} catch (final InputMismatchException ex) {
ctx.status(404).result(ResponseSerializers.serializeInputMismatchException(ex).toString());
} catch (SimulationDatasetMismatchException ex) {
ctx.status(404).result(ResponseSerializers.serializeSimulationDatasetMismatchException(ex).toString());
} catch (final PermissionsServiceException ex) {
ctx.status(503).result(ExceptionSerializers.serializePermissionsServiceException(ex).toString());
} catch (final Unauthorized ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema;
import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanDatasetException;
import gov.nasa.jpl.aerie.merlin.server.exceptions.NoSuchPlanException;
import gov.nasa.jpl.aerie.merlin.server.exceptions.SimulationDatasetMismatchException;
import gov.nasa.jpl.aerie.merlin.server.remotes.MissionModelAccessException;
import gov.nasa.jpl.aerie.merlin.server.services.GetSimulationResultsAction;
import gov.nasa.jpl.aerie.merlin.server.services.LocalMissionModelService;
Expand Down Expand Up @@ -468,6 +469,13 @@ public static JsonValue serializeInputMismatchException(final InputMismatchExcep
.build();
}

public static JsonValue serializeSimulationDatasetMismatchException(final SimulationDatasetMismatchException ex){
return Json.createObjectBuilder()
.add("message", "input mismatch exception")
.add("cause", ex.getMessage())
.build();
}

private static final class ValueSchemaSerializer implements ValueSchema.Visitor<JsonValue> {
@Override
public JsonValue onReal() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public Optional<ResultsProtocol.ReaderRole> lookup(final PlanId planId) {
}
}

@Override
public Optional<ResultsProtocol.ReaderRole> lookup(final PlanId planId, final SimulationDatasetId simulationDatasetId) {
return lookup(planId);
}

public boolean isEqualTo(final InMemoryResultsCellRepository other) {
return this.cells.equals(other.cells);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package gov.nasa.jpl.aerie.merlin.server.remotes;

import gov.nasa.jpl.aerie.merlin.server.ResultsProtocol;
import gov.nasa.jpl.aerie.merlin.server.exceptions.SimulationDatasetMismatchException;
import gov.nasa.jpl.aerie.merlin.server.models.PlanId;
import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId;

import java.util.Optional;

Expand All @@ -11,4 +13,6 @@ public interface ResultsCellRepository {
Optional<ResultsProtocol.OwnerRole> claim(PlanId planId, Long datasetId);

Optional<ResultsProtocol.ReaderRole> lookup(PlanId planId);

Optional<ResultsProtocol.ReaderRole> lookup(PlanId planId, SimulationDatasetId simulationDatasetId) throws SimulationDatasetMismatchException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package gov.nasa.jpl.aerie.merlin.server.remotes.postgres;

import gov.nasa.jpl.aerie.merlin.server.models.Timestamp;
import org.intellij.lang.annotations.Language;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Optional;

public class GetSimulationDatasetByIdAction implements AutoCloseable {
private static final @Language("Sql") String sql = """
select
d.simulation_id as simulation_id,
d.status as status,
d.reason as reason,
d.canceled as canceled,
to_char(d.simulation_start_time, 'YYYY-DDD"T"HH24:MI:SS.FF6') as simulation_start_time,
to_char(d.simulation_end_time, 'YYYY-DDD"T"HH24:MI:SS.FF6') as simulation_end_time,
d.dataset_id as dataset_id
from simulation_dataset as d
where
d.id = ?
""";

private final PreparedStatement statement;

public GetSimulationDatasetByIdAction(final Connection connection) throws SQLException {
this.statement = connection.prepareStatement(sql);
}

public Optional<SimulationDatasetRecord> get(
final long simulationDatasetId
) throws SQLException {
this.statement.setLong(1, simulationDatasetId);

try (final var results = this.statement.executeQuery()) {
if (!results.next()) return Optional.empty();

final SimulationStateRecord.Status status;
try {
status = SimulationStateRecord.Status.fromString(results.getString("status"));
} catch (final SimulationStateRecord.Status.InvalidSimulationStatusException ex) {
throw new Error("Simulation Dataset initialized with invalid state.");
}

final var simulationId = results.getLong("simulation_id");
final var reason = PreparedStatements.getFailureReason(results, 3);
final var canceled = results.getBoolean("canceled");
final var state = new SimulationStateRecord(status, reason);
final var simStartTime = Timestamp.fromString(results.getString("simulation_start_time"));
final var simEndTime = Timestamp.fromString(results.getString("simulation_end_time"));
final var datasetId = results.getLong("dataset_id");

return Optional.of(
new SimulationDatasetRecord(
simulationId,
datasetId,
state,
canceled,
simStartTime,
simEndTime,
simulationDatasetId));
}
}

@Override
public void close() throws SQLException {
this.statement.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ public void insertConstraintRuns(

@Override
public Map<Long, ConstraintRunRecord> getValidConstraintRuns(List<Long> constraintIds, SimulationDatasetId simulationDatasetId) {
try (final var connection = this.dataSource.getConnection()) {
final var constraintRuns = new GetValidConstraintRunsAction(connection, constraintIds, simulationDatasetId).get();
try (final var connection = this.dataSource.getConnection();
final var validConstraintRunAction = new GetValidConstraintRunsAction(connection, constraintIds, simulationDatasetId)) {
final var constraintRuns = validConstraintRunAction.get();
final var validConstraintRuns = new HashMap<Long, ConstraintRunRecord>();

for (final var constraintRun : constraintRuns) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema;
import gov.nasa.jpl.aerie.merlin.server.ResultsProtocol;
import gov.nasa.jpl.aerie.merlin.server.ResultsProtocol.State;
import gov.nasa.jpl.aerie.merlin.server.exceptions.SimulationDatasetMismatchException;
import gov.nasa.jpl.aerie.merlin.server.models.PlanId;
import gov.nasa.jpl.aerie.merlin.server.models.ProfileSet;
import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId;
Expand Down Expand Up @@ -120,14 +121,32 @@ public Optional<ResultsProtocol.OwnerRole> claim(final PlanId planId, final Long
public Optional<ResultsProtocol.ReaderRole> lookup(final PlanId planId) {
try (final var connection = this.dataSource.getConnection()) {
final var simulation = getSimulation(connection, planId);
final var datasetRecord = lookupSimulationDatasetRecord(
connection,
simulation.id());
final var datasetId$ = datasetRecord.map(SimulationDatasetRecord::datasetId);
final var datasetRecord = lookupSimulationDatasetRecord(connection, simulation.id());

if (datasetRecord.isEmpty()) return Optional.empty();

if (datasetId$.isEmpty()) return Optional.empty();
final var datasetId = datasetRecord.get().datasetId();
return Optional.of(new PostgresResultsCell(this.dataSource, simulation, datasetId));
} catch (final SQLException ex) {
throw new DatabaseException("Failed to get simulation", ex);
}
}

@Override
public Optional<ResultsProtocol.ReaderRole> lookup(final PlanId planId, final SimulationDatasetId simulationDatasetId) throws SimulationDatasetMismatchException {
try (final var connection = this.dataSource.getConnection()) {
final var simulation = getSimulation(connection, planId);
final var datasetRecord = getSimulationDatasetRecordById(connection, simulationDatasetId.id());

if (datasetRecord.isEmpty()) return Optional.empty();
// If this check fails, then the specified sim dataset is not a simulation for the specified plan
if (datasetRecord.get().simulationId() != simulation.id()) {
throw new SimulationDatasetMismatchException(
planId,
new SimulationDatasetId(datasetRecord.get().simulationDatasetId()));
}

final var datasetId = datasetId$.get();
final var datasetId = datasetRecord.get().datasetId();
return Optional.of(new PostgresResultsCell(this.dataSource, simulation, datasetId));
} catch (final SQLException ex) {
throw new DatabaseException("Failed to get simulation", ex);
Expand Down Expand Up @@ -165,6 +184,16 @@ private static Optional<SimulationDatasetRecord> getSimulationDatasetRecord(
}
}

private static Optional<SimulationDatasetRecord> getSimulationDatasetRecordById(
final Connection connection,
final long simulationDatasetId
) throws SQLException
{
try (final var lookupSimulationDatasetAction = new GetSimulationDatasetByIdAction(connection)) {
return lookupSimulationDatasetAction.get(simulationDatasetId);
}
}

private static SimulationDatasetRecord createSimulationDataset(
final Connection connection,
final SimulationRecord simulation,
Expand Down
Loading

0 comments on commit 323cf5b

Please sign in to comment.