Fix ids for kurt so front end re-uses same ones on executions (#4780)

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* working test;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update src/wasm-lib/tests/executor/main.rs

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Update src/wasm-lib/tests/executor/main.rs

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* fix race condition

* fix whoopsie

* fix tsc

* for some dumb ass reason the model executes twice on load

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
Jess Frazelle
2024-12-12 18:06:26 -08:00
committed by GitHub
parent d44dc1b21a
commit ac60082e67
8 changed files with 103 additions and 41 deletions

View File

@ -112,10 +112,10 @@ test.describe('Editor tests', () => {
await u.openDebugPanel() await u.openDebugPanel()
await expect( await expect(
page.locator('[data-receive-command-type="scene_clear_all"]') page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(1) ).toHaveCount(2)
await expect( await expect(
page.locator('[data-message-type="execution-done"]') page.locator('[data-message-type="execution-done"]')
).toHaveCount(1) ).toHaveCount(2)
// Add whitespace to the end of the code. // Add whitespace to the end of the code.
await u.codeLocator.click() await u.codeLocator.click()
@ -133,10 +133,10 @@ test.describe('Editor tests', () => {
// Make sure we didn't clear the scene. // Make sure we didn't clear the scene.
await expect( await expect(
page.locator('[data-message-type="execution-done"]') page.locator('[data-message-type="execution-done"]')
).toHaveCount(2) ).toHaveCount(3)
await expect( await expect(
page.locator('[data-receive-command-type="scene_clear_all"]') 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 ({ test('if you click the format button it formats your code and executes so lints are still there', async ({

View File

@ -311,8 +311,6 @@ export class KclManager {
// Do not send send scene commands if the program was interrupted, go to clean up // Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) { if (!isInterrupted) {
this.addDiagnostics(await lintAst({ ast: ast })) this.addDiagnostics(await lintAst({ ast: ast }))
sceneInfra.modelingSend({ type: 'code edit during sketch' })
setSelectionFilterToDefault(execState.memory, this.engineCommandManager) setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
if (args.zoomToFit) { if (args.zoomToFit) {
@ -358,7 +356,13 @@ export class KclManager {
this.lastSuccessfulProgramMemory = execState.memory this.lastSuccessfulProgramMemory = execState.memory
} }
this.ast = { ...ast } this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(this.ast)
this._executeCallback() this._executeCallback()
if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' })
}
this.engineCommandManager.addCommandLog({ this.engineCommandManager.addCommandLog({
type: 'execution-done', type: 'execution-done',
data: null, data: null,

View File

@ -66,9 +66,7 @@ export async function executeAst({
? enginelessExecutor(ast, programMemoryOverride) ? enginelessExecutor(ast, programMemoryOverride)
: _executor(ast, engineCommandManager)) : _executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands( await engineCommandManager.waitForAllCommands()
programMemoryOverride !== undefined
)
return { return {
logs: [], logs: [],

View File

@ -247,7 +247,7 @@ extrude003 = extrude(-15, sketch003)`
selectedSegmentSnippet, selectedSegmentSnippet,
expectedExtrudeSnippet expectedExtrudeSnippet
) )
}) }, 5_000)
}) })
const runModifyAstCloneWithEdgeTreatmentAndTag = async ( const runModifyAstCloneWithEdgeTreatmentAndTag = async (
@ -477,7 +477,7 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)` |> chamfer({ length = 5, tags = [seg01] }, %)`
const segmentSnippets = ['line([-20, 0], %)'] const segmentSnippets = ['line([-20, 0], %)']
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
@ -487,8 +487,8 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %) |> chamfer({ length = 5, tags = [seg01] }, %)
|> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)`
await runModifyAstCloneWithEdgeTreatmentAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,

View File

@ -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 { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' 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 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. * When this is done when we build the artifact map synchronously.
*/ */
async waitForAllCommands(useFakeExecutor = false) { waitForAllCommands() {
await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise)) return Promise.all(
setTimeout(() => { Object.values(this.pendingCommands).map((a) => a.promise)
// 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 updateArtifactGraph(ast: Program) {
// TODO: race condition
if (!this?.kclManager) return
this.artifactGraph = createArtifactGraph({ this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands, orderedCommands: this.orderedCommands,
responseMap: this.responseMap, responseMap: this.responseMap,
ast: this.kclManager.ast, ast,
}) })
if (useFakeExecutor) { // TODO check if these still need to be deferred once e2e tests are working again.
// 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) { if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null) this.deferredArtifactEmptied(null)
} else { } else {
this.deferredArtifactPopulated(null) this.deferredArtifactPopulated(null)
} }
})
} }
/** /**

View File

@ -1895,11 +1895,19 @@ impl ExecutorContext {
}; };
if cache_result.clear_scene && !self.is_mock() { 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 // 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. // anyways and from the TS side we override memory and don't want to clear it.
self.reset_scene(exec_state, Default::default()).await?; 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. // TODO: Use the top-level file's path.

View File

@ -69,7 +69,7 @@ async fn do_execute_and_snapshot(
Ok((exec_state, img)) Ok((exec_state, img))
} }
async fn new_context( pub async fn new_context(
units: UnitLength, units: UnitLength,
with_auth: bool, with_auth: bool,
project_directory: Option<PathBuf>, project_directory: Option<PathBuf>,

View File

@ -1,7 +1,7 @@
mod cache; mod cache;
use kcl_lib::{ 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, UnitLength,
}; };
@ -2001,3 +2001,62 @@ async fn kcl_test_error_no_auth_websocket() {
.to_string() .to_string()
.contains("Please send the following object over this websocket")); .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);
}