PureYAMLStreaming/.github/workflows/wasm-site.yml

358 lines
18 KiB
YAML

name: WASM Test Site
on:
push:
branches: [main]
paths:
- "Package.swift"
- "Package.resolved"
- "Sources/PureYAMLStreaming/**"
- "Sources/PureYAMLStreamingWasmSmoke/**"
- "WasmSite/**"
- "scripts/build-wasm-site.sh"
- ".github/workflows/wasm-site.yml"
pull_request:
paths:
- "Package.swift"
- "Package.resolved"
- "Sources/PureYAMLStreaming/**"
- "Sources/PureYAMLStreamingWasmSmoke/**"
- "WasmSite/**"
- "scripts/build-wasm-site.sh"
- ".github/workflows/wasm-site.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pureyaml-streaming-wasm-site-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install build tools
run: |
sudo apt-get update
sudo apt-get install -y binaryen gzip libcurl4-openssl-dev
- name: Install swiftly
run: |
curl -L https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz > swiftly.tar.gz
tar zxf swiftly.tar.gz
./swiftly init --quiet-shell-followup --assume-yes --skip-install
- name: Install Swift 6.3.2 and Wasm SDK
run: |
. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"
swiftly install --use 6.3.2
swiftly run swift sdk install https://download.swift.org/swift-6.3.2-release/wasm-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_wasm.artifactbundle.tar.gz --checksum a61f0584c93283589f8b2f42db05c1f9a182b506c2957271402992655591dd7c +6.3.2
- name: Build WASM site
run: |
. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"
bash scripts/build-wasm-site.sh
mkdir -p site
cp WasmSite/index.html site/index.html
cp WasmSite/main.js site/main.js
cp WasmSite/coi-serviceworker.js site/coi-serviceworker.js
cp WasmSite/stream-worker.js site/stream-worker.js
cp WasmSite/style.css site/style.css
cp WasmSite/pureyaml-streaming-wasm-smoke.wasm.gz site/pureyaml-streaming-wasm-smoke.wasm.gz
cp -R WasmSite/vendor site/vendor
mkdir -p site/fixtures
python3 - <<'PY'
from pathlib import Path
fixture = Path("site/fixtures/generated-large.yaml")
payload = "x" * 1024
with fixture.open("w", encoding="utf-8") as handle:
for index in range(3000):
handle.write("---\n")
handle.write(f"name: ci-doc-{index}\n")
handle.write(f"enabled: {'true' if index % 2 == 0 else 'false'}\n")
handle.write(f"payload: {payload}\n")
PY
ls -lh site
- name: Install Playwright Chromium
run: |
mkdir -p .build/playwright-ci
cd .build/playwright-ci
npm init -y
npm install playwright-chromium
npx playwright install --with-deps chromium
- name: Test site locally
run: |
python3 -m http.server 8766 --bind 127.0.0.1 --directory site &
server_pid=$!
trap 'kill "$server_pid"' EXIT
node <<'NODE'
const { chromium } = require('./.build/playwright-ci/node_modules/playwright-chromium');
async function gotoIsolated(page, url) {
await page.goto(url);
for (let attempt = 0; attempt < 6; attempt += 1) {
if (await page.evaluate(() => globalThis.crossOriginIsolated === true)) {
return;
}
await page.waitForTimeout(500);
await page.reload();
}
throw new Error('Expected page to become crossOriginIsolated');
}
function parsedStdout(output) {
const stdoutMatch = output.match(/stdout:\n([\s\S]*?)\n\nstderr:/);
return JSON.parse(stdoutMatch?.[1] || '{}');
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await gotoIsolated(page, 'http://127.0.0.1:8766/?v=' + Date.now());
const optionCount = await page.locator('#sampleSelect option').count();
if (optionCount < 10) {
throw new Error(`Expected at least 10 public YAML samples, got ${optionCount}`);
}
if (await page.locator('#streamButton').isDisabled()) {
throw new Error('Expected true streaming button to be enabled');
}
await page.fill('#urlInput', 'http://127.0.0.1:8766/fixtures/generated-large.yaml');
await page.click('#runButton');
await page.waitForFunction(
() => document.querySelector('#status')?.textContent === 'Passed',
null,
{ timeout: 60000 }
);
const output = await page.locator('#output').textContent();
const bytes = await page.locator('#bytesMetric').textContent();
const parse = await page.locator('#parseMetric').textContent();
const throughput = await page.locator('#throughputMetric').textContent();
const documents = await page.locator('#documentsMetric').textContent();
const runLog = await page.locator('#runLog').textContent();
const streamDocument = await page.locator('#streamDocument').textContent();
const parsed = parsedStdout(output);
if (parsed.ok !== true) throw new Error(output);
if (parsed.transport !== 'SwiftWASIHTTPClient.HostHTTPClient') throw new Error(output);
if (parsed.httpStatusCode !== 200) throw new Error(output);
if (parsed.documentCount !== 3000) throw new Error(output);
if (parsed.firstName !== 'ci-doc-0') throw new Error(output);
if (parsed.lastName !== 'ci-doc-2999') throw new Error(output);
if (!Number.isFinite(parsed.parseMilliseconds) || parsed.parseMilliseconds <= 0) {
throw new Error(`Expected positive parseMilliseconds, got ${parsed.parseMilliseconds}`);
}
if (!Number.isFinite(parsed.chunkCount) || parsed.chunkCount <= 1) {
throw new Error(`Expected multiple Swift chunks, got ${parsed.chunkCount}`);
}
if (parsed.chunkSize !== 31) {
throw new Error(`Expected chunkSize 31, got ${parsed.chunkSize}`);
}
if (parsed.maxChunkByteCount > parsed.chunkSize) {
throw new Error(`Expected max chunk <= chunkSize, got ${parsed.maxChunkByteCount}`);
}
if (parsed.scannerDocumentSourceCount !== 3000) {
throw new Error(`Expected scanner to emit 3000 sources, got ${parsed.scannerDocumentSourceCount}`);
}
if (documents !== '3000') {
throw new Error(`Expected document metric 3000, got: ${documents}`);
}
if (bytes === '-' || parse === '-') {
throw new Error(`Expected benchmark metrics, got bytes=${bytes}, parse=${parse}`);
}
if (!throughput.includes('/s')) {
throw new Error(`Expected throughput metric, got: ${throughput}`);
}
if (!runLog.includes('Fetching YAML bytes') || !runLog.includes('SwiftWASIHTTPClient.HostHTTPClient') || !runLog.includes('Starting Swift parser.') || !runLog.includes('ChunkedUTF8Reader consumed') || !runLog.includes('Benchmark completed successfully.')) {
throw new Error(`Expected detailed run log, got: ${runLog}`);
}
if (!streamDocument.includes('name: ci-doc-0') || !streamDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected buffered document panel to show generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected buffered document panel syntax highlighting');
}
await page.click('#copyRunLogButton');
await page.waitForFunction(
() => document.querySelector('#copyRunLogButton')?.textContent === 'Copied',
null,
{ timeout: 5000 }
);
await page.click('#streamButton');
await page.waitForFunction(
() => document.querySelector('#status')?.textContent === 'Passed',
null,
{ timeout: 60000 }
);
const streamingOutput = await page.locator('#output').textContent();
const streamingRunLog = await page.locator('#runLog').textContent();
const streamingDocument = await page.locator('#streamDocument').textContent();
const streamingParsed = parsedStdout(streamingOutput);
if (streamingParsed.ok !== true) throw new Error(streamingOutput);
if (streamingParsed.transport !== 'SharedArrayBuffer fd_read stdin') throw new Error(streamingOutput);
if (streamingParsed.httpStatusCode !== 200) throw new Error(streamingOutput);
if (streamingParsed.documentCount !== 3000) throw new Error(streamingOutput);
if (streamingParsed.firstName !== 'ci-doc-0') throw new Error(streamingOutput);
if (streamingParsed.lastName !== 'ci-doc-2999') throw new Error(streamingOutput);
if (!Number.isFinite(streamingParsed.stdinReadCount) || streamingParsed.stdinReadCount <= 1) {
throw new Error(`Expected multiple stdin reads, got ${streamingParsed.stdinReadCount}`);
}
if (streamingParsed.maxStdinReadByteCount > streamingParsed.chunkSize) {
throw new Error(`Expected max stdin read <= chunkSize, got ${streamingParsed.maxStdinReadByteCount}`);
}
if (!streamingRunLog.includes('Streaming HTTP 200 response body into SharedArrayBuffer') || !streamingRunLog.includes('Network chunk') || !streamingRunLog.includes('Worker fd_read calls') || !streamingRunLog.includes('SharedArrayBuffer fd_read stdin')) {
throw new Error(`Expected true streaming run log, got: ${streamingRunLog}`);
}
if (!streamingDocument.includes('name: ci-doc-0') || !streamingDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected streaming document panel to append generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected streaming document panel syntax highlighting');
}
await browser.close();
})();
NODE
- name: Configure Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/configure-pages@v5
- name: Upload Pages artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: site
deploy:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: Install Playwright Chromium
run: |
mkdir -p .build/playwright-live
cd .build/playwright-live
npm init -y
npm install playwright-chromium
npx playwright install --with-deps chromium
- name: Validate live Pages benchmark
env:
PAGE_URL: ${{ steps.deployment.outputs.page_url }}
run: |
node <<'NODE'
const { chromium } = require('./.build/playwright-live/node_modules/playwright-chromium');
async function gotoIsolated(page, url) {
await page.goto(url);
for (let attempt = 0; attempt < 6; attempt += 1) {
if (await page.evaluate(() => globalThis.crossOriginIsolated === true)) {
return;
}
await page.waitForTimeout(500);
await page.reload();
}
throw new Error('Expected live page to become crossOriginIsolated');
}
function parsedStdout(output) {
const stdoutMatch = output.match(/stdout:\n([\s\S]*?)\n\nstderr:/);
return JSON.parse(stdoutMatch?.[1] || '{}');
}
(async () => {
const pageURL = process.env.PAGE_URL;
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await gotoIsolated(page, pageURL + '?v=' + Date.now());
await page.fill('#urlInput', new URL('fixtures/generated-large.yaml', pageURL).href);
await page.click('#runButton');
await page.waitForFunction(
() => document.querySelector('#status')?.textContent === 'Passed',
null,
{ timeout: 60000 }
);
const output = await page.locator('#output').textContent();
const throughput = await page.locator('#throughputMetric').textContent();
const runLog = await page.locator('#runLog').textContent();
const streamDocument = await page.locator('#streamDocument').textContent();
const parsed = parsedStdout(output);
if (parsed.transport !== 'SwiftWASIHTTPClient.HostHTTPClient') throw new Error(output);
if (parsed.httpStatusCode !== 200) throw new Error(output);
if (parsed.documentCount !== 3000) throw new Error(output);
if (parsed.firstName !== 'ci-doc-0') throw new Error(output);
if (parsed.lastName !== 'ci-doc-2999') throw new Error(output);
if (!Number.isFinite(parsed.parseMilliseconds) || parsed.parseMilliseconds <= 0) {
throw new Error(`Expected positive parseMilliseconds, got ${parsed.parseMilliseconds}`);
}
if (!Number.isFinite(parsed.chunkCount) || parsed.chunkCount <= 1) {
throw new Error(`Expected multiple Swift chunks, got ${parsed.chunkCount}`);
}
if (parsed.chunkSize !== 31) {
throw new Error(`Expected chunkSize 31, got ${parsed.chunkSize}`);
}
if (parsed.maxChunkByteCount > parsed.chunkSize) {
throw new Error(`Expected max chunk <= chunkSize, got ${parsed.maxChunkByteCount}`);
}
if (parsed.scannerDocumentSourceCount !== 3000) {
throw new Error(`Expected scanner to emit 3000 sources, got ${parsed.scannerDocumentSourceCount}`);
}
if (!throughput.includes('/s')) {
throw new Error(`Expected live throughput metric, got: ${throughput}`);
}
if (!runLog.includes('Fetching YAML bytes') || !runLog.includes('SwiftWASIHTTPClient.HostHTTPClient') || !runLog.includes('Starting Swift parser.') || !runLog.includes('ChunkedUTF8Reader consumed') || !runLog.includes('Benchmark completed successfully.')) {
throw new Error(`Expected live detailed run log, got: ${runLog}`);
}
if (!streamDocument.includes('name: ci-doc-0') || !streamDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected live buffered document panel to show generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected live buffered document panel syntax highlighting');
}
await page.click('#streamButton');
await page.waitForFunction(
() => document.querySelector('#status')?.textContent === 'Passed',
null,
{ timeout: 60000 }
);
const streamingOutput = await page.locator('#output').textContent();
const streamingRunLog = await page.locator('#runLog').textContent();
const streamingDocument = await page.locator('#streamDocument').textContent();
const streamingParsed = parsedStdout(streamingOutput);
if (streamingParsed.transport !== 'SharedArrayBuffer fd_read stdin') throw new Error(streamingOutput);
if (streamingParsed.httpStatusCode !== 200) throw new Error(streamingOutput);
if (streamingParsed.documentCount !== 3000) throw new Error(streamingOutput);
if (streamingParsed.firstName !== 'ci-doc-0') throw new Error(streamingOutput);
if (streamingParsed.lastName !== 'ci-doc-2999') throw new Error(streamingOutput);
if (!Number.isFinite(streamingParsed.stdinReadCount) || streamingParsed.stdinReadCount <= 1) {
throw new Error(`Expected multiple live stdin reads, got ${streamingParsed.stdinReadCount}`);
}
if (!streamingRunLog.includes('Streaming HTTP 200 response body into SharedArrayBuffer') || !streamingRunLog.includes('Network chunk') || !streamingRunLog.includes('Worker fd_read calls') || !streamingRunLog.includes('SharedArrayBuffer fd_read stdin')) {
throw new Error(`Expected live true streaming run log, got: ${streamingRunLog}`);
}
if (!streamingDocument.includes('name: ci-doc-0') || !streamingDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected live streaming document panel to append generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected live streaming document panel syntax highlighting');
}
await browser.close();
})();
NODE