diff --git a/e2e/playwright/editor-tests.spec.ts b/e2e/playwright/editor-tests.spec.ts index 531ada7ed..41fd88bfd 100644 --- a/e2e/playwright/editor-tests.spec.ts +++ b/e2e/playwright/editor-tests.spec.ts @@ -112,10 +112,10 @@ test.describe('Editor tests', () => { await u.openDebugPanel() await expect( page.locator('[data-receive-command-type="scene_clear_all"]') - ).toHaveCount(1) + ).toHaveCount(2) await expect( page.locator('[data-message-type="execution-done"]') - ).toHaveCount(1) + ).toHaveCount(2) // Add whitespace to the end of the code. await u.codeLocator.click() @@ -133,10 +133,10 @@ test.describe('Editor tests', () => { // Make sure we didn't clear the scene. await expect( page.locator('[data-message-type="execution-done"]') - ).toHaveCount(2) + ).toHaveCount(3) await expect( page.locator('[data-receive-command-type="scene_clear_all"]') - ).toHaveCount(1) + ).toHaveCount(2) }) test('if you click the format button it formats your code and executes so lints are still there', async ({ diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 383f087bc..1688d4cbe 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -311,8 +311,6 @@ export class KclManager { // Do not send send scene commands if the program was interrupted, go to clean up if (!isInterrupted) { this.addDiagnostics(await lintAst({ ast: ast })) - - sceneInfra.modelingSend({ type: 'code edit during sketch' }) setSelectionFilterToDefault(execState.memory, this.engineCommandManager) if (args.zoomToFit) { @@ -358,7 +356,13 @@ export class KclManager { this.lastSuccessfulProgramMemory = execState.memory } this.ast = { ...ast } + // updateArtifactGraph relies on updated executeState/programMemory + await this.engineCommandManager.updateArtifactGraph(this.ast) this._executeCallback() + if (!isInterrupted) { + sceneInfra.modelingSend({ type: 'code edit during sketch' }) + } + this.engineCommandManager.addCommandLog({ type: 'execution-done', data: null, diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index 3e604705d..64920e7ae 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -66,9 +66,7 @@ export async function executeAst({ ? enginelessExecutor(ast, programMemoryOverride) : _executor(ast, engineCommandManager)) - await engineCommandManager.waitForAllCommands( - programMemoryOverride !== undefined - ) + await engineCommandManager.waitForAllCommands() return { logs: [], diff --git a/src/lang/modifyAst/addEdgeTreatment.test.ts b/src/lang/modifyAst/addEdgeTreatment.test.ts index 481b965f3..930599229 100644 --- a/src/lang/modifyAst/addEdgeTreatment.test.ts +++ b/src/lang/modifyAst/addEdgeTreatment.test.ts @@ -247,7 +247,7 @@ extrude003 = extrude(-15, sketch003)` selectedSegmentSnippet, expectedExtrudeSnippet ) - }) + }, 5_000) }) const runModifyAstCloneWithEdgeTreatmentAndTag = async ( @@ -477,7 +477,7 @@ extrude001 = extrude(-15, sketch001) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%) extrude001 = extrude(-15, sketch001) - |> chamfer({ length: 5, tags: [seg01] }, %)` + |> chamfer({ length = 5, tags = [seg01] }, %)` const segmentSnippets = ['line([-20, 0], %)'] const expectedCode = `sketch001 = startSketchOn('XY') |> startProfileAt([-10, 10], %) @@ -487,8 +487,8 @@ extrude001 = extrude(-15, sketch001) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%) extrude001 = extrude(-15, sketch001) - |> chamfer({ length: 5, tags: [seg01] }, %) - |> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)` + |> chamfer({ length = 5, tags = [seg01] }, %) + |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)` await runModifyAstCloneWithEdgeTreatmentAndTag( code, diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index 5a92dd155..3ed12a0f3 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -1,4 +1,4 @@ -import { defaultSourceRange, SourceRange } from 'lang/wasm' +import { defaultSourceRange, Program, SourceRange } from 'lang/wasm' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' import { Models } from '@kittycad/lib' import { exportSave } from 'lib/exportSave' @@ -2099,30 +2099,23 @@ export class EngineCommandManager extends EventTarget { * When an execution takes place we want to wait until we've got replies for all of the commands * When this is done when we build the artifact map synchronously. */ - async waitForAllCommands(useFakeExecutor = false) { - await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise)) - setTimeout(() => { - // the ast is wrong without this one tick timeout. - // an example is `Solids should be select and deletable` e2e test will fail - // because the out of date ast messes with selections - // TODO: race condition - if (!this?.kclManager) return - this.artifactGraph = createArtifactGraph({ - orderedCommands: this.orderedCommands, - responseMap: this.responseMap, - ast: this.kclManager.ast, - }) - if (useFakeExecutor) { - // mock executions don't produce an artifactGraph, so this will always be empty - // skipping the below logic to wait for the next real execution - return - } - if (this.artifactGraph.size) { - this.deferredArtifactEmptied(null) - } else { - this.deferredArtifactPopulated(null) - } + waitForAllCommands() { + return Promise.all( + Object.values(this.pendingCommands).map((a) => a.promise) + ) + } + updateArtifactGraph(ast: Program) { + this.artifactGraph = createArtifactGraph({ + orderedCommands: this.orderedCommands, + responseMap: this.responseMap, + ast, }) + // TODO check if these still need to be deferred once e2e tests are working again. + if (this.artifactGraph.size) { + this.deferredArtifactEmptied(null) + } else { + this.deferredArtifactPopulated(null) + } } /** diff --git a/src/wasm-lib/kcl/src/execution/mod.rs b/src/wasm-lib/kcl/src/execution/mod.rs index d543b3e7f..c648be085 100644 --- a/src/wasm-lib/kcl/src/execution/mod.rs +++ b/src/wasm-lib/kcl/src/execution/mod.rs @@ -1895,11 +1895,19 @@ impl ExecutorContext { }; if cache_result.clear_scene && !self.is_mock() { + // Pop the execution state, since we are starting fresh. + let mut id_generator = exec_state.id_generator.clone(); + // We do not pop the ids, since we want to keep the same id generator. + // This is for the front end to keep track of the ids. + id_generator.next_id = 0; + *exec_state = ExecState { + id_generator, + ..Default::default() + }; + // We don't do this in mock mode since there is no engine connection // anyways and from the TS side we override memory and don't want to clear it. self.reset_scene(exec_state, Default::default()).await?; - // Pop the execution state, since we are starting fresh. - *exec_state = Default::default(); } // TODO: Use the top-level file's path. diff --git a/src/wasm-lib/kcl/src/test_server.rs b/src/wasm-lib/kcl/src/test_server.rs index f7246145f..bbed7e7dd 100644 --- a/src/wasm-lib/kcl/src/test_server.rs +++ b/src/wasm-lib/kcl/src/test_server.rs @@ -69,7 +69,7 @@ async fn do_execute_and_snapshot( Ok((exec_state, img)) } -async fn new_context( +pub async fn new_context( units: UnitLength, with_auth: bool, project_directory: Option, diff --git a/src/wasm-lib/tests/executor/main.rs b/src/wasm-lib/tests/executor/main.rs index 3006fcf1c..d76094aa2 100644 --- a/src/wasm-lib/tests/executor/main.rs +++ b/src/wasm-lib/tests/executor/main.rs @@ -1,7 +1,7 @@ mod cache; use kcl_lib::{ - test_server::{execute_and_snapshot, execute_and_snapshot_no_auth}, + test_server::{execute_and_snapshot, execute_and_snapshot_no_auth, new_context}, UnitLength, }; @@ -2001,3 +2001,62 @@ async fn kcl_test_error_no_auth_websocket() { .to_string() .contains("Please send the following object over this websocket")); } + +#[tokio::test(flavor = "multi_thread")] +async fn kcl_test_ids_stable_between_executions() { + let code = r#"sketch001 = startSketchOn('XZ') + |> startProfileAt([61.74, 206.13], %) + |> xLine(305.11, %, $seg01) + |> yLine(-291.85, %) + |> xLine(-segLen(seg01), %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + |> extrude(40.14, %) + |> shell({ + faces: [seg01], + thickness: 3.14, + }, %) +"#; + + let ctx = new_context(UnitLength::Mm, true, None).await.unwrap(); + let old_program = kcl_lib::Program::parse_no_errs(code).unwrap(); + // Execute the program. + let mut exec_state = Default::default(); + let cache_info = kcl_lib::CacheInformation { + old: None, + new_ast: old_program.ast.clone(), + }; + ctx.run(cache_info, &mut exec_state).await.unwrap(); + + // Get the id_generator from the first execution. + let id_generator = exec_state.id_generator.clone(); + + let code = r#"sketch001 = startSketchOn('XZ') + |> startProfileAt([62.74, 206.13], %) + |> xLine(305.11, %, $seg01) + |> yLine(-291.85, %) + |> xLine(-segLen(seg01), %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + |> extrude(40.14, %) + |> shell({ + faces: [seg01], + thickness: 3.14, + }, %) +"#; + + // Execute a slightly different program again. + let program = kcl_lib::Program::parse_no_errs(code).unwrap(); + let cache_info = kcl_lib::CacheInformation { + old: Some(kcl_lib::OldAstState { + ast: old_program.ast.clone(), + exec_state: exec_state.clone(), + settings: ctx.settings.clone(), + }), + new_ast: program.ast.clone(), + }; + // Execute the program. + ctx.run(cache_info, &mut exec_state).await.unwrap(); + + assert_eq!(id_generator, exec_state.id_generator); +}