Merge branch 'franknoirot/4088/decouple-homeMachine' into franknoirot/4088/create-file-url

This commit is contained in:
Frank Noirot
2024-10-15 13:07:29 -07:00
32 changed files with 1141 additions and 112 deletions

View File

@ -25,6 +25,7 @@ jobs:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps:
- uses: actions/checkout@v4
@ -53,20 +54,31 @@ jobs:
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: |
echo "$NOTES" > release-notes.md
cat release-notes.md
- uses: actions/upload-artifact@v3
with:
name: prepared-files
path: |
package.json
src/wasm-lib/pkg/wasm_lib*
release-notes.md
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- id: export_notes
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
- uses: actions/upload-artifact@v3
with:
@ -107,6 +119,7 @@ jobs:
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
mkdir src/wasm-lib/pkg
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
cp prepared-files/release-notes.md release-notes.md
- name: Sync node version and setup cache
uses: actions/setup-node@v4
@ -192,7 +205,7 @@ jobs:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
NOTES: ${{ needs.prepare-files.outputs.notes }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}

View File

@ -83520,6 +83520,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -87076,6 +87118,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -90636,6 +90720,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -115048,6 +115174,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -118997,6 +119165,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -122553,6 +122763,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -126107,6 +126359,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},

View File

@ -82,6 +82,78 @@ Raise a number to a power.
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -1097,15 +1097,16 @@ test(
page.on('console', console.log)
// Constants and locators
const projectLinks = page.getByTestId('project-link')
// expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible()
await createProject({ name: 'project-000', page, returnHome: true })
await expect(
page.getByTestId('project-link').getByText('project-000')
).toBeVisible()
await expect(projectLinks.getByText('project-000')).toBeVisible()
await page.getByTestId('project-link').getByText('project-000').click()
await projectLinks.getByText('project-000').click()
await u.waitForPageLoad()
@ -1153,7 +1154,7 @@ extrude001 = extrude(200, sketch001)`)
for (let i = 1; i <= 10; i++) {
const name = `project-${i.toString().padStart(3, '0')}`
await createProject({ name, page, returnHome: true })
await expect(page.getByText(name)).toBeVisible()
await expect(projectLinks.getByText(name)).toBeVisible()
}
await electronApp.close()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -73,3 +73,5 @@ publish:
- provider: generic
url: https://dl.zoo.dev/releases/modeling-app
channel: latest
releaseInfo:
releaseNotesFile: release-notes.md

2
interface.d.ts vendored
View File

@ -73,7 +73,7 @@ export interface IElectronAPI {
callback: (value: { version: string }) => void
) => Electron.IpcRenderer
onUpdateDownloaded: (
callback: (value: string) => void
callback: (value: { version: string; releaseNotes: string }) => void
) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void

View File

@ -425,6 +425,34 @@
]
}
},
"/metrics": {
"get": {
"operationId": "get_metrics",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"title": "String",
"type": "string"
}
}
},
"description": "successful operation"
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
},
"summary": "List available machines and their statuses",
"tags": [
"hidden"
]
}
},
"/ping": {
"get": {
"operationId": "ping",
@ -492,6 +520,13 @@
}
},
"tags": [
{
"description": "Hidden API endpoints that should not show up in the docs.",
"externalDocs": {
"url": "https://docs.zoo.dev/api/machines"
},
"name": "hidden"
},
{
"description": "Utilities for making parts and discovering machines.",
"externalDocs": {

View File

@ -0,0 +1,153 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { ToastUpdate } from './ToastUpdate'
describe('ToastUpdate tests', () => {
const testData = {
version: '0.255.255',
files: [
{
url: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
size: 141277345,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.zip',
sha512:
'b+ugdg7A4LhYYJaFkPRxh1RvmGGMlPJJj7inkLg9PwRtCnR9ePMlktj2VRciXF1iLh59XW4bLc4dK1dFQHMULA==',
size: 135278259,
},
{
url: 'Zoo Modeling App-0.255.255-x64-mac.dmg',
sha512:
'gCUqww05yj8OYwPiTq6bo5GbkpngSbXGtenmDD7+kUm0UyVK8WD3dMAfQJtGNG5HY23aHCHe9myE2W4mbZGmiQ==',
size: 146004232,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.dmg',
sha512:
'ND871ayf81F1ZT+iWVLYTc2jdf/Py6KThuxX2QFWz14ebmIbJPL07lNtxQOexOFiuk0MwRhlCy1RzOSG1b9bmw==',
size: 140021522,
},
],
path: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
releaseNotes:
'## Some markdown release notes\n\n- This is a list item\n- This is another list item\n\n```javascript\nconsole.log("Hello, world!")\n```\n',
releaseDate: '2024-10-09T11:57:59.133Z',
} as const
test('Happy path: renders the toast with good data', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={testData.releaseNotes}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.getByTestId('release-notes')
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(restartButton).toBeEnabled()
fireEvent.click(restartButton)
expect(onRestart.mock.calls).toHaveLength(1)
expect(dismissButton).toBeEnabled()
fireEvent.click(dismissButton)
expect(onDismiss.mock.calls).toHaveLength(1)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(releaseNotes).toHaveTextContent('Release notes')
const releaseNotesListItems = screen.getAllByRole('listitem')
expect(releaseNotesListItems.map((el) => el.textContent)).toEqual([
'This is a list item',
'This is another list item',
])
})
test('Happy path: renders the breaking changes notice', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- This is a list item
- This is another list item with a breaking change
- This is a list item
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const releaseNotes = screen.getByText('Release notes', {
selector: 'summary',
})
const listItemContents = screen
.getAllByRole('listitem')
.map((el) => el.textContent)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(listItemContents).toEqual([
'This is a list item',
'This is another list item with a breaking change',
'This is a list item',
])
})
test('Missing release notes: renders the toast without release notes', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={''}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.queryByText(/release notes/i, {
selector: 'details > summary',
})
const releaseNotesListItem = screen.queryByRole('listitem', {
name: /this is a list item/i,
})
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(releaseNotes).not.toBeInTheDocument()
expect(releaseNotesListItem).not.toBeInTheDocument()
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
})

View File

@ -1,14 +1,23 @@
import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
export function ToastUpdate({
version,
releaseNotes,
onRestart,
onDismiss,
}: {
version: string
releaseNotes?: string
onRestart: () => void
onDismiss: () => void
}) {
const containsBreakingChanges = releaseNotes
?.toLocaleLowerCase()
.includes('breaking')
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -19,7 +28,7 @@ export function ToastUpdate({
>
v{version}
</span>
<span className="ml-4 text-md text-bold">
<p className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '}
<a
@ -32,15 +41,39 @@ export function ToastUpdate({
>
here on GitHub.
</a>
</span>
</p>
</div>
{releaseNotes && (
<details
className="my-4 border border-chalkboard-30 dark:border-chalkboard-60 rounded"
open={containsBreakingChanges}
data-testid="release-notes"
>
<summary className="p-2 select-none cursor-pointer">
Release notes
{containsBreakingChanges && (
<strong className="text-destroy-50"> (Breaking changes)</strong>
)}
</summary>
<div
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
gfm: true,
breaks: true,
sanitize: true,
}),
}}
></div>
</details>
)}
<div className="flex justify-between gap-8">
<ActionButton
Element="button"
iconStart={{
icon: 'arrowRotateRight',
}}
name="Restart app now"
name="restart"
onClick={onRestart}
>
Restart app now
@ -50,9 +83,10 @@ export function ToastUpdate({
iconStart={{
icon: 'checkmark',
}}
name="Got it"
name="dismiss"
onClick={() => {
toast.dismiss()
onDismiss()
}}
>
Got it

View File

@ -293,6 +293,24 @@ code {
which lets you use them with @apply in your CSS, and get
autocomplete in classNames in your JSX.
*/
.parsed-markdown ul,
.parsed-markdown ol {
@apply list-outside pl-4 lg:pl-8 my-2;
}
.parsed-markdown ul li {
@apply list-disc;
}
.parsed-markdown li p {
@apply inline;
}
.parsed-markdown code {
@apply px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 text-chalkboard-80;
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
}
}
#code-mirror-override .cm-scroller,

View File

@ -70,15 +70,17 @@ if (isDesktop()) {
id: AUTO_UPDATER_TOAST_ID,
})
})
window.electron.onUpdateDownloaded((version: string) => {
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message)
toast.custom(
ToastUpdate({
version,
releaseNotes,
onRestart: () => {
window.electron.appRestart()
},
onDismiss: () => {},
}),
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
)

View File

@ -620,7 +620,7 @@ describe('Testing button states', () => {
it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
})
it('hould return false when body exists and not a segment is selected', async () => {
it('should return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false)
})
})

View File

@ -1,5 +1,7 @@
import {
CallExpression,
Expr,
Identifier,
ObjectExpression,
PathToNode,
Program,
@ -27,7 +29,7 @@ import {
sketchLineHelperMap,
} from '../std/sketch'
import { err, trap } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
import { Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import {
ArtifactGraph,
@ -66,7 +68,10 @@ export function modifyAstCloneWithFilletAndTag(
const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
const extrudeToTagsMap: Map<
PathToNode,
Array<{ tag: string; selectionType: string }>
> = new Map()
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selectionRange of selection.codeBasedSelections) {
@ -74,6 +79,7 @@ export function modifyAstCloneWithFilletAndTag(
codeBasedSelections: [selectionRange],
otherSelections: [],
}
const selectionType = singleSelection.codeBasedSelections[0].type
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
@ -89,6 +95,7 @@ export function modifyAstCloneWithFilletAndTag(
)
if (err(tagResult)) return tagResult
const { tag } = tagResult
const tagInfo = { tag, selectionType }
// Group tags by their corresponding extrude node
const extrudeKey = JSON.stringify(pathToExtrudeNode)
@ -96,23 +103,29 @@ export function modifyAstCloneWithFilletAndTag(
if (lookupMap.has(extrudeKey)) {
const existingPath = lookupMap.get(extrudeKey)
if (!existingPath) return new Error('Path to extrude node not found.')
extrudeToTagsMap.get(existingPath)?.push(tag)
extrudeToTagsMap.get(existingPath)?.push(tagInfo)
} else {
lookupMap.set(extrudeKey, pathToExtrudeNode)
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
}
}
// Step 2: Apply fillet(s) for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags
const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const tagCalls = tagInfos.map(({ tag, selectionType }) => {
return getEdgeTagCall(tag, selectionType)
})
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radiusValue,
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
tags: createArrayExpression(tagCalls),
}),
createPipeSubstitution(),
])
@ -144,7 +157,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tags[0]
firstTag
)
pathToFilletNodes.push(pathToFilletNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
@ -165,7 +178,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tags[0]
firstTag
)
pathToFilletNodes.push(pathToFilletNode)
} else {
@ -276,6 +289,21 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag }
}
function getEdgeTagCall(
tag: string,
selectionType: string
): Identifier | CallExpression {
let tagCall: Expr = createIdentifier(tag)
// Modify the tag based on selectionType
if (selectionType === 'edge') {
tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall])
} else if (selectionType === 'adjacent-edge') {
tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall])
}
return tagCall
}
function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
@ -311,7 +339,7 @@ function locateExtrudeDeclarator(
function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
tag: Identifier | CallExpression
): PathToNode {
let pathToFilletObj: PathToNode = []
let inFillet = false
@ -347,12 +375,30 @@ function getPathToNodeOfFilletLiteral(
]
}
function hasTag(node: ObjectExpression, tag: string): boolean {
function hasTag(
node: ObjectExpression,
tag: Identifier | CallExpression
): boolean {
return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) => element.type === 'Identifier' && element.name === tag
)
// if selection is a base edge:
if (tag.type === 'Identifier') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag.name
)
}
// if selection is an adjacent or opposite edge:
if (tag.type === 'CallExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'CallExpression' &&
element.callee.name === tag.callee.name && // edge location
element.arguments[0].type === 'Identifier' &&
tag.arguments[0].type === 'Identifier' &&
element.arguments[0].name === tag.arguments[0].name // tag name
)
}
}
return false
})
@ -383,7 +429,7 @@ export const hasValidFilletSelection = ({
ast: Program
code: string
}) => {
// case 0: check if there is anything filletable in the scene
// check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
@ -394,65 +440,88 @@ export const hasValidFilletSelection = ({
})
if (!extrudeExists) return false
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
// check if nothing is selected
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
// check if selection is last string in code
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
return true
}
// selection exists:
for (const selection of selectionRanges.codeBasedSelections) {
// check if all selections are in sketchLineHelperMap
const path = getNodePathFromSourceRange(ast, selection.range)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
path,
'CallExpression'
)
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
// Add check whether the tag exists at all:
if (!(segmentNode.node.arguments.length === 3)) return true
// If the tag exists, check if it is already filleted
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
}
} else {
if (err(segmentNode)) return false
if (segmentNode.node.type !== 'CallExpression') {
return false
}
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
return false
}
}
return canFilletSelection(selectionRanges)
// check if selection is extruded
// TODO: option 1 : extrude is in the sketch pipe
// option 2: extrude is outside the sketch pipe
const extrudeExists = hasSketchPipeBeenExtruded(selection, ast)
if (err(extrudeExists)) {
return false
}
if (!extrudeExists) {
return false
}
// check if tag exists for the selection
let tagExists = false
let tag = ''
traverse(segmentNode.node, {
enter(node) {
if (node.type === 'TagDeclarator') {
tagExists = true
tag = node.value
}
},
})
// check if tag is used in fillet
if (tagExists) {
// create tag call
let tagCall: Expr = getEdgeTagCall(tag, selection.type)
// check if tag is used in fillet
let inFillet = false
let tagUsedInFillet = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (hasTag(node, tagCall)) {
tagUsedInFillet = true
}
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
if (tagUsedInFillet) {
return false
}
}
}
return true
}
type EdgeTypes =

View File

@ -9,7 +9,7 @@ import {
getConstraintLevelFromSourceRange,
} from './sketchcombos'
import { ToolTip } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Selection, Selections } from 'lib/selections'
import { err } from 'lib/trap'
import { enginelessExecutor } from '../../lib/testHelpers'
@ -96,6 +96,86 @@ function makeSelections(
}
describe('testing transformAstForSketchLines for equal length constraint', () => {
describe(`should always reorder selections to have the base selection first`, () => {
const inputScript = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([5, 5], %)
|> line([-2, 5], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
const expectedModifiedScript = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([5, 5], %, $seg01)
|> angledLine([112, segLen(seg01)], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
const selectLine = (script: string, lineNumber: number): Selection => {
const lines = script.split('\n')
const codeBeforeLine = lines.slice(0, lineNumber).join('\n').length
const line = lines.find((_, i) => i === lineNumber)
if (!line) {
throw new Error(
`line index ${lineNumber} not found in test sample, friend`
)
}
const start = codeBeforeLine + line.indexOf('|> ' + 5)
const range: [number, number] = [start, start]
return {
type: 'default',
range,
}
}
async function applyTransformation(
inputCode: string,
selectionRanges: Selections['codeBasedSelections']
) {
const ast = parse(inputCode)
if (err(ast)) return Promise.reject(ast)
const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)),
ast,
'equalLength'
)
const transformedSelection = makeSelections(selectionRanges)
const newAst = transformSecondarySketchLinesTagFirst({
ast,
selectionRanges: transformedSelection,
transformInfos,
programMemory: execState.memory,
})
if (err(newAst)) return Promise.reject(newAst)
const newCode = recast(newAst.modifiedAst)
return newCode
}
it(`Should reorder when user selects first-to-last`, async () => {
const selectionRanges: Selections['codeBasedSelections'] = [
selectLine(inputScript, 3),
selectLine(inputScript, 4),
]
const newCode = await applyTransformation(inputScript, selectionRanges)
expect(newCode).toBe(expectedModifiedScript)
})
it(`Should reorder when user selects last-to-first`, async () => {
const selectionRanges: Selections['codeBasedSelections'] = [
selectLine(inputScript, 4),
selectLine(inputScript, 3),
]
const newCode = await applyTransformation(inputScript, selectionRanges)
expect(newCode).toBe(expectedModifiedScript)
})
})
const inputScript = `myVar = 3
myVar2 = 5
myVar3 = 6

View File

@ -1559,7 +1559,15 @@ export function transformSecondarySketchLinesTagFirst({
}
| Error {
// let node = structuredClone(ast)
const primarySelection = selectionRanges.codeBasedSelections[0].range
// We need to sort the selections by their start position
// so that we can process them in dependency order and not write invalid KCL.
const sortedCodeBasedSelections =
selectionRanges.codeBasedSelections.toSorted(
(a, b) => a.range[0] - b.range[0]
)
const primarySelection = sortedCodeBasedSelections[0].range
const secondarySelections = sortedCodeBasedSelections.slice(1)
const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName)
if (err(_tag)) return _tag
@ -1569,7 +1577,7 @@ export function transformSecondarySketchLinesTagFirst({
ast: modifiedAst,
selectionRanges: {
...selectionRanges,
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
codeBasedSelections: secondarySelections,
},
referencedSegmentRange: primarySelection,
transformInfos,

View File

@ -55,6 +55,23 @@ export interface paths {
patch?: never
trace?: never
}
'/metrics': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** List available machines and their statuses */
get: operations['get_metrics']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/ping': {
parameters: {
query?: never
@ -278,6 +295,28 @@ export interface operations {
'5XX': components['responses']['Error']
}
}
get_metrics: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody?: never
responses: {
/** @description successful operation */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': string
}
}
'4XX': components['responses']['Error']
'5XX': components['responses']['Error']
}
}
ping: {
parameters: {
query?: never

View File

@ -288,7 +288,10 @@ app.on('ready', () => {
autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info)
mainWindow?.webContents.send('update-downloaded', info.version)
mainWindow?.webContents.send('update-downloaded', {
version: info.version,
releaseNotes: info.releaseNotes,
})
})
ipcMain.handle('app.restart', () => {

View File

@ -16,11 +16,12 @@ const startDeviceFlow = (host: string): Promise<string> =>
ipcRenderer.invoke('startDeviceFlow', host)
const loginWithDeviceFlow = (): Promise<string> =>
ipcRenderer.invoke('loginWithDeviceFlow')
const onUpdateDownloaded = (
callback: (value: { version: string; releaseNotes: string }) => void
) => ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateDownloadStart = (
callback: (value: { version: string }) => void
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
const onUpdateDownloaded = (callback: (value: string) => void) =>
ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateError = (callback: (value: Error) => void) =>
ipcRenderer.on('update-error', (_event, value) => callback(value))
const appRestart = () => ipcRenderer.invoke('app.restart')

View File

@ -1533,16 +1533,16 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.70"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kcl-lib"
version = "0.2.20"
version = "0.2.21"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1617,7 +1617,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.12"
version = "0.1.13"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -3907,9 +3907,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [
"cfg-if",
"once_cell",
@ -3918,9 +3918,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [
"bumpalo",
"log",
@ -3933,9 +3933,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d"
dependencies = [
"cfg-if",
"futures-core",
@ -3946,9 +3946,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -3956,9 +3956,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
@ -3969,9 +3969,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "wasm-lib"

View File

@ -20,7 +20,7 @@ tokio = { version = "1.40.0", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42"
wasm-bindgen-futures = "0.4.44"
[dev-dependencies]
anyhow = "1"
@ -36,9 +36,9 @@ uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
futures = "0.3.31"
js-sys = "0.3.69"
js-sys = "0.3.72"
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen-futures = { version = "0.4.41", features = ["futures-core-03-stream"] }
wasm-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream"] }
wasm-streams = "0.4.1"
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.12"
version = "0.1.13"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.20"
version = "0.2.21"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -53,11 +53,11 @@ winnow = "0.6.18"
zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" }
js-sys = { version = "0.3.72" }
tokio = { version = "1.40.0", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42"
wasm-bindgen-futures = "0.4.44"
web-sys = { version = "0.3.69", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@ -2778,6 +2778,12 @@ impl BinaryExpression {
BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::Eq => (left == right).into(),
BinaryOperator::Neq => (left != right).into(),
BinaryOperator::Gt => (left > right).into(),
BinaryOperator::Gte => (left >= right).into(),
BinaryOperator::Lt => (left < right).into(),
BinaryOperator::Lte => (left <= right).into(),
};
Ok(KclValue::UserVal(UserVal {
@ -2861,6 +2867,30 @@ pub enum BinaryOperator {
#[serde(rename = "^")]
#[display("^")]
Pow,
/// Are two numbers equal?
#[serde(rename = "==")]
#[display("==")]
Eq,
/// Are two numbers not equal?
#[serde(rename = "!=")]
#[display("!=")]
Neq,
/// Is left greater than right
#[serde(rename = ">")]
#[display(">")]
Gt,
/// Is left greater than or equal to right
#[serde(rename = ">=")]
#[display(">=")]
Gte,
/// Is left less than right
#[serde(rename = "<")]
#[display("<")]
Lt,
/// Is left less than or equal to right
#[serde(rename = "<=")]
#[display("<=")]
Lte,
}
/// Mathematical associativity.
@ -2889,6 +2919,12 @@ impl BinaryOperator {
BinaryOperator::Div => *b"div",
BinaryOperator::Mod => *b"mod",
BinaryOperator::Pow => *b"pow",
BinaryOperator::Eq => *b"eqq",
BinaryOperator::Neq => *b"neq",
BinaryOperator::Gt => *b"gtr",
BinaryOperator::Gte => *b"gte",
BinaryOperator::Lt => *b"ltr",
BinaryOperator::Lte => *b"lte",
}
}
@ -2899,6 +2935,8 @@ impl BinaryOperator {
BinaryOperator::Add | BinaryOperator::Sub => 11,
BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12,
BinaryOperator::Pow => 13,
Self::Gt | Self::Gte | Self::Lt | Self::Lte => 9,
Self::Eq | Self::Neq => 8,
}
}
@ -2908,6 +2946,7 @@ impl BinaryOperator {
match self {
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left,
Self::Pow => Associativity::Right,
Self::Gt | Self::Gte | Self::Lt | Self::Lte | Self::Eq | Self::Neq => Associativity::Left, // I don't know if this is correct
}
}
}

View File

@ -303,6 +303,12 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
"*" => BinaryOperator::Mul,
"%" => BinaryOperator::Mod,
"^" => BinaryOperator::Pow,
"==" => BinaryOperator::Eq,
"!=" => BinaryOperator::Neq,
">" => BinaryOperator::Gt,
">=" => BinaryOperator::Gte,
"<" => BinaryOperator::Lt,
"<=" => BinaryOperator::Lte,
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
@ -3705,6 +3711,8 @@ const my14 = 4 ^ 2 - 3 ^ 2 * 2
5
}"#
);
snapshot_test!(be, "let x = 3 == 3");
snapshot_test!(bf, "let x = 3 != 3");
snapshot_test!(bg, r#"x = 4"#);
}

View File

@ -0,0 +1,65 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 14,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 14,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 14,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "x",
"digest": null
},
"init": {
"type": "BinaryExpression",
"type": "BinaryExpression",
"start": 8,
"end": 14,
"operator": "==",
"left": {
"type": "Literal",
"type": "Literal",
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"right": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 14,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -0,0 +1,65 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 14,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 14,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 14,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "x",
"digest": null
},
"init": {
"type": "BinaryExpression",
"type": "BinaryExpression",
"start": 8,
"end": 14,
"operator": "!=",
"left": {
"type": "Literal",
"type": "Literal",
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"right": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 14,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -27,7 +27,7 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> {
'.' => alt((number, double_period, period)),
'#' => hash,
'$' => dollar,
'!' => bang,
'!' => alt((operator, bang)),
' ' | '\t' | '\n' => whitespace,
_ => alt((operator, keyword,type_, word))
}
@ -90,7 +90,7 @@ fn word(i: &mut Located<&str>) -> PResult<Token> {
fn operator(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = alt((
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^",
">=", "<=", "==", "=>", "!=", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^",
))
.with_span()
.parse_next(i)?;
@ -1522,6 +1522,18 @@ const things = "things"
assert_tokens(expected, actual);
}
#[test]
fn not_eq() {
let actual = lexer("!=").unwrap();
let expected = vec![Token {
token_type: TokenType::Operator,
value: "!=".to_owned(),
start: 0,
end: 2,
}];
assert_eq!(actual, expected);
}
#[test]
fn test_unrecognized_token() {
let actual = lexer("12 ; 8").unwrap();

View File

@ -0,0 +1,13 @@
assert(3 == 3, "equality")
assert(3.0 == 3.0, "equality of floats")
assert(3 != 4, "non-equality")
assert(3.0 != 4.0, "non-equality of floats")
assert(3 < 4, "lt")
assert(3 <= 4, "lte but actually lt")
assert(4 <= 4, "lte but actually eq")
assert(4 > 3, "gt")
assert(4 >= 3, "gte but actually gt")
assert(3 >= 3, "gte but actually eq")
assert(0.0 == 0.0, "equality of zero")
assert(0.0 == -0.0, "equality of zero and neg zero")

View File

@ -0,0 +1 @@
assert(3 == 3 == 3, "this should not compile")

View File

@ -57,6 +57,7 @@ async fn run_fail(code: &str) -> KclError {
gen_test!(property_of_object);
gen_test!(index_of_array);
gen_test!(comparisons);
gen_test_fail!(
invalid_index_str,
"semantic: Only integers >= 0 can be used as the index of an array, but you're using a string"
@ -99,4 +100,5 @@ gen_test!(if_else);
// if_else_no_expr,
// "syntax: blocks inside an if/else expression must end in an expression"
// );
gen_test_fail!(comparisons_multiple, "syntax: Invalid number: true");
gen_test!(add_lots);