Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into wip/jtulach/Autosc…
Browse files Browse the repository at this point in the history
…opedConstructors_8645
  • Loading branch information
JaroslavTulach committed Feb 29, 2024
2 parents 8266b52 + 97033a2 commit 774438c
Show file tree
Hide file tree
Showing 231 changed files with 6,691 additions and 3,697 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/bench-upload.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# This file is manually managed. It is used to upload benchmarks to to the
# https://github.com/enso-org/engine-benchmark-results repository.

name: Benchmarks upload
on:
workflow_run:
workflows: ["Benchmark Engine", "Benchmark Standard Libraries"]
types:
- completed
jobs:
upload-benchmarks:
name: Upload benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout enso repository
uses: actions/checkout@v4
with:
repository: enso-org/enso
path: enso
- name: Checkout engine-benchmark-results repository
uses: actions/checkout@v4
with:
repository: enso-org/engine-benchmark-results
path: engine-benchmark-results
token: ${{ secrets.ENSO_BENCHMARK_RESULTS_TOKEN }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
python3 \
python3-jinja2 \
python3-numpy \
python3-pandas
sudo apt-get install -y gh
- name: Set up git
run: |
git config --global user.email "[email protected]"
git config --global user.name "Enso CI Bot"
- name: Upload benchmarks
run: |
cd enso/tools/performance/engine-benchmarks
python3 website_regen.py \
-v \
--local-repo ${{ github.workspace }}/engine-benchmark-results
env:
GITHUB_TOKEN: ${{ secrets.ENSO_BENCHMARK_RESULTS_TOKEN }}
5 changes: 5 additions & 0 deletions app/gui2/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export async function goToGraph(page: Page) {
await expect(page.locator('.App')).toBeVisible()
// Wait until nodes are loaded.
await customExpect.toExist(locate.graphNode(page))
// Wait for position initialization
await expect(locate.graphNode(page).first()).toHaveCSS(
'transform',
'matrix(1, 0, 0, 1, -16, -16)',
)
}

// =================
Expand Down
45 changes: 43 additions & 2 deletions app/gui2/e2e/componentBrowser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import * as actions from './actions'
import * as customExpect from './customExpect'
import * as locate from './locate'

const ACCEPT_SUGGESTION_SHORTCUT = os.platform() === 'darwin' ? 'Meta+Enter' : 'Control+Enter'
const CONTROL_KEY = os.platform() === 'darwin' ? 'Meta' : 'Control'
const ACCEPT_SUGGESTION_SHORTCUT = `${CONTROL_KEY}+Enter`

async function deselectAllNodes(page: Page) {
await page.keyboard.press('Escape')
Expand Down Expand Up @@ -155,7 +156,7 @@ test('Editing existing nodes', async ({ page }) => {
const ADDED_PATH = '"/home/enso/Input.txt"'

// Start node editing
await locate.graphNodeIcon(node).click({ modifiers: ['Control'] })
await locate.graphNodeIcon(node).click({ modifiers: [CONTROL_KEY] })
await expect(locate.componentBrowser(page)).toBeVisible()
const input = locate.componentBrowserInput(page).locator('input')
await expect(input).toHaveValue('Data.read')
Expand All @@ -181,3 +182,43 @@ test('Editing existing nodes', async ({ page }) => {
await expect(node.locator('.WidgetToken')).toHaveText(['Data', '.', 'read'])
await expect(node.locator('.WidgetText')).not.toBeVisible()
})

test('Visualization preview: type-based visualization selection', async ({ page }) => {
await actions.goToGraph(page)
const nodeCount = await locate.graphNode(page).count()
await locate.addNewNodeButton(page).click()
await customExpect.toExist(locate.componentBrowser(page))
await customExpect.toExist(locate.componentBrowserEntry(page))
const input = locate.componentBrowserInput(page).locator('input')
await input.fill('4')
await expect(input).toHaveValue('4')
await customExpect.toExist(locate.jsonVisualization(page))
await input.fill('Table.ne')
await expect(input).toHaveValue('Table.ne')
// The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
// visualization is no longer selected.
await expect(locate.jsonVisualization(page)).not.toBeVisible()
await page.keyboard.press('Escape')
await expect(locate.componentBrowser(page)).not.toBeVisible()
await expect(locate.graphNode(page)).toHaveCount(nodeCount)
})

test('Visualization preview: user visualization selection', async ({ page }) => {
await actions.goToGraph(page)
const nodeCount = await locate.graphNode(page).count()
await locate.addNewNodeButton(page).click()
await customExpect.toExist(locate.componentBrowser(page))
await customExpect.toExist(locate.componentBrowserEntry(page))
const input = locate.componentBrowserInput(page).locator('input')
await input.fill('4')
await expect(input).toHaveValue('4')
await customExpect.toExist(locate.jsonVisualization(page))
await locate.showVisualizationSelectorButton(page).click()
await page.getByRole('button', { name: 'Table' }).click()
// The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
// visualization is no longer selected.
await expect(locate.jsonVisualization(page)).not.toBeVisible()
await page.keyboard.press('Escape')
await expect(locate.componentBrowser(page)).not.toBeVisible()
await expect(locate.graphNode(page)).toHaveCount(nodeCount)
})
6 changes: 6 additions & 0 deletions app/gui2/e2e/customExpect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ export function toExist(locator: Locator) {
export function toBeSelected(locator: Locator) {
return expect(locator).toHaveClass(/(?<=^| )selected(?=$| )/)
}

export module not {
export function toBeSelected(locator: Locator) {
return expect(locator).not.toHaveClass(/(?<=^| )selected(?=$| )/)
}
}
2 changes: 1 addition & 1 deletion app/gui2/e2e/locate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const toggleFullscreenButton = or(enterFullscreenButton, exitFullscreenBu
// === Nodes ===

declare const nodeLocatorBrand: unique symbol
type Node = Locator & { [nodeLocatorBrand]: never }
export type Node = Locator & { [nodeLocatorBrand]: never }

export function graphNode(page: Page | Locator): Node {
return page.locator('.GraphNode') as Node
Expand Down
58 changes: 58 additions & 0 deletions app/gui2/e2e/selectingNodes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { expect, test } from '@playwright/test'
import assert from 'assert'
import { nextTick } from 'vue'
import * as actions from './actions'
import * as customExpect from './customExpect'
import * as locate from './locate'

test('Selecting nodes by click', async ({ page }) => {
await actions.goToGraph(page)
const node1 = locate.graphNodeByBinding(page, 'five')
const node2 = locate.graphNodeByBinding(page, 'ten')
await customExpect.not.toBeSelected(node1)
await customExpect.not.toBeSelected(node2)

await locate.graphNodeIcon(node1).click()
await customExpect.toBeSelected(node1)
await customExpect.not.toBeSelected(node2)

await locate.graphNodeIcon(node2).click()
await customExpect.not.toBeSelected(node1)
await customExpect.toBeSelected(node2)

await page.waitForTimeout(600) // Avoid double clicks
await locate.graphNodeIcon(node1).click({ modifiers: ['Shift'] })
await customExpect.toBeSelected(node1)
await customExpect.toBeSelected(node2)

await locate.graphNodeIcon(node2).click()
await customExpect.not.toBeSelected(node1)
await customExpect.toBeSelected(node2)

await page.mouse.click(200, 200)
await customExpect.not.toBeSelected(node1)
await customExpect.not.toBeSelected(node2)
})

test('Selecting nodes by area drag', async ({ page }) => {
await actions.goToGraph(page)
const node1 = locate.graphNodeByBinding(page, 'five')
const node2 = locate.graphNodeByBinding(page, 'ten')
await customExpect.not.toBeSelected(node1)
await customExpect.not.toBeSelected(node2)

const node1BBox = await node1.locator('.selection').boundingBox()
const node2BBox = await node2.boundingBox()
assert(node1BBox)
assert(node2BBox)
await page.mouse.move(node1BBox.x - 50, node1BBox.y - 50)
await page.mouse.down()
await page.mouse.move(node1BBox.x - 49, node1BBox.y - 49)
await expect(page.locator('.SelectionBrush')).toBeVisible()
await page.mouse.move(node2BBox.x + node2BBox.width, node2BBox.y + node2BBox.height)
await customExpect.toBeSelected(node1)
await customExpect.toBeSelected(node2)
await page.mouse.up()
await customExpect.toBeSelected(node1)
await customExpect.toBeSelected(node2)
})
20 changes: 20 additions & 0 deletions app/gui2/mock/engine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as random from 'lib0/random'
import * as Ast from 'shared/ast'
import {
Builder,
EnsoUUID,
Expand Down Expand Up @@ -405,6 +406,25 @@ export const mockLSHandler: MockTransportData = async (method, data, transport)
sendVizData(data_.visualizationId, data_.visualizationConfig)
return
}
case 'executionContext/executeExpression': {
const data_ = data as {
executionContextId: ContextId
visualizationId: Uuid
expressionId: ExpressionId
expression: string
}
const { func, args } = Ast.analyzeAppLike(Ast.parse(data_.expression))
if (!(func instanceof Ast.PropertyAccess && func.lhs)) return
const visualizationConfig: VisualizationConfiguration = {
executionContextId: data_.executionContextId,
visualizationModule: func.lhs.code(),
expression: func.rhs.code(),
positionalArgumentsExpressions: args.map((ast) => ast.code()),
}
visualizationExprIds.set(data_.visualizationId, data_.expressionId)
sendVizData(data_.visualizationId, visualizationConfig)
return
}
case 'search/getSuggestionsDatabase':
return {
entries: mockDb.map((suggestion, id) => ({
Expand Down
1 change: 1 addition & 0 deletions app/gui2/mock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function guiConfig(app: App) {
},
window: {
topBarOffset: 96,
vibrancy: false,
},
authentication: {
enabled: true,
Expand Down
38 changes: 36 additions & 2 deletions app/gui2/shared/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ExternalId } from '../yjsModel'
import type { Module } from './mutableModule'
import type { SyncTokenId } from './token'
import type { AstId } from './tree'
import { Ast, MutableAst } from './tree'
import { App, Ast, Group, MutableAst, OprApp, Wildcard } from './tree'

export * from './mutableModule'
export * from './parse'
Expand All @@ -25,7 +25,8 @@ export function asOwned<T>(t: T): Owned<T> {
return t as Owned<T>
}

export type NodeChild<T = AstId | SyncTokenId> = { whitespace?: string | undefined; node: T }
export type NodeChild<T> = { whitespace?: string | undefined; node: T }
export type RawNodeChild = NodeChild<AstId> | NodeChild<SyncTokenId>

export function newExternalId(): ExternalId {
return random.uuidv4() as ExternalId
Expand Down Expand Up @@ -68,3 +69,36 @@ export function subtreeRoots(module: Module, ids: Set<AstId>): Set<AstId> {
}
return roots
}

function unwrapGroups(ast: Ast) {
while (ast instanceof Group && ast.expression) ast = ast.expression
return ast
}

/** Tries to recognize inputs that are semantically-equivalent to a sequence of `App`s, and returns the arguments
* identified and LHS of the analyzable chain.
*
* In particular, this function currently recognizes syntax used in visualization-preprocessor expressions.
*/
export function analyzeAppLike(ast: Ast): { func: Ast; args: Ast[] } {
const deferredOperands = new Array<Ast>()
while (
ast instanceof OprApp &&
ast.operator.ok &&
ast.operator.value.code() === '<|' &&
ast.lhs &&
ast.rhs
) {
deferredOperands.push(unwrapGroups(ast.rhs))
ast = unwrapGroups(ast.lhs)
}
deferredOperands.reverse()
const args = new Array<Ast>()
while (ast instanceof App) {
const deferredOperand = ast.argument instanceof Wildcard ? deferredOperands.pop() : undefined
args.push(deferredOperand ?? unwrapGroups(ast.argument))
ast = ast.function
}
args.reverse()
return { func: ast, args }
}
19 changes: 9 additions & 10 deletions app/gui2/shared/ast/mutableModule.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import * as random from 'lib0/random'
import * as Y from 'yjs'
import {
Token,
asOwned,
isTokenId,
newExternalId,
subtreeRoots,
type AstId,
type Owned,
type SyncTokenId,
} from '.'
import type { AstId, NodeChild, Owned, RawNodeChild, SyncTokenId } from '.'
import { Token, asOwned, isTokenId, newExternalId, subtreeRoots } from '.'
import { assert, assertDefined } from '../util/assert'
import type { SourceRangeEdit } from '../util/data/text'
import { defaultLocalOrigin, tryAsOrigin, type ExternalId, type Origin } from '../yjsModel'
Expand Down Expand Up @@ -38,6 +30,7 @@ export interface Module {
getToken(token: SyncTokenId): Token
getToken(token: SyncTokenId | undefined): Token | undefined
getAny(node: AstId | SyncTokenId): Ast | Token
getConcrete(child: RawNodeChild): NodeChild<Ast> | NodeChild<Token>
has(id: AstId): boolean
}

Expand Down Expand Up @@ -322,6 +315,12 @@ export class MutableModule implements Module {
return isTokenId(node) ? this.getToken(node) : this.get(node)
}

getConcrete(child: RawNodeChild): NodeChild<Ast> | NodeChild<Token> {
if (isTokenId(child.node))
return { whitespace: child.whitespace, node: this.getToken(child.node) }
else return { whitespace: child.whitespace, node: this.get(child.node) }
}

/** @internal Copy a node into the module, if it is bound to a different module. */
copyIfForeign<T extends MutableAst>(ast: Owned<T>): Owned<T>
copyIfForeign<T extends MutableAst>(ast: Owned<T> | undefined): Owned<T> | undefined {
Expand Down
Loading

0 comments on commit 774438c

Please sign in to comment.