Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ forge/pardinus-cli/out
forge/pardinus-cli/classes
*.icloud

*quotes_in_*filename.frg
*quotes_in_*filename.frg

# Node.js / Playwright e2e tests
forge/e2e/node_modules/
forge/e2e/playwright-report/
forge/e2e/test-results/
5 changes: 0 additions & 5 deletions e2e/README.md

This file was deleted.

16 changes: 0 additions & 16 deletions e2e/fail_assertion.frg

This file was deleted.

21 changes: 0 additions & 21 deletions e2e/failing_decls_example.frg

This file was deleted.

19 changes: 0 additions & 19 deletions e2e/failing_example.frg

This file was deleted.

18 changes: 0 additions & 18 deletions e2e/passing_example.frg

This file was deleted.

19 changes: 0 additions & 19 deletions e2e/sat_pass_unsat_pass_2_runs.frg

This file was deleted.

39 changes: 39 additions & 0 deletions forge/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Forge E2E Tests

End-to-end tests for Forge/Sterling integration using Playwright.

## Setup

```bash
cd e2e
npm install
npx playwright install # Install browser binaries
```

## Running Tests

```bash
# Run all tests (headless)
npm test

# Run tests with browser visible
npm run test:headed

# Run tests in debug mode (step through)
npm run test:debug

# Run tests with Playwright UI
npm run test:ui
```

## Test Structure

- `fixtures/` - Forge files used as test inputs
- `helpers/` - Test utilities (Forge process runner, etc.)
- `tests/` - Playwright test specs

## Notes

- Tests run serially (single worker) to avoid port conflicts
- Each test spawns its own Forge process for isolation
- Timeout is set to 60s to allow for solver time
11 changes: 11 additions & 0 deletions forge/e2e/fixtures/multi-instance.frg
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#lang forge

-- Model that produces multiple distinct instances for testing navigation
sig Node {
edge: lone Node
}

-- With 3 nodes and lone edge, we get many possible configurations
multiRun: run {
some edge
} for exactly 3 Node
23 changes: 23 additions & 0 deletions forge/e2e/fixtures/simple-graph.frg
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#lang forge

-- Simple test model for e2e testing
sig Node {
edges: set Node
}

pred connected {
all n1, n2: Node | n1 in n2.^edges or n2 in n1.^edges or n1 = n2
}

pred someEdges {
some edges
}

simpleRun: run {
someEdges
} for exactly 3 Node

connectedRun: run {
connected
someEdges
} for exactly 3 Node
12 changes: 12 additions & 0 deletions forge/e2e/fixtures/unsat-model.frg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#lang forge

-- Model with unsatisfiable constraints for testing unsat display
sig Node {
edges: set Node
}

-- This is unsatisfiable: can't have exactly 3 nodes where each has no edges but some edges exist
unsatRun: run {
no edges
some edges
} for exactly 3 Node
133 changes: 133 additions & 0 deletions forge/e2e/helpers/forge-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { spawn, ChildProcess } from 'child_process';
import * as path from 'path';

export interface ForgeInstance {
process: ChildProcess;
sterlingUrl: string;
staticPort: number;
providerPort: number;
cleanup: () => void;
}

import { Page, expect } from '@playwright/test';

/**
* Helper to select and execute a run in Sterling.
* Sterling requires selecting from dropdown AND clicking "Run" button.
*/
export async function selectAndRunCommand(page: Page, runName: string): Promise<void> {
// Select from the combobox
const runSelect = page.getByRole('combobox');
await expect(runSelect).toBeVisible({ timeout: 10000 });
await runSelect.selectOption({ label: runName });

// Click the Run button
const runButton = page.getByRole('button', { name: 'Run', exact: true });
await expect(runButton).toBeVisible({ timeout: 5000 });
await runButton.click();

// Wait for instance to load - SVG appears in main area
await expect(page.locator('svg').first()).toBeVisible({ timeout: 20000 });
}

/**
* Starts a Forge file and waits for Sterling to be ready.
* Returns the Sterling URL and a cleanup function.
*/
export async function startForge(
forgeFilePath: string,
options: { timeout?: number; providerPort?: number; staticPort?: number } = {}
): Promise<ForgeInstance> {
const timeout = options.timeout ?? 30000;
const providerPort = options.providerPort ?? 18000 + Math.floor(Math.random() * 1000);
const staticPort = options.staticPort ?? 19000 + Math.floor(Math.random() * 1000);
const forgePath = path.resolve(__dirname, '../../', forgeFilePath);

return new Promise((resolve, reject) => {
// Pass options for headless mode with known ports
const proc = spawn(
'racket',
[
forgePath,
'-O', 'run_sterling', 'headless',
'-O', 'sterling_port', String(providerPort),
'-O', 'sterling_static_port', String(staticPort),
],
{
cwd: path.resolve(__dirname, '../..'),
stdio: ['pipe', 'pipe', 'pipe'],
}
);

let stdout = '';
let stderr = '';
let resolved = false;

const timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true;
proc.kill();
reject(new Error(`Timeout waiting for Sterling to start. stdout: ${stdout}, stderr: ${stderr}`));
}
}, timeout);

proc.stdout?.on('data', (data: Buffer) => {
const chunk = data.toString();
stdout += chunk;

// Look for: "Opening Forge menu in Sterling (static server port=XXXX)"
// This confirms the server is ready (we already know the port)
const serverReady = stdout.includes('static server port=');

if (serverReady && !resolved) {
// Wait a moment for the server to be fully ready
setTimeout(() => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);

const sterlingUrl = `http://127.0.0.1:${staticPort}/?${providerPort}`;

resolve({
process: proc,
sterlingUrl,
staticPort,
providerPort,
cleanup: () => {
// Send newline to stdin to trigger Forge shutdown
proc.stdin?.write('\n');
proc.stdin?.end();
// Force kill after a short delay if still running
setTimeout(() => {
if (!proc.killed) {
proc.kill('SIGKILL');
}
}, 2000);
},
});
}
}, 500);
}
});

proc.stderr?.on('data', (data: Buffer) => {
stderr += data.toString();
});

proc.on('error', (err) => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
reject(new Error(`Failed to start Forge: ${err.message}`));
}
});

proc.on('exit', (code) => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
reject(new Error(`Forge exited unexpectedly with code ${code}. stdout: ${stdout}, stderr: ${stderr}`));
}
});
});
}
Loading