From b38b698d6de8adbc9b2f356ec5c8338e2897df8c Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Oct 2025 12:56:51 -0500 Subject: [PATCH 01/13] chore: possible solution for isolated wasm integration testing --- src/integration.spec.ts | 365 ---------------------------------- src/lang/recast.spec.ts | 400 ++++++++++++++++++++++++++++++++++++++ src/lang/wasm.ts | 23 ++- src/lang/wasmUtilsNode.ts | 24 +++ 4 files changed, 441 insertions(+), 371 deletions(-) create mode 100644 src/lang/recast.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index 6e0f5ce4171..b853a982139 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -101,7 +101,6 @@ import { expect } from 'vitest' import type { VariableDeclaration } from '@src/lang/wasm' import { updateCenterRectangleSketch } from '@src/lib/rectangleTool' import { trap } from '@src/lib/trap' -import fs from 'node:fs' import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' import type { Parameter } from '@src/lang/wasm' import { modelingMachine } from '@src/machines/modelingMachine' @@ -5153,370 +5152,6 @@ profile001 = startProfile(at = [80, 120]) }) }) -describe('recast.test.ts', () => { - describe('recast', () => { - it('recasts a simple program', () => { - const code = '1 + 2' - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it('variable declaration', () => { - const code = 'myVar = 5' - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it("variable declaration that's binary with string", () => { - const code = "myVar = 5 + 'yo'" - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - const codeWithOtherQuotes = 'myVar = 5 + "yo"' - const { ast: ast2 } = code2ast(codeWithOtherQuotes) - const recastRetVal = recast(ast2) - if (err(recastRetVal)) throw recastRetVal - expect(recastRetVal.trim()).toBe(codeWithOtherQuotes) - }) - it('test assigning two variables, the second summing with the first', () => { - const code = `myVar = 5 -newVar = myVar + 1 -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - it('test assigning a var by cont concatenating two strings string', () => { - const code = fs.readFileSync( - './src/lang/testExamples/variableDeclaration.cado', - 'utf-8' - ) - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('test with function call', () => { - const code = `myVar = "hello" -log(5, exp = myVar) -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - it('function declaration with call', () => { - const code = [ - 'fn funcN(a, b) {', - ' return a + b', - '}', - 'theVar = 60', - 'magicNum = funcN(a = 9, b = theVar)', - ].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it('recast sketch declaration', () => { - let code = `mySketch = startSketchOn(XY) - |> startProfile(at = [0, 0]) - |> line(endAbsolute = [0, 1], tag = $myPath) - |> line(endAbsolute = [1, 1]) - |> line(endAbsolute = [1, 0], tag = $rightPath) - |> close() -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - it('sketch piped into callExpression', () => { - const code = [ - 'mySk1 = startSketchOn(XY)', - ' |> startProfile(at = [0, 0])', - ' |> line(endAbsolute = [1, 1])', - ' |> line(endAbsolute = [0, 1], tag = $myTag)', - ' |> line(endAbsolute = [1, 1])', - ' |> rx(90)', - ].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('recast BinaryExpression piped into CallExpression', () => { - const code = [ - 'fn myFn(@a) {', - ' return a + 1', - '}', - 'myVar = 5 + 1', - ' |> myFn(%)', - ].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it('recast nested binary expression', () => { - const code = ['myVar = 1 + 2 * 5'].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('recast nested binary expression with parans', () => { - const code = ['myVar = 1 + (1 + 2) * 5'].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('unnecessary paran wrap will be remove', () => { - const code = ['myVar = 1 + (2 * 5)'].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.replace('(', '').replace(')', '')) - }) - it('complex nested binary expression', () => { - const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('multiplied paren expressions', () => { - const code = ['3 + (1 + 2) * (3 + 4)'].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('recast array declaration', () => { - const code = ['three = 3', "yo = [1, '2', three, 4 + 5]"].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('recast long array declaration', () => { - const code = [ - 'three = 3', - 'yo = [', - ' 1,', - " '2',", - ' three,', - ' 4 + 5,', - " 'hey oooooo really long long long'", - ']', - ].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code.trim()) - }) - it('recast long object execution', () => { - const code = `three = 3 -yo = { - aStr = 'str', - anum = 2, - identifier = three, - binExp = 4 + 5 -} -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - it('recast short object execution', () => { - const code = `yo = { key = 'val' } -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - it('recast object execution with member expression', () => { - const code = `yo = { a = { b = { c = '123' } } } -key = 'c' -myVar = yo.a['b'][key] -key2 = 'b' -myVar2 = yo['a'][key2].c -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - }) - - describe('testing recasting with comments and whitespace', () => { - it('code with comments', () => { - const code = `yo = { a = { b = { c = '123' } } } -// this is a comment -key = 'c' -` - - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - - expect(recasted).toBe(code) - }) - it('comments at the start and end', () => { - const code = `// this is a comment -yo = { a = { b = { c = '123' } } } -key = 'c' - -// this is also a comment -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - it('comments in a pipe expression', () => { - const code = [ - 'mySk1 = startSketchOn(XY)', - ' |> startProfile(at = [0, 0])', - ' |> line(endAbsolute = [1, 1])', - ' |> line(endAbsolute = [0, 1], tag = $myTag)', - ' |> line(endAbsolute = [1, 1])', - ' // a comment', - ' |> rx(90)', - ].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it('comments sprinkled in all over the place', () => { - const code = ` -/* comment at start */ - -mySk1 = startSketchOn(XY) - |> startProfile(at = [0, 0]) - |> line(endAbsolute = [1, 1]) - // comment here - |> line(endAbsolute = [0, 1], tag = $myTag) - |> line(endAbsolute = [1, 1]) /* and - here - */ - // a comment between pipe expression statements - |> rx(90) - // and another with just white space between others below - |> ry(45) - - - |> rx(45) -/* -one more for good measure -*/ -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(`/* comment at start */ - -mySk1 = startSketchOn(XY) - |> startProfile(at = [0, 0]) - |> line(endAbsolute = [1, 1]) - // comment here - |> line(endAbsolute = [0, 1], tag = $myTag) - |> line(endAbsolute = [1, 1]) /* and - here */ - // a comment between pipe expression statements - |> rx(90) - // and another with just white space between others below - |> ry(45) - |> rx(45) -/* one more for good measure */ -`) - }) - }) - - describe('testing call Expressions in BinaryExpressions and UnaryExpressions', () => { - it('nested callExpression in binaryExpression', () => { - const code = 'myVar = 2 + min([100, legLen(hypotenuse = 5, leg = 3)])' - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it('nested callExpression in unaryExpression', () => { - const code = 'myVar = -min([100, legLen(hypotenuse = 5, leg = 3)])' - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it('with unaryExpression in callExpression', () => { - const code = 'myVar = min([5, -legLen(hypotenuse = 5, leg = 4)])' - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - it('with unaryExpression in sketch situation', () => { - const code = [ - 'part001 = startSketchOn(XY)', - ' |> startProfile(at = [0, 0])', - ' |> line(end = [\n -2.21,\n -legLen(hypotenuse = 5, leg = min([3, 999]))\n ])', - ].join('\n') - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted.trim()).toBe(code) - }) - }) - - describe('it recasts wrapped object expressions in pipe bodies with correct indentation', () => { - it('with a single line', () => { - const code = `part001 = startSketchOn(XY) - |> startProfile(at = [-0.01, -0.08]) - |> line(end = [0.62, 4.15], tag = $seg01) - |> line(end = [2.77, -1.24]) - |> angledLineThatIntersects(angle = 201, offset = -1.35, intersectTag = $seg01) - |> line(end = [-0.42, -1.72]) -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - it('recasts wrapped object expressions NOT in pipe body correctly', () => { - const code = `angledLineThatIntersects(angle = 201, offset = -1.35, intersectTag = $seg01) -` - const { ast } = code2ast(code) - const recasted = recast(ast) - if (err(recasted)) throw recasted - expect(recasted).toBe(code) - }) - }) - - describe('it recasts binary expression using brackets where needed', () => { - it('when there are two minus in a row', () => { - const code = `part001 = 1 - (def - abc) -` - const recasted = recast(code2ast(code).ast) - expect(recasted).toBe(code) - }) - }) - - // helpers - - function code2ast(code: string): { ast: Program } { - const ast = assertParse(code) - return { ast } - } -}) - describe('getNodePathFromSourceRange.test.ts', () => { describe('testing getNodePathFromSourceRange', () => { it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => { diff --git a/src/lang/recast.spec.ts b/src/lang/recast.spec.ts new file mode 100644 index 00000000000..b555b787109 --- /dev/null +++ b/src/lang/recast.spec.ts @@ -0,0 +1,400 @@ +import { err } from '@src/lib/trap' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import fs from 'node:fs' + +import type { Program } from '@src/lang/wasm' +import { assertParse, recast } from '@src/lang/wasm' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' + +function code2ast(code: string, instance: ModuleType): { ast: Program } { + const ast = assertParse(code, instance) + return { ast } +} +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('recast', () => { + it('recasts a simple program', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = '1 + 2' + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it('variable declaration', async () => { + const code = 'myVar = 5' + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it("variable declaration that's binary with string", async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = "myVar = 5 + 'yo'" + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + const codeWithOtherQuotes = 'myVar = 5 + "yo"' + const { ast: ast2 } = code2ast(codeWithOtherQuotes, instance) + const recastRetVal = recast(ast2, instance) + if (err(recastRetVal)) throw recastRetVal + expect(recastRetVal.trim()).toBe(codeWithOtherQuotes) + }) + it('test assigning two variables, the second summing with the first', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `myVar = 5 +newVar = myVar + 1 +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) + it('test assigning a var by cont concatenating two strings string', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = fs.readFileSync( + './src/lang/testExamples/variableDeclaration.cado', + 'utf-8' + ) + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('test with function call', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `myVar = "hello" +log(5, exp = myVar) +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) + it('function declaration with call', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = [ + 'fn funcN(a, b) {', + ' return a + b', + '}', + 'theVar = 60', + 'magicNum = funcN(a = 9, b = theVar)', + ].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it('recast sketch declaration', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + let code = `mySketch = startSketchOn(XY) + |> startProfile(at = [0, 0]) + |> line(endAbsolute = [0, 1], tag = $myPath) + |> line(endAbsolute = [1, 1]) + |> line(endAbsolute = [1, 0], tag = $rightPath) + |> close() +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) + it('sketch piped into callExpression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = [ + 'mySk1 = startSketchOn(XY)', + ' |> startProfile(at = [0, 0])', + ' |> line(endAbsolute = [1, 1])', + ' |> line(endAbsolute = [0, 1], tag = $myTag)', + ' |> line(endAbsolute = [1, 1])', + ' |> rx(90)', + ].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('recast BinaryExpression piped into CallExpression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = [ + 'fn myFn(@a) {', + ' return a + 1', + '}', + 'myVar = 5 + 1', + ' |> myFn(%)', + ].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it('recast nested binary expression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ['myVar = 1 + 2 * 5'].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('recast nested binary expression with parans', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ['myVar = 1 + (1 + 2) * 5'].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('unnecessary paran wrap will be remove', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ['myVar = 1 + (2 * 5)'].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.replace('(', '').replace(')', '')) + }) + it('complex nested binary expression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('multiplied paren expressions', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ['3 + (1 + 2) * (3 + 4)'].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('recast array declaration', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ['three = 3', "yo = [1, '2', three, 4 + 5]"].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('recast long array declaration', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = [ + 'three = 3', + 'yo = [', + ' 1,', + " '2',", + ' three,', + ' 4 + 5,', + " 'hey oooooo really long long long'", + ']', + ].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code.trim()) + }) + it('recast long object execution', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `three = 3 +yo = { + aStr = 'str', + anum = 2, + identifier = three, + binExp = 4 + 5 +} +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) + it('recast short object execution', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `yo = { key = 'val' } +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) + it('recast object execution with member expression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `yo = { a = { b = { c = '123' } } } +key = 'c' +myVar = yo.a['b'][key] +key2 = 'b' +myVar2 = yo['a'][key2].c +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) +}) + +describe('testing recasting with comments and whitespace', () => { + it('code with comments', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `yo = { a = { b = { c = '123' } } } +// this is a comment +key = 'c' +` + + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + + expect(recasted).toBe(code) + }) + it('comments at the start and end', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `// this is a comment +yo = { a = { b = { c = '123' } } } +key = 'c' + +// this is also a comment +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) + it('comments in a pipe expression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = [ + 'mySk1 = startSketchOn(XY)', + ' |> startProfile(at = [0, 0])', + ' |> line(endAbsolute = [1, 1])', + ' |> line(endAbsolute = [0, 1], tag = $myTag)', + ' |> line(endAbsolute = [1, 1])', + ' // a comment', + ' |> rx(90)', + ].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it('comments sprinkled in all over the place', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ` +/* comment at start */ + +mySk1 = startSketchOn(XY) + |> startProfile(at = [0, 0]) + |> line(endAbsolute = [1, 1]) + // comment here + |> line(endAbsolute = [0, 1], tag = $myTag) + |> line(endAbsolute = [1, 1]) /* and + here + */ + // a comment between pipe expression statements + |> rx(90) + // and another with just white space between others below + |> ry(45) + + + |> rx(45) +/* +one more for good measure +*/ +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(`/* comment at start */ + +mySk1 = startSketchOn(XY) + |> startProfile(at = [0, 0]) + |> line(endAbsolute = [1, 1]) + // comment here + |> line(endAbsolute = [0, 1], tag = $myTag) + |> line(endAbsolute = [1, 1]) /* and + here */ + // a comment between pipe expression statements + |> rx(90) + // and another with just white space between others below + |> ry(45) + |> rx(45) +/* one more for good measure */ +`) + }) +}) + +describe('testing call Expressions in BinaryExpressions and UnaryExpressions', () => { + it('nested callExpression in binaryExpression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = 'myVar = 2 + min([100, legLen(hypotenuse = 5, leg = 3)])' + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it('nested callExpression in unaryExpression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = 'myVar = -min([100, legLen(hypotenuse = 5, leg = 3)])' + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it('with unaryExpression in callExpression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = 'myVar = min([5, -legLen(hypotenuse = 5, leg = 4)])' + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) + it('with unaryExpression in sketch situation', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = [ + 'part001 = startSketchOn(XY)', + ' |> startProfile(at = [0, 0])', + ' |> line(end = [\n -2.21,\n -legLen(hypotenuse = 5, leg = min([3, 999]))\n ])', + ].join('\n') + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted.trim()).toBe(code) + }) +}) + +describe('it recasts wrapped object expressions in pipe bodies with correct indentation', () => { + it('with a single line', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `part001 = startSketchOn(XY) + |> startProfile(at = [-0.01, -0.08]) + |> line(end = [0.62, 4.15], tag = $seg01) + |> line(end = [2.77, -1.24]) + |> angledLineThatIntersects(angle = 201, offset = -1.35, intersectTag = $seg01) + |> line(end = [-0.42, -1.72]) +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) + it('recasts wrapped object expressions NOT in pipe body correctly', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `angledLineThatIntersects(angle = 201, offset = -1.35, intersectTag = $seg01) +` + const { ast } = code2ast(code, instance) + const recasted = recast(ast, instance) + if (err(recasted)) throw recasted + expect(recasted).toBe(code) + }) +}) + +describe('it recasts binary expression using brackets where needed', () => { + it('when there are two minus in a row', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `part001 = 1 - (def - abc) +` + const recasted = recast(code2ast(code, instance).ast, instance) + expect(recasted).toBe(code) + }) +}) diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index ad263ca90bb..6ee8ee70a62 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -40,6 +40,7 @@ import openWindow from '@src/lib/openWindow' import { Reason, err } from '@src/lib/trap' import type { DeepPartial } from '@src/lib/types' import { isArray } from '@src/lib/utils' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' import { base64_decode, change_default_units, @@ -209,11 +210,16 @@ export function resultIsOk(result: ParseResult): result is SuccessParseResult { return !!result.program && result.errors.length === 0 } -export const parse = (code: string | Error): ParseResult | Error => { +export const parse = ( + code: string | Error, + instance?: ModuleType +): ParseResult | Error => { if (err(code)) return code try { - const parsed: [Node, CompilationError[]] = parse_wasm(code) + const parsed: [Node, CompilationError[]] = instance + ? instance.parse_wasm(code) + : parse_wasm(code) let errs = splitErrors(parsed[1]) return new ParseResult(parsed[0], errs.errors, errs.warnings) } catch (e: any) { @@ -238,8 +244,11 @@ export const parse = (code: string | Error): ParseResult | Error => { /** * Parse and throw an exception if there are any errors (probably not suitable for use outside of testing). */ -export function assertParse(code: string): Node { - const result = parse(code) +export function assertParse( + code: string, + instance?: ModuleType +): Node { + const result = parse(code, instance) // eslint-disable-next-line suggest-no-throw/suggest-no-throw if (err(result)) throw result if (!resultIsOk(result)) { @@ -424,8 +433,10 @@ export async function nodePathFromRange( } } -export const recast = (ast: Program): string | Error => { - return recast_wasm(JSON.stringify(ast)) +export const recast = (ast: Program, instance?: ModuleType): string | Error => { + return instance + ? instance.recast_wasm(JSON.stringify(ast)) + : recast_wasm(JSON.stringify(ast)) } /** diff --git a/src/lang/wasmUtilsNode.ts b/src/lang/wasmUtilsNode.ts index 2e476a45cc0..5b33c97c175 100644 --- a/src/lang/wasmUtilsNode.ts +++ b/src/lang/wasmUtilsNode.ts @@ -2,6 +2,7 @@ import fs from 'fs' import path from 'path' import { init, reloadModule } from '@src/lib/wasm_lib_wrapper' import fsPromises from 'fs/promises' +import { processEnv } from '@src/env' export const wasmUrlNode = () => { // In prod the file will be right next to the compiled js file. @@ -27,6 +28,13 @@ export const wasmUrlNode = () => { // Initialise the wasm module. const initialiseNode = async () => { + if (processEnv()?.VITEST) { + const message = + 'wasmUtilsNode is trying to call initialiseNode. This will be blocked in VITEST runtimes.' + console.log(message) + return Promise.resolve(message) + } + try { await reloadModule() const fullPath = wasmUrlNode() @@ -39,3 +47,19 @@ const initialiseNode = async () => { } export const initPromiseNode = initialiseNode() + +/** + * Given a path to a .wasm file read it from disk and load the module + * then return the instance to use. Not globally shared. + */ +export const loadAndInitialiseWasmInstance = async (path: string) => { + // Read the .wasm blob off disk + const wasmBuffer = await fsPromises.readFile(path) + // get an instance of the wasm lib loader + const instanceOfWasmLibImport = await import( + `@rust/kcl-wasm-lib/pkg/kcl_wasm_lib` + ) + // Tell the instance to load the was buffer + await instanceOfWasmLibImport.default({ module_or_path: wasmBuffer }) + return instanceOfWasmLibImport +} From d1a1690d8fc339375b56cb6894b8b924924b9ace Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Oct 2025 13:46:17 -0500 Subject: [PATCH 02/13] fix: rectangleTool.spec.ts created from integration spec.ts --- src/integration.spec.ts | 74 ------------------------------- src/lib/rectangleTool.spec.ts | 83 +++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 74 deletions(-) create mode 100644 src/lib/rectangleTool.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index b853a982139..e0e0f6a7837 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -98,9 +98,6 @@ import type { Coords2d } from '@src/lang/util' import { isPointsCCW } from '@src/lang/wasm' import { closestPointOnRay } from '@src/lib/utils2d' import { expect } from 'vitest' -import type { VariableDeclaration } from '@src/lang/wasm' -import { updateCenterRectangleSketch } from '@src/lib/rectangleTool' -import { trap } from '@src/lib/trap' import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' import type { Parameter } from '@src/lang/wasm' import { modelingMachine } from '@src/machines/modelingMachine' @@ -5081,77 +5078,6 @@ describe('utils2d.test.ts', () => { }) }) -describe('rectangleTool.test.ts', () => { - describe('library rectangleTool helper functions', () => { - describe('updateCenterRectangleSketch', () => { - // regression test for https://github.com/KittyCAD/modeling-app/issues/5157 - test('should update AST and source code', async () => { - // Base source code that will be edited in place - const sourceCode = `sketch001 = startSketchOn(XZ) -profile001 = startProfile(at = [120.37, 162.76]) -|> angledLine(angle = 0, length = 0, tag = $rectangleSegmentA001) -|> angledLine(angle = segAng(rectangleSegmentA001) + 90deg, length = 0, tag = $rectangleSegmentB001) -|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001) -|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) -|> close() -` - // Create ast - const _ast = assertParse(sourceCode) - let ast = structuredClone(_ast) - - // Find some nodes and paths to reference - const sketchSnippet = `startProfile(at = [120.37, 162.76])` - const start = sourceCode.indexOf(sketchSnippet) - expect(start).toBeGreaterThanOrEqual(0) - const sketchRange = topLevelRange(start, start + sketchSnippet.length) - const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange) - const _node = getNodeFromPath( - ast, - sketchPathToNode || [], - 'VariableDeclaration' - ) - if (trap(_node)) return - const sketchInit = _node.node?.declaration.init - - // Hard code inputs that a user would have taken with their mouse - const x = 40 - const y = 60 - const rectangleOrigin = [120, 180] - const tags: [string, string, string] = [ - 'rectangleSegmentA001', - 'rectangleSegmentB001', - 'rectangleSegmentC001', - ] - - // Update the ast - if (sketchInit.type === 'PipeExpression') { - const maybeErr = updateCenterRectangleSketch( - sketchInit, - x, - y, - tags[0], - rectangleOrigin[0], - rectangleOrigin[1] - ) - expect(maybeErr).toEqual(undefined) - } - - // ast is edited in place from the updateCenterRectangleSketch - const expectedSourceCode = `sketch001 = startSketchOn(XZ) -profile001 = startProfile(at = [80, 120]) - |> angledLine(angle = 0, length = 80, tag = $rectangleSegmentA001) - |> angledLine(angle = segAng(rectangleSegmentA001) + 90deg, length = 120, tag = $rectangleSegmentB001) - |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -` - const recasted = recast(ast) - expect(recasted).toEqual(expectedSourceCode) - }) - }) - }) -}) - describe('getNodePathFromSourceRange.test.ts', () => { describe('testing getNodePathFromSourceRange', () => { it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => { diff --git a/src/lib/rectangleTool.spec.ts b/src/lib/rectangleTool.spec.ts new file mode 100644 index 00000000000..115d3177140 --- /dev/null +++ b/src/lib/rectangleTool.spec.ts @@ -0,0 +1,83 @@ +import { expect } from 'vitest' +import { getNodeFromPath } from '@src/lang/queryAst' +import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' +import { topLevelRange } from '@src/lang/util' +import type { VariableDeclaration } from '@src/lang/wasm' +import { assertParse, recast } from '@src/lang/wasm' +import { updateCenterRectangleSketch } from '@src/lib/rectangleTool' +import { trap } from '@src/lib/trap' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { join } from 'path' + +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('library rectangleTool helper functions', () => { + describe('updateCenterRectangleSketch', () => { + // regression test for https://github.com/KittyCAD/modeling-app/issues/5157 + test('should update AST and source code', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + + // Base source code that will be edited in place + const sourceCode = `sketch001 = startSketchOn(XZ) +profile001 = startProfile(at = [120.37, 162.76]) +|> angledLine(angle = 0, length = 0, tag = $rectangleSegmentA001) +|> angledLine(angle = segAng(rectangleSegmentA001) + 90deg, length = 0, tag = $rectangleSegmentB001) +|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001) +|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) +|> close() +` + // Create ast + const _ast = assertParse(sourceCode, instance) + let ast = structuredClone(_ast) + + // Find some nodes and paths to reference + const sketchSnippet = `startProfile(at = [120.37, 162.76])` + const start = sourceCode.indexOf(sketchSnippet) + expect(start).toBeGreaterThanOrEqual(0) + const sketchRange = topLevelRange(start, start + sketchSnippet.length) + const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange) + const _node = getNodeFromPath( + ast, + sketchPathToNode || [], + 'VariableDeclaration' + ) + if (trap(_node)) return + const sketchInit = _node.node?.declaration.init + + // Hard code inputs that a user would have taken with their mouse + const x = 40 + const y = 60 + const rectangleOrigin = [120, 180] + const tags: [string, string, string] = [ + 'rectangleSegmentA001', + 'rectangleSegmentB001', + 'rectangleSegmentC001', + ] + + // Update the ast + if (sketchInit.type === 'PipeExpression') { + const maybeErr = updateCenterRectangleSketch( + sketchInit, + x, + y, + tags[0], + rectangleOrigin[0], + rectangleOrigin[1] + ) + expect(maybeErr).toEqual(undefined) + } + + // ast is edited in place from the updateCenterRectangleSketch + const expectedSourceCode = `sketch001 = startSketchOn(XZ) +profile001 = startProfile(at = [80, 120]) + |> angledLine(angle = 0, length = 80, tag = $rectangleSegmentA001) + |> angledLine(angle = segAng(rectangleSegmentA001) + 90deg, length = 120, tag = $rectangleSegmentB001) + |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +` + const recasted = recast(ast, instance) + expect(recasted).toEqual(expectedSourceCode) + }) + }) +}) From 2872f36e07ce7d0204ace54225478d9963f97396 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Oct 2025 14:09:41 -0500 Subject: [PATCH 03/13] fix: moving operations.test.ts out into operations.spec.ts --- src/integration.spec.ts | 252 ------------------------------- src/lang/KclSingleton.ts | 17 +++ src/lang/wasm.ts | 8 +- src/lang/wasmUtils.ts | 15 +- src/lib/operations.spec.ts | 270 ++++++++++++++++++++++++++++++++++ src/lib/rectangleTool.spec.ts | 1 - 6 files changed, 300 insertions(+), 263 deletions(-) create mode 100644 src/lib/operations.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index e0e0f6a7837..5f16fd65d44 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -11,8 +11,6 @@ import { type PipeExpression, type SourceRange, type VariableDeclarator, - defaultNodePath, - nodePathFromRange, type ParseResult, } from '@src/lang/wasm' import type { Selection, Selections } from '@src/machines/modelingSharedTypes' @@ -85,10 +83,6 @@ import { codeRefFromRange } from '@src/lang/std/artifactGraph' import { topLevelRange } from '@src/lang/util' import { isOverlap } from '@src/lib/utils' import { addSubtract } from '@src/lang/modifyAst/boolean' -import type { NodePath } from '@rust/kcl-lib/bindings/NodePath' -import type { Operation } from '@rust/kcl-lib/bindings/Operation' -import { defaultSourceRange } from '@src/lang/sourceRange' -import { filterOperations, getOperationVariableName } from '@src/lib/operations' import { getCalculatedKclExpressionValue, getStringValue, @@ -4462,252 +4456,6 @@ extrude003 = extrude(profile003, length = -1)` }) }) -describe('operations.test.ts', () => { - function stdlib(name: string): Operation { - return { - type: 'StdLibCall', - name, - unlabeledArg: null, - labeledArgs: {}, - nodePath: defaultNodePath(), - sourceRange: defaultSourceRange(), - isError: false, - } - } - - function userCall(name: string): Operation { - return { - type: 'GroupBegin', - group: { - type: 'FunctionCall', - name, - functionSourceRange: defaultSourceRange(), - unlabeledArg: null, - labeledArgs: {}, - }, - nodePath: defaultNodePath(), - sourceRange: defaultSourceRange(), - } - } - - function userReturn(): Operation { - return { - type: 'GroupEnd', - } - } - - function moduleBegin(name: string): Operation { - return { - type: 'GroupBegin', - group: { - type: 'ModuleInstance', - name, - moduleId: 0, - }, - nodePath: defaultNodePath(), - sourceRange: defaultSourceRange(), - } - } - - function moduleEnd(): Operation { - return { - type: 'GroupEnd', - } - } - - describe('operations filtering', () => { - it('drops stdlib operations inside a user-defined function call', async () => { - const operations = [ - stdlib('std1'), - userCall('foo'), - stdlib('std2'), - stdlib('std3'), - userReturn(), - stdlib('std4'), - stdlib('std5'), - ] - const actual = filterOperations(operations) - expect(actual).toEqual([ - stdlib('std1'), - userCall('foo'), - stdlib('std4'), - stdlib('std5'), - ]) - }) - it('drops user-defined function calls that contain no stdlib operations', async () => { - const operations = [ - stdlib('std1'), - userCall('foo'), - userReturn(), - stdlib('std2'), - userCall('bar'), - userReturn(), - stdlib('std3'), - ] - const actual = filterOperations(operations) - expect(actual).toEqual([stdlib('std1'), stdlib('std2'), stdlib('std3')]) - }) - it('does not drop module instances that contain no operations', async () => { - const operations = [ - stdlib('std1'), - moduleBegin('foo'), - moduleEnd(), - stdlib('std2'), - moduleBegin('bar'), - moduleEnd(), - stdlib('std3'), - ] - const actual = filterOperations(operations) - expect(actual).toEqual([ - stdlib('std1'), - moduleBegin('foo'), - stdlib('std2'), - moduleBegin('bar'), - stdlib('std3'), - ]) - }) - it('preserves user-defined function calls at the end of the list', async () => { - const operations = [stdlib('std1'), userCall('foo')] - const actual = filterOperations(operations) - expect(actual).toEqual([stdlib('std1'), userCall('foo')]) - }) - it('drops all user-defined function return operations', async () => { - // The returns allow us to group operations with the call, but we never - // display the returns. - const operations = [ - stdlib('std1'), - userCall('foo'), - stdlib('std2'), - userReturn(), - stdlib('std3'), - stdlib('std4'), - userCall('foo2'), - stdlib('std5'), - stdlib('std6'), - userReturn(), - stdlib('std7'), - ] - const actual = filterOperations(operations) - expect(actual).toEqual([ - stdlib('std1'), - userCall('foo'), - stdlib('std3'), - stdlib('std4'), - userCall('foo2'), - stdlib('std7'), - ]) - }) - it('correctly filters with nested function calls', async () => { - const operations = [ - stdlib('std1'), - userCall('foo'), - stdlib('std2'), - userReturn(), - stdlib('std3'), - stdlib('std4'), - userCall('foo2'), - stdlib('std5'), - userCall('foo3-nested'), - stdlib('std6'), - userReturn(), - stdlib('std7'), - userReturn(), - stdlib('std8'), - ] - const actual = filterOperations(operations) - expect(actual).toEqual([ - stdlib('std1'), - userCall('foo'), - stdlib('std3'), - stdlib('std4'), - userCall('foo2'), - stdlib('std8'), - ]) - }) - }) - - function rangeOfText(fullCode: string, target: string): SourceRange { - const start = fullCode.indexOf(target) - if (start === -1) { - throw new Error(`Could not find \`${target}\` in: ${fullCode}`) - } - return topLevelRange(start, start + target.length) - } - - async function buildNodePath( - code: string, - target: string - ): Promise { - const sourceRange = rangeOfText(code, target) - const program = assertParse(code) - return (await nodePathFromRange(program, sourceRange)) ?? defaultNodePath() - } - - describe('variable name of operations', () => { - it('finds the variable name with simple assignment', async () => { - const op = stdlib('stdLibFn') - if (op.type !== 'StdLibCall') { - throw new Error('Expected operation to be a StdLibCall') - } - const code = `myVar = stdLibFn()` - // Make the path match the code. - op.nodePath = await buildNodePath(code, 'stdLibFn()') - - const program = assertParse(code) - const variableName = getOperationVariableName(op, program) - expect(variableName).toBe('myVar') - }) - it('finds the variable name inside a function with simple assignment', async () => { - const op = stdlib('stdLibFn') - if (op.type !== 'StdLibCall') { - throw new Error('Expected operation to be a StdLibCall') - } - const code = `fn myFunc() { - myVar = stdLibFn() - return 0 -} -` - // Make the path match the code. - op.nodePath = await buildNodePath(code, 'stdLibFn()') - - const program = assertParse(code) - const variableName = getOperationVariableName(op, program) - expect(variableName).toBe('myVar') - }) - it("finds the variable name when it's the last in a pipeline", async () => { - const op = stdlib('stdLibFn') - if (op.type !== 'StdLibCall') { - throw new Error('Expected operation to be a StdLibCall') - } - const code = `myVar = foo() - |> stdLibFn() -` - // Make the path match the code. - op.nodePath = await buildNodePath(code, 'stdLibFn()') - - const program = assertParse(code) - const variableName = getOperationVariableName(op, program) - expect(variableName).toBe('myVar') - }) - it("finds nothing when it's not the last in a pipeline", async () => { - const op = stdlib('stdLibFn') - if (op.type !== 'StdLibCall') { - throw new Error('Expected operation to be a StdLibCall') - } - const code = `myVar = foo() - |> stdLibFn() - |> bar() -` - // Make the path match the code. - op.nodePath = await buildNodePath(code, 'stdLibFn()') - - const program = assertParse(code) - const variableName = getOperationVariableName(op, program) - expect(variableName).toBeUndefined() - }) - }) -}) - describe('kclHelpers.test.ts', () => { describe('KCL expression calculations', () => { it('calculates a simple expression without units', async () => { diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 6aabad89753..ff94bc4bda3 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -52,6 +52,7 @@ import type { Selections, } from '@src/machines/modelingSharedTypes' import { type handleSelectionBatch as handleSelectionBatchFn } from '@src/lib/selections' +import { processEnv } from '@src/env' interface ExecuteArgs { ast?: Node @@ -281,6 +282,15 @@ export class KclManager extends EventTarget { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.ensureWasmInit().then(async () => { + if (this.wasmInitFailed) { + if (processEnv()?.VITEST) { + console.log( + 'Running in vitest runtime. KclSingleton polluting global runtime.' + ) + return + } + } + await this.safeParse(this.singletons.codeManager.code).then((ast) => { if (ast) { this.ast = ast @@ -422,6 +432,13 @@ export class KclManager extends EventTarget { } async ensureWasmInit() { + if (processEnv()?.VITEST) { + const message = + 'kclSingle is trying to call ensureWasmInit. This will be blocked in VITEST runtimes.' + console.log(message) + return Promise.resolve(message) + } + try { await initPromise if (this.wasmInitFailed) { diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 6ee8ee70a62..0afdbd5c5a3 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -418,10 +418,14 @@ export async function rustImplPathToNode( export async function nodePathFromRange( ast: Program, - range: SourceRange + range: SourceRange, + instance?: ModuleType ): Promise { try { - const nodePath: NodePath | null = await node_path_from_range( + const node_path_from_range_fn = instance + ? instance.node_path_from_range + : node_path_from_range + const nodePath: NodePath | null = await node_path_from_range_fn( JSON.stringify(ast), JSON.stringify(range) ) diff --git a/src/lang/wasmUtils.ts b/src/lang/wasmUtils.ts index c553f3eb555..9485c7e3dc7 100644 --- a/src/lang/wasmUtils.ts +++ b/src/lang/wasmUtils.ts @@ -24,12 +24,15 @@ export const wasmUrl = () => { wasmFile return fullUrl } - -export const GLOBAL_MESSAGE_FOR_VITEST = - 'process.env.VITEST is running, a test required this. Error is being caught and ignored.' - // Initialise the wasm module. const initialise = async () => { + if (processEnv()?.VITEST) { + const message = + 'wasmUtils is trying to call initialise. This will be blocked in VITEST runtimes.' + console.log(message) + return Promise.resolve(message) + } + try { await reloadModule() const fullUrl = wasmUrl() @@ -37,10 +40,6 @@ const initialise = async () => { const buffer = await input.arrayBuffer() return await init({ module_or_path: buffer }) } catch (e) { - if (processEnv()?.VITEST) { - console.log(GLOBAL_MESSAGE_FOR_VITEST) - return Promise.resolve(GLOBAL_MESSAGE_FOR_VITEST) - } console.log('Error initialising WASM', e) return Promise.reject(e) } diff --git a/src/lib/operations.spec.ts b/src/lib/operations.spec.ts new file mode 100644 index 00000000000..3610f1f408a --- /dev/null +++ b/src/lib/operations.spec.ts @@ -0,0 +1,270 @@ +import type { NodePath } from '@rust/kcl-lib/bindings/NodePath' +import type { Operation } from '@rust/kcl-lib/bindings/Operation' +import { defaultSourceRange } from '@src/lang/sourceRange' +import { topLevelRange } from '@src/lang/util' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { join } from 'path' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +import { + type SourceRange, + assertParse, + defaultNodePath, + nodePathFromRange, +} from '@src/lang/wasm' +import { filterOperations, getOperationVariableName } from '@src/lib/operations' + +function stdlib(name: string): Operation { + return { + type: 'StdLibCall', + name, + unlabeledArg: null, + labeledArgs: {}, + nodePath: defaultNodePath(), + sourceRange: defaultSourceRange(), + isError: false, + } +} + +function userCall(name: string): Operation { + return { + type: 'GroupBegin', + group: { + type: 'FunctionCall', + name, + functionSourceRange: defaultSourceRange(), + unlabeledArg: null, + labeledArgs: {}, + }, + nodePath: defaultNodePath(), + sourceRange: defaultSourceRange(), + } +} + +function userReturn(): Operation { + return { + type: 'GroupEnd', + } +} + +function moduleBegin(name: string): Operation { + return { + type: 'GroupBegin', + group: { + type: 'ModuleInstance', + name, + moduleId: 0, + }, + nodePath: defaultNodePath(), + sourceRange: defaultSourceRange(), + } +} + +function moduleEnd(): Operation { + return { + type: 'GroupEnd', + } +} + +describe('operations.test.ts', () => { + describe('operations filtering', () => { + it('drops stdlib operations inside a user-defined function call', async () => { + const operations = [ + stdlib('std1'), + userCall('foo'), + stdlib('std2'), + stdlib('std3'), + userReturn(), + stdlib('std4'), + stdlib('std5'), + ] + const actual = filterOperations(operations) + expect(actual).toEqual([ + stdlib('std1'), + userCall('foo'), + stdlib('std4'), + stdlib('std5'), + ]) + }) + it('drops user-defined function calls that contain no stdlib operations', async () => { + const operations = [ + stdlib('std1'), + userCall('foo'), + userReturn(), + stdlib('std2'), + userCall('bar'), + userReturn(), + stdlib('std3'), + ] + const actual = filterOperations(operations) + expect(actual).toEqual([stdlib('std1'), stdlib('std2'), stdlib('std3')]) + }) + it('does not drop module instances that contain no operations', async () => { + const operations = [ + stdlib('std1'), + moduleBegin('foo'), + moduleEnd(), + stdlib('std2'), + moduleBegin('bar'), + moduleEnd(), + stdlib('std3'), + ] + const actual = filterOperations(operations) + expect(actual).toEqual([ + stdlib('std1'), + moduleBegin('foo'), + stdlib('std2'), + moduleBegin('bar'), + stdlib('std3'), + ]) + }) + it('preserves user-defined function calls at the end of the list', async () => { + const operations = [stdlib('std1'), userCall('foo')] + const actual = filterOperations(operations) + expect(actual).toEqual([stdlib('std1'), userCall('foo')]) + }) + it('drops all user-defined function return operations', async () => { + // The returns allow us to group operations with the call, but we never + // display the returns. + const operations = [ + stdlib('std1'), + userCall('foo'), + stdlib('std2'), + userReturn(), + stdlib('std3'), + stdlib('std4'), + userCall('foo2'), + stdlib('std5'), + stdlib('std6'), + userReturn(), + stdlib('std7'), + ] + const actual = filterOperations(operations) + expect(actual).toEqual([ + stdlib('std1'), + userCall('foo'), + stdlib('std3'), + stdlib('std4'), + userCall('foo2'), + stdlib('std7'), + ]) + }) + it('correctly filters with nested function calls', async () => { + const operations = [ + stdlib('std1'), + userCall('foo'), + stdlib('std2'), + userReturn(), + stdlib('std3'), + stdlib('std4'), + userCall('foo2'), + stdlib('std5'), + userCall('foo3-nested'), + stdlib('std6'), + userReturn(), + stdlib('std7'), + userReturn(), + stdlib('std8'), + ] + const actual = filterOperations(operations) + expect(actual).toEqual([ + stdlib('std1'), + userCall('foo'), + stdlib('std3'), + stdlib('std4'), + userCall('foo2'), + stdlib('std8'), + ]) + }) + }) + + function rangeOfText(fullCode: string, target: string): SourceRange { + const start = fullCode.indexOf(target) + if (start === -1) { + throw new Error(`Could not find \`${target}\` in: ${fullCode}`) + } + return topLevelRange(start, start + target.length) + } + + async function buildNodePath( + code: string, + target: string, + instance: ModuleType + ): Promise { + const sourceRange = rangeOfText(code, target) + const program = assertParse(code, instance) + return ( + (await nodePathFromRange(program, sourceRange, instance)) ?? + defaultNodePath() + ) + } + + describe('variable name of operations', () => { + it('finds the variable name with simple assignment', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const op = stdlib('stdLibFn') + if (op.type !== 'StdLibCall') { + throw new Error('Expected operation to be a StdLibCall') + } + const code = `myVar = stdLibFn()` + // Make the path match the code. + op.nodePath = await buildNodePath(code, 'stdLibFn()', instance) + + const program = assertParse(code, instance) + const variableName = getOperationVariableName(op, program) + expect(variableName).toBe('myVar') + }) + it('finds the variable name inside a function with simple assignment', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const op = stdlib('stdLibFn') + if (op.type !== 'StdLibCall') { + throw new Error('Expected operation to be a StdLibCall') + } + const code = `fn myFunc() { + myVar = stdLibFn() + return 0 +} +` + // Make the path match the code. + op.nodePath = await buildNodePath(code, 'stdLibFn()', instance) + + const program = assertParse(code, instance) + const variableName = getOperationVariableName(op, program) + expect(variableName).toBe('myVar') + }) + it("finds the variable name when it's the last in a pipeline", async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const op = stdlib('stdLibFn') + if (op.type !== 'StdLibCall') { + throw new Error('Expected operation to be a StdLibCall') + } + const code = `myVar = foo() + |> stdLibFn() +` + // Make the path match the code. + op.nodePath = await buildNodePath(code, 'stdLibFn()', instance) + + const program = assertParse(code, instance) + const variableName = getOperationVariableName(op, program) + expect(variableName).toBe('myVar') + }) + it("finds nothing when it's not the last in a pipeline", async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const op = stdlib('stdLibFn') + if (op.type !== 'StdLibCall') { + throw new Error('Expected operation to be a StdLibCall') + } + const code = `myVar = foo() + |> stdLibFn() + |> bar() +` + // Make the path match the code. + op.nodePath = await buildNodePath(code, 'stdLibFn()', instance) + + const program = assertParse(code, instance) + const variableName = getOperationVariableName(op, program) + expect(variableName).toBeUndefined() + }) + }) +}) diff --git a/src/lib/rectangleTool.spec.ts b/src/lib/rectangleTool.spec.ts index 115d3177140..998a5a75a44 100644 --- a/src/lib/rectangleTool.spec.ts +++ b/src/lib/rectangleTool.spec.ts @@ -8,7 +8,6 @@ import { updateCenterRectangleSketch } from '@src/lib/rectangleTool' import { trap } from '@src/lib/trap' import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' import { join } from 'path' - const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') describe('library rectangleTool helper functions', () => { From dfbb48ad6c16fcce15676bf97a0e985afd6ae796 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Oct 2025 15:20:34 -0500 Subject: [PATCH 04/13] fix: saving off progress. I think I found an actual bug in a unit test --- src/lang/langHelpers.ts | 1 + src/lib/kclHelpers.spec.ts | 184 +++++++++++++++++++++++++++++++++++++ src/lib/kclHelpers.ts | 10 +- src/lib/rustContext.ts | 17 +++- 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 src/lib/kclHelpers.spec.ts diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index 7d00133f5ed..b662d383794 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -107,6 +107,7 @@ export async function executeAstMock({ path, usePrevMemory ) + console.log('huh', execState.variables) await rustContext.waitForAllEngineCommands() return { diff --git a/src/lib/kclHelpers.spec.ts b/src/lib/kclHelpers.spec.ts new file mode 100644 index 00000000000..fb1e4975ec3 --- /dev/null +++ b/src/lib/kclHelpers.spec.ts @@ -0,0 +1,184 @@ +import type { ParseResult, SourceRange } from '@src/lang/wasm' +import { + getCalculatedKclExpressionValue, + getStringValue, +} from '@src/lib/kclHelpers' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { join } from 'path' +import { ConnectionManager } from '@src/network/connectionManager' +import RustContext from '@src/lib/rustContext' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('KCL expression calculations', () => { + it('calculates a simple expression without units', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue('1 + 2', undefined, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual).not.toHaveProperty('errors') + expect(coercedActual.valueAsString).toEqual('3') + expect(coercedActual?.astNode).toBeDefined() + }) + it.only('calculates a simple expression with units', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue('1deg + 30deg', undefined, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual).not.toHaveProperty('errors') + expect(coercedActual.valueAsString).toEqual('31deg') + expect(coercedActual?.astNode).toBeDefined() + }) + it('returns NAN for an invalid expression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue('1 + x', undefined, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual.valueAsString).toEqual('NAN') + expect(coercedActual.astNode).toBeDefined() + }) + + it('returns NAN for arrays when allowArrays is false (default)', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', undefined, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual.valueAsString).toEqual('NAN') + expect(coercedActual.astNode).toBeDefined() + }) + + it('returns NAN for arrays when allowArrays is explicitly false', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', false, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual.valueAsString).toEqual('NAN') + expect(coercedActual.astNode).toBeDefined() + }) + + it('formats simple number arrays when allowArrays is true', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', true, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual).not.toHaveProperty('errors') + expect(coercedActual.valueAsString).toEqual('[1, 2, 3]') + expect(coercedActual.astNode).toBeDefined() + }) + + it('formats arrays with units when allowArrays is true', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue( + '[1mm, 2mm, 3mm]', + true, + instance, + rustContext + ) + const coercedActual = actual as Exclude + expect(coercedActual).not.toHaveProperty('errors') + expect(coercedActual.valueAsString).toEqual('[1mm, 2mm, 3mm]') + expect(coercedActual.astNode).toBeDefined() + }) + + it('formats mixed arrays when allowArrays is true', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const actual = await getCalculatedKclExpressionValue('[0, 1, 0]', true, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual).not.toHaveProperty('errors') + expect(coercedActual.valueAsString).toEqual('[0, 1, 0]') + expect(coercedActual.astNode).toBeDefined() + }) + + it('rejects arrays with non-numeric types when allowArrays is true', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + // Arrays with non-numeric values should be rejected even when allowArrays is true + const actual = await getCalculatedKclExpressionValue('[1, true, 0]', true, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual.valueAsString).toEqual('NAN') + expect(coercedActual.astNode).toBeDefined() + }) + + it('formats arrays with mixed numeric values (integers and floats) when allowArrays is true', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + // Arrays with different numeric types should work fine + const actual = await getCalculatedKclExpressionValue('[1, 2.5, 0]', true, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual).not.toHaveProperty('errors') + expect(coercedActual.valueAsString).toEqual('[1, 2.5, 0]') + expect(coercedActual.astNode).toBeDefined() + }) + + it('handles arrays with undefined variables when allowArrays is true', async () => { + + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + // Test what happens with arrays containing undefined variables like [0, x, 0] + const actual = await getCalculatedKclExpressionValue('[0, x, 0]', true, instance, rustContext) + const coercedActual = actual as Exclude + // This returns 'NAN' because 'x' is undefined - the entire array expression fails + expect(coercedActual.valueAsString).toEqual('NAN') + expect(coercedActual.astNode).toBeDefined() + }) + + it('handles arrays with arithmetic expressions when allowArrays is true', async () => { + + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + // Test arrays containing expressions like [0, 2 + 3, 0] that evaluate to numbers + const actual = await getCalculatedKclExpressionValue('[0, 2 + 3, 0]', true, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual).not.toHaveProperty('errors') + expect(coercedActual.valueAsString).toEqual('[0, 5, 0]') + expect(coercedActual.astNode).toBeDefined() + }) + + it('rejects empty arrays when allowArrays is true', async () => { + + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + // Empty arrays aren't useful for geometric operations and should be rejected + const actual = await getCalculatedKclExpressionValue('[]', true, instance, rustContext) + const coercedActual = actual as Exclude + expect(coercedActual.valueAsString).toEqual('NAN') + expect(coercedActual.astNode).toBeDefined() + }) + + it('rejects arrays when allowArrays parameter is omitted', async () => { + const actual = await getCalculatedKclExpressionValue('[1, 2, 3]') + const coercedActual = actual as Exclude + expect(coercedActual.valueAsString).toEqual('NAN') + expect(coercedActual.astNode).toBeDefined() + }) +}) + +describe('getStringValue', () => { + it('returns a string value from a range', () => { + const code = `appearance(abc, color = "#00FF00")` + const range: SourceRange = [25, 25 + 7, 0] // '#00FF00' range + const result = getStringValue(code, range) + expect(result).toBe('#00FF00') + }) + + it('an empty string on bad range', () => { + const code = `badboi` + const range: SourceRange = [10, 12, 0] + const result = getStringValue(code, range) + expect(result).toBe('') + }) +}) diff --git a/src/lib/kclHelpers.ts b/src/lib/kclHelpers.ts index ae7fe60f863..ab89b4ce102 100644 --- a/src/lib/kclHelpers.ts +++ b/src/lib/kclHelpers.ts @@ -9,6 +9,8 @@ import { import type { KclExpression } from '@src/lib/commandTypes' import { rustContext } from '@src/lib/singletons' import { err } from '@src/lib/trap' +import { ModuleType } from '@src/lib/wasm_lib_wrapper' +import RustContext from '@src/lib/rustContext' const DUMMY_VARIABLE_NAME = '__result__' @@ -27,18 +29,20 @@ function isNumberValueItem(item: KclValue): item is KclNumber { */ export async function getCalculatedKclExpressionValue( value: string, - allowArrays?: boolean + allowArrays?: boolean, + instance?: ModuleType, + providedRustContext?: RustContext ) { // Create a one-line program that assigns the value to a variable const dummyProgramCode = `${DUMMY_VARIABLE_NAME} = ${value}` - const pResult = parse(dummyProgramCode) + const pResult = parse(dummyProgramCode, instance) if (err(pResult) || !resultIsOk(pResult)) return pResult const ast = pResult.program // Execute the program without hitting the engine const { execState } = await executeAstMock({ ast, - rustContext: rustContext, + rustContext: providedRustContext ? providedRustContext : rustContext, }) // Find the variable declaration for the result diff --git a/src/lib/rustContext.ts b/src/lib/rustContext.ts index b6296302d19..79f92d922ff 100644 --- a/src/lib/rustContext.ts +++ b/src/lib/rustContext.ts @@ -48,7 +48,7 @@ export default class RustContext { } } - constructor(engineCommandManager: ConnectionManager) { + constructor(engineCommandManager: ConnectionManager, instance?: ModuleType) { this.engineCommandManager = engineCommandManager this.ensureWasmInit() @@ -56,6 +56,10 @@ export default class RustContext { this.ctxInstance = this.create() }) .catch(reportRejection) + + if (instance) { + this.createFromInstance(instance) + } } /** Create a new context instance */ @@ -70,6 +74,17 @@ export default class RustContext { return ctxInstance } + createFromInstance(instance: ModuleType) { + this.rustInstance = instance + + const ctxInstance = new this.rustInstance.Context( + this.engineCommandManager, + projectFsManager + ) + + this.ctxInstance = ctxInstance + } + async sendOpenProject( project: Project, currentFilePath: string | null From db8389eb14cd05c78f7606eeeaca325b43078748 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Oct 2025 15:20:46 -0500 Subject: [PATCH 05/13] fix: fmt --- src/lib/kclHelpers.spec.ts | 87 +++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/src/lib/kclHelpers.spec.ts b/src/lib/kclHelpers.spec.ts index fb1e4975ec3..620c5854381 100644 --- a/src/lib/kclHelpers.spec.ts +++ b/src/lib/kclHelpers.spec.ts @@ -14,7 +14,12 @@ describe('KCL expression calculations', () => { const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) - const actual = await getCalculatedKclExpressionValue('1 + 2', undefined, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '1 + 2', + undefined, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual).not.toHaveProperty('errors') expect(coercedActual.valueAsString).toEqual('3') @@ -24,7 +29,12 @@ describe('KCL expression calculations', () => { const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) - const actual = await getCalculatedKclExpressionValue('1deg + 30deg', undefined, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '1deg + 30deg', + undefined, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual).not.toHaveProperty('errors') expect(coercedActual.valueAsString).toEqual('31deg') @@ -34,7 +44,12 @@ describe('KCL expression calculations', () => { const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) - const actual = await getCalculatedKclExpressionValue('1 + x', undefined, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '1 + x', + undefined, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual.valueAsString).toEqual('NAN') expect(coercedActual.astNode).toBeDefined() @@ -44,7 +59,12 @@ describe('KCL expression calculations', () => { const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) - const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', undefined, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[1, 2, 3]', + undefined, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual.valueAsString).toEqual('NAN') expect(coercedActual.astNode).toBeDefined() @@ -54,7 +74,12 @@ describe('KCL expression calculations', () => { const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) - const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', false, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[1, 2, 3]', + false, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual.valueAsString).toEqual('NAN') expect(coercedActual.astNode).toBeDefined() @@ -64,7 +89,12 @@ describe('KCL expression calculations', () => { const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) - const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', true, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[1, 2, 3]', + true, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual).not.toHaveProperty('errors') expect(coercedActual.valueAsString).toEqual('[1, 2, 3]') @@ -91,7 +121,12 @@ describe('KCL expression calculations', () => { const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) - const actual = await getCalculatedKclExpressionValue('[0, 1, 0]', true, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[0, 1, 0]', + true, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual).not.toHaveProperty('errors') expect(coercedActual.valueAsString).toEqual('[0, 1, 0]') @@ -103,7 +138,12 @@ describe('KCL expression calculations', () => { const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) // Arrays with non-numeric values should be rejected even when allowArrays is true - const actual = await getCalculatedKclExpressionValue('[1, true, 0]', true, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[1, true, 0]', + true, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual.valueAsString).toEqual('NAN') expect(coercedActual.astNode).toBeDefined() @@ -114,7 +154,12 @@ describe('KCL expression calculations', () => { const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) // Arrays with different numeric types should work fine - const actual = await getCalculatedKclExpressionValue('[1, 2.5, 0]', true, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[1, 2.5, 0]', + true, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual).not.toHaveProperty('errors') expect(coercedActual.valueAsString).toEqual('[1, 2.5, 0]') @@ -122,12 +167,16 @@ describe('KCL expression calculations', () => { }) it('handles arrays with undefined variables when allowArrays is true', async () => { - const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) // Test what happens with arrays containing undefined variables like [0, x, 0] - const actual = await getCalculatedKclExpressionValue('[0, x, 0]', true, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[0, x, 0]', + true, + instance, + rustContext + ) const coercedActual = actual as Exclude // This returns 'NAN' because 'x' is undefined - the entire array expression fails expect(coercedActual.valueAsString).toEqual('NAN') @@ -135,12 +184,16 @@ describe('KCL expression calculations', () => { }) it('handles arrays with arithmetic expressions when allowArrays is true', async () => { - const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) // Test arrays containing expressions like [0, 2 + 3, 0] that evaluate to numbers - const actual = await getCalculatedKclExpressionValue('[0, 2 + 3, 0]', true, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[0, 2 + 3, 0]', + true, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual).not.toHaveProperty('errors') expect(coercedActual.valueAsString).toEqual('[0, 5, 0]') @@ -148,12 +201,16 @@ describe('KCL expression calculations', () => { }) it('rejects empty arrays when allowArrays is true', async () => { - const instance = await loadAndInitialiseWasmInstance(WASM_PATH) const engineCommandManager = new ConnectionManager() const rustContext = new RustContext(engineCommandManager, instance) // Empty arrays aren't useful for geometric operations and should be rejected - const actual = await getCalculatedKclExpressionValue('[]', true, instance, rustContext) + const actual = await getCalculatedKclExpressionValue( + '[]', + true, + instance, + rustContext + ) const coercedActual = actual as Exclude expect(coercedActual.valueAsString).toEqual('NAN') expect(coercedActual.astNode).toBeDefined() From e3404eb8c8959d9513f1f86f8472926ee18c6d90 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Oct 2025 15:37:23 -0500 Subject: [PATCH 06/13] fix: kclHelpers takin from integration! --- src/integration.spec.ts | 229 ---------------------------------------- src/lang/langHelpers.ts | 1 - src/lang/wasm.ts | 8 +- src/lib/kclHelpers.ts | 8 +- 4 files changed, 10 insertions(+), 236 deletions(-) diff --git a/src/integration.spec.ts b/src/integration.spec.ts index 5f16fd65d44..f89e969d6e6 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -11,7 +11,6 @@ import { type PipeExpression, type SourceRange, type VariableDeclarator, - type ParseResult, } from '@src/lang/wasm' import type { Selection, Selections } from '@src/machines/modelingSharedTypes' import { enginelessExecutor } from '@src/lib/testHelpers' @@ -83,10 +82,6 @@ import { codeRefFromRange } from '@src/lang/std/artifactGraph' import { topLevelRange } from '@src/lang/util' import { isOverlap } from '@src/lib/utils' import { addSubtract } from '@src/lang/modifyAst/boolean' -import { - getCalculatedKclExpressionValue, - getStringValue, -} from '@src/lib/kclHelpers' import { getSafeInsertIndex } from '@src/lang/queryAst/getSafeInsertIndex' import type { Coords2d } from '@src/lang/util' import { isPointsCCW } from '@src/lang/wasm' @@ -4456,230 +4451,6 @@ extrude003 = extrude(profile003, length = -1)` }) }) -describe('kclHelpers.test.ts', () => { - describe('KCL expression calculations', () => { - it('calculates a simple expression without units', async () => { - const actual = await getCalculatedKclExpressionValue('1 + 2') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('3') - expect(coercedActual?.astNode).toBeDefined() - }) - it('calculates a simple expression with units', async () => { - const actual = await getCalculatedKclExpressionValue('1deg + 30deg') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('31deg') - expect(coercedActual?.astNode).toBeDefined() - }) - it('returns NAN for an invalid expression', async () => { - const actual = await getCalculatedKclExpressionValue('1 + x') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - }) - - describe('getStringValue', () => { - it('returns a string value from a range', () => { - const code = `appearance(abc, color = "#00FF00")` - const range: SourceRange = [25, 25 + 7, 0] // '#00FF00' range - const result = getStringValue(code, range) - expect(result).toBe('#00FF00') - }) - - it('an empty string on bad range', () => { - const code = `badboi` - const range: SourceRange = [10, 12, 0] - const result = getStringValue(code, range) - expect(result).toBe('') - }) - }) - describe('KCL expression calculations', () => { - it('calculates a simple expression without units', async () => { - const actual = await getCalculatedKclExpressionValue('1 + 2') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('3') - expect(coercedActual?.astNode).toBeDefined() - }) - it('calculates a simple expression with units', async () => { - const actual = await getCalculatedKclExpressionValue('1deg + 30deg') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('31deg') - expect(coercedActual?.astNode).toBeDefined() - }) - it('returns NAN for an invalid expression', async () => { - const actual = await getCalculatedKclExpressionValue('1 + x') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - - it('returns NAN for arrays when allowArrays is false (default)', async () => { - const actual = await getCalculatedKclExpressionValue('[1, 2, 3]') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - - it('returns NAN for arrays when allowArrays is explicitly false', async () => { - const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', false) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - - it('formats simple number arrays when allowArrays is true', async () => { - const actual = await getCalculatedKclExpressionValue('[1, 2, 3]', true) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('[1, 2, 3]') - expect(coercedActual.astNode).toBeDefined() - }) - - it('formats arrays with units when allowArrays is true', async () => { - const actual = await getCalculatedKclExpressionValue( - '[1mm, 2mm, 3mm]', - true - ) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('[1mm, 2mm, 3mm]') - expect(coercedActual.astNode).toBeDefined() - }) - - it('formats mixed arrays when allowArrays is true', async () => { - const actual = await getCalculatedKclExpressionValue('[0, 1, 0]', true) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('[0, 1, 0]') - expect(coercedActual.astNode).toBeDefined() - }) - - it('rejects arrays with non-numeric types when allowArrays is true', async () => { - // Arrays with non-numeric values should be rejected even when allowArrays is true - const actual = await getCalculatedKclExpressionValue('[1, true, 0]', true) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - - it('formats arrays with mixed numeric values (integers and floats) when allowArrays is true', async () => { - // Arrays with different numeric types should work fine - const actual = await getCalculatedKclExpressionValue('[1, 2.5, 0]', true) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('[1, 2.5, 0]') - expect(coercedActual.astNode).toBeDefined() - }) - - it('handles arrays with undefined variables when allowArrays is true', async () => { - // Test what happens with arrays containing undefined variables like [0, x, 0] - const actual = await getCalculatedKclExpressionValue('[0, x, 0]', true) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - // This returns 'NAN' because 'x' is undefined - the entire array expression fails - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - - it('handles arrays with arithmetic expressions when allowArrays is true', async () => { - // Test arrays containing expressions like [0, 2 + 3, 0] that evaluate to numbers - const actual = await getCalculatedKclExpressionValue( - '[0, 2 + 3, 0]', - true - ) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual).not.toHaveProperty('errors') - expect(coercedActual.valueAsString).toEqual('[0, 5, 0]') - expect(coercedActual.astNode).toBeDefined() - }) - - it('rejects empty arrays when allowArrays is true', async () => { - // Empty arrays aren't useful for geometric operations and should be rejected - const actual = await getCalculatedKclExpressionValue('[]', true) - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - - it('rejects arrays when allowArrays parameter is omitted', async () => { - const actual = await getCalculatedKclExpressionValue('[1, 2, 3]') - const coercedActual = actual as Exclude< - typeof actual, - Error | ParseResult - > - expect(coercedActual.valueAsString).toEqual('NAN') - expect(coercedActual.astNode).toBeDefined() - }) - }) - - describe('getStringValue', () => { - it('returns a string value from a range', () => { - const code = `appearance(abc, color = "#00FF00")` - const range: SourceRange = [25, 25 + 7, 0] // '#00FF00' range - const result = getStringValue(code, range) - expect(result).toBe('#00FF00') - }) - - it('an empty string on bad range', () => { - const code = `badboi` - const range: SourceRange = [10, 12, 0] - const result = getStringValue(code, range) - expect(result).toBe('') - }) - }) -}) - describe('getSafeInsertIndex.test.ts', () => { describe(`getSafeInsertIndex`, () => { it(`expression with no identifiers`, () => { diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index b662d383794..7d00133f5ed 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -107,7 +107,6 @@ export async function executeAstMock({ path, usePrevMemory ) - console.log('huh', execState.variables) await rustContext.waitForAllEngineCommands() return { diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 0afdbd5c5a3..f6ff6084382 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -465,10 +465,14 @@ export function formatNumberLiteral( */ export function formatNumberValue( value: number, - numericType: NumericType + numericType: NumericType, + instance?: ModuleType ): string | Error { try { - return format_number_value(value, JSON.stringify(numericType)) + const format_number_value_fn = instance + ? instance.format_number_value + : format_number_value + return format_number_value_fn(value, JSON.stringify(numericType)) } catch (e) { return new Error( `Error formatting number value: value=${value}, numericType=${numericType}`, diff --git a/src/lib/kclHelpers.ts b/src/lib/kclHelpers.ts index ab89b4ce102..8b54c71256d 100644 --- a/src/lib/kclHelpers.ts +++ b/src/lib/kclHelpers.ts @@ -9,8 +9,8 @@ import { import type { KclExpression } from '@src/lib/commandTypes' import { rustContext } from '@src/lib/singletons' import { err } from '@src/lib/trap' -import { ModuleType } from '@src/lib/wasm_lib_wrapper' -import RustContext from '@src/lib/rustContext' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' +import type RustContext from '@src/lib/rustContext' const DUMMY_VARIABLE_NAME = '__result__' @@ -84,7 +84,7 @@ export async function getCalculatedKclExpressionValue( const arrayValues = varValue.value.map((item: KclValue) => { if (isNumberValueItem(item)) { - const formatted = formatNumberValue(item.value, item.ty) + const formatted = formatNumberValue(item.value, item.ty, instance) if (!err(formatted)) { return formatted } @@ -106,7 +106,7 @@ export async function getCalculatedKclExpressionValue( if (!varValue || varValue.type !== 'Number') { return undefined } - const formatted = formatNumberValue(varValue.value, varValue.ty) + const formatted = formatNumberValue(varValue.value, varValue.ty, instance) if (err(formatted)) return undefined return formatted })() From 524105e11265daa57c8a0f266713390e5abcfc2e Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Oct 2025 16:13:02 -0500 Subject: [PATCH 07/13] chore: created utils2d.spec.ts, moved out of integration.spec.ts --- src/integration.spec.ts | 89 ---------------------------------------- src/lang/wasm.ts | 5 ++- src/lib/utils2d.spec.ts | 91 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 91 deletions(-) create mode 100644 src/lib/utils2d.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index f89e969d6e6..168ea52a4af 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -83,9 +83,6 @@ import { topLevelRange } from '@src/lang/util' import { isOverlap } from '@src/lib/utils' import { addSubtract } from '@src/lang/modifyAst/boolean' import { getSafeInsertIndex } from '@src/lang/queryAst/getSafeInsertIndex' -import type { Coords2d } from '@src/lang/util' -import { isPointsCCW } from '@src/lang/wasm' -import { closestPointOnRay } from '@src/lib/utils2d' import { expect } from 'vitest' import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' import type { Parameter } from '@src/lang/wasm' @@ -4511,92 +4508,6 @@ z = x + y`) }) }) -describe('utils2d.test.ts', () => { - describe('test isPointsCW', () => { - test('basic test', () => { - const points: Coords2d[] = [ - [2, 2], - [2, 0], - [0, -2], - ] - const pointsRev = [...points].reverse() - const CCW = isPointsCCW(pointsRev) - const CW = isPointsCCW(points) - expect(CCW).toBe(1) - expect(CW).toBe(-1) - }) - }) - - describe('test closestPointOnRay', () => { - test('point lies on ray', () => { - const rayOrigin: Coords2d = [0, 0] - const rayDirection: Coords2d = [1, 0] - const pointToCheck: Coords2d = [7, 0] - - const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) - expect(result.closestPoint).toEqual([7, 0]) - expect(result.t).toBe(7) - }) - - test('point is above ray', () => { - const rayOrigin: Coords2d = [1, 0] - const rayDirection: Coords2d = [1, 0] - const pointToCheck: Coords2d = [7, 7] - - const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) - expect(result.closestPoint).toEqual([7, 0]) - expect(result.t).toBe(6) - }) - - test('point lies behind ray origin and allowNegative=false', () => { - const rayOrigin: Coords2d = [0, 0] - const rayDirection: Coords2d = [1, 0] - const pointToCheck: Coords2d = [-7, 7] - - const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) - expect(result.closestPoint).toEqual([0, 0]) - expect(result.t).toBe(0) - }) - - test('point lies behind ray origin and allowNegative=true', () => { - const rayOrigin: Coords2d = [0, 0] - const rayDirection: Coords2d = [1, 0] - const pointToCheck: Coords2d = [-7, 7] - - const result = closestPointOnRay( - rayOrigin, - rayDirection, - pointToCheck, - true - ) - expect(result.closestPoint).toEqual([-7, 0]) - expect(result.t).toBe(-7) - }) - - test('diagonal ray and point', () => { - const rayOrigin: Coords2d = [0, 0] - const rayDirection: Coords2d = [1, 1] - const pointToCheck: Coords2d = [3, 4] - - const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) - expect(result.closestPoint[0]).toBeCloseTo(3.5) - expect(result.closestPoint[1]).toBeCloseTo(3.5) - expect(result.t).toBeCloseTo(4.95, 1) - }) - - test('non-normalized direction vector', () => { - const rayOrigin: Coords2d = [0, 0] - const rayDirection: Coords2d = [2, 2] - const pointToCheck: Coords2d = [3, 4] - - const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) - expect(result.closestPoint[0]).toBeCloseTo(3.5) - expect(result.closestPoint[1]).toBeCloseTo(3.5) - expect(result.t).toBeCloseTo(4.95, 1) - }) - }) -}) - describe('getNodePathFromSourceRange.test.ts', () => { describe('testing getNodePathFromSourceRange', () => { it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => { diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index f6ff6084382..5881ef65934 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -498,8 +498,9 @@ export function humanDisplayNumber( } } -export function isPointsCCW(points: Coords2d[]): number { - return is_points_ccw(new Float64Array(points.flat())) +export function isPointsCCW(points: Coords2d[], instance?: ModuleType): number { + const is_points_ccw_fn = instance ? instance.is_points_ccw : is_points_ccw + return is_points_ccw_fn(new Float64Array(points.flat())) } export function getTangentialArcToInfo({ diff --git a/src/lib/utils2d.spec.ts b/src/lib/utils2d.spec.ts new file mode 100644 index 00000000000..b95ee7b5b5d --- /dev/null +++ b/src/lib/utils2d.spec.ts @@ -0,0 +1,91 @@ +import type { Coords2d } from '@src/lang/util' +import { isPointsCCW } from '@src/lang/wasm' +import { closestPointOnRay } from '@src/lib/utils2d' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('test isPointsCW', () => { + test('basic test', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const points: Coords2d[] = [ + [2, 2], + [2, 0], + [0, -2], + ] + const pointsRev = [...points].reverse() + const CCW = isPointsCCW(pointsRev, instance) + const CW = isPointsCCW(points, instance) + expect(CCW).toBe(1) + expect(CW).toBe(-1) + }) +}) + +describe('test closestPointOnRay', () => { + test('point lies on ray', () => { + const rayOrigin: Coords2d = [0, 0] + const rayDirection: Coords2d = [1, 0] + const pointToCheck: Coords2d = [7, 0] + + const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) + expect(result.closestPoint).toEqual([7, 0]) + expect(result.t).toBe(7) + }) + + test('point is above ray', () => { + const rayOrigin: Coords2d = [1, 0] + const rayDirection: Coords2d = [1, 0] + const pointToCheck: Coords2d = [7, 7] + + const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) + expect(result.closestPoint).toEqual([7, 0]) + expect(result.t).toBe(6) + }) + + test('point lies behind ray origin and allowNegative=false', () => { + const rayOrigin: Coords2d = [0, 0] + const rayDirection: Coords2d = [1, 0] + const pointToCheck: Coords2d = [-7, 7] + + const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) + expect(result.closestPoint).toEqual([0, 0]) + expect(result.t).toBe(0) + }) + + test('point lies behind ray origin and allowNegative=true', () => { + const rayOrigin: Coords2d = [0, 0] + const rayDirection: Coords2d = [1, 0] + const pointToCheck: Coords2d = [-7, 7] + + const result = closestPointOnRay( + rayOrigin, + rayDirection, + pointToCheck, + true + ) + expect(result.closestPoint).toEqual([-7, 0]) + expect(result.t).toBe(-7) + }) + + test('diagonal ray and point', () => { + const rayOrigin: Coords2d = [0, 0] + const rayDirection: Coords2d = [1, 1] + const pointToCheck: Coords2d = [3, 4] + + const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) + expect(result.closestPoint[0]).toBeCloseTo(3.5) + expect(result.closestPoint[1]).toBeCloseTo(3.5) + expect(result.t).toBeCloseTo(4.95, 1) + }) + + test('non-normalized direction vector', () => { + const rayOrigin: Coords2d = [0, 0] + const rayDirection: Coords2d = [2, 2] + const pointToCheck: Coords2d = [3, 4] + + const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck) + expect(result.closestPoint[0]).toBeCloseTo(3.5) + expect(result.closestPoint[1]).toBeCloseTo(3.5) + expect(result.t).toBeCloseTo(4.95, 1) + }) +}) From 0c3b1c2d52be0af20a1535cb912a551d45809b97 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 6 Oct 2025 11:56:03 -0500 Subject: [PATCH 08/13] fix: moving another test: --- src/integration.spec.ts | 74 -------------------- src/lang/modifyAst/boolean.spec.ts | 108 +++++++++++++++++++++++++++++ src/lib/testHelpers.ts | 7 +- 3 files changed, 113 insertions(+), 76 deletions(-) create mode 100644 src/lang/modifyAst/boolean.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index 168ea52a4af..f6b3b327b0e 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -81,7 +81,6 @@ import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { codeRefFromRange } from '@src/lang/std/artifactGraph' import { topLevelRange } from '@src/lang/util' import { isOverlap } from '@src/lib/utils' -import { addSubtract } from '@src/lang/modifyAst/boolean' import { getSafeInsertIndex } from '@src/lang/queryAst/getSafeInsertIndex' import { expect } from 'vitest' import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' @@ -4375,79 +4374,6 @@ chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` }) }) -describe('boolean.test.ts', () => { - describe('Testing addSubtract', () => { - async function runAddSubtractTest(code: string, toolCount = 1) { - const ast = assertParse(code) - if (err(ast)) throw ast - - const { artifactGraph } = await enginelessExecutor(ast) - const sweeps = [...artifactGraph.values()].filter( - (n) => n.type === 'sweep' - ) - const solids: Selections = { - graphSelections: sweeps.slice(-1), - otherSelections: [], - } - const tools: Selections = { - graphSelections: sweeps.slice(0, toolCount), - otherSelections: [], - } - const result = addSubtract({ - ast, - artifactGraph, - solids, - tools, - }) - if (err(result)) throw result - - await enginelessExecutor(ast) - return recast(result.modifiedAst) - } - - it('should add a standalone call on standalone sweeps selection', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0.2, 0.2], radius = 0.1) -extrude001 = extrude(profile001, length = 1) - -sketch002 = startSketchOn(XZ) -profile002 = circle(sketch002, center = [0.2, 0.2], radius = 0.05) -extrude002 = extrude(profile002, length = -1)` - const expectedNewLine = `solid001 = subtract(extrude002, tools = extrude001)` - const newCode = await runAddSubtractTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - it('should push a call in pipe if selection was in variable-less pipe', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0.2, 0.2], radius = 0.1) -extrude001 = extrude(profile001, length = 1) - -startSketchOn(XZ) - |> circle(center = [0.2, 0.2], radius = 0.05) - |> extrude(length = -1)` - const expectedNewLine = ` |> subtract(tools = extrude001)` - const newCode = await runAddSubtractTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - it('should support multi-profile extrude as tool', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0.2, 0.2], radius = 0.05) -profile002 = circle(sketch001, center = [0.2, 0.4], radius = 0.05) -extrude001 = extrude([profile001, profile002], length = 1) - -sketch003 = startSketchOn(XZ) -profile003 = circle(sketch003, center = [0.2, 0.2], radius = 0.1) -extrude003 = extrude(profile003, length = -1)` - const expectedNewLine = `solid001 = subtract(extrude003, tools = extrude001)` - const toolCount = 2 - const newCode = await runAddSubtractTest(code, toolCount) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - }) -}) - describe('getSafeInsertIndex.test.ts', () => { describe(`getSafeInsertIndex`, () => { it(`expression with no identifiers`, () => { diff --git a/src/lang/modifyAst/boolean.spec.ts b/src/lang/modifyAst/boolean.spec.ts new file mode 100644 index 00000000000..ae3b60cdce4 --- /dev/null +++ b/src/lang/modifyAst/boolean.spec.ts @@ -0,0 +1,108 @@ +import { assertParse, recast } from '@src/lang/wasm' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { err } from '@src/lib/trap' +import type { Selections } from '@src/machines/modelingSharedTypes' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' +import { addSubtract } from '@src/lang/modifyAst/boolean' +import { enginelessExecutor } from '@src/lib/testHelpers' +import RustContext from '@src/lib/rustContext' +import { ConnectionManager } from '@src/network/connectionManager' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('boolean', () => { + describe('Testing addSubtract', () => { + async function runAddSubtractTest( + code: string, + toolCount = 1, + instance: ModuleType, + rustContext: RustContext + ) { + const ast = assertParse(code, instance) + if (err(ast)) throw ast + + const { artifactGraph } = await enginelessExecutor( + ast, + undefined, + undefined, + rustContext + ) + const sweeps = [...artifactGraph.values()].filter( + (n) => n.type === 'sweep' + ) + const solids: Selections = { + graphSelections: sweeps.slice(-1), + otherSelections: [], + } + const tools: Selections = { + graphSelections: sweeps.slice(0, toolCount), + otherSelections: [], + } + const result = addSubtract({ + ast, + artifactGraph, + solids, + tools, + }) + if (err(result)) throw result + + await enginelessExecutor(ast, undefined, undefined, rustContext) + return recast(result.modifiedAst, instance) + } + + it('should add a standalone call on standalone sweeps selection', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0.2, 0.2], radius = 0.1) +extrude001 = extrude(profile001, length = 1) + +sketch002 = startSketchOn(XZ) +profile002 = circle(sketch002, center = [0.2, 0.2], radius = 0.05) +extrude002 = extrude(profile002, length = -1)` + const expectedNewLine = `solid001 = subtract(extrude002, tools = extrude001)` + const newCode = await runAddSubtractTest(code, 1, instance, rustContext) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + + it('should push a call in pipe if selection was in variable-less pipe', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0.2, 0.2], radius = 0.1) +extrude001 = extrude(profile001, length = 1) + +startSketchOn(XZ) + |> circle(center = [0.2, 0.2], radius = 0.05) + |> extrude(length = -1)` + const expectedNewLine = ` |> subtract(tools = extrude001)` + const newCode = await runAddSubtractTest(code, 1, instance, rustContext) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + + it('should support multi-profile extrude as tool', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0.2, 0.2], radius = 0.05) +profile002 = circle(sketch001, center = [0.2, 0.4], radius = 0.05) +extrude001 = extrude([profile001, profile002], length = 1) + +sketch003 = startSketchOn(XZ) +profile003 = circle(sketch003, center = [0.2, 0.2], radius = 0.1) +extrude003 = extrude(profile003, length = -1)` + const expectedNewLine = `solid001 = subtract(extrude003, tools = extrude001)` + const toolCount = 2 + const newCode = await runAddSubtractTest( + code, + toolCount, + instance, + rustContext + ) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + }) +}) diff --git a/src/lib/testHelpers.ts b/src/lib/testHelpers.ts index dad0699b777..40202789258 100644 --- a/src/lib/testHelpers.ts +++ b/src/lib/testHelpers.ts @@ -3,12 +3,15 @@ import type { Node } from '@rust/kcl-lib/bindings/Node' import type { ExecState, Program } from '@src/lang/wasm' import { jsAppSettings } from '@src/lib/settings/settingsUtils' import { rustContext } from '@src/lib/singletons' +import type RustContext from '@src/lib/rustContext' export async function enginelessExecutor( ast: Node, usePrevMemory?: boolean, - path?: string + path?: string, + providedRustContext?: RustContext ): Promise { const settings = await jsAppSettings() - return await rustContext.executeMock(ast, settings, path, usePrevMemory) + const theRustContext = providedRustContext ? providedRustContext : rustContext + return await theRustContext.executeMock(ast, settings, path, usePrevMemory) } From 3303d2d0118c36769bb1b354341c2822b00164aa Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 6 Oct 2025 12:17:47 -0500 Subject: [PATCH 09/13] chore: removing another integration file --- src/integration.spec.ts | 66 --------------- .../getTagDeclaratorsInProgram.spec.ts | 83 +++++++++++++++++++ 2 files changed, 83 insertions(+), 66 deletions(-) create mode 100644 src/lang/queryAst/getTagDeclaratorsInProgram.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index f6b3b327b0e..76bef0014a7 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -91,7 +91,6 @@ import { vi } from 'vitest' import { getConstraintInfoKw } from '@src/lang/std/sketch' import { ARG_END_ABSOLUTE, ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants' import { removeSingleConstraintInfo } from '@src/lang/modifyAst' -import { getTagDeclaratorsInProgram } from '@src/lang/queryAst/getTagDeclaratorsInProgram' import { modelingMachineDefaultContext } from '@src/machines/modelingSharedContext' import { removeSingleConstraint, @@ -5856,68 +5855,3 @@ p3 = [342.51, 216.38], }) }) }) - -describe('getTagDeclaratorsInProgram.test.ts', () => { - function tagDeclaratorWithIndex( - value: string, - start: number, - end: number, - bodyIndex: number - ) { - return { - tag: { - type: 'TagDeclarator', - value, - start, - end, - moduleId: 0, - commentStart: start, - }, - bodyIndex, - } - } - describe(`getTagDeclaratorsInProgram`, () => { - it(`finds no tag declarators in an empty program`, () => { - const tagDeclarators = getTagDeclaratorsInProgram(assertParse('')) - expect(tagDeclarators).toEqual([]) - }) - it(`finds a single tag declarators in a small program`, () => { - const tagDeclarators = getTagDeclaratorsInProgram( - assertParse(`sketch001 = startSketchOn(XZ) -profile001 = startProfile(sketch001, at = [0, 0]) - |> angledLine(angle = 0, length = 11, tag = $a)`) - ) - expect(tagDeclarators).toEqual([tagDeclaratorWithIndex('a', 126, 128, 1)]) - }) - it(`finds multiple tag declarators in a small program`, () => { - const program = `sketch001 = startSketchOn(XZ) -profile001 = startProfile(sketch001, at = [0.07, 0]) - |> angledLine(angle = 0, length = 11, tag = $a) - |> angledLine(angle = segAng(a) + 90, length = 11.17, tag = $b) - |> angledLine(angle = segAng(a), length = -segLen(a), tag = $c) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close()` - const tagDeclarators = getTagDeclaratorsInProgram(assertParse(program)) - expect(tagDeclarators).toEqual([ - tagDeclaratorWithIndex('a', 129, 131, 1), - tagDeclaratorWithIndex('b', 195, 197, 1), - tagDeclaratorWithIndex('c', 261, 263, 1), - ]) - }) - it(`finds tag declarators at different indices`, () => { - const program = `sketch001 = startSketchOn(XZ) -profile001 = startProfile(sketch001, at = [0.07, 0]) - |> angledLine(angle = 0, length = 11, tag = $a) -profile002 = angledLine(profile001, angle = segAng(a) + 90, length = 11.17, tag = $b) - |> angledLine(angle = segAng(a), length = -segLen(a), tag = $c) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close()` - const tagDeclarators = getTagDeclaratorsInProgram(assertParse(program)) - expect(tagDeclarators).toEqual([ - tagDeclaratorWithIndex('a', 129, 131, 1), - tagDeclaratorWithIndex('b', 215, 217, 2), - tagDeclaratorWithIndex('c', 281, 283, 2), - ]) - }) - }) -}) diff --git a/src/lang/queryAst/getTagDeclaratorsInProgram.spec.ts b/src/lang/queryAst/getTagDeclaratorsInProgram.spec.ts new file mode 100644 index 00000000000..b4647b6ba5c --- /dev/null +++ b/src/lang/queryAst/getTagDeclaratorsInProgram.spec.ts @@ -0,0 +1,83 @@ +import { getTagDeclaratorsInProgram } from '@src/lang/queryAst/getTagDeclaratorsInProgram' +import { assertParse } from '@src/lang/wasm' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { join } from 'path' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('getTagDeclaratorsInProgram', () => { + function tagDeclaratorWithIndex( + value: string, + start: number, + end: number, + bodyIndex: number + ) { + return { + tag: { + type: 'TagDeclarator', + value, + start, + end, + moduleId: 0, + commentStart: start, + }, + bodyIndex, + } + } + describe(`getTagDeclaratorsInProgram`, () => { + it(`finds no tag declarators in an empty program`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const tagDeclarators = getTagDeclaratorsInProgram( + assertParse('', instance) + ) + expect(tagDeclarators).toEqual([]) + }) + it(`finds a single tag declarators in a small program`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const tagDeclarators = getTagDeclaratorsInProgram( + assertParse( + `sketch001 = startSketchOn(XZ) +profile001 = startProfile(sketch001, at = [0, 0]) + |> angledLine(angle = 0, length = 11, tag = $a)`, + instance + ) + ) + expect(tagDeclarators).toEqual([tagDeclaratorWithIndex('a', 126, 128, 1)]) + }) + it(`finds multiple tag declarators in a small program`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const program = `sketch001 = startSketchOn(XZ) +profile001 = startProfile(sketch001, at = [0.07, 0]) + |> angledLine(angle = 0, length = 11, tag = $a) + |> angledLine(angle = segAng(a) + 90, length = 11.17, tag = $b) + |> angledLine(angle = segAng(a), length = -segLen(a), tag = $c) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()` + const tagDeclarators = getTagDeclaratorsInProgram( + assertParse(program, instance) + ) + expect(tagDeclarators).toEqual([ + tagDeclaratorWithIndex('a', 129, 131, 1), + tagDeclaratorWithIndex('b', 195, 197, 1), + tagDeclaratorWithIndex('c', 261, 263, 1), + ]) + }) + it(`finds tag declarators at different indices`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const program = `sketch001 = startSketchOn(XZ) +profile001 = startProfile(sketch001, at = [0.07, 0]) + |> angledLine(angle = 0, length = 11, tag = $a) +profile002 = angledLine(profile001, angle = segAng(a) + 90, length = 11.17, tag = $b) + |> angledLine(angle = segAng(a), length = -segLen(a), tag = $c) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()` + const tagDeclarators = getTagDeclaratorsInProgram( + assertParse(program, instance) + ) + expect(tagDeclarators).toEqual([ + tagDeclaratorWithIndex('a', 129, 131, 1), + tagDeclaratorWithIndex('b', 215, 217, 2), + tagDeclaratorWithIndex('c', 281, 283, 2), + ]) + }) + }) +}) From cada4cc043392c505fc94f097f4ff3c5b406e8a1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 6 Oct 2025 12:34:32 -0500 Subject: [PATCH 10/13] chore: removing another integration test file --- src/integration.spec.ts | 171 ------------------- src/lang/getNodePathFromSourceRange.spec.ts | 120 +++++++++++++ src/lang/modifyAst/boolean.spec.ts | 4 +- src/lang/queryAst/getSafeInsertIndex.spec.ts | 89 ++++++++++ 4 files changed, 211 insertions(+), 173 deletions(-) create mode 100644 src/lang/getNodePathFromSourceRange.spec.ts create mode 100644 src/lang/queryAst/getSafeInsertIndex.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index 76bef0014a7..75b94deeefc 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -81,10 +81,7 @@ import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { codeRefFromRange } from '@src/lang/std/artifactGraph' import { topLevelRange } from '@src/lang/util' import { isOverlap } from '@src/lib/utils' -import { getSafeInsertIndex } from '@src/lang/queryAst/getSafeInsertIndex' import { expect } from 'vitest' -import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' -import type { Parameter } from '@src/lang/wasm' import { modelingMachine } from '@src/machines/modelingMachine' import { createActor } from 'xstate' import { vi } from 'vitest' @@ -4373,174 +4370,6 @@ chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` }) }) -describe('getSafeInsertIndex.test.ts', () => { - describe(`getSafeInsertIndex`, () => { - it(`expression with no identifiers`, () => { - const baseProgram = assertParse(`x = 5 + 2 -y = 2 -z = x + y`) - const targetExpr = assertParse(`5`) - expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0) - }) - it(`expression with no identifiers in longer program`, () => { - const baseProgram = assertParse(`x = 5 + 2 - profile001 = startProfile(sketch001, at = [0.07, 0]) - |> angledLine(angle = 0, length = x, tag = $a) - |> angledLine(angle = segAng(a) + 90, length = 5) - |> angledLine(angle = segAng(a), length = -segLen(a)) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close()`) - const targetExpr = assertParse(`5`) - expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0) - }) - it(`expression with an identifier in the middle`, () => { - const baseProgram = assertParse(`x = 5 + 2 -y = 2 -z = x + y`) - const targetExpr = assertParse(`5 + y`) - expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) - }) - it(`expression with an identifier at the end`, () => { - const baseProgram = assertParse(`x = 5 + 2 -y = 2 -z = x + y`) - const targetExpr = assertParse(`z * z`) - expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(3) - }) - it(`expression with a tag declarator add to end`, () => { - const baseProgram = assertParse(`x = 5 + 2 - profile001 = startProfile(sketch001, at = [0.07, 0]) - |> angledLine(angle = 0, length = x, tag = $a) - |> angledLine(angle = segAng(a) + 90, length = 5) - |> angledLine(angle = segAng(a), length = -segLen(a)) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close()`) - const targetExpr = assertParse(`5 + segAng(a)`) - expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) - }) - it(`expression with a tag declarator and variable in the middle`, () => { - const baseProgram = assertParse(`x = 5 + 2 - profile001 = startProfile(sketch001, at = [0.07, 0]) - |> angledLine(angle = 0, length = x, tag = $a) - |> angledLine(angle = segAng(a) + 90, length = 5) - |> angledLine(angle = segAng(a), length = -segLen(a)) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - y = x + x`) - const targetExpr = assertParse(`x + segAng(a)`) - expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) - }) - }) -}) - -describe('getNodePathFromSourceRange.test.ts', () => { - describe('testing getNodePathFromSourceRange', () => { - it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => { - const code = ` -myVar = 5 -sk3 = startSketchOn(XY) - |> startProfile(at = [0, 0]) - |> line(endAbsolute = [1, 2]) - |> line(endAbsolute = [3, 4], tag = $yo) - |> close() -` - const subStr = 'line(endAbsolute = [3, 4], tag = $yo)' - const lineToSubstringIndex = code.indexOf(subStr) - const sourceRange = topLevelRange( - lineToSubstringIndex, - lineToSubstringIndex + subStr.length - ) - - const ast = assertParse(code) - const nodePath = getNodePathFromSourceRange(ast, sourceRange) - const _node = getNodeFromPath(ast, nodePath) - if (err(_node)) throw _node - const { node } = _node - - expect(topLevelRange(node.start, node.end)).toEqual(sourceRange) - expect(node.type).toBe('CallExpressionKw') - }) - it('gets path right for function definition params', () => { - const code = `fn cube(pos, scale) { - sg = startSketchOn(XY) - |> startProfile(at = pos) - |> line(end = [0, scale]) - |> line(end = [scale, 0]) - |> line(end = [0, -scale]) - - return sg -} - -b1 = cube(pos = [0,0], scale = 10)` - const subStr = 'pos, scale' - const subStrIndex = code.indexOf(subStr) - const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'pos'.length) - - const ast = assertParse(code) - const nodePath = getNodePathFromSourceRange(ast, sourceRange) - const _node = getNodeFromPath(ast, nodePath) - if (err(_node)) throw _node - const node = _node.node - - expect(nodePath).toEqual([ - ['body', ''], - [0, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', ''], - ['params', 'FunctionExpression'], - [0, 'index'], - ]) - expect(node.type).toBe('Parameter') - expect(node.identifier.name).toBe('pos') - }) - it('gets path right for deep within function definition body', () => { - const code = `fn cube(pos, scale) { - sg = startSketchOn(XY) - |> startProfile(at = pos) - |> line(end = [0, scale]) - |> line(end = [scale, 0]) - |> line(end = [0, -scale]) - - return sg -} - -b1 = cube(pos = [0,0], scale = 10)` - const subStr = 'scale, 0' - const subStrIndex = code.indexOf(subStr) - const sourceRange = topLevelRange( - subStrIndex, - subStrIndex + 'scale'.length - ) - - const ast = assertParse(code) - const nodePath = getNodePathFromSourceRange(ast, sourceRange) - const _node = getNodeFromPath(ast, nodePath) - if (err(_node)) throw _node - const node = _node.node - expect(nodePath).toEqual([ - ['body', ''], - [0, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', ''], - ['body', 'FunctionExpression'], - ['body', 'FunctionExpression'], - [0, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', ''], - ['body', 'PipeExpression'], - [3, 'index'], - ['arguments', 'CallExpressionKw'], - [0, ARG_INDEX_FIELD], - ['arg', LABELED_ARG_FIELD], - ['elements', 'ArrayExpression'], - [0, 'index'], - ]) - expect(node.type).toBe('Name') - expect(node.name.name).toBe('scale') - }) - }) -}) - const GLOBAL_TIMEOUT_FOR_MODELING_MACHINE = 5000 describe('modelingMachine.test.ts', () => { // Define mock implementations that will be referenced in vi.mock calls diff --git a/src/lang/getNodePathFromSourceRange.spec.ts b/src/lang/getNodePathFromSourceRange.spec.ts new file mode 100644 index 00000000000..d669f15a508 --- /dev/null +++ b/src/lang/getNodePathFromSourceRange.spec.ts @@ -0,0 +1,120 @@ +import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' +import { assertParse, type Name, type Parameter } from '@src/lang/wasm' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { topLevelRange } from '@src/lang/util' +import { getNodeFromPath } from '@src/lang/queryAst' +import { err } from '@src/lib/trap' +import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('getNodePathFromSourceRange', () => { + describe('testing getNodePathFromSourceRange', () => { + it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = ` +myVar = 5 +sk3 = startSketchOn(XY) + |> startProfile(at = [0, 0]) + |> line(endAbsolute = [1, 2]) + |> line(endAbsolute = [3, 4], tag = $yo) + |> close() +` + const subStr = 'line(endAbsolute = [3, 4], tag = $yo)' + const lineToSubstringIndex = code.indexOf(subStr) + const sourceRange = topLevelRange( + lineToSubstringIndex, + lineToSubstringIndex + subStr.length + ) + + const ast = assertParse(code, instance) + const nodePath = getNodePathFromSourceRange(ast, sourceRange) + const _node = getNodeFromPath(ast, nodePath) + if (err(_node)) throw _node + const { node } = _node + + expect(topLevelRange(node.start, node.end)).toEqual(sourceRange) + expect(node.type).toBe('CallExpressionKw') + }) + it('gets path right for function definition params', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `fn cube(pos, scale) { + sg = startSketchOn(XY) + |> startProfile(at = pos) + |> line(end = [0, scale]) + |> line(end = [scale, 0]) + |> line(end = [0, -scale]) + + return sg +} + +b1 = cube(pos = [0,0], scale = 10)` + const subStr = 'pos, scale' + const subStrIndex = code.indexOf(subStr) + const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'pos'.length) + + const ast = assertParse(code, instance) + const nodePath = getNodePathFromSourceRange(ast, sourceRange) + const _node = getNodeFromPath(ast, nodePath) + if (err(_node)) throw _node + const node = _node.node + + expect(nodePath).toEqual([ + ['body', ''], + [0, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', ''], + ['params', 'FunctionExpression'], + [0, 'index'], + ]) + expect(node.type).toBe('Parameter') + expect(node.identifier.name).toBe('pos') + }) + it('gets path right for deep within function definition body', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const code = `fn cube(pos, scale) { + sg = startSketchOn(XY) + |> startProfile(at = pos) + |> line(end = [0, scale]) + |> line(end = [scale, 0]) + |> line(end = [0, -scale]) + + return sg +} + +b1 = cube(pos = [0,0], scale = 10)` + const subStr = 'scale, 0' + const subStrIndex = code.indexOf(subStr) + const sourceRange = topLevelRange( + subStrIndex, + subStrIndex + 'scale'.length + ) + + const ast = assertParse(code, instance) + const nodePath = getNodePathFromSourceRange(ast, sourceRange) + const _node = getNodeFromPath(ast, nodePath) + if (err(_node)) throw _node + const node = _node.node + expect(nodePath).toEqual([ + ['body', ''], + [0, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', ''], + ['body', 'FunctionExpression'], + ['body', 'FunctionExpression'], + [0, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', ''], + ['body', 'PipeExpression'], + [3, 'index'], + ['arguments', 'CallExpressionKw'], + [0, ARG_INDEX_FIELD], + ['arg', LABELED_ARG_FIELD], + ['elements', 'ArrayExpression'], + [0, 'index'], + ]) + expect(node.type).toBe('Name') + expect(node.name.name).toBe('scale') + }) + }) +}) diff --git a/src/lang/modifyAst/boolean.spec.ts b/src/lang/modifyAst/boolean.spec.ts index ae3b60cdce4..7de8e14b680 100644 --- a/src/lang/modifyAst/boolean.spec.ts +++ b/src/lang/modifyAst/boolean.spec.ts @@ -1,6 +1,4 @@ import { assertParse, recast } from '@src/lang/wasm' -import { join } from 'path' -import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' import { err } from '@src/lib/trap' import type { Selections } from '@src/machines/modelingSharedTypes' import type { ModuleType } from '@src/lib/wasm_lib_wrapper' @@ -8,6 +6,8 @@ import { addSubtract } from '@src/lang/modifyAst/boolean' import { enginelessExecutor } from '@src/lib/testHelpers' import RustContext from '@src/lib/rustContext' import { ConnectionManager } from '@src/network/connectionManager' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') describe('boolean', () => { diff --git a/src/lang/queryAst/getSafeInsertIndex.spec.ts b/src/lang/queryAst/getSafeInsertIndex.spec.ts new file mode 100644 index 00000000000..f938a899c77 --- /dev/null +++ b/src/lang/queryAst/getSafeInsertIndex.spec.ts @@ -0,0 +1,89 @@ +import { assertParse } from '@src/lang/wasm' +import { getSafeInsertIndex } from '@src/lang/queryAst/getSafeInsertIndex' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +describe('getSafeInsertIndex.test.ts', () => { + describe(`getSafeInsertIndex`, () => { + it(`expression with no identifiers`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const baseProgram = assertParse( + `x = 5 + 2 +y = 2 +z = x + y`, + instance + ) + const targetExpr = assertParse(`5`, instance) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0) + }) + it(`expression with no identifiers in longer program`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const baseProgram = assertParse( + `x = 5 + 2 + profile001 = startProfile(sketch001, at = [0.07, 0]) + |> angledLine(angle = 0, length = x, tag = $a) + |> angledLine(angle = segAng(a) + 90, length = 5) + |> angledLine(angle = segAng(a), length = -segLen(a)) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()`, + instance + ) + const targetExpr = assertParse(`5`, instance) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0) + }) + it(`expression with an identifier in the middle`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const baseProgram = assertParse( + `x = 5 + 2 +y = 2 +z = x + y`, + instance + ) + const targetExpr = assertParse(`5 + y`, instance) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) + }) + it(`expression with an identifier at the end`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const baseProgram = assertParse( + `x = 5 + 2 +y = 2 +z = x + y`, + instance + ) + const targetExpr = assertParse(`z * z`, instance) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(3) + }) + it(`expression with a tag declarator add to end`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const baseProgram = assertParse( + `x = 5 + 2 + profile001 = startProfile(sketch001, at = [0.07, 0]) + |> angledLine(angle = 0, length = x, tag = $a) + |> angledLine(angle = segAng(a) + 90, length = 5) + |> angledLine(angle = segAng(a), length = -segLen(a)) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()`, + instance + ) + const targetExpr = assertParse(`5 + segAng(a)`, instance) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) + }) + it(`expression with a tag declarator and variable in the middle`, async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const baseProgram = assertParse( + `x = 5 + 2 + profile001 = startProfile(sketch001, at = [0.07, 0]) + |> angledLine(angle = 0, length = x, tag = $a) + |> angledLine(angle = segAng(a) + 90, length = 5) + |> angledLine(angle = segAng(a), length = -segLen(a)) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + y = x + x`, + instance + ) + const targetExpr = assertParse(`x + segAng(a)`, instance) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) + }) + }) +}) From ddd6821107a747e8b1519e88e39111bc14e429c6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 6 Oct 2025 14:17:20 -0500 Subject: [PATCH 11/13] chore: moved a massive integration file that built the world and connected to engine --- src/integration.spec.ts | 1048 --------------- src/lang/modifyAst/addEdgeTreatment.spec.ts | 1316 +++++++++++++++++++ 2 files changed, 1316 insertions(+), 1048 deletions(-) create mode 100644 src/lang/modifyAst/addEdgeTreatment.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index 75b94deeefc..f9f54aa55ae 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -8,9 +8,6 @@ import { type Name, type PlaneArtifact, type CallExpressionKw, - type PipeExpression, - type SourceRange, - type VariableDeclarator, } from '@src/lang/wasm' import type { Selection, Selections } from '@src/machines/modelingSharedTypes' import { enginelessExecutor } from '@src/lib/testHelpers' @@ -30,7 +27,6 @@ import { kclManager, rustContext, codeManager, - editorManager, } from '@src/lib/singletons' import type { ArtifactGraph } from '@src/lang/wasm' import { initPromise } from '@src/lang/wasmUtils' @@ -64,23 +60,7 @@ import { createIdentifier, createVariableDeclaration, } from '@src/lang/create' -import type { - ChamferParameters, - EdgeTreatmentParameters, - FilletParameters, -} from '@src/lang/modifyAst/addEdgeTreatment' -import { - EdgeTreatmentType, - deleteEdgeTreatment, - getPathToExtrudeForSegmentSelection, - hasValidEdgeTreatmentSelection, - modifyAstWithEdgeTreatmentAndTag, -} from '@src/lang/modifyAst/addEdgeTreatment' import { getEdgeCutMeta, getNodeFromPath } from '@src/lang/queryAst' -import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' -import { codeRefFromRange } from '@src/lang/std/artifactGraph' -import { topLevelRange } from '@src/lang/util' -import { isOverlap } from '@src/lib/utils' import { expect } from 'vitest' import { modelingMachine } from '@src/machines/modelingMachine' import { createActor } from 'xstate' @@ -3342,1034 +3322,6 @@ plane001 = offsetPlane(planeOf(extrude001, face = seg01), offset = 20)`) }) }) -describe('addEdgeTreatment.test.ts', () => { - const dependencies = { - kclManager, - engineCommandManager, - editorManager, - codeManager, - } - - const runGetPathToExtrudeForSegmentSelectionTest = async ( - code: string, - selectedSegmentSnippet: string, - expectedExtrudeSnippet: string, - expectError?: boolean - ) => { - // helpers - function getExtrudeExpression( - ast: Program, - pathToExtrudeNode: PathToNode - ): CallExpressionKw | PipeExpression | undefined | Error { - if (pathToExtrudeNode.length === 0) return undefined // no extrude node - - const extrudeNodeResult = getNodeFromPath( - ast, - pathToExtrudeNode - ) - if (err(extrudeNodeResult)) { - return extrudeNodeResult - } - return extrudeNodeResult.node - } - - function getExpectedExtrudeExpression( - ast: Program, - code: string, - expectedExtrudeSnippet: string - ): CallExpressionKw | PipeExpression | Error { - const extrudeRange = topLevelRange( - code.indexOf(expectedExtrudeSnippet), - code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length - ) - const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange) - const expectedExtrudeNodeResult = getNodeFromPath< - VariableDeclarator | CallExpressionKw - >(ast, expectedExtrudePath) - if (err(expectedExtrudeNodeResult)) { - return expectedExtrudeNodeResult - } - const expectedExtrudeNode = expectedExtrudeNodeResult.node - - // check whether extrude is in the sketch pipe - const extrudeInSketchPipe = - expectedExtrudeNode.type === 'CallExpressionKw' - if (extrudeInSketchPipe) { - return expectedExtrudeNode - } - if (!extrudeInSketchPipe) { - const init = expectedExtrudeNode.init - if ( - init.type !== 'CallExpressionKw' && - init.type !== 'PipeExpression' - ) { - return new Error( - 'Expected extrude expression is not a CallExpression or PipeExpression' - ) - } - return init - } - return new Error('Expected extrude expression not found') - } - - // ast - const ast = assertParse(code) - - // range - const segmentRange = topLevelRange( - code.indexOf(selectedSegmentSnippet), - code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length - ) - - // executeAst and artifactGraph - await kclManager.executeAst({ ast }) - const artifactGraph = kclManager.artifactGraph - - expect(kclManager.errors).toEqual([]) - - // find artifact - const maybeArtifact = [...artifactGraph].find(([, artifact]) => { - if (!('codeRef' in artifact && artifact.codeRef)) return false - return isOverlap(artifact.codeRef.range, segmentRange) - }) - - // build selection - const selection: Selection = { - codeRef: codeRefFromRange(segmentRange, ast), - artifact: maybeArtifact ? maybeArtifact[1] : undefined, - } - - // get extrude expression - const pathResult = getPathToExtrudeForSegmentSelection( - ast, - selection, - artifactGraph - ) - if (err(pathResult)) { - if (!expectError) { - expect(pathResult).toBeUndefined() - } - return pathResult - } - const { pathToExtrudeNode } = pathResult - const extrudeExpression = getExtrudeExpression(ast, pathToExtrudeNode) - - // test - if (expectedExtrudeSnippet) { - const expectedExtrudeExpression = getExpectedExtrudeExpression( - ast, - code, - expectedExtrudeSnippet - ) - if (err(expectedExtrudeExpression)) return expectedExtrudeExpression - expect(extrudeExpression).toEqual(expectedExtrudeExpression) - } else { - expect(extrudeExpression).toBeUndefined() - } - } - describe('Testing getPathToExtrudeForSegmentSelection', () => { - it('should return the correct paths for a valid selection and extrusion', async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - const selectedSegmentSnippet = `line(end = [20, 0])` - const expectedExtrudeSnippet = `extrude001 = extrude(sketch001, length = -15)` - await runGetPathToExtrudeForSegmentSelectionTest( - code, - selectedSegmentSnippet, - expectedExtrudeSnippet - ) - }, 10_000) - it('should return the correct paths when extrusion occurs within the sketch pipe', async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - |> extrude(length = 15)` - const selectedSegmentSnippet = `line(end = [20, 0])` - const expectedExtrudeSnippet = `extrude(length = 15)` - await runGetPathToExtrudeForSegmentSelectionTest( - code, - selectedSegmentSnippet, - expectedExtrudeSnippet - ) - }, 10_000) - it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-30, 30]) - |> line(end = [15, 0]) - |> line(end = [0, -15]) - |> line(end = [-15, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -sketch002 = startSketchOn(XY) - |> startProfile(at = [30, 30]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -sketch003 = startSketchOn(XY) - |> startProfile(at = [30, -30]) - |> line(end = [25, 0]) - |> line(end = [0, -25]) - |> line(end = [-25, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) -extrude002 = extrude(sketch002, length = -15) -extrude003 = extrude(sketch003, length = -15)` - const selectedSegmentSnippet = `line(end = [20, 0])` - const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = -15)` - await runGetPathToExtrudeForSegmentSelectionTest( - code, - selectedSegmentSnippet, - expectedExtrudeSnippet - ) - }, 10_000) - it('should return the correct paths for a (piped) extrude based on the other body (face)', async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-25, -25]) - |> yLine(length = 50) - |> xLine(length = 50) - |> yLine(length = -50) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - |> extrude(length = 50) -sketch002 = startSketchOn(sketch001, face = 'END') - |> startProfile(at = [-15, -15]) - |> yLine(length = 30) - |> xLine(length = 30) - |> yLine(length = -30) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - |> extrude(length = 30)` - const selectedSegmentSnippet = `xLine(length = 30)` - const expectedExtrudeSnippet = `extrude(length = 30)` - await runGetPathToExtrudeForSegmentSelectionTest( - code, - selectedSegmentSnippet, - expectedExtrudeSnippet - ) - }, 10_000) - it('should return the correct paths for a (non-piped) extrude based on the other body (face)', async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-25, -25]) - |> yLine(length = 50) - |> xLine(length = 50) - |> yLine(length = -50) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = 50) -sketch002 = startSketchOn(extrude001, face = 'END') - |> startProfile(at = [-15, -15]) - |> yLine(length = 30) - |> xLine(length = 30) - |> yLine(length = -30) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude002 = extrude(sketch002, length = 30)` - const selectedSegmentSnippet = `xLine(length = 30)` - const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = 30)` - await runGetPathToExtrudeForSegmentSelectionTest( - code, - selectedSegmentSnippet, - expectedExtrudeSnippet - ) - }, 10_000) - it('should not return any path for missing extrusion', async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-30, 30]) - |> line(end = [15, 0]) - |> line(end = [0, -15]) - |> line(end = [-15, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -sketch002 = startSketchOn(XY) - |> startProfile(at = [30, 30]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -sketch003 = startSketchOn(XY) - |> startProfile(at = [30, -30]) - |> line(end = [25, 0]) - |> line(end = [0, -25]) - |> line(end = [-25, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) -extrude003 = extrude(sketch003, length = -15)` - const selectedSegmentSnippet = `line(end = [20, 0])` - const expectedExtrudeSnippet = `` - await runGetPathToExtrudeForSegmentSelectionTest( - code, - selectedSegmentSnippet, - expectedExtrudeSnippet, - true - ) - }, 10_000) - }) - - const runModifyAstCloneWithEdgeTreatmentAndTag = async ( - code: string, - selectionSnippets: Array, - parameters: EdgeTreatmentParameters, - expectedCode: string - ) => { - // ast - const ast = assertParse(code) - - // selection - const segmentRanges: Array = selectionSnippets.map( - (selectionSnippet) => - topLevelRange( - code.indexOf(selectionSnippet), - code.indexOf(selectionSnippet) + selectionSnippet.length - ) - ) - - // executeAst - await kclManager.executeAst({ ast }) - const artifactGraph = kclManager.artifactGraph - - expect(kclManager.errors).toEqual([]) - - const selection: Selections = { - graphSelections: segmentRanges.map((segmentRange) => { - const maybeArtifact = [...artifactGraph].find(([, a]) => { - if (!('codeRef' in a && a.codeRef)) return false - return isOverlap(a.codeRef.range, segmentRange) - }) - return { - codeRef: codeRefFromRange(segmentRange, ast), - artifact: maybeArtifact ? maybeArtifact[1] : undefined, - } - }), - otherSelections: [], - } - - // apply edge treatment to selection - const result = await modifyAstWithEdgeTreatmentAndTag( - ast, - selection, - parameters, - dependencies - ) - if (err(result)) { - expect(result).toContain(expectedCode) - return result - } - const { modifiedAst } = result - - const newCode = recast(modifiedAst) - - expect(newCode).toContain(expectedCode) - } - const runDeleteEdgeTreatmentTest = async ( - code: string, - edgeTreatmentSnippet: string, - expectedCode: string - ) => { - // parse ast - const ast = assertParse(code) - - // update artifact graph - await kclManager.executeAst({ ast }) - const artifactGraph = kclManager.artifactGraph - - expect(kclManager.errors).toEqual([]) - - // define snippet range - const edgeTreatmentRange = topLevelRange( - code.indexOf(edgeTreatmentSnippet), - code.indexOf(edgeTreatmentSnippet) + edgeTreatmentSnippet.length - ) - - // find artifact - const maybeArtifact = [...artifactGraph].find(([, artifact]) => { - if (!('codeRef' in artifact)) return false - return isOverlap(artifact.codeRef.range, edgeTreatmentRange) - }) - - // build selection - const selection: Selection = { - codeRef: codeRefFromRange(edgeTreatmentRange, ast), - artifact: maybeArtifact ? maybeArtifact[1] : undefined, - } - - // delete edge treatment - const result = await deleteEdgeTreatment(ast, selection) - if (err(result)) { - expect(result).toContain(expectedCode) - return result - } - - // recast and check - const newCode = recast(result) - expect(newCode).toContain(expectedCode) - } - const createFilletParameters = (radiusValue: number): FilletParameters => ({ - type: EdgeTreatmentType.Fillet, - radius: { - valueAst: createLiteral(radiusValue), - valueText: radiusValue.toString(), - valueCalculated: radiusValue.toString(), - }, - }) - const createChamferParameters = (lengthValue: number): ChamferParameters => ({ - type: EdgeTreatmentType.Chamfer, - length: { - valueAst: createLiteral(lengthValue), - valueText: lengthValue.toString(), - valueCalculated: lengthValue.toString(), - }, - }) - // Iterate tests over all edge treatment types - Object.values(EdgeTreatmentType).forEach( - (edgeTreatmentType: EdgeTreatmentType) => { - // create parameters based on the edge treatment type - let parameterName: string - let parameters: EdgeTreatmentParameters - if (edgeTreatmentType === EdgeTreatmentType.Fillet) { - parameterName = 'radius' - parameters = createFilletParameters(3) - } else if (edgeTreatmentType === EdgeTreatmentType.Chamfer) { - parameterName = 'length' - parameters = createChamferParameters(3) - } else { - // Handle future edge treatments - return new Error( - `Unsupported edge treatment type: ${edgeTreatmentType}` - ) - } - // run tests - describe(`Testing modifyAstCloneWithEdgeTreatmentAndTag with ${edgeTreatmentType}s`, () => { - it(`should add a ${edgeTreatmentType} to a specific segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - const segmentSnippets = ['line(end = [0, -20])'] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20], tag = $seg01) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add a ${edgeTreatmentType} to the sketch pipe`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - |> extrude(length = -15)` - const segmentSnippets = ['line(end = [0, -20])'] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20], tag = $seg01) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - |> extrude(length = -15, tagEnd = $capEnd001) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add a ${edgeTreatmentType} to "close" if last segment is missing`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> close() - |> extrude(length = -15)` - const segmentSnippets = ['close()'] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> close(tag = $seg01) - |> extrude(length = -15, tagEnd = $capEnd001) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add a ${edgeTreatmentType} to an already tagged segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20], tag = $seg01) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - const segmentSnippets = ['line(end = [0, -20], tag = $seg01)'] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20], tag = $seg01) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add a ${edgeTreatmentType} with existing tag on other segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - const segmentSnippets = ['line(end = [-20, 0])'] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg02, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add a ${edgeTreatmentType} with existing fillet on other segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> fillet( - radius = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - )` - const segmentSnippets = ['line(end = [-20, 0])'] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> fillet( - radius = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - ) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg02, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add a ${edgeTreatmentType} with existing chamfer on other segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> chamfer( - length = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - )` - const segmentSnippets = ['line(end = [-20, 0])'] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> chamfer( - length = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]) - ], - ) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg02, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add a ${edgeTreatmentType} to two segments of a single extrusion`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - const segmentSnippets = [ - 'line(end = [20, 0])', - 'line(end = [-20, 0])', - ] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]), - getCommonEdge(faces = [seg02, capEnd001]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - it(`should add ${edgeTreatmentType}s to two bodies`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) -sketch002 = startSketchOn(XY) - |> startProfile(at = [30, 10]) - |> line(end = [15, 0]) - |> line(end = [0, -15]) - |> line(end = [-15, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude002 = extrude(sketch002, length = -25)` // <--- body 2 - const segmentSnippets = [ - 'line(end = [20, 0])', - 'line(end = [-20, 0])', - 'line(end = [0, -15])', - ] - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg01, capEnd001]), - getCommonEdge(faces = [seg02, capEnd001]) - ], - ) -sketch002 = startSketchOn(XY) - |> startProfile(at = [30, 10]) - |> line(end = [15, 0]) - |> line(end = [0, -15], tag = $seg03) - |> line(end = [-15, 0]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude002 = extrude(sketch002, length = -25, tagEnd = $capEnd002) - |> ${edgeTreatmentType}( - ${parameterName} = 3, - tags = [ - getCommonEdge(faces = [seg03, capEnd002]) - ], - )` - - await runModifyAstCloneWithEdgeTreatmentAndTag( - code, - segmentSnippets, - parameters, - expectedCode - ) - }, 10_000) - }) - describe(`Testing deleteEdgeTreatment with ${edgeTreatmentType}s`, () => { - // simple cases - it(`should delete a piped ${edgeTreatmentType} from a single segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) - |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01])` - const edgeTreatmentSnippet = `${edgeTreatmentType}(${parameterName} = 3, tags = [seg01])` - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - - await runDeleteEdgeTreatmentTest( - code, - edgeTreatmentSnippet, - expectedCode - ) - }, 10_000) - it(`should delete a standalone assigned ${edgeTreatmentType} from a single segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) -${edgeTreatmentType}001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])` - const edgeTreatmentSnippet = `${edgeTreatmentType}001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])` - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - - await runDeleteEdgeTreatmentTest( - code, - edgeTreatmentSnippet, - expectedCode - ) - }, 10_000) - it(`should delete a standalone ${edgeTreatmentType} without assignment from a single segment`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) -${edgeTreatmentType}(extrude001, ${parameterName} = 5, tags = [seg01])` - const edgeTreatmentSnippet = `${edgeTreatmentType}(extrude001, ${parameterName} = 5, tags = [seg01])` - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - - await runDeleteEdgeTreatmentTest( - code, - edgeTreatmentSnippet, - expectedCode - ) - }, 10_000) - // getOppositeEdge and getNextAdjacentEdge cases - it(`should delete a piped ${edgeTreatmentType} tagged with getOppositeEdge`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) -fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getOppositeEdge(seg01)])` - const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getOppositeEdge(seg01)])` - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - - await runDeleteEdgeTreatmentTest( - code, - edgeTreatmentSnippet, - expectedCode - ) - }, 10_000) - it(`should delete a non-piped ${edgeTreatmentType} tagged with getNextAdjacentEdge`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) -fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)])` - const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)])` - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0]) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg01) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15)` - - await runDeleteEdgeTreatmentTest( - code, - edgeTreatmentSnippet, - expectedCode - ) - }, 10_000) - // cases with several edge treatments - it(`should delete a piped ${edgeTreatmentType} from a body with multiple treatments`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) - |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01]) - |> fillet(radius = 5, tags = [getOppositeEdge(seg02)]) -fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02]) -chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` - const edgeTreatmentSnippet = `${edgeTreatmentType}(${parameterName} = 3, tags = [seg01])` - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) - |> fillet(radius = 5, tags = [getOppositeEdge(seg02)]) -fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02]) -chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` - - await runDeleteEdgeTreatmentTest( - code, - edgeTreatmentSnippet, - expectedCode - ) - }, 10_000) - it(`should delete a non-piped ${edgeTreatmentType} from a body with multiple treatments`, async () => { - const code = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) - |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01]) - |> fillet( radius = 5, tags = [getOppositeEdge(seg02)] ) -fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02]) -chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` - const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02])` - const expectedCode = `sketch001 = startSketchOn(XY) - |> startProfile(at = [-10, 10]) - |> line(end = [20, 0], tag = $seg01) - |> line(end = [0, -20]) - |> line(end = [-20, 0], tag = $seg02) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() -extrude001 = extrude(sketch001, length = -15) - |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01]) - |> fillet(radius = 5, tags = [getOppositeEdge(seg02)]) -chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` - - await runDeleteEdgeTreatmentTest( - code, - edgeTreatmentSnippet, - expectedCode - ) - }, 10_000) - }) - } - ) - - describe('Testing button states', () => { - const runButtonStateTest = async ( - code: string, - segmentSnippet: string, - expectedState: boolean - ) => { - const ast = assertParse(code) - - const start = code.indexOf(segmentSnippet) - expect(start).toBeGreaterThan(-1) - const range = segmentSnippet - ? topLevelRange(start, start + segmentSnippet.length) - : topLevelRange(ast.end, ast.end) // empty line in the end of the code - - const selectionRanges: Selections = { - graphSelections: [ - { - codeRef: codeRefFromRange(range, ast), - }, - ], - otherSelections: [], - } - - // state - const buttonState = hasValidEdgeTreatmentSelection({ - ast, - selectionRanges, - code, - }) - - expect(buttonState).toEqual(expectedState) - } - const codeWithBody: string = ` - sketch001 = startSketchOn(XY) - |> startProfile(at = [-20, -5]) - |> line(end = [0, 10]) - |> line(end = [10, 0]) - |> line(end = [0, -10]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - extrude001 = extrude(sketch001, length = -10) - ` - const codeWithoutBodies: string = ` - sketch001 = startSketchOn(XY) - |> startProfile(at = [-20, -5]) - |> line(end = [0, 10]) - |> line(end = [10, 0]) - |> line(end = [0, -10]) - |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) - |> close() - ` - // body is missing - it('should return false when body is missing and nothing is selected', async () => { - await runButtonStateTest(codeWithoutBodies, '', false) - }, 10_000) - it('should return false when body is missing and segment is selected', async () => { - await runButtonStateTest(codeWithoutBodies, `line(end = [10, 0])`, false) - }, 10_000) - - // body exists - it('should return true when body exists and nothing is selected', async () => { - await runButtonStateTest(codeWithBody, '', true) - }, 10_000) - it('should return true when body exists and segment is selected', async () => { - await runButtonStateTest(codeWithBody, `line(end = [10, 0])`, true) - }, 10_000) - it('should return false when body exists and not a segment is selected', async () => { - await runButtonStateTest(codeWithBody, `close()`, false) - }, 10_000) - }) -}) - const GLOBAL_TIMEOUT_FOR_MODELING_MACHINE = 5000 describe('modelingMachine.test.ts', () => { // Define mock implementations that will be referenced in vi.mock calls diff --git a/src/lang/modifyAst/addEdgeTreatment.spec.ts b/src/lang/modifyAst/addEdgeTreatment.spec.ts new file mode 100644 index 00000000000..35352758b45 --- /dev/null +++ b/src/lang/modifyAst/addEdgeTreatment.spec.ts @@ -0,0 +1,1316 @@ +import { getNodeFromPath } from '@src/lang/queryAst' +import { + assertParse, + type CallExpressionKw, + type PipeExpression, + type VariableDeclarator, + type Program, + type PathToNode, + recast, + type SourceRange, +} from '@src/lang/wasm' +import { err, reportRejection } from '@src/lib/trap' +import { topLevelRange } from '@src/lang/util' +import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' +import { isOverlap } from '@src/lib/utils' +import { codeRefFromRange } from '@src/lang/std/artifactGraph' +import { + type ChamferParameters, + deleteEdgeTreatment, + type EdgeTreatmentParameters, + EdgeTreatmentType, + type FilletParameters, + getPathToExtrudeForSegmentSelection, + hasValidEdgeTreatmentSelection, + modifyAstWithEdgeTreatmentAndTag, +} from '@src/lang/modifyAst/addEdgeTreatment' +import { KclManager } from '@src/lang/KclSingleton' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' +import { ConnectionManager } from '@src/network/connectionManager' +import RustContext from '@src/lib/rustContext' +import { SceneInfra } from '@src/clientSideScene/sceneInfra' +import EditorManager from '@src/editor/manager' +import CodeManager from '@src/lang/codeManager' +import { createLiteral } from '@src/lang/create' +import type { Selection, Selections } from '@src/machines/modelingSharedTypes' +import env from '@src/env' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + +async function buildTheWorldAndConnectToEngine() { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const sceneInfra = new SceneInfra(engineCommandManager) + const editorManager = new EditorManager(engineCommandManager) + const codeManager = new CodeManager({ editorManager }) + const kclManager = new KclManager(engineCommandManager, { + rustContext, + codeManager, + editorManager, + sceneInfra, + }) + editorManager.kclManager = kclManager + editorManager.codeManager = codeManager + engineCommandManager.kclManager = kclManager + engineCommandManager.codeManager = codeManager + engineCommandManager.sceneInfra = sceneInfra + engineCommandManager.rustContext = rustContext + await new Promise((resolve) => { + engineCommandManager + .start({ + token: env().VITE_KITTYCAD_API_TOKEN || '', + width: 256, + height: 256, + setStreamIsReady: () => { + console.log('no op for a unit test') + }, + callbackOnUnitTestingConnection: () => { + resolve(true) + }, + }) + .catch(reportRejection) + }) + return { + instance, + engineCommandManager, + rustContext, + sceneInfra, + editorManager, + codeManager, + kclManager, + } +} + +describe('addEdgeTreatment', () => { + const runGetPathToExtrudeForSegmentSelectionTest = async ( + code: string, + selectedSegmentSnippet: string, + expectedExtrudeSnippet: string, + instance: ModuleType, + kclManager: KclManager, + expectError?: boolean + ) => { + // helpers + function getExtrudeExpression( + ast: Program, + pathToExtrudeNode: PathToNode + ): CallExpressionKw | PipeExpression | undefined | Error { + if (pathToExtrudeNode.length === 0) return undefined // no extrude node + + const extrudeNodeResult = getNodeFromPath( + ast, + pathToExtrudeNode + ) + if (err(extrudeNodeResult)) { + return extrudeNodeResult + } + return extrudeNodeResult.node + } + + function getExpectedExtrudeExpression( + ast: Program, + code: string, + expectedExtrudeSnippet: string + ): CallExpressionKw | PipeExpression | Error { + const extrudeRange = topLevelRange( + code.indexOf(expectedExtrudeSnippet), + code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length + ) + const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange) + const expectedExtrudeNodeResult = getNodeFromPath< + VariableDeclarator | CallExpressionKw + >(ast, expectedExtrudePath) + if (err(expectedExtrudeNodeResult)) { + return expectedExtrudeNodeResult + } + const expectedExtrudeNode = expectedExtrudeNodeResult.node + + // check whether extrude is in the sketch pipe + const extrudeInSketchPipe = + expectedExtrudeNode.type === 'CallExpressionKw' + if (extrudeInSketchPipe) { + return expectedExtrudeNode + } + if (!extrudeInSketchPipe) { + const init = expectedExtrudeNode.init + if ( + init.type !== 'CallExpressionKw' && + init.type !== 'PipeExpression' + ) { + return new Error( + 'Expected extrude expression is not a CallExpression or PipeExpression' + ) + } + return init + } + return new Error('Expected extrude expression not found') + } + + // ast + const ast = assertParse(code, instance) + + // range + const segmentRange = topLevelRange( + code.indexOf(selectedSegmentSnippet), + code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length + ) + + // executeAst and artifactGraph + await kclManager.executeAst({ ast }) + const artifactGraph = kclManager.artifactGraph + + expect(kclManager.errors).toEqual([]) + + // find artifact + const maybeArtifact = [...artifactGraph].find(([, artifact]) => { + if (!('codeRef' in artifact && artifact.codeRef)) return false + return isOverlap(artifact.codeRef.range, segmentRange) + }) + + // build selection + const selection: Selection = { + codeRef: codeRefFromRange(segmentRange, ast), + artifact: maybeArtifact ? maybeArtifact[1] : undefined, + } + + // get extrude expression + const pathResult = getPathToExtrudeForSegmentSelection( + ast, + selection, + artifactGraph + ) + if (err(pathResult)) { + if (!expectError) { + expect(pathResult).toBeUndefined() + } + return pathResult + } + const { pathToExtrudeNode } = pathResult + const extrudeExpression = getExtrudeExpression(ast, pathToExtrudeNode) + + // test + if (expectedExtrudeSnippet) { + const expectedExtrudeExpression = getExpectedExtrudeExpression( + ast, + code, + expectedExtrudeSnippet + ) + if (err(expectedExtrudeExpression)) return expectedExtrudeExpression + expect(extrudeExpression).toEqual(expectedExtrudeExpression) + } else { + expect(extrudeExpression).toBeUndefined() + } + } + describe('Testing getPathToExtrudeForSegmentSelection', () => { + it('should return the correct paths for a valid selection and extrusion', async () => { + const { instance, kclManager, engineCommandManager } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + const selectedSegmentSnippet = `line(end = [20, 0])` + const expectedExtrudeSnippet = `extrude001 = extrude(sketch001, length = -15)` + await runGetPathToExtrudeForSegmentSelectionTest( + code, + selectedSegmentSnippet, + expectedExtrudeSnippet, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it('should return the correct paths when extrusion occurs within the sketch pipe', async () => { + const { instance, kclManager, engineCommandManager } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + |> extrude(length = 15)` + const selectedSegmentSnippet = `line(end = [20, 0])` + const expectedExtrudeSnippet = `extrude(length = 15)` + await runGetPathToExtrudeForSegmentSelectionTest( + code, + selectedSegmentSnippet, + expectedExtrudeSnippet, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => { + const { instance, kclManager, engineCommandManager } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-30, 30]) + |> line(end = [15, 0]) + |> line(end = [0, -15]) + |> line(end = [-15, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +sketch002 = startSketchOn(XY) + |> startProfile(at = [30, 30]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +sketch003 = startSketchOn(XY) + |> startProfile(at = [30, -30]) + |> line(end = [25, 0]) + |> line(end = [0, -25]) + |> line(end = [-25, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) +extrude002 = extrude(sketch002, length = -15) +extrude003 = extrude(sketch003, length = -15)` + const selectedSegmentSnippet = `line(end = [20, 0])` + const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = -15)` + await runGetPathToExtrudeForSegmentSelectionTest( + code, + selectedSegmentSnippet, + expectedExtrudeSnippet, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it('should return the correct paths for a (piped) extrude based on the other body (face)', async () => { + const { instance, kclManager, engineCommandManager } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-25, -25]) + |> yLine(length = 50) + |> xLine(length = 50) + |> yLine(length = -50) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + |> extrude(length = 50) +sketch002 = startSketchOn(sketch001, face = 'END') + |> startProfile(at = [-15, -15]) + |> yLine(length = 30) + |> xLine(length = 30) + |> yLine(length = -30) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + |> extrude(length = 30)` + const selectedSegmentSnippet = `xLine(length = 30)` + const expectedExtrudeSnippet = `extrude(length = 30)` + await runGetPathToExtrudeForSegmentSelectionTest( + code, + selectedSegmentSnippet, + expectedExtrudeSnippet, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it('should return the correct paths for a (non-piped) extrude based on the other body (face)', async () => { + const { instance, kclManager, engineCommandManager } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-25, -25]) + |> yLine(length = 50) + |> xLine(length = 50) + |> yLine(length = -50) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = 50) +sketch002 = startSketchOn(extrude001, face = 'END') + |> startProfile(at = [-15, -15]) + |> yLine(length = 30) + |> xLine(length = 30) + |> yLine(length = -30) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude002 = extrude(sketch002, length = 30)` + const selectedSegmentSnippet = `xLine(length = 30)` + const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = 30)` + await runGetPathToExtrudeForSegmentSelectionTest( + code, + selectedSegmentSnippet, + expectedExtrudeSnippet, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it('should not return any path for missing extrusion', async () => { + const { instance, kclManager, engineCommandManager } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-30, 30]) + |> line(end = [15, 0]) + |> line(end = [0, -15]) + |> line(end = [-15, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +sketch002 = startSketchOn(XY) + |> startProfile(at = [30, 30]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +sketch003 = startSketchOn(XY) + |> startProfile(at = [30, -30]) + |> line(end = [25, 0]) + |> line(end = [0, -25]) + |> line(end = [-25, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) +extrude003 = extrude(sketch003, length = -15)` + const selectedSegmentSnippet = `line(end = [20, 0])` + const expectedExtrudeSnippet = `` + await runGetPathToExtrudeForSegmentSelectionTest( + code, + selectedSegmentSnippet, + expectedExtrudeSnippet, + instance, + kclManager, + true + ) + engineCommandManager.tearDown() + }, 10_000) + }) + + const runModifyAstCloneWithEdgeTreatmentAndTag = async ( + code: string, + selectionSnippets: Array, + parameters: EdgeTreatmentParameters, + expectedCode: string, + instance: ModuleType, + kclManager: KclManager, + engineCommandManager: ConnectionManager, + editorManager: EditorManager, + codeManager: CodeManager + ) => { + // ast + const ast = assertParse(code, instance) + + // selection + const segmentRanges: Array = selectionSnippets.map( + (selectionSnippet) => + topLevelRange( + code.indexOf(selectionSnippet), + code.indexOf(selectionSnippet) + selectionSnippet.length + ) + ) + + // executeAst + await kclManager.executeAst({ ast }) + const artifactGraph = kclManager.artifactGraph + + expect(kclManager.errors).toEqual([]) + + const selection: Selections = { + graphSelections: segmentRanges.map((segmentRange) => { + const maybeArtifact = [...artifactGraph].find(([, a]) => { + if (!('codeRef' in a && a.codeRef)) return false + return isOverlap(a.codeRef.range, segmentRange) + }) + return { + codeRef: codeRefFromRange(segmentRange, ast), + artifact: maybeArtifact ? maybeArtifact[1] : undefined, + } + }), + otherSelections: [], + } + + // apply edge treatment to selection + const result = await modifyAstWithEdgeTreatmentAndTag( + ast, + selection, + parameters, + { + kclManager, + engineCommandManager, + editorManager, + codeManager, + } + ) + if (err(result)) { + expect(result).toContain(expectedCode) + return result + } + const { modifiedAst } = result + + const newCode = recast(modifiedAst, instance) + + expect(newCode).toContain(expectedCode) + } + const runDeleteEdgeTreatmentTest = async ( + code: string, + edgeTreatmentSnippet: string, + expectedCode: string, + instance: ModuleType, + kclManager: KclManager + ) => { + // parse ast + const ast = assertParse(code, instance) + + // update artifact graph + await kclManager.executeAst({ ast }) + const artifactGraph = kclManager.artifactGraph + + expect(kclManager.errors).toEqual([]) + + // define snippet range + const edgeTreatmentRange = topLevelRange( + code.indexOf(edgeTreatmentSnippet), + code.indexOf(edgeTreatmentSnippet) + edgeTreatmentSnippet.length + ) + + // find artifact + const maybeArtifact = [...artifactGraph].find(([, artifact]) => { + if (!('codeRef' in artifact)) return false + return isOverlap(artifact.codeRef.range, edgeTreatmentRange) + }) + + // build selection + const selection: Selection = { + codeRef: codeRefFromRange(edgeTreatmentRange, ast), + artifact: maybeArtifact ? maybeArtifact[1] : undefined, + } + + // delete edge treatment + const result = await deleteEdgeTreatment(ast, selection) + if (err(result)) { + expect(result).toContain(expectedCode) + return result + } + + // recast and check + const newCode = recast(result, instance) + expect(newCode).toContain(expectedCode) + } + const createFilletParameters = (radiusValue: number): FilletParameters => ({ + type: EdgeTreatmentType.Fillet, + radius: { + valueAst: createLiteral(radiusValue), + valueText: radiusValue.toString(), + valueCalculated: radiusValue.toString(), + }, + }) + const createChamferParameters = (lengthValue: number): ChamferParameters => ({ + type: EdgeTreatmentType.Chamfer, + length: { + valueAst: createLiteral(lengthValue), + valueText: lengthValue.toString(), + valueCalculated: lengthValue.toString(), + }, + }) + // Iterate tests over all edge treatment types + Object.values(EdgeTreatmentType).forEach( + (edgeTreatmentType: EdgeTreatmentType) => { + // create parameters based on the edge treatment type + let parameterName: string + let parameters: EdgeTreatmentParameters + if (edgeTreatmentType === EdgeTreatmentType.Fillet) { + parameterName = 'radius' + parameters = createFilletParameters(3) + } else if (edgeTreatmentType === EdgeTreatmentType.Chamfer) { + parameterName = 'length' + parameters = createChamferParameters(3) + } else { + // Handle future edge treatments + return new Error( + `Unsupported edge treatment type: ${edgeTreatmentType}` + ) + } + // run tests + describe(`Testing modifyAstCloneWithEdgeTreatmentAndTag with ${edgeTreatmentType}s`, () => { + it(`should add a ${edgeTreatmentType} to a specific segment`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + const segmentSnippets = ['line(end = [0, -20])'] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20], tag = $seg01) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add a ${edgeTreatmentType} to the sketch pipe`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + |> extrude(length = -15)` + const segmentSnippets = ['line(end = [0, -20])'] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20], tag = $seg01) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + |> extrude(length = -15, tagEnd = $capEnd001) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add a ${edgeTreatmentType} to "close" if last segment is missing`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> close() + |> extrude(length = -15)` + const segmentSnippets = ['close()'] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> close(tag = $seg01) + |> extrude(length = -15, tagEnd = $capEnd001) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + )` + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add a ${edgeTreatmentType} to an already tagged segment`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20], tag = $seg01) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + const segmentSnippets = ['line(end = [0, -20], tag = $seg01)'] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20], tag = $seg01) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add a ${edgeTreatmentType} with existing tag on other segment`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + const segmentSnippets = ['line(end = [-20, 0])'] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg02, capEnd001]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add a ${edgeTreatmentType} with existing fillet on other segment`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> fillet( + radius = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + )` + const segmentSnippets = ['line(end = [-20, 0])'] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> fillet( + radius = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + ) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg02, capEnd001]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add a ${edgeTreatmentType} with existing chamfer on other segment`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> chamfer( + length = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + )` + const segmentSnippets = ['line(end = [-20, 0])'] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> chamfer( + length = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]) + ], + ) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg02, capEnd001]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add a ${edgeTreatmentType} to two segments of a single extrusion`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + const segmentSnippets = [ + 'line(end = [20, 0])', + 'line(end = [-20, 0])', + ] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]), + getCommonEdge(faces = [seg02, capEnd001]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should add ${edgeTreatmentType}s to two bodies`, async () => { + const { + kclManager, + engineCommandManager, + editorManager, + codeManager, + instance, + } = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) +sketch002 = startSketchOn(XY) + |> startProfile(at = [30, 10]) + |> line(end = [15, 0]) + |> line(end = [0, -15]) + |> line(end = [-15, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude002 = extrude(sketch002, length = -25)` // <--- body 2 + const segmentSnippets = [ + 'line(end = [20, 0])', + 'line(end = [-20, 0])', + 'line(end = [0, -15])', + ] + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15, tagEnd = $capEnd001) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg01, capEnd001]), + getCommonEdge(faces = [seg02, capEnd001]) + ], + ) +sketch002 = startSketchOn(XY) + |> startProfile(at = [30, 10]) + |> line(end = [15, 0]) + |> line(end = [0, -15], tag = $seg03) + |> line(end = [-15, 0]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude002 = extrude(sketch002, length = -25, tagEnd = $capEnd002) + |> ${edgeTreatmentType}( + ${parameterName} = 3, + tags = [ + getCommonEdge(faces = [seg03, capEnd002]) + ], + )` + + await runModifyAstCloneWithEdgeTreatmentAndTag( + code, + segmentSnippets, + parameters, + expectedCode, + instance, + kclManager, + engineCommandManager, + editorManager, + codeManager + ) + engineCommandManager.tearDown() + }, 10_000) + }) + describe(`Testing deleteEdgeTreatment with ${edgeTreatmentType}s`, () => { + // simple cases + it(`should delete a piped ${edgeTreatmentType} from a single segment`, async () => { + const { kclManager, engineCommandManager, instance } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) + |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01])` + const edgeTreatmentSnippet = `${edgeTreatmentType}(${parameterName} = 3, tags = [seg01])` + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + + await runDeleteEdgeTreatmentTest( + code, + edgeTreatmentSnippet, + expectedCode, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should delete a standalone assigned ${edgeTreatmentType} from a single segment`, async () => { + const { kclManager, engineCommandManager, instance } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) +${edgeTreatmentType}001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])` + const edgeTreatmentSnippet = `${edgeTreatmentType}001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])` + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + + await runDeleteEdgeTreatmentTest( + code, + edgeTreatmentSnippet, + expectedCode, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should delete a standalone ${edgeTreatmentType} without assignment from a single segment`, async () => { + const { kclManager, engineCommandManager, instance } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) +${edgeTreatmentType}(extrude001, ${parameterName} = 5, tags = [seg01])` + const edgeTreatmentSnippet = `${edgeTreatmentType}(extrude001, ${parameterName} = 5, tags = [seg01])` + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + + await runDeleteEdgeTreatmentTest( + code, + edgeTreatmentSnippet, + expectedCode, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + // getOppositeEdge and getNextAdjacentEdge cases + it(`should delete a piped ${edgeTreatmentType} tagged with getOppositeEdge`, async () => { + const { kclManager, engineCommandManager, instance } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) +fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getOppositeEdge(seg01)])` + const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getOppositeEdge(seg01)])` + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + + await runDeleteEdgeTreatmentTest( + code, + edgeTreatmentSnippet, + expectedCode, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should delete a non-piped ${edgeTreatmentType} tagged with getNextAdjacentEdge`, async () => { + const { kclManager, engineCommandManager, instance } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) +fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)])` + const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)])` + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0]) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15)` + + await runDeleteEdgeTreatmentTest( + code, + edgeTreatmentSnippet, + expectedCode, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + // cases with several edge treatments + it(`should delete a piped ${edgeTreatmentType} from a body with multiple treatments`, async () => { + const { kclManager, engineCommandManager, instance } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) + |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01]) + |> fillet(radius = 5, tags = [getOppositeEdge(seg02)]) +fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02]) +chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` + const edgeTreatmentSnippet = `${edgeTreatmentType}(${parameterName} = 3, tags = [seg01])` + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) + |> fillet(radius = 5, tags = [getOppositeEdge(seg02)]) +fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02]) +chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` + + await runDeleteEdgeTreatmentTest( + code, + edgeTreatmentSnippet, + expectedCode, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + it(`should delete a non-piped ${edgeTreatmentType} from a body with multiple treatments`, async () => { + const { kclManager, engineCommandManager, instance } = + await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) + |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01]) + |> fillet( radius = 5, tags = [getOppositeEdge(seg02)] ) +fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02]) +chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` + const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 6, tags = [seg02])` + const expectedCode = `sketch001 = startSketchOn(XY) + |> startProfile(at = [-10, 10]) + |> line(end = [20, 0], tag = $seg01) + |> line(end = [0, -20]) + |> line(end = [-20, 0], tag = $seg02) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude001 = extrude(sketch001, length = -15) + |> ${edgeTreatmentType}(${parameterName} = 3, tags = [seg01]) + |> fillet(radius = 5, tags = [getOppositeEdge(seg02)]) +chamfer001 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])` + + await runDeleteEdgeTreatmentTest( + code, + edgeTreatmentSnippet, + expectedCode, + instance, + kclManager + ) + engineCommandManager.tearDown() + }, 10_000) + }) + } + ) + + describe('Testing button states', () => { + const runButtonStateTest = async ( + code: string, + segmentSnippet: string, + expectedState: boolean, + instance: ModuleType + ) => { + const ast = assertParse(code, instance) + + const start = code.indexOf(segmentSnippet) + expect(start).toBeGreaterThan(-1) + const range = segmentSnippet + ? topLevelRange(start, start + segmentSnippet.length) + : topLevelRange(ast.end, ast.end) // empty line in the end of the code + + const selectionRanges: Selections = { + graphSelections: [ + { + codeRef: codeRefFromRange(range, ast), + }, + ], + otherSelections: [], + } + + // state + const buttonState = hasValidEdgeTreatmentSelection({ + ast, + selectionRanges, + code, + }) + + expect(buttonState).toEqual(expectedState) + } + const codeWithBody: string = ` + sketch001 = startSketchOn(XY) + |> startProfile(at = [-20, -5]) + |> line(end = [0, 10]) + |> line(end = [10, 0]) + |> line(end = [0, -10]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + extrude001 = extrude(sketch001, length = -10) + ` + const codeWithoutBodies: string = ` + sketch001 = startSketchOn(XY) + |> startProfile(at = [-20, -5]) + |> line(end = [0, 10]) + |> line(end = [10, 0]) + |> line(end = [0, -10]) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + ` + // body is missing + it('should return false when body is missing and nothing is selected', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + await runButtonStateTest(codeWithoutBodies, '', false, instance) + }, 10_000) + it('should return false when body is missing and segment is selected', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + await runButtonStateTest( + codeWithoutBodies, + `line(end = [10, 0])`, + false, + instance + ) + }, 10_000) + + // body exists + it('should return true when body exists and nothing is selected', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + await runButtonStateTest(codeWithBody, '', true, instance) + }, 10_000) + it('should return true when body exists and segment is selected', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + await runButtonStateTest( + codeWithBody, + `line(end = [10, 0])`, + true, + instance + ) + }, 10_000) + it('should return false when body exists and not a segment is selected', async () => { + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + await runButtonStateTest(codeWithBody, `close()`, false, instance) + }, 10_000) + }) +}) From c9ec7a0206da0281e7d20a137173b4206775537a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 8 Oct 2025 17:18:53 -0500 Subject: [PATCH 12/13] chore: updating more tests in the transforms.spec.ts --- src/integration.spec.ts | 582 ------------------- src/lang/modifyAst/addEdgeTreatment.spec.ts | 47 +- src/lang/modifyAst/transforms.spec.ts | 614 ++++++++++++++++++++ src/lib/kclHelpers.ts | 8 +- src/unitTestUtils.ts | 56 ++ 5 files changed, 678 insertions(+), 629 deletions(-) create mode 100644 src/lang/modifyAst/transforms.spec.ts diff --git a/src/integration.spec.ts b/src/integration.spec.ts index f9f54aa55ae..13305bd489f 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -107,17 +107,6 @@ async function ENGLINELESS_getAstAndArtifactGraph(code: string) { return { ast, artifactGraph } } -async function getAstAndArtifactGraph(code: string) { - const ast = assertParse(code) - await kclManager.executeAst({ ast }) - const { - artifactGraph, - execState: { operations }, - variables, - } = kclManager - await new Promise((resolve) => setTimeout(resolve, 100)) - return { ast, artifactGraph, operations, variables } -} const executeCode = async (code: string) => { const ast = assertParse(code) await kclManager.executeAst({ ast }) @@ -126,22 +115,6 @@ const executeCode = async (code: string) => { return { ast, artifactGraph } } -function createSelectionFromPathArtifact( - artifacts: (Artifact & { codeRef: CodeRef })[] -): Selections { - const graphSelections = artifacts.map( - (artifact) => - ({ - codeRef: artifact.codeRef, - artifact, - }) as Selection - ) - return { - graphSelections, - otherSelections: [], - } -} - async function ENGINELESS_getAstAndSketchSelections(code: string) { const { ast, artifactGraph } = await ENGLINELESS_getAstAndArtifactGraph(code) const artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path') @@ -152,25 +125,6 @@ async function ENGINELESS_getAstAndSketchSelections(code: string) { return { artifactGraph, ast, sketches } } -async function getAstAndSketchSelections(code: string) { - const { ast, artifactGraph } = await getAstAndArtifactGraph(code) - const artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path') - if (artifacts.length === 0) { - throw new Error('Artifact not found in the graph') - } - const sketches = createSelectionFromPathArtifact(artifacts) - return { artifactGraph, ast, sketches } -} - -async function getKclCommandValue(value: string) { - const result = await stringToKclExpression(value) - if (err(result) || 'errors' in result) { - throw new Error(`Couldn't create kcl expression`) - } - - return result -} - async function runNewAstAndCheckForSweep(ast: Node) { const { artifactGraph } = await enginelessExecutor(ast) const sweepArtifact = artifactGraph.values().find((a) => a.type === 'sweep') @@ -225,542 +179,6 @@ const createSelectionWithFirstMatchingArtifact = async ( return { selection } } -describe('transforms.test.ts', () => { - describe('Testing addTranslate', () => { - async function runAddTranslateTest(code: string) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addTranslate({ - ast, - artifactGraph, - objects, - x: await getKclCommandValue('1'), - y: await getKclCommandValue('2'), - z: await getKclCommandValue('3'), - global: true, - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should add a standalone translate call on sweep selection', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1)` - const expectedNewLine = `translate( - extrude001, - x = 1, - y = 2, - z = 3, - global = true, -)` - const newCode = await runAddTranslateTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - it('should push a call in pipe if selection was in variable-less pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1)` - const expectedNewLine = ` |> translate( - x = 1, - y = 2, - z = 3, - global = true, - )` - const newCode = await runAddTranslateTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - async function runEditTranslateTest(code: string, nodeToEdit: PathToNode) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addTranslate({ - ast, - artifactGraph, - objects, - x: await getKclCommandValue('4'), - y: await getKclCommandValue('5'), - z: await getKclCommandValue('6'), - global: false, - nodeToEdit, - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should edit a call with variable if og selection was a variable sweep', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -translate( - extrude001, - x = 1, - y = 2, - z = 3, - global = true, -)` - const expectedNewCode = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -translate( - extrude001, - x = 4, - y = 5, - z = 6, -)` - const nodeToEdit: PathToNode = [ - ['body', ''], - [3, 'index'], - ['expression', 'ExpressionStatement'], - ] - const newCode = await runEditTranslateTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - - it('should edit a call in pipe if og selection was in pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> translate( - x = 1, - y = 2, - z = 3, - global = true, - )` - const expectedNewCode = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> translate(x = 4, y = 5, z = 6) -` - const nodeToEdit: PathToNode = [ - ['body', ''], - [0, 'index'], - ['expression', 'ExpressionStatement'], - ['body', 'PipeExpression'], - [3, 'index'], - ] - const newCode = await runEditTranslateTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - }) - - describe('Testing addScale', () => { - async function runAddScaleTest(code: string) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addScale({ - ast, - artifactGraph, - objects, - x: await getKclCommandValue('1'), - y: await getKclCommandValue('2'), - z: await getKclCommandValue('3'), - global: true, - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should add a standalone call on sweep selection', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1)` - const expectedNewLine = `scale( - extrude001, - x = 1, - y = 2, - z = 3, - global = true, -)` - const newCode = await runAddScaleTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - it('should push a call in pipe if selection was in variable-less pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1)` - const expectedNewLine = ` |> scale( - x = 1, - y = 2, - z = 3, - global = true, - )` - const newCode = await runAddScaleTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - async function runEditScaleTest(code: string, nodeToEdit: PathToNode) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addScale({ - ast, - artifactGraph, - objects, - x: await getKclCommandValue('4'), - y: await getKclCommandValue('5'), - z: await getKclCommandValue('6'), - global: false, - nodeToEdit, - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should edit a scale call with variable if og selection was a variable sweep', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -scale( - extrude001, - x = 1, - y = 2, - z = 3, - global = true, -)` - const expectedNewCode = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -scale( - extrude001, - x = 4, - y = 5, - z = 6, -)` - const nodeToEdit: PathToNode = [ - ['body', ''], - [3, 'index'], - ['expression', 'ExpressionStatement'], - ] - const newCode = await runEditScaleTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - - it('should edit a call in pipe if og selection was in pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> scale( - x = 1, - y = 2, - z = 3, - global = true, - )` - const expectedNewCode = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> scale(x = 4, y = 5, z = 6) -` - const nodeToEdit: PathToNode = [ - ['body', ''], - [0, 'index'], - ['expression', 'ExpressionStatement'], - ['body', 'PipeExpression'], - [3, 'index'], - ] - const newCode = await runEditScaleTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - - // TODO: missing multi-objects test - }) - - describe('Testing addRotate', () => { - async function runAddRotateTest(code: string) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addRotate({ - ast, - artifactGraph, - objects, - roll: await getKclCommandValue('10'), - pitch: await getKclCommandValue('20'), - yaw: await getKclCommandValue('30'), - global: true, - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should add a standalone call on sweep selection', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1)` - const expectedNewLine = `rotate( - extrude001, - roll = 10, - pitch = 20, - yaw = 30, - global = true, -)` - const newCode = await runAddRotateTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - it('should push a call in pipe if selection was in variable-less pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1)` - const expectedNewLine = ` |> rotate( - roll = 10, - pitch = 20, - yaw = 30, - global = true, - )` - const newCode = await runAddRotateTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - async function runEditRotateTest(code: string, nodeToEdit: PathToNode) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addRotate({ - ast, - artifactGraph, - objects, - roll: await getKclCommandValue('40'), - pitch: await getKclCommandValue('50'), - yaw: await getKclCommandValue('60'), - global: false, - nodeToEdit, - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should edit a call with variable if og selection was a variable sweep', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -rotate( - extrude001, - roll = 4, - pitch = 5, - yaw = 6, - global = true, -)` - const expectedNewCode = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -rotate( - extrude001, - roll = 40, - pitch = 50, - yaw = 60, -)` - const nodeToEdit: PathToNode = [ - ['body', ''], - [3, 'index'], - ['expression', 'ExpressionStatement'], - ] - const newCode = await runEditRotateTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - - it('should edit a call in pipe if og selection was in pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> rotate( - roll = 1, - pitch = 2, - yaw = 3, - global = true, - )` - const expectedNewCode = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> rotate(roll = 40, pitch = 50, yaw = 60) -` - const nodeToEdit: PathToNode = [ - ['body', ''], - [0, 'index'], - ['expression', 'ExpressionStatement'], - ['body', 'PipeExpression'], - [3, 'index'], - ] - const newCode = await runEditRotateTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - - // TODO: missing multi-objects test - }) - - describe('Testing addClone', () => { - async function runAddCloneTest(code: string) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addClone({ - ast, - artifactGraph, - objects, - variableName: 'yoyoyo', - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should add a standalone call on sweep selection', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1)` - const expectedNewLine = `yoyoyo = clone(extrude001)` - const newCode = await runAddCloneTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - }) - - describe('Testing addAppearance', () => { - async function runAddAppearanceTest(code: string) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addAppearance({ - ast, - artifactGraph, - objects, - color: '#FF0000', - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - const box = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1)` - it('should add a standalone call on sweep selection', async () => { - const expectedNewLine = `appearance(extrude001, color = '#FF0000')` - const newCode = await runAddAppearanceTest(box) - expect(newCode).toContain(box + '\n' + expectedNewLine) - }) - - it('should push a call in pipe if selection was in variable-less pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1)` - const expectedNewLine = ` |> appearance(color = '#FF0000')` - const newCode = await runAddAppearanceTest(code) - expect(newCode).toContain(code + '\n' + expectedNewLine) - }) - - it('should add a call with metalness and roughness', async () => { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(box) - const result = addAppearance({ - ast, - artifactGraph, - objects, - color: '#FF0000', - metalness: await getKclCommandValue('1'), - roughness: await getKclCommandValue('2'), - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - const newCode = recast(result.modifiedAst) - expect(newCode).toContain(`${box} -appearance( - extrude001, - color = '#FF0000', - metalness = 1, - roughness = 2, -)`) - }) - - async function runEditAppearanceTest(code: string, nodeToEdit: PathToNode) { - const { - artifactGraph, - ast, - sketches: objects, - } = await getAstAndSketchSelections(code) - const result = addAppearance({ - ast, - artifactGraph, - objects, - color: '#00FF00', - nodeToEdit, - }) - if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) - } - - it('should edit a call with variable if og selection was a variable sweep', async () => { - const code = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -appearance(extrude001, color = '#FF0000')` - const expectedNewCode = `sketch001 = startSketchOn(XY) -profile001 = circle(sketch001, center = [0, 0], radius = 1) -extrude001 = extrude(profile001, length = 1) -appearance(extrude001, color = '#00FF00')` - const nodeToEdit: PathToNode = [ - ['body', ''], - [3, 'index'], - ['expression', 'ExpressionStatement'], - ] - const newCode = await runEditAppearanceTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - - it('should edit a call in pipe if og selection was in pipe', async () => { - const code = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> appearance(color = '#FF0000')` - const expectedNewCode = `startSketchOn(XY) - |> circle(center = [0, 0], radius = 1) - |> extrude(length = 1) - |> appearance(color = '#00FF00')` - const nodeToEdit: PathToNode = [ - ['body', ''], - [0, 'index'], - ['expression', 'ExpressionStatement'], - ['body', 'PipeExpression'], - [3, 'index'], - ] - const newCode = await runEditAppearanceTest(code, nodeToEdit) - expect(newCode).toContain(expectedNewCode) - }) - - // TODO: missing multi-objects test - }) -}) - describe('tagManagement.test.ts', () => { // Tests for modifyAstWithTagsForSelection describe('modifyAstWithTagsForSelection', () => { diff --git a/src/lang/modifyAst/addEdgeTreatment.spec.ts b/src/lang/modifyAst/addEdgeTreatment.spec.ts index 35352758b45..093d72f2cb7 100644 --- a/src/lang/modifyAst/addEdgeTreatment.spec.ts +++ b/src/lang/modifyAst/addEdgeTreatment.spec.ts @@ -36,52 +36,9 @@ import CodeManager from '@src/lang/codeManager' import { createLiteral } from '@src/lang/create' import type { Selection, Selections } from '@src/machines/modelingSharedTypes' import env from '@src/env' -const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') +import { buildTheWorldAndConnectToEngine } from '@src/unitTestUtils' -async function buildTheWorldAndConnectToEngine() { - const instance = await loadAndInitialiseWasmInstance(WASM_PATH) - const engineCommandManager = new ConnectionManager() - const rustContext = new RustContext(engineCommandManager, instance) - const sceneInfra = new SceneInfra(engineCommandManager) - const editorManager = new EditorManager(engineCommandManager) - const codeManager = new CodeManager({ editorManager }) - const kclManager = new KclManager(engineCommandManager, { - rustContext, - codeManager, - editorManager, - sceneInfra, - }) - editorManager.kclManager = kclManager - editorManager.codeManager = codeManager - engineCommandManager.kclManager = kclManager - engineCommandManager.codeManager = codeManager - engineCommandManager.sceneInfra = sceneInfra - engineCommandManager.rustContext = rustContext - await new Promise((resolve) => { - engineCommandManager - .start({ - token: env().VITE_KITTYCAD_API_TOKEN || '', - width: 256, - height: 256, - setStreamIsReady: () => { - console.log('no op for a unit test') - }, - callbackOnUnitTestingConnection: () => { - resolve(true) - }, - }) - .catch(reportRejection) - }) - return { - instance, - engineCommandManager, - rustContext, - sceneInfra, - editorManager, - codeManager, - kclManager, - } -} describe('addEdgeTreatment', () => { const runGetPathToExtrudeForSegmentSelectionTest = async ( diff --git a/src/lang/modifyAst/transforms.spec.ts b/src/lang/modifyAst/transforms.spec.ts new file mode 100644 index 00000000000..f0672740b97 --- /dev/null +++ b/src/lang/modifyAst/transforms.spec.ts @@ -0,0 +1,614 @@ +import { Selections, Selection } from "@src/machines/modelingSharedTypes" +import { Artifact, assertParse, CodeRef, PathToNode, recast, type Program } from "@src/lang/wasm" +import { ModuleType } from "@src/lib/wasm_lib_wrapper" +import { KclManager } from "@src/lang/KclSingleton" +import { addTranslate } from "./transforms" +import { stringToKclExpression } from '@src/lib/kclHelpers' +import { err} from '@src/lib/trap' +import { enginelessExecutor } from '@src/lib/testHelpers' +import type { Node } from '@rust/kcl-lib/bindings/Node' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { join } from 'path' +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') +import { buildTheWorldAndConnectToEngine } from '@src/unitTestUtils' +import RustContext from "@src/lib/rustContext" +import { + addScale, +} from '@src/lang/modifyAst/transforms' + +async function getKclCommandValue(value: string, instance: ModuleType, rustContext: RustContext) { + const result = await stringToKclExpression(value, undefined, instance, rustContext) + if (err(result) || 'errors' in result) { + throw new Error(`Couldn't create kcl expression`) + } + + return result +} + +async function runNewAstAndCheckForSweep(ast: Node, rustContext: RustContext) { + const { artifactGraph } = await enginelessExecutor(ast, undefined, undefined, rustContext) + const sweepArtifact = artifactGraph.values().find((a) => a.type === 'sweep') + expect(sweepArtifact).toBeDefined() +} + +function createSelectionFromPathArtifact( + artifacts: (Artifact & { codeRef: CodeRef })[] +): Selections { + const graphSelections = artifacts.map( + (artifact) => + ({ + codeRef: artifact.codeRef, + artifact, + }) as Selection + ) + return { + graphSelections, + otherSelections: [], + } +} + +async function getAstAndArtifactGraph(code: string, instance: ModuleType, kclManager: KclManager) { + const ast = assertParse(code, instance) + await kclManager.executeAst({ ast }) + const { + artifactGraph, + execState: { operations }, + variables, + } = kclManager + await new Promise((resolve) => setTimeout(resolve, 100)) + return { ast, artifactGraph, operations, variables } +} + +async function getAstAndSketchSelections(code: string, instance: ModuleType, kclManager: KclManager) { + const { ast, artifactGraph } = await getAstAndArtifactGraph(code, instance, kclManager) + const artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path') + if (artifacts.length === 0) { + throw new Error('Artifact not found in the graph') + } + const sketches = createSelectionFromPathArtifact(artifacts) + return { artifactGraph, ast, sketches } +} + +describe('transforms.test.ts', () => { + describe('Testing addTranslate', () => { + async function runAddTranslateTest(code: string, instance: ModuleType, kclManager: KclManager, rustContext: RustContext) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code, instance, kclManager) + const result = addTranslate({ + ast, + artifactGraph, + objects, + x: await getKclCommandValue('1', instance, rustContext), + y: await getKclCommandValue('2', instance, rustContext), + z: await getKclCommandValue('3', instance, rustContext), + global: true, + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) + } + + it('should add a standalone translate call on sweep selection', async () => { + const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1)` + const expectedNewLine = `translate( + extrude001, + x = 1, + y = 2, + z = 3, + global = true, +)` + const newCode = await runAddTranslateTest(code, instance, kclManager, rustContext) + expect(newCode).toContain(code + '\n' + expectedNewLine) + engineCommandManager.tearDown() + }) + + it('should push a call in pipe if selection was in variable-less pipe', async () => { + const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1)` + const expectedNewLine = ` |> translate( + x = 1, + y = 2, + z = 3, + global = true, + )` + const newCode = await runAddTranslateTest(code, instance, kclManager, rustContext) + expect(newCode).toContain(code + '\n' + expectedNewLine) +engineCommandManager.tearDown() + }) + + async function runEditTranslateTest(code: string, nodeToEdit: PathToNode, instance: ModuleType, kclManager: KclManager, rustContext: RustContext) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code, instance, kclManager) + const result = addTranslate({ + ast, + artifactGraph, + objects, + x: await getKclCommandValue('4', instance, rustContext), + y: await getKclCommandValue('5', instance, rustContext), + z: await getKclCommandValue('6', instance, rustContext), + global: false, + nodeToEdit, + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) + } + + it('should edit a call with variable if og selection was a variable sweep', async () => { +const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +translate( + extrude001, + x = 1, + y = 2, + z = 3, + global = true, +)` + const expectedNewCode = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +translate( + extrude001, + x = 4, + y = 5, + z = 6, +)` + const nodeToEdit: PathToNode = [ + ['body', ''], + [3, 'index'], + ['expression', 'ExpressionStatement'], + ] + const newCode = await runEditTranslateTest(code, nodeToEdit, instance, kclManager, rustContext) + expect(newCode).toContain(expectedNewCode) +engineCommandManager.tearDown() + }) + + it('should edit a call in pipe if og selection was in pipe', async () => { +const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> translate( + x = 1, + y = 2, + z = 3, + global = true, + )` + const expectedNewCode = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> translate(x = 4, y = 5, z = 6) +` + const nodeToEdit: PathToNode = [ + ['body', ''], + [0, 'index'], + ['expression', 'ExpressionStatement'], + ['body', 'PipeExpression'], + [3, 'index'], + ] + const newCode = await runEditTranslateTest(code, nodeToEdit, instance, kclManager, rustContext) + expect(newCode).toContain(expectedNewCode) +engineCommandManager.tearDown() + }) + }) + + describe('Testing addScale', () => { + async function runAddScaleTest(code: string, instance: ModuleType, kclManager: KclManager, rustContext: RustContext) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code, instance, kclManager) + const result = addScale({ + ast, + artifactGraph, + objects, + x: await getKclCommandValue('1', instance, rustContext), + y: await getKclCommandValue('2', instance, rustContext), + z: await getKclCommandValue('3', instance, rustContext), + global: true, + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) + } + + it('should add a standalone call on sweep selection', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1)` + const expectedNewLine = `scale( + extrude001, + x = 1, + y = 2, + z = 3, + global = true, +)` + const newCode = await runAddScaleTest(code) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + + it('should push a call in pipe if selection was in variable-less pipe', async () => { + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1)` + const expectedNewLine = ` |> scale( + x = 1, + y = 2, + z = 3, + global = true, + )` + const newCode = await runAddScaleTest(code) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + + async function runEditScaleTest(code: string, nodeToEdit: PathToNode) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code) + const result = addScale({ + ast, + artifactGraph, + objects, + x: await getKclCommandValue('4'), + y: await getKclCommandValue('5'), + z: await getKclCommandValue('6'), + global: false, + nodeToEdit, + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + return recast(result.modifiedAst) + } + + it('should edit a scale call with variable if og selection was a variable sweep', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +scale( + extrude001, + x = 1, + y = 2, + z = 3, + global = true, +)` + const expectedNewCode = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +scale( + extrude001, + x = 4, + y = 5, + z = 6, +)` + const nodeToEdit: PathToNode = [ + ['body', ''], + [3, 'index'], + ['expression', 'ExpressionStatement'], + ] + const newCode = await runEditScaleTest(code, nodeToEdit) + expect(newCode).toContain(expectedNewCode) + }) + + it('should edit a call in pipe if og selection was in pipe', async () => { + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> scale( + x = 1, + y = 2, + z = 3, + global = true, + )` + const expectedNewCode = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> scale(x = 4, y = 5, z = 6) +` + const nodeToEdit: PathToNode = [ + ['body', ''], + [0, 'index'], + ['expression', 'ExpressionStatement'], + ['body', 'PipeExpression'], + [3, 'index'], + ] + const newCode = await runEditScaleTest(code, nodeToEdit) + expect(newCode).toContain(expectedNewCode) + }) + + // TODO: missing multi-objects test + }) + + describe('Testing addRotate', () => { + async function runAddRotateTest(code: string) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code) + const result = addRotate({ + ast, + artifactGraph, + objects, + roll: await getKclCommandValue('10'), + pitch: await getKclCommandValue('20'), + yaw: await getKclCommandValue('30'), + global: true, + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + return recast(result.modifiedAst) + } + + it('should add a standalone call on sweep selection', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1)` + const expectedNewLine = `rotate( + extrude001, + roll = 10, + pitch = 20, + yaw = 30, + global = true, +)` + const newCode = await runAddRotateTest(code) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + + it('should push a call in pipe if selection was in variable-less pipe', async () => { + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1)` + const expectedNewLine = ` |> rotate( + roll = 10, + pitch = 20, + yaw = 30, + global = true, + )` + const newCode = await runAddRotateTest(code) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + + async function runEditRotateTest(code: string, nodeToEdit: PathToNode) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code) + const result = addRotate({ + ast, + artifactGraph, + objects, + roll: await getKclCommandValue('40'), + pitch: await getKclCommandValue('50'), + yaw: await getKclCommandValue('60'), + global: false, + nodeToEdit, + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + return recast(result.modifiedAst) + } + + it('should edit a call with variable if og selection was a variable sweep', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +rotate( + extrude001, + roll = 4, + pitch = 5, + yaw = 6, + global = true, +)` + const expectedNewCode = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +rotate( + extrude001, + roll = 40, + pitch = 50, + yaw = 60, +)` + const nodeToEdit: PathToNode = [ + ['body', ''], + [3, 'index'], + ['expression', 'ExpressionStatement'], + ] + const newCode = await runEditRotateTest(code, nodeToEdit) + expect(newCode).toContain(expectedNewCode) + }) + + it('should edit a call in pipe if og selection was in pipe', async () => { + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> rotate( + roll = 1, + pitch = 2, + yaw = 3, + global = true, + )` + const expectedNewCode = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> rotate(roll = 40, pitch = 50, yaw = 60) +` + const nodeToEdit: PathToNode = [ + ['body', ''], + [0, 'index'], + ['expression', 'ExpressionStatement'], + ['body', 'PipeExpression'], + [3, 'index'], + ] + const newCode = await runEditRotateTest(code, nodeToEdit) + expect(newCode).toContain(expectedNewCode) + }) + + // TODO: missing multi-objects test + }) + + describe('Testing addClone', () => { + async function runAddCloneTest(code: string) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code) + const result = addClone({ + ast, + artifactGraph, + objects, + variableName: 'yoyoyo', + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + return recast(result.modifiedAst) + } + + it('should add a standalone call on sweep selection', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1)` + const expectedNewLine = `yoyoyo = clone(extrude001)` + const newCode = await runAddCloneTest(code) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + }) + + describe('Testing addAppearance', () => { + async function runAddAppearanceTest(code: string) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code) + const result = addAppearance({ + ast, + artifactGraph, + objects, + color: '#FF0000', + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + return recast(result.modifiedAst) + } + + const box = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1)` + it('should add a standalone call on sweep selection', async () => { + const expectedNewLine = `appearance(extrude001, color = '#FF0000')` + const newCode = await runAddAppearanceTest(box) + expect(newCode).toContain(box + '\n' + expectedNewLine) + }) + + it('should push a call in pipe if selection was in variable-less pipe', async () => { + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1)` + const expectedNewLine = ` |> appearance(color = '#FF0000')` + const newCode = await runAddAppearanceTest(code) + expect(newCode).toContain(code + '\n' + expectedNewLine) + }) + + it('should add a call with metalness and roughness', async () => { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(box) + const result = addAppearance({ + ast, + artifactGraph, + objects, + color: '#FF0000', + metalness: await getKclCommandValue('1'), + roughness: await getKclCommandValue('2'), + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(`${box} +appearance( + extrude001, + color = '#FF0000', + metalness = 1, + roughness = 2, +)`) + }) + + async function runEditAppearanceTest(code: string, nodeToEdit: PathToNode) { + const { + artifactGraph, + ast, + sketches: objects, + } = await getAstAndSketchSelections(code) + const result = addAppearance({ + ast, + artifactGraph, + objects, + color: '#00FF00', + nodeToEdit, + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + return recast(result.modifiedAst) + } + + it('should edit a call with variable if og selection was a variable sweep', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +appearance(extrude001, color = '#FF0000')` + const expectedNewCode = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 1) +appearance(extrude001, color = '#00FF00')` + const nodeToEdit: PathToNode = [ + ['body', ''], + [3, 'index'], + ['expression', 'ExpressionStatement'], + ] + const newCode = await runEditAppearanceTest(code, nodeToEdit) + expect(newCode).toContain(expectedNewCode) + }) + + it('should edit a call in pipe if og selection was in pipe', async () => { + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> appearance(color = '#FF0000')` + const expectedNewCode = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) + |> extrude(length = 1) + |> appearance(color = '#00FF00')` + const nodeToEdit: PathToNode = [ + ['body', ''], + [0, 'index'], + ['expression', 'ExpressionStatement'], + ['body', 'PipeExpression'], + [3, 'index'], + ] + const newCode = await runEditAppearanceTest(code, nodeToEdit) + expect(newCode).toContain(expectedNewCode) + }) + + // TODO: missing multi-objects test + }) +}) diff --git a/src/lib/kclHelpers.ts b/src/lib/kclHelpers.ts index 8b54c71256d..fad5cb8eab0 100644 --- a/src/lib/kclHelpers.ts +++ b/src/lib/kclHelpers.ts @@ -126,11 +126,15 @@ export async function getCalculatedKclExpressionValue( export async function stringToKclExpression( value: string, - allowArrays?: boolean + allowArrays?: boolean, + instance?: ModuleType, + providedRustContext?: RustContext ) { const calculatedResult = await getCalculatedKclExpressionValue( value, - allowArrays + allowArrays, + instance, + providedRustContext ) if (err(calculatedResult) || 'errors' in calculatedResult) { return calculatedResult diff --git a/src/unitTestUtils.ts b/src/unitTestUtils.ts index c44ed86a337..9bcf18b5293 100644 --- a/src/unitTestUtils.ts +++ b/src/unitTestUtils.ts @@ -9,6 +9,16 @@ import { import { createArrayExpression } from '@src/lang/create' import { findKwArg, findKwArgAny } from '@src/lang/util' import type { CallExpressionKw, Expr } from '@src/lang/wasm' +import { join } from 'path' +import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' +import { ConnectionManager } from '@src/network/connectionManager' +import RustContext from '@src/lib/rustContext' +import { SceneInfra } from '@src/clientSideScene/sceneInfra' +import EditorManager from '@src/editor/manager' +import CodeManager from '@src/lang/codeManager' +import { KclManager } from '@src/lang/KclSingleton' +import { err, reportRejection } from '@src/lib/trap' +import env from '@src/env' /** * Throw x if it's an Error. Only use this in tests. @@ -40,3 +50,49 @@ export function findAngleLengthPair(call: CallExpressionKw): Expr | undefined { return createArrayExpression([angle, lengthLike]) } } + +export async function buildTheWorldAndConnectToEngine() { + const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') + const instance = await loadAndInitialiseWasmInstance(WASM_PATH) + const engineCommandManager = new ConnectionManager() + const rustContext = new RustContext(engineCommandManager, instance) + const sceneInfra = new SceneInfra(engineCommandManager) + const editorManager = new EditorManager(engineCommandManager) + const codeManager = new CodeManager({ editorManager }) + const kclManager = new KclManager(engineCommandManager, { + rustContext, + codeManager, + editorManager, + sceneInfra, + }) + editorManager.kclManager = kclManager + editorManager.codeManager = codeManager + engineCommandManager.kclManager = kclManager + engineCommandManager.codeManager = codeManager + engineCommandManager.sceneInfra = sceneInfra + engineCommandManager.rustContext = rustContext + await new Promise((resolve) => { + engineCommandManager + .start({ + token: env().VITE_KITTYCAD_API_TOKEN || '', + width: 256, + height: 256, + setStreamIsReady: () => { + console.log('no op for a unit test') + }, + callbackOnUnitTestingConnection: () => { + resolve(true) + }, + }) + .catch(reportRejection) + }) + return { + instance, + engineCommandManager, + rustContext, + sceneInfra, + editorManager, + codeManager, + kclManager, + } +} From aa801f6d4fa86fd02b17d1ffd95819d68dc1e04c Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 9 Oct 2025 15:05:25 -0500 Subject: [PATCH 13/13] chore: trying to make the transforms.spec.ts sheesh --- src/integration.spec.ts | 8 - src/lang/langHelpers.ts | 5 +- src/lang/modifyAst/addEdgeTreatment.spec.ts | 16 +- src/lang/modifyAst/transforms.spec.ts | 392 +++++++++++++++----- src/lib/rustContext.ts | 4 + src/unitTestUtils.ts | 2 +- 6 files changed, 320 insertions(+), 107 deletions(-) diff --git a/src/integration.spec.ts b/src/integration.spec.ts index 13305bd489f..9bf0fac337f 100644 --- a/src/integration.spec.ts +++ b/src/integration.spec.ts @@ -2,7 +2,6 @@ import { type Artifact, assertParse, type CodeRef, - type PathToNode, type Program, recast, type Name, @@ -14,13 +13,6 @@ import { enginelessExecutor } from '@src/lib/testHelpers' import { err, reportRejection } from '@src/lib/trap' import { stringToKclExpression } from '@src/lib/kclHelpers' import type { Node } from '@rust/kcl-lib/bindings/Node' -import { - addAppearance, - addClone, - addRotate, - addScale, - addTranslate, -} from '@src/lang/modifyAst/transforms' import { modifyAstWithTagsForSelection } from '@src/lang/modifyAst/tagManagement' import { engineCommandManager, diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index 7d00133f5ed..6c448d97382 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -8,6 +8,7 @@ import { emptyExecState, kclLint } from '@src/lang/wasm' import { EXECUTE_AST_INTERRUPT_ERROR_STRING } from '@src/lib/constants' import type RustContext from '@src/lib/rustContext' import { jsAppSettings } from '@src/lib/settings/settingsUtils' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' import { REJECTED_TOO_EARLY_WEBSOCKET_MESSAGE } from '@src/network/utils' import type { EditorView } from 'codemirror' @@ -171,12 +172,14 @@ function handleExecuteError(e: any): ExecutionResult { export async function lintAst({ ast, sourceCode, + instance, }: { ast: Program sourceCode: string + instance?: ModuleType }): Promise> { try { - const discovered_findings = await kclLint(ast) + const discovered_findings = await kclLint(ast, instance) return discovered_findings.map((lint) => { let actions const suggestion = lint.suggestion diff --git a/src/lang/modifyAst/addEdgeTreatment.spec.ts b/src/lang/modifyAst/addEdgeTreatment.spec.ts index 093d72f2cb7..7bf22c756b4 100644 --- a/src/lang/modifyAst/addEdgeTreatment.spec.ts +++ b/src/lang/modifyAst/addEdgeTreatment.spec.ts @@ -9,7 +9,7 @@ import { recast, type SourceRange, } from '@src/lang/wasm' -import { err, reportRejection } from '@src/lib/trap' +import { err } from '@src/lib/trap' import { topLevelRange } from '@src/lang/util' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { isOverlap } from '@src/lib/utils' @@ -24,22 +24,18 @@ import { hasValidEdgeTreatmentSelection, modifyAstWithEdgeTreatmentAndTag, } from '@src/lang/modifyAst/addEdgeTreatment' -import { KclManager } from '@src/lang/KclSingleton' +import type { KclManager } from '@src/lang/KclSingleton' import { join } from 'path' import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' import type { ModuleType } from '@src/lib/wasm_lib_wrapper' -import { ConnectionManager } from '@src/network/connectionManager' -import RustContext from '@src/lib/rustContext' -import { SceneInfra } from '@src/clientSideScene/sceneInfra' -import EditorManager from '@src/editor/manager' -import CodeManager from '@src/lang/codeManager' +import type { ConnectionManager } from '@src/network/connectionManager' +import type EditorManager from '@src/editor/manager' +import type CodeManager from '@src/lang/codeManager' import { createLiteral } from '@src/lang/create' import type { Selection, Selections } from '@src/machines/modelingSharedTypes' -import env from '@src/env' - const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') +const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') import { buildTheWorldAndConnectToEngine } from '@src/unitTestUtils' - describe('addEdgeTreatment', () => { const runGetPathToExtrudeForSegmentSelectionTest = async ( code: string, diff --git a/src/lang/modifyAst/transforms.spec.ts b/src/lang/modifyAst/transforms.spec.ts index f0672740b97..a82482b98a1 100644 --- a/src/lang/modifyAst/transforms.spec.ts +++ b/src/lang/modifyAst/transforms.spec.ts @@ -1,23 +1,33 @@ -import { Selections, Selection } from "@src/machines/modelingSharedTypes" -import { Artifact, assertParse, CodeRef, PathToNode, recast, type Program } from "@src/lang/wasm" -import { ModuleType } from "@src/lib/wasm_lib_wrapper" -import { KclManager } from "@src/lang/KclSingleton" -import { addTranslate } from "./transforms" +import type { Selections, Selection } from '@src/machines/modelingSharedTypes' +import type { Artifact, CodeRef, PathToNode } from '@src/lang/wasm' +import { assertParse, recast, type Program } from '@src/lang/wasm' +import type { ModuleType } from '@src/lib/wasm_lib_wrapper' +import type { KclManager } from '@src/lang/KclSingleton' +import { + addTranslate, + addRotate, + addClone, + addAppearance, +} from '@src/lang/modifyAst/transforms' import { stringToKclExpression } from '@src/lib/kclHelpers' -import { err} from '@src/lib/trap' +import { err } from '@src/lib/trap' import { enginelessExecutor } from '@src/lib/testHelpers' import type { Node } from '@rust/kcl-lib/bindings/Node' -import { loadAndInitialiseWasmInstance } from '@src/lang/wasmUtilsNode' -import { join } from 'path' -const WASM_PATH = join(process.cwd(), 'public/kcl_wasm_lib_bg.wasm') import { buildTheWorldAndConnectToEngine } from '@src/unitTestUtils' -import RustContext from "@src/lib/rustContext" -import { - addScale, -} from '@src/lang/modifyAst/transforms' - -async function getKclCommandValue(value: string, instance: ModuleType, rustContext: RustContext) { - const result = await stringToKclExpression(value, undefined, instance, rustContext) +import type RustContext from '@src/lib/rustContext' +import { addScale } from '@src/lang/modifyAst/transforms' + +async function getKclCommandValue( + value: string, + instance: ModuleType, + rustContext: RustContext +) { + const result = await stringToKclExpression( + value, + undefined, + instance, + rustContext + ) if (err(result) || 'errors' in result) { throw new Error(`Couldn't create kcl expression`) } @@ -25,8 +35,16 @@ async function getKclCommandValue(value: string, instance: ModuleType, rustConte return result } -async function runNewAstAndCheckForSweep(ast: Node, rustContext: RustContext) { - const { artifactGraph } = await enginelessExecutor(ast, undefined, undefined, rustContext) +async function runNewAstAndCheckForSweep( + ast: Node, + rustContext: RustContext +) { + const { artifactGraph } = await enginelessExecutor( + ast, + undefined, + undefined, + rustContext + ) const sweepArtifact = artifactGraph.values().find((a) => a.type === 'sweep') expect(sweepArtifact).toBeDefined() } @@ -47,7 +65,11 @@ function createSelectionFromPathArtifact( } } -async function getAstAndArtifactGraph(code: string, instance: ModuleType, kclManager: KclManager) { +async function getAstAndArtifactGraph( + code: string, + instance: ModuleType, + kclManager: KclManager +) { const ast = assertParse(code, instance) await kclManager.executeAst({ ast }) const { @@ -59,8 +81,16 @@ async function getAstAndArtifactGraph(code: string, instance: ModuleType, kclMan return { ast, artifactGraph, operations, variables } } -async function getAstAndSketchSelections(code: string, instance: ModuleType, kclManager: KclManager) { - const { ast, artifactGraph } = await getAstAndArtifactGraph(code, instance, kclManager) +async function getAstAndSketchSelections( + code: string, + instance: ModuleType, + kclManager: KclManager +) { + const { ast, artifactGraph } = await getAstAndArtifactGraph( + code, + instance, + kclManager + ) const artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path') if (artifacts.length === 0) { throw new Error('Artifact not found in the graph') @@ -71,7 +101,12 @@ async function getAstAndSketchSelections(code: string, instance: ModuleType, kcl describe('transforms.test.ts', () => { describe('Testing addTranslate', () => { - async function runAddTranslateTest(code: string, instance: ModuleType, kclManager: KclManager, rustContext: RustContext) { + async function runAddTranslateTest( + code: string, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, @@ -92,7 +127,8 @@ describe('transforms.test.ts', () => { } it('should add a standalone translate call on sweep selection', async () => { - const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1)` @@ -103,13 +139,19 @@ extrude001 = extrude(profile001, length = 1)` z = 3, global = true, )` - const newCode = await runAddTranslateTest(code, instance, kclManager, rustContext) + const newCode = await runAddTranslateTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) engineCommandManager.tearDown() }) it('should push a call in pipe if selection was in variable-less pipe', async () => { - const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1)` @@ -119,12 +161,23 @@ extrude001 = extrude(profile001, length = 1)` z = 3, global = true, )` - const newCode = await runAddTranslateTest(code, instance, kclManager, rustContext) + const newCode = await runAddTranslateTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) -engineCommandManager.tearDown() + engineCommandManager.tearDown() }) - async function runEditTranslateTest(code: string, nodeToEdit: PathToNode, instance: ModuleType, kclManager: KclManager, rustContext: RustContext) { + async function runEditTranslateTest( + code: string, + nodeToEdit: PathToNode, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, @@ -146,7 +199,8 @@ engineCommandManager.tearDown() } it('should edit a call with variable if og selection was a variable sweep', async () => { -const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1) @@ -171,13 +225,20 @@ translate( [3, 'index'], ['expression', 'ExpressionStatement'], ] - const newCode = await runEditTranslateTest(code, nodeToEdit, instance, kclManager, rustContext) + const newCode = await runEditTranslateTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) -engineCommandManager.tearDown() + engineCommandManager.tearDown() }) it('should edit a call in pipe if og selection was in pipe', async () => { -const { instance, kclManager, engineCommandManager, rustContext} = await buildTheWorldAndConnectToEngine() + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1) @@ -199,14 +260,25 @@ const { instance, kclManager, engineCommandManager, rustContext} = await buildTh ['body', 'PipeExpression'], [3, 'index'], ] - const newCode = await runEditTranslateTest(code, nodeToEdit, instance, kclManager, rustContext) + const newCode = await runEditTranslateTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) -engineCommandManager.tearDown() + engineCommandManager.tearDown() }) }) describe('Testing addScale', () => { - async function runAddScaleTest(code: string, instance: ModuleType, kclManager: KclManager, rustContext: RustContext) { + async function runAddScaleTest( + code: string, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, @@ -227,6 +299,8 @@ engineCommandManager.tearDown() } it('should add a standalone call on sweep selection', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1)` @@ -237,11 +311,19 @@ extrude001 = extrude(profile001, length = 1)` z = 3, global = true, )` - const newCode = await runAddScaleTest(code) + const newCode = await runAddScaleTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) + engineCommandManager.tearDown() }) it('should push a call in pipe if selection was in variable-less pipe', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1)` @@ -251,32 +333,46 @@ extrude001 = extrude(profile001, length = 1)` z = 3, global = true, )` - const newCode = await runAddScaleTest(code) + const newCode = await runAddScaleTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) + engineCommandManager.tearDown() }) - async function runEditScaleTest(code: string, nodeToEdit: PathToNode) { + async function runEditScaleTest( + code: string, + nodeToEdit: PathToNode, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, sketches: objects, - } = await getAstAndSketchSelections(code) + } = await getAstAndSketchSelections(code, instance, kclManager) const result = addScale({ ast, artifactGraph, objects, - x: await getKclCommandValue('4'), - y: await getKclCommandValue('5'), - z: await getKclCommandValue('6'), + x: await getKclCommandValue('4', instance, rustContext), + y: await getKclCommandValue('5', instance, rustContext), + z: await getKclCommandValue('6', instance, rustContext), global: false, nodeToEdit, }) if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) } it('should edit a scale call with variable if og selection was a variable sweep', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1) @@ -301,11 +397,20 @@ scale( [3, 'index'], ['expression', 'ExpressionStatement'], ] - const newCode = await runEditScaleTest(code, nodeToEdit) + const newCode = await runEditScaleTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) + engineCommandManager.tearDown() }) it('should edit a call in pipe if og selection was in pipe', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1) @@ -327,35 +432,49 @@ scale( ['body', 'PipeExpression'], [3, 'index'], ] - const newCode = await runEditScaleTest(code, nodeToEdit) + const newCode = await runEditScaleTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) + engineCommandManager.tearDown() }) // TODO: missing multi-objects test }) describe('Testing addRotate', () => { - async function runAddRotateTest(code: string) { + async function runAddRotateTest( + code: string, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, sketches: objects, - } = await getAstAndSketchSelections(code) + } = await getAstAndSketchSelections(code, instance, kclManager) const result = addRotate({ ast, artifactGraph, objects, - roll: await getKclCommandValue('10'), - pitch: await getKclCommandValue('20'), - yaw: await getKclCommandValue('30'), + roll: await getKclCommandValue('10', instance, rustContext), + pitch: await getKclCommandValue('20', instance, rustContext), + yaw: await getKclCommandValue('30', instance, rustContext), global: true, }) if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) } it('should add a standalone call on sweep selection', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1)` @@ -366,11 +485,19 @@ extrude001 = extrude(profile001, length = 1)` yaw = 30, global = true, )` - const newCode = await runAddRotateTest(code) + const newCode = await runAddRotateTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) + engineCommandManager.tearDown() }) it('should push a call in pipe if selection was in variable-less pipe', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1)` @@ -380,32 +507,46 @@ extrude001 = extrude(profile001, length = 1)` yaw = 30, global = true, )` - const newCode = await runAddRotateTest(code) + const newCode = await runAddRotateTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) + engineCommandManager.tearDown() }) - async function runEditRotateTest(code: string, nodeToEdit: PathToNode) { + async function runEditRotateTest( + code: string, + nodeToEdit: PathToNode, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, sketches: objects, - } = await getAstAndSketchSelections(code) + } = await getAstAndSketchSelections(code, instance, kclManager) const result = addRotate({ ast, artifactGraph, objects, - roll: await getKclCommandValue('40'), - pitch: await getKclCommandValue('50'), - yaw: await getKclCommandValue('60'), + roll: await getKclCommandValue('40', instance, rustContext), + pitch: await getKclCommandValue('50', instance, rustContext), + yaw: await getKclCommandValue('60', instance, rustContext), global: false, nodeToEdit, }) if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) } it('should edit a call with variable if og selection was a variable sweep', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1) @@ -430,11 +571,20 @@ rotate( [3, 'index'], ['expression', 'ExpressionStatement'], ] - const newCode = await runEditRotateTest(code, nodeToEdit) + const newCode = await runEditRotateTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) + engineCommandManager.tearDown() }) it('should edit a call in pipe if og selection was in pipe', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1) @@ -456,20 +606,32 @@ rotate( ['body', 'PipeExpression'], [3, 'index'], ] - const newCode = await runEditRotateTest(code, nodeToEdit) + const newCode = await runEditRotateTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) + engineCommandManager.tearDown() }) // TODO: missing multi-objects test }) describe('Testing addClone', () => { - async function runAddCloneTest(code: string) { + async function runAddCloneTest( + code: string, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, sketches: objects, - } = await getAstAndSketchSelections(code) + } = await getAstAndSketchSelections(code, instance, kclManager) const result = addClone({ ast, artifactGraph, @@ -477,27 +639,40 @@ rotate( variableName: 'yoyoyo', }) if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) } it('should add a standalone call on sweep selection', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1)` const expectedNewLine = `yoyoyo = clone(extrude001)` - const newCode = await runAddCloneTest(code) + const newCode = await runAddCloneTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) + engineCommandManager.tearDown() }) }) describe('Testing addAppearance', () => { - async function runAddAppearanceTest(code: string) { + async function runAddAppearanceTest( + code: string, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, sketches: objects, - } = await getAstAndSketchSelections(code) + } = await getAstAndSketchSelections(code, instance, kclManager) const result = addAppearance({ ast, artifactGraph, @@ -505,45 +680,63 @@ extrude001 = extrude(profile001, length = 1)` color: '#FF0000', }) if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) } const box = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1)` it('should add a standalone call on sweep selection', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const expectedNewLine = `appearance(extrude001, color = '#FF0000')` - const newCode = await runAddAppearanceTest(box) + const newCode = await runAddAppearanceTest( + box, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(box + '\n' + expectedNewLine) + engineCommandManager.tearDown() }) it('should push a call in pipe if selection was in variable-less pipe', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1)` const expectedNewLine = ` |> appearance(color = '#FF0000')` - const newCode = await runAddAppearanceTest(code) + const newCode = await runAddAppearanceTest( + code, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(code + '\n' + expectedNewLine) + engineCommandManager.tearDown() }) it('should add a call with metalness and roughness', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const { artifactGraph, ast, sketches: objects, - } = await getAstAndSketchSelections(box) + } = await getAstAndSketchSelections(box, instance, kclManager) const result = addAppearance({ ast, artifactGraph, objects, color: '#FF0000', - metalness: await getKclCommandValue('1'), - roughness: await getKclCommandValue('2'), + metalness: await getKclCommandValue('1', instance, rustContext), + roughness: await getKclCommandValue('2', instance, rustContext), }) if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - const newCode = recast(result.modifiedAst) + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + const newCode = recast(result.modifiedAst, instance) expect(newCode).toContain(`${box} appearance( extrude001, @@ -551,14 +744,21 @@ appearance( metalness = 1, roughness = 2, )`) + engineCommandManager.tearDown() }) - async function runEditAppearanceTest(code: string, nodeToEdit: PathToNode) { + async function runEditAppearanceTest( + code: string, + nodeToEdit: PathToNode, + instance: ModuleType, + kclManager: KclManager, + rustContext: RustContext + ) { const { artifactGraph, ast, sketches: objects, - } = await getAstAndSketchSelections(code) + } = await getAstAndSketchSelections(code, instance, kclManager) const result = addAppearance({ ast, artifactGraph, @@ -567,11 +767,13 @@ appearance( nodeToEdit, }) if (err(result)) throw result - await runNewAstAndCheckForSweep(result.modifiedAst) - return recast(result.modifiedAst) + await runNewAstAndCheckForSweep(result.modifiedAst, rustContext) + return recast(result.modifiedAst, instance) } it('should edit a call with variable if og selection was a variable sweep', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) extrude001 = extrude(profile001, length = 1) @@ -585,11 +787,20 @@ appearance(extrude001, color = '#00FF00')` [3, 'index'], ['expression', 'ExpressionStatement'], ] - const newCode = await runEditAppearanceTest(code, nodeToEdit) + const newCode = await runEditAppearanceTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) - }) + engineCommandManager.tearDown() + }, 30_000) it('should edit a call in pipe if og selection was in pipe', async () => { + const { instance, kclManager, engineCommandManager, rustContext } = + await buildTheWorldAndConnectToEngine() const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) |> extrude(length = 1) @@ -605,8 +816,15 @@ appearance(extrude001, color = '#00FF00')` ['body', 'PipeExpression'], [3, 'index'], ] - const newCode = await runEditAppearanceTest(code, nodeToEdit) + const newCode = await runEditAppearanceTest( + code, + nodeToEdit, + instance, + kclManager, + rustContext + ) expect(newCode).toContain(expectedNewCode) + engineCommandManager.tearDown() }) // TODO: missing multi-objects test diff --git a/src/lib/rustContext.ts b/src/lib/rustContext.ts index 79f92d922ff..0a1cf5eafe5 100644 --- a/src/lib/rustContext.ts +++ b/src/lib/rustContext.ts @@ -74,6 +74,10 @@ export default class RustContext { return ctxInstance } + getRustInstance() { + return this.rustInstance || undefined + } + createFromInstance(instance: ModuleType) { this.rustInstance = instance diff --git a/src/unitTestUtils.ts b/src/unitTestUtils.ts index 9bcf18b5293..89e37b342e6 100644 --- a/src/unitTestUtils.ts +++ b/src/unitTestUtils.ts @@ -17,7 +17,7 @@ import { SceneInfra } from '@src/clientSideScene/sceneInfra' import EditorManager from '@src/editor/manager' import CodeManager from '@src/lang/codeManager' import { KclManager } from '@src/lang/KclSingleton' -import { err, reportRejection } from '@src/lib/trap' +import { reportRejection } from '@src/lib/trap' import env from '@src/env' /**