From 674837ef023529b82d2ad2f4a613dc736208ccd1 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 14 Aug 2023 09:22:08 -0700 Subject: [PATCH] Add e2e-test --- .../src/tests/scheduler-with-sim.test.ts | 319 ++++++++++++++++++ e2e-tests/src/tests/scheduler.test.ts | 33 +- e2e-tests/src/utilities/gql.ts | 40 +++ e2e-tests/src/utilities/requests.ts | 84 +++++ 4 files changed, 449 insertions(+), 27 deletions(-) create mode 100644 e2e-tests/src/tests/scheduler-with-sim.test.ts diff --git a/e2e-tests/src/tests/scheduler-with-sim.test.ts b/e2e-tests/src/tests/scheduler-with-sim.test.ts new file mode 100644 index 0000000000..e62e783e0a --- /dev/null +++ b/e2e-tests/src/tests/scheduler-with-sim.test.ts @@ -0,0 +1,319 @@ + +import {expect, test} from "@playwright/test"; +import req, {awaitScheduling, awaitSimulation} from "../utilities/requests.js"; +import time from "../utilities/time.js"; + +/* +This is testing the check on Plan Revision before loading initial simulation results. We inject results associated with an old plan revision. +In these results, there is only one GrowBanana activity instead of the actual 2 present in the latest plan revision. +A coexistence goal attaching to GrowBanana activities shows that the scheduler did not use the stale sim results. +*/ +test.describe.serial('Scheduling with initial sim results', () => { + const rd = Math.random() * 100; + const plan_start_timestamp = "2021-001T00:00:00.000"; + const plan_end_timestamp = "2021-001T12:00:00.000"; + + test('Main', async ({ request }) => { + //upload bananation jar + const jar_id = await req.uploadJarFile(request); + + const model: MissionModelInsertInput = { + jar_id, + mission: 'aerie_e2e_tests' + rd, + name: 'Banananation (e2e tests)' + rd, + version: '0.0.0' + rd, + }; + const mission_model_id = await req.createMissionModel(request, model); + //delay for generation + await delay(2000); + const plan_input : CreatePlanInput = { + model_id : mission_model_id, + name : 'test_plan' + rd, + start_time : plan_start_timestamp, + duration : time.getIntervalFromDoyRange(plan_start_timestamp, plan_end_timestamp) + }; + const plan_id = await req.createPlan(request, plan_input); + + const firstGrowBananaToInsert : ActivityInsertInput = + { + //no arguments to ensure that the scheduler is getting effective arguments + arguments : {}, + plan_id: plan_id, + type : "GrowBanana", + start_offset : "1h" + }; + + await req.insertActivity(request, firstGrowBananaToInsert); + + await awaitSimulation(request, plan_id); + + const secondGrowBananaToInsert : ActivityInsertInput = + { + //no arguments to ensure that the scheduler is getting effective arguments + arguments : {}, + plan_id: plan_id, + type : "GrowBanana", + start_offset : "2h" + }; + + await req.insertActivity(request, secondGrowBananaToInsert); + + const schedulingGoal1 : SchedulingGoalInsertInput = + { + description: "Test goal", + model_id: mission_model_id, + name: "ForEachGrowPeel"+rd, + definition: `export default function myGoal() { + return Goal.CoexistenceGoal({ + forEach: ActivityExpression.ofType(ActivityType.GrowBanana), + activityTemplate: ActivityTemplates.BiteBanana({ + biteSize: 1, + }), + startsAt:TimingConstraint.singleton(WindowProperty.END) + }) + }` + }; + + const first_goal_id = await req.insertSchedulingGoal(request, schedulingGoal1); + + let plan_revision = await req.getPlanRevision(request, plan_id); + + const schedulingSpecification : SchedulingSpecInsertInput = { + // @ts-ignore + horizon_end: plan_end_timestamp, + horizon_start: plan_start_timestamp, + plan_id : plan_id, + plan_revision : plan_revision, + simulation_arguments : {}, + analysis_only: false + } + const scheduling_specification_id = await req.insertSchedulingSpecification(request, schedulingSpecification); + + const priority = 0; + const specGoal: SchedulingSpecGoalInsertInput = { + goal_id: first_goal_id, + priority: priority, + specification_id: scheduling_specification_id, + }; + await req.createSchedulingSpecGoal(request, specGoal); + + const { status, datasetId } = await awaitScheduling(request, scheduling_specification_id); + + expect(status).toEqual("complete") + expect(datasetId).not.toBeNull(); + + const plan = await req.getPlan(request, plan_id) + expect(plan.activity_directives.length).toEqual(4); + + //delete plan + const deleted_plan_id = await req.deletePlan(request, plan_id); + expect(deleted_plan_id).not.toBeNull(); + expect(deleted_plan_id).toBeDefined(); + expect(deleted_plan_id).toEqual(plan_id); + + //delete mission model + const deleted_mission_model_id = await req.deleteMissionModel(request, mission_model_id) + expect(deleted_mission_model_id).not.toBeNull(); + expect(deleted_mission_model_id).toBeDefined(); + expect(deleted_mission_model_id).toEqual(mission_model_id); + }); + + + /* In this test, we load simulation results with the current plan revision but with a different sim config. If the + injected results are picked up, the goal will not be satisfied and the number of activities will stay at its original value. + */ + test('Scheduling sim results 2', async ({ request }) => { + const rd = Math.random() * 100; + const plan_start_timestamp = "2021-001T00:00:00.000"; + const plan_end_timestamp = "2021-001T12:00:00.000"; + + //upload bananation jar + const jar_id = await req.uploadJarFile(request); + + const model: MissionModelInsertInput = { + jar_id, + mission: 'aerie_e2e_tests' + rd, + name: 'Banananation (e2e tests)' + rd, + version: '0.0.0' + rd, + }; + const mission_model_id = await req.createMissionModel(request, model); + //delay for generation + await delay(2000); + const plan_input : CreatePlanInput = { + model_id : mission_model_id, + name : 'test_plan' + rd, + start_time : plan_start_timestamp, + duration : time.getIntervalFromDoyRange(plan_start_timestamp, plan_end_timestamp) + }; + const plan_id = await req.createPlan(request, plan_input); + + const simulation_id = await req.getSimulationId(request, plan_id); + + const simulation_template : InsertSimulationTemplateInput = { + model_id: mission_model_id, + arguments: { + "initialPlantCount": 400, + }, + description: 'Template for Plan ' +plan_id + }; + + await req.insertAndAssociateSimulationTemplate(request, simulation_template, simulation_id); + + await awaitSimulation(request, plan_id); + + const empty_simulation_template : InsertSimulationTemplateInput = { + model_id: mission_model_id, + arguments: { + }, + description: 'Template for Plan ' +plan_id + }; + + await req.insertAndAssociateSimulationTemplate(request, empty_simulation_template, simulation_id); + + const schedulingGoal1 : SchedulingGoalInsertInput = + { + description: "Test goal", + model_id: mission_model_id, + name: "ForEachPlanLessThan300"+rd, + definition: `export default () => Goal.CoexistenceGoal({ + forEach: Real.Resource("/plant").lessThan(300), + activityTemplate: ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), + startsAt: TimingConstraint.singleton(WindowProperty.START) + })` + }; + + const first_goal_id = await req.insertSchedulingGoal(request, schedulingGoal1); + + const plan_revision = await req.getPlanRevision(request, plan_id); + + const schedulingSpecification : SchedulingSpecInsertInput = { + // @ts-ignore + horizon_end: plan_end_timestamp, + horizon_start: plan_start_timestamp, + plan_id : plan_id, + plan_revision : plan_revision, + simulation_arguments : {}, + analysis_only: false + } + const specification_id = await req.insertSchedulingSpecification(request, schedulingSpecification); + + const priority = 0; + const specGoal: SchedulingSpecGoalInsertInput = { + goal_id: first_goal_id, + priority: priority, + specification_id: specification_id, + }; + await req.createSchedulingSpecGoal(request, specGoal); + + await awaitScheduling(request, specification_id); + + const plan = await req.getPlan(request, plan_id) + expect(plan.activity_directives.length).toEqual(1); + + //delete plan + const deleted_plan_id = await req.deletePlan(request, plan_id); + expect(deleted_plan_id).not.toBeNull(); + expect(deleted_plan_id).toBeDefined(); + expect(deleted_plan_id).toEqual(plan_id); + + //delete mission model + const deleted_mission_model_id = await req.deleteMissionModel(request, mission_model_id) + expect(deleted_mission_model_id).not.toBeNull(); + expect(deleted_mission_model_id).toBeDefined(); + expect(deleted_mission_model_id).toEqual(mission_model_id); + }); + + /* In this test, we inject simulation results to test that the scheduler is loading them properly. If they are picked up, + there should be no activity in the plan (plant>300). Otherwise, there should be one activity. + */ + test('Scheduling sim results 3', async ({ request }) => { + const rd = Math.random() * 100; + const plan_start_timestamp = "2021-001T00:00:00.000"; + const plan_end_timestamp = "2021-001T12:00:00.000"; + //upload bananation jar + const jar_id = await req.uploadJarFile(request); + + const model: MissionModelInsertInput = { + jar_id, + mission: 'aerie_e2e_tests' + rd, + name: 'Banananation (e2e tests)' + rd, + version: '0.0.0' + rd, + }; + const mission_model_id = await req.createMissionModel(request, model); + //delay for generation + await delay(2000); + const plan_input : CreatePlanInput = { + model_id : mission_model_id, + name : 'test_plan' + rd, + start_time : plan_start_timestamp, + duration : time.getIntervalFromDoyRange(plan_start_timestamp, plan_end_timestamp) + }; + const plan_id = await req.createPlan(request, plan_input); + + const simId = await req.getSimulationId(request, plan_id); + let plan_revision = await req.getPlanRevision(request, plan_id); + + const datasetId = await req.insertSimulationDataset( + request, + simId, + plan_start_timestamp, + plan_end_timestamp, + "success", + {}, + plan_revision); + + const profileId = await req.insertProfile(request, datasetId, "12h", "/plant", { "type": "discrete", "schema": { "type": "int" } }); + await req.insertProfileSegment(request, datasetId, 400, false, profileId, "0h"); + + const schedulingGoal1 : SchedulingGoalInsertInput = + { + description: "Test goal", + model_id: mission_model_id, + name: "ForEachPlanLessThan300"+rd, + definition: `export default () => Goal.CoexistenceGoal({ + forEach: Real.Resource("/plant").lessThan(300), + activityTemplate: ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), + startsAt: TimingConstraint.singleton(WindowProperty.START) + })` + }; + + const first_goal_id = await req.insertSchedulingGoal(request, schedulingGoal1); + + plan_revision = await req.getPlanRevision(request, plan_id); + + const schedulingSpecification : SchedulingSpecInsertInput = { + // @ts-ignore + horizon_end: plan_end_timestamp, + horizon_start: plan_start_timestamp, + plan_id : plan_id, + plan_revision : plan_revision, + simulation_arguments : {}, + analysis_only: false + } + const specification_id = await req.insertSchedulingSpecification(request, schedulingSpecification); + + const priority = 0; + const specGoal: SchedulingSpecGoalInsertInput = { + goal_id: first_goal_id, + priority: priority, + specification_id: specification_id, + }; + await req.createSchedulingSpecGoal(request, specGoal); + + await awaitScheduling(request, specification_id); + + const plan = await req.getPlan(request, plan_id) + expect(plan.activity_directives.length).toEqual(0); + + //delete plan + await req.deletePlan(request, plan_id); + + //delete mission model + await req.deleteMissionModel(request, mission_model_id) + }); +}); + + +function delay(ms: number) { + return new Promise( resolve => setTimeout(resolve, ms) ); +} diff --git a/e2e-tests/src/tests/scheduler.test.ts b/e2e-tests/src/tests/scheduler.test.ts index 0aa6fec393..64949d3bec 100644 --- a/e2e-tests/src/tests/scheduler.test.ts +++ b/e2e-tests/src/tests/scheduler.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import req from '../utilities/requests.js'; +import req, {awaitScheduling} from '../utilities/requests.js'; import time from '../utilities/time.js'; const eqSet = (xs, ys) => @@ -173,34 +173,13 @@ test.describe.serial('Scheduling', () => { }); test('Run scheduling', async ({ request }) => { - let status_local: string; - let analysisId_local: number; - const { reason, status, analysisId } = await req.schedule(request, specification_id); - expect(status).not.toBeNull(); - expect(status).toBeDefined(); + const schedulingResults = await awaitScheduling(request, specification_id); + const { analysisId, status, datasetId } = schedulingResults; + dataset_id = datasetId + expect(status).toEqual("complete") expect(analysisId).not.toBeNull(); - expect(analysisId).toBeDefined(); + expect(datasetId).not.toBeNull(); expect(typeof analysisId).toEqual("number") - analysisId_local = analysisId; - const max_it = 10; - let it = 0; - let reason_local: string; - while (it++ < max_it && (status == 'pending' || status == 'incomplete')) { - const { reason, status, analysisId, datasetId } = await req.schedule(request, specification_id); - status_local = status; - reason_local = reason; - expect(status).not.toBeNull(); - expect(status).toBeDefined(); - dataset_id = datasetId - await delay(1000); - } - if (status_local == "failed") { - console.error(reason_local) - throw new Error(reason_local); - } - expect(status_local).toEqual("complete") - expect(analysisId_local).toEqual(analysisId) - expect(dataset_id).not.toBeNull(); }); test('Verify posting of simulation results', async ({ request }) => { diff --git a/e2e-tests/src/utilities/gql.ts b/e2e-tests/src/utilities/gql.ts index 86ccd069bf..fde180557f 100644 --- a/e2e-tests/src/utilities/gql.ts +++ b/e2e-tests/src/utilities/gql.ts @@ -273,6 +273,46 @@ const gql = { } `, + INSERT_SPAN:`#graphql + mutation InsertSpan( + $parentId: Int!, + $duration: interval, + $datasetId: Int!, + $type: String, + $startOffset: interval, + $attributes: jsonb + ){ + insert_span_one(object: {parent_id: $parentId, duration: $duration, dataset_id: $datasetId, type: $type, start_offset: $startOffset, attributes: $attributes}) { + id + } +} +`, + +INSERT_SIMULATION_DATASET:`#graphql + mutation InsertSimulationDataset($simulationDatasetInsertInput:simulation_dataset_insert_input! + ){ + insert_simulation_dataset_one(object: $simulationDatasetInsertInput) { + dataset_id + } + } + `, + + INSERT_PROFILE: `#graphql + mutation insertProfile($datasetId: Int!, $duration:interval, $name:String, $type:jsonb){ + insert_profile_one(object: {dataset_id: $datasetId, duration: $duration, name: $name, type: $type}) { + id + } + } + `, + + INSERT_PROFILE_SEGMENT:`#graphql + mutation insertProfileSegment($datasetId: Int!, $dynamics:jsonb, $isGap: Boolean, $profileId:Int!, $startOffset:interval){ + insert_profile_segment_one(object: {dataset_id: $datasetId, dynamics: $dynamics, is_gap: $isGap, profile_id: $profileId, start_offset: $startOffset}){ + dataset_id + } + } + `, + INSERT_SIMULATION_TEMPLATE: `#graphql mutation CreateSimulationTemplate($simulationTemplateInsertInput: simulation_template_insert_input!) { insert_simulation_template_one(object: $simulationTemplateInsertInput) { diff --git a/e2e-tests/src/utilities/requests.ts b/e2e-tests/src/utilities/requests.ts index e0c82e92a7..d38352ee35 100644 --- a/e2e-tests/src/utilities/requests.ts +++ b/e2e-tests/src/utilities/requests.ts @@ -152,6 +152,53 @@ const req = { return simulation_dataset[0] as SimulationDataset; }, + async insertSpan( + request: APIRequestContext, + parentId: number, + duration: string, + simulationDatasetId: number, + type: string, + startOffset: string, + attributes: any){ + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_SPAN, { + parentId: parentId, + duration: duration, + datasetId: simulationDatasetId, + type: type, + startOffset: startOffset, + attributes: attributes},{}); + const { insert_span_one } = data; + const { id } = insert_span_one; + return id; + }, + + async insertSimulationDataset( + request: APIRequestContext, + simulationId: number, + simulationStartTime: string, + simulationEndTime:string, + status:string, + simulationArguments:ArgumentsMap, + planRevision: number + + ){ + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_SIMULATION_DATASET, { + simulationDatasetInsertInput : { + simulation_id: simulationId, + simulation_start_time:simulationStartTime, + simulation_end_time: simulationEndTime, + status:status, + arguments: simulationArguments, + plan_revision: planRevision + } + },{}); + const { insert_simulation_dataset_one } = data; + const { dataset_id : datasetId } = insert_simulation_dataset_one; + return datasetId; + }, + async insertAndAssociateSimulationTemplate( request: APIRequestContext, template: InsertSimulationTemplateInput, @@ -330,6 +377,22 @@ const req = { return id; }, + async insertProfile(request: APIRequestContext, datasetId: number, duration:string, name: string, type:object): Promise { + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_PROFILE, { datasetId, duration, name, type }, {}); + const { insert_profile_one } = data; + const { id } = insert_profile_one; + return id; + }, + + async insertProfileSegment(request: APIRequestContext, datasetId: number, dynamics:number, isGap: boolean, profileId:number, startOffset:string): Promise { + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_PROFILE_SEGMENT, { datasetId, dynamics, isGap, profileId, startOffset }, {}); + const { insert_profile_segment_one } = data; + const { dataset_id } = insert_profile_segment_one; + return dataset_id; + }, + async updateConstraint( request: APIRequestContext, constraintId: number, @@ -450,4 +513,25 @@ export async function awaitSimulation(request: APIRequestContext, plan_id: numbe throw Error(`Simulation timed out after ${max_iter} iterations`); } +export async function awaitScheduling(request: APIRequestContext, scheduling_specification_id: number): Promise { + const max_iter = 10; + for (let i = 0; i < max_iter; i++) { + const resp = await req.schedule(request, scheduling_specification_id); + const { reason, status } = resp; + + switch (status) { + case 'pending': + case 'incomplete': + await time.delay(1000); + break; + case 'complete': + return resp; + default: + throw Error(`Scheduling returned bad status: ${status} with reason ${reason}`); + } + } + + throw Error(`Scheduling timed out after ${max_iter} iterations`); +} + export default req;