Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
#!/bin/sh
# 干净容器验收;先上传归档、checksum、项目夹具和 test/dever-tests/fixtures/tls。
set -eu
cd /work
for tool in rustc cargo rustup gcc cc ld; do
if command -v "$tool"; then
echo "unexpected preinstalled compiler: $tool" >&2
exit 1
fi
done
test ! -f /usr/include/stdio.h
cat /etc/os-release
if [ "${1:-}" = --installed ]; then
mv "$2" /work/dever
else
archive=${1:-dever-0.0.1-linux-x86_64-development.tar.xz}
sha256sum -c "$archive.sha256"
python3 -m tarfile -e "$archive" --filter=data
fi
validation_path=$PATH
export PATH="/work/dever/bin:$PATH"
dever new hello
dever check hello
dever fmt hello --check
dever test hello
dever run hello -- hello.greeting.greet '{"name":"Dever"}' > hello.json
grep -F '"data":"Hello, Dever"' hello.json
dever build hello --output /work/hello-app
mv dever 'dever moved'
export PATH="/work/dever moved/bin:$validation_path"
dever build hello --output /work/hello-moved
./hello-moved hello.greeting.greet '{"name":"moved"}' > moved.json
grep -F '"data":"Hello, moved"' moved.json
mkdir http database
dever build fixtures/http-application --output /work/http/app
dever build fixtures/database --output /work/database/app
mkdir database/config
cat > database/config/setting.json <<'JSON'
{"database":{"default":{"type":"sqlite","path":"data/account.db","max_connections":1,"max_page_size":20}}}
JSON
./database/app demo.account.create '{"name":"offline"}' > created.json
grep -F '"name":"offline"' created.json
./database/app demo.account.get '{"id":1}' > fetched.json
cmp created.json fetched.json
if ./database/app demo.account.explode '{"name":"rollback","value":9223372036854775807}' > rollback.json 2> rollback.log; then
echo 'transaction fault unexpectedly succeeded' >&2
exit 1
fi
grep -F 'integer arithmetic fault' rollback.log
./database/app demo.account.count '{}' > count.json
grep -F '"data":1' count.json
# 全部运行库特性须在裁剪后的 vendor 中仍能离线解析和编译,包括 PostgreSQL、Job、Worker。
toolchain_root=$(dirname "$(dever skill path)")
python3 - "$toolchain_root" <<'PY'
import json
from pathlib import Path
import sys
import tomllib
runtime = Path(sys.argv[1]) / 'rust/src/runtime'
features = sorted(tomllib.loads((runtime / 'Cargo.toml').read_text())['features'])
project = Path('/work/runtime-all')
(project / 'src').mkdir(parents=True)
(project / 'src/main.rs').write_text('fn main() {}\n')
(project / 'Cargo.toml').write_text(
'[package]\nname = "all-runtime-features"\nversion = "0.0.0"\nedition = "2024"\n'
'[workspace]\n[dependencies]\n'
f'dever-runtime = {{ path = {json.dumps(str(runtime))}, features = {json.dumps(features)} }}\n'
)
PY
"$toolchain_root/toolchain/bin/cargo" build --offline --release \
--manifest-path /work/runtime-all/Cargo.toml --target-dir "$toolchain_root/rust/target"
"$toolchain_root/rust/target/release/all-runtime-features"
echo 'ALL_RUNTIME_FEATURES_PASS'
# 隐藏整个开发包后,正式二进制依然必须正常执行。
mv 'dever moved' hidden-toolchain
./hello-app hello.greeting.greet '{"name":"standalone"}' > standalone.json
grep -F '"data":"Hello, standalone"' standalone.json
echo 'BUILD_AND_DATABASE_PASS'
python3 /work/transport.py
wc -c /work/hello-app /work/http/app /work/database/app
echo 'CLEAN_DEVELOPMENT_PASS'
+60
View File
@@ -0,0 +1,60 @@
apiVersion: batch/v1
kind: Job
metadata:
name: dever-development-check
spec:
ttlSecondsAfterFinished: 86400
activeDeadlineSeconds: 7200
backoffLimit: 0
template:
metadata:
labels:
app: dever-development-check
spec:
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: check
image: docker.io/library/python@sha256:aaf08c5490d52cd92bc27142a5215b1e47355a5eaf9adfad2a0fbc022d0b5490
command:
- /bin/sh
- -ec
- 'while [ ! -f /work/start ]; do sleep 1; done; exec /bin/sh /work/check.sh'
env:
- name: CARGO_BUILD_JOBS
value: "4"
resources:
requests:
cpu: 250m
memory: 256Mi
ephemeral-storage: 4Gi
limits:
cpu: "4"
memory: 8Gi
ephemeral-storage: 12Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
volumeMounts:
- name: work
mountPath: /work
volumes:
- name: work
emptyDir: {}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: dever-development-check
spec:
podSelector:
matchLabels:
app: dever-development-check
policyTypes: [Egress]
egress: []
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# 真实下载与共享缓存验收;准备完成后由调用方关闭本 Job 的外网再跑通用开发验收。
set -euo pipefail
cd /work
export SHELL=/bin/bash
for certificate in root.pem server.pem server-key.pem; do
test -f "/work/tls/$certificate" || { echo "Missing TLS fixture: $certificate" >&2; exit 1; }
done
for tool in rustc cargo rustup gcc cc ld; do
if command -v "$tool"; then
echo "unexpected preinstalled compiler: $tool" >&2
exit 1
fi
done
python3 /work/server.py > server.log 2>&1 &
server=$!
finish() {
local code=$?
if (( code )); then
echo "ONLINE_VALIDATION_FAILED: $code" >&2
for log in retry.log first-run.err first-test.err wait.log http/server-False.log http/server-True.log server.log; do
if [[ -f $log ]]; then printf '\n%s\n' "$log"; tail -n 20 "$log"; fi
done
fi
kill "$server"
wait "$server" || true
}
trap finish EXIT
python3 - <<'PY'
import socket
import time
for attempt in range(100):
try:
with socket.create_connection(('127.0.0.1', 18080), timeout=1):
break
except ConnectionRefusedError:
time.sleep(0.1)
else:
raise RuntimeError('local release server did not start')
PY
url=http://127.0.0.1:18080
prefix='/work/install/dever tools'
export RUSTUP_DIST_SERVER=$url
touch /work/reject-environment
curl -fsSL "$url/install" | timeout 30 bash -s -- --prefix "$prefix" --release-url "$url" > install.log 2>&1
cat install.log
if grep -Ei 'rustc|cargo|private Rust' install.log; then exit 1; fi
test "$("$prefix/bin/dever" --version)" = 'dever 0.0.1'
"$prefix/bin/dever" new editing
"$prefix/bin/dever" check editing
"$prefix/bin/dever" fmt editing --check
test ! -e "$prefix/versions/0.0.1/toolchain/ready"
# 安装已返回且编辑命令可用;首次编译须报告真实下载失败,不能假成功。
if "$prefix/bin/dever" build editing --output /work/rejected-app > failed-build.log 2>&1; then
echo 'build unexpectedly succeeded without an environment' >&2
exit 1
fi
grep -F 'Dever environment preparation failed' failed-build.log
grep -F '404' "$prefix/versions/0.0.1/toolchain/prepare.log"
test ! -e /work/rejected-app
rm /work/reject-environment
touch /work/hold-environment
# 用真实归档前缀模拟上次断线已下载的内容,服务端记录并验证 Range 恢复。
native=$(basename /work/assets/dever-native-*.tar.xz)
head -c 65536 "/work/assets/$native" > "$prefix/cache/downloads/$native.part"
curl -fsSL "$url/install" | timeout 30 bash -s -- --prefix "$prefix" --release-url "$url" > retry.log 2>&1
"$prefix/bin/dever" run editing -- hello.greeting.greet '{"name":"background"}' > first-run.log 2> first-run.err &
running=$!
"$prefix/bin/dever" test editing > first-test.log 2> first-test.err &
testing=$!
bash assets/install --prefix "$prefix" --release-url "$url" --wait > wait.log 2>&1 &
waiting=$!
for attempt in {1..100}; do
if [[ -f /work/parallel-downloads ]]; then break; fi
sleep 0.1
done
test -f /work/parallel-downloads
kill -0 "$running" "$testing" "$waiting"
test ! -e "$prefix/versions/0.0.1/toolchain/ready"
if flock -n "$prefix/.prepare.lock" true; then
echo 'preparation lock was not held' >&2
exit 1
fi
echo BACKGROUND_EDITING_AND_PARALLEL_DOWNLOAD_PASS
rm /work/hold-environment
wait "$running"
wait "$testing"
wait "$waiting"
cat first-run.log first-run.err first-test.log
grep -F '"data":"Hello, background"' first-run.log
test "$(cat /work/resumed-offset)" = 65536
test -f "$prefix/versions/0.0.1/toolchain/ready"
if grep -Ei 'rustc|cargo|rust-std' first-run.err first-test.err wait.log; then exit 1; fi
echo RESUME_AND_CONCURRENT_BUILD_PASS
test "$(grep -c 'dever.*env' "$HOME/.bashrc")" -eq 1
first=$(find "$prefix/toolchains" -type f -printf '%P %s %T@\n' | sort | sha256sum)
curl -fsSL "$url/install" | bash -s -- --prefix "$prefix" --release-url "$url" --wait
second=$(find "$prefix/toolchains" -type f -printf '%P %s %T@\n' | sort | sha256sum)
test "$first" = "$second"
test "$(grep -c 'dever.*env' "$HOME/.bashrc")" -eq 1
# 校验失败不能切换现有命令,下载错误不得伪报成功。
active=$(readlink "$prefix/bin/dever")
cp assets/dever-0.0.1-linux-x86_64.tar.xz assets/dever-0.0.2-linux-x86_64.tar.xz
printf '%064d dever-0.0.2-linux-x86_64.tar.xz\n' 0 > assets/dever-0.0.2-linux-x86_64.tar.xz.sha256
if bash assets/install --prefix "$prefix" --release-url "$url" --version 0.0.2 > rejected.log 2>&1; then
echo 'corrupt release unexpectedly installed' >&2
exit 1
fi
grep -F FAILED rejected.log
test "$(readlink "$prefix/bin/dever")" = "$active"
test ! -e "$prefix/versions/0.0.2"
test -z "$(find "$prefix" -maxdepth 1 -name '.install.*' -type d -print)"
du -sk "$prefix" "$prefix/cache/cargo" "$prefix/toolchains"
echo ONLINE_INSTALL_PASS
touch /work/online-ready
while [ ! -f /work/offline-start ]; do sleep 1; done
bash /work/check.sh --installed "$prefix"
+44
View File
@@ -0,0 +1,44 @@
apiVersion: batch/v1
kind: Job
metadata:
name: dever-online-check
spec:
ttlSecondsAfterFinished: 86400
activeDeadlineSeconds: 7200
backoffLimit: 0
template:
metadata:
labels:
app: dever-online-check
spec:
automountServiceAccountToken: false
restartPolicy: Never
containers:
- name: check
image: git.dever.cc/platform/workspace@sha256:795ba3591832e16622bc949adae46fb907eacb54499ba3f80dae708361bc2752
command:
- /bin/sh
- -ec
- |
while [ ! -f /work/start ]; do sleep 1; done
exec bash /work/online.sh
env:
- name: CARGO_BUILD_JOBS
value: "4"
resources:
requests:
cpu: 250m
memory: 256Mi
ephemeral-storage: 4Gi
limits:
cpu: "4"
memory: 8Gi
ephemeral-storage: 12Gi
securityContext:
allowPrivilegeEscalation: false
volumeMounts:
- name: work
mountPath: /work
volumes:
- name: work
emptyDir: {}
+61
View File
@@ -0,0 +1,61 @@
"""仅为安装验收控制下载边界;工具链内容仍来自官方源。"""
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import threading
import time
WORK = Path('/work')
lock = threading.Lock()
active = 0
class Handler(SimpleHTTPRequestHandler):
def do_GET(self):
global active
environment = self.path.startswith(('/dist/', '/dever-native-'))
if environment and (WORK / 'reject-environment').exists():
self.send_error(404, 'intentional preparation failure')
return
archive = environment and self.path.endswith('.tar.xz')
if archive:
with lock:
active += 1
if active >= 2:
(WORK / 'parallel-downloads').touch()
try:
while (WORK / 'hold-environment').exists():
time.sleep(0.1)
self.download()
finally:
with lock:
active -= 1
else:
self.download()
def download(self):
if self.path.startswith('/dist/'):
self.send_response(302)
self.send_header('Location', 'https://static.rust-lang.org' + self.path)
self.end_headers()
return
resume = self.headers.get('Range')
if resume and self.path.startswith('/dever-native-'):
offset = int(resume.removeprefix('bytes=').removesuffix('-'))
path = Path(self.translate_path(self.path))
size = path.stat().st_size
(WORK / 'resumed-offset').write_text(str(offset))
self.send_response(206)
self.send_header('Content-Range', f'bytes {offset}-{size - 1}/{size}')
self.send_header('Content-Length', str(size - offset))
self.end_headers()
with path.open('rb') as source:
source.seek(offset)
self.copyfile(source, self.wfile)
return
super().do_GET()
ThreadingHTTPServer(('127.0.0.1', 18080), partial(Handler, directory='/work/assets')).serve_forever()
+52
View File
@@ -0,0 +1,52 @@
"""在验收容器的回环接口验证已脱离开发包的 HTTP/TLS 产物。"""
import http.client
import json
from pathlib import Path
import socket
import ssl
import subprocess
import time
root = Path("/work/http")
(root / "config").mkdir(exist_ok=True)
certificates = Path("/work/tls")
for encrypted in (False, True):
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
settings = {"http": {"host": "127.0.0.1", "port": port},
"sites": {"public": {"path": "", "hosts": ["localhost"]}}}
if encrypted:
settings["http"]["tls"] = {"certificate": str(certificates / "server.pem"),
"key": str(certificates / "server-key.pem")}
(root / "config/setting.json").write_text(json.dumps(settings))
with (root / f"server-{encrypted}.log").open("w") as log:
server = subprocess.Popen([str(root / "app")], stdout=log, stderr=log)
try:
deadline = time.monotonic() + 10
while True:
assert server.poll() is None, (root / f"server-{encrypted}.log").read_text()
try:
with socket.create_connection(("127.0.0.1", port), timeout=1):
break
except ConnectionRefusedError:
assert time.monotonic() < deadline, "HTTP server startup timed out"
time.sleep(0.05)
if encrypted:
context = ssl.create_default_context(cafile=str(certificates / "root.pem"))
client = http.client.HTTPSConnection("localhost", port, context=context, timeout=5)
else:
client = http.client.HTTPConnection("localhost", port, timeout=5)
try:
client.request("GET", "/demo/greeting/greet?name=Dever")
response = client.getresponse()
assert response.status == 200, response.status
assert json.load(response)["data"] == "Hello, Dever"
finally:
client.close()
finally:
server.terminate()
server.wait(timeout=10)
print("HTTP_TLS_PASS")
+11
View File
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBmTCCAT+gAwIBAgIUYx/Ad4tJiQ4uakAPbpXu5Senv2YwCgYIKoZIzj0EAwIw
GjEYMBYGA1UEAwwPRGV2ZXItVGVzdC1Sb290MB4XDTI2MDkxMjA4MTY1MFoXDTM2
MDkwOTA4MTY1MFowGjEYMBYGA1UEAwwPRGV2ZXItVGVzdC1Sb290MFkwEwYHKoZI
zj0CAQYIKoZIzj0DAQcDQgAECOux92VpsFwb2YOTRs6UATfp7HPm+JeCTMkJNXPo
rqLZa4Vi/71dLQzNFxxsXxqFyf4oBYoCD0XMEicxYUgnO6NjMGEwHQYDVR0OBBYE
FIo/Vdu7qm5dIikkwg6A0yRp1YzVMB8GA1UdIwQYMBaAFIo/Vdu7qm5dIikkwg6A
0yRp1YzVMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49
BAMCA0gAMEUCICmmB/eG055dU7F9OP7K9BUxOkFnYNtzIpYJfd0VwG6LAiEA4oiP
pnep4mLGHAnvsucvvqDA5SVdHzYh22OYPdSgNeY=
-----END CERTIFICATE-----
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYl2wcdhzZMZrQxSV
ClBRv8AQqRyuJf5bSVXw5w6XLWmhRANCAAROwX6ZplJMfQicbaDXSZ7qoLPGDdr6
EMtvQvlJdwVI44/NVX/lIS2dhF0h1GxKAippeSiLj9+Srh2mmg7+zEvK
-----END PRIVATE KEY-----
+10
View File
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBdjCCARugAwIBAgIBATAKBggqhkjOPQQDAjAaMRgwFgYDVQQDDA9EZXZlci1U
ZXN0LVJvb3QwHhcNMjYwOTEyMDgxNjUwWhcNMzYwOTA5MDgxNjUwWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAROwX6ZplJM
fQicbaDXSZ7qoLPGDdr6EMtvQvlJdwVI44/NVX/lIS2dhF0h1GxKAippeSiLj9+S
rh2mmg7+zEvKo1gwVjAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFEc4
R7JUtHLl16fHXxBvGDz0qwnKMB8GA1UdIwQYMBaAFIo/Vdu7qm5dIikkwg6A0yRp
1YzVMAoGCCqGSM49BAMCA0kAMEYCIQCzl7jB3XrkRoQXTXMoi9j1OzmCPO3UXLr5
eDq/F8G/twIhALgVU/5C4hx6hpVtlHUmd49EibizuPlNeaCjRPB1WqDM
-----END CERTIFICATE-----
+98
View File
@@ -0,0 +1,98 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const Module = require('node:module');
const { pathToFileURL } = require('node:url');
const { promisify } = require('node:util');
const run = promisify(require('node:child_process').execFile);
test('generated flowcharts render offline and source clicks retain the actual location', async () => {
assert.ok(process.env.DEVER_TEST_BIN);
assert.ok(process.env.DEVER_BROWSER_TOOLS, 'directory containing existing playwright-core and @sparticuz/chromium');
const tools = Module.createRequire(path.join(process.env.DEVER_BROWSER_TOOLS, '..', 'package.json'));
const { chromium: playwright } = tools('playwright-core');
const chromium = tools('@sparticuz/chromium').default;
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'dever flow browser '));
const extension = path.resolve(__dirname, '../../editor/vscode');
const project = path.join(root, 'project');
const subscriptions = [];
let browser;
const originalLoad = Module._load;
try {
await run(process.env.DEVER_TEST_BIN, ['new', project]);
await run(process.env.DEVER_TEST_BIN, ['inspect', project, '--links']);
const uri = filename => ({ fsPath: filename, toString: () => pathToFileURL(filename).toString() });
const panels = [];
const vscode = {
Uri: { joinPath: (base, ...parts) => uri(path.join(base.fsPath, ...parts)) },
ViewColumn: { Beside: 2 },
window: { createWebviewPanel() {
const panel = { dispose() {}, onDidDispose() {}, webview: {
cspSource: 'https://dever-preview.test',
asWebviewUri: uri => `https://dever-preview.test/${path.basename(uri.fsPath)}`,
onDidReceiveMessage: () => ({ dispose() {} })
} };
panels.push(panel);
return panel;
} }
};
Module._load = function(request, ...rest) { return request === 'vscode' ? vscode : originalLoad.call(this, request, ...rest); };
const { openFlowPreview } = require('../../editor/vscode/dist/flow-preview.cjs');
Module._load = originalLoad;
browser = await playwright.launch({ executablePath: await chromium.executablePath(), args: chromium.args, headless: true });
const page = await browser.newPage();
page.on('pageerror', error => console.error('Browser error:', error.message));
page.on('console', message => { if (message.type() === 'error') console.error('Browser console:', message.text()); });
const scripts = new Map(await Promise.all(['flow-browser.js', 'mermaid.js'].map(async name => [`https://dever-preview.test/${name}`, await fs.readFile(path.join(extension, 'dist', name), 'utf8')])));
const messages = [];
await page.exposeFunction('deverMessage', message => messages.push(message));
await page.route('**/*', route => scripts.has(route.request().url())
? route.fulfill({ body: scripts.get(route.request().url()), contentType: 'text/javascript' }) : route.abort());
const projects = [project];
for (const relative of ['test/rust/fixtures/auth', 'test/rust/fixtures/adapters', 'skills/assets/examples/command', 'skills/assets/examples/external-lib']) {
const copy = path.join(root, path.basename(relative));
await fs.cp(path.resolve(__dirname, '../..', relative), copy, { recursive: true });
await run(process.env.DEVER_TEST_BIN, ['inspect', copy, '--links']);
projects.push(copy);
}
if (process.env.DEVER_FLOW_PROJECT) projects.push(process.env.DEVER_FLOW_PROJECT);
const targets = [];
for (const owner of projects) {
for await (const filename of fs.glob('module/*/*/link.md', { cwd: owner })) targets.push({ filename: path.join(owner, filename), owner });
}
let count = 0;
for (const { filename, owner } of targets) {
console.log(`Rendering ${filename}`);
await openFlowPreview({ subscriptions, extensionUri: uri(extension) }, uri(filename), owner);
const html = panels.at(-1).webview.html;
const expected = (html.match(/class="language-mermaid"/g) || []).length;
const nonce = /nonce="([^"]+)"/.exec(html)[1];
const bridge = `<script nonce="${nonce}">window.acquireVsCodeApi = () => ({postMessage: message => window.deverMessage(message)});</script>`;
await page.goto('about:blank');
await page.setContent(html.replace('<body ', `${bridge}<body `));
assert.equal(await page.evaluate(() => typeof globalThis.mermaid?.render), 'function');
await page.waitForSelector('body[data-rendered="true"]', { timeout: 60000 });
assert.deepEqual(await page.locator('.dever-flow-error').allTextContents(), [], filename);
assert.equal(await page.locator('.dever-flow > svg').count(), expected, filename);
assert.ok(expected > 0, filename);
count += expected;
const source = page.locator('.dever-flow a').first();
const href = await source.getAttribute('href') || await source.getAttribute('xlink:href');
await source.click();
await page.waitForTimeout(20);
assert.deepEqual(messages.at(-1), { type: 'open', href });
assert.match(href, /\.dever(?:\.md)?#L\d+,\d+$/);
if (process.env.DEVER_FLOW_SCREENSHOT && path.basename(path.dirname(filename)) === 'audit') {
await page.screenshot({ path: process.env.DEVER_FLOW_SCREENSHOT, fullPage: true });
}
}
console.log(`Rendered ${count} real generated diagrams in ${targets.length} pages without network dependencies`);
} finally {
Module._load = originalLoad;
await browser?.close();
for (const subscription of subscriptions) subscription.dispose();
await fs.rm(root, { recursive: true });
}
});
+278
View File
@@ -0,0 +1,278 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createRequire } = require('node:module');
const test = require('node:test');
const root = path.resolve(__dirname, '../..');
const extension = path.join(root, 'editor/vscode');
const extensionRequire = createRequire(path.join(extension, 'package.json'));
const textmate = extensionRequire('vscode-textmate');
const oniguruma = extensionRequire('vscode-oniguruma');
const cliArguments = process.argv.slice(2);
assert.equal(cliArguments.length, 2, 'pass --markdown-grammar followed by the real VS Code grammar path');
assert.equal(cliArguments[0], '--markdown-grammar');
const markdownPath = path.resolve(cliArguments[1]);
const manifest = JSON.parse(fs.readFileSync(path.join(extension, 'package.json'), 'utf8'));
const configuration = JSON.parse(fs.readFileSync(path.join(extension, 'language-configuration.json'), 'utf8'));
async function loadGrammars() {
const wasm = fs.readFileSync(extensionRequire.resolve('vscode-oniguruma/release/onig.wasm'));
await oniguruma.loadWASM(wasm.buffer.slice(wasm.byteOffset, wasm.byteOffset + wasm.byteLength));
const catalog = new Map();
for (const contribution of manifest.contributes.grammars) {
const filename = path.join(extension, contribution.path);
catalog.set(contribution.scopeName, textmate.parseRawGrammar(fs.readFileSync(filename, 'utf8'), filename));
}
const markdown = textmate.parseRawGrammar(fs.readFileSync(markdownPath, 'utf8'), markdownPath);
assert.equal(markdown.scopeName, 'text.html.markdown');
catalog.set(markdown.scopeName, markdown);
const registry = new textmate.Registry({
onigLib: Promise.resolve({
createOnigScanner: patterns => new oniguruma.OnigScanner(patterns),
createOnigString: value => new oniguruma.OnigString(value)
}),
loadGrammar: scope => catalog.get(scope) || null,
getInjections: scope => manifest.contributes.grammars
.filter(contribution => contribution.injectTo?.includes(scope))
.map(contribution => contribution.scopeName)
});
return {
source: await registry.loadGrammarWithConfiguration('source.dever', 1, {}),
markdown: await registry.loadGrammarWithConfiguration('text.html.markdown', 2, {
embeddedLanguages: { 'meta.embedded.block.dever': 1 },
tokenTypes: { 'meta.embedded.block.dever': 0 }
})
};
}
const grammars = loadGrammars();
function tokenize(grammar, document) {
let state = textmate.INITIAL;
return document.split(/\r\n|\r|\n/).map(line => {
const encoded = grammar.tokenizeLine2(line, state);
const result = grammar.tokenizeLine(line, state);
state = result.ruleStack;
return { line, tokens: result.tokens, encoded: encoded.tokens };
});
}
function tokenAt(rows, line, fragment) {
const row = rows[line];
const offset = row.line.indexOf(fragment);
assert.ok(offset >= 0, `line ${line + 1} does not contain ${fragment}`);
const token = row.tokens.find(candidate => candidate.startIndex <= offset && candidate.endIndex > offset);
assert.ok(token, `no token for ${fragment} on line ${line + 1}`);
return token;
}
function hasScope(rows, line, fragment, scope) {
const actual = tokenAt(rows, line, fragment).scopes;
assert.ok(actual.includes(scope), `${fragment}: expected ${scope}, received ${actual.join(' ')}`);
}
function sourceFiles(directory, suffix) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
const filename = path.join(directory, entry.name);
return entry.isDirectory() ? sourceFiles(filename, suffix) : filename.endsWith(suffix) ? [filename] : [];
});
}
test('types, fields and named function inputs/outputs receive Dever scopes', async () => {
const rows = tokenize((await grammars).source, [
'type Profile {',
' id: Int?',
' name: Text',
'}',
'greet(name: Text) (message: Text) {',
' message = app.format(name)',
'}'
].join('\n'));
hasScope(rows, 0, 'type', 'storage.type.dever');
hasScope(rows, 0, 'Profile', 'entity.name.type.dever');
hasScope(rows, 1, 'id', 'variable.other.member.dever');
hasScope(rows, 1, 'Int', 'support.type.dever');
hasScope(rows, 4, 'greet', 'entity.name.function.dever');
hasScope(rows, 4, 'name', 'variable.other.member.dever');
hasScope(rows, 4, 'message', 'variable.other.member.dever');
hasScope(rows, 5, 'app', 'entity.name.namespace.dever');
hasScope(rows, 5, 'format', 'entity.name.function.dever');
});
test('typed local declarations and direct JSON capture retain ordinary scopes', async () => {
const rows = tokenize((await grammars).source, [
'read(payload: Json) (value: Text) {',
' input: CreateInput = dever.json.decode(payload)',
' decoded: DecodeResult = result(dever.json.decode(payload))',
' value = input.name',
'}'
].join('\n'));
hasScope(rows, 1, 'input', 'variable.other.member.dever');
hasScope(rows, 1, 'CreateInput', 'entity.name.type.dever');
hasScope(rows, 1, 'decode', 'entity.name.function.dever');
hasScope(rows, 2, 'DecodeResult', 'entity.name.type.dever');
hasScope(rows, 2, 'result', 'entity.name.function.dever');
});
test('API, Model, Job and external Adapter declarations are covered', async () => {
const source = (await grammars).source;
for (const method of ['get', 'post', 'put', 'delete', 'cmd', 'rest']) {
const rows = tokenize(source, `${method} profile = app.profile`);
hasScope(rows, 0, method, 'keyword.control.api.dever');
}
const rows = tokenize(source, [
'public get profile = app.profile',
' private owner user_id: user.account.model.id = dever.auth.userId()',
'index(user_id, created_at)',
'sql ownedArticles(id: Int) (articles: List<Article>) {',
'job send(message: Text) () fails app.Error {',
'schedule send_every_minute {',
'external npm "worker.js" {}'
].join('\n'));
hasScope(rows, 0, 'public', 'storage.modifier.dever');
hasScope(rows, 1, 'owner', 'storage.modifier.dever');
hasScope(rows, 2, 'index', 'keyword.other.declaration.dever');
hasScope(rows, 3, 'sql', 'keyword.other.declaration.dever');
hasScope(rows, 4, 'job', 'storage.modifier.dever');
hasScope(rows, 4, 'fails', 'storage.modifier.dever');
hasScope(rows, 5, 'schedule', 'keyword.other.declaration.dever');
hasScope(rows, 6, 'npm', 'keyword.other.external.dever');
});
test('hash comments, quoted text and escapes do not leak into subsequent lines', async () => {
const rows = tokenize((await grammars).source, [
'# type Fake and return',
String.raw`message = "中文 # and type \n \u{4E2D} \""`,
String.raw`invalid = "\q"`,
'broken = "unfinished',
'type Next { value: Text }'
].join('\n'));
hasScope(rows, 0, 'type', 'comment.line.number-sign.dever');
hasScope(rows, 1, '#', 'string.quoted.double.dever');
hasScope(rows, 1, '\\n', 'constant.character.escape.dever');
hasScope(rows, 1, '\\u{4E2D}', 'constant.character.escape.dever');
hasScope(rows, 2, '\\q', 'invalid.illegal.escape.dever');
hasScope(rows, 4, 'type', 'storage.type.dever');
assert.ok(!tokenAt(rows, 4, 'type').scopes.includes('string.quoted.double.dever'));
});
test('integer division is an operator, not a JavaScript line comment', async () => {
const rows = tokenize((await grammars).source, 'count = 12 // 3\namount = 1.25e-2\nvalid = true and not false');
hasScope(rows, 0, '//', 'keyword.operator.dever');
hasScope(rows, 0, '3', 'constant.numeric.dever');
assert.ok(!tokenAt(rows, 0, '3').scopes.some(scope => scope.startsWith('comment.')));
hasScope(rows, 1, '1.25e-2', 'constant.numeric.dever');
hasScope(rows, 2, 'true', 'constant.language.dever');
hasScope(rows, 2, 'and', 'keyword.operator.logical.dever');
});
test('excluded control-flow syntax is not presented as a valid function', async () => {
const rows = tokenize((await grammars).source, 'if(value)\nreturn value\nimport other\npackage main');
for (const [line, keyword] of ['if', 'return', 'import', 'package'].entries()) {
hasScope(rows, line, keyword, 'invalid.illegal.keyword.dever');
}
});
test('Markdown fences reuse source.dever while prose keeps Markdown scopes', async () => {
for (const marker of ['```', '~~~']) {
for (const language of ['dever', 'typescript dever']) {
const rows = tokenize((await grammars).markdown, [
'# 文档', '', '## 类型', `${marker}${language}`,
'type Profile { name: Text }', marker, '', '正文 type Profile'
].join('\n'));
hasScope(rows, 4, 'type', 'storage.type.dever');
hasScope(rows, 4, 'Text', 'support.type.dever');
hasScope(rows, 4, 'Profile', 'meta.embedded.block.dever');
assert.ok(!tokenAt(rows, 7, 'type').scopes.includes('source.dever'));
}
}
});
test('embedded tokens carry the Dever language ID and are not string tokens', async () => {
const rows = tokenize((await grammars).markdown, '```dever\ntype Profile {}\n```');
const metadata = rows[1].encoded[1];
assert.equal(metadata & 0xff, 1);
assert.equal((metadata >>> 8) & 3, 0);
});
test('fence closing honors its character and minimum opening length', async () => {
const rows = tokenize((await grammars).markdown, [
'````dever', 'type First {}', '```', 'type Second {}',
'~~~~', 'type Third {}', '`````', '正文 type Outside'
].join('\n'));
for (const line of [1, 3, 5]) hasScope(rows, line, 'type', 'storage.type.dever');
assert.ok(!tokenAt(rows, 7, 'type').scopes.includes('source.dever'));
const waves = tokenize((await grammars).markdown, '~~~dever\ntype Inside {}\n~~~~\n正文 type Outside');
assert.ok(!tokenAt(waves, 3, 'type').scopes.includes('source.dever'));
});
test('valid indentation and CRLF work without taking over other language fences', async () => {
const rows = tokenize((await grammars).markdown, ' ```dever\r\ntype Profile {}\r\n ```\r\n正文');
hasScope(rows, 1, 'type', 'storage.type.dever');
for (const language of ['typescript', 'javascript', 'dever extra', 'typescript dever']) {
const other = tokenize((await grammars).markdown, `\`\`\`${language}\ntype Profile {}\n\`\`\``);
assert.ok(!tokenAt(other, 1, 'type').scopes.includes('source.dever'), language);
}
});
test('quotes, lists, showcase fences, indented code and comments retain Markdown scopes', async () => {
for (const [document, line] of [
['> ```dever\n> type Quoted {}\n> ```', 1],
['- 示例\n\n ```dever\n type Nested {}\n ```', 3],
['````markdown\n```dever\ntype Showcase {}\n```\n````', 2],
[' ```dever\n type Indented {}\n ```', 1],
['<!--\n```dever\ntype Comment {}\n```\n-->', 2]
]) {
const rows = tokenize((await grammars).markdown, document);
assert.ok(!tokenAt(rows, line, 'type').scopes.includes('source.dever'), document);
}
});
test('language configuration uses Dever comments, pairs and indentation', () => {
assert.equal(configuration.comments.lineComment, '#');
assert.equal(configuration.comments.blockComment, undefined);
assert.deepEqual(configuration.brackets, [['{', '}'], ['[', ']'], ['(', ')']]);
const increase = new RegExp(configuration.indentationRules.increaseIndentPattern);
const decrease = new RegExp(configuration.indentationRules.decreaseIndentPattern);
for (const line of ['greet(name: Text) (message: Text) {', ' values = [', 'type Profile { # 字段']) {
assert.ok(increase.test(line), line);
}
for (const line of ['# example {', 'message = "literal {"', 'message = "value" # {']) {
assert.ok(!increase.test(line), line);
}
assert.ok(decrease.test(' }'));
assert.ok(decrease.test(' ]'));
assert.ok(configuration.autoClosingPairs.every(pair => pair.notIn.includes('string') && pair.notIn.includes('comment')));
assert.deepEqual(manifest.contributes.languages[0].extensions, ['.dever']);
const injection = manifest.contributes.grammars.find(contribution => contribution.injectTo?.includes('text.html.markdown'));
assert.deepEqual(injection.embeddedLanguages, { 'meta.embedded.block.dever': 'dever' });
assert.deepEqual(injection.tokenTypes, { 'meta.embedded.block.dever': 'other' });
assert.equal(manifest.main, './extension.cjs');
assert.equal(manifest.dependencies, undefined);
});
test('maintained plain and Markdown CMS files tokenize without illegal tokens', async () => {
const loaded = await grammars;
for (const [directory, suffix, grammar] of [
['examples/cms/dever', '.dever', loaded.source]
]) {
const files = sourceFiles(path.join(root, directory), suffix);
assert.ok(files.length > 0, directory);
let highlighted = 0;
for (const filename of files) {
const rows = tokenize(grammar, fs.readFileSync(filename, 'utf8'));
let fileHighlighted = 0;
for (const row of rows) {
for (const token of row.tokens) {
assert.ok(!token.scopes.some(scope => scope.startsWith('invalid.illegal.')), `${filename}: ${row.line}`);
if (token.scopes.includes('source.dever')) fileHighlighted += 1;
}
}
assert.ok(fileHighlighted > 0, `${filename} must actually enter the Dever grammar`);
highlighted += fileHighlighted;
}
assert.ok(highlighted > files.length, `${directory} must actually enter the Dever grammar`);
process.stdout.write(`CMS ${directory}: ${files.length} files, ${highlighted} Dever tokens\n`);
}
});
+246
View File
@@ -0,0 +1,246 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const Module = require('node:module');
const { pathToFileURL, fileURLToPath } = require('node:url');
// VS Code 是外部边界;源码关系由真实候选编译器生成,不在测试中伪造索引。
class Uri {
constructor(url) { this.url = url; }
static file(value) { return new Uri(pathToFileURL(value)); }
static parse(value) { return new Uri(new URL(value)); }
static joinPath(base, ...segments) { return Uri.file(path.join(base.fsPath, ...segments)); }
static from(value) {
const url = new URL(`${value.scheme}:/`);
url.pathname = value.path;
if (value.query) url.search = value.query;
return new Uri(url);
}
get scheme() { return this.url.protocol.slice(0, -1); }
get fsPath() { return this.scheme === 'file' ? fileURLToPath(this.url) : this.url.pathname; }
get query() { return decodeURIComponent(this.url.search.slice(1)); }
with(value) { const url = new URL(this.url); if (value.fragment !== undefined) url.hash = value.fragment; if (value.query !== undefined) url.search = value.query; return new Uri(url); }
toString() { return this.url.toString(); }
}
class Range {
constructor(first, second, third, fourth) {
this.start = typeof first === 'number' ? { line: first, character: second } : first;
this.end = typeof first === 'number' ? { line: third, character: fourth } : second;
}
}
class EventEmitter {
callbacks = new Set();
event = callback => { this.callbacks.add(callback); return { dispose: () => this.callbacks.delete(callback) }; };
fire(value) { for (const callback of this.callbacks) callback(value); }
dispose() { this.callbacks.clear(); }
}
test('generated dispatch remains in call hierarchy without hijacking source definitions', async () => {
const run = require('node:util').promisify(require('node:child_process').execFile);
const { at, relations } = require('../../editor/vscode/graph.cjs');
for (const name of ['auth', 'adapters']) {
const root = path.resolve(__dirname, '../rust/fixtures', name);
const result = await run(process.env.DEVER_TEST_BIN, ['inspect', root, '--json'], { maxBuffer: 8 * 1024 * 1024 });
const index = JSON.parse(result.stdout);
if (name === 'auth') {
for (const call of index.calls.filter(call => call.kind === 'http')) {
assert.deepEqual(at(index, call.site.file, call.site.start).map(symbol => symbol.id), [call.callee]);
assert.ok(relations(index, call.caller, false).some(group => group.calls.some(edge => edge.kind === 'verify')));
}
const rest = index.symbols.find(symbol => symbol.kind === 'rest');
assert.deepEqual(at(index, rest.declarations[0].file, rest.declarations[0].start).map(symbol => symbol.id), [rest.id]);
} else {
const port = index.symbols.find(symbol => symbol.name === 'demo.value.port.calculate');
assert.deepEqual(at(index, port.declarations[0].file, port.declarations[0].start).map(symbol => symbol.id), [port.id]);
assert.equal(relations(index, port.id, false).filter(group => group.calls.some(edge => edge.kind === 'adapter')).length, 2);
}
}
});
test('real compiler powers navigation, documentation hover and saved-source refresh', async () => {
assert.ok(process.env.DEVER_TEST_BIN, 'set DEVER_TEST_BIN to the built candidate compiler');
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'dever editor '));
const sourcePath = path.join(root, 'main.dever');
const code = 'label(name: Text) (text: Text) {\n text = name\n}\nfirst(name: Text) (text: Text) {\n text = "😀" + label(name) + label("另")\n}\nsecond(name: Text) (text: Text) {\n text = label(name)\n}\n';
const source = code;
await fs.writeFile(sourcePath, source);
const providers = {};
const commands = {};
const executed = [];
const panels = [];
const opened = [];
const documents = new Map();
const saved = new EventEmitter();
const disposable = () => ({ dispose() {} });
const context = { subscriptions: [], extensionUri: Uri.file(path.resolve(__dirname, '../../editor/vscode')) };
function document(uri, text) {
return {
uri, languageId: uri.fsPath.endsWith('.md') ? 'markdown' : 'dever', isDirty: false, text,
offsetAt(position) {
return this.text.split('\n').slice(0, position.line).reduce((offset, line) => offset + line.length + 1, 0) + position.character;
},
getText(range) { return range ? this.text.slice(this.offsetAt(range.start), this.offsetAt(range.end)) : this.text; }
};
}
const vscode = {
Uri, Range, EventEmitter,
MarkdownString: class {
value = '';
appendMarkdown(text) { this.value += text; return this; }
appendCodeblock(text, language) { this.value += '```' + language + '\n' + text + '\n```\n'; return this; }
},
Hover: class { constructor(contents) { this.contents = contents; } },
Location: class { constructor(uri, range) { Object.assign(this, { uri, range }); } },
CodeLens: class { constructor(range, command) { Object.assign(this, { range, command }); } },
CallHierarchyItem: class { constructor(kind, name, detail, uri, range, selectionRange) { Object.assign(this, { kind, name, detail, uri, range, selectionRange }); } },
CallHierarchyIncomingCall: class { constructor(from, fromRanges) { Object.assign(this, { from, fromRanges }); } },
CallHierarchyOutgoingCall: class { constructor(to, fromRanges) { Object.assign(this, { to, fromRanges }); } },
SymbolKind: { Function: 11 },
ViewColumn: { One: 1, Beside: 2 },
window: {
createOutputChannel: () => ({ appendLine() {}, dispose() {} }),
showErrorMessage(message) { throw new Error(message); },
async showTextDocument(document, options) { opened.push({ document, options }); },
createWebviewPanel(type, title, column, options) {
const disposed = new EventEmitter();
const panel = { type, title, column, options, dispose() { disposed.fire(); }, onDidDispose: disposed.event,
webview: { cspSource: 'https://webview.test', asWebviewUri: uri => uri,
onDidReceiveMessage(callback) { panel.message = callback; return disposable(); } }
};
panels.push(panel);
return panel;
}
},
commands: { registerCommand(name, callback) { commands[name] = callback; return disposable(); }, executeCommand: async (...values) => { executed.push(values); } },
languages: {},
workspace: {
isTrusted: true,
get textDocuments() { return [...documents.values()]; },
getWorkspaceFolder: () => ({ uri: Uri.file(root) }),
getConfiguration: () => ({ get: () => [process.env.DEVER_TEST_BIN] }),
onDidSaveTextDocument: saved.event, onDidChangeConfiguration: disposable, onDidCloseTextDocument: disposable,
createFileSystemWatcher: () => ({ onDidChange: disposable, onDidCreate: disposable, onDidDelete: disposable, dispose() {} }),
registerTextDocumentContentProvider(scheme, provider) { providers[scheme] = provider; return disposable(); },
async openTextDocument(uri) {
const key = uri.toString();
if (!documents.has(key)) {
const text = uri.scheme === 'file' ? await fs.readFile(uri.fsPath, 'utf8') : await providers[uri.scheme].provideTextDocumentContent(uri);
documents.set(key, document(uri, text));
}
return documents.get(key);
}
}
};
for (const name of ['Definition', 'Reference', 'CallHierarchy', 'CodeLens', 'Hover']) {
vscode.languages[`register${name}Provider`] = (_, provider) => { providers[name] = provider; return disposable(); };
}
const originalLoad = Module._load;
Module._load = function(request, ...rest) { return request === 'vscode' ? vscode : originalLoad.call(this, request, ...rest); };
try {
const { registerNavigation } = require('../../editor/vscode/navigation.cjs');
registerNavigation(context);
Module._load = originalLoad;
const doc = await vscode.workspace.openTextDocument(Uri.file(sourcePath));
const callLine = source.split('\n').findIndex(line => line.includes('"😀"'));
const position = { line: callLine, character: source.split('\n')[callLine].indexOf('label') + 1 };
const definitions = await providers.Definition.provideDefinition(doc, position);
assert.equal(definitions.length, 1);
assert.equal(definitions[0].uri.fsPath, sourcePath);
assert.equal(definitions[0].range.start.line, source.split('\n').findIndex(line => line.startsWith('label(')));
const references = await providers.Reference.provideReferences(doc, position, { includeDeclaration: false });
assert.equal(references.length, 3);
assert.equal(references[0].range.start.character, source.split('\n')[callLine].indexOf('label'));
const [target] = await providers.CallHierarchy.prepareCallHierarchy(doc, position);
const incoming = await providers.CallHierarchy.provideCallHierarchyIncomingCalls(target);
assert.equal(incoming.length, 2);
assert.deepEqual(incoming.map(call => call.fromRanges.length).sort(), [1, 2]);
const outgoing = await providers.CallHierarchy.provideCallHierarchyOutgoingCalls(incoming[0].from);
assert.equal(outgoing[0].to.name, 'main.label');
const lenses = await providers.CodeLens.provideCodeLenses(doc);
assert.match(lenses[0].command.title, /2 个调用方 · 3 处调用/);
const preview = Uri.from({ scheme: 'dever-calls', path: '/calls.md', query: JSON.stringify({ root, name: 'main.label' }) });
const markdown = await providers['dever-calls'].provideTextDocumentContent(preview);
assert.match(markdown, /## 定义/);
assert.ok(markdown.includes('```dever'));
assert.match(markdown, /\| 函数 \| 领域/);
assert.match(markdown, /main.first/);
assert.match(markdown, /main.second/);
assert.ok(markdown.includes(`#L${callLine + 1}`));
await commands['dever.showCalls'](root, 'main.label');
const callsPanel = panels.at(-1);
assert.match(callsPanel.webview.html, /<table>/);
assert.match(callsPanel.webview.html, /<a href="file:/);
const sourceHref = /<a href="(file:[^"]+)"/.exec(callsPanel.webview.html)[1];
await callsPanel.message({ type: 'open', href: sourceHref });
assert.equal(opened.at(-1).document.uri.fsPath, sourcePath);
await callsPanel.message({ type: 'open', href: 'command:dever.showCalls?' + encodeURIComponent(JSON.stringify([root, 'main.first'])) });
assert.match(callsPanel.webview.html, /<h1>main.first<\/h1>/);
callsPanel.dispose();
doc.isDirty = true;
assert.deepEqual(await providers.Definition.provideDefinition(doc, position), []);
doc.isDirty = false;
doc.text = source.replace('text = label(name)', 'text = name');
await fs.writeFile(sourcePath, doc.text);
saved.fire(doc);
assert.equal((await providers.Reference.provideReferences(doc, position, { includeDeclaration: false })).length, 2);
doc.text = source.replace('label(name)', 'missing(name)');
await fs.writeFile(sourcePath, doc.text);
saved.fire(doc);
await assert.rejects(providers.Definition.provideDefinition(doc, position), /unresolved|unknown|not found/);
const project = path.join(root, 'application');
const run = require('node:util').promisify(require('node:child_process').execFile);
await run(process.env.DEVER_TEST_BIN, ['new', project]);
await run(process.env.DEVER_TEST_BIN, ['inspect', project, '--links']);
const app = await vscode.workspace.openTextDocument(Uri.file(path.join(project, 'module/hello/greeting/app.dever')));
vscode.window.activeTextEditor = { document: app, selection: { active: { line: 0, character: 1 } } };
const hover = await providers.Hover.provideHover(app, { line: 0, character: 1 });
assert.match(hover.contents[0].value, /greet\(name: Text\) \(message: Text\)/);
assert.match(hover.contents[0].value, /按原样保留/);
assert.match(hover.contents[0].value, /hello\.greeting\.api\.greet/);
assert.match(hover.contents[0].value, /command:dever.showCalls/);
const api = await vscode.workspace.openTextDocument(Uri.file(path.join(project, 'module/hello/greeting/api.dever')));
assert.match((await providers.Hover.provideHover(api, { line: 0, character: api.getText().indexOf('app.greet') + 5 })).contents[0].value, /按原样保留/);
const topic = await vscode.workspace.openTextDocument(Uri.file(path.join(project, 'module/hello/greeting/doc/greeting.md')));
topic.text = topic.text.replace('按原样保留', '输入不做改写');
topic.isDirty = true;
await assert.rejects(providers.Hover.provideHover(app, { line: 0, character: 1 }), /保存/);
await fs.writeFile(topic.uri.fsPath, topic.text);
topic.isDirty = false;
saved.fire(topic);
assert.match((await providers.Hover.provideHover(app, { line: 0, character: 1 })).contents[0].value, /输入不做改写/);
await commands['dever.previewDocumentation'](project, 'hello.greeting.greet');
assert.equal(executed.at(-1)[0], 'markdown.showPreviewToSide');
await commands['dever.openDomainLinks']();
const panel = panels.at(-1);
assert.equal(panel.type, 'dever.links');
assert.match(panel.webview.html, /language-mermaid/);
assert.match(panel.webview.html, /flowchart TD/);
assert.match(panel.webview.html, /flow-browser\.js/);
await panel.message({ type: 'open', href: 'app.dever#L15,3' });
assert.equal(opened.at(-1).document.uri.fsPath, app.uri.fsPath);
assert.deepEqual(opened.at(-1).options.selection.start, { line: 14, character: 2 });
const anchor = /id="(flow-[^"]+)"/.exec(panel.webview.html)[1];
await panel.message({ type: 'open', href: `link.md#${anchor}` });
assert.ok(panel.webview.html.includes(`data-anchor="${anchor}"`));
await assert.rejects(panel.message({ type: 'open', href: 'https://example.invalid/' }), /只打开项目/);
await assert.rejects(panel.message({ type: 'open', href: '../../../../outside.dever' }), /超出当前项目/);
topic.text = topic.text.replace('## hello.greeting.greet', '## hello.greeting.missing');
await fs.writeFile(topic.uri.fsPath, topic.text);
saved.fire(topic);
const invalidDocumentation = await providers.Hover.provideHover(app, { line: 0, character: 1 });
assert.match(invalidDocumentation.contents[0].value, /说明校验未通过/);
assert.match(invalidDocumentation.contents[0].value, /greet\(name: Text\)/);
await assert.rejects(commands['dever.openDomainLinks'](project, 'hello.greeting.greet'), /unknown documentation symbol/);
} finally {
Module._load = originalLoad;
for (const subscription of context.subscriptions) subscription.dispose();
for (const name of ['navigation', 'inspection', 'reading', 'hover']) {
delete require.cache[require.resolve(`../../editor/vscode/${name}.cjs`)];
}
delete require.cache[require.resolve('../../editor/vscode/dist/flow-preview.cjs')];
await fs.rm(root, { recursive: true });
}
});
+40
View File
@@ -0,0 +1,40 @@
#include "dever_ffi.h"
#include <stdio.h>
#include <string.h>
extern long long fixture_triple(long long value);
static int invoke(void *context, dever_ffi_slice operation, dever_ffi_slice payload, dever_ffi_output *output) {
(void)context;
if (operation.size == 6 && memcmp(operation.data, "reject", 6) == 0) {
const char *error = "{\"error\":\"example.Rejected\",\"payload\":null}";
memcpy(output->data, error, strlen(error));
output->size = strlen(error);
return DEVER_FFI_ERROR;
}
if (operation.size == 10 && memcmp(operation.data, "bad_length", 10) == 0) {
output->size = output->capacity + 1;
return DEVER_FFI_OK;
}
if (operation.size == 5 && memcmp(operation.data, "fault", 5) == 0) return DEVER_FFI_FAULT;
if (payload.size >= 64) return DEVER_FFI_FAULT;
char input[64];
memcpy(input, payload.data, (size_t)payload.size);
input[payload.size] = 0;
long long value = 0;
if (sscanf(input, "{\"value\":%lld}", &value) != 1 || value > 1000000 || value < -1000000) return DEVER_FFI_FAULT;
int size = snprintf(output->data, (size_t)output->capacity, "{\"value\":%lld}", fixture_triple(value));
if (size < 0 || (uint64_t)size >= output->capacity) return DEVER_FFI_FAULT;
output->size = (uint64_t)size;
return DEVER_FFI_OK;
}
static void closed(void *context) {
(void)context;
fputs("closed\n", stderr);
}
int main(void) {
const dever_ffi_handler handler = {DEVER_FFI_ABI_V1, NULL, NULL, invoke, closed};
return dever_ffi_serve(&handler);
}
+282
View File
@@ -0,0 +1,282 @@
"""Opt-in host SDK contract check. Pass only absolute runtime paths."""
import argparse
import json
from pathlib import Path
import struct
import subprocess
import tempfile
import threading
import time
ROOT = Path(__file__).resolve().parents[2]
HELLO = {
"kind": "hello", "version": "dever-component-1", "port": "example.Text",
"schema": "fixture-schema-v1", "adapter": "example.TextAdapter",
"capabilities": [], "operations": ["text.render", "text.fail", "number.echo", "text.stubborn"],
"setting": {"prefix": "hello "},
}
def frame(message):
body = json.dumps(message, separators=(",", ":")).encode()
return struct.pack(">I", len(body)) + body
def read_exact(stream, size):
result = bytearray()
while len(result) < size:
part = stream.read(size - len(result))
if not part:
raise AssertionError("worker closed protocol stream")
result.extend(part)
return bytes(result)
def receive(process):
deadline = threading.Timer(3, process.kill)
deadline.start()
try:
size = struct.unpack(">I", read_exact(process.stdout, 4))[0]
assert 0 < size <= 16 * 1024 * 1024
return json.loads(read_exact(process.stdout, size))
finally:
deadline.cancel()
def send(process, message):
process.stdin.write(frame(message))
process.stdin.flush()
def run_case(command, *, keep_input_open=False):
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
try:
send(process, HELLO)
ready = receive(process)
assert ready == {key: value for key, value in HELLO.items() if key != "setting"} | {"kind": "ready"}, ready
send(process, {"kind": "health", "id": 0})
assert receive(process) == {"kind": "health", "id": 0}
send(process, {"kind": "call", "id": 1, "operation": "text.render", "payload": {"text": "world", "delay_ms": 0}})
assert receive(process) == {"kind": "result", "id": 1, "payload": {"value": "hello world"}}
send(process, {"kind": "call", "id": 2, "operation": "text.fail", "payload": {"text": "x", "delay_ms": 0}})
assert receive(process) == {"kind": "error", "id": 2, "error": "example.rejected", "payload": {"reason": "rejected"}}
send(process, {"kind": "call", "id": 3, "operation": "text.render", "payload": {"text": "late", "delay_ms": 500}})
send(process, {"kind": "cancel", "id": 3})
assert receive(process) == {"kind": "error", "id": 3, "error": "dever.cancelled", "payload": None}
time.sleep(0.6)
send(process, {"kind": "call", "id": 4, "operation": "text.render", "payload": {"text": "again", "delay_ms": 0}})
assert receive(process) == {"kind": "result", "id": 4, "payload": {"value": "hello again"}}
for request_id, number in enumerate((9007199254740993, 9223372036854775807, -9223372036854775808), 5):
send(process, {"kind": "call", "id": request_id, "operation": "number.echo", "payload": {"number": number}})
assert receive(process) == {"kind": "result", "id": request_id, "payload": {"number": number}}
for request_id in (8, 9):
send(process, {"kind": "call", "id": request_id, "operation": "text.render", "payload": {"text": "cancel", "delay_ms": 500}})
send(process, {"kind": "cancel", "id": request_id})
assert receive(process) == {"kind": "error", "id": request_id, "error": "dever.cancelled", "payload": None}
send(process, {"kind": "shutdown"})
assert receive(process) == {"kind": "shutdown"}
# Python must finalize its reader without relying on the host's EOF timing.
if not keep_input_open:
process.stdin.close()
assert process.wait(timeout=3) == 0, process.stderr.read().decode()
except Exception as error:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
raise AssertionError(f"worker case failed: {error}; stderr={process.stderr.read().decode()}") from error
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
def run_queued_shutdown(command):
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
try:
send(process, HELLO)
assert receive(process)["kind"] == "ready"
messages = [
{"kind": "call", "id": 1, "operation": "text.render", "payload": {"text": "cancel", "delay_ms": 500}},
{"kind": "shutdown"},
*[{"kind": "health", "id": 0} for _ in range(4)],
]
process.stdin.write(b"".join(frame(message) for message in messages))
process.stdin.flush()
assert receive(process) == {"kind": "shutdown"}
assert process.wait(timeout=3) == 0, process.stderr.read().decode()
assert process.stderr.read() == b"", "shutdown left task/transport diagnostics"
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
def run_typed_case(command, *, keep_input_open=False):
hello = {
"kind": "hello", "version": "dever-component-1", "port": "example.Typed",
"schema": "typed-fixture-v1", "adapter": "example.Managed",
"capabilities": [], "operations": ["render"], "setting": {"prefix": "hello "},
}
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
try:
send(process, hello)
assert receive(process)["kind"] == "ready"
message = {"name": "Ada", "number": 9223372036854775807, "labels": ["a", "b"]}
send(process, {"kind": "call", "id": 1, "operation": "render", "payload": {"message": message}})
assert receive(process) == {"kind": "result", "id": 1, "payload": {"text": "hello Ada9223372036854775807"}}
message["name"] = "reject"
send(process, {"kind": "call", "id": 2, "operation": "render", "payload": {"message": message}})
assert receive(process) == {"kind": "error", "id": 2, "error": "example.Rejected", "payload": {"reason": "rejected"}}
send(process, {"kind": "shutdown"})
assert receive(process) == {"kind": "shutdown"}
if not keep_input_open:
process.stdin.close()
assert process.wait(timeout=3) == 0, process.stderr.read().decode()
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
try:
send(process, hello)
assert receive(process)["kind"] == "ready"
send(process, {"kind": "call", "id": 1, "operation": "render", "payload": {
"message": {"name": "Ada", "number": 9223372036854775808, "labels": []},
}})
process.stdin.close()
assert process.wait(timeout=3) != 0
assert process.stdout.read() == b""
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
def run_rejection(command, body):
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
try:
process.stdin.write(struct.pack(">I", len(body)) + body)
process.stdin.flush()
process.stdin.close()
assert process.wait(timeout=3) != 0
assert process.stdout.read() == b""
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
def run_protocol_rejection(command, body, declared_size=None):
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
try:
send(process, HELLO)
assert receive(process)["kind"] == "ready"
process.stdin.write(struct.pack(">I", len(body) if declared_size is None else declared_size) + body)
process.stdin.flush()
process.stdin.close()
assert process.wait(timeout=3) != 0
assert process.stdout.read() == b""
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
def run_unresponsive_handler(command, final_message):
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
try:
send(process, HELLO)
assert receive(process)["kind"] == "ready"
send(process, {"kind": "call", "id": 1, "operation": "text.stubborn", "payload": {"text": "late", "delay_ms": 0}})
time.sleep(0.1)
send(process, final_message)
if final_message["kind"] == "cancel":
send(process, {"kind": "call", "id": 2, "operation": "text.render", "payload": {"text": "new", "delay_ms": 0}})
send(process, {"kind": "cancel", "id": 2})
send(process, {"kind": "shutdown"})
assert process.wait(timeout=3) != 0
assert process.stdout.read() == b""
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=3)
def runtime(path):
if path is None:
return None
executable = Path(path)
if not executable.is_absolute() or not executable.is_file():
raise SystemExit(f"runtime path must be an existing absolute file: {path}")
return str(executable)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--python")
parser.add_argument("--node")
parser.add_argument("--go")
args = parser.parse_args()
with tempfile.TemporaryDirectory(prefix="dever-sdk-check-") as directory:
selected = {
"Python": [runtime(args.python), "-B", str(ROOT / "sdk/python/example.py")] if args.python else None,
"JavaScript": [runtime(args.node), str(ROOT / "sdk/javascript/example.mjs")] if args.node else None,
}
if args.go:
go = runtime(args.go)
binary = str(Path(directory) / "go-worker")
build_environment = {
"GOCACHE": str(Path(directory) / "go-cache"),
"GOPATH": str(Path(directory) / "go-path"),
"GOENV": "off", "GOTOOLCHAIN": "local", "GOPROXY": "off", "GOSUMDB": "off",
}
subprocess.run([go, "build", "-o", binary, "./example"], cwd=ROOT / "sdk/go", env=build_environment, check=True)
selected["Go"] = [binary]
else:
selected["Go"] = None
manifest = str(ROOT / "test/external-sdk/typed_contract.json")
typed = {
"Python": [runtime(args.python), "-B", str(ROOT / "test/external-sdk/typed_worker.py"), manifest] if args.python else None,
"JavaScript": [runtime(args.node), str(ROOT / "test/external-sdk/typed_worker.mjs"), manifest] if args.node else None,
}
if args.go:
typed_binary = str(Path(directory) / "go-typed-worker")
subprocess.run([go, "build", "-o", typed_binary, str(ROOT / "test/external-sdk/typed_worker.go")], cwd=ROOT / "sdk/go", env=build_environment, check=True)
typed["Go"] = [typed_binary, manifest]
else:
typed["Go"] = None
for language, command in selected.items():
if command is None:
print(f"{language}: unavailable (no absolute runtime path supplied)")
continue
run_case(command, keep_input_open=language == "Python")
if language == "Python":
run_queued_shutdown(command)
run_rejection(command, json.dumps({**HELLO, "schema": "wrong"}).encode())
run_rejection(command, (json.dumps(HELLO)[:-1] + ',"schema":"duplicate"}').encode())
run_rejection(command, (json.dumps(HELLO)[:-1] + ',"extra":true}').encode())
run_rejection(command, b"\xff")
call = {"kind": "call", "id": 1, "operation": "text.render", "payload": {"text": "x", "delay_ms": 0}}
run_protocol_rejection(command, json.dumps({**call, "id": 2}).encode())
run_protocol_rejection(command, json.dumps({**call, "operation": "unknown"}).encode())
run_protocol_rejection(command, (json.dumps(call)[:-2] + ',"text":"duplicate"}}').encode())
run_protocol_rejection(command, (json.dumps(call)[:-1] + ',"extra":true}').encode())
run_protocol_rejection(command, b'{"kind":"call","id":1,"operation":"text.render","payload":NaN}')
run_protocol_rejection(command, b'{"kind":"call","id":1,"operation":"text.render","payload":1e999}')
nested = 0
for _ in range(64):
nested = [nested]
run_protocol_rejection(command, json.dumps({**call, "payload": nested}).encode())
run_protocol_rejection(command, json.dumps({**call, "payload": [0] * 65_537}).encode())
run_protocol_rejection(command, b"", declared_size=16 * 1024 * 1024 + 1)
run_unresponsive_handler(command, {"kind": "cancel", "id": 1})
run_unresponsive_handler(command, {"kind": "shutdown"})
run_typed_case(typed[language], keep_input_open=language == "Python")
print(f"{language}: handshake, health, result, business error, cancel, shutdown and rejection passed")
if __name__ == "__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
import argparse
import json
from pathlib import Path
import struct
import subprocess
import tempfile
import unittest
COMPILER = None
ROOT = Path(__file__).resolve().parents[2]
MANIFEST = {
"port": "example.Native", "adapter": "example.C", "schema": "ffi-test-v1",
"capabilities": [], "operations": ["triple", "reject", "fault", "bad_length"],
}
HELLO = MANIFEST | {"kind": "hello", "version": "dever-component-1", "setting": {}}
def frame(message):
data = json.dumps(message, separators=(",", ":")).encode()
return struct.pack(">I", len(data)) + data
def responses(data):
values = []
while data:
if len(data) < 4:
raise AssertionError("truncated frame")
size = struct.unpack(">I", data[:4])[0]
if not 0 < size <= 16 * 1024 * 1024 or size > len(data) - 4:
raise AssertionError("invalid frame size")
values.append(json.loads(data[4:4 + size]))
data = data[4 + size:]
return values
class NativeFfi(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temporary = tempfile.TemporaryDirectory(prefix="dever-c-ffi-")
cls.addClassCleanup(cls.temporary.cleanup)
cls.directory = Path(cls.temporary.name)
(cls.directory / "lib").mkdir()
library = cls.directory / "library.c"
library.write_text("long long fixture_triple(long long value) { return value * 3; }\n")
subprocess.run([COMPILER, "-shared", "-fPIC", "-Wl,-soname,libfixture.so", str(library), "-o", str(cls.directory / "lib/libfixture.so")], check=True, timeout=30)
cls.binary = cls.directory / "worker"
subprocess.run([
COMPILER, "-std=c11", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "sdk/c"),
str(ROOT / "sdk/c/dever_ffi.c"), str(ROOT / "test/external-sdk/c_ffi_adapter.c"),
"-L", str(cls.directory / "lib"), "-lfixture", "-Wl,-rpath,$ORIGIN/lib", "-o", str(cls.binary),
], check=True, timeout=30)
(cls.directory / "contract.json").write_text(json.dumps(MANIFEST, indent=2))
def execute(self, messages, suffix=b""):
result = subprocess.run([str(self.binary)], input=b"".join(frame(message) for message in messages) + suffix, cwd=self.directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5, env={})
return result, responses(result.stdout)
def test_c_abi_calls_a_shared_library_and_preserves_protocol_lifecycle(self):
result, values = self.execute([
HELLO, {"kind": "health", "id": 0},
{"kind": "call", "id": 1, "operation": "triple", "payload": {"value": 7}},
{"kind": "cancel", "id": 1},
{"kind": "call", "id": 2, "operation": "reject", "payload": {}},
{"kind": "call", "id": 3, "operation": "triple", "payload": {"value": -9}},
{"kind": "shutdown"},
])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(values, [
MANIFEST | {"kind": "ready", "version": "dever-component-1"},
{"kind": "health", "id": 0},
{"kind": "result", "id": 1, "payload": {"value": 21}},
{"kind": "error", "id": 2, "error": "example.Rejected", "payload": None},
{"kind": "result", "id": 3, "payload": {"value": -27}},
{"kind": "shutdown"},
])
self.assertEqual(result.stderr, b"closed\n")
def test_identity_cannot_be_supplied_by_the_caller(self):
for changed in [{"schema": "wrong"}, {"capabilities": ["gpu"]}, {"port": "wrong"}, {"operations": ["other"]}]:
with self.subTest(changed=changed):
result, values = self.execute([HELLO | changed])
self.assertNotEqual(result.returncode, 0)
self.assertEqual(values, [])
self.assertNotIn(b"closed", result.stderr)
def test_invalid_calls_and_native_results_fail_closed(self):
for call in [
{"id": 2, "operation": "triple"},
{"id": 1, "operation": "missing"},
{"id": 1, "operation": "bad_length"},
{"id": 1, "operation": "fault"},
{"id": -1, "operation": "triple"},
{"id": 9223372036854775808, "operation": "triple"},
]:
with self.subTest(call=call):
result, values = self.execute([HELLO, {"kind": "call", "payload": {"value": 7}} | call])
self.assertNotEqual(result.returncode, 0)
self.assertEqual(len(values), 1)
self.assertIn(b"closed\n", result.stderr)
def test_truncated_oversized_and_malformed_frames_fail_closed(self):
for suffix in [b"\x00", struct.pack(">I", 0), struct.pack(">I", 16777217), struct.pack(">I", 5) + b"{}", frame({"kind": "shutdown", "extra": 1})]:
with self.subTest(suffix=suffix):
result, values = self.execute([HELLO], suffix)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(len(values), 1)
for malformed in [b"{", b'{"kind":"call","id":1e}', b'{"kind":"\\q"}', b"[" * 65 + b"]" * 65, b'{"kind":"shutdown"}\x00']:
result, values = self.execute([HELLO], struct.pack(">I", len(malformed)) + malformed)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(len(values), 1)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--cc", required=True)
options = parser.parse_args()
if not Path(options.cc).is_absolute() or not Path(options.cc).is_file():
parser.error("--cc must be an explicit absolute compiler file")
COMPILER = options.cc
unittest.main(argv=[__file__], verbosity=2)
+19
View File
@@ -0,0 +1,19 @@
{
"port": "example.Typed",
"adapter": "example.Managed",
"ecosystem": "exec",
"entry": "test/typed",
"schema": "typed-fixture-v1",
"libs": [],
"capabilities": [],
"operations": ["render"],
"setting": {"type": "record", "fields": {"prefix": {"type": "text"}}},
"inputs": {"render": {"type": "record", "fields": {
"message": {"type": "record", "fields": {
"name": {"type": "text"}, "number": {"type": "int64"},
"labels": {"type": "list", "value": {"type": "text"}}
}}
}}},
"outputs": {"render": {"type": "record", "fields": {"text": {"type": "text"}}}},
"errors": {"example.Rejected": {"type": "record", "fields": {"reason": {"type": "text"}}}}
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
component "dever-component"
)
func main() {
bytes, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
var manifest component.Manifest
if err = json.Unmarshal(bytes, &manifest); err != nil {
panic(err)
}
worker, err := component.NewFromManifest(manifest)
if err == nil {
err = worker.RegisterHandlers(map[string]func(context.Context, any, any) (any, error){
"render": func(_ context.Context, payload any, setting any) (any, error) {
message := payload.(map[string]any)["message"].(map[string]any)
if message["name"] == "reject" {
return nil, component.BusinessError{Identity: "example.Rejected", Payload: map[string]any{"reason": "rejected"}}
}
prefix := setting.(map[string]any)["prefix"].(string)
return map[string]any{"text": prefix + message["name"].(string) + fmt.Sprint(message["number"])}, nil
},
})
}
if err == nil {
err = worker.Serve(os.Stdin, os.Stdout)
}
if err != nil {
fmt.Fprintln(os.Stderr, "typed worker:", err)
os.Exit(1)
}
}
+12
View File
@@ -0,0 +1,12 @@
import { readFileSync } from 'node:fs';
import { BusinessError, Worker, main } from '../../sdk/javascript/dever_component.mjs';
const manifest = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const worker = Worker.fromManifest(manifest, {
render: async (payload, setting) => {
const message = payload.message;
if (message.name === 'reject') throw new BusinessError('example.Rejected', { reason: 'rejected' });
return { text: setting.prefix + message.name + String(message.number) };
},
});
await main(worker);
+17
View File
@@ -0,0 +1,17 @@
import json
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "sdk/python"))
from dever_component import BusinessError, Worker, main
async def render(payload, setting):
message = payload["message"]
if message["name"] == "reject":
raise BusinessError("example.Rejected", {"reason": "rejected"})
return {"text": setting["prefix"] + message["name"] + str(message["number"])}
manifest = json.loads(Path(sys.argv[1]).read_text())
main(Worker.from_manifest(manifest, {"render": render}))
+89
View File
@@ -0,0 +1,89 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"go/build"
"os"
"path/filepath"
"sort"
"strings"
)
type selectedPackage struct {
Name string `json:"name"`
Files []string `json:"files"`
Imports []string `json:"imports"`
Embed map[string][]string `json:"embed"`
}
func main() {
if len(os.Args) != 4 {
fail(errors.New("usage: analyze <goos> <goarch> <directory>"))
}
ctx := build.Default
ctx.GOOS, ctx.GOARCH = os.Args[1], os.Args[2]
ctx.CgoEnabled = false
ctx.BuildTags = nil
pkg, err := ctx.ImportDir(os.Args[3], 0)
if err != nil {
fail(err)
}
if len(pkg.CgoFiles)+len(pkg.SFiles)+len(pkg.CFiles)+len(pkg.CXXFiles)+len(pkg.SwigFiles)+len(pkg.SwigCXXFiles) != 0 {
fail(errors.New("managed Go Worker does not support cgo, assembly, or native source"))
}
if len(pkg.GoFiles) == 0 {
fail(errors.New("no Go source selected for target"))
}
embed := make(map[string][]string, len(pkg.EmbedPatterns))
for _, rawPattern := range pkg.EmbedPatterns {
includeHidden := strings.HasPrefix(rawPattern, "all:")
pattern := strings.TrimPrefix(rawPattern, "all:")
matches, err := filepath.Glob(filepath.Join(os.Args[3], filepath.FromSlash(pattern)))
if err != nil || len(matches) == 0 {
fail(fmt.Errorf("Go embed pattern %q has no matches", rawPattern))
}
for _, match := range matches {
err = filepath.WalkDir(match, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.Type()&os.ModeSymlink != 0 {
return errors.New("Go embed contains a symbolic link")
}
if !includeHidden && path != match && (strings.HasPrefix(entry.Name(), ".") || strings.HasPrefix(entry.Name(), "_")) {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
if entry.IsDir() {
return nil
}
relative, err := filepath.Rel(os.Args[3], path)
if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
return errors.New("Go embed escapes package directory")
}
embed[rawPattern] = append(embed[rawPattern], filepath.ToSlash(relative))
return nil
})
if err != nil {
fail(err)
}
}
if len(embed[rawPattern]) == 0 {
fail(fmt.Errorf("Go embed pattern %q has no embeddable files", rawPattern))
}
sort.Strings(embed[rawPattern])
}
result := selectedPackage{Name: pkg.Name, Files: pkg.GoFiles, Imports: pkg.Imports, Embed: embed}
if err := json.NewEncoder(os.Stdout).Encode(result); err != nil {
fail(err)
}
}
func fail(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
+212
View File
@@ -0,0 +1,212 @@
//go:build ignore
// make_pack.go creates private native/cross fixtures. It is not a signed release maker.
package main
import (
"archive/tar"
"bufio"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
)
func main() {
if len(os.Args) != 3 && len(os.Args) != 4 {
fatal(errors.New("usage: go run make_pack.go <absolute-go-binary> <output-runtime.pack> [linux-aarch64]"))
}
goos, goarch := runtime.GOOS, runtime.GOARCH
if len(os.Args) == 4 {
if os.Args[3] != "linux-aarch64" || runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
fatal(errors.New("cross fixture supports only Linux x86_64 to linux-aarch64"))
}
goarch = "arm64"
}
goBinary, err := filepath.Abs(os.Args[1])
if err != nil {
fatal(err)
}
outputPack, err := filepath.Abs(os.Args[2])
if err != nil {
fatal(err)
}
root, err := os.MkdirTemp("", "dever-go-pack-")
if err != nil {
fatal(err)
}
defer os.RemoveAll(root)
toolDir := filepath.Join(filepath.Dir(goBinary), "..", "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH)
goRoot := filepath.Clean(filepath.Join(filepath.Dir(goBinary), ".."))
analyzer := filepath.Join(root, "analyze")
build := exec.Command(goBinary, "build", "-p=1", "-o", analyzer, "./test/go-managed/analyze")
build.Env = []string{"GOROOT=" + goRoot, "GOTOOLCHAIN=local", "GOWORK=off", "GO111MODULE=off", "CGO_ENABLED=0", "GOCACHE=" + filepath.Join(filepath.Dir(outputPack), "author-cache"), "GOMODCACHE=" + filepath.Join(root, "modules")}
if output, err := build.CombinedOutput(); err != nil {
fatal(fmt.Errorf("build Go source selector: %w: %s", err, output))
}
list := exec.Command(goBinary, "list", "-p=1", "-deps", "-export", "-f", "{{if .Export}}{{.ImportPath}} {{.Export}}{{end}}", "std")
list.Env = append(build.Env, "GOOS="+goos, "GOARCH="+goarch)
output, err := list.Output()
if err != nil {
fatal(fmt.Errorf("list fixed-target standard library: %w", err))
}
stdlib := make(map[string]string)
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) == 2 {
stdlib[fields[0]] = fields[1]
}
}
if err := scanner.Err(); err != nil {
fatal(err)
}
if len(stdlib) == 0 || stdlib["runtime"] == "" {
fatal(errors.New("standard library export map is incomplete"))
}
files := map[string]string{
"bin/compile": filepath.Join(toolDir, "compile"),
"bin/link": filepath.Join(toolDir, "link"),
"bin/analyze": analyzer,
}
if goarch != runtime.GOARCH {
tools := crossTools(goBinary, goRoot, root, goarch, build.Env)
files["bin/compile"], files["bin/link"] = tools[0], tools[1]
}
var imports []string
for path, archive := range stdlib {
files["stdlib/"+path+".a"] = archive
imports = append(imports, path)
}
sort.Strings(imports)
var importcfg strings.Builder
for _, path := range imports {
fmt.Fprintf(&importcfg, "packagefile %s=stdlib/%s.a\n", path, path)
}
version := exec.Command(goBinary, "version")
version.Env = []string{"GOROOT=" + goRoot, "GOTOOLCHAIN=local"}
versionText, err := version.Output()
if err != nil {
fatal(err)
}
deverOS, deverArch := runtime.GOOS, runtime.GOARCH
if deverOS == "darwin" {
deverOS = "macos"
}
if deverArch == "amd64" {
deverArch = "x86_64"
} else if deverArch == "arm64" {
deverArch = "aarch64"
}
target := deverOS + "-" + deverArch
if len(os.Args) == 4 {
target = os.Args[3]
}
manifest, err := json.Marshal(map[string]any{
"format": "dever-worker-runtime-v1", "ecosystem": "go", "target": target,
"build": map[string]any{"host": deverOS + "-" + deverArch, "compiler": "bin/compile", "linker": "bin/link", "analyzer": "bin/analyze", "stdlib_importcfg": "stdlib/importcfg", "goos": goos, "goarch": goarch, "go_version": strings.TrimSpace(string(versionText))},
})
if err != nil {
fatal(err)
}
outputFile, err := os.Create(outputPack)
if err != nil {
fatal(err)
}
defer outputFile.Close()
gzipWriter := gzip.NewWriter(outputFile)
tarWriter := tar.NewWriter(gzipWriter)
writeBytes(tarWriter, "dever-runtime.json", manifest)
writeBytes(tarWriter, "stdlib/importcfg", []byte(importcfg.String()))
names := make([]string, 0, len(files))
for name := range files {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
input, err := os.Open(files[name])
if err != nil {
fatal(err)
}
info, err := input.Stat()
if err != nil {
fatal(err)
}
mode := int64(0644)
if strings.HasPrefix(name, "bin/") {
mode = 0755
}
if err := tarWriter.WriteHeader(&tar.Header{Name: name, Size: info.Size(), Mode: mode, Typeflag: tar.TypeReg}); err != nil {
fatal(err)
}
if _, err := io.Copy(tarWriter, input); err != nil {
fatal(err)
}
input.Close()
}
if err := tarWriter.Close(); err != nil {
fatal(err)
}
if err := gzipWriter.Close(); err != nil {
fatal(err)
}
}
// The product clears the environment. Bake the output architecture into private
// host tools through an overlay; never edit the author's installed Go SDK.
func crossTools(goBinary, goRoot, root, architecture string, environment []string) []string {
original := filepath.Join(goRoot, "src", "internal", "buildcfg", "zbootstrap.go")
contents, err := os.ReadFile(original)
if err != nil {
fatal(err)
}
const declaration = "const defaultGOARCH = runtime.GOARCH"
if strings.Count(string(contents), declaration) != 1 {
fatal(errors.New("Go SDK default architecture declaration differs"))
}
override := filepath.Join(root, "zbootstrap.go")
updated := strings.Replace(string(contents), declaration, "const defaultGOARCH = "+fmt.Sprintf("%q", architecture), 1)
if err := os.WriteFile(override, []byte(updated), 0600); err != nil {
fatal(err)
}
overlay := filepath.Join(root, "overlay.json")
encoded, err := json.Marshal(map[string]any{"Replace": map[string]string{original: override}})
if err != nil {
fatal(err)
}
if err := os.WriteFile(overlay, encoded, 0600); err != nil {
fatal(err)
}
var outputs []string
for _, tool := range []string{"compile", "link"} {
output := filepath.Join(root, tool)
command := exec.Command(goBinary, "build", "-p=1", "-overlay", overlay, "-o", output, "cmd/"+tool)
command.Env = environment
if bytes, err := command.CombinedOutput(); err != nil {
fatal(fmt.Errorf("build cross %s: %w: %s", tool, err, bytes))
}
outputs = append(outputs, output)
}
return outputs
}
func writeBytes(writer *tar.Writer, name string, data []byte) {
if err := writer.WriteHeader(&tar.Header{Name: name, Size: int64(len(data)), Mode: 0644, Typeflag: tar.TypeReg}); err != nil {
fatal(err)
}
if _, err := writer.Write(data); err != nil {
fatal(err)
}
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
+105
View File
@@ -0,0 +1,105 @@
import hashlib
import importlib.util
import io
from pathlib import Path
import subprocess
import tarfile
import tempfile
import unittest
from unittest.mock import patch
SOURCE = Path(__file__).resolve().parents[2] / "sdk/python-runtime.py"
SPECIFICATION = importlib.util.spec_from_file_location("python_runtime", SOURCE)
runtime = importlib.util.module_from_spec(SPECIFICATION)
SPECIFICATION.loader.exec_module(runtime)
class PythonRuntimeTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
def archive(self, names):
path = self.root / "python.tar.gz"
with tarfile.open(path, "w:gz") as archive:
for name in names:
member = tarfile.TarInfo(name)
member.size = 4
archive.addfile(member, io.BytesIO(b"test"))
return path
def test_only_runtime_files_are_selected(self):
for name in ["python/bin/python3.13", "python/lib/libpython3.13.so.1.0",
"python/lib/python3.13/LICENSE.txt", "python/lib/python3.13/asyncio/tasks.py"]:
self.assertTrue(runtime.selected(name, "3.13"), name)
for name in ["python/bin/python3", "python/bin/pip3", "python/include/python3.13/Python.h",
"python/lib/python3.13/site-packages/pip/__init__.py",
"python/lib/python3.13/test/test_os.py", "python/lib/python3.13/__pycache__/os.pyc"]:
self.assertFalse(runtime.selected(name, "3.13"), name)
def test_wheel_tags_include_compatible_abis_without_claiming_newer_glibc(self):
tags = runtime.wheel_tags("3.13", "linux-x86_64", (2, 17))
self.assertEqual(tags[0], "cp313-cp313-manylinux_2_17_x86_64")
self.assertEqual(len(tags), len(set(tags)))
for tag in ["cp313-cp313-manylinux2014_x86_64", "cp39-abi3-manylinux_2_17_x86_64",
"cp38-abi3-manylinux1_x86_64", "cp313-none-manylinux_2_17_x86_64",
"cp313-cp313-linux_x86_64", "py313-none-any", "py3-none-any", "py38-none-any"]:
self.assertIn(tag, tags)
for tag in ["cp314-abi3-manylinux_2_17_x86_64", "cp312-cp312-manylinux_2_17_x86_64",
"cp313-cp313-manylinux_2_28_x86_64"]:
self.assertNotIn(tag, tags)
arm_tags = runtime.wheel_tags("3.13", "linux-aarch64", (2, 17))
self.assertIn("cp39-abi3-manylinux2014_aarch64", arm_tags)
self.assertIn("cp313-cp313-linux_aarch64", arm_tags)
self.assertFalse(any("manylinux1_" in tag or "manylinux2010_" in tag for tag in arm_tags))
with self.assertRaises(ValueError):
runtime.wheel_tags("3.13", "linux-aarch64", (2, 16))
def test_extraction_rejects_traversal_and_duplicates(self):
for names in [["python/../escape"], ["/python/bin/python3.13"],
["python/bin/python3.13", "python/bin/python3.13"]]:
with self.subTest(names=names), self.assertRaises(ValueError):
runtime.extract(self.archive(names), self.root / "output", "3.13")
def test_wrong_hash_does_not_create_output(self):
archive = self.archive([])
output = self.root / "output"
with self.assertRaisesRegex(ValueError, "SHA-256"):
runtime.prepare(archive, "0" * 64, "3.13.5", "linux-x86_64", (2, 17), SOURCE, output)
self.assertFalse(output.exists())
def test_failure_cleans_owned_output_and_preserves_existing_output(self):
archive = self.archive([])
output = self.root / "output"
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
with self.assertRaisesRegex(ValueError, "interpreter or license"):
runtime.prepare(archive, digest, "3.13.5", "linux-x86_64", (2, 17), SOURCE, output)
self.assertFalse(output.exists())
output.mkdir()
(output / "keep").write_text("existing")
with self.assertRaises(FileExistsError):
runtime.prepare(archive, digest, "3.13.5", "linux-x86_64", (2, 17), SOURCE, output)
self.assertEqual((output / "keep").read_text(), "existing")
def test_newer_glibc_is_rejected_before_python_executes(self):
binary = self.root / "python"
binary.write_bytes(b"\x7fELF")
text = "Machine: Advanced Micro Devices X86-64\nName: GLIBC_2.38\n"
result = subprocess.CompletedProcess([], 0, text, "")
with patch.object(runtime.subprocess, "run", return_value=result):
with self.assertRaisesRegex(ValueError, "GLIBC_2.38"):
runtime.check_elf(binary, SOURCE, "linux-x86_64", (2, 17))
result.stdout = "Machine: AArch64\nName: GLIBC_2.17\n"
with patch.object(runtime.subprocess, "run", return_value=result):
with self.assertRaisesRegex(ValueError, "wrong target"):
runtime.check_elf(binary, SOURCE, "linux-x86_64", (2, 17))
result.stdout = "Machine: Advanced Micro Devices X86-64\nName: GLIBC_PRIVATE\n"
with patch.object(runtime.subprocess, "run", return_value=result):
with self.assertRaisesRegex(ValueError, "private glibc"):
runtime.check_elf(binary, SOURCE, "linux-x86_64", (2, 17))
if __name__ == "__main__":
unittest.main()
+56
View File
@@ -0,0 +1,56 @@
# 性能测试
主入口是 `test/rust/performance.rs`,比较真实 Dever 生成程序和 Rust 实现。Go 与 Web 框架对照使用本目录的 Python 驱动。先测改动涉及的功能,完整矩阵用于阶段检查。
## 运行
在仓库的 `rust/` 目录执行,使用锁定工具链:
```sh
cargo +1.95.0 bench --locked --offline --bench performance -- --storage
cargo +1.95.0 bench --locked --offline --bench performance -- --map-values
cargo +1.95.0 bench --locked --offline --bench performance -- --json
```
| 参数 | 工作负载 |
| --- | --- |
| 不传参数 | 基础内核 |
| `--storage`、`--map-values` | Map/List 的构造、查找、借用和筛选 |
| `--collections`、`--handlers`、`--text`、`--bytes` | 集合处理、函数传递、文本和字节 |
| `--numeric`、`--decimal-ops`、`--decimal-sum` | 数值与十进制计算 |
| `--application`、`--query` | CMD、JSON 输入输出和 query 解码 |
| `--json`、`--json-document` | JSON 与动态文档 |
| `--resources`、`--tasks` | 文件、流、任务调度和取消 |
| `--database`、`--database-postgres` | 数据库操作,PostgreSQL 需独立测试配置 |
| `--websocket-protocol` | 独立协议对端的消息传输 |
| `--websocket` | 共享运行库的 WebSocket 生成适配 |
其他诊断开关见主入口及 `performance/extended/`、`performance/transport/`。比较程序保存在 `rust/target/programs/release/`,使用 `--build-only` 可只构建,再单独运行生成的程序。
Python 驱动从仓库根使用,先查看参数:
```sh
python3 test/performance/kernel_go.py --help
python3 test/performance/http_go.py --help
python3 test/performance/cms.py --help
```
这些测试可能编译程序、启动回环服务或访问测试数据库;地址和工具路径使用现有测试配置,不能指向业务数据。
## 公平比较
- 使用相同语义、输入、精度、依赖、编译选项和资源限制。Dever 的错误处理、复制和任务清理,参照也须完成。
- 绑定可用 CPU,避免同时构建或跑其他基准。记录频率、节流和负载,保留正向与反向执行顺序。
- 提前固定轮次和预热次数,保留全部样本、绝对耗时、失败次数及源码/二进制摘要,不测到通过才停。
- 独立 Rust 参照用于比较实现;同表、同函数或共享运行库仅用于诊断生成层与测量波动。
- 报告修改前后及相对参照的变化。一次快结果不能代表稳定提速,一个样例不能代表整门语言。
## 网络与内存
网络结果须同时检查响应内容、错误、丢弃请求和实际测量时长,不能只看 QPS。客户端与服务端分别限制 CPU;并发数相同不等于负载相同。
`--memory` 构建分配诊断产物,探针位于 `test/rust/performance/memory.c`。分配次数、存活字节、RSS/PSS 和 cgroup 内存分别报告;插桩结果不作为普通性能耗时。
## 保存结果
原始数据放在忽略的 `target/performance/` 或测试输出目录,与源码提交和编译器摘要对应。README 只维护测试方法,不追加逐轮成绩和修改日志。
+292
View File
@@ -0,0 +1,292 @@
"""Measure real typed/raw JSON API requests with isolated native applications."""
import argparse
import http.client
import json
import os
from pathlib import Path
import platform
import secrets
import shutil
import signal
import socket
import statistics
import subprocess
import tempfile
import time
from build import fingerprint
from process import process_sample
from settings import write_private_json
FIXTURE = Path(__file__).parent / "fixtures/api-json"
def configuration(port):
return {
"http": {"host": "127.0.0.1", "port": port},
"runtime": {"mode": "api", "shutdown_ms": 2000},
"log": {"level": "error"},
"database": {"default": {"type": "sqlite", "path": "data/control.db",
"max_connections": 2}},
"auth": {"providers": {"session": {
"verify": "benchmark.auth.verify", "jwtSecret": secrets.token_hex(32),
"cookie": "benchmark_session"}}},
"sites": {"test": {"path": "", "auth": "session", "hosts": ["localhost"],
"origin": "https://localhost"}},
}
def build(arguments):
output = arguments.output.resolve()
output.mkdir(parents=True, exist_ok=False)
project = output / "project"
shutil.copytree(FIXTURE / "benchmark", project / "module/benchmark")
(project / "config").mkdir()
write_private_json(project / "config/setting.json", configuration(8080))
artifacts = {}
for label, compiler in (("before", arguments.baseline_compiler),
("after", arguments.compiler)):
compiler = compiler.resolve()
executable = output / label
started = time.monotonic()
with (output / f"{label}.build.log").open("x") as log:
subprocess.run([compiler, "build", project, "--output", executable],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=900)
artifacts[label] = {"path": str(executable), **fingerprint(executable),
"compiler": {"path": str(compiler), **fingerprint(compiler)},
"build_seconds": time.monotonic() - started,
"runtime_manifests": {
str(path.relative_to(compiler.parent)): fingerprint(path)
for path in sorted((compiler.parent / "runtime").glob("*/manifest.json"))}}
if arguments.with_peers:
go_binary = output / "go"
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", go_binary,
FIXTURE.resolve() / "peer.go"], check=True, timeout=180)
artifacts["go"] = {"path": str(go_binary), **fingerprint(go_binary),
"compiler": subprocess.check_output(["go", "version"], text=True).strip(),
"source": fingerprint(FIXTURE / "peer.go"),
"scope": "native codec/transport baseline; not full Dever API semantics"}
built = subprocess.run(["cargo", "build", "--offline", "--locked", "--release",
"-p", "dever-tests", "--example", "api-json-peer",
"--message-format=json"], capture_output=True, text=True,
timeout=600)
(output / "rust.build.log").write_text(built.stderr)
built.check_returncode()
executables = [message["executable"] for line in built.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "api-json-peer"]
if len(executables) != 1:
raise RuntimeError("expected one native Rust API peer")
rust_binary = output / "rust"
shutil.copy2(executables[0], rust_binary)
artifacts["rust"] = {"path": str(rust_binary), **fingerprint(rust_binary),
"compiler": subprocess.check_output(["rustc", "--version"], text=True).strip(),
"source": fingerprint(FIXTURE / "peer.rs"),
"profile": "release: opt-level=3, fat LTO, codegen-units=1",
"scope": "native codec/transport baseline; not full Dever API semantics"}
if arguments.with_runtime_peer:
runtime = arguments.compiler.resolve().parent / "runtime/linux-x86_64"
if not (runtime / "libc.a").is_file() or not (runtime / "crt1.o").is_file():
raise ValueError("Rust runtime peer requires the compiler's Linux x86_64 runtime inputs")
command = ["cargo", "rustc", "--offline", "--locked", "--profile", "runtime-pack",
"-p", "dever-runtime", "--features", "api,crypto,external,sqlite",
"--example", "api-json-runtime-peer", "--message-format=json", "--",
"-C", "target-feature=+crt-static", "-C", "relocation-model=static",
"-C", f"link-arg=-B{runtime}/", "-C", f"link-arg=-L{runtime}/",
"-C", f"link-arg=-Wl,-Map,{output / 'rust-runtime.link.map'}"]
built = subprocess.run(command, capture_output=True, text=True, timeout=600)
(output / "rust-runtime.build.log").write_text(built.stderr)
built.check_returncode()
executables = [message["executable"] for line in built.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "api-json-runtime-peer"]
if len(executables) != 1:
raise RuntimeError("expected one Rust runtime API peer")
runtime_binary = output / "rust-runtime"
shutil.copy2(executables[0], runtime_binary)
artifacts["rust_runtime"] = {
"path": str(runtime_binary), **fingerprint(runtime_binary),
"compiler": subprocess.check_output(["rustc", "--version"], text=True).strip(),
"source": fingerprint(FIXTURE / "runtime-peer.rs"), "command": command,
"runtime_inputs": {name: fingerprint(runtime / name) for name in
["crt1.o", "crti.o", "crtbeginT.o", "crtend.o", "crtn.o", "libc.a", "libm.a", "libgcc.a", "libgcc_eh.a"]},
"profile": "runtime-pack: opt-level=3, fat LTO, static executable, codegen-units=1",
"scope": "shared Dever runtime contract with native Rust values and codecs; not an independent runtime",
}
manifest = {
"artifacts": artifacts,
"sources": {str(path.relative_to(FIXTURE)): fingerprint(path)
for path in sorted(FIXTURE.rglob("*.dever"))},
}
write_private_json(output / "manifest.json", manifest)
print(output / "manifest.json", flush=True)
def request(client, route, body, expected):
started = time.perf_counter_ns()
client.request("POST", f"/benchmark/echo/{route}", body,
{"Host": "localhost", "Content-Type": "application/json"})
response = client.getresponse()
encoded = response.read()
elapsed = time.perf_counter_ns() - started
if response.status != 200 or json.loads(encoded) != {
"code": 0, "message": "ok", "data": expected}:
raise AssertionError(f"API response mismatch: HTTP {response.status}")
return elapsed
def percentile(values, fraction):
return sorted(values)[min(len(values) - 1, int(len(values) * fraction))] / 1000
def measure(artifact, route, payload, arguments):
expected = payload["payload" if route == "echo" else "source"]
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
with tempfile.TemporaryDirectory(prefix="api-json-", dir=arguments.output) as temporary:
directory = Path(temporary)
executable = directory / "app"
shutil.copy2(artifact["path"], executable)
(directory / "config").mkdir()
write_private_json(directory / "config/setting.json", configuration(port))
with (directory / "stdout").open("w+") as stdout, \
(directory / "stderr").open("w+") as stderr:
process = subprocess.Popen(["taskset", "-c", str(arguments.server_cpu), executable],
cwd=directory, stdout=stdout, stderr=stderr,
start_new_session=True)
client = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
deadline = time.monotonic() + 20
while True:
if process.poll() is not None:
raise RuntimeError(f"API exited before readiness: {process.returncode}")
try:
request(client, route, body, expected)
break
except (ConnectionError, http.client.HTTPException):
client.close()
if time.monotonic() >= deadline:
raise TimeoutError("API readiness timeout")
time.sleep(0.02)
for _ in range(50):
request(client, route, body, expected)
before = process_sample(process.pid)
started = time.monotonic()
latencies = [request(client, route, body, expected)
for _ in range(arguments.requests)]
elapsed = time.monotonic() - started
after = process_sample(process.pid)
result = {
"requests": len(latencies), "request_bytes": len(body),
"elapsed_seconds": elapsed,
"p50_us": percentile(latencies, 0.5),
"p95_us": percentile(latencies, 0.95),
"p99_us": percentile(latencies, 0.99),
"server_cpu_us_per_request":
(after["cpu_seconds"] - before["cpu_seconds"]) * 1e6 / len(latencies),
"rss_bytes": after["rss_bytes"],
"rss_growth_bytes": after["rss_bytes"] - before["rss_bytes"],
}
except BaseException as error:
stderr.flush()
stderr.seek(0)
error.add_note(stderr.read()[-4000:])
raise
finally:
client.close()
if process.poll() is None:
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait(timeout=5)
raise TimeoutError("API did not drain after SIGTERM")
if process.returncode:
raise RuntimeError(f"API shutdown failed: {process.returncode}")
return result
def run(arguments):
allowed = os.sched_getaffinity(0)
if (arguments.server_cpu == arguments.client_cpu or
not {arguments.server_cpu, arguments.client_cpu} <= allowed):
raise ValueError("server and client require distinct allowed CPUs")
if arguments.requests < 1 or arguments.repeats < 1:
raise ValueError("requests and repeats must be positive")
manifest = json.loads(arguments.manifest.read_text())
for artifact in manifest["artifacts"].values():
if fingerprint(Path(artifact["path"])) != {
key: artifact[key] for key in ("bytes", "sha256")}:
raise ValueError("API benchmark binary changed after build")
arguments.output.mkdir(parents=True, exist_ok=False)
os.sched_setaffinity(0, {arguments.client_cpu})
labels = list(manifest["artifacts"])
report = {"manifest": manifest, "server_cpu": arguments.server_cpu,
"client_cpu": arguments.client_cpu, "samples": [], "summary": [],
"environment": {"kernel": platform.release(), "machine": platform.machine(),
"allowed_cpus": sorted(allowed), "python": platform.python_version()},
"runner": fingerprint(Path(__file__)),
"requests_per_process": arguments.requests, "repeats": arguments.repeats}
try:
for count in (1, 16, 128):
row = {"id": 9007199254740993, "text": "雪😀\n\"" + "x" * 80,
"weight": 1.25, "note": None}
value = {"rows": [row] * count, "raw": {"large": 123456789012345678901234567890}}
for route, argument in (("echo", "payload"), ("raw", "source")):
for repeat in range(arguments.repeats):
offset = repeat % len(labels)
order = labels[offset:] + labels[:offset]
for label in order:
sample = measure(manifest["artifacts"][label], route,
{argument: value}, arguments)
report["samples"].append({"label": label, "route": route,
"rows": count, "repeat": repeat, **sample})
for label in labels:
samples = [row for row in report["samples"]
if row["label"] == label and row["route"] == route
and row["rows"] == count]
summary = {"label": label, "route": route, "rows": count}
for metric in ("p50_us", "p95_us", "p99_us",
"server_cpu_us_per_request", "rss_bytes"):
summary[metric] = statistics.median(sample[metric] for sample in samples)
report["summary"].append(summary)
print(json.dumps(summary), flush=True)
for artifact in manifest["artifacts"].values():
if fingerprint(Path(artifact["path"])) != {
key: artifact[key] for key in ("bytes", "sha256")}:
raise ValueError("API benchmark binary changed during measurement")
report["passed"] = True
finally:
write_private_json(arguments.output / "report.json", report)
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
prepare = commands.add_parser("build")
prepare.add_argument("--compiler", type=Path, required=True)
prepare.add_argument("--baseline-compiler", type=Path, required=True)
prepare.add_argument("--output", type=Path, required=True)
prepare.add_argument("--with-peers", action="store_true")
prepare.add_argument("--with-runtime-peer", action="store_true")
prepare.set_defaults(action=build)
measure = commands.add_parser("run")
measure.add_argument("--manifest", type=Path, required=True)
measure.add_argument("--output", type=Path, required=True)
measure.add_argument("--server-cpu", type=int, required=True)
measure.add_argument("--client-cpu", type=int, required=True)
measure.add_argument("--requests", type=int, default=1000)
measure.add_argument("--repeats", type=int, default=7)
measure.set_defaults(action=run)
arguments = parser.parse_args()
arguments.output = arguments.output.resolve()
arguments.action(arguments)
if __name__ == "__main__":
main()
+174
View File
@@ -0,0 +1,174 @@
"""Verify the API JSON fixture's observable contract before runtime comparison."""
import argparse
import base64
from contextlib import ExitStack, contextmanager
from copy import deepcopy
import hashlib
import hmac
import http.client
import json
from pathlib import Path
import shutil
import signal
import socket
import subprocess
import tempfile
import time
from api_json import configuration, request, write_private_json
from build import fingerprint
@contextmanager
def server(binary, output, settings):
with tempfile.TemporaryDirectory(prefix="api-contract-", dir=output) as temporary:
directory = Path(temporary)
executable = directory / "app"
shutil.copy2(binary, executable)
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
settings = deepcopy(settings)
settings["http"]["port"] = port
(directory / "config").mkdir()
write_private_json(directory / "config/setting.json", settings)
with (directory / "stdout").open("w+") as stdout, (directory / "stderr").open("w+") as stderr:
process = subprocess.Popen([executable], cwd=directory, stdout=stdout,
stderr=stderr, start_new_session=True)
client = http.client.HTTPConnection("127.0.0.1", port, timeout=20)
try:
deadline = time.monotonic() + 30
while True:
if process.poll() is not None:
raise RuntimeError(f"API contract server exited: {process.returncode}")
try:
request(client, "raw", b'{"source":0}', 0)
break
except (ConnectionError, http.client.HTTPException):
client.close()
if time.monotonic() >= deadline:
raise TimeoutError("API contract server readiness timeout")
time.sleep(0.05)
yield client
finally:
client.close()
if process.poll() is None:
process.send_signal(signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
raise
if process.returncode:
raise RuntimeError(f"API contract server shutdown failed: {process.returncode}")
def credential(settings):
def encode(value):
return base64.urlsafe_b64encode(json.dumps(value, separators=(",", ":")).encode()).rstrip(b"=")
now = int(time.time())
payload = encode({"alg": "HS256", "typ": "JWT"}) + b"." + encode({
"iss": "session", "sub": "fixture", "sid": "fixture-session", "aud": "test",
"tenant": None, "iat": now, "exp": now + 600,
})
key = settings["auth"]["providers"]["session"]["jwtSecret"].encode()
signature = base64.urlsafe_b64encode(hmac.digest(key, payload, "sha256")).rstrip(b"=")
return (payload + b"." + signature).decode()
def cases(settings):
result = []
row = '{"id":9007199254740993,"text":"雪😀\\n\\\"","weight":1.25,"note":null}'
typed = '{"payload":{"rows":[' + row + '],"raw":{"large":123456789012345678901234567890}}}'
def add(name, body, status=200, route="raw", headers=None, method="POST", query=""):
result.append({"name": name, "body": body.encode() if isinstance(body, str) else body,
"status": status, "path": "/benchmark/echo/" + route + query,
"headers": headers or [("Host", "localhost"), ("Content-Type", "application/json")],
"method": method})
add("typed-unicode-exact-number", typed, route="echo")
add("typed-optional-absent", typed.replace(',"note":null', ""), route="echo")
add("typed-optional-text", typed.replace('"note":null', '"note":"雪"'), route="echo")
for spelling in ["-0", "-9223372036854775808", "9223372036854775807"]:
add("typed-int-" + spelling, typed.replace("9007199254740993", spelling), route="echo")
for spelling in ["1.0", "1e0", "9223372036854775808", "-9223372036854775809", "true"]:
add("typed-invalid-int-" + spelling, typed.replace("9007199254740993", spelling), 400, "echo")
add("typed-nonfinite", typed.replace("1.25", "1e999"), 400, "echo")
add("typed-unknown-field", typed.replace('"weight":', '"extra":0,"weight":'), 400, "echo")
add("typed-missing-field", typed.replace('"weight":1.25,', ""), 400, "echo")
add("typed-duplicate-escaped-field", typed.replace('"weight":', '"we\\u0069ght":0,"weight":'), 400, "echo")
add("typed-invalid-optional", typed.replace('"note":null', '"note":false'), 400, "echo")
for name, value in [("number", "1e999"), ("null", "null"), ("array", "[1,true,null]"),
("unicode", '"\\ud83d\\ude00"'), ("object", '{"large":123456789012345678901234567890}')]:
add("raw-" + name, '{"source":' + value + '}')
for name, body in [("duplicate", '{"source":{"key":1,"k\\u0065y":2}}'),
("surrogate", '{"source":"\\ud800"}'),
("trailing-comma", '{"source":[0,]}'),
("root-duplicate", '{"source":0,"source":1}'),
("root-unknown", '{"source":0,"extra":1}'),
("root-missing", '{}'), ("trailing-root", '{"source":0}[]'),
("invalid-utf8", b'{"source":"\xff"}')]:
add(name, body, 400)
for depth, status in [(63, 200), (64, 400)]:
add(f"depth-{depth}", '{"source":' + '[' * depth + '0' + ']' * depth + '}', status)
for count, status in [(65534, 200), (65535, 400)]:
add(f"elements-{count}", '{"source":[' + ','.join(['0'] * count) + ']}', status)
add("wrong-method", '{}', 405, method="GET")
add("unknown-route", '{}', 404, route="missing")
add("query-on-post", '{"source":0}', 400, query="?value=1")
add("wrong-content-type", '{}', 400, headers=[("Host", "localhost"), ("Content-Type", "text/plain")])
add("duplicate-content-type", '{}', 400, headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Content-Type", "application/json")])
add("wrong-host", '{}', 401, headers=[("Host", "other.invalid"), ("Content-Type", "application/json")])
add("invalid-bearer", '{}', 401, headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Authorization", "Bearer invalid")])
add("valid-bearer", typed, route="echo", headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Authorization", "Bearer " + credential(settings))])
add("public-expired-cookie", '{"source":0}', headers=[("Host", "localhost"), ("Content-Type", "application/json"), ("Cookie", "benchmark_session=invalid")])
return result
def verify(dever, rust, output):
output.mkdir(parents=True, exist_ok=False)
artifacts = {label: {"path": str(binary.resolve()), **fingerprint(binary)}
for label, binary in [("dever", dever), ("rust_runtime", rust)]}
settings = configuration(8080)
report = {"artifacts": artifacts, "cases": [], "passed": False}
try:
with ExitStack() as stack:
clients = {label: stack.enter_context(server(Path(artifact["path"]), output, settings))
for label, artifact in artifacts.items()}
for case in cases(settings):
observed = {}
for label, client in clients.items():
client.putrequest(case["method"], case["path"], skip_host=True, skip_accept_encoding=True)
for name, value in case["headers"]:
client.putheader(name, value)
client.putheader("Content-Length", str(len(case["body"])))
client.endheaders(case["body"])
response = client.getresponse()
body = response.read()
observed[label] = (response.status, response.getheader("Content-Type"), response.getheader("Allow"), body)
client.close()
passed = all(value[0] == case["status"] for value in observed.values()) and len(set(observed.values())) == 1
report["cases"].append({"name": case["name"], "expected_status": case["status"], "passed": passed,
"observed": {label: {"status": value[0], "body_sha256": hashlib.sha256(value[3]).hexdigest(), "bytes": len(value[3])}
for label, value in observed.items()}})
if not passed:
raise AssertionError(f"API contract differs for {case['name']}: " + repr({label: (value[0], value[3][:240]) for label, value in observed.items()}))
for artifact in artifacts.values():
assert fingerprint(Path(artifact["path"])) == {key: artifact[key] for key in ["bytes", "sha256"]}
report["passed"] = True
finally:
write_private_json(output / "report.json", report)
print(json.dumps({"passed": True, "cases": len(report["cases"])}), flush=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dever", type=Path, required=True)
parser.add_argument("--rust", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
arguments = parser.parse_args()
verify(arguments.dever, arguments.rust, arguments.output)
+478
View File
@@ -0,0 +1,478 @@
"""构建可搬运的基准产物;构建耗时与运行测量分开。"""
from copy import deepcopy
import hashlib
import json
from pathlib import Path
import secrets
import shutil
import subprocess
import tempfile
import time
from urllib.parse import urlsplit
from settings import PEER_CONFIGURATION, stage_peer, write_private_json
ROOT = Path(__file__).resolve().parents[2]
FIXTURES = Path(__file__).parent / "fixtures"
TLS = ROOT / "test/dever-tests/fixtures/tls"
SETTING_PATH = ROOT / "config/setting.json"
POSTGRES_TEST_CONNECTION = "postgres_test"
APPLICATION_CONFIGURATION = "llvm-cmd-v1"
ENTRIES = {
"runtime": ("sync_idle", "async_idle", "tasks", "tasks_on_worker", "channels", "cancel"),
"http": ("http", "https", "http2", "https2"),
"live": ("tcp", "ws", "sse"),
}
ORM_ENTRIES = ("orm_idle", "orm_crud", "orm_list", "orm_cursor", "orm_stream",
"orm_pool", "orm_http")
RUNTIME_PROFILES = {
"base": ("runtime-api", "runtime-external"),
"sqlite": ("runtime-api", "runtime-external", "runtime-sqlite"),
"postgres": ("runtime-api", "runtime-external", "runtime-postgres"),
"both": ("runtime-api", "runtime-external", "runtime-sqlite", "runtime-postgres"),
}
BENCHMARK_COMMAND = "benchmark.command.run"
CONFIG_FUNCTIONS = {
"runtime": ("lifetime", "idleSampleMs", "pending", "channelCapacity", "input", "pendingInput"),
"http": ("lifetime", "limits", "http2Limits", "largeBody", "certificate", "key"),
"live": (),
"orm": ("lifetime", "ormInput", "ormRows", "ormConcurrency", "ormCancelWork"),
}
def fingerprint(path):
with path.open("rb") as binary:
digest = hashlib.file_digest(binary, "sha256").hexdigest()
return {"sha256": digest, "bytes": path.stat().st_size}
def postgres_test_connection(document):
try:
connection = document["database"][POSTGRES_TEST_CONNECTION]
except (KeyError, TypeError) as error:
raise ValueError(
f"config/setting.json must define database.{POSTGRES_TEST_CONNECTION}"
) from error
if not isinstance(connection, dict):
raise ValueError(f"database.{POSTGRES_TEST_CONNECTION} must be an object")
if connection.get("type") != "postgres":
raise ValueError(f"database.{POSTGRES_TEST_CONNECTION} must use type 'postgres'")
max_connections = connection.get("max_connections")
if type(max_connections) is not int or not 1 <= max_connections <= 256:
raise ValueError(
f"database.{POSTGRES_TEST_CONNECTION}.max_connections must be an explicit 1..256 integer"
)
return connection
def postgres_case_url(document, case_name):
template = postgres_test_connection(document).get("url")
if not isinstance(template, str):
raise ValueError(f"database.{POSTGRES_TEST_CONNECTION}.url must be Text")
parsed = urlsplit(template)
database = parsed.path.removeprefix("/")
if (template.count("{case}") != 1 or "{case}" not in database
or not database or "/" in database):
raise ValueError(
f"database.{POSTGRES_TEST_CONNECTION}.url must contain one {{case}} placeholder "
"in its database name"
)
if parsed.scheme not in ("postgres", "postgresql"):
raise ValueError(
f"database.{POSTGRES_TEST_CONNECTION}.url must use postgres:// or postgresql://"
)
return template.replace("{case}", case_name)
def postgres_database_case(output, case_name):
run = hashlib.sha256(str(output.resolve()).encode()).hexdigest()[:12]
return f"{run}_{case_name.replace('-', '_')}"
def load_postgres_setting(path=SETTING_PATH):
if not path.is_file():
return None
try:
document = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"cannot load '{path}': {error}") from error
if not isinstance(document, dict):
raise ValueError(f"'{path}' must contain a JSON object")
connections = document.get("database")
if connections is None:
return None
if not isinstance(connections, dict):
raise ValueError(f"'{path}' database must be an object")
if POSTGRES_TEST_CONNECTION not in connections:
return None
postgres_case_url(document, "validation")
return document
def configuration(args, postgres_setting=None):
settings = {
"workers": args.workers,
"connections": args.connections,
"pending": args.pending,
"iterations": args.iterations,
"channel_capacity": 64,
"timeout_ms": 2000,
"lifetime_ms": 3_600_000,
"idle_sample_ms": 3000,
"task_capacity": max(args.pending + 16, 2 * args.connections + 16),
"max_blocking_threads": 2,
"http2_streams": args.http2_streams,
"http2_stream_window_bytes": args.http2_stream_window_bytes,
"http2_connection_window_bytes": args.http2_connection_window_bytes,
"orm_iterations": args.orm_iterations,
"orm_rows": args.orm_rows,
"orm_concurrency": args.orm_concurrency,
"orm_cancel_work": args.orm_cancel_work,
"orm_sqlite_connections": args.orm_sqlite_connections,
"orm_postgres_connections": (
postgres_test_connection(postgres_setting)["max_connections"]
if postgres_setting is not None else None
),
}
if settings["task_capacity"] > 65536:
raise ValueError("task capacity exceeds the language limit")
return settings
def config_source(settings, fixture):
names = CONFIG_FUNCTIONS[fixture]
functions = {
"lifetime": ("Int", str(settings["lifetime_ms"])),
"idleSampleMs": ("Int", str(settings["idle_sample_ms"])),
"pending": ("Int", str(settings["pending"])),
"channelCapacity": ("Int", str(settings["channel_capacity"])),
"input": ("Bytes", f'dever.bytes.fromText({json.dumps("x" * settings["iterations"])})'),
"pendingInput": ("Bytes", f'dever.bytes.fromText({json.dumps("x" * settings["pending"])})'),
"ormInput": ("Bytes", f'dever.bytes.fromText({json.dumps("x" * settings["orm_iterations"])})'),
"ormRows": ("Bytes", f'dever.bytes.fromText({json.dumps("r" * settings["orm_rows"])})'),
"ormConcurrency": ("Int", str(settings["orm_concurrency"])),
"ormCancelWork": ("Int", str(settings["orm_cancel_work"])),
"limits": ("dever.http.Limits", "dever.http.Limits {\nheader_bytes = 8192\n"
f'body_bytes = 65536\ntimeout_ms = {settings["timeout_ms"]}\n'
f'connections = {settings["connections"]}\nhttp2 = null\n}}'),
"http2Limits": ("dever.http.Limits", "dever.http.Limits {\nheader_bytes = 8192\n"
f'body_bytes = 65536\ntimeout_ms = {settings["timeout_ms"]}\n'
f'connections = {settings["connections"]}\nhttp2 = dever.http.Http2Limits {{\n'
f'streams = {settings["http2_streams"]}\n'
f'stream_window_bytes = {settings["http2_stream_window_bytes"]}\n'
f'connection_window_bytes = {settings["http2_connection_window_bytes"]}\n}}\n}}'),
}
if "certificate" in names:
functions["certificate"] = ("Bytes", f'dever.bytes.fromText({json.dumps((TLS / "server.pem").read_text())})')
if "key" in names:
functions["key"] = ("Bytes", f'dever.bytes.fromText({json.dumps((TLS / "server-key.pem").read_text())})')
source = ""
for name in names:
if name == "largeBody":
continue
kind, expression = functions[name]
source += f"\n{name}() (result: {kind}) {{\nresult = {expression}\n}}\n"
if "largeBody" in names:
source += '\ndouble(value: Int, text: Text) (result: Text) { result = text + text }\n'
source += '\nlargeBody() (result: Bytes) {\nresult = dever.bytes.fromText(reduce(double, '
source += '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "x"))\n}\n'
return source
def stage_application_command(module, capability):
command = module / "benchmark/command"
command.mkdir(parents=True, exist_ok=True)
(command / "app.dever").write_text(
f"run() (result: Bool) {{\n result = {capability}()\n}}\n"
)
(command / "api.dever").write_text("cmd run = app.run\n")
def stage_benchmark_support(module, settings, fixture):
if fixture in ("runtime", "orm"):
bench = module / "benchmark/bench/app.dever"
bench.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(FIXTURES / "bench.dever", bench)
if CONFIG_FUNCTIONS[fixture]:
config = module / "benchmark/config/app.dever"
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text(config_source(settings, fixture))
def database_setting(driver, settings, postgres_setting=None, case_name=None):
if driver == "sqlite":
database = {
"type": "sqlite",
"path": "data/db/orm.db",
"max_connections": settings["orm_sqlite_connections"],
"max_page_size": max(32, min(settings["orm_rows"], 10_000)),
}
elif driver == "postgres":
if postgres_setting is None or case_name is None:
raise ValueError("PostgreSQL requires config/setting.json")
database = deepcopy(postgres_test_connection(postgres_setting))
database["url"] = postgres_case_url(postgres_setting, case_name)
else:
raise ValueError(f"unknown database driver: {driver}")
document = ({name: deepcopy(postgres_setting[name])
for name in ("http", "log") if name in postgres_setting}
if driver == "postgres" else {})
document.setdefault("http", {})
document.setdefault("log", {})
document["database"] = {"default": database}
document["auth"] = {"providers": {"benchmark": {
"verify": "benchmark.item.verify",
"jwtSecret": secrets.token_urlsafe(48),
"ttlSeconds": 3600,
}}}
document["sites"] = {"benchmark": {"path": "", "auth": "benchmark"}}
return document
def profile_database_setting(profile):
if profile == "base":
return {}
sqlite = {
"type": "sqlite",
"path": "data/db/profile.db",
"max_connections": 1,
"max_page_size": 100,
}
if profile == "sqlite":
return {"database": {"default": sqlite}}
if profile not in ("postgres", "both"):
raise ValueError(f"unknown runtime profile: {profile}")
postgres = {
"type": "postgres",
"url": "postgres://build-only.invalid/dever_profile",
"tls": "disabled",
"max_connections": 1,
}
if profile == "postgres":
return {"database": {"default": postgres}}
return {"database": {"default": sqlite, "postgres_profile": postgres}}
def dependency_packages(text):
return sorted({line.removesuffix(" (*)") for line in text.splitlines() if line})
def runtime_dependencies(cargo, features):
command = [cargo, "tree", "--offline", "--locked", "-p", "dever-backend-bridge",
"--no-default-features", "--prefix", "none"]
if features:
command.extend(("--features", ",".join(features)))
return dependency_packages(subprocess.check_output(command, cwd=ROOT, text=True))
def prepare_native_project(destination, fixture, settings):
module = destination / "module"
domain = module / "benchmark" / fixture
domain.mkdir(parents=True)
shutil.copyfile(FIXTURES / fixture / "app.dever", domain / "app.dever")
for role in ("port", "adapter", "api"):
shutil.copyfile(FIXTURES / fixture / f"{role}.dever",
domain / f"{role}.dever")
stage_benchmark_support(module, settings, fixture)
config = destination / "config"
config.mkdir()
write_private_json(config / "setting.json",
native_runtime_setting(settings) if fixture == "live" else {})
def prepare_project(destination, fixture, settings, driver, postgres_setting=None):
module = destination / "module"
domain = module / "benchmark/item"
domain.mkdir(parents=True)
shutil.copyfile(FIXTURES / fixture / "app.dever", domain / "app.dever")
shutil.copyfile(FIXTURES / fixture / "app/model/item.dever", domain / "model.dever")
if fixture == "orm":
shutil.copyfile(FIXTURES / fixture / "app/api.dever", domain / "api.dever")
stage_benchmark_support(module, settings, fixture)
stage_application_command(module, "benchmark.item.ormIdle")
config = destination / "config"
config.mkdir()
write_private_json(
config / "setting.json",
database_setting(driver, settings, postgres_setting, "build_only"),
)
(destination / "data").mkdir()
def prepare_profile_project(destination, setting):
shutil.copytree(FIXTURES / "profile", destination / "module")
config = destination / "config"
config.mkdir()
write_private_json(config / "setting.json", setting)
(destination / "data").mkdir()
def prepare_cms_project(destination, source):
shutil.copytree(ROOT / "examples/cms" / source, destination)
def native_runtime_setting(settings):
return {"adapter": {"benchmark.live": {"setting": {
"limits": {
"header_bytes": 8192,
"body_bytes": 65536,
"timeout_ms": settings["timeout_ms"],
"connections": settings["connections"],
"http2": None,
},
"lifetime_ms": settings["lifetime_ms"],
}}}}
def record_binary(manifest, name, binary, started, **metadata):
manifest["binaries"][name] = {
**fingerprint(binary),
"build_seconds": time.monotonic() - started,
**metadata,
}
def build_profile_report(args, manifest, log):
report = {"source": "profile/benchmark/command/app.dever", "profiles": {}}
for profile, features in RUNTIME_PROFILES.items():
item = {"runtime_features": list(features),
"dependencies": runtime_dependencies(args.cargo, features)}
print(f"build profile-{profile}", flush=True)
binary_name = f"profile-{profile}"
binary = args.output.resolve() / binary_name
started = time.monotonic()
project = args.output.resolve() / "source" / f"profile-{profile}"
setting = profile_database_setting(profile)
prepare_profile_project(project, setting)
subprocess.run(
[args.compiler.resolve(), "build", project, "--output", binary],
stdout=log,
stderr=log,
check=True,
)
record_binary(manifest, binary_name, binary, started,
profile=profile, fixture="profile")
item.update({"status": "built", "binary": fingerprint(binary)})
report["profiles"][profile] = item
path = args.output.resolve() / "profile-report.json"
path.write_text(json.dumps(report, indent=2) + "\n")
manifest["files"]["profile-report.json"] = fingerprint(path)
def build(args):
has_protocols = args.selection in ("all", "protocols")
if args.with_go and not has_protocols:
raise ValueError("--with-go requires protocol fixtures")
postgres_setting = load_postgres_setting() if args.selection == "all" else None
settings = configuration(args, postgres_setting)
destination = args.output.resolve()
destination.mkdir(parents=True, exist_ok=False)
compiler = args.compiler.resolve()
cargo = shutil.which(str(args.cargo))
if cargo is None:
raise ValueError(f"cannot find Cargo command: {args.cargo}")
args.cargo = Path(cargo)
peers = ({name: getattr(args, name).resolve()
for name in ("network_bench", "live_bench", "async_bench")}
if has_protocols else {})
required_tools = (compiler, *peers.values())
for executable in required_tools:
if not executable.is_file():
raise ValueError(f"build the executable first: {executable}")
# 在昂贵构建之前拒绝旧环境变量协议产物;这里只读配置,不启动网络。
for name, peer in peers.items():
if name == "async_bench":
continue # The finite Rust reference takes explicit positional arguments.
with tempfile.TemporaryDirectory(prefix="dever-peer-config-") as temporary:
executable = stage_peer(peer, Path(temporary), {"workers": 2})
result = subprocess.run([executable, "check-config"], capture_output=True,
text=True, check=True, timeout=5)
if json.loads(result.stdout) != {"workers": 2}:
raise ValueError(f"benchmark peer does not implement {PEER_CONFIGURATION}: {peer}")
manifest = {
"peer_configuration": PEER_CONFIGURATION,
"application_configuration": APPLICATION_CONFIGURATION,
"selection": args.selection,
"settings": settings,
"cargo": subprocess.check_output([cargo, "-V"], text=True),
"compiler": fingerprint(compiler),
"optimization": "LLVM O2 application objects, LLD static link, selected prebuilt runtime archive",
"runtime_scheduling": "Dever uses production runtime defaults; workers/task_capacity configure Rust references; workers also sets Go GOMAXPROCS when selected",
"binaries": {},
"files": {},
}
with (destination / "build.log").open("w") as log:
for fixture, entries in (ENTRIES.items() if has_protocols else ()):
source = destination / "source" / fixture
prepare_native_project(source, fixture, settings)
print(f"build {fixture}", flush=True)
started = time.monotonic()
executable = source / "app"
subprocess.run([compiler, "build", source, "--output", executable],
stdout=log, stderr=log, check=True)
for entry in entries:
binary = destination / entry
shutil.copy2(executable, binary)
record_binary(manifest, entry, binary, started, profile="base", fixture=fixture,
command=f"benchmark.{fixture}.{entry}", shared_application=fixture)
drivers = ()
if args.selection == "all":
drivers = ("sqlite", "postgres") if postgres_setting is not None else ("sqlite",)
for driver in drivers:
project = destination / "source" / f"orm-{driver}"
try:
prepare_project(project, "orm", settings, driver, postgres_setting)
subprocess.run([compiler, "fmt", project], stdout=log, stderr=log, check=True)
for entry in ORM_ENTRIES:
name = f"{driver}-{entry.removeprefix('orm_')}"
print(f"build {name}", flush=True)
binary = destination / name
started = time.monotonic()
method = "ormSeed" if entry == "orm_http" else "orm" + entry.removeprefix("orm_").capitalize()
command_entry = f"benchmark.item.{method}"
stage_application_command(project / "module", command_entry)
subprocess.run([compiler, "build", project, "--output", binary],
stdout=log, stderr=log, check=True)
record_binary(manifest, name, binary, started, profile=driver, fixture="orm",
entry=entry)
finally:
if driver == "postgres":
(project / "config/setting.json").unlink(missing_ok=True)
cms_sources = {"all": ("dever",), "cms-profiles": ("dever",),
"profiles": (), "protocols": ()}[args.selection]
for source in cms_sources:
cms_project = destination / "source" / f"cms-{source}"
prepare_cms_project(cms_project, source)
config_name = f"source/cms-{source}/config/setting.json"
manifest["files"][config_name] = fingerprint(cms_project / "config/setting.json")
binary_name = "cms"
print(f"build cms-{source}", flush=True)
cms_binary = destination / binary_name
started = time.monotonic()
subprocess.run([compiler, "build", cms_project, "--output", cms_binary],
stdout=log, stderr=log, check=True)
record_binary(manifest, binary_name, cms_binary, started,
profile="sqlite", fixture="cms", source=source)
if args.selection != "protocols":
build_profile_report(args, manifest, log)
for name, peer in peers.items():
shutil.copy2(peer, destination / name)
manifest["binaries"][name] = fingerprint(destination / name)
if has_protocols:
shutil.copytree(TLS, destination / "tls")
for path in sorted((destination / "tls").rglob("*")):
if path.is_file():
name = path.relative_to(destination).as_posix()
manifest["files"][name] = fingerprint(path)
if args.with_go:
binary = destination / "go_http"
source = FIXTURES / "http/peer.go"
with (destination / "go.build.log").open("w") as log:
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", binary, source],
stdout=log, stderr=log, check=True, timeout=120)
manifest["binaries"]["go_http"] = fingerprint(binary)
manifest["go"] = {"compiler": subprocess.check_output(["go", "version"], text=True).strip(),
"source": fingerprint(source), "profile": "go build -trimpath -ldflags=-s -w"}
(destination / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(f"artifacts: {destination}", flush=True)
+675
View File
@@ -0,0 +1,675 @@
"""验收 CMS 的真实身份、租户、发布链及专属内存预算。"""
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
import http.client
from http.cookies import SimpleCookie
import json
import math
import os
from pathlib import Path
import secrets
import shutil
import signal
import socket
import subprocess
import tempfile
from threading import Event
import time
from urllib.parse import urlencode
from build import ROOT, fingerprint
from process import Budget, Process, observe, summarize, resource_trend
from settings import write_private_json
FORMATS = ("dever",)
ARTICLE_PATH = "/news/article/admin/manage"
ARTICLE_LIST_PATH = f"{ARTICLE_PATH}/list?size=100"
REVISION_PATH = "/content/revision/admin/manage/create"
SESSION_PATH = "/user/account/{site}/session/login"
PUBLICATIONS_PATH = "/news/article/front/browse/list?size=100"
def require(condition, message):
if not condition:
raise RuntimeError(message)
def execute(command, directory, log_name, *, envelope=False, timeout=180):
child = subprocess.Popen([str(value) for value in command], cwd=directory,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
failure = None
try:
stdout, stderr = child.communicate(timeout=timeout)
except BaseException as error:
failure = error
try:
signal_group(child.pid, signal.SIGTERM)
stdout, stderr = child.communicate(timeout=3)
except subprocess.TimeoutExpired:
signal_group(child.pid, signal.SIGKILL)
stdout, stderr = child.communicate(timeout=3)
(directory / f"{log_name}.stdout.log").write_bytes(stdout)
(directory / f"{log_name}.stderr.log").write_bytes(stderr)
if failure is not None:
raise failure
require(child.returncode == 0, f"{log_name} exited {child.returncode}: {directory}")
if envelope:
return decode_response(stdout, 200)
def signal_group(pid, signal_number):
try:
os.killpg(pid, signal_number)
except ProcessLookupError:
pass
def decode_response(body, status):
document = json.loads(body)
require(isinstance(document, dict) and set(document) == {"code", "message", "data"},
f"unexpected response envelope (HTTP {status})")
require(status == 200 and document["code"] == 0,
f"request failed: HTTP {status}, code={document['code']}, {document['message']}")
return document["data"]
def copy_cms_project(source, destination):
destination.mkdir()
for name in ("config", "module", "test"):
shutil.copytree(source / name, destination / name)
def build_cases(compiler, output):
output.mkdir(parents=True, exist_ok=False)
cases = {}
for source_format in FORMATS:
source = ROOT / "examples/cms" / source_format
project = output / source_format
copy_cms_project(source, project)
started = time.monotonic()
execute([compiler, "check", project], project, "check")
execute([compiler, "build", project, "--output", project / "cms-app"], project, "build", timeout=900)
executable = project / "cms-app"
cases[source_format] = {
"executable": str(executable), "config": str(project / "config/setting.json"),
"fingerprint": fingerprint(executable),
"build_seconds": time.monotonic() - started,
}
manifest = {"format": "dever-cms-publish-v1", "cases": cases}
write_private_json(output / "manifest.json", manifest)
return manifest
def available_port():
with socket.socket() as reservation:
reservation.bind(("127.0.0.1", 0))
return reservation.getsockname()[1]
def isolated_settings(settings, port):
settings = deepcopy(settings)
settings["runtime"]["mode"] = "api"
settings["http"] = {"host": "127.0.0.1", "port": port}
settings["auth"]["providers"]["session"]["jwtSecret"] = secrets.token_hex(32)
for site in settings["sites"].values():
site["origin"] = f"http://127.0.0.1:{port}"
site["hosts"] = ["127.0.0.1"]
database = settings["database"]["default"]
if database["type"] == "sqlite":
database["path"] = "data/db/platform.db"
database["tenant_directory"] = "data/tenants"
elif database["type"] == "postgres":
require(bool(database.get("url")) and bool(database.get("tenant_database_prefix")),
"PostgreSQL CMS acceptance requires an explicit isolated control database and tenant prefix")
else:
raise RuntimeError("CMS acceptance supports only SQLite and PostgreSQL")
settings["log"]["level"] = "error"
return settings
class CmsClient:
def __init__(self, port):
self.connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
self.origin = f"http://127.0.0.1:{port}"
self.cookies = {}
self.timings = {}
self.record_timings = True
def request(self, method, path, payload=None, *, site=None, expected=200):
headers = {"Content-Type": "application/json", "Origin": self.origin}
if site in self.cookies:
headers["Cookie"] = self.cookies[site]
body = None
if payload is not None:
if method in ("GET", "DELETE"):
query = {name: value if isinstance(value, str) else json.dumps(value)
for name, value in payload.items()}
path += ("&" if "?" in path else "?") + urlencode(query)
else:
body = json.dumps(payload)
started = time.monotonic()
self.connection.request(method, path, body, headers)
response = self.connection.getresponse()
body = response.read()
route = path.split("?")[0]
label = f"{method} {route}" + (f" expected {expected}" if expected != 200 else "")
if self.record_timings:
self.timings.setdefault(label, []).append(
(time.monotonic() - started) * 1000)
require(response.status == expected,
f"{method} {path}: HTTP {response.status}, expected {expected}: {body!r}")
if expected != 200:
document = json.loads(body)
require(document.get("code") != 0, "rejected request returned success")
return None
data = decode_response(body, response.status)
if path.endswith("/login"):
cookie = SimpleCookie()
for name, value in response.getheaders():
if name.lower() == "set-cookie":
cookie.load(value)
require(bool(cookie), "login did not return a session cookie")
self.cookies[site] = "; ".join(f"{name}={value.value}" for name, value in cookie.items())
return data
def close(self):
self.connection.close()
def wait_ready(process, client):
deadline = time.monotonic() + 20
while time.monotonic() < deadline:
process.sample()
require(process.child.poll() is None, f"CMS exited before readiness: {process.directory}")
try:
client.request("GET", ARTICLE_LIST_PATH, expected=401)
return time.monotonic() - process.started
except (ConnectionError, http.client.HTTPException, OSError):
client.close()
time.sleep(0.02)
raise TimeoutError(f"CMS readiness timeout: {process.directory}")
def login_client(client, identity, password, tenant_key):
for site in ("admin", "front"):
data = client.request("POST", SESSION_PATH.format(site=site), {
"credential_id": identity["credential_id"], "password": password, "tenant": tenant_key,
}, site=site)
require(data["account"]["id"] == identity["user_id"]
and data["tenant_id"] == identity["tenant_id"] and data["site"] == site,
"login changed the trusted identity or tenant/site")
def publish_workload(client, process, identity, password, articles, *,
tenant_key="bench", title_prefix="Article"):
login_client(client, identity, password, tenant_key)
titles = []
ids = []
for index in range(articles):
slug, title = f"bench-{index}", f"{title_prefix} {index}"
created = client.request("POST", f"{ARTICLE_PATH}/create", {
"slug": f" {slug.upper()} ", "title": f" {title} ", "body": "Draft body",
}, site="admin")
require(created["slug"] == slug and created["title"] == title,
"article creation did not normalize input")
require("author_id" not in created and "status" not in created,
"article creation leaked private owner or publication state")
changed = client.request("POST", REVISION_PATH, {
"selected_article_id": created["id"], "expected_version": created["version"],
"title": f" {title} published ", "body": "Published body",
}, site="admin")
expected_title = f"{title} published"
require(changed["title"] == expected_title
and changed["article_id"] == created["id"]
and changed["version"] == created["version"] + 1,
"revision did not preserve normalized content and advance the article version")
published = client.request("POST", f"{ARTICLE_PATH}/publish", {
"article_id": created["id"],
}, site="admin")
require(published["slug"] == slug and published["title"] == expected_title
and published["body"] == "Published body",
"publication did not reflect the stored article")
titles.append(expected_title)
ids.append(created["id"])
process.sample()
listing = client.request("GET", PUBLICATIONS_PATH, site="front")
require(listing["total"] == articles
and sorted(value["title"] for value in listing["items"]) == sorted(titles),
"front query did not return every published article exactly once")
client.request("POST", f"{ARTICLE_PATH}/publish", {"article_id": ids[0]},
site="admin", expected=409)
return {"articles": articles, "titles": sorted(titles), "duplicate_publish_status": 409}
def provision_identity(executable, directory, tenant_key):
password = secrets.token_urlsafe(24)
identity = execute([executable, "user.account.bootstrap", json.dumps({
"tenant_key": tenant_key, "tenant_name": f"Benchmark {tenant_key}",
"email": f"{tenant_key}@example.invalid", "display_name": "Benchmark",
"password": password,
})], directory, f"bootstrap-{tenant_key}", envelope=True)
execute([executable, "--tenant", identity["tenant_id"], "dever.tenant.migrate", "null"],
directory, f"migrate-{tenant_key}")
for site in ("admin", "front"):
execute([executable, "--tenant", identity["tenant_id"], "dever.auth.owner",
json.dumps({"site": site, "user_id": identity["user_id"]})],
directory, f"owner-{tenant_key}-{site}")
return identity, password
def latency_summary(timings):
result = {}
for action, samples in timings.items():
values = sorted(samples)
result[action] = {"samples": len(values), "p50_ms": values[len(values) // 2],
"max_ms": values[-1]}
return result
def catalog_key(catalog, component, domain, site, action, method):
matches = [entry["key"] for entry in catalog
if (entry["component"], entry["domain"], entry["site"],
entry["action"], entry["method"]) == (component, domain, site, action, method)]
require(len(matches) == 1 and bool(matches[0]),
f"permission catalog must uniquely identify {component}.{domain}.{site}.{action}")
return matches[0]
def run_postgres_rbac(executable, directory, port, owner, identity):
catalog = owner.request("GET", "/user/authorization/admin/manage/permissions", site="admin")
front_catalog = owner.request("GET", "/user/authorization/front/manage/permissions", site="front")
permissions = {
"admin_read": catalog_key(catalog, "news", "article", "admin", "list", "GET"),
"admin_permissions": catalog_key(catalog, "user", "authorization", "admin", "permissions", "GET"),
"front_list": catalog_key(front_catalog, "news", "article", "front", "list", "GET"),
}
require(len(set(permissions.values())) == 3, "RBAC acceptance reused a permission key")
password = secrets.token_urlsafe(24)
member = execute([executable, "user.account.fixture_member", json.dumps({
"selected_tenant_id": identity["tenant_id"], "email": f"member-{secrets.token_hex(8)}@example.invalid",
"display_name": "Acceptance member", "password": password,
})], directory, "rbac-member", envelope=True)
require(isinstance(member["user_id"], int) and not isinstance(member["user_id"], bool)
and member["user_id"] > 0 and member["user_id"] != identity["user_id"]
and member["tenant_id"] == identity["tenant_id"],
"RBAC acceptance did not create a distinct member in the owner's tenant")
probes = (("admin", ARTICLE_LIST_PATH),
("admin", "/user/authorization/admin/manage/permissions"),
("front", PUBLICATIONS_PATH))
client = CmsClient(port)
try:
login_client(client, member, password, "tenant_a")
for site, path in probes:
client.request("GET", path, site=site, expected=403)
roles = (("admin", "fixture-reader", "admin_read"),
("admin", "fixture-catalog", "admin_permissions"),
("front", "fixture-reader", "front_list"))
for site, role_id, permission in roles:
base = f"/user/authorization/{site}/manage"
owner.request("POST", f"{base}/save", {
"id": role_id, "name": role_id, "all_permissions": False,
"permission_keys": [permissions[permission]],
}, site=site)
owner.request("POST", f"{base}/grant", {
"user_id": member["user_id"], "role_id": role_id,
}, site=site)
for site, path in probes:
client.request("GET", path, site=site)
owner.request("DELETE", "/user/authorization/admin/manage/revoke", {
"user_id": member["user_id"], "role_id": "fixture-reader",
}, site="admin")
client.request("GET", ARTICLE_LIST_PATH, site="admin", expected=403)
client.request("GET", probes[1][1], site="admin")
client.request("GET", PUBLICATIONS_PATH, site="front")
for site, role_id, path in (("admin", "fixture-catalog", probes[1][1]),
("front", "fixture-reader", PUBLICATIONS_PATH)):
owner.request("DELETE", f"/user/authorization/{site}/manage/revoke", {
"user_id": member["user_id"], "role_id": role_id,
}, site=site)
client.request("GET", path, site=site, expected=403)
finally:
client.close()
return {"member_user_id": member["user_id"], "permissions": permissions,
"before_grant_denied": True, "both_admin_roles_allowed": True,
"revoked_role_denied": True, "second_role_still_allowed": True,
"front_same_named_role_still_allowed": True}
class ReadLatency:
"""固定内存的对数直方图;分位数报告桶上界,最大相邻桶比例约1.011。"""
def __init__(self):
self.buckets = [0] * 2048
self.count = 0
def add(self, milliseconds):
index = math.ceil(math.log2(1 + milliseconds) * 64)
require(0 <= index < len(self.buckets), "CMS latency exceeds histogram bounds")
self.buckets[index] += 1
self.count += 1
def merge(self, other):
self.buckets = [left + right for left, right in zip(self.buckets, other.buckets)]
self.count += other.count
def percentile(self, fraction):
threshold = math.ceil(self.count * fraction)
count = 0
for index, samples in enumerate(self.buckets):
count += samples
if count >= threshold:
return 2 ** (index / 64) - 1
raise ValueError("empty latency histogram")
def concurrent_reads(port, cookies, expected, process, *, concurrency, seconds, interval):
"""Each closed-loop client keeps its own connection and verifies every reply."""
stop = Event()
started = time.monotonic()
deadline = started + seconds
first = len(process.samples)
def read_publications():
client = CmsClient(port)
client.cookies = cookies.copy()
client.record_timings = False
latency = ReadLatency()
try:
while not stop.is_set() and time.monotonic() < deadline:
request_started = time.monotonic()
listing = client.request("GET", PUBLICATIONS_PATH, site="front")
require(listing == expected, "concurrent CMS read changed publication results")
latency.add((time.monotonic() - request_started) * 1000)
return latency
except BaseException:
stop.set()
raise
finally:
client.close()
with ThreadPoolExecutor(max_workers=concurrency) as clients:
futures = [clients.submit(read_publications) for _ in range(concurrency)]
try:
while any(not future.done() for future in futures):
process.check()
observe([process], interval, interval)
elapsed = time.monotonic() - started
latency = ReadLatency()
for future in futures:
latency.merge(future.result())
finally:
stop.set()
require(latency.count > 0, "concurrent CMS read completed without a successful request")
require(elapsed >= seconds, "concurrent CMS read ended before the measurement window")
process.sample()
return {"mode": "closed_loop_authenticated_read", "concurrency": concurrency,
"requested_seconds": seconds, "elapsed_seconds": elapsed,
"succeeded": latency.count, "errors": 0, "result_checked_on_every_request": True,
"actual_success_qps": latency.count / elapsed,
"latency_measurement": "request and reply validation; logarithmic bucket upper bound, 64 buckets per octave",
"request_latency_ms": {name: latency.percentile(fraction)
for name, fraction in (("p50", .5), ("p95", .95), ("p99", .99))},
"resources": summarize(process.samples[first:]),
"trend": resource_trend(process.samples[first:]) if len(process.samples[first:]) >= 4 else None}
def sqlite_pool_descriptor_limits(settings, directory):
database = settings["database"]["default"]
require(database["type"] == "sqlite", "CMS read benchmark requires isolated SQLite")
databases = [directory / database["path"]]
databases.extend((directory / database["tenant_directory"]).glob("*.db"))
# 每个连接可持有主文件与WAL;同进程各连接共用一个SHM句柄。
return {str(path.resolve()) + suffix: limit for path in databases
for suffix, limit in (("", database["max_connections"]),
("-wal", database["max_connections"]), ("-shm", 1))}
def validate_read_descriptor_recovery(load):
before = Counter(load["descriptors_before"].values())
after = Counter(load["descriptors_after"].values())
for path, limit in load["pooled_descriptor_limits"].items():
require(after[path] <= limit, f"CMS SQLite descriptor count exceeds pool limit: {path}")
before.pop(path, None)
after.pop(path, None)
require(before == after,
f"CMS read descriptors did not recover: added={dict(after - before)}, missing={dict(before - after)}")
def run_case(case, directory, *, articles, cgroup_parent, memory_mib,
idle_seconds=0.2, interval=0.05, server_cpu=None, cpu_quota=None,
concurrency=0, read_seconds=5):
executable = Path(case["executable"])
require(fingerprint(executable) == case["fingerprint"], "CMS artifact fingerprint changed")
directory.mkdir(parents=True, exist_ok=False)
target = directory / "cms-app"
shutil.copy2(executable, target)
(directory / "config").mkdir()
(directory / "data").mkdir()
port = available_port()
settings = isolated_settings(json.loads(Path(case["config"]).read_text()), port)
write_private_json(directory / "config/setting.json", settings)
identity, password = provision_identity(target, directory, "bench")
budget = Budget(cgroup_parent, memory_mib, cpu_quota)
client = CmsClient(port)
failure = None
result = {}
try:
with Process([target], directory / "server", budget=budget,
cpus=(server_cpu,) if server_cpu is not None else None) as process:
result["raw"] = process.samples
ready_seconds = wait_ready(process, client)
started = time.monotonic()
contract = publish_workload(client, process, identity, password, articles)
elapsed = time.monotonic() - started
result.update(contract=contract, ready_seconds=ready_seconds,
workload_seconds=elapsed, published_articles_per_second=articles / elapsed,
latency=latency_summary(client.timings))
if concurrency:
expected = client.request("GET", PUBLICATIONS_PATH, site="front")
# 控制连接不参与长读负载,先关闭,避免其空闲超时改变基线。
client.close()
observe([process], 0.3, interval)
descriptors_before = process.descriptors()
pooled_limits = sqlite_pool_descriptor_limits(settings, directory)
read_load = {"descriptors_before": descriptors_before,
"pooled_descriptor_limits": pooled_limits}
result["concurrent_reads"] = read_load
read_load.update(concurrent_reads(port, client.cookies, expected, process,
concurrency=concurrency, seconds=read_seconds, interval=interval))
observe([process], 0.3, interval)
descriptors_after = process.descriptors()
read_load.update(descriptors_after=descriptors_after,
fd_delta_after_disconnect=len(descriptors_after) - len(descriptors_before))
validate_read_descriptor_recovery(read_load)
observe([process], idle_seconds, interval)
process.sample()
process.check()
require(process.child.poll() is None, "CMS exited during the publish workload")
resources = summarize(process.samples)
result.update(resources=resources, budget_before_stop=budget.sample())
except BaseException as error:
failure = error
finally:
client.close()
try:
budget.close()
except BaseException as error:
if failure is None:
failure = error
else:
failure.add_note(f"cannot close owned CMS budget: {error}")
result["cgroup"] = budget.final
if failure is None and budget.final is not None and (
budget.final["events"]["oom"] or budget.final["events"]["oom_kill"]):
failure = RuntimeError("CMS exceeded its memory budget")
result["status"] = "failed" if failure else "passed"
if failure is not None:
result["error"] = str(failure)
write_private_json(directory / "report.json", result)
if failure is not None:
raise failure
return result
def run_postgres_case(project, output, compiler, *, articles, rbac_fixture=False):
# 构建、临时密钥和端口只写测试副本,调用者的项目配置始终保持不变。
with tempfile.TemporaryDirectory(prefix="dever-cms-postgres-") as temporary:
staged = Path(temporary) / "project"
copy_cms_project(project, staged)
return run_staged_postgres_case(staged, output, compiler, articles=articles,
rbac_fixture=rbac_fixture)
def run_staged_postgres_case(project, output, compiler, *, articles, rbac_fixture):
require(1 <= articles <= 100, "PostgreSQL CMS acceptance requires 1..100 articles")
output.mkdir(parents=True, exist_ok=False)
report = {"status": "running", "source": "dever", "tenants": {},
"scope": "PostgreSQL HTTP owner flow and physical database tenant isolation; not non-owner RBAC revocation"}
if rbac_fixture:
report["scope"] = "PostgreSQL HTTP owner flow, physical database tenant isolation and non-owner multi-role/site grant-revoke"
clients = {}
try:
setting_path = project / "config/setting.json"
initial = json.loads(setting_path.read_text())
require(initial["database"]["default"]["type"] == "postgres",
"postgres-case requires a PostgreSQL project configuration")
port = available_port()
write_private_json(setting_path, isolated_settings(initial, port), replace=True)
execute([compiler, "check", project], output, "check")
executable = project / "cms-app"
execute([compiler, "build", project, "--output", executable], output, "build", timeout=900)
report["binary"] = fingerprint(executable)
identities = {key: provision_identity(executable, output, key)
for key in ("tenant_a", "tenant_b")}
require(identities["tenant_a"][0]["tenant_id"] != identities["tenant_b"][0]["tenant_id"],
"PostgreSQL acceptance reused a tenant identity")
clients = {key: CmsClient(port) for key in identities}
with Process([executable], output / "server") as process:
report["ready_seconds"] = wait_ready(process, clients["tenant_a"])
for key, (identity, password) in identities.items():
contract = publish_workload(clients[key], process, identity, password, articles,
tenant_key=key, title_prefix=key)
permissions = clients[key].request(
"GET", "/user/authorization/admin/manage/permissions", site="admin")
require(bool(permissions), "CMS permission catalog is empty")
report["tenants"][key] = {"tenant_id": identity["tenant_id"],
"user_id": identity["user_id"], "contract": contract}
for key, client in clients.items():
listing = client.request("GET", PUBLICATIONS_PATH, site="front")
contract = report["tenants"][key]["contract"]
require(listing["total"] == articles
and sorted(value["title"] for value in listing["items"]) == contract["titles"],
"PostgreSQL tenant query leaked another tenant's articles")
require(sorted(value["slug"] for value in listing["items"])
== sorted(f"bench-{index}" for index in range(articles)),
"PostgreSQL tenants did not preserve the same slug namespace")
report["tenants"][key]["slugs"] = sorted(value["slug"] for value in listing["items"])
identity, password = identities["tenant_a"]
clients["tenant_a"].request("POST", SESSION_PATH.format(site="admin"), {
"credential_id": identity["credential_id"], "password": password, "tenant": "tenant_b",
}, site="admin", expected=401)
front_cookie = clients["tenant_a"].cookies["front"]
clients["tenant_a"].cookies["front"] = clients["tenant_a"].cookies["admin"]
clients["tenant_a"].request("GET", PUBLICATIONS_PATH, site="front", expected=401)
clients["tenant_a"].cookies["front"] = front_cookie
if rbac_fixture:
report["rbac"] = run_postgres_rbac(executable, output, port,
clients["tenant_a"], identity)
process.sample()
process.check()
require(process.child.poll() is None, "PostgreSQL CMS exited during tenant acceptance")
report["resources"] = summarize(process.samples)
report["cross_site_cookie_rejected"] = True
report["cross_tenant_login_rejected"] = True
report["status"] = "passed"
except BaseException as error:
report["status"] = "failed"
report["error"] = str(error)
raise
finally:
for client in clients.values():
client.close()
write_private_json(output / "report.json", report)
return report
def run_cases(manifest, output, *, articles, cgroup_parent, memory_mib,
concurrency=0, read_seconds=5):
require(manifest["format"] == "dever-cms-publish-v1", "unexpected CMS manifest format")
require(1 <= articles <= 100, "articles must be 1..100")
require(0 <= concurrency <= 64, "read concurrency must be 0..64")
require(math.isfinite(read_seconds) and 0.1 <= read_seconds <= 3600,
"read seconds must be finite and within 0.1..3600")
require((memory_mib is None) == (cgroup_parent is None),
"memory budget and cgroup parent must be provided together")
output.mkdir(parents=True, exist_ok=False)
report = {"format": manifest["format"], "memory_mib": memory_mib,
"scope": "owned loopback HTTP/1, sequential publish, isolated SQLite; not a saturation result",
"cases": {}}
if concurrency:
report["scope"] = "owned loopback HTTP/1, sequential publish and closed-loop authenticated reads, isolated SQLite"
try:
for source_format in FORMATS:
report["cases"][source_format] = run_case(manifest["cases"][source_format],
output / source_format, articles=articles,
cgroup_parent=cgroup_parent, memory_mib=memory_mib,
concurrency=concurrency, read_seconds=read_seconds)
report["status"] = "passed"
except BaseException as error:
report["status"] = "failed"
report["error"] = str(error)
raise
finally:
for source_format in FORMATS:
case_report = output / source_format / "report.json"
if source_format not in report["cases"] and case_report.is_file():
report["cases"][source_format] = json.loads(case_report.read_text())
write_private_json(output / "report.json", report)
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
actions = parser.add_subparsers(dest="action", required=True)
build = actions.add_parser("build")
build.add_argument("--compiler", type=Path, default=ROOT / "target/debug/dever")
build.add_argument("--output", type=Path, required=True)
run = actions.add_parser("run")
run.add_argument("--manifest", type=Path, required=True)
run.add_argument("--output", type=Path, required=True)
run.add_argument("--articles", type=int, default=16)
run.add_argument("--cgroup-parent", type=Path)
run.add_argument("--memory-mib", type=int, choices=(64, 128))
run.add_argument("--concurrency", type=int, default=0,
help="0 disables load; otherwise 1..64 independent authenticated read clients")
run.add_argument("--read-seconds", type=float, default=5)
postgres = actions.add_parser("postgres-case")
postgres.add_argument("--project", type=Path, required=True)
postgres.add_argument("--output", type=Path, required=True)
postgres.add_argument("--compiler", type=Path, default=ROOT / "target/debug/dever")
postgres.add_argument("--articles", type=int, default=2)
postgres.add_argument("--rbac-fixture", action="store_true",
help="run test-only member CMD provided by the Rust PostgreSQL fixture")
args = parser.parse_args()
if args.action == "build":
result = build_cases(args.compiler.resolve(strict=True), args.output.resolve())
elif args.action == "postgres-case":
report = run_postgres_case(args.project.resolve(strict=True), args.output.resolve(),
args.compiler.resolve(strict=True), articles=args.articles,
rbac_fixture=args.rbac_fixture)
result = {"status": report["status"], "report": str(args.output.resolve() / "report.json")}
else:
result = run_cases(json.loads(args.manifest.read_text()), args.output.resolve(),
articles=args.articles, cgroup_parent=args.cgroup_parent,
memory_mib=args.memory_mib, concurrency=args.concurrency,
read_seconds=args.read_seconds)
result = {"status": result["status"], "memory_mib": result["memory_mib"],
"report": str(args.output / "report.json")}
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
+200
View File
@@ -0,0 +1,200 @@
"""同 Pod 两个隔离容器:server 执行固定矩阵,client 只负责施压。
构建产物与本文件、process.py、settings.py 放入 /work;两个角色通过 emptyDir
交接有限测试命令。测量采用闭环长连接,延迟含收发和正文验证,不含建连时间。
"""
import argparse
import hashlib
import http.client
import json
import os
from pathlib import Path
import subprocess
import time
from process import Process, summarize
ROOT = Path("/work")
def write(path, value):
temporary = path.with_suffix(".tmp")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
temporary.replace(path)
def counters():
return {name: (Path("/sys/fs/cgroup") / name).read_text()
for name in ("cpu.max", "cpu.stat", "memory.max", "memory.events")}
def client():
request = ROOT / "request.json"
while not (ROOT / "finished").exists():
if not request.exists():
time.sleep(0.02)
continue
item = json.loads(request.read_text())
request.unlink()
directory = ROOT / item["name"]
before = counters()
with Process([ROOT / "bin/rust-http", "load", item["port"], item["path"],
item["concurrency"], item["seconds"]], directory, cpus={2, 3, 4, 5}) as process:
deadline = time.monotonic() + item["seconds"] + 30
while process.child.poll() is None and time.monotonic() < deadline:
process.sample()
time.sleep(0.2)
if process.child.poll() is None:
raise TimeoutError(f"load client timed out: {item}")
result = {"exit_code": process.child.returncode, "stdout": process.text(),
"stderr": (directory / "stderr.log").read_text(),
"resources": summarize(process.samples), "samples": process.samples,
"cgroup_before": before, "cgroup_after": counters()}
write(directory / "result.json", result)
def load(process, name, port, path, concurrency, seconds):
result_path = ROOT / name / "result.json"
write(ROOT / "request.json", {"name": name, "port": port, "path": path,
"concurrency": concurrency, "seconds": seconds})
deadline = time.monotonic() + seconds + 40
while not result_path.exists() and time.monotonic() < deadline:
process.sample()
time.sleep(0.2)
result = json.loads(result_path.read_text())
if result["exit_code"] == 0:
result["measurement"] = json.loads(result["stdout"])
return result
def probe(port):
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
try:
connection.request("GET", "/json")
response = connection.getresponse()
return response.status == 200 and response.read() == b'{"ok":true}'
except OSError:
return False
finally:
connection.close()
def command(language, capacity=2048):
if language == "dever":
return [ROOT / "bin" / ("dever-http" if capacity == 2048 else "dever-overload")]
if language == "rust":
return [ROOT / "bin/rust-http", "serve", capacity]
binary = {"go": "go-http", "axum": "axum-http", "fiber": "fiber-http"}[language]
return [ROOT / "bin" / binary, capacity]
def measure(language, path, concurrency, seconds, name):
with Process(command(language), ROOT / name / "server", cpus={0, 1}) as process:
port = process.ready()
if not probe(port):
raise RuntimeError(f"smoke failed: {name}")
warmup = load(process, name + "/warmup", port, path, concurrency, 2)
process.samples.clear()
before = counters()
result = load(process, name + "/load", port, path, concurrency, seconds)
after = counters()
recovered = probe(port)
record = {"name": name, "language": language, "path": path,
"concurrency": concurrency, "seconds": seconds,
"warmup": warmup, "client": result,
"server": summarize(process.samples), "samples": process.samples,
"server_alive": process.child.poll() is None, "recovery_probe": recovered,
"stderr": (process.directory / "stderr.log").read_text(),
"cgroup_before": before, "cgroup_after": after}
write(ROOT / name / "result.json", record)
with (ROOT / "results.jsonl").open("a") as output:
output.write(json.dumps(record) + "\n")
print(json.dumps({"name": name, "measurement": result.get("measurement"),
"rss_mib": record["server"]["rss_high_water_bytes"] / 1048576,
"server_cores": record["server"]["cpu_cores_sampled"],
"client_cores": result["resources"]["cpu_cores_sampled"],
"recovery": recovered}), flush=True)
for phase in (warmup, result):
measurement = phase.get("measurement", {})
if phase["exit_code"] != 0 or measurement.get("errors") != 0 or not measurement.get("succeeded"):
raise RuntimeError(f"load failed; retained result: {name}")
if not record["server_alive"] or not recovered:
raise RuntimeError(f"server did not recover: {name}")
def overload(language):
name = "overload/" + language
with Process(command(language, 8192), ROOT / name / "server", cpus={0, 1}) as process:
port = process.ready()
connections = []
successful = 0
failed = 0
started = time.monotonic()
try:
for _ in range(4500):
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=2)
connections.append(connection)
connection.request("GET", "/json")
response = connection.getresponse()
if response.status != 200 or response.read() != b'{"ok":true}':
failed += 1
break
successful += 1
except (OSError, http.client.HTTPException):
failed += 1
held_seconds = time.monotonic() - started
process.sample()
for connection in connections:
connection.close()
time.sleep(6)
recovered = probe(port)
record = {"language": language, "connections_target": 4500,
"successful_connections": successful, "failed": failed,
"open_seconds": held_seconds, "server_alive": process.child.poll() is None,
"recovered_after_release": recovered, "stdout": process.text(),
"stderr": (process.directory / "stderr.log").read_text(), "samples": process.samples}
write(ROOT / name / "result.json", record)
print(json.dumps(record), flush=True)
def server(mode, suite):
languages = ["dever", "axum", "fiber"] if suite == "frameworks" else ["dever", "rust", "go"]
if mode == "smoke":
for language in languages:
for path in ("/json", "/echo"):
measure(language, path, 4, 1, f"smoke/{language}/{path[1:]}")
return
manifest = {"suite": suite, "languages": languages,
"protocol": "HTTP/1.1", "client": "closed loop; one outstanding request per connection",
"server_cpus": [0, 1], "client_cpus": [2, 3, 4, 5],
"replicates": 3, "seconds": 12, "warmup_seconds": 2,
"server_capacity": 2048, "concurrency": [64, 256, 1024],
"paths": {"/json": "GET; 11-byte JSON", "/echo": "POST; 65536-byte body returned unchanged"},
"hashes": {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in (ROOT / "bin").iterdir()}}
write(ROOT / "manifest.json", manifest)
for replicate in range(3):
for path in ("/json", "/echo"):
for concurrency in (64, 256, 1024):
for language in languages[replicate:] + languages[:replicate]:
name = f"matrix/{replicate}/{path[1:]}/{concurrency}/{language}"
measure(language, path, concurrency, 12, name)
for language in languages:
measure(language, "/json", 1024, 60, "sustained/" + language)
if suite == "core":
overload(language)
(ROOT / "finished").touch()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("role", choices=("server", "client"))
parser.add_argument("--mode", choices=("smoke", "matrix"), default="matrix")
parser.add_argument("--suite", choices=("core", "frameworks"), default="core")
arguments = parser.parse_args()
if arguments.role == "client":
client()
else:
try:
server(arguments.mode, arguments.suite)
finally:
(ROOT / "finished").touch()
@@ -0,0 +1,13 @@
type Identity {
id: Text
user_id: model.id?
tenant_id: model.id?
}
verify(claims: dever.auth.Claims) (identity: Identity) {
identity = Identity {
id = claims.subject
user_id = null
tenant_id = null
}
}
@@ -0,0 +1,3 @@
global type Auth {
label: Text(1, 64)
}
@@ -0,0 +1,3 @@
public post echo = app.echo
public post raw = app.raw
@@ -0,0 +1,19 @@
type Row {
id: Int
text: Text
weight: Float
note: Text?
}
type Payload {
rows: List<Row>
raw: Json
}
echo(payload: Payload) (value: Payload) {
value = payload
}
raw(source: Json) (value: Json) {
value = source
}
@@ -0,0 +1,51 @@
use std::io::Write;
use std::path::Path;
use dever_backend_bridge::{Target, emit_object};
use dever_core::source::SourceMap;
use dever_runtime::config::Settings;
fn main() -> Result<(), String> {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
let (project, output, ir) = match arguments.as_slice() {
[project, output] => (project, output, None),
[project, output, ir] => (project, output, Some(ir)),
_ => {
return Err("usage: api-json-object <project> <new-object-file> [new-ir-file]".into());
}
};
let project = Path::new(project);
let sources =
SourceMap::load(&project.join("module")).map_err(|errors| format!("{errors:?}"))?;
let settings = Settings::load_project(project).map_err(|error| error.to_string())?;
let program = dever_core::check_with_settings(&sources, &settings).map_err(|errors| {
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
})?;
let errors = program.application_errors()?;
if !errors.is_empty() {
return Err(errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>());
}
let module = dever_core::llvm::emit_executable(&program, &sources)?;
if let Some(ir) = ir {
let mut ir = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(ir)
.map_err(|error| error.to_string())?;
ir.write_all(module.as_bytes())
.map_err(|error| error.to_string())?;
}
let object = emit_object(&module, Target::LinuxX86_64)?;
let mut output = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(output)
.map_err(|error| error.to_string())?;
output.write_all(&object).map_err(|error| error.to_string())
}
+115
View File
@@ -0,0 +1,115 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"syscall"
"time"
)
type row struct {
ID int64 `json:"id"`
Text string `json:"text"`
Weight float64 `json:"weight"`
Note *string `json:"note"`
}
type payload struct {
Rows []row `json:"rows"`
Raw json.RawMessage `json:"raw"`
}
type envelope struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
func handle(writer http.ResponseWriter, request *http.Request) {
typed := request.URL.Path == "/benchmark/echo/echo"
if request.Method != http.MethodPost || (!typed && request.URL.Path != "/benchmark/echo/raw") {
http.NotFound(writer, request)
return
}
body := http.MaxBytesReader(writer, request.Body, 16*1024*1024)
defer body.Close()
decoder := json.NewDecoder(body)
decoder.DisallowUnknownFields()
var result any
var err error
if typed {
var input struct {
Payload payload `json:"payload"`
}
err = decoder.Decode(&input)
result = input.Payload
} else {
var input struct {
Source json.RawMessage `json:"source"`
}
err = decoder.Decode(&input)
result = input.Source
}
var extra any
if err != nil || decoder.Decode(&extra) != io.EOF {
http.Error(writer, "invalid JSON", http.StatusBadRequest)
return
}
encoded, err := json.Marshal(envelope{0, "ok", result})
if err != nil {
http.Error(writer, "invalid response", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json")
writer.Write(encoded)
}
func run() error {
runtime.GOMAXPROCS(1)
content, err := os.ReadFile("config/setting.json")
if err != nil {
return err
}
var settings struct {
HTTP struct {
Port uint16 `json:"port"`
} `json:"http"`
}
if err := json.Unmarshal(content, &settings); err != nil {
return err
}
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", settings.HTTP.Port))
if err != nil {
return err
}
server := &http.Server{Handler: http.HandlerFunc(handle), ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 10 * time.Second}
stopping := make(chan os.Signal, 1)
signal.Notify(stopping, syscall.SIGTERM)
defer signal.Stop(stopping)
closed := make(chan error, 1)
go func() {
<-stopping
context, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
closed <- server.Shutdown(context)
}()
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
return err
}
return <-closed
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
+131
View File
@@ -0,0 +1,131 @@
use std::convert::Infallible;
use std::time::Duration;
use bytes::Bytes;
use http_body_util::{BodyExt, Full, Limited};
use hyper::{
Request, Response, StatusCode, body::Incoming, server::conn::http1, service::service_fn,
};
use hyper_util::rt::TokioIo;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use tokio::net::TcpListener;
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::watch;
use tokio::task::JoinSet;
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Row {
id: i64,
text: String,
weight: f64,
note: Option<String>,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Payload {
rows: Vec<Row>,
raw: Box<RawValue>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TypedRequest {
payload: Payload,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawRequest {
source: Box<RawValue>,
}
#[derive(Serialize)]
struct Envelope<Value> {
code: u8,
message: &'static str,
data: Value,
}
async fn handle(request: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let typed = request.uri().path() == "/benchmark/echo/echo";
if request.method() != hyper::Method::POST
|| !typed && request.uri().path() != "/benchmark/echo/raw"
{
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::new()))
.unwrap());
}
let body = Limited::new(request.into_body(), 16 * 1024 * 1024)
.collect()
.await;
let encoded = body.map_err(|error| error.to_string()).and_then(|body| {
let bytes = body.to_bytes();
if typed {
serde_json::from_slice::<TypedRequest>(&bytes).and_then(|value| {
serde_json::to_vec(&Envelope {
code: 0,
message: "ok",
data: value.payload,
})
})
} else {
serde_json::from_slice::<RawRequest>(&bytes).and_then(|value| {
serde_json::to_vec(&Envelope {
code: 0,
message: "ok",
data: value.source,
})
})
}
.map_err(|error| error.to_string())
});
let (status, body) = match encoded {
Ok(body) => (StatusCode::OK, body),
Err(_) => (StatusCode::BAD_REQUEST, Vec::new()),
};
Ok(Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
tokio::runtime::Builder::new_current_thread().enable_all().build()?.block_on(async {
let settings: serde_json::Value = serde_json::from_slice(&std::fs::read("config/setting.json")?)?;
let port = u16::try_from(settings["http"]["port"].as_u64().ok_or("missing port")?)?;
let listener = TcpListener::bind(("127.0.0.1", port)).await?;
let mut terminate = signal(SignalKind::terminate())?;
let (shutdown, stopping) = watch::channel(false);
let mut connections = JoinSet::new();
loop {
tokio::select! {
_ = terminate.recv() => break,
Some(result) = connections.join_next(), if !connections.is_empty() => { result?; },
accepted = listener.accept() => {
let (stream, _) = accepted?;
stream.set_nodelay(true)?;
let mut stopping = stopping.clone();
connections.spawn(async move {
let connection = http1::Builder::new().serve_connection(TokioIo::new(stream), service_fn(handle));
tokio::pin!(connection);
tokio::select! {
_ = &mut connection => {},
_ = stopping.changed() => {
connection.as_mut().graceful_shutdown();
let _ = tokio::time::timeout(Duration::from_secs(2), connection).await;
}
}
});
}
}
}
shutdown.send(true)?;
while let Some(result) = connections.join_next().await { result?; }
Ok(())
})
}
@@ -0,0 +1,246 @@
use dever_runtime::api;
use dever_runtime::application::Session;
use dever_runtime::auth;
use dever_runtime::config::RuntimeProfile;
use dever_runtime::http::{Request, Response};
use dever_runtime::wire::decode::{self, Fields, Reader};
use dever_runtime::wire::{Encoded, Encoder};
use dever_runtime::{lifecycle, task};
#[derive(Default)]
struct Row {
id: i64,
text: String,
weight: f64,
note: Option<String>,
}
impl Fields for Row {
fn count(&self) -> usize {
4
}
fn index(&self, name: &str) -> Result<usize, String> {
match name {
"id" => Ok(0),
"text" => Ok(1),
"weight" => Ok(2),
"note" => Ok(3),
_ => Err("unknown wire record field".into()),
}
}
fn required(&self, index: usize) -> bool {
index != 3
}
fn read(&mut self, index: usize, reader: &mut Reader<'_, '_>) -> Result<(), String> {
match index {
0 => self.id = reader.int()?,
1 => self.text = reader.text()?.into_owned(),
2 => self.weight = reader.float()?,
3 => {
reader.nullable(&mut |reader| {
self.note = Some(reader.text()?.into_owned());
Ok(())
})?;
}
_ => return Err("invalid wire field index".into()),
}
Ok(())
}
}
#[derive(Default)]
struct Payload {
rows: Vec<Row>,
raw: Option<Encoded>,
}
impl Fields for Payload {
fn count(&self) -> usize {
2
}
fn index(&self, name: &str) -> Result<usize, String> {
match name {
"rows" => Ok(0),
"raw" => Ok(1),
_ => Err("unknown wire record field".into()),
}
}
fn required(&self, _: usize) -> bool {
true
}
fn read(&mut self, index: usize, reader: &mut Reader<'_, '_>) -> Result<(), String> {
match index {
0 => reader.list(&mut |reader| {
let mut row = Row::default();
reader.record(&mut row)?;
self.rows.push(row);
Ok(())
}),
1 => {
self.raw = Some(reader.json()?);
Ok(())
}
_ => Err("invalid wire field index".into()),
}
}
}
impl Payload {
fn encode(self) -> Result<Encoded, String> {
let mut encoder = Encoder::default();
encoder.compiled_record(2, |encoder| {
encoder.compiled_key("\"rows\":")?;
encoder.begin_array()?;
for row in self.rows {
encoder.compiled_record(4, |encoder| {
encoder.compiled_key("\"id\":")?;
encoder.int(row.id)?;
encoder.compiled_key(",\"text\":")?;
encoder.text(&row.text)?;
encoder.compiled_key(",\"weight\":")?;
encoder.float(row.weight)?;
encoder.compiled_key(",\"note\":")?;
match row.note {
Some(note) => encoder.text(&note),
None => encoder.null(),
}
})?;
}
encoder.end()?;
encoder.compiled_key(",\"raw\":")?;
encoder.encoded(&self.raw.ok_or("missing required wire field")?)
})?;
encoder.finish()
}
}
enum Value {
Typed(Payload),
Raw(Encoded),
}
struct Arguments {
typed: bool,
value: Option<Value>,
}
impl Fields for Arguments {
fn count(&self) -> usize {
1
}
fn index(&self, name: &str) -> Result<usize, String> {
if name == if self.typed { "payload" } else { "source" } {
Ok(0)
} else {
Err("unknown wire record field".into())
}
}
fn required(&self, _: usize) -> bool {
true
}
fn read(&mut self, _: usize, reader: &mut Reader<'_, '_>) -> Result<(), String> {
self.value = Some(if self.typed {
let mut payload = Payload::default();
reader.record(&mut payload)?;
Value::Typed(payload)
} else {
Value::Raw(reader.json()?)
});
Ok(())
}
}
async fn call(mut request: Request, typed: bool) -> Response {
if let Err(error) = auth::require_components(&[], &[]).await {
return api::auth_error(error);
}
if let Err(error) = api::upload::read_body(&mut request).await {
return api::invalid_input(error);
}
let body = match api::Inputs::json_body(&request) {
Ok(body) => body,
Err(error) => return api::invalid_input(error),
};
let mut arguments = Arguments { typed, value: None };
if let Err(error) = decode::read(body, &mut |reader| reader.record(&mut arguments)) {
return api::invalid_input(api::InputError(error));
}
let encoded = match arguments.value {
Some(Value::Typed(payload)) => payload.encode(),
Some(Value::Raw(value)) => Ok(value),
None => Err("missing required wire field".into()),
};
match encoded {
Ok(value) => {
if api::commit_response_metadata().is_err() {
return api::internal_error();
}
api::success(value)
}
Err(_) => api::internal_error(),
}
}
async fn route(request: Request) -> Result<Response, String> {
let typed = match request.target.split('?').next().unwrap_or("") {
"/benchmark/echo/echo" => true,
"/benchmark/echo/raw" => false,
_ => return Ok(api::not_found()),
};
if request.method != "POST" {
return Ok(api::method_not_allowed(&["POST"]));
}
let prepared = match auth::prepare(&[], &request, true) {
Ok(prepared) => prepared,
Err(error) => return Ok(api::auth_error(error)),
};
let identity = prepared.claims().map(|claims| auth::Identity {
id: claims.subject.clone(),
user_id: None,
tenant_id: None,
});
Ok(
match auth::scope(prepared, identity, call(request, typed)).await {
Ok(response) => response,
Err(error) => api::auth_error(error),
},
)
}
fn main() -> Result<(), String> {
let session = Session::load(
RuntimeProfile {
sqlite: true,
postgres: false,
},
&[(None, "benchmark", false)],
&[],
)?;
session.initialize_authorization(false)?;
let cleanup = session.clone();
task::run_entry_with_typed_cleanup(
task::RuntimeConfig::default(),
session.clone().scope(async move {
session
.databases()
.prepare()
.await
.map_err(|error| error.to_string())?;
lifecycle::configure(true, false)?;
lifecycle::run(api::serve(route)).await
}),
|result| async move {
let closed = cleanup.close().await;
result.and(closed)
},
)
}
@@ -0,0 +1,112 @@
#define _POSIX_C_SOURCE 200809L
#include "runtime.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
extern long long dever_test_allocations(void);
static void text_clone(const void *source, void *target) {
*(void **)target = dever_rt_v1_text_retain(*(void *const *)source);
}
static void text_drop(void *value) {
dever_rt_v1_text_release(*(void **)value);
*(void **)value = NULL;
}
static uint8_t text_equal(const void *left, const void *right) {
return dever_rt_v1_text_equal(*(void *const *)left, *(void *const *)right);
}
static const DeverRtType text_type = {
sizeof(void *), _Alignof(void *), text_clone, text_drop, text_equal, NULL
};
static uint32_t input_field(void *inputs, void *out, DeverRtBuffer *error) {
const DeverRtWireName name = {(const uint8_t *)"source", 6};
uint8_t present = 0;
return dever_rt_v1_cmd_input_field(inputs, &name, 1, out, &present, error);
}
static uint32_t decode(const void *reader, void *out, DeverRtBuffer *error) {
return dever_rt_v1_wire_read_raw(reader, out, error);
}
static uint32_t encode(const void *value, void *encoder, DeverRtBuffer *error) {
return dever_rt_v1_wire_encoder_json(encoder, *(void *const *)value, error);
}
static uint32_t operation(unsigned phase, void *body, void *source, void **out, DeverRtBuffer *error) {
if (phase == 0) return dever_rt_v1_cmd_decode(body, &text_type, input_field, out, error);
if (phase == 1) return dever_rt_v1_wire_decode(source, &text_type, decode, out, error);
if (phase == 2) return dever_rt_v1_wire_encode(&source, &text_type, encode, out, error);
return dever_rt_v1_json_from_text(source, out, error);
}
int dever_test_run(void) {
const char *setting = getenv("DEVER_WIRE_BENCH_ROWS");
size_t rows = setting == NULL ? 128 : (size_t)strtoul(setting, NULL, 10);
int wide = getenv("DEVER_WIRE_BENCH_WIDE") != NULL;
if (rows != 1 && rows != 16 && rows != 128 && !(wide && rows == 1024)) return 1;
const char *row = "{\"id\":9007199254740993,\"text\":\"雪😀\\n\\\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\"weight\":1.25,\"note\":null}";
const char *suffix = "],\"raw\":{\"large\":123456789012345678901234567890}}";
size_t capacity = rows * (strlen(row) + 1) + strlen(suffix) + 64;
char *raw = malloc(capacity);
if (raw == NULL) return 2;
strcpy(raw, wide ? "{" : "{\"rows\":[");
for (size_t index = 0; index < rows; ++index) {
if (index != 0) strcat(raw, ",");
if (wide) {
char field[64];
snprintf(field, sizeof(field), "\"field_%zu\":%zu", index, index);
strcat(raw, field);
} else {
strcat(raw, row);
}
}
strcat(raw, wide ? "}" : suffix);
size_t length = strlen(raw);
DeverRtBuffer error = {0};
void *source = NULL, *body = NULL;
if (dever_rt_v1_text_new((const uint8_t *)raw, length, &source, &error)) return 3;
if (getenv("DEVER_WIRE_BENCH_VALIDATED") != NULL) {
void *validated = NULL;
if (dever_rt_v1_wire_decode(source, &text_type, decode, &validated, &error)) return 11;
dever_rt_v1_text_release(source);
source = validated;
}
char *request = malloc(length + 12);
if (request == NULL) return 4;
snprintf(request, length + 12, "{\"source\":%s}", raw);
if (dever_rt_v1_text_new((const uint8_t *)request, strlen(request), &body, &error)) return 5;
free(request);
for (unsigned phase = 0; phase < 4; ++phase) {
void *output = NULL;
if (operation(phase, body, source, &output, &error)) return 6;
DeverRtBuffer bytes = {0};
if (dever_rt_v1_text_export(output, &bytes) || bytes.len != length || memcmp(bytes.ptr, raw, length) != 0) return 7;
dever_rt_v1_buffer_free(bytes.ptr, bytes.len);
dever_rt_v1_text_release(output);
struct timespec started, finished;
if (clock_gettime(CLOCK_MONOTONIC, &started)) return 8;
long long allocations = dever_test_allocations();
const size_t iterations = 500;
for (size_t iteration = 0; iteration < iterations; ++iteration) {
if (operation(phase, body, source, &output, &error)) return 9;
dever_rt_v1_text_release(output);
}
allocations = dever_test_allocations() - allocations;
if (clock_gettime(CLOCK_MONOTONIC, &finished)) return 10;
int64_t elapsed = (int64_t)(finished.tv_sec - started.tv_sec) * INT64_C(1000000000)
+ finished.tv_nsec - started.tv_nsec;
printf("{\"phase\":%u,\"rows\":%zu,\"iterations\":%zu,\"elapsed_ns\":%lld,\"allocations\":%lld}\n",
phase, rows, iterations, (long long)elapsed, allocations);
}
dever_rt_v1_text_release(source);
dever_rt_v1_text_release(body);
free(raw);
return 0;
}
@@ -0,0 +1,135 @@
#define _POSIX_C_SOURCE 200809L
#include "runtime.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
extern long long dever_test_allocations(void);
#ifndef WIRE_TEXT_KEY
#define WIRE_TEXT_KEY "text"
#endif
#ifndef WIRE_NUMBER_KEY
#define WIRE_NUMBER_KEY "number"
#endif
#ifndef WIRE_RECORD
#define WIRE_RECORD 0
#endif
typedef struct {
void *text;
int64_t rows;
} Payload;
#if WIRE_RECORD != 2
static const DeverRtWireName fields[] = {
{(const uint8_t *)WIRE_TEXT_KEY, sizeof(WIRE_TEXT_KEY) - 1},
{(const uint8_t *)WIRE_NUMBER_KEY, sizeof(WIRE_NUMBER_KEY) - 1}
};
#endif
static void payload_clone(const void *source, void *destination) {
const Payload *input = source;
Payload *output = destination;
*output = *input;
output->text = dever_rt_v1_text_retain(input->text);
}
static void payload_drop(void *value) {
Payload *payload = value;
dever_rt_v1_text_release(payload->text);
payload->text = NULL;
}
static uint8_t payload_equal(const void *left, const void *right) {
const Payload *first = left;
const Payload *second = right;
return first->rows == second->rows && dever_rt_v1_text_equal(first->text, second->text);
}
static const DeverRtType payload_type = {
sizeof(Payload), _Alignof(Payload), payload_clone, payload_drop, payload_equal, NULL
};
#if WIRE_RECORD == 2
static const DeverRtWireName prefixes[] = {
{(const uint8_t *)"\"" WIRE_TEXT_KEY "\":", sizeof("\"" WIRE_TEXT_KEY "\":") - 1},
{(const uint8_t *)",\"" WIRE_NUMBER_KEY "\":", sizeof(",\"" WIRE_NUMBER_KEY "\":") - 1}
};
static uint32_t encode_fields(const void *value, void *writer, DeverRtBuffer *error) {
const Payload *payload = value;
uint32_t status = dever_rt_v1_wire_encoder_compiled_key(writer, &prefixes[0], error);
if (status == 0) status = dever_rt_v1_wire_encoder_text(writer, payload->text, error);
if (status == 0) status = dever_rt_v1_wire_encoder_compiled_key(writer, &prefixes[1], error);
if (status == 0) status = dever_rt_v1_wire_encoder_int(writer, INT64_C(9007199254740993), error);
return status;
}
#elif WIRE_RECORD
static uint32_t encode_field(uint64_t index, const void *value, void *writer, DeverRtBuffer *error) {
const Payload *payload = value;
if (index == 0) return dever_rt_v1_wire_encoder_text(writer, payload->text, error);
if (index == 1) return dever_rt_v1_wire_encoder_int(writer, INT64_C(9007199254740993), error);
return 2;
}
#endif
static uint32_t encode(const void *value, void *writer, DeverRtBuffer *error) {
const Payload *payload = value;
uint32_t status = dever_rt_v1_wire_encoder_begin_array(writer, error);
for (int64_t index = 0; index < payload->rows && status == 0; ++index) {
#if WIRE_RECORD == 2
status = dever_rt_v1_wire_encoder_compiled_record(writer, value, &payload_type, 2, encode_fields, error);
#elif WIRE_RECORD
status = dever_rt_v1_wire_encoder_record(writer, value, &payload_type, fields, 2, encode_field, error);
#else
status = dever_rt_v1_wire_encoder_begin_object(writer, error);
if (status == 0) status = dever_rt_v1_wire_encoder_key(writer, &fields[0], error);
if (status == 0) status = dever_rt_v1_wire_encoder_text(writer, payload->text, error);
if (status == 0) status = dever_rt_v1_wire_encoder_key(writer, &fields[1], error);
if (status == 0) status = dever_rt_v1_wire_encoder_int(writer, INT64_C(9007199254740993), error);
if (status == 0) status = dever_rt_v1_wire_encoder_end(writer, error);
#endif
}
if (status == 0) status = dever_rt_v1_wire_encoder_end(writer, error);
return status;
}
int dever_test_run(void) {
const char *configured = getenv("DEVER_WIRE_BENCH_ROWS");
int64_t rows = configured == NULL ? 128 : strtoll(configured, NULL, 10);
if (rows != 4 && rows != 128) return 1;
const char *text = "Unicode 雪😀 and newline\n";
const char *row = "{\"" WIRE_TEXT_KEY "\":\"Unicode 雪😀 and newline\\n\",\"" WIRE_NUMBER_KEY "\":9007199254740993}";
Payload payload = {NULL, rows};
DeverRtBuffer error = {0};
if (dever_rt_v1_text_new((const uint8_t *)text, strlen(text), &payload.text, &error)) return 2;
void *output = NULL;
if (dever_rt_v1_wire_encode(&payload, &payload_type, encode, &output, &error)) return 3;
DeverRtBuffer bytes = {0};
if (dever_rt_v1_text_export(output, &bytes)) return 4;
size_t row_length = strlen(row);
if (bytes.len != 1 + (row_length + 1) * (size_t)rows || bytes.ptr[0] != '[' || bytes.ptr[bytes.len - 1] != ']') return 5;
for (int64_t index = 0; index < rows; ++index) {
if (memcmp(bytes.ptr + 1 + (size_t)index * (row_length + 1), row, row_length) != 0) return 6;
}
dever_rt_v1_buffer_free(bytes.ptr, bytes.len);
dever_rt_v1_text_release(output);
struct timespec started, finished;
if (clock_gettime(CLOCK_MONOTONIC, &started)) return 7;
long long allocations = dever_test_allocations();
const int64_t iterations = 500;
for (int64_t iteration = 0; iteration < iterations; ++iteration) {
if (dever_rt_v1_wire_encode(&payload, &payload_type, encode, &output, &error)) return 8;
dever_rt_v1_text_release(output);
}
allocations = dever_test_allocations() - allocations;
if (clock_gettime(CLOCK_MONOTONIC, &finished)) return 9;
int64_t elapsed = (int64_t)(finished.tv_sec - started.tv_sec) * INT64_C(1000000000)
+ finished.tv_nsec - started.tv_nsec;
payload_drop(&payload);
printf("{\"rows\":%lld,\"iterations\":%lld,\"elapsed_ns\":%lld,\"allocations\":%lld}\n",
(long long)rows, (long long)iterations, (long long)elapsed, allocations);
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
ready(port: Int) () {
dever.io.println("READY|" + int.toText(port))
}
pause(millis: Int) () {
dever.task.sleep(millis)
}
report(
begin: Int,
finish: Int,
name: Text,
count: Int,
checksum: Int
) () recover("基准失败输出由运行器检查") {
dever.io.println(
"SAMPLE|" + name + "|" + int.toText(count) + "|" + int.toText(finish - begin) + "|" + int.toText(
checksum
)
)
}
@@ -0,0 +1,3 @@
long cmd run = app.run
long cmd continuous = app.continuous
@@ -0,0 +1,39 @@
type InputResult {
Parsed(value: Decimal)
error Invalid
}
type State {
total: Decimal
factor: Decimal
}
type Sample {
elapsed_ns: Int
checksum: Text
}
parsed(value: Decimal) (result: Decimal) { result = value }
parsed(value: null) (result: Decimal) { fail(InputResult.Invalid) }
step(value: Int, state: State) (next: State) {
next = State {
total = state.total + decimal.fromInt(value) * state.factor
factor = state.factor
}
}
run(ticks: Text, initial: Text, factor: Text) (sample: Sample) {
bytes = dever.bytes.fromText(ticks)
state = State { total = parsed(decimal.parse(initial))
factor = parsed(decimal.parse(factor)) }
begin = dever.time.monotonicNanos()
final = reduce(step, bytes, state)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = decimal.toText(final.total) }
}
continuous(ticks: Text, initial: Text, factor: Text) (samples: List<Sample>) {
samples = [run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor), run(ticks, initial, factor)]
}
+71
View File
@@ -0,0 +1,71 @@
use dec::{Context, Decimal128, Rounding};
use dever_runtime::number::DecimalValue;
use std::hint::black_box;
use std::time::Instant;
fn finite(context: &Context<Decimal128>, value: Decimal128) -> Decimal128 {
let status = context.status();
assert!(!status.division_by_zero() && !status.division_undefined());
assert!(!status.overflow() && !status.underflow());
assert!(!status.invalid_operation() && !status.invalid_context() && value.is_finite());
value
}
fn measure(mode: &str, ticks: &str, initial: &str, factor: &str) -> serde_json::Value {
let bytes = black_box(ticks.as_bytes());
let (checksum, elapsed) = if mode == "runtime" {
let mut total = DecimalValue::parse(initial).expect("initial");
let factor = DecimalValue::parse(factor).expect("factor");
let begin = Instant::now();
for value in bytes {
let product = DecimalValue::from_int(i64::from(*value))
.checked_mul(factor)
.expect("multiply");
total = total.checked_add(product).expect("add");
}
black_box(total);
let elapsed = begin.elapsed();
(total.to_string(), elapsed)
} else {
assert_eq!(mode, "decimal128");
let mut context = Context::<Decimal128>::default();
let mut total = context.parse(initial).expect("initial");
assert!(!context.status().inexact() && total.is_finite());
context.clear_status();
let factor = context.parse(factor).expect("factor");
assert!(!context.status().inexact() && factor.is_finite());
context.set_rounding(Rounding::HalfEven);
let begin = Instant::now();
for value in bytes {
let item = Decimal128::from(i64::from(*value));
context.clear_status();
let product = context.mul(item, factor);
let product = finite(&context, product);
context.clear_status();
let sum = context.add(total, product);
total = finite(&context, sum);
}
black_box(total);
let elapsed = begin.elapsed();
(context.reduce(total).to_string(), elapsed)
};
serde_json::json!({"checksum": checksum, "elapsed_ns": elapsed.as_nanos()})
}
fn main() {
let arguments = std::env::args().collect::<Vec<_>>();
let input: serde_json::Value = serde_json::from_str(&arguments[3]).expect("input object");
let ticks = input["ticks"].as_str().expect("ticks");
let initial = input["initial"].as_str().expect("initial");
let factor = input["factor"].as_str().expect("factor");
let output = match arguments[2].as_str() {
"run" => measure(&arguments[1], ticks, initial, factor),
"continuous" => serde_json::Value::Array(
(0..10)
.map(|_| measure(&arguments[1], ticks, initial, factor))
.collect(),
),
_ => panic!("unknown measurement"),
};
println!("{output}");
}
@@ -0,0 +1,136 @@
type Content {
body: Bytes
kind: Text
}
listening(result: dever.net.ListenResult.Listening(listener)) (
value: dever.system.Listener
) {
value = listener
}
listening(result: dever.net.ListenResult.Failed(message)) (value: dever.system.Listener) {
fail(app.WorkerFailure.Failed(message))
}
bound(result: dever.net.PortResult.Bound(port)) (value: Int) {
value = port
}
bound(result: dever.net.PortResult.Failed(message)) (value: Int) {
fail(app.WorkerFailure.Failed(message))
}
paused(result: dever.time.SleepResult.Done) () {}
paused(result: dever.time.SleepResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
closed(result: dever.io.CloseResult.Closed) () {}
closed(result: dever.io.CloseResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
serverTls(result: dever.tls.ServerResult.Ready(tls)) (value: dever.system.ServerTls) {
value = tls
}
serverTls(result: dever.tls.ServerResult.Failed(message)) (
value: dever.system.ServerTls
) {
fail(app.WorkerFailure.Failed(message))
}
responseValue(content: Content) (result: dever.http.Response) {
result = dever.http.Response {
status = 200
headers = [dever.http.header("Content-Type", content.kind)]
body = content.body
}
}
route(request: dever.http.Request, large: Bytes) (response: dever.http.Response) {
response = responseValue(select(request.target, large))
}
select(path: "/plain", large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.fromText("hello")
kind = "text/plain"
}
}
select(path: "/json", large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.fromText("{\"ok\":true}")
kind = "application/json"
}
}
select(path: "/bytes", large: Bytes) (result: Content) {
result = Content {
body = large
kind = "application/octet-stream"
}
}
select(path: other, large: Bytes) (result: Content) {
result = Content {
body = dever.bytes.fromText("unknown benchmark path")
kind = "text/plain"
}
}
serve(
listener: dever.system.Listener,
tls: null,
limits: dever.http.Limits,
large: Bytes
) () {
dever.system.httpServe(route, listener, limits, large)
}
serve(
listener: dever.system.Listener,
tls: dever.system.ServerTls,
limits: dever.http.Limits,
large: Bytes
) () {
dever.system.httpServeTls(route, listener, limits, tls, large)
}
worker(
tls: dever.system.ServerTls?,
limits: dever.http.Limits,
large: Bytes,
lifetime: Int
) () {
listener = listening(result(dever.net.listen("127.0.0.1", 0)))
port = bound(result(dever.net.port(listener)))
server = run(serve(listener, tls, limits, large))
blocking(dever.io.println("READY|" + int.toText(port)))
paused(result(dever.task.sleep(lifetime)))
closed(result(dever.net.closeListener(listener)))
stop(server)
}
port.plain(limits: dever.http.Limits, large: Bytes, lifetime: Int) () {
worker(null, limits, large, lifetime)
}
port.secure(
limits: dever.http.Limits,
large: Bytes,
lifetime: Int,
certificate: Bytes,
key: Bytes
) () {
worker(
serverTls(result(dever.tls.server(certificate, key))),
limits,
large,
lifetime
)
}
+7
View File
@@ -0,0 +1,7 @@
cmd http = app.http
cmd https = app.https
cmd http2 = app.http2
cmd https2 = app.https2
+35
View File
@@ -0,0 +1,35 @@
type WorkerFailure {
error Failed(message: Text)
}
http() (result: Bool) {
port.plain(config.limits(), config.largeBody(), config.lifetime())
result = true
}
https() (result: Bool) {
port.secure(
config.limits(),
config.largeBody(),
config.lifetime(),
config.certificate(),
config.key()
)
result = true
}
http2() (result: Bool) {
port.plain(config.http2Limits(), config.largeBody(), config.lifetime())
result = true
}
https2() (result: Bool) {
port.secure(
config.http2Limits(),
config.largeBody(),
config.lifetime(),
config.certificate(),
config.key()
)
result = true
}
+66
View File
@@ -0,0 +1,66 @@
//! 独立 Axum 路由;用官方 Hyper 入口统一连接、报文和超时上限。
use axum::{
Router,
body::Bytes,
extract::DefaultBodyLimit,
http::header,
routing::{get, post},
};
use hyper_util::{
rt::{TokioIo, TokioTimer},
service::TowerToHyperService,
};
use std::{sync::Arc, time::Duration};
use tokio::{net::TcpListener, sync::Semaphore};
async fn serve(capacity: usize) -> Result<(), Box<dyn std::error::Error>> {
let router = Router::new()
.route(
"/json",
get(async |_: Bytes| {
(
[(header::CONTENT_TYPE, "application/json")],
"{\"ok\":true}",
)
}),
)
.route(
"/echo",
post(async |body: Bytes| ([(header::CONTENT_TYPE, "application/octet-stream")], body)),
)
.layer(DefaultBodyLimit::max(65536));
let listener = TcpListener::bind("127.0.0.1:0").await?;
let slots = Arc::new(Semaphore::new(capacity));
println!("READY|{}", listener.local_addr()?.port());
loop {
let permit = slots.clone().acquire_owned().await?;
let (stream, _) = listener.accept().await?;
stream.set_nodelay(true)?;
let service = TowerToHyperService::new(router.clone());
tokio::spawn(async move {
let _permit = permit;
let mut builder = hyper::server::conn::http1::Builder::new();
builder
.timer(TokioTimer::new())
.header_read_timeout(Duration::from_secs(5))
.max_buf_size(8192);
if let Err(error) = builder
.serve_connection(TokioIo::new(stream), service)
.await
{
if !error.is_incomplete_message() && !error.is_closed() {
eprintln!("connection: {error}");
}
}
});
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let capacity = std::env::args().nth(1).unwrap().parse()?;
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()?
.block_on(serve(capacity))
}
@@ -0,0 +1,31 @@
route(request: dever.http.Request) (response: dever.http.Response) {
response = selected(request.target, request.body)
}
selected(path: "/json", body: Bytes) (response: dever.http.Response) {
response = dever.http.Response { status = 200, headers = [dever.http.header("content-type", "application/json")], body = dever.bytes.fromText("{\"ok\":true}") }
}
selected(path: "/echo", body: Bytes) (response: dever.http.Response) {
response = dever.http.Response { status = 200, headers = [dever.http.header("content-type", "application/octet-stream")], body = body }
}
selected(path: other, body: Bytes) (response: dever.http.Response) {
response = dever.http.Response { status = 404, headers = [], body = dever.bytes.fromText("") }
}
serve(connections: Int) () {
listener = dever.net.listen("127.0.0.1", 0)
port = dever.net.port(listener)
limits = dever.http.defaultLimits()
limits.connections = connections
limits.body_bytes = 65536
blocking(dever.io.println("READY|" + int.toText(port)))
dever.http.serve(route, listener, limits)
}
main() () {
serve(2048)
}
overload() () {
serve(8192)
}
@@ -0,0 +1,78 @@
package main
import (
"fmt"
"io"
"net"
"net/http"
"os"
"runtime"
"strconv"
"sync"
"time"
)
type listener struct {
net.Listener
slots chan struct{}
}
type connection struct {
net.Conn
slots chan struct{}
once sync.Once
}
func (value *connection) Close() error {
err := value.Conn.Close()
value.once.Do(func() { <-value.slots })
return err
}
func (value *listener) Accept() (net.Conn, error) {
value.slots <- struct{}{}
stream, err := value.Listener.Accept()
if err != nil {
<-value.slots
return nil, err
}
return &connection{Conn: stream, slots: value.slots}, nil
}
func main() {
capacity, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
runtime.GOMAXPROCS(2)
bound, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
server := http.Server{
ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second, MaxHeaderBytes: 8192,
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
defer request.Body.Close()
body, err := io.ReadAll(http.MaxBytesReader(writer, request.Body, 65536))
if err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
return
}
switch request.URL.RequestURI() {
case "/json":
writer.Header().Set("Content-Type", "application/json")
writer.Write([]byte(`{"ok":true}`))
case "/echo":
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Write(body)
default:
writer.WriteHeader(http.StatusNotFound)
}
}),
}
fmt.Printf("READY|%d\n", bound.Addr().(*net.TCPAddr).Port)
if err := server.Serve(&listener{Listener: bound, slots: make(chan struct{}, capacity)}); err != nil {
panic(err)
}
}
@@ -0,0 +1,121 @@
//! 独立 Hyper 服务及三种语言共用的 HTTP/1.1 客户端,不链接 Dever 运行库。
use bytes::Bytes;
use http_body_util::{BodyExt, Full, Limited};
use hyper::{Request, Response, body::Incoming, service::service_fn};
use hyper_util::rt::{TokioIo, TokioTimer};
use std::{convert::Infallible, sync::Arc, time::Duration};
use tokio::{net::TcpListener, sync::{Barrier, Semaphore}, time::Instant};
async fn route(request: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let path = request.uri().path().to_owned();
let body = match Limited::new(request.into_body(), 65536).collect().await {
Ok(body) => body.to_bytes(),
Err(_) => return Ok(Response::builder().status(400).body(Full::new(Bytes::new())).unwrap()),
};
let (status, kind, body) = match path.as_str() {
"/json" => (200, "application/json", Bytes::from_static(b"{\"ok\":true}")),
"/echo" => (200, "application/octet-stream", body),
_ => (404, "text/plain", Bytes::new()),
};
Ok(Response::builder().status(status).header("content-type", kind).body(Full::new(body)).unwrap())
}
async fn serve(capacity: usize) -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let slots = Arc::new(Semaphore::new(capacity));
println!("READY|{}", listener.local_addr()?.port());
loop {
let permit = slots.clone().acquire_owned().await?;
let (stream, _) = listener.accept().await?;
stream.set_nodelay(true)?;
tokio::spawn(async move {
let _permit = permit;
let mut builder = hyper::server::conn::http1::Builder::new();
builder.timer(TokioTimer::new()).header_read_timeout(Duration::from_secs(5)).max_buf_size(8192);
if let Err(error) = builder.serve_connection(TokioIo::new(stream), service_fn(route)).await {
if !error.is_incomplete_message() && !error.is_closed() {
eprintln!("connection: {error}");
}
}
});
}
}
async fn load(port: u16, path: String, concurrency: usize, seconds: f64) -> Result<(), Box<dyn std::error::Error>> {
let payload = if path == "/echo" { Bytes::from(vec![b'x'; 65536]) } else { Bytes::new() };
let expected = if path == "/echo" { payload.clone() } else { Bytes::from_static(b"{\"ok\":true}") };
let kind = if path == "/echo" { "application/octet-stream" } else { "application/json" };
let barrier = Arc::new(Barrier::new(concurrency + 1));
let mut workers = Vec::new();
for _ in 0..concurrency {
let stream = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?;
stream.set_nodelay(true)?;
let (mut sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
let driver = tokio::spawn(connection);
let barrier = barrier.clone();
let payload = payload.clone();
let expected = expected.clone();
let path = path.clone();
workers.push(tokio::spawn(async move {
barrier.wait().await;
let deadline = Instant::now() + Duration::from_secs_f64(seconds);
let mut samples = Vec::new();
let mut errors = 0_u64;
while Instant::now() < deadline {
let request = Request::builder().method(if path == "/echo" { "POST" } else { "GET" })
.uri(&path).header("host", "localhost").body(Full::new(payload.clone())).unwrap();
let started = Instant::now();
let exchange = async {
let response = sender.send_request(request).await.map_err(|error| error.to_string())?;
if response.status() != 200 || response.headers().get("content-type").and_then(|value| value.to_str().ok()) != Some(kind) {
return Err("status or content type differs".to_owned());
}
let body = response.into_body().collect().await.map_err(|error| error.to_string())?.to_bytes();
if body != expected { return Err("response body differs".to_owned()); }
Ok(())
};
match tokio::time::timeout(Duration::from_secs(5), exchange).await {
Ok(Ok(())) => samples.push(started.elapsed().as_micros() as u64),
result => {
eprintln!("request: {result:?}");
errors += 1;
break;
}
}
}
drop(sender);
driver.abort();
let _ = driver.await;
(samples, errors)
}));
}
let started = Instant::now();
barrier.wait().await;
let mut samples = Vec::new();
let mut errors = 0;
for worker in workers {
let (mut values, failed) = worker.await?;
samples.append(&mut values);
errors += failed;
}
let elapsed = started.elapsed().as_secs_f64();
samples.sort_unstable();
let percentile = |percent: usize| -> Option<f64> {
if samples.is_empty() { None } else { Some(samples[(samples.len() * percent).div_ceil(100) - 1] as f64 / 1000.0) }
};
println!("{}", serde_json::json!({"concurrency":concurrency,"seconds":seconds,"elapsed_seconds":elapsed,
"succeeded":samples.len(),"errors":errors,"qps":samples.len() as f64 / elapsed,
"p50_ms":percentile(50),"p95_ms":percentile(95),"p99_ms":percentile(99)}));
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
let server = arguments[0] == "serve";
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(if server { 2 } else { 4 }).enable_all().build()?;
if server {
runtime.block_on(serve(arguments[1].parse()?))
} else {
runtime.block_on(load(arguments[1].parse()?, arguments[2].clone(), arguments[3].parse()?, arguments[4].parse()?))
}
}
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"fmt"
"net"
"os"
"runtime"
"strconv"
"time"
"github.com/gofiber/fiber/v3"
)
func main() {
capacity, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
runtime.GOMAXPROCS(2)
app := fiber.New(fiber.Config{
Concurrency: capacity,
BodyLimit: 65536,
ReadBufferSize: 8192,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
CaseSensitive: true,
StrictRouting: true,
})
app.Get("/json", func(context fiber.Ctx) error {
context.Set("Content-Type", "application/json")
return context.SendString(`{"ok":true}`)
})
app.Post("/echo", func(context fiber.Ctx) error {
context.Set("Content-Type", "application/octet-stream")
return context.Send(context.BodyRaw())
})
bound, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
fmt.Printf("READY|%d\n", bound.Addr().(*net.TCPAddr).Port)
if err := app.Listener(bound, fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
panic(err)
}
}
+142
View File
@@ -0,0 +1,142 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
type settings struct {
Workers int `json:"workers"`
Connections int `json:"connections"`
TimeoutMS int `json:"timeout_ms"`
LifetimeMS int `json:"lifetime_ms"`
Certificate string `json:"cert_path"`
Key string `json:"key_path"`
}
type limitedListener struct {
net.Listener
slots chan struct{}
closed chan struct{}
once sync.Once
}
type limitedConnection struct {
net.Conn
slots chan struct{}
once sync.Once
}
func (connection *limitedConnection) Close() error {
err := connection.Conn.Close()
connection.once.Do(func() { <-connection.slots })
return err
}
func (listener *limitedListener) Accept() (net.Conn, error) {
connection, err := listener.Listener.Accept()
if err != nil {
return nil, err
}
select {
case listener.slots <- struct{}{}:
return &limitedConnection{Conn: connection, slots: listener.slots}, nil
case <-listener.closed:
connection.Close()
return nil, net.ErrClosed
}
}
func (listener *limitedListener) Close() error {
listener.once.Do(func() { close(listener.closed) })
return listener.Listener.Close()
}
func run() error {
if len(os.Args) != 2 || (os.Args[1] != "http" && os.Args[1] != "https") {
return errors.New("usage: go_http http|https")
}
executable, err := os.Executable()
if err != nil {
return err
}
content, err := os.ReadFile(filepath.Join(filepath.Dir(executable), "config", "setting.json"))
if err != nil {
return err
}
var document struct {
Benchmark settings `json:"benchmark"`
}
if err := json.Unmarshal(content, &document); err != nil {
return err
}
config := document.Benchmark
if config.Workers < 1 || config.Connections < 1 || config.TimeoutMS < 1 || config.LifetimeMS < 1 {
return errors.New("benchmark requires positive workers, connections, timeout_ms and lifetime_ms")
}
runtime.GOMAXPROCS(config.Workers)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
bounded := &limitedListener{Listener: listener, slots: make(chan struct{}, config.Connections), closed: make(chan struct{})}
defer bounded.Close()
large := bytes.Repeat([]byte{'x'}, 65536)
deadline := time.Duration(config.TimeoutMS) * time.Millisecond
server := &http.Server{
ReadTimeout: deadline, ReadHeaderTimeout: deadline, WriteTimeout: deadline,
IdleTimeout: deadline, MaxHeaderBytes: 8192,
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
body := http.MaxBytesReader(writer, request.Body, 65536)
defer body.Close()
if _, err := io.Copy(io.Discard, body); err != nil {
http.Error(writer, "invalid request body", http.StatusBadRequest)
return
}
switch request.URL.RequestURI() {
case "/plain":
writer.Header().Set("Content-Type", "text/plain")
io.WriteString(writer, "hello")
case "/json":
writer.Header().Set("Content-Type", "application/json")
io.WriteString(writer, `{"ok":true}`)
case "/bytes":
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Write(large)
default:
http.NotFound(writer, request)
}
}),
}
timer := time.AfterFunc(time.Duration(config.LifetimeMS)*time.Millisecond, func() { server.Close() })
defer timer.Stop()
fmt.Printf("READY|%d\n", listener.Addr().(*net.TCPAddr).Port)
if os.Args[1] == "https" {
err = server.ServeTLS(bounded, config.Certificate, config.Key)
} else {
err = server.Serve(bounded)
}
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
@@ -0,0 +1,9 @@
plain(limits: dever.http.Limits, large: Bytes, lifetime: Int) () fails app.WorkerFailure
secure(
limits: dever.http.Limits,
large: Bytes,
lifetime: Int,
certificate: Bytes,
key: Bytes
) () fails app.WorkerFailure
+1
View File
@@ -0,0 +1 @@
long cmd run = app.run
+56
View File
@@ -0,0 +1,56 @@
type State {
source: Json
update: Bool
checksum: Int
}
run(source: Json, update: Bool, ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
initial = State {
source = source
update = update
checksum = 0
}
output = transform(source, update)
begin = dever.time.monotonicNanos()
final = reduce(step, input, initial)
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = final.checksum
output = output
}
}
type Sample {
elapsed_ns: Int
checksum: Int
output: Text
}
step(unused: Int, state: State) (next: State) {
output = transform(state.source, state.update)
next = State {
source = state.source
update = state.update
checksum = state.checksum + length(dever.bytes.fromText(output))
}
}
transform(source: Json, update: false) (output: Text) {
output = selected(dever.json.propertyValue(source, "data"))
}
transform(source: Json, update: true) (output: Text) {
output = dever.json.toText(
dever.json.setProperty(source, "active", dever.json.boolean(true))
)
}
selected(value: Json) (output: Text) {
output = dever.json.toText(value)
}
selected(value: null) (output: Text) {
output = "null"
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"time"
)
type input struct {
Source json.RawMessage `json:"source"`
Update bool `json:"update"`
Ticks string `json:"ticks"`
}
type sample struct {
Elapsed int64 `json:"elapsed_ns"`
Checksum int `json:"checksum"`
Output string `json:"output"`
}
func transform(source []byte, update bool) []byte {
decoder := json.NewDecoder(bytes.NewReader(source))
decoder.UseNumber()
var object map[string]any
if err := decoder.Decode(&object); err != nil {
panic(err)
}
var selected any = object["data"]
if update {
object["active"] = true
selected = object
}
output, err := json.Marshal(selected)
if err != nil {
panic(err)
}
return output
}
func main() {
var arguments input
if len(os.Args) != 2 {
panic("expected one JSON input")
}
if err := json.Unmarshal([]byte(os.Args[1]), &arguments); err != nil {
panic(err)
}
if !json.Valid(arguments.Source) {
panic("invalid source")
}
output := transform(arguments.Source, arguments.Update)
checksum := 0
begin := time.Now()
for range []byte(arguments.Ticks) {
checksum += len(transform(arguments.Source, arguments.Update))
}
result := sample{time.Since(begin).Nanoseconds(), checksum, string(output)}
encoded, err := json.Marshal(result)
if err != nil {
panic(err)
}
fmt.Println(string(encoded))
}
+46
View File
@@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
use serde_json::{Value, value::RawValue};
use std::hint::black_box;
use std::time::Instant;
#[derive(Deserialize)]
struct Input {
source: Box<RawValue>,
update: bool,
ticks: String,
}
#[derive(Serialize)]
struct Sample {
elapsed_ns: u128,
checksum: usize,
output: String,
}
fn transform(source: &str, update: bool) -> String {
let mut value: Value = serde_json::from_str(source).expect("valid JSON input");
let object = value.as_object_mut().expect("object input");
if update {
object.insert("active".into(), Value::Bool(true));
serde_json::to_string(&value).expect("serializable object")
} else {
serde_json::to_string(object.get("data").expect("data field")).expect("serializable value")
}
}
fn main() {
let argument = std::env::args().nth(1).expect("one JSON argument");
let input: Input = serde_json::from_str(&argument).expect("valid input fields");
let output = transform(input.source.get(), input.update);
let mut checksum = 0;
let started = Instant::now();
for _ in input.ticks.bytes() {
checksum += transform(black_box(input.source.get()), input.update).len();
}
let sample = Sample {
elapsed_ns: started.elapsed().as_nanos(),
checksum,
output,
};
println!("{}", serde_json::to_string(&sample).unwrap());
}
@@ -0,0 +1,67 @@
long cmd record_update = app.recordUpdate
long cmd continuous_record_update = app.continuousRecordUpdate
long cmd integer = app.integer
long cmd record_inspect = app.recordInspect
long cmd continuous_record_inspect = app.continuousRecordInspect
long cmd map_lookup = app.mapLookup
long cmd continuous_map_lookup = app.continuousMapLookup
long cmd map_text_lookup = app.mapTextLookup
long cmd continuous_map_text_lookup = app.continuousMapTextLookup
long cmd text_equal = app.textEqual
long cmd continuous_text_equal = app.continuousTextEqual
long cmd text_contains = app.textContains
long cmd continuous_text_contains = app.continuousTextContains
long cmd float = app.float
long cmd bytes_concat = app.bytesConcat
long cmd text_concat = app.textConcat
long cmd list = app.list
long cmd map = app.map
long cmd list_read = app.listRead
long cmd map_read = app.mapRead
long cmd list_record = app.listRecord
long cmd list_record_read = app.listRecordRead
long cmd continuous_integer = app.continuousInteger
long cmd continuous_float = app.continuousFloat
long cmd continuous_list = app.continuousList
long cmd continuous_map = app.continuousMap
long cmd continuous_list_read = app.continuousListRead
long cmd continuous_map_read = app.continuousMapRead
long cmd continuous_list_record = app.continuousListRecord
long cmd continuous_list_record_read = app.continuousListRecordRead
long cmd continuous_text_concat = app.continuousTextConcat
long cmd continuous_bytes_concat = app.continuousBytesConcat
long cmd call_read = app.callRead
long cmd continuous_call_read = app.continuousCallRead
+393
View File
@@ -0,0 +1,393 @@
type Lookup {
values: Map<Int, Int>
index: Int
total: Int
}
selected(value: Int) (answer: Int) { answer = value }
selected(value: null) (answer: Int) { answer = 0 }
lookup(value: Int, state: Lookup) (next: Lookup) {
found = selected(get(state.values, state.index))
next = Lookup { values = state.values
index = state.index + 1
total = state.total + found }
}
mapLookup(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
values = reduce(putValue, input, {})
initial = Lookup { values = values
index = 0
total = 0 }
begin = dever.time.monotonicNanos()
final = reduce(lookup, input, initial)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = final.total }
}
continuousMapLookup(ticks: Text) (samples: List<Sample>) {
samples = each(mapLookup, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type TextLookup {
values: Map<Text, Int>
total: Int
}
type TextPair {
left: Text
right: Text
}
textEntry(entry: MapEntry<Int, Int>, state: Map<Text, Int>) (next: Map<Text, Int>) {
next = put(state, "lookup-key-" + int.toText(entry.key), entry.value)
}
textKey(entry: MapEntry<Text, Int>) (key: Text) { key = entry.key }
textLookup(key: Text, state: TextLookup) (next: TextLookup) {
found = selected(get(state.values, key))
next = TextLookup { values = state.values
total = state.total + found }
}
mapTextLookup(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
values = reduce(textEntry, entries(reduce(putValue, input, {})), {})
keys = each(textKey, entries(values))
initial = TextLookup { values = values
total = 0 }
begin = dever.time.monotonicNanos()
final = reduce(textLookup, keys, initial)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = final.total + length(keys) - length(input) }
}
continuousMapTextLookup(ticks: Text) (samples: List<Sample>) {
samples = each(mapTextLookup, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
textPair(value: Int) (pair: TextPair) {
left = "compare-value-" + int.toText(value)
pair = TextPair { left = left
right = text.lower(text.upper(left)) }
}
matchedValue(matched: true) (value: Int) { value = 120 }
matchedValue(matched: false) (value: Int) { value = 0 }
equalTextPair(row: TextPair, total: Int) (next: Int) {
next = total + matchedValue(row.left == row.right)
}
containsTextPair(row: TextPair, total: Int) (next: Int) {
next = total + matchedValue(text.contains(row.left, row.right))
}
textEqual(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
rows = each(textPair, input)
begin = dever.time.monotonicNanos()
total = reduce(equalTextPair, rows, 0)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total + length(rows) - length(input) }
}
continuousTextEqual(ticks: Text) (samples: List<Sample>) {
samples = each(textEqual, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
textContains(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
rows = each(textPair, input)
begin = dever.time.monotonicNanos()
total = reduce(containsTextPair, rows, 0)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total + length(rows) - length(input) }
}
continuousTextContains(ticks: Text) (samples: List<Sample>) {
samples = each(textContains, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type Sample {
elapsed_ns: Int
checksum: Int
}
type FloatSample {
elapsed_ns: Int
checksum: Float
}
addFloat(value: Int, state: Float) (next: Float) {
next = state + float.fromInt(value)
}
float(ticks: Text) (sample: FloatSample) {
bytes = dever.bytes.fromText(ticks)
zero = float.fromInt(0)
begin = dever.time.monotonicNanos()
total = reduce(addFloat, bytes, zero)
end = dever.time.monotonicNanos()
sample = FloatSample { elapsed_ns = end - begin
checksum = total }
}
joinBytes(value: Bytes, state: Bytes) (next: Bytes) {
next = dever.bytes.concat(state, value)
}
bytesConcat(ticks: Text) (sample: Sample) {
chunks = each(dever.bytes.fromText, text.split(ticks, ""))
empty = dever.bytes.fromText("")
begin = dever.time.monotonicNanos()
output = reduce(joinBytes, chunks, empty)
end = dever.time.monotonicNanos()
total = reduce(add, output, 0)
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
joinText(value: Text, state: Text) (next: Text) {
next = state + value
}
textConcat(ticks: Text) (sample: Sample) {
chunks = text.split(ticks, ",")
begin = dever.time.monotonicNanos()
output = reduce(joinText, chunks, "")
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = reduce(add, dever.bytes.fromText(output), 0)
}
}
add(value: Int, state: Int) (next: Int) {
next = state + value
}
appendValue(value: Int, state: List<Int>) (next: List<Int>) {
next = append(state, value)
}
putValue(value: Int, state: Map<Int, Int>) (next: Map<Int, Int>) {
key = length(state)
next = put(state, key, value)
}
mapped(entry: MapEntry<Int, Int>) (value: Int) {
value = entry.value
}
integer(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
value = reduce(add, input, 0)
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = value
}
}
list(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
value = reduce(appendValue, input, [])
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = sum(value)
}
}
map(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
value = reduce(putValue, input, {})
end = dever.time.monotonicNanos()
sample = Sample {
elapsed_ns = end - begin
checksum = sum(each(mapped, entries(value)))
}
}
listRead(ticks: Text) (sample: Sample) {
values = reduce(appendValue, dever.bytes.fromText(ticks), [])
begin = dever.time.monotonicNanos()
total = sum(values)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
mapRead(ticks: Text) (sample: Sample) {
values = reduce(putValue, dever.bytes.fromText(ticks), {})
begin = dever.time.monotonicNanos()
total = sum(mapped, entries(values))
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
type Record {
first: Int
second: Int
third: Int
}
appendRecord(value: Int, state: List<Record>) (next: List<Record>) {
next = append(state, Record { first = value
second = 0
third = 0 })
}
recordValue(row: Record) (value: Int) {
value = row.first + row.second + row.third
}
listRecord(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
values = reduce(appendRecord, input, [])
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = sum(recordValue, values) }
}
listRecordRead(ticks: Text) (sample: Sample) {
values = reduce(appendRecord, dever.bytes.fromText(ticks), [])
begin = dever.time.monotonicNanos()
total = sum(recordValue, values)
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = total }
}
continuousInteger(ticks: Text) (samples: List<Sample>) {
samples = each(integer, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousFloat(ticks: Text) (samples: List<FloatSample>) {
samples = each(float, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousList(ticks: Text) (samples: List<Sample>) {
samples = each(list, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousMap(ticks: Text) (samples: List<Sample>) {
samples = each(map, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousListRead(ticks: Text) (samples: List<Sample>) {
samples = each(listRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousMapRead(ticks: Text) (samples: List<Sample>) {
samples = each(mapRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousListRecord(ticks: Text) (samples: List<Sample>) {
samples = each(listRecord, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousListRecordRead(ticks: Text) (samples: List<Sample>) {
samples = each(listRecordRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousTextConcat(ticks: Text) (samples: List<Sample>) {
samples = each(textConcat, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
continuousBytesConcat(ticks: Text) (samples: List<Sample>) {
samples = each(bytesConcat, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type OwnedRecord {
name: Text
tags: List<Text>
count: Int
}
ownedRecord(value: Int) (row: OwnedRecord) {
name = "row-value-" + int.toText(value)
row = OwnedRecord { name = name
tags = [name]
count = value - length(name) - 1 }
}
inspectOwned(row: OwnedRecord) (value: Int) {
value = row.count + length(row.name) + length(row.tags)
}
forwardOwned(row: OwnedRecord) (value: Int) {
value = inspectOwned(row)
}
callRead(ticks: Text) (sample: Sample) {
rows = each(ownedRecord, dever.bytes.fromText(ticks))
begin = dever.time.monotonicNanos()
total = sum(forwardOwned, rows)
end = dever.time.monotonicNanos()
preserved = length(rows)
sample = Sample { elapsed_ns = end - begin
checksum = total + preserved - length(rows) }
}
continuousCallRead(ticks: Text) (samples: List<Sample>) {
samples = each(callRead, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
type Accumulator {
values: List<Int>
count: Int
}
appendField(value: Int, state: Accumulator) (next: Accumulator) {
values = append(state.values, value)
next = Accumulator { values = values
count = state.count + 1 }
}
recordUpdate(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
state = reduce(appendField, input, Accumulator { values = []
count = 0 })
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = sum(state.values) + state.count - length(state.values) }
}
continuousRecordUpdate(ticks: Text) (samples: List<Sample>) {
samples = each(recordUpdate, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
appendInspected(value: Int, state: Accumulator) (next: Accumulator) {
count = length(state.values) + 1
values = append(state.values, value)
next = Accumulator { values = values
count = count }
}
recordInspect(ticks: Text) (sample: Sample) {
input = dever.bytes.fromText(ticks)
begin = dever.time.monotonicNanos()
state = reduce(appendInspected, input, Accumulator { values = []
count = 0 })
end = dever.time.monotonicNanos()
sample = Sample { elapsed_ns = end - begin
checksum = sum(state.values) + state.count - length(state.values) }
}
continuousRecordInspect(ticks: Text) (samples: List<Sample>) {
samples = each(recordInspect, [ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks, ticks])
}
+214
View File
@@ -0,0 +1,214 @@
use std::hint::black_box;
use std::time::{Duration, Instant};
#[derive(Debug, PartialEq)]
struct Fault {
message: String,
locations: Vec<&'static str>,
causes: Vec<String>,
}
impl Fault {
fn overflow(location: &'static str) -> Self {
Self {
message: "Int overflow".into(),
locations: vec![location],
causes: Vec::new(),
}
}
fn at(mut self, location: &'static str) -> Self {
self.locations.push(location);
self
}
}
struct Record {
first: i64,
second: i64,
third: i64,
}
struct OwnedRecord {
name: String,
tags: Vec<String>,
count: i64,
}
fn inspect_owned(row: &OwnedRecord) -> Result<i64, Fault> {
let partial = row
.count
.checked_add(row.name.chars().count() as i64)
.ok_or_else(|| Fault::overflow("inspect_owned"))?;
partial
.checked_add(row.tags.len() as i64)
.ok_or_else(|| Fault::overflow("inspect_owned"))
}
fn forward_owned(row: &OwnedRecord) -> Result<i64, Fault> {
inspect_owned(row).map_err(|error| error.at("forward_owned"))
}
fn owned_sum(rows: &[OwnedRecord]) -> Result<i64, Fault> {
let mut total = 0_i64;
for row in rows {
let projected = forward_owned(row).map_err(|error| error.at("call_read"))?;
total = total
.checked_add(projected)
.ok_or_else(|| Fault::overflow("call_read"))?;
}
Ok(total)
}
fn add(total: i64, value: i64) -> Result<i64, Fault> {
total
.checked_add(value)
.ok_or_else(|| Fault::overflow("add"))
}
fn record_value(row: &Record) -> Result<i64, Fault> {
let partial = row
.first
.checked_add(row.second)
.ok_or_else(|| Fault::overflow("record_value"))?;
partial
.checked_add(row.third)
.ok_or_else(|| Fault::overflow("record_value"))
}
fn record_sum(values: &[Record]) -> Result<i64, Fault> {
let mut total = 0i64;
for row in values {
let projected = record_value(row).map_err(|error| error.at("record_sum"))?;
total = total
.checked_add(projected)
.ok_or_else(|| Fault::overflow("record_sum"))?;
}
Ok(total)
}
fn measure(operation: &str, ticks: &str) -> Result<(i64, Duration), Fault> {
let source = black_box(ticks.as_bytes());
match operation {
"call_read" => {
let rows = source
.iter()
.map(|value| {
let name = format!("row-value-{value}");
OwnedRecord {
count: i64::from(*value) - name.chars().count() as i64 - 1,
tags: vec![name.clone()],
name,
}
})
.collect::<Vec<_>>();
let borrowed = black_box(&rows);
let begin = Instant::now();
let total = owned_sum(borrowed)?;
black_box(total);
let elapsed = begin.elapsed();
black_box(&rows);
Ok((total, elapsed))
}
"integer" => {
let begin = Instant::now();
let mut total = 0;
for value in source {
total = add(total, i64::from(*value)).map_err(|error| error.at("integer"))?;
}
Ok((black_box(total), begin.elapsed()))
}
"list_record_read" => {
let mut values = Vec::new();
for value in source {
values.push(Record {
first: i64::from(*value),
second: 0,
third: 0,
});
}
let borrowed = black_box(&values);
let begin = Instant::now();
let total = record_sum(borrowed)?;
black_box(total);
drop(values);
Ok((total, begin.elapsed()))
}
_ => panic!("unknown checked kernel"),
}
}
fn verify_errors() {
let fault = owned_sum(&[OwnedRecord {
name: "overflow".into(),
tags: vec!["preserved".into()],
count: i64::MAX,
}])
.unwrap_err();
assert_eq!(fault.message, "Int overflow");
assert_eq!(
fault.locations,
["inspect_owned", "forward_owned", "call_read"]
);
assert!(fault.causes.is_empty());
assert_eq!(add(i64::MAX, 0).unwrap(), i64::MAX);
assert_eq!(add(i64::MIN, 0).unwrap(), i64::MIN);
for (left, right) in [(i64::MAX, 1), (i64::MIN, -1)] {
let fault = add(left, right).unwrap_err().at("integer");
assert_eq!(fault.message, "Int overflow");
assert_eq!(fault.locations, ["add", "integer"]);
assert!(fault.causes.is_empty());
}
let fault = record_sum(&[Record {
first: i64::MAX,
second: 1,
third: 0,
}])
.unwrap_err();
assert_eq!(fault.locations, ["record_value", "record_sum"]);
let fault = record_sum(&[
Record {
first: i64::MAX,
second: 0,
third: 0,
},
Record {
first: 1,
second: 0,
third: 0,
},
])
.unwrap_err();
assert_eq!(fault.locations, ["record_sum"]);
}
fn main() {
let arguments = std::env::args().collect::<Vec<_>>();
if arguments[1] == "--verify-errors" {
verify_errors();
println!("checked overflow and error frames verified");
return;
}
let input: serde_json::Value = serde_json::from_str(&arguments[2]).expect("input object");
let ticks = input["ticks"].as_str().expect("ticks string");
let (operation, count) = arguments[1]
.strip_prefix("continuous_")
.map_or((arguments[1].as_str(), 1), |operation| (operation, 10));
let mut samples = Vec::new();
for _ in 0..count {
match measure(operation, ticks) {
Ok((checksum, elapsed)) => samples
.push(serde_json::json!({"checksum": checksum, "elapsed_ns": elapsed.as_nanos()})),
Err(error) => {
eprintln!("{error:?}");
std::process::exit(1);
}
}
}
let output = if count == 1 {
samples.pop().unwrap()
} else {
serde_json::Value::Array(samples)
};
println!("{output}");
}
+278
View File
@@ -0,0 +1,278 @@
package main
import (
"encoding/json"
"fmt"
"math"
"os"
"runtime"
"strconv"
"strings"
"time"
"unicode/utf8"
)
type input struct {
Ticks string `json:"ticks"`
}
type sample struct {
Elapsed int64 `json:"elapsed_ns"`
Checksum int64 `json:"checksum"`
}
type record struct {
First int64
Second int64
Third int64
}
type ownedRecord struct {
Name string
Tags []string
Count int64
}
func inspectOwned(row ownedRecord) int64 {
return checkedAdd(checkedAdd(row.Count, int64(utf8.RuneCountInString(row.Name))), int64(len(row.Tags)))
}
func forwardOwned(row ownedRecord) int64 {
return inspectOwned(row)
}
func checkedAdd(total, value int64) int64 {
if value > 0 && total > math.MaxInt64-value {
panic("overflow")
}
return total + value
}
func measure(operation string, ticks string) sample {
source := []byte(ticks)
total := int64(0)
elapsed := int64(0)
switch operation {
case "record_update":
var values []int64
count := int64(0)
begin := time.Now()
for _, value := range source {
values = append(values, int64(value))
count = checkedAdd(count, 1)
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, value)
}
total = checkedAdd(total, count) - int64(len(values))
case "record_inspect":
var values []int64
count := int64(0)
begin := time.Now()
for _, value := range source {
count = checkedAdd(int64(len(values)), 1)
values = append(values, int64(value))
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, value)
}
total = checkedAdd(total, count) - int64(len(values))
case "map_lookup":
values := make(map[int64]int64, len(source))
for index, value := range source {
values[int64(index)] = int64(value)
}
index := int64(0)
begin := time.Now()
for range source {
total = checkedAdd(total, values[index])
index = checkedAdd(index, 1)
}
elapsed = time.Since(begin).Nanoseconds()
case "text_equal", "text_contains":
rows := make([][2]string, len(source))
for index, value := range source {
left := "compare-value-" + strconv.Itoa(int(value))
rows[index] = [2]string{left, strings.ToLower(strings.ToUpper(left))}
}
begin := time.Now()
if operation == "text_equal" {
for _, row := range rows {
matched := int64(0)
if row[0] == row[1] {
matched = 120
}
total = checkedAdd(total, matched)
}
} else {
for _, row := range rows {
matched := int64(0)
if strings.Contains(row[0], row[1]) {
matched = 120
}
total = checkedAdd(total, matched)
}
}
elapsed = time.Since(begin).Nanoseconds()
runtime.KeepAlive(rows)
total = checkedAdd(total, int64(len(rows))) - int64(len(source))
case "map_text_lookup":
keys := make([]string, len(source))
values := make(map[string]int64, len(source))
for index, value := range source {
key := "lookup-key-" + strconv.Itoa(index)
keys[index] = key
values[key] = int64(value)
}
begin := time.Now()
for _, key := range keys {
total = checkedAdd(total, values[key])
}
elapsed = time.Since(begin).Nanoseconds()
runtime.KeepAlive(values)
runtime.KeepAlive(keys)
total = checkedAdd(total, int64(len(keys))) - int64(len(source))
case "call_read":
rows := make([]ownedRecord, 0, len(source))
for _, value := range source {
name := "row-value-" + strconv.Itoa(int(value))
rows = append(rows, ownedRecord{name, []string{name}, int64(value) - int64(utf8.RuneCountInString(name)) - 1})
}
begin := time.Now()
for _, row := range rows {
total = checkedAdd(total, forwardOwned(row))
}
elapsed = time.Since(begin).Nanoseconds()
case "float":
floating := 0.0
begin := time.Now()
for _, value := range source {
floating += float64(value)
}
elapsed = time.Since(begin).Nanoseconds()
total = int64(floating)
case "bytes_concat":
chunks := make([][]byte, len(source))
for index := range source {
chunks[index] = source[index : index+1]
}
var output []byte
begin := time.Now()
for _, chunk := range chunks {
output = append(output, chunk...)
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range output {
total = checkedAdd(total, int64(value))
}
case "text_concat":
chunks := strings.Split(ticks, ",")
begin := time.Now()
var output strings.Builder
for _, chunk := range chunks {
output.WriteString(chunk)
}
joined := output.String()
elapsed = time.Since(begin).Nanoseconds()
for index := 0; index < len(joined); index++ {
total = checkedAdd(total, int64(joined[index]))
}
case "integer":
begin := time.Now()
for _, value := range source {
total = checkedAdd(total, int64(value))
}
elapsed = time.Since(begin).Nanoseconds()
case "list":
var values []int64
begin := time.Now()
for _, value := range source {
values = append(values, int64(value))
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, value)
}
case "map":
values := make(map[int64]int64)
var order []int64
begin := time.Now()
for _, value := range source {
key := int64(len(values))
values[key] = int64(value)
order = append(order, key)
}
elapsed = time.Since(begin).Nanoseconds()
for _, key := range order {
total = checkedAdd(total, values[key])
}
case "list_read":
var values []int64
for _, value := range source {
values = append(values, int64(value))
}
begin := time.Now()
for _, value := range values {
total = checkedAdd(total, value)
}
elapsed = time.Since(begin).Nanoseconds()
case "map_read":
values := make(map[int64]int64)
var order []int64
for _, value := range source {
key := int64(len(values))
values[key] = int64(value)
order = append(order, key)
}
begin := time.Now()
for _, key := range order {
total = checkedAdd(total, values[key])
}
elapsed = time.Since(begin).Nanoseconds()
case "list_record":
var values []record
begin := time.Now()
for _, value := range source {
values = append(values, record{int64(value), 0, 0})
}
elapsed = time.Since(begin).Nanoseconds()
for _, value := range values {
total = checkedAdd(total, checkedAdd(checkedAdd(value.First, value.Second), value.Third))
}
case "list_record_read":
var values []record
for _, value := range source {
values = append(values, record{int64(value), 0, 0})
}
begin := time.Now()
for _, value := range values {
total = checkedAdd(total, checkedAdd(checkedAdd(value.First, value.Second), value.Third))
}
elapsed = time.Since(begin).Nanoseconds()
default:
panic("unknown kernel")
}
return sample{elapsed, total}
}
func main() {
var arguments input
if err := json.Unmarshal([]byte(os.Args[2]), &arguments); err != nil {
panic(err)
}
var result any
if operation, continuous := strings.CutPrefix(os.Args[1], "continuous_"); continuous {
samples := make([]sample, 0, 10)
for range 10 {
samples = append(samples, measure(operation, arguments.Ticks))
}
result = samples
} else {
result = measure(os.Args[1], arguments.Ticks)
}
output, err := json.Marshal(result)
if err != nil {
panic(err)
}
fmt.Println(string(output))
}
+306
View File
@@ -0,0 +1,306 @@
use indexmap::IndexMap;
use std::hint::black_box;
use std::time::Instant;
struct Record {
first: i64,
second: i64,
third: i64,
}
struct OwnedRecord {
name: String,
tags: Vec<String>,
count: i64,
}
fn inspect_owned(row: &OwnedRecord) -> i64 {
add(
add(row.count, row.name.chars().count() as i64),
row.tags.len() as i64,
)
}
fn forward_owned(row: &OwnedRecord) -> i64 {
inspect_owned(row)
}
fn add(total: i64, value: i64) -> i64 {
total.checked_add(value).expect("integer overflow")
}
fn record_value(row: &Record) -> i64 {
add(add(row.first, row.second), row.third)
}
fn measure(operation: &str, ticks: &str) -> serde_json::Value {
let source = black_box(ticks.as_bytes());
let (checksum, elapsed) = match operation {
"record_update" => {
let begin = Instant::now();
let mut values = Vec::new();
let mut count = 0;
for value in source {
values.push(i64::from(*value));
count = add(count, 1);
}
black_box((&values, count));
let elapsed = begin.elapsed();
let total = values.iter().fold(0, |total, value| add(total, *value));
let checksum = add(total, count)
.checked_sub(values.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"record_inspect" => {
let begin = Instant::now();
let mut values = Vec::new();
let mut count = 0;
for value in source {
count = add(values.len() as i64, 1);
values.push(i64::from(*value));
}
black_box((&values, count));
let elapsed = begin.elapsed();
let total = values.iter().fold(0, |total, value| add(total, *value));
let checksum = add(total, count)
.checked_sub(values.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"map_lookup" => {
let values = source
.iter()
.enumerate()
.map(|(index, value)| (index as i64, i64::from(*value)))
.collect::<IndexMap<_, _>>();
let borrowed = black_box(&values);
let mut index = 0;
let mut total = 0;
let begin = Instant::now();
for _ in source {
total = add(total, borrowed.get(&index).copied().unwrap_or(0));
index = add(index, 1);
}
black_box(total);
let elapsed = begin.elapsed();
black_box(&values);
(total, elapsed)
}
"text_equal" | "text_contains" => {
let rows = source
.iter()
.map(|value| {
let left = format!("compare-value-{value}");
let right = left.to_uppercase().to_lowercase();
(left, right)
})
.collect::<Vec<_>>();
let borrowed = black_box(&rows);
let mut total = 0;
let begin = Instant::now();
if operation == "text_equal" {
for (left, right) in borrowed {
total = add(total, if left == right { 120 } else { 0 });
}
} else {
for (left, right) in borrowed {
total = add(
total,
if left.contains(right.as_str()) {
120
} else {
0
},
);
}
}
black_box(total);
let elapsed = begin.elapsed();
black_box(&rows);
let checksum = add(total, rows.len() as i64)
.checked_sub(source.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"map_text_lookup" => {
// 与 Dever 使用相同的拥有键和插入顺序;HashMap<&str, _> 是不同的数据结构。
let values = source
.iter()
.enumerate()
.map(|(index, value)| (format!("lookup-key-{index}"), i64::from(*value)))
.collect::<IndexMap<_, _>>();
let keys = values.keys().cloned().collect::<Vec<_>>();
let borrowed = black_box(&values);
let mut total = 0;
let begin = Instant::now();
for key in &keys {
total = add(total, borrowed.get(key.as_str()).copied().unwrap_or(0));
}
black_box(total);
let elapsed = begin.elapsed();
black_box((&keys, &values));
let checksum = add(total, keys.len() as i64)
.checked_sub(source.len() as i64)
.expect("integer overflow");
(checksum, elapsed)
}
"call_read" => {
let rows = source
.iter()
.map(|value| {
let name = format!("row-value-{value}");
OwnedRecord {
count: i64::from(*value) - name.chars().count() as i64 - 1,
tags: vec![name.clone()],
name,
}
})
.collect::<Vec<_>>();
let borrowed = black_box(&rows);
let begin = Instant::now();
let total = borrowed
.iter()
.fold(0, |total, row| add(total, forward_owned(row)));
black_box(total);
let elapsed = begin.elapsed();
black_box(&rows);
(total, elapsed)
}
"float" => {
let begin = Instant::now();
let total = source
.iter()
.fold(0.0, |total, value| total + f64::from(*value));
black_box(total);
let elapsed = begin.elapsed();
(total as i64, elapsed)
}
"bytes_concat" => {
let chunks = source.chunks(1).map(<[u8]>::to_vec).collect::<Vec<_>>();
let begin = Instant::now();
let mut output = Vec::new();
for chunk in &chunks {
output.extend_from_slice(chunk);
}
black_box(&output);
let elapsed = begin.elapsed();
drop(chunks);
let total = output
.iter()
.fold(0, |total, byte| add(total, i64::from(*byte)));
(total, elapsed)
}
"text_concat" => {
let chunks = std::str::from_utf8(source)
.unwrap()
.split(',')
.map(str::to_owned)
.collect::<Vec<_>>();
let begin = Instant::now();
let mut output = String::new();
for chunk in &chunks {
output.push_str(chunk);
}
black_box(&output);
let elapsed = begin.elapsed();
drop(chunks);
let total = output
.bytes()
.fold(0, |total, byte| add(total, i64::from(byte)));
(total, elapsed)
}
"integer" => {
let begin = Instant::now();
let total = source
.iter()
.fold(0, |total, value| add(total, i64::from(*value)));
(black_box(total), begin.elapsed())
}
"list" | "list_read" => {
let mut values = Vec::new();
let begin = Instant::now();
values.extend(source.iter().map(|value| i64::from(*value)));
let construction = begin.elapsed();
let borrowed = black_box(&values);
let begin = Instant::now();
let total = borrowed.iter().fold(0, |total, value| add(total, *value));
black_box(total);
let traversal = begin.elapsed();
drop(values);
(
total,
if operation == "list" {
construction
} else {
traversal
},
)
}
"list_record" | "list_record_read" => {
let mut values = Vec::new();
let begin = Instant::now();
for value in source {
values.push(Record {
first: i64::from(*value),
second: 0,
third: 0,
});
}
let construction = begin.elapsed();
let borrowed = black_box(&values);
let begin = Instant::now();
let total = borrowed
.iter()
.fold(0, |total, row| add(total, record_value(row)));
black_box(total);
let traversal = begin.elapsed();
drop(values);
(
total,
if operation == "list_record" {
construction
} else {
traversal
},
)
}
"map" | "map_read" => {
let mut values = IndexMap::new();
let begin = Instant::now();
for value in source {
let key = i64::try_from(values.len()).expect("map size");
values.insert(key, i64::from(*value));
}
let construction = begin.elapsed();
let borrowed = black_box(&values);
let begin = Instant::now();
let total = borrowed.values().fold(0, |total, value| add(total, *value));
black_box(total);
drop(values);
let traversal = begin.elapsed();
(
total,
if operation == "map" {
construction
} else {
traversal
},
)
}
_ => panic!("unknown kernel"),
};
serde_json::json!({"checksum": checksum, "elapsed_ns": elapsed.as_nanos()})
}
fn main() {
let arguments = std::env::args().collect::<Vec<_>>();
let input: serde_json::Value = serde_json::from_str(&arguments[2]).expect("input object");
let ticks = input["ticks"].as_str().expect("ticks string");
let output = if let Some(operation) = arguments[1].strip_prefix("continuous_") {
serde_json::Value::Array((0..10).map(|_| measure(operation, ticks)).collect())
} else {
measure(&arguments[1], ticks)
};
println!("{output}");
}
@@ -0,0 +1,213 @@
setting {
limits: dever.http.Limits
lifetime_ms: Int
}
type Service {
Tcp
Live
}
listening(result: dever.net.ListenResult.Listening(listener)) (
value: dever.system.Listener
) {
value = listener
}
listening(result: dever.net.ListenResult.Failed(message)) (value: dever.system.Listener) {
fail(app.WorkerFailure.Failed(message))
}
bound(result: dever.net.PortResult.Bound(port)) (value: Int) {
value = port
}
bound(result: dever.net.PortResult.Failed(message)) (value: Int) {
fail(app.WorkerFailure.Failed(message))
}
written(result: dever.io.WriteResult.Done) () {}
written(result: dever.io.WriteResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
closed(result: dever.io.CloseResult.Closed) () {}
closed(result: dever.io.CloseResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
paused(result: dever.time.SleepResult.Done) () {}
paused(result: dever.time.SleepResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
tcpChunk(event: dever.io.ReadEvent.Chunk(bytes), socket: dever.system.Socket) () recover("TCP 连接错误由运行器检查") {
written(result(dever.net.write(socket, bytes)))
}
tcpChunk(event: dever.io.ReadEvent.Failed(message), socket: dever.system.Socket) () recover("TCP 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
tcpStream(
result: dever.net.ReadStreamResult.Streaming(stream),
socket: dever.system.Socket
) () {
each(tcpChunk, stream, socket)
}
tcpStream(
result: dever.net.ReadStreamResult.Failed(message),
socket: dever.system.Socket
) () {
fail(app.WorkerFailure.Failed(message))
}
tcpConnection(
event: dever.net.ConnectResult.Connected(socket),
limits: dever.http.Limits
) () recover("TCP 连接错误由运行器检查") {
written(result(dever.net.timeout(socket, limits.timeout_ms)))
tcpStream(result(dever.net.chunks(socket, 4096)), socket)
closed(result(dever.net.close(socket)))
}
tcpConnection(
event: dever.net.ConnectResult.Failed(message),
limits: dever.http.Limits
) () recover("TCP 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
tcpServer(listener: dever.system.Listener, limits: dever.http.Limits) () {
stream = dever.net.connections(listener)
parallelEach(tcpConnection, stream, limits.connections, limits)
}
websocketMessage(
event: dever.websocket.MessageEvent.Read(message),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
websocketEcho(message, socket)
}
websocketMessage(
event: dever.websocket.MessageEvent.Failed(message),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
blocking(dever.io.println("ERROR|" + message))
}
websocketEcho(
message: dever.websocket.Message.Text(value),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
written(result(dever.websocket.send(socket, dever.websocket.Message.Text(value))))
}
websocketEcho(
message: dever.websocket.Message.Binary(value),
socket: dever.system.WebSocket
) () recover("WebSocket 连接错误由运行器检查") {
written(result(dever.websocket.send(socket, dever.websocket.Message.Binary(value))))
}
websocketEcho(message: other, socket: dever.system.WebSocket) () recover("WebSocket 连接错误由运行器检查") {}
websocketLimits() (result: dever.websocket.Limits) {
result = dever.websocket.Limits {
message_bytes = 4096
idle_ms = setting.lifetime_ms
write_ms = setting.limits.timeout_ms
}
}
websocketConnection(result: dever.websocket.ConnectResult.Connected(socket)) () {
each(websocketMessage, dever.websocket.messages(socket), socket)
}
websocketConnection(result: dever.websocket.ConnectResult.Failed(message)) () {
fail(app.WorkerFailure.Failed(message))
}
route(request: dever.http.Request, reply: dever.system.HttpReply) () {
livePath(request.target, reply)
}
livePath(target: "/ws", reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
websocketConnection(result(dever.websocket.accept(reply, websocketLimits())))
}
livePath(target: "/events", reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
written(result(dever.sse.start(reply, [])))
written(
result(
dever.sse.send(
reply,
dever.sse.Event {
event = "live"
data = "ready"
id = "1"
retry_ms = null
}
)
)
)
paused(result(dever.task.sleep(setting.lifetime_ms)))
}
livePath(target: other, reply: dever.system.HttpReply) () recover("实时连接错误由运行器检查") {
written(
result(
dever.http.respond(
reply,
dever.http.Response {
status = 404
headers = []
body = dever.bytes.fromText("unknown live benchmark path")
}
)
)
)
}
liveLimits() (result: dever.http.LiveLimits) {
result = dever.http.LiveLimits {
chunk_bytes = 4096
idle_ms = setting.lifetime_ms
heartbeat_ms = 100
}
}
liveServer(listener: dever.system.Listener) () {
dever.system.httpServeLive(route, listener, setting.limits, liveLimits())
}
serve(service: Service.Tcp, listener: dever.system.Listener) () {
tcpServer(listener, setting.limits)
}
serve(service: Service.Live, listener: dever.system.Listener) () {
liveServer(listener)
}
worker(service: Service) () {
listener = listening(result(dever.net.listen("127.0.0.1", 0)))
port = bound(result(dever.net.port(listener)))
server = run(serve(service, listener))
blocking(dever.io.println("READY|" + int.toText(port)))
paused(result(dever.task.sleep(setting.lifetime_ms)))
stop(server)
closed(result(dever.net.closeListener(listener)))
}
port.tcp() () {
worker(Service.Tcp)
}
port.live() () {
worker(Service.Live)
}
+5
View File
@@ -0,0 +1,5 @@
cmd tcp = app.tcp
cmd ws = app.ws
cmd sse = app.sse
+18
View File
@@ -0,0 +1,18 @@
type WorkerFailure {
error Failed(message: Text)
}
tcp() (result: Bool) {
port.tcp()
result = true
}
ws() (result: Bool) {
port.live()
result = true
}
sse() (result: Bool) {
port.live()
result = true
}
@@ -0,0 +1,3 @@
tcp() () fails app.WorkerFailure
live() () fails app.WorkerFailure
+144
View File
@@ -0,0 +1,144 @@
type Identity {
id: Text
user_id: model.id?
tenant_id: model.id?
}
verify(claims: dever.auth.Claims) (identity: Identity) {
identity = Identity {
id = claims.subject
user_id = null
tenant_id = null
}
}
ready() () {
blocking(bench.ready(0))
}
populateOne(value: Int) () {
created = model.create({
value = value
label = "fixture"
})
}
populate() () {
each(populateOne, config.ormRows())
}
crudOnce(value: Int) () {
begin = dever.time.monotonicNanos()
created = model.create({
value = value
label = "created"
})
changed = model.update(created.id, { label = "updated" })
removed = model.delete(created.id)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "crud", 1, changed + removed))
}
listOnce(value: Int) () {
begin = dever.time.monotonicNanos()
page = model.list({
order = id.asc
page = 1
size = 32
})
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "list", 1, page.total + length(page.items)))
}
cursorOnce(value: Int) () {
begin = dever.time.monotonicNanos()
page = model.cursor({
order = id.asc
size = 32
})
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "cursor", 1, length(page.items)))
}
countItem(value: model.Item, total: Int) (next: Int) {
next = total + 1
}
streamOnce(value: Int) () {
begin = dever.time.monotonicNanos()
rows = model.stream({
order = id.asc
size = 16
})
total = reduce(countItem, rows, 0)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "stream", 1, total))
}
poolOnce(value: Int) () {
begin = dever.time.monotonicNanos()
total = model.count()
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "pool", 1, total))
}
transaction fillCancelledTransaction() () {
created = model.create({
value = 1
label = "cancelled"
})
delayed = model.delay(config.ormCancelWork())
}
ormIdle() (result: Bool) {
total = model.count()
blocking(bench.ready(total))
bench.pause(config.lifetime())
result = true
}
ormCrud() (result: Bool) {
ready()
each(crudOnce, config.ormInput())
result = true
}
ormList() (result: Bool) {
populate()
ready()
each(listOnce, config.ormInput())
result = true
}
ormCursor() (result: Bool) {
populate()
ready()
each(cursorOnce, config.ormInput())
result = true
}
ormStream() (result: Bool) {
ready()
each(streamOnce, config.ormInput())
result = true
}
ormPool() (result: Bool) {
ready()
parallelEach(poolOnce, config.ormInput(), config.ormConcurrency())
result = true
}
ormSeed() (result: Bool) {
populate()
result = true
}
count() (total: Int) {
total = model.count()
}
cancel() (result: Bool) {
fillCancelledTransaction()
result = true
}
@@ -0,0 +1,9 @@
public get count = app.count
public post cancel = app.cancel
cmd idle = app.ormIdle
cmd crud = app.ormCrud
cmd list = app.ormList
cmd cursor = app.ormCursor
cmd stream = app.ormStream
cmd pool = app.ormPool
cmd seed = app.ormSeed
@@ -0,0 +1,13 @@
type DelayResult {
total: Int
}
global type Item {
value: Int index
label: Text(1, 32)
}
sql delay(work: Int) (result: DelayResult) {
sqlite = "WITH RECURSIVE counter(value) AS (SELECT 1 UNION ALL SELECT value + 1 FROM counter WHERE value < ?1) SELECT SUM(value) AS total FROM counter"
postgres = "SELECT SUM(value)::bigint AS total FROM generate_series(1::bigint, $1::bigint) AS value"
}
@@ -0,0 +1 @@
cmd run = app.run
@@ -0,0 +1,4 @@
run() (result: Bool) {
dever.io.println("profile")
result = true
}
@@ -0,0 +1,7 @@
port.blockingPause(millis: Int) () {
dever.time.sleep(millis)
}
port.reportError(message: Text) () {
dever.io.println("ERROR|" + message)
}
@@ -0,0 +1,11 @@
cmd sync_idle = app.syncIdle
cmd async_idle = app.asyncIdle
cmd tasks = app.tasks
cmd tasks_on_worker = app.tasksOnWorker
cmd channels = app.channels
cmd cancel = app.cancel
+132
View File
@@ -0,0 +1,132 @@
type Parking {
ready: Channel<Int>
gate: Channel<Int>
}
syncIdle() (result: Bool) {
bench.ready(0)
port.blockingPause(config.lifetime())
result = true
}
idle() () {
blocking(bench.ready(0))
bench.pause(config.lifetime())
}
asyncIdle() (result: Bool) {
idle()
result = true
}
identity(value: Int) (result: Int) {
result = value
}
taskSum(value: Int, state: Int) (result: Int) {
job = run(identity(value))
result = state + wait(job)
}
sum(value: Int, state: Int) (result: Int) {
result = state + value
}
taskSample(index: Int, input: Bytes) () {
begin = dever.time.monotonicNanos()
checksum = reduce(taskSum, input, 0)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "tasks", length(input), checksum))
}
sendByte(value: Int, output: Channel<Int>) () {
send(output, value)
}
produce(input: Bytes, output: Channel<Int>) () {
each(sendByte, input, output)
close(output)
}
channelSample(index: Int, input: Bytes) () {
output = channel(Int, config.channelCapacity())
begin = dever.time.monotonicNanos()
producer = run(produce(input, output))
checksum = reduce(sum, stream(output), 0)
wait(producer)
end = dever.time.monotonicNanos()
blocking(bench.report(begin, end, "channels", length(input), checksum))
}
waitOne(value: Int, parking: Parking) () {
send(parking.ready, value)
signaled(receive(parking.gate))
}
signaled(value: Int) () {}
signaled(value: null) () {
blocking(port.reportError("channel closed before expected signal"))
}
hold(parking: Parking) () {
parallelEach(waitOne, config.pendingInput(), config.pending(), parking)
}
started(value: Int, ready: Channel<Int>) () {
signaled(receive(ready))
}
cancelWorker() () {
parking = Parking {
ready = channel(Int, config.pending())
gate = channel(Int, 1)
}
job = run(hold(parking))
each(started, config.pendingInput(), parking.ready)
blocking(bench.ready(config.pending()))
bench.pause(config.idleSampleMs())
begin = dever.time.monotonicNanos()
stop(job)
end = dever.time.monotonicNanos()
close(parking.ready)
close(parking.gate)
blocking(bench.report(begin, end, "cancel", config.pending(), config.pending()))
}
taskWorker() () {
input = config.input()
blocking(bench.ready(0))
each(taskSample, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], input)
}
channelWorker() () {
input = config.input()
blocking(bench.ready(0))
each(channelSample, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], input)
}
tasks() (result: Bool) {
taskWorker()
result = true
}
nestedTaskWorker() () {
job = run(taskWorker())
wait(job)
}
tasksOnWorker() (result: Bool) {
nestedTaskWorker()
result = true
}
channels() (result: Bool) {
channelWorker()
result = true
}
cancel() (result: Bool) {
cancelWorker()
result = true
}
@@ -0,0 +1,3 @@
blockingPause(millis: Int) () fails dever.time.SleepResult
reportError(message: Text) () fails dever.io.WriteResult
+196
View File
@@ -0,0 +1,196 @@
"""Compare native Dever JSON operations with Go and Rust; build separately."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import statistics
import subprocess
import tempfile
import time
FIXTURE = Path(__file__).parent / "fixtures/json"
def fingerprint(path):
with path.open("rb") as source:
digest = hashlib.file_digest(source, "sha256").hexdigest()
return {"path": str(path), "bytes": path.stat().st_size, "sha256": digest}
def build(arguments):
output = arguments.output.resolve()
output.mkdir(parents=True, exist_ok=False)
module = output / "module/benchmark/json"
module.mkdir(parents=True)
for name in ("app.dever", "api.dever"):
shutil.copy2(FIXTURE / name, module / name)
(output / "config").mkdir()
(output / "config/setting.json").write_text("{}\n")
artifacts = {}
for label, compiler in (("dever", arguments.compiler),
("dever_before", arguments.baseline_compiler)):
if compiler is None:
continue
binary = output / label
with (output / (label + ".build.log")).open("w") as log:
subprocess.run([compiler, "build", str(output), "--output", str(binary)],
stdout=log, stderr=subprocess.STDOUT, check=True)
artifacts[label] = fingerprint(binary)
artifacts[label]["compiler"] = fingerprint(Path(compiler).resolve())
binary = output / "go"
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", str(binary),
str(FIXTURE.resolve() / "peer.go")], check=True)
artifacts["go"] = fingerprint(binary)
artifacts["go"]["compiler"] = subprocess.check_output(["go", "version"], text=True).strip()
command = ["cargo", "build", "--offline", "--locked", "--release", "-p", "dever-tests",
"--example", "json-peer", "--message-format=json"]
process = subprocess.run(command, cwd=FIXTURE.resolve().parents[3],
capture_output=True, text=True, check=True, timeout=300)
(output / "rust.build.log").write_text(process.stderr)
executables = [message["executable"] for line in process.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "json-peer"]
if len(executables) != 1:
raise RuntimeError("expected exactly one native Rust JSON peer")
binary = output / "rust"
shutil.copy2(executables[0], binary)
artifacts["rust"] = fingerprint(binary)
artifacts["rust"]["compiler"] = subprocess.check_output(["rustc", "--version"], text=True).strip()
artifacts["rust"]["source"] = fingerprint(FIXTURE.resolve() / "peer.rs")
artifacts["rust"]["profile"] = "release: opt-level=3, fat LTO, codegen-units=1"
(output / "manifest.json").write_text(json.dumps(artifacts, indent=2) + "\n")
print(output / "manifest.json")
def measure(command):
with tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr:
process = subprocess.Popen(command, stdout=stdout, stderr=stderr, close_fds=False)
deadline = time.monotonic() + 120
high_water = 0
try:
while True:
try:
process_root = Path("/proc") / str(process.pid)
if (process_root / "exe").resolve() == Path(command[0]).resolve():
for line in (process_root / "status").read_text().splitlines():
if line.startswith("VmHWM:"):
high_water = max(high_water, int(line.split()[1]))
except (FileNotFoundError, ProcessLookupError):
pass
waited, status, _ = os.wait4(process.pid, os.WNOHANG)
if waited:
process.returncode = os.waitstatus_to_exitcode(status)
break
if time.monotonic() >= deadline:
raise TimeoutError("benchmark exceeded 120 seconds")
time.sleep(0.005)
finally:
if process.returncode is None:
process.kill()
process.wait()
stdout.seek(0)
stderr.seek(0)
errors = stderr.read().decode()
if process.returncode or errors:
raise RuntimeError(errors or "benchmark failed")
result = json.loads(stdout.read())
if "code" in result:
if result["code"] != 0:
raise RuntimeError("Dever benchmark returned failure")
result = result["data"]
if high_water == 0:
raise RuntimeError("process ended before native RSS could be sampled")
result["sampled_hwm_kib"] = high_water
return result
def run(arguments):
if arguments.output.exists():
raise FileExistsError(arguments.output)
if arguments.cpu is not None:
if arguments.cpu not in os.sched_getaffinity(0):
raise ValueError("requested CPU is outside the allowed affinity")
os.sched_setaffinity(0, {arguments.cpu})
manifest = json.loads(arguments.manifest.read_text())
for artifact in manifest.values():
actual = fingerprint(Path(artifact["path"]))
if any(actual[field] != artifact[field] for field in ("bytes", "sha256")):
raise ValueError("benchmark artifact changed after build")
report = {"artifacts": manifest, "cpu_affinity": sorted(os.sched_getaffinity(0)),
"order": "rotate implementations each repeat within each workload",
"samples": [], "summary": []}
row = {"zeta": "field", "id": 9007199254740993, "alpha": "x" * 80,
"enabled": True, "values": [1, None, "escaped\nvalue"]}
for rows in (4, 40, 160):
source = {"zeta": {"caption": "form"}, "data": {"rows": [row] * rows},
"active": False, "alpha": "end"}
payload_bytes = len(json.dumps(source, separators=(",", ":")).encode())
for update in (False, True):
expected = dict(source, active=True) if update else source["data"]
groups = {label: [] for label in manifest}
labels = list(manifest)
for repeat in range(arguments.repeats):
offset = repeat % len(labels)
for label in labels[offset:] + labels[:offset]:
artifact = manifest[label]
iterations = (arguments.baseline_iterations if label == "dever_before"
else arguments.iterations)
payload = json.dumps({"source": source, "update": update,
"ticks": "x" * iterations}, separators=(",", ":"))
command = [artifact["path"]]
if label.startswith("dever"):
command.append("benchmark.json.run")
sample = measure(command + [payload])
if (json.loads(sample["output"]) != expected or
sample["checksum"] != len(sample["output"].encode()) * iterations or
sample["elapsed_ns"] <= 0):
raise AssertionError("JSON output or checksum mismatch")
sample.pop("output")
sample.update(label=label, update=update, bytes=payload_bytes,
iterations=iterations, repeat=repeat,
us_per_operation=sample["elapsed_ns"] / iterations / 1000)
groups[label].append(sample)
report["samples"].append(sample)
for label, samples in groups.items():
summary = {"label": label, "operation": "set" if update else "get",
"bytes": payload_bytes,
"median_us": statistics.median(item["us_per_operation"] for item in samples),
"sampled_hwm_kib": max(item["sampled_hwm_kib"] for item in samples)}
report["summary"].append(summary)
print(json.dumps(summary), flush=True)
with arguments.output.open("x") as output:
json.dump(report, output, indent=2)
output.write("\n")
def positive(value):
parsed = int(value)
if not 1 <= parsed <= 10000:
raise argparse.ArgumentTypeError("expected 1..10000")
return parsed
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
building = commands.add_parser("build")
building.add_argument("--compiler", required=True)
building.add_argument("--baseline-compiler")
building.add_argument("--output", type=Path, required=True)
running = commands.add_parser("run")
running.add_argument("--manifest", type=Path, required=True)
running.add_argument("--output", type=Path, required=True)
running.add_argument("--iterations", type=positive, default=2000)
running.add_argument("--baseline-iterations", type=positive, default=1)
running.add_argument("--repeats", type=positive, default=5)
running.add_argument("--cpu", type=int)
arguments = parser.parse_args()
(build if arguments.command == "build" else run)(arguments)
if __name__ == "__main__":
main()
+304
View File
@@ -0,0 +1,304 @@
"""Compare current Dever collection kernels with independent Go and Rust programs."""
import argparse
import errno
import hashlib
import json
import os
from pathlib import Path
import shutil
import statistics
import subprocess
import tempfile
import time
FIXTURE = Path(__file__).parent / "fixtures/kernel"
CORE = Path(__file__).resolve().parents[2]
CASES = {"integer": (16384, 65536), "float": (16384, 65536),
"list": (512, 1024, 2048, 4096),
"map": (512, 1024, 2048, 4096),
"list_read": (512, 1024, 2048, 4096),
"list_record": (512, 1024, 2048, 4096),
"list_record_read": (512, 1024, 2048, 4096),
"record_update": (512, 1024, 2048, 4096),
"record_inspect": (512, 1024, 2048, 4096),
"call_read": (512, 1024, 2048, 4096),
"map_read": (512, 1024, 2048, 4096),
"map_lookup": (512, 1024, 2048, 4096),
"map_text_lookup": (512, 1024, 2048, 4096),
"text_equal": (512, 1024, 2048, 4096),
"text_contains": (512, 1024, 2048, 4096),
"text_concat": (512, 2048, 8192, 32768),
"bytes_concat": (512, 2048, 8192, 32768)}
def fingerprint(path):
with path.open("rb") as source:
digest = hashlib.file_digest(source, "sha256").hexdigest()
return {"path": str(path), "bytes": path.stat().st_size, "sha256": digest}
def source_identity():
files = [CORE / "rust" / name for name in
("Cargo.toml", "Cargo.lock", "rust-toolchain.toml")]
for directory in ("rust/src", "lib", "sdk"):
files.extend(path for path in (CORE / directory).rglob("*")
if path.is_file() and path.suffix in
{".rs", ".toml", ".md", ".dever", ".py", ".c", ".h", ".js", ".mjs", ".json", ".go"})
digest = hashlib.sha256()
for path in sorted(files):
digest.update(str(path.relative_to(CORE)).encode() + b"\0")
digest.update(fingerprint(path)["sha256"].encode() + b"\n")
return {"sha256": digest.hexdigest(), "files": len(files)}
def build(arguments):
tree = source_identity()
output = arguments.output.resolve()
output.mkdir(parents=True, exist_ok=False)
module = output / "module/benchmark/kernel"
module.mkdir(parents=True)
for name in ("app.dever", "api.dever"):
shutil.copy2(FIXTURE / name, module / name)
(output / "config").mkdir()
(output / "config/setting.json").write_text("{}\n")
artifacts = {}
for label, compiler in (("dever", arguments.compiler),
("dever_before", arguments.baseline_compiler)):
if compiler is None:
continue
binary = output / label
with (output / (label + ".build.log")).open("w") as log:
subprocess.run([compiler, "build", str(output), "--output", str(binary)],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=300)
artifacts[label] = fingerprint(binary)
artifacts[label]["compiler"] = fingerprint(Path(compiler).resolve())
artifacts[label]["source_tree"] = tree
generated = output / (label + ".rs")
with generated.open("w") as source:
subprocess.run([compiler, "emit", str(output)], stdout=source,
check=True, timeout=120)
artifacts[label]["generated"] = fingerprint(generated)
binary = output / "go"
subprocess.run(["go", "build", "-trimpath", "-ldflags=-s -w", "-o", str(binary),
str(FIXTURE.resolve() / "peer.go")], check=True, timeout=120)
artifacts["go"] = fingerprint(binary)
artifacts["go"]["compiler"] = subprocess.check_output(["go", "version"], text=True).strip()
peer = output / "rust-peer"
peer.mkdir()
shutil.copy2(FIXTURE / "peer.rs", peer / "main.rs")
core = FIXTURE.resolve().parents[3]
shutil.copy2(core / "rust/rust-toolchain.toml", peer / "rust-toolchain.toml")
(peer / "Cargo.toml").write_text(
'[package]\nname = "kernel-peer"\nversion = "0.0.1"\nedition = "2024"\n'
'[workspace]\n[[bin]]\nname = "kernel-peer"\npath = "main.rs"\n'
'[dependencies]\nserde_json = "=1.0.145"\nindexmap = "=2.14.0"\n'
'[profile.release]\nopt-level = 3\nlto = "fat"\ncodegen-units = 1\n'
'overflow-checks = true\nstrip = "symbols"\n')
subprocess.run(["cargo", "generate-lockfile", "--offline"], cwd=peer,
check=True, timeout=120)
command = ["cargo", "build", "--offline", "--locked", "--release",
"--bin", "kernel-peer", "--message-format=json"]
process = subprocess.run(command, cwd=peer,
capture_output=True, text=True, check=True, timeout=300)
(output / "rust.build.log").write_text(process.stderr)
executables = [message["executable"] for line in process.stdout.splitlines()
if (message := json.loads(line)).get("reason") == "compiler-artifact"
and message.get("executable") and message["target"]["name"] == "kernel-peer"]
if len(executables) != 1:
raise RuntimeError("expected exactly one native Rust peer")
binary = output / "rust"
shutil.copy2(executables[0], binary)
artifacts["rust"] = fingerprint(binary)
artifacts["rust"]["compiler"] = subprocess.check_output(
["rustc", "--version"], cwd=peer, text=True).strip()
artifacts["rust"]["profile"] = "release: opt-level=3, fat LTO, codegen-units=1"
artifacts["rust"]["source"] = fingerprint(peer / "main.rs")
artifacts["rust"]["lock"] = fingerprint(peer / "Cargo.lock")
artifacts["rust"]["map"] = "ordered IndexMap with owned keys, matching Dever"
if source_identity() != tree:
raise RuntimeError("sources changed during build; rebuild the comparison")
(output / "manifest.json").write_text(json.dumps(artifacts, indent=2) + "\n")
print(output / "manifest.json")
def run(arguments):
if arguments.output.exists():
raise FileExistsError(arguments.output)
if arguments.cpu is not None:
if arguments.cpu not in os.sched_getaffinity(0):
raise ValueError("requested CPU is outside the allowed affinity")
os.sched_setaffinity(0, {arguments.cpu})
manifest = json.loads(arguments.manifest.read_text())
if arguments.candidate not in manifest:
raise ValueError("candidate is missing from the manifest")
if arguments.parity_with is not None and arguments.parity_with not in manifest:
raise ValueError("parity comparator is missing from the manifest")
for artifact in manifest.values():
actual = fingerprint(Path(artifact["path"]))
if any(actual[field] != artifact[field] for field in ("bytes", "sha256")):
raise ValueError("benchmark artifact changed after build")
identities = [artifact["source_tree"] for artifact in manifest.values()
if "source_tree" in artifact]
if identities and any(identity != source_identity() for identity in identities):
raise ValueError("current sources differ from the build; rebuild before measuring")
report = {"artifacts": manifest, "cpu_affinity": sorted(os.sched_getaffinity(0)),
"measured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"manifest": fingerprint(arguments.manifest), "repeats": arguments.repeats,
"samples_per_process": 10 if arguments.continuous else 1,
"measurement": "continuous" if arguments.continuous else "cold",
"samples": [], "growth": {}}
operations = getattr(arguments, "operations", None) or list(CASES)
if len(set(operations)) != len(operations) or any(operation not in CASES for operation in operations):
raise ValueError("kernel selection contains duplicate or unknown operations")
if arguments.check_growth and not any(operation in operations for operation in ("list", "map", "list_record")):
raise ValueError("growth checking requires a collection construction operation")
report["operations"] = operations
with tempfile.TemporaryDirectory(prefix="kernel-run-", dir=arguments.output.resolve().parent) as temporary:
execution = stage_execution(manifest, Path(temporary))
report["execution"] = execution
measure(arguments, report)
def stage_execution(manifest, directory):
for label, artifact in manifest.items():
if label.startswith("dever"):
configuration = Path(artifact["path"]).parent / "config/setting.json"
if json.loads(configuration.read_text()) != {}:
raise ValueError("kernel benchmark requires an empty application configuration")
configuration = directory / "config/setting.json"
configuration.parent.mkdir()
configuration.write_text("{}\n")
artifacts = {}
for index, (label, artifact) in enumerate(manifest.items()):
binary = directory / f"program-{index:04d}"
method = "hardlink"
try:
os.link(artifact["path"], binary)
except OSError as error:
if error.errno != errno.EXDEV:
raise
shutil.copy2(artifact["path"], binary)
method = "copy"
actual = fingerprint(binary)
if any(actual[field] != artifact[field] for field in ("bytes", "sha256")):
raise ValueError("benchmark artifact changed during staging")
artifacts[label] = {**actual, "method": method}
return {"artifacts": artifacts, "configuration": fingerprint(configuration)}
def measure(arguments, report):
manifest = report["artifacts"]
operations = report["operations"]
executables = report["execution"]["artifacts"]
for operation in operations:
sizes = CASES[operation]
for count in sizes:
ticks = ",".join(["x"] * count) if operation == "text_concat" else "x" * count
continuous = arguments.continuous
payload = json.dumps({"ticks": ticks})
timings = {label: [] for label in manifest}
process_timings = {label: [] for label in manifest}
process_elapsed = {label: [] for label in manifest}
for repeat in range(arguments.repeats):
labels = list(manifest)
offset = repeat % len(labels)
labels = labels[offset:] + labels[:offset]
for label in labels:
action = (("benchmark.kernel." if label.startswith("dever") else "")
+ ("continuous_" if continuous else "") + operation)
started = time.perf_counter_ns()
process = subprocess.run([executables[label]["path"], action, payload],
capture_output=True, text=True, timeout=120, check=True)
process_elapsed[label].append((time.perf_counter_ns() - started) / 1_000_000)
if process.stderr:
raise RuntimeError(process.stderr)
value = json.loads(process.stdout)
if label.startswith("dever"):
if value["code"] != 0:
raise RuntimeError("Dever benchmark returned failure")
value = value["data"]
values = value if continuous else [value]
if not isinstance(values, list) or len(values) != (10 if continuous else 1):
raise AssertionError("kernel sample count mismatch")
if any(sample["checksum"] != count * 120 or sample["elapsed_ns"] <= 0
for sample in values):
raise AssertionError("kernel output or checksum mismatch")
measured = [sample["elapsed_ns"] / 1_000_000 for sample in values]
process_timings[label].append(measured)
timings[label].extend(measured[1:] if continuous else measured)
item = {"operation": operation, "count": count, "samples_ms": timings,
"process_samples_ms": process_timings,
"process_elapsed_ms": process_elapsed,
"first_sample_median_ms": {
label: statistics.median(values[0] for values in processes)
for label, processes in process_timings.items()},
"median_ms": {label: statistics.median(values) for label, values in timings.items()}}
report["samples"].append(item)
print(json.dumps(item), flush=True)
for operation in ("list", "map", "list_record"):
if operation not in operations:
continue
samples = [item for item in report["samples"] if item["operation"] == operation]
report["growth"][operation] = {
label: samples[-1]["median_ms"][label] / samples[0]["median_ms"][label]
for label in manifest}
if arguments.parity_with is not None:
cases = [{
"operation": item["operation"],
"count": item["count"],
"ratio": item["median_ms"][arguments.candidate] / item["median_ms"][arguments.parity_with],
"passed": item["median_ms"][arguments.candidate] <= item["median_ms"][arguments.parity_with],
} for item in report["samples"]]
report["parity"] = {
"candidate": arguments.candidate,
"comparator": arguments.parity_with,
"passed": all(item["passed"] for item in cases),
"cases": cases,
}
with arguments.output.open("x") as output:
json.dump(report, output, indent=2)
output.write("\n")
if arguments.check_growth and any(values[arguments.candidate] > 24 for values in report["growth"].values()):
raise AssertionError("8x input exceeded 24x collection construction time; inspect raw samples")
if arguments.parity_with is not None and not report["parity"]["passed"]:
failures = [item for item in report["parity"]["cases"] if not item["passed"]]
raise AssertionError(
f"{len(failures)} of {len(cases)} cases did not reach {arguments.parity_with} parity; "
f"full samples and ratios saved to {arguments.output}"
)
def repeats(value):
parsed = int(value)
if not 3 <= parsed <= 25:
raise argparse.ArgumentTypeError("expected 3..25 repetitions")
return parsed
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
building = commands.add_parser("build")
building.add_argument("--compiler", required=True)
building.add_argument("--baseline-compiler")
building.add_argument("--output", type=Path, required=True)
running = commands.add_parser("run")
running.add_argument("--manifest", type=Path, required=True)
running.add_argument("--output", type=Path, required=True)
running.add_argument("--repeats", type=repeats, default=5)
running.add_argument("--continuous", action="store_true",
help="retain ten calls per process; compare calls 2..10 separately from the first")
running.add_argument("--cpu", type=int)
running.add_argument("--check-growth", action="store_true")
running.add_argument("--operations", nargs="+", choices=tuple(CASES),
help="select diagnostic kernels; omitted runs the complete matrix")
running.add_argument("--parity-with", choices=("go", "rust"))
running.add_argument("--candidate", default="dever")
arguments = parser.parse_args()
(build if arguments.command == "build" else run)(arguments)
if __name__ == "__main__":
main()
+97
View File
@@ -0,0 +1,97 @@
"""长连接对端的阶段校验与采样;进程预算由统一运行器管理。"""
import json
import time
from process import Process, summarize
PHASES = ("connecting", "connected", "closing", "disconnected")
SETTLE_SECONDS = 0.3
PROCESS_MARGIN_SECONDS = 10
def phase_sequence(cycles):
return [(cycle, phase) for cycle in range(cycles) for phase in PHASES]
def lifecycle_seconds(cycles, hold_seconds, timeout_ms):
timeout_seconds = timeout_ms / 1000
# Each cycle has independent connect, hold-overrun and close deadlines.
return cycles * (hold_seconds + 3 * timeout_seconds + SETTLE_SECONDS) + PROCESS_MARGIN_SECONDS
def stable_phase(before, after):
return before[-1] if before and before == after else None
def parse_progress(text, cycles):
phases = []
result = None
# stdout is sampled while its writer is alive; only consume complete lines.
for line in text.splitlines(keepends=True):
if not line.endswith("\n"):
continue
if result is not None:
raise ValueError("live benchmark output continued after its final result")
if line.startswith("PHASE|"):
_, cycle, phase = line.strip().split("|")
phases.append((int(cycle), phase))
elif line.startswith("{"):
result = json.loads(line)
else:
raise ValueError(f"unexpected live benchmark output: {line.strip()}")
if phases != phase_sequence(cycles)[:len(phases)]:
raise ValueError("live benchmark phases are missing, duplicated or out of order")
return phases, result
def validate_result(result, protocol, connections, cycles):
counts = ("attempted_connections", "connected", "disconnected", "messages", "errors")
if result is None or any(type(result[key]) is not int or result[key] < 0 for key in counts):
raise ValueError("missing or invalid live counters")
expected = connections * cycles
if (result["protocol"] != protocol or result["connections"] != connections or result["cycles"] != cycles
or any(result[key] != expected for key in counts[:3])
or result["messages"] < expected or result["errors"] != 0 or result["elapsed_ms"] <= 0):
raise ValueError("live benchmark did not validate and close all requested connections")
def load_live(args, process, port, protocol, connections, timeout_ms):
scheme, path = {"tcp": ("tcp", "/"), "ws": ("ws", "/ws"), "sse": ("http", "/events")}[protocol]
hold_ms = round(args.duration * 1000)
command = [args.artifacts / "live_bench", protocol, f"{scheme}://127.0.0.1:{port}{path}",
connections, args.cycles, hold_ms, timeout_ms]
phases = {phase: [] for phase in phase_sequence(args.cycles)}
with Process(command, process.directory / "load", cpus=args.client_cpus,
peer_settings={"workers": args.client_workers}) as client:
deadline = time.monotonic() + lifecycle_seconds(args.cycles, args.duration, timeout_ms)
while client.child.poll() is None:
if time.monotonic() >= deadline:
raise TimeoutError("long connection client exceeded its bounded lifecycle")
progress_before, _ = parse_progress(client.text(), args.cycles)
first = len(process.samples)
process.sample()
process.check()
if process.child.poll() is not None:
raise RuntimeError("server exited during long connection load")
progress_after, _ = parse_progress(client.text(), args.cycles)
if phase := stable_phase(progress_before, progress_after):
phases[phase].extend(process.samples[first:])
client.sample()
time.sleep(args.interval)
client.check()
progress, result = parse_progress(client.text(), args.cycles)
if progress != phase_sequence(args.cycles):
raise ValueError("long connection client ended before all phases")
validate_result(result, protocol, connections, args.cycles)
# Connection setup/close can finish between polls. Only the two explicit
# hold windows require observations; retain setup samples when present.
observations = []
for (cycle, phase), samples in phases.items():
if phase in ("connected", "disconnected") and len(samples) < 2:
raise ValueError(f"sampling interval missed {cycle}/{phase} window")
observations.append({"cycle": cycle, "phase": phase,
"resources": summarize(samples) if samples else None,
"last": samples[-1] if samples else None, "raw": samples})
return {"result": result, "phases": observations,
"client_resources": summarize(client.samples), "client_raw": client.samples}
+237
View File
@@ -0,0 +1,237 @@
"""Linux 进程与可选专属 cgroup 的生命周期和采样。"""
import os
from pathlib import Path
import signal
import statistics
import subprocess
import time
import uuid
from settings import stage_peer
def key_values(path):
return {fields[0].rstrip(":"): fields[1] for line in path.read_text().splitlines()
if len(fields := line.split()) >= 2}
def process_sample(pid):
root = Path("/proc") / str(pid)
status = key_values(root / "status")
if "VmRSS" not in status:
raise ProcessLookupError(f"process {pid} exited during sampling")
memory = key_values(root / "smaps_rollup")
# comm 字段可含空格和括号;从最后一个右括号之后解析固定 stat 字段。
stat = (root / "stat").read_text().rsplit(")", 1)[1].split()
switches = {"voluntary_ctxt_switches": 0, "nonvoluntary_ctxt_switches": 0}
for thread in (root / "task").iterdir():
try:
thread_status = key_values(thread / "status")
except FileNotFoundError:
continue # A completed blocking thread can disappear during a sample.
for name in switches:
switches[name] += int(thread_status[name])
return {
"time": time.monotonic(),
"rss_bytes": int(status["VmRSS"]) * 1024,
"rss_high_water_bytes": int(status["VmHWM"]) * 1024,
"pss_bytes": int(memory["Pss"]) * 1024,
"threads": int(status["Threads"]),
"fds": len(list((root / "fd").iterdir())),
"cpu_seconds": (int(stat[11]) + int(stat[12])) / os.sysconf("SC_CLK_TCK"),
**switches,
}
class Budget:
def __init__(self, parent, memory_mib, cpu_quota):
self.path = None
self.final = None
if parent is None:
if memory_mib is not None or cpu_quota is not None:
raise ValueError("memory/cpu quotas require --cgroup-parent")
return
self.path = parent.resolve() / ("dever-perf-" + uuid.uuid4().hex)
self.path.mkdir()
try:
if memory_mib is not None:
(self.path / "memory.max").write_text(str(memory_mib * 1024 * 1024))
(self.path / "memory.swap.max").write_text("0")
if cpu_quota is not None:
(self.path / "cpu.max").write_text(f"{round(cpu_quota * 100000)} 100000")
except BaseException:
self.path.rmdir()
raise
def sample(self):
if self.path is None:
return None
return {
"current_bytes": int((self.path / "memory.current").read_text()),
"peak_bytes": int((self.path / "memory.peak").read_text()),
"events": {k: int(v) for k, v in key_values(self.path / "memory.events").items()},
"cpu": {k: int(v) for k, v in key_values(self.path / "cpu.stat").items()},
"max": (self.path / "memory.max").read_text().strip(),
"cpu_max": (self.path / "cpu.max").read_text().strip(),
}
def close(self):
if self.path is not None:
try:
self.final = self.sample()
except OSError as error:
try:
self.path.rmdir()
except OSError as cleanup_error:
error.add_note(f"cannot remove owned cgroup {self.path}: {cleanup_error}")
raise
else:
self.path.rmdir()
class Process:
def __init__(self, command, directory, *, peer_settings=None, cpus=None, budget=None):
self.command = [str(arg) for arg in command]
self.directory = directory
self.budget = budget
self.samples = []
self.directory.mkdir(parents=True, exist_ok=False)
if peer_settings is not None:
self.command[0] = str(stage_peer(command[0], directory, peer_settings))
self.stdout = (directory / "stdout.log").open("w")
self.stderr = (directory / "stderr.log").open("w")
self.started = time.monotonic()
def prepare_child():
# 运行器单线程;只对即将 exec 的自有子进程设置 CPU/cgroup。
if cpus is not None:
os.sched_setaffinity(0, cpus)
if budget is not None and budget.path is not None:
(budget.path / "cgroup.procs").write_text(str(os.getpid()))
try:
self.child = subprocess.Popen(self.command, stdout=self.stdout, stderr=self.stderr,
preexec_fn=prepare_child, start_new_session=True)
except BaseException:
self.stdout.close()
self.stderr.close()
raise
def text(self):
return (self.directory / "stdout.log").read_text()
def descriptors(self):
descriptors = {}
for path in (Path("/proc") / str(self.child.pid) / "fd").iterdir():
try:
descriptors[path.name] = os.readlink(path)
except FileNotFoundError:
continue # A descriptor may close while /proc is being read.
return descriptors
def sample(self):
if self.child.poll() is not None:
return
try:
sample = process_sample(self.child.pid)
except (FileNotFoundError, ProcessLookupError):
# Linux can release /proc memory fields before waitpid reports exit.
# This observation is unavailable; check() still owns exit status.
return
except PermissionError as failure:
try:
self.child.wait(timeout=0.1)
except subprocess.TimeoutExpired:
raise failure
return
sample["elapsed_seconds"] = sample.pop("time") - self.started
if self.budget is not None:
sample["cgroup"] = self.budget.sample()
self.samples.append(sample)
def ready(self, timeout=10):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
self.sample()
for line in self.text().splitlines():
if line.startswith("ERROR|"):
raise RuntimeError(line)
if line.startswith("READY|"):
self.ready_seconds = time.monotonic() - self.started
return int(line.split("|")[1])
if self.child.poll() is not None:
raise RuntimeError(f"process exited before readiness: {self.directory}")
time.sleep(0.005)
raise TimeoutError(f"readiness timeout: {self.directory}")
def check(self):
code = self.child.poll()
if code is not None and code != 0:
raise RuntimeError(f"process exited {code}: {self.directory}")
if "ERROR|" in self.text() or (self.directory / "stderr.log").stat().st_size:
raise RuntimeError(f"benchmark reported an error: {self.directory}")
def close(self):
try:
if self.child.poll() is None:
try:
os.killpg(self.child.pid, signal.SIGTERM)
except ProcessLookupError:
# 子进程可能恰好在 poll 与 signal 之间自然退出。
pass
try:
self.child.wait(timeout=3)
except subprocess.TimeoutExpired:
os.killpg(self.child.pid, signal.SIGKILL)
self.child.wait(timeout=3)
finally:
self.stdout.close()
self.stderr.close()
def __enter__(self):
return self
def __exit__(self, *_):
self.close()
def observe(processes, seconds, interval):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
for process in processes:
process.sample()
process.check()
time.sleep(min(interval, max(0, deadline - time.monotonic())))
def summarize(samples):
if not samples:
raise ValueError("process ended before any resource sample")
result = {"samples": len(samples)}
for metric in ("rss_bytes", "pss_bytes", "threads", "fds"):
values = sorted(sample[metric] for sample in samples)
result[metric + "_median"] = values[len(values) // 2]
result[metric + "_max"] = max(values)
result["rss_high_water_bytes"] = max(sample["rss_high_water_bytes"] for sample in samples)
elapsed = samples[-1]["elapsed_seconds"] - samples[0]["elapsed_seconds"]
cpu = samples[-1]["cpu_seconds"] - samples[0]["cpu_seconds"]
result["cpu_seconds_sampled"] = cpu
result["cpu_cores_sampled"] = cpu / elapsed if elapsed > 0 else None
for name in ("voluntary_ctxt_switches", "nonvoluntary_ctxt_switches"):
result[name + "_sampled"] = samples[-1][name] - samples[0][name]
return result
def resource_trend(samples):
"""报告四段中位数与后半程增长,避免将启动分配误记为长期泄漏。"""
if len(samples) < 4:
raise ValueError("resource trend requires at least four observations")
windows = [samples[len(samples) * index // 4:len(samples) * (index + 1) // 4]
for index in range(4)]
medians = [statistics.median(sample["rss_bytes"] for sample in window) for window in windows]
elapsed = samples[-1]["elapsed_seconds"] - samples[0]["elapsed_seconds"]
return {"observed_seconds": elapsed, "rss_quarter_medians_bytes": medians,
"rss_late_growth_bytes": medians[-1] - medians[2],
"rss_late_growth_bytes_per_second": (medians[-1] - medians[2]) / (elapsed / 4) if elapsed > 0 else None,
"fds_first": samples[0]["fds"], "fds_last": samples[-1]["fds"]}
File diff suppressed because it is too large. Load diff
+59
View File
@@ -0,0 +1,59 @@
"""基准配置与隔离 peer 布置;配置只来自 config/setting.json。"""
import errno
import json
import os
from pathlib import Path
import shutil
import tempfile
ROOT = Path(__file__).resolve().parents[2]
PEER_CONFIGURATION = "setting-json-v1"
def write_private_json(path, value, *, replace=False):
path = Path(path)
temporary = None
if replace:
descriptor, temporary = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}-")
else:
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(descriptor, "w") as output:
os.fchmod(output.fileno(), 0o600)
json.dump(value, output, indent=2)
output.write("\n")
if temporary is not None:
os.replace(temporary, path)
finally:
if temporary is not None:
Path(temporary).unlink(missing_ok=True)
def stage_peer(executable, directory, settings):
"""独立配置共用不可变程序;测量期间不得重写输入二进制。"""
executable = Path(executable).resolve(strict=True)
destination = directory / executable.name
(directory / "config").mkdir()
write_private_json(directory / "config/setting.json", {"benchmark": settings})
try:
os.link(executable, destination)
except OSError as error:
if error.errno != errno.EXDEV:
raise
# 配置目录可以位于另一文件系统;这种情况下硬链接不可用。
with destination.open("xb") as output, executable.open("rb") as source:
shutil.copyfileobj(source, output)
shutil.copymode(executable, destination)
return destination.resolve()
def configured_peer(name, setting_path=ROOT / "config/setting.json"):
document = json.loads(setting_path.read_text())
section = document.get("performance") if isinstance(document, dict) else None
value = section.get(name) if isinstance(section, dict) else None
if not isinstance(value, str) or not value.strip():
raise ValueError(f"config/setting.json must define performance.{name} as a binary path")
# 仓库设置中的相对路径以项目根目录为基准。
path = Path(value)
return (setting_path.parent.parent / path).resolve(strict=True)
+272
View File
@@ -0,0 +1,272 @@
import json
from pathlib import Path
import subprocess
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from cms import (CmsClient, ReadLatency, concurrent_reads, decode_response, execute, isolated_settings, latency_summary,
provision_identity, publish_workload, run_postgres_case, run_postgres_rbac)
from settings import write_private_json
class CmsMeasurementTests(unittest.TestCase):
def test_identity_provisioning_uses_current_tenant_commands(self):
identity = {"tenant_id": 19, "user_id": 31, "credential_id": 7}
executable = Path("owned-app")
with patch("cms.execute", side_effect=[identity, None, None, None]) as execute_command:
actual, password = provision_identity(executable, Path("owned-output"), "example")
self.assertEqual(actual, identity)
self.assertTrue(password)
calls = execute_command.call_args_list
self.assertEqual(calls[0].args[0][1], "user.account.bootstrap")
self.assertEqual(calls[1].args[0],
[executable, "--tenant", 19, "dever.tenant.migrate", "null"])
for call, site in zip(calls[2:], ("admin", "front")):
self.assertEqual(call.args[0][:4],
[executable, "--tenant", 19, "dever.auth.owner"])
self.assertEqual(json.loads(call.args[0][4]), {"site": site, "user_id": 31})
def test_long_read_histogram_is_bounded_and_reports_upper_bounds(self):
latency = ReadLatency()
for _ in range(10000):
latency.add(3)
other = ReadLatency()
other.add(100)
latency.merge(other)
self.assertEqual(len(latency.buckets), 2048)
self.assertEqual(latency.count, 10001)
self.assertGreaterEqual(latency.percentile(.99), 3)
self.assertLess(latency.percentile(.99), 3.05)
client = CmsClient(12345)
client.record_timings = False
client.connection = SimpleNamespace(request=Mock(), getresponse=Mock(return_value=SimpleNamespace(
status=200, read=Mock(return_value=b'{"code":0,"message":"ok","data":1}'))), close=Mock())
try:
self.assertEqual(client.request("GET", "/test"), 1)
self.assertEqual(client.timings, {})
finally:
client.close()
def test_concurrent_read_failure_closes_clients_and_cannot_be_counted_as_success(self):
expected = {"total": 1, "items": [{"title": "published"}]}
for reply in ({"total": 0, "items": []}, RuntimeError("HTTP 403")):
with self.subTest(reply=reply):
opened = []
def client(_port):
request = Mock(side_effect=reply) if isinstance(reply, Exception) else Mock(return_value=reply)
instance = SimpleNamespace(cookies={}, timings={}, request=request, close=Mock())
opened.append(instance)
return instance
process = SimpleNamespace(samples=[], check=Mock())
with patch("cms.CmsClient", side_effect=client), patch("cms.observe"):
with self.assertRaises(RuntimeError):
concurrent_reads(12345, {"front": "test-only-cookie"}, expected, process,
concurrency=2, seconds=.1, interval=.01)
self.assertEqual(len(opened), 2)
for instance in opened:
instance.close.assert_called_once()
def test_delete_inputs_use_query_instead_of_json_body(self):
client = CmsClient(12345)
response = SimpleNamespace(status=200, read=Mock(return_value=b'{"code":0,"message":"ok","data":true}'))
connection = SimpleNamespace(request=Mock(), getresponse=Mock(return_value=response), close=Mock())
client.connection = connection
try:
self.assertIs(client.request("DELETE", "/revoke", {"user_id": 3, "role_id": "fixture reader"}), True)
sent = connection.request.call_args.args
self.assertEqual(sent[:3], ("DELETE", "/revoke?user_id=3&role_id=fixture+reader", None))
finally:
client.close()
def test_postgres_rbac_checks_catalog_roles_and_same_session_revocation(self):
catalog = [
{"component": "news", "domain": "article", "site": "admin", "action": "list",
"method": "GET", "key": "news.article.admin.list"},
{"component": "user", "domain": "authorization", "site": "admin", "action": "permissions",
"method": "GET", "key": "user.authorization.admin.permissions"},
{"component": "news", "domain": "article", "site": "front", "action": "list",
"method": "GET", "key": "news.article.front.list"},
]
identity = {"user_id": 1, "tenant_id": 2}
member = {"user_id": 3, "tenant_id": 2, "credential_id": 4}
login = [{"account": {"id": 3}, "tenant_id": 2, "site": site}
for site in ("admin", "front")]
for failure in (False, True):
with self.subTest(failure=failure):
owner = SimpleNamespace(request=Mock(side_effect=[
[entry for entry in catalog if entry["site"] == "admin"],
[entry for entry in catalog if entry["site"] == "front"],
] + [True] * 9))
responses = login + ([RuntimeError("missing denial")] if failure else [None] * 11)
client = SimpleNamespace(request=Mock(side_effect=responses), close=Mock())
with patch("cms.execute", return_value=member), patch("cms.CmsClient", return_value=client):
if failure:
with self.assertRaisesRegex(RuntimeError, "missing denial"):
run_postgres_rbac(Path("owned-app"), Path("owned-output"), 12345, owner, identity)
else:
result = run_postgres_rbac(Path("owned-app"), Path("owned-output"), 12345, owner, identity)
self.assertEqual(result["member_user_id"], 3)
self.assertEqual(set(result["permissions"]), {"admin_read", "admin_permissions", "front_list"})
self.assertNotIn("password", result)
calls = client.request.call_args_list[2:]
self.assertEqual([call.kwargs.get("expected", 200) for call in calls],
[403, 403, 403, 200, 200, 200, 403, 200, 200, 403, 403])
saves = [call for call in owner.request.call_args_list if call.args[1].endswith("/save")]
self.assertEqual(owner.request.call_args_list[1].kwargs["site"], "front")
self.assertEqual(saves[0].args[2]["id"], saves[2].args[2]["id"])
self.assertNotEqual(saves[0].kwargs["site"], saves[2].kwargs["site"])
client.close.assert_called_once()
def test_postgres_case_keeps_source_configuration_on_build_failure(self):
with tempfile.TemporaryDirectory(prefix="dever-cms-source-") as temporary:
directory = Path(temporary)
source = directory / "project"
source.mkdir()
for name in ("config", "module", "test"):
(source / name).mkdir()
original = {
"runtime": {"mode": "all"},
"database": {"default": {
"type": "postgres", "url": "postgres://fixture@127.0.0.1:12345/control",
"tenant_database_prefix": "dever_test_owned",
}},
"auth": {"providers": {"session": {"jwtSecret": "original"}}},
"sites": {}, "log": {"level": "info"},
}
setting = source / "config/setting.json"
write_private_json(setting, original)
before = setting.read_bytes()
staged_paths = []
def fail_check(command, *_args, **_kwargs):
staged = command[2]
staged_paths.append(staged)
self.assertNotEqual(staged, source)
changed = json.loads((staged / "config/setting.json").read_text())
self.assertEqual(changed["runtime"]["mode"], "api")
self.assertNotEqual(changed["auth"]["providers"]["session"]["jwtSecret"], "original")
raise RuntimeError("owned check failed")
output = directory / "result"
with patch("cms.execute", side_effect=fail_check):
with self.assertRaisesRegex(RuntimeError, "owned check failed"):
run_postgres_case(source, output, directory / "compiler", articles=2)
self.assertEqual(setting.read_bytes(), before)
self.assertFalse((source / "cms-app").exists())
self.assertTrue(staged_paths)
self.assertFalse(staged_paths[0].exists())
self.assertEqual(json.loads((output / "report.json").read_text())["status"], "failed")
def test_private_config_replacement_is_atomic_and_keeps_exclusive_creation(self):
with tempfile.TemporaryDirectory(prefix="dever-cms-setting-") as temporary:
directory = Path(temporary)
setting = directory / "setting.json"
write_private_json(setting, {"original": True})
with self.assertRaises(FileExistsError):
write_private_json(setting, {"overwrite": True})
with self.assertRaises(TypeError):
write_private_json(setting, {"invalid": object()}, replace=True)
self.assertEqual(json.loads(setting.read_text()), {"original": True})
self.assertEqual(list(directory.iterdir()), [setting])
write_private_json(setting, {"postgres": True}, replace=True)
self.assertEqual(json.loads(setting.read_text()), {"postgres": True})
self.assertEqual(setting.stat().st_mode & 0o777, 0o600)
self.assertEqual(list(directory.iterdir()), [setting])
def test_envelope_failure_cannot_count_as_publication(self):
self.assertEqual(decode_response(b'{"code":0,"message":"ok","data":{"id":1}}', 200),
{"id": 1})
for document, status in (({"code": 0, "message": "ok", "data": {}}, 500),
({"code": 409, "message": "conflict", "data": None}, 200),
({"id": 1}, 200)):
with self.assertRaises(RuntimeError):
decode_response(json.dumps(document).encode(), status)
def test_isolation_preserves_site_contract_without_mutating_input(self):
source = {
"runtime": {"mode": "all"}, "http": {"port": 8080},
"database": {"default": {"type": "sqlite", "path": "old.db"}},
"auth": {"providers": {"session": {"jwtSecret": "example", "verify": "user.account.verify"}}},
"sites": {"admin": {"path": "admin", "auth": "session", "origin": "https://example.invalid"}},
"log": {"level": "info"},
}
staged = isolated_settings(source, 12345)
self.assertEqual(source["runtime"]["mode"], "all")
self.assertEqual(source["database"]["default"]["path"], "old.db")
self.assertEqual(staged["runtime"]["mode"], "api")
self.assertEqual(staged["sites"]["admin"]["path"], "admin")
self.assertEqual(staged["sites"]["admin"]["origin"], "http://127.0.0.1:12345")
self.assertEqual(staged["sites"]["admin"]["hosts"], ["127.0.0.1"])
self.assertNotEqual(staged["auth"]["providers"]["session"]["jwtSecret"], "example")
self.assertEqual(staged["auth"]["providers"]["session"]["verify"], "user.account.verify")
self.assertEqual(source["log"]["level"], "info")
self.assertEqual(staged["log"]["level"], "error")
source["database"]["default"]["type"] = "postgres"
with self.assertRaises(RuntimeError):
isolated_settings(source, 12345)
def test_postgres_isolation_preserves_explicit_control_database_and_prefix(self):
source = {
"runtime": {"mode": "all"},
"database": {"default": {
"type": "postgres", "url": "postgres://fixture@127.0.0.1:12345/control",
"tenant_database_prefix": "dever_test_owned", "tls": "disabled",
}},
"auth": {"providers": {"session": {"jwtSecret": "example"}}},
"sites": {"admin": {"origin": "https://example.invalid"}},
"log": {"level": "info"},
}
staged = isolated_settings(source, 23456)
self.assertEqual(staged["database"], source["database"])
self.assertEqual(staged["http"], {"host": "127.0.0.1", "port": 23456})
self.assertNotIn("path", staged["database"]["default"])
source["database"]["default"].pop("tenant_database_prefix")
with self.assertRaises(RuntimeError):
isolated_settings(source, 23456)
def test_publish_workload_uses_tenant_and_title_parameters(self):
identity = {"credential_id": 11, "user_id": 3, "tenant_id": 2}
publication = {"slug": "bench-0", "title": "Tenant A 0 published",
"body": "Published body"}
client = SimpleNamespace(request=Mock(side_effect=[
{"account": {"id": 3}, "tenant_id": 2, "site": "admin"},
{"account": {"id": 3}, "tenant_id": 2, "site": "front"},
{"id": 1, "slug": "bench-0", "title": "Tenant A 0", "version": 1},
{"id": 7, "article_id": 1, "title": "Tenant A 0 published", "version": 2},
publication, {"total": 1, "items": [publication]}, None,
]))
process = SimpleNamespace(sample=Mock())
contract = publish_workload(client, process, identity, "fixture-password", 1,
tenant_key="tenant_a", title_prefix="Tenant A")
self.assertEqual(contract, {"articles": 1, "titles": ["Tenant A 0 published"],
"duplicate_publish_status": 409})
for call in client.request.call_args_list[:2]:
self.assertEqual(call.args[2]["tenant"], "tenant_a")
revision = client.request.call_args_list[3]
self.assertEqual(revision.args[:2], ("POST", "/content/revision/admin/manage/create"))
self.assertEqual(revision.args[2]["expected_version"], 1)
self.assertEqual(revision.args[2]["selected_article_id"], 1)
self.assertEqual(process.sample.call_count, 1)
def test_small_workload_reports_samples_without_implying_p99(self):
self.assertEqual(latency_summary({"publish": [4, 1, 3]}),
{"publish": {"samples": 3, "p50_ms": 3, "max_ms": 4}})
def test_command_timeout_retains_output_and_reaps_its_owned_process(self):
with tempfile.TemporaryDirectory(prefix="dever-cms-timeout-") as temporary:
directory = Path(temporary)
command = [sys.executable, "-u", "-c",
"import time,sys; print('starting'); print('diagnostic',file=sys.stderr); time.sleep(10)"]
with self.assertRaises(subprocess.TimeoutExpired):
execute(command, directory, "timeout", timeout=0.2)
self.assertEqual((directory / "timeout.stdout.log").read_text(), "starting\n")
self.assertEqual((directory / "timeout.stderr.log").read_text(), "diagnostic\n")
if __name__ == "__main__":
unittest.main()
+122
View File
@@ -0,0 +1,122 @@
"""用独立 Python 对端检查 Rust 长连接客户端的正文与生命周期判定。"""
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import socketserver
import subprocess
import tempfile
import threading
import time
import unittest
from live import parse_progress, phase_sequence, validate_result
from settings import configured_peer, stage_peer
@contextmanager
def serving(server):
with server:
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield server.server_address[1]
finally:
server.shutdown()
thread.join(timeout=2)
if thread.is_alive():
raise RuntimeError("owned test listener did not stop")
class Echo(socketserver.BaseRequestHandler):
def handle(self):
self.request.settimeout(2)
while payload := self.request.recv(4096):
response = b"!" * len(payload) if self.server.wrong_body else payload
self.request.sendall(response)
if self.server.wrong_body:
return
class InvalidEvent(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
body = b"event: live\nid: 1\ndata: wrong\n\n"
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
try:
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError):
# The client aborts sibling sessions after the first deliberate mismatch.
return
class SilentEvent(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.end_headers()
self.wfile.write(b"event: live\nid: 1\ndata: ready\n\n")
self.wfile.flush()
time.sleep(1)
class LiveClientTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory(prefix="dever-live-client-")
self.addCleanup(directory.cleanup)
self.executable = stage_peer(configured_peer("live_peer"), Path(directory.name), {"workers": 1})
def execute(self, protocol, port, path):
scheme = "http" if protocol == "sse" else protocol
return subprocess.run([self.executable, protocol, f"{scheme}://127.0.0.1:{port}{path}", "2", "2", "300", "1000"],
capture_output=True, text=True, timeout=8)
def test_tcp_reconnect_and_exact_echo(self):
listener = socketserver.ThreadingTCPServer(("127.0.0.1", 0), Echo)
listener.wrong_body = False
with serving(listener) as port:
result = self.execute("tcp", port, "/")
self.assertEqual(result.returncode, 0, result.stderr)
phases, counts = parse_progress(result.stdout, 2)
self.assertEqual(phases, phase_sequence(2))
validate_result(counts, "tcp", 2, 2)
def test_tcp_wrong_body_cannot_be_counted_as_held(self):
listener = socketserver.ThreadingTCPServer(("127.0.0.1", 0), Echo)
listener.wrong_body = True
with serving(listener) as port:
result = self.execute("tcp", port, "/")
self.assert_failure(result)
def test_sse_wrong_event_cannot_pass_connection_readiness(self):
with serving(ThreadingHTTPServer(("127.0.0.1", 0), InvalidEvent)) as port:
result = self.execute("sse", port, "/events")
self.assert_failure(result)
def test_sse_silent_hold_cannot_pass_heartbeat_validation(self):
with serving(ThreadingHTTPServer(("127.0.0.1", 0), SilentEvent)) as port:
result = self.execute("sse", port, "/events")
self.assertNotEqual(result.returncode, 0)
counts = json.loads(result.stdout.splitlines()[-1])
self.assertGreater(counts["errors"], 0)
self.assertIn("PHASE|0|connected\n", result.stdout)
self.assertNotIn("PHASE|0|closing\n", result.stdout)
def assert_failure(self, result):
self.assertNotEqual(result.returncode, 0)
counts = json.loads(result.stdout.splitlines()[-1])
self.assertGreater(counts["errors"], 0)
self.assertNotIn("PHASE|0|connected\n", result.stdout)
if __name__ == "__main__":
unittest.main()
+120
View File
@@ -0,0 +1,120 @@
"""用独立的 Python HTTP 对端验证负载计数;只占用自有随机回环端口。"""
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
from pathlib import Path
import subprocess
import tempfile
import threading
import unittest
from process import Process
from run import validate_load
from settings import configured_peer, stage_peer
ROOT = Path(__file__).resolve().parents[2]
TLS = ROOT / "test/dever-tests/fixtures/tls"
@contextmanager
def peer(body):
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *_):
pass
server = HTTPServer(("127.0.0.1", 0), Handler)
worker = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.01})
worker.start()
try:
yield server.server_port
finally:
server.shutdown()
server.server_close()
worker.join(timeout=2)
if worker.is_alive():
raise RuntimeError("owned HTTP peer did not stop")
@contextmanager
def benchmark_service(executable, mode):
settings = {"workers": 1, "connections": 4, "task_capacity": 128,
"timeout_ms": 1000, "lifetime_ms": 5000, "http2_streams": 16,
"http2_stream_window_bytes": 65535, "http2_connection_window_bytes": 262144,
"cert_path": str(TLS / "server.pem"), "key_path": str(TLS / "server-key.pem")}
with tempfile.TemporaryDirectory(prefix="dever-h2-bench-") as directory:
with Process([executable, mode], Path(directory) / "server", peer_settings=settings) as process:
yield process.ready()
process.check()
class NetworkMeasurementTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory(prefix="dever-network-client-")
self.addCleanup(directory.cleanup)
self.executable = stage_peer(configured_peer("network_peer"), Path(directory.name), {"workers": 1})
def load(self, port):
executable = self.executable
output = subprocess.run([str(executable), "load", "h1", f"http://localhost:{port}",
"/plain", "20", "0.2", "1", "1", "500", "true", "-"],
capture_output=True, text=True, timeout=5, check=True)
self.assertEqual(output.stderr, "")
result = json.loads(output.stdout)
validate_load(result)
self.assertEqual(result["scheduled"], 4)
self.assertGreaterEqual(result["elapsed_seconds"], 0.2)
return result
def test_success_requires_exact_body_and_full_window(self):
with peer(b"hello") as port:
result = self.load(port)
self.assertEqual(result["succeeded"], 4)
self.assertEqual(result["errors"], 0)
self.assertEqual(result["dropped"], 0)
self.assertEqual(result["latency_samples"], 4)
self.assertEqual(result["http_version"], "h1")
self.assertEqual(result["physical_connections"], 1)
self.assertEqual(result["streams_per_connection"], 1)
self.assertEqual(result["request_slots"], 1)
def test_wrong_body_is_an_error_with_no_success_latency(self):
with peer(b"wrong") as port:
result = self.load(port)
self.assertEqual(result["succeeded"], 0)
self.assertEqual(result["errors"], 4)
self.assertEqual(result["latency_samples"], 0)
self.assertIsNone(result["latency_ms"])
def test_http2_uses_one_physical_connection_for_sixteen_request_slots(self):
executable = self.executable
for mode, transport in (("runtime-http2", "http"), ("hyper-http2", "http"),
("runtime-https2", "https"), ("hyper-https2", "https")):
with self.subTest(mode=mode), benchmark_service(executable, mode) as port:
output = subprocess.run(
[str(executable), "load", "h2", f"{transport}://localhost:{port}",
"/bytes", "200", "0.1", "1", "16", "1000", "true", str(TLS / "root.pem")],
capture_output=True, text=True, timeout=5, check=True)
self.assertEqual(output.stderr, "")
result = json.loads(output.stdout)
validate_load(result)
self.assertEqual(result["errors"], 0)
self.assertGreater(result["succeeded"], 0)
self.assertEqual(result["physical_connections"], 1)
self.assertEqual(result["streams_per_connection"], 16)
self.assertEqual(result["request_slots"], 16)
self.assertEqual(result["successful_handshakes"], 1)
if __name__ == "__main__":
unittest.main()
+47
View File
@@ -0,0 +1,47 @@
"""只执行 peer 的配置检查入口,不创建网络或连接数据库。"""
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from settings import configured_peer, stage_peer
class PeerSettingsTests(unittest.TestCase):
def check(self, peer, document):
with tempfile.TemporaryDirectory(prefix="dever-peer-config-test-") as temporary:
root = Path(temporary)
executable = stage_peer(configured_peer(peer), root, {})
setting = root / "config/setting.json"
if document is None:
setting.unlink()
else:
setting.write_text(json.dumps(document))
# 不在 executable 目录启动,验证读取位置不受工作目录影响。
return subprocess.run([executable, "check-config"], cwd=root.parent,
capture_output=True, text=True, timeout=5)
def test_both_peers_use_the_external_integer_setting(self):
for peer in ("network_peer", "live_peer"):
for workers in (1, 4, 64):
with self.subTest(peer=peer, workers=workers):
output = self.check(peer, {"benchmark": {"workers": workers}})
self.assertEqual(output.returncode, 0, output.stderr)
self.assertEqual(json.loads(output.stdout), {"workers": workers})
def test_both_peers_reject_missing_malformed_and_out_of_range_configuration(self):
invalid = [None, {}, [], {"benchmark": []}, {"benchmark": {"unknown": 1}}]
invalid.extend({"benchmark": {"workers": value}} for value in (0, 65, -1, 1.5, True, "4", None))
for peer in ("network_peer", "live_peer"):
for document in invalid:
with self.subTest(peer=peer, document=document):
output = self.check(peer, document)
self.assertNotEqual(output.returncode, 0)
self.assertEqual(output.stdout, "")
self.assertTrue(output.stderr.strip())
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large. Load diff
+46
View File
@@ -0,0 +1,46 @@
//! 已收集参数直接交给解析器,保持子命令、校验与透传行为。
use super::parse;
#[test]
fn supplied_arguments_preserve_owned_command_and_forwarded_values() {
let arguments = ["run", "project", "--", "--tenant", "7", "demo.value", "{}"].map(String::from);
let command = arguments[0].as_ptr();
let parsed = parse(arguments.into_iter()).unwrap().unwrap();
assert_eq!(parsed.command.as_ptr(), command);
assert_eq!(parsed.source, std::path::Path::new("project"));
assert_eq!(parsed.arguments, ["--tenant", "7", "demo.value", "{}"]);
}
#[test]
fn supplied_arguments_keep_help_tenant_and_build_validation() {
for arguments in [vec![], vec!["--help"], vec!["--version"]] {
assert!(
parse(arguments.into_iter().map(String::from))
.unwrap()
.is_none()
);
}
let tenant = parse(
["tenant", "migrate", "project", "7"]
.map(String::from)
.into_iter(),
)
.unwrap()
.unwrap();
assert_eq!(tenant.command, "run");
assert_eq!(
tenant.arguments,
["--tenant", "7", "dever.tenant.migrate", "null"]
);
for arguments in [
vec!["build", "source"],
vec!["build", "source", "--output", "a", "--output", "b"],
vec!["check", "source", "--output", "a"],
vec!["run", "source", "--target", "linux-x86_64"],
vec!["run", "source", "--entry"],
vec!["unknown"],
] {
assert!(parse(arguments.into_iter().map(String::from)).is_err());
}
}
+194
View File
@@ -0,0 +1,194 @@
use dever::compiler::check;
use dever::syntax::source::Source;
#[path = "compiler/application/mod.rs"]
mod application;
#[path = "compiler/auth/mod.rs"]
mod auth;
#[path = "compiler/database/mod.rs"]
mod database;
#[path = "compiler/job/mod.rs"]
mod job;
#[path = "compiler/api.rs"]
mod api;
#[path = "compiler/bytes.rs"]
mod bytes;
#[path = "compiler/calendar.rs"]
mod calendar;
#[path = "compiler/choice.rs"]
mod choice;
#[path = "compiler/collection.rs"]
mod collection;
#[path = "compiler/crypto.rs"]
mod crypto;
#[path = "compiler/failure.rs"]
mod failure;
#[path = "compiler/format.rs"]
mod format;
#[path = "compiler/handler.rs"]
mod handler;
#[path = "compiler/http.rs"]
mod http;
#[path = "compiler/id.rs"]
mod id;
#[path = "compiler/io/mod.rs"]
mod io;
#[path = "compiler/json.rs"]
mod json;
#[path = "compiler/net.rs"]
mod net;
#[path = "compiler/numeric/mod.rs"]
mod numeric;
#[path = "compiler/package.rs"]
mod package;
#[path = "compiler/port/mod.rs"]
mod port;
#[path = "compiler/standard.rs"]
mod standard;
#[path = "compiler/storage.rs"]
mod storage;
#[path = "compiler/task/mod.rs"]
mod task;
#[path = "compiler/tenant.rs"]
mod tenant;
#[path = "compiler/upload.rs"]
mod upload;
#[path = "compiler/usage.rs"]
mod usage;
#[path = "compiler/uuid.rs"]
mod uuid;
fn checked(
text: &str,
) -> Result<dever::compiler::model::Program, dever::syntax::diagnostic::Diagnostic> {
let source = Source::new(0, "case.dever", text.into());
check::check(vec![dever::syntax::parse(&source)?], &["main".into()])
}
#[test]
fn rejects_overlap_gaps_and_recursion() {
assert_eq!(
checked("pick(value: Int < 0) (answer: Int) { answer = 1 }")
.unwrap_err()
.code,
"C009"
);
assert_eq!(checked("pick(value: Int <= 0) (answer: Int) { answer = 1 }\npick(value: Int >= 0) (answer: Int) { answer = 2 }").unwrap_err().code, "C009");
assert_eq!(
checked("cycle() (answer: Int) { answer = cycle() }")
.unwrap_err()
.code,
"C007"
);
assert!(checked("pick(value: Int < 0) (answer: Int) { answer = 1 }\npick(value: other) (answer: Int) { answer = 2 }").is_ok());
}
#[test]
fn rejects_uninitialized_outputs_and_overflow() {
assert!(checked("main() (answer: Int) { value = answer }").is_err());
assert!(checked("main() (answer: Int) {}").is_err());
assert!(checked("main() (answer: Int) { answer = 9223372036854775807 + 1 }").is_err());
assert!(checked("main() (answer: Int) { answer = -9223372036854775808 }").is_ok());
assert!(checked("main() (answer: Int) { answer = 9223372036854775806 + (1 + 1) }").is_err());
assert!(checked("main() (answer: Decimal) { answer = 1.0 / (2.0 - 2.0) }").is_err());
}
#[test]
fn preserves_record_and_list_types() {
assert!(checked("type Row { name: Text\n count: Int }\nmain() (answer: Row) { answer = Row { name = \"雪\"\n count = sum([1, 2, 3]) } }").is_ok());
assert!(
checked("type Row { count: Int }\nmain() (answer: Row) { answer = Row { wrong = 1 } }")
.is_err()
);
assert!(checked("main() (answer: List<Int>) { answer = [1, true] }").is_err());
}
#[test]
fn retains_original_naming_constraints() {
assert_eq!(checked("read_value() () {}").unwrap_err().code, "C010");
assert_eq!(checked("Wrong() () {}").unwrap_err().code, "C010");
assert_eq!(checked("type bad { count: Int }").unwrap_err().code, "C010");
assert_eq!(checked("main() () { Bad = 1 }").unwrap_err().code, "C010");
assert!(checked("type HttpClient { user_id: Int }\nreadValue(input_value: Int) (output_value: Int) { output_value = input_value }").is_ok());
}
#[test]
#[ignore = "explicit compiler cost probe: fixed 15 rounds, run without concurrent CPU work"]
fn compiler_reduction_costs() {
use dever::compiler::project::Project;
use std::{hint::black_box, path::Path, time::Instant};
let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test/rust/fixtures");
for (label, count) in [("medium", 32), ("large", 512)] {
let mut text = "copyText(value: Text) (answer: Text) { answer = value }\n".to_owned();
for index in 0..count {
let mut body =
format!("case{index}(value: Text) (answer: Text) {{\nanswer = copyText(value)\n");
for _ in 0..15 {
body.push_str("answer = copyText(answer)\n");
}
body.push_str("}\n");
text.push_str(&body);
}
let source = Source::new(0, "cost.dever", text);
let mut project = Project::load(&fixtures.join("core.dever")).unwrap();
project.sources[0] = Source::new(0, project.sources[0].path.clone(), source.text.clone());
let packages = vec!["main".to_owned()];
let expected_project_functions = project.check().unwrap().functions.len();
assert!(expected_project_functions > count);
println!(
"audit_compiler input={label} functions={} calls={} bytes={} project_bytes={} rounds=15 profile=test",
count + 1,
count * 16,
source.text.len(),
project.sources[0].text.len()
);
for round in 1..=15 {
let started = Instant::now();
let module = dever::syntax::parse(black_box(&source)).unwrap();
let elapsed = started.elapsed().as_nanos();
assert_eq!(module.declarations.len(), count + 1);
println!("audit_compiler case={label} phase=parse round={round} ns={elapsed}");
let started = Instant::now();
let program = check::check(vec![module], black_box(&packages)).unwrap();
let elapsed = started.elapsed().as_nanos();
assert_eq!(program.functions.len(), count + 1);
black_box(&program);
println!("audit_compiler case={label} phase=check round={round} ns={elapsed}");
drop(program);
let started = Instant::now();
let program = black_box(&project).check().unwrap();
let elapsed = started.elapsed().as_nanos();
assert_eq!(program.functions.len(), expected_project_functions);
black_box(&program);
println!("audit_compiler case={label} phase=project round={round} ns={elapsed}");
}
}
let project = Project::load(&fixtures.join("tenant")).unwrap();
let program = project.check().unwrap();
let roots = program.entries().collect::<Vec<_>>();
assert!(!roots.is_empty());
println!(
"audit_compiler input=tenant roots={} repeats=64 rounds=15",
roots.len()
);
for round in 1..=15 {
let started = Instant::now();
let mut required = 0usize;
for _ in 0..64 {
for root in &roots {
let access = black_box(&program).tenant_access(black_box(*root));
required += usize::from(access.required);
black_box(access);
}
}
let elapsed = started.elapsed().as_nanos();
println!(
"audit_compiler case=tenant phase=access round={round} ns={elapsed} required={required}"
);
}
}
+109
View File
@@ -0,0 +1,109 @@
use super::{checked, package::packages};
#[test]
fn api_snapshot_uses_declared_contracts_not_private_bodies_or_inferred_constants() {
let snapshot = |body: &str| {
checked(&format!("public value(input: Int) (answer: Int) {{ answer = helper(input) }}\nhelper(input: Int) (answer: Int) {{ {body} }}")).unwrap().api_snapshot()
};
let first = snapshot("answer = 1");
assert_eq!(
first,
"dever-api 5\nfunction main.value(Int) (answer: Int []) pure=false recover=None effects=[] errors=[] recoveries={}\n"
);
assert_eq!(first, snapshot("answer = input + 2"));
let constrained =
checked("public value(input: Int) (answer: Int >= 0) { answer = 1 }").unwrap();
assert!(
constrained
.api_snapshot()
.contains("answer: Int [0..9223372036854775807]")
);
let difference = constrained.check_api(&first).unwrap_err();
assert!(
difference.contains("- function main.value")
&& difference.contains("+ function main.value")
);
}
#[test]
fn api_snapshot_tracks_public_shape_construction_failure_and_transitive_recovery() {
let snapshot = |key: &str| {
checked(&format!(
"public type Session {{ private key: {key}\nlabel: Text }}"
))
.unwrap()
.api_snapshot()
};
let private = snapshot("Int");
assert_eq!(private, snapshot("Text"));
assert_eq!(
private,
"dever-api 5\ntype main.Session record construct=private {label: Text []}\n"
);
let exposed = checked("public type Session { key: Int\nlabel: Text }").unwrap();
assert!(
exposed
.check_api(&private)
.unwrap_err()
.contains("construct=public")
);
let failing = checked("public type Problem { error Bad(message: Text) }\npublic value() () { fail(Problem.Bad(\"bad\")) }").unwrap();
let baseline = failing.api_snapshot();
assert!(baseline.contains("errors=[main.Problem.Bad]"));
assert!(baseline.contains("type main.Problem error Bad(message: Text [])"));
let snapshot = |reason: &str| {
checked(&format!(
"public value() () {{ helper() }}\nhelper() () recover(\"{reason}\") {{}}"
))
.unwrap()
.api_snapshot()
};
assert_ne!(snapshot("use zero"), snapshot("use one"));
assert!(snapshot("use zero").contains("recoveries={\"use zero\"}"));
}
#[test]
fn api_snapshot_is_stable_across_app_topics_and_binds_command_inputs() {
let snapshot = |topic: &str, input: &str| {
packages(&[
(
topic,
&format!("value({input}: Int) (answer: Int) {{ answer = {input} }}"),
),
("shop.account.api", "cmd value = app.value"),
])
.unwrap()
.api_snapshot()
};
assert_eq!(
snapshot("shop.account.app", "input"),
snapshot("shop.account.app.read", "input")
);
assert_ne!(
snapshot("shop.account.app", "input"),
snapshot("shop.account.app", "renamed")
);
assert!(
snapshot("shop.account.app", "input")
.contains("cmd shop.account.value = shop.account.value(input: Int [])")
);
let program = checked("public apply(action: handler(input: Int) (answer: Int), input: Int) (answer: Int) { answer = action(input) }").unwrap();
assert!(
program
.api_snapshot()
.contains("function main.apply(handler(Int [])(answer: Int []), Int)")
);
}
#[test]
fn public_handler_snapshot_keeps_constraints_and_ignores_specializations() {
let source = "public apply(action: handler(input: Int >= 0) (answer: Int >= 0)) (answer: Int >= 0) { answer = action(1) }";
let original = checked(source).unwrap().api_snapshot();
assert!(
original.contains(
"handler(Int [0..9223372036854775807])(answer: Int [0..9223372036854775807])"
)
);
let called = checked(&format!("{source}\nnonnegative(input: Int) (answer: Int >= 0) {{ answer = 0 }}\nmain() (answer: Int) {{ answer = apply(nonnegative) }}")).unwrap();
assert_eq!(original, called.api_snapshot());
}
+442
View File
@@ -0,0 +1,442 @@
use super::package::packages;
#[test]
fn fixed_wire_budget_proof_retains_dynamic_checks_at_schema_limits() {
for (levels, fields, input_fixed, output_fixed) in [
(63, 1, true, true),
(64, 1, false, false),
(14, 2, true, true),
(15, 2, true, false),
(16, 2, false, false),
] {
let mut app = String::new();
let mut kind = "Int".to_owned();
for level in 0..levels {
let members = if fields == 1 {
format!("value: {kind}")
} else {
format!("first: {kind}\nsecond: {kind}")
};
app.push_str(&format!("type Layer{level} {{ {members} }}\n"));
kind = format!("Layer{level}");
}
app.push_str(&format!(
"run(value: {kind}) (answer: {kind}) {{ answer = value }}"
));
let program = packages(&[
("shop.account.app", &app),
(
"shop.account.api",
"cmd run = app.run\npublic post run = app.run",
),
])
.unwrap();
let emitted = dever::compiler::emit::application::generate(&program);
assert_eq!(
emitted.contains("wire::decode_fixed(input)"),
input_fixed,
"{levels}/{fields}"
);
assert_eq!(
emitted.contains("input.decode(dever_runtime::wire::decode_fixed)"),
input_fixed,
"HTTP {levels}/{fields}"
);
assert_eq!(
emitted.matches("wire::response_fixed(&result)").count(),
usize::from(output_fixed),
"{levels}/{fields}"
);
assert_eq!(
emitted.contains("wire::response_fixed_bytes(&result)"),
output_fixed,
"HTTP {levels}/{fields}"
);
}
for (kind, decoder, encoder) in [
("List<Int>", "decode_shaped", "response"),
("Json", "decode_json", "response_with"),
("List<Json>", "decode", "response_with"),
("List<Text>?", "decode_shaped", "response"),
] {
let app = format!("run(value: {kind}) (answer: {kind}) {{ answer = value }}");
let program = packages(&[
("shop.account.app", &app),
(
"shop.account.api",
"cmd run = app.run\npublic post run = app.run",
),
])
.unwrap();
let emitted = dever::compiler::emit::application::generate(&program);
if decoder == "decode_json" {
assert!(emitted.contains("let args: Args0 = decode_args0(input)?;"));
assert!(emitted.contains("input.decode(decode_httpargs0)"));
assert_eq!(emitted.matches("wire::decode_json(input,").count(), 2);
assert_eq!(
emitted.contains("wire::decode(input)"),
kind == "List<Json>"
);
} else {
assert!(!emitted.contains("wire::decode_json("));
assert!(
emitted.contains(&format!("wire::{decoder}(input)")),
"{kind}"
);
assert!(
emitted.contains(&format!("input.decode(dever_runtime::wire::{decoder})")),
"{kind}"
);
}
assert!(emitted.contains(&format!("wire::{encoder}(")), "{kind}");
}
}
#[test]
fn json_input_budget_includes_fixed_records_and_optional_json() {
let program = packages(&[
(
"shop.account.app",
"type Row { payload: Json?\nnote: Text? }\nrun(row: Row?, other: Json, label: Text) (answer: Row?, extra: Json, name: Text) { answer = row\nextra = other\nname = label }",
),
(
"shop.account.api",
"cmd run = app.run\npublic post run = app.run",
),
])
.unwrap();
let generated = dever::compiler::emit::application::generate(&program);
assert_eq!(generated.matches("wire::decode_json(input,").count(), 2);
assert!(generated.contains("budget.open()?; budget.value(3)?;"));
assert!(generated.contains("budget.open()?; budget.value(2)?;"));
assert!(generated.contains("if let Some(item) = (&(args).field0).as_ref()"));
assert!(generated.contains("budget.json(item)?;"));
assert!(generated.contains("budget.json(&(args).field1)?;"));
assert!(!generated.contains("Json::from_validated_raw"));
assert!(!generated.contains("Json::deserialize_validated"));
}
#[test]
fn json_input_with_any_typed_list_keeps_whole_input_validation() {
for kind in ["List<Int>", "List<Json>", "List<Json?>?"] {
for nested in [false, true] {
let (definition, kind) = if nested {
(format!("type Row {{ values: {kind} }}\n"), "Row?")
} else {
(String::new(), kind)
};
let app = format!(
"{definition}run(payload: Json, other: {kind}) (answer: Json, extra: {kind}) {{ answer = payload\nextra = other }}"
);
let program = packages(&[
("shop.account.app", &app),
(
"shop.account.api",
"cmd run = app.run\npublic post run = app.run",
),
])
.unwrap();
let generated = dever::compiler::emit::application::generate(&program);
assert!(generated.contains("let args: Args0 = dever_runtime::wire::decode(input)?;"));
assert!(generated.contains("input.decode(dever_runtime::wire::decode)"));
assert!(!generated.contains("wire::decode_json("));
assert!(!generated.contains("budget."));
}
}
}
#[test]
fn variable_wire_depth_is_proved_before_skipping_validation() {
for (levels, decoder) in [(63, "decode_shaped"), (64, "decode")] {
let kind = format!("{}Int{}", "List<".repeat(levels), ">".repeat(levels));
let app = format!("run(value: {kind}) (answer: Int) {{ answer = length(value) }}");
let program = packages(&[
("shop.account.app", &app),
(
"shop.account.api",
"cmd run = app.run\npublic post run = app.run",
),
])
.unwrap();
let emitted = dever::compiler::emit::application::generate(&program);
assert!(
emitted.contains(&format!("wire::{decoder}(input)")),
"{levels}"
);
assert!(
emitted.contains(&format!("input.decode(dever_runtime::wire::{decoder})")),
"{levels}"
);
}
}
#[test]
fn http_entries_bind_typed_apps_and_keep_protected_entries_closed() {
assert!(packages(&[("main", "get(value: Int) (answer: Int) { answer = value }")]).is_ok());
let app = "echo(value: Int) (answer: Int) { answer = value }";
let program = packages(&[
("shop.account.app", app),
(
"shop.account.api.admin.session",
"public get status = app.echo",
),
])
.unwrap();
assert_eq!(program.http[0].path, "/shop/account/admin/session/status");
assert_eq!(program.http[0].topic, "admin/session");
assert_eq!(program.http[0].method, "GET");
for (declaration, code) in [
("get status = app.echo", "C014"),
("authenticated get status = app.echo", "C014"),
(
"public get status = app.echo\npublic get status = app.echo",
"C002",
),
("public get status = other.echo", "C006"),
] {
assert_eq!(
packages(&[("shop.account.app", app), ("shop.account.api", declaration)])
.unwrap_err()
.code,
code
);
}
assert_eq!(
packages(&[
(
"shop.account.app",
"echo(value: List<Int>) (answer: List<Int>) { answer = value }"
),
("shop.account.api", "public get status = app.echo")
])
.unwrap_err()
.code,
"C005"
);
}
#[test]
fn command_wire_rejects_nested_constrained_records() {
for kind in ["Port", "List<Port?>", "Envelope"] {
let app = format!(
"type Port {{ value: Int >= 1 }}\ntype Envelope {{ ports: List<Port?> }}\nrun(value: {kind}) (answer: {kind}) {{ answer = value }}"
);
assert_eq!(
packages(&[
("shop.account.app", &app),
("shop.account.api", "cmd run = app.run")
])
.unwrap_err()
.code,
"C011"
);
}
}
#[test]
fn application_topics_share_app_contracts_and_domain_dependencies() {
let program = packages(&[
("shop.account.app.read", "type User { name: Text }\nread(name: Text) (answer: User) { answer = User { name = domain.label(name) } }"),
("shop.account.app.write", "echo(user: User) (answer: User) { answer = user }"),
("shop.account.domain", "label(name: Text) (answer: Text) { answer = name + \"!\" }"),
("shop.account.api", "cmd read = app.read\ncmd echo = app.echo"),
]).unwrap();
assert_eq!(program.commands.len(), 2);
assert_eq!(program.commands[0].name, "shop.account.read");
assert!(
program
.functions
.iter()
.any(|function| function.signature.name == "label")
);
}
#[test]
fn private_app_fields_share_topic_ownership_and_reject_other_domains() {
let declaration = "type Session { private key: Text\nlabel: Text }";
assert!(packages(&[
("shop.account.app.types", declaration),
("shop.account.app.write", "make(key: Text) (answer: Session) { answer = Session { key = key, label = \"name\" } }"),
("shop.account.app.read", "read(value: Session) (answer: Text) { value.key = value.key + \"!\"\nanswer = value.key }"),
]).is_ok());
for operation in [
"answer = value.key",
"value.key = \"changed\"\nanswer = value.label",
"value = shop.account.Session { key = \"hidden\", label = \"name\" }\nanswer = value.label",
] {
let caller = format!("read(value: shop.account.Session) (answer: Text) {{ {operation} }}");
assert_eq!(
packages(&[
("shop.account.app", declaration),
("shop.other.app", &caller)
])
.unwrap_err()
.code,
"C006"
);
}
}
#[test]
fn command_wire_rejects_nested_private_records_in_both_directions() {
for kind in ["Session", "List<Session?>", "Envelope"] {
for function in [
format!("run(value: {kind}) () {{}}"),
format!("run(value: {kind}) (answer: {kind}) {{ answer = value }}"),
] {
let app = format!(
"type Session {{ private key: Text }}\ntype Envelope {{ sessions: List<Session?> }}\n{function}"
);
let error = packages(&[
("shop.account.app", &app),
("shop.account.api", "cmd run = app.run"),
])
.unwrap_err();
assert_eq!(error.code, "C006", "{error:?}");
assert!(error.message.contains("wire boundary"), "{error:?}");
}
}
let app = "type Session { private key: Text }\nrun() (answer: Session) { answer = Session { key = \"hidden\" } }";
assert_eq!(
packages(&[
("shop.account.app", app),
("shop.account.api", "cmd run = app.run")
])
.unwrap_err()
.code,
"C006"
);
}
#[test]
fn application_role_access_cannot_be_bypassed_with_qualified_names() {
for (source, target) in [
("shop.first.domain", "shop.second.run"),
("shop.first.domain", "app.run"),
("shop.first.app", "shop.second.domain.hidden"),
] {
let caller = format!("entry() (answer: Int) {{ answer = {target}() }}");
let result = packages(&[
(source, &caller),
("shop.first.app.extra", "run() (answer: Int) { answer = 1 }"),
("shop.second.app", "run() (answer: Int) { answer = 2 }"),
(
"shop.second.domain",
"hidden() (answer: Int) { answer = 3 }",
),
]);
assert_eq!(result.unwrap_err().code, "C006", "{source}: {target}");
}
assert_eq!(
packages(&[
("shop.account.app", "type Visible { secret: domain.Hidden }"),
("shop.account.domain", "type Hidden { value: Int }"),
])
.unwrap_err()
.code,
"C006"
);
assert_eq!(
packages(&[("shop.account.app", "public run() () {}")])
.unwrap_err()
.code,
"C006"
);
assert_eq!(
packages(&[("shop.account.api", "run() () {}")])
.unwrap_err()
.code,
"C006"
);
assert_eq!(
packages(&[("main", "cmd run = app.run")]).unwrap_err().code,
"C006"
);
}
#[test]
fn command_secret_inputs_use_the_existing_input_codec_without_output_access() {
for kind in ["Secret", "Secret?", "List<Secret>"] {
let app = format!("run(value: {kind}) (answer: Bool) {{ answer = true }}");
assert!(
packages(&[
("shop.account.app", &app),
("shop.account.api", "cmd run = app.run")
])
.is_ok()
);
let leaking = format!("run(value: {kind}) (answer: {kind}) {{ answer = value }}");
assert!(
packages(&[
("shop.account.app", &leaking),
("shop.account.api", "cmd run = app.run")
])
.is_err()
);
}
}
#[test]
fn commands_reject_ambiguous_targets_and_invalid_wire_contracts() {
for app in [
"run() () {}\nrun(value: Int) () {}",
"run(action: handler() ()) () { action() }",
"type State { Ready(value: Int) }\nrun(value: State) (answer: State) { answer = value }",
"type State { Ready\n error Failed }\nrun(value: State) (answer: State) { answer = value }",
] {
assert_eq!(
packages(&[
("shop.account.app", app),
("shop.account.api", "cmd run = app.run")
])
.unwrap_err()
.code,
"C005"
);
}
assert_eq!(
packages(&[
("shop.account.app", "run() () {}"),
("shop.account.api", "cmd run = app.run\ncmd run = app.run"),
])
.unwrap_err()
.code,
"C002"
);
assert_eq!(
packages(&[
("shop.account.app", "run() () {}"),
("shop.account.api", "cmd run = shop.account.run"),
])
.unwrap_err()
.code,
"C006"
);
}
#[test]
fn application_entry_reachability_and_domain_cycles_are_checked() {
assert_eq!(
packages(&[
("shop.account.app", "run() () {}\nunused() () {}"),
("shop.account.api", "cmd run = app.run"),
])
.unwrap_err()
.code,
"C016"
);
assert_eq!(packages(&[
("shop.first.app", "run() (answer: Int) { answer = second.read() }\nread() (answer: Int) { answer = 1 }"),
("shop.second.app", "read() (answer: Int) { answer = first.read() }"),
("shop.first.api", "cmd run = app.run"),
]).unwrap_err().code, "C007");
assert_eq!(
packages(&[
("shop.account.app.read", "run() () {}"),
("shop.account.app.write", "run() () {}"),
])
.unwrap_err()
.code,
"C013"
);
}
+103
View File
@@ -0,0 +1,103 @@
use super::{APP, ERROR, packages};
const CLOCK: &str = "public type ClockResult { Read(value: Int)\nerror Failed(message: Text) }\npublic unixMillis() (value: Int) { value = read(dever.system.timeUnixMillis()) }\nread(result: ClockResult.Read(number)) (value: Int) { value = number }\nread(result: ClockResult.Failed(message)) (value: Int) { fail(ClockResult.Failed(message)) }\npublic type SleepResult { Done\nerror Failed(message: Text) }\npublic sleep(millis: Int) () { slept(dever.system.timeSleep(millis)) }\nslept(result: SleepResult.Done) () {}\nslept(result: SleepResult.Failed(message)) () { fail(SleepResult.Failed(message)) }";
const TASK: &str = "public sleep(millis: Int) () { slept(dever.system.taskSleep(millis)) }\nslept(result: dever.time.SleepResult.Done) () {}\nslept(result: dever.time.SleepResult.Failed(message)) () { fail(dever.time.SleepResult.Failed(message)) }";
#[test]
fn provider_expiration_reads_clock_but_cannot_wait() {
for (operation, error) in [
("current = dever.time.unixMillis()", None),
("dever.time.sleep(1)\ncurrent = 0", Some("C013")),
("dever.task.sleep(1)\ncurrent = 0", Some("C014")),
] {
let app = APP
.replace(
"account = model.first",
&format!("{operation}\naccount = model.first"),
)
.replace(
"session == claims.session",
"session == claims.session and expires_at > current",
);
let result = packages(&[
("dever.database", ERROR),
("dever.api", "public type Error { error Unauthorized }"),
(
"dever.auth",
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
),
("dever.time", CLOCK),
("dever.task", TASK),
(
"demo.account.model",
"global type Account { subject: Text\nsession: Text\nexpires_at: Int }",
),
("demo.account.app", &app),
("demo.account.api", "public get health = app.health"),
]);
match error {
Some(code) => assert_eq!(result.unwrap_err().code, code, "{operation}"),
None => {
let program = result.unwrap();
let effects = program.functions[program.verifiers[0].function]
.signature
.effects;
assert!(effects.clock);
assert!(!effects.timer);
}
}
}
}
#[test]
fn shipped_session_expiration_contract_checks_without_dropping_the_deadline() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../skills/assets/examples/http-rest");
let project = dever::compiler::project::Project::load(&root).unwrap();
let program = project.check().unwrap();
assert!(
program
.verifiers
.iter()
.any(|verifier| { program.functions[verifier.function].signature.effects.clock })
);
}
#[test]
fn provider_checks_every_model_loaded_by_a_relation() {
let model =
"global type Account { subject: Text\nsession: Text\nprofile_id: demo.profile.model.id }";
let app = APP.replace(
"session == claims.session })",
"session == claims.session, with = [profile] })",
);
for global in [true, false] {
let profile = format!(
"{}type Profile {{ name: Text }}",
if global { "global " } else { "" }
);
let result = packages(&[
("dever.database", ERROR),
("dever.api", "public type Error { error Unauthorized }"),
(
"dever.auth",
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
),
("demo.account.model", model),
("demo.profile.model", &profile),
("demo.account.app", &app),
("demo.account.api", "public get health = app.health"),
]);
if global {
result.unwrap();
} else {
let error = result.unwrap_err();
assert_eq!(error.code, "C014");
assert!(
error
.message
.contains("Provider verify cannot read a tenant Model")
);
}
}
}
+67
View File
@@ -0,0 +1,67 @@
use super::{database::ERROR, package::packages};
use dever::{compiler::model::Program, syntax::diagnostic::Diagnostic};
#[path = "../authorization/mod.rs"]
mod capability;
mod effects;
mod request;
mod rest;
const APP: &str = "type Identity { id: Text\nuser_id: model.id?\ntenant_id: model.id? }\nverify(claims: dever.auth.Claims) (identity: Identity) { account = model.first({ where = subject == claims.subject and session == claims.session })\nidentity = identity(account) }\nidentity(account: model.Account) (identity: Identity) { identity = Identity { id = account.subject, user_id = account.id, tenant_id = null } }\nidentity(account: null) (identity: Identity) { fail(dever.api.Error.Unauthorized) }\nhealth() (value: Bool) { value = true }";
fn checked(model: &str, app: &str) -> Result<Program, Diagnostic> {
packages(&[
("dever.database", ERROR),
("dever.api", "public type Error { error Unauthorized }"),
(
"dever.auth",
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
),
("demo.account.model", model),
("demo.account.app", app),
("demo.account.api", "public get health = app.health"),
])
}
#[test]
fn provider_identity_is_a_compiled_app_entry_with_global_reads() {
let program = checked("global type Account { subject: Text\nsession: Text }", APP).unwrap();
let verifier = &program.verifiers[0];
assert_eq!(verifier.name, "demo.account.verify");
assert!(program.models[0].global);
assert!(program.entries().any(|entry| entry == verifier.function));
assert!(program.api_snapshot().contains("global=true"));
assert_eq!(
checked("type Account { subject: Text\nsession: Text }", APP)
.unwrap_err()
.code,
"C014"
);
assert_eq!(
packages(&[("main", "global type Value { number: Int }")])
.unwrap_err()
.code,
"C006"
);
}
#[test]
fn provider_cannot_write_or_replace_nominal_identity_with_permissions() {
let model = "global type Account { subject: Text\nsession: Text }";
let writing = APP.replace(
"model.first({ where = subject == claims.subject and session == claims.session })",
"model.create({ subject = claims.subject, session = claims.session })",
);
assert_eq!(checked(model, &writing).unwrap_err().code, "C014");
let invalid = APP
.replace("user_id: model.id?", "user_id: Int?")
.replace("user_id = account.id", "user_id = 1");
assert_eq!(checked(model, &invalid).unwrap_err().code, "C005");
let invalid = APP
.replace(
"tenant_id: model.id?",
"tenant_id: model.id?\npermissions: List<Text>",
)
.replace("tenant_id = null", "tenant_id = null, permissions = []");
assert_eq!(checked(model, &invalid).unwrap_err().code, "C005");
}
+61
View File
@@ -0,0 +1,61 @@
use super::packages;
const ERROR: &str = "public type Error { error Invalid\nerror Unauthorized\nerror Forbidden }";
const AUTH: &str = "public id() (value: Text) { value = dever.system.authId() }";
#[test]
fn identity_cannot_escape_to_public_commands_domains_or_tasks() {
for api in ["public get me = app.me", "cmd me = app.me"] {
let error = packages(&[
("dever.api", ERROR),
("dever.auth", AUTH),
(
"demo.user.app",
"me() (value: Text) { value = dever.auth.id() }",
),
("demo.user.api", api),
])
.unwrap_err();
assert_eq!(error.code, "C014", "{api}: {error:?}");
}
for (package, source, code) in [
(
"demo.user.domain",
"me() (value: Text) { value = dever.auth.id() }",
"C014",
),
(
"main",
"main() (value: Text) { task = run(dever.auth.id())\nvalue = wait(task) }",
"C013",
),
(
"main",
"main() (value: Text) { value = dever.system.authId() }",
"C006",
),
] {
assert_eq!(
packages(&[
("dever.api", ERROR),
("dever.auth", AUTH),
(package, source)
])
.unwrap_err()
.code,
code
);
}
}
#[test]
fn provider_cannot_reenter_request_identity() {
let error = packages(&[
("dever.api", ERROR),
("dever.auth", "public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }\npublic id() (value: Text) { value = dever.system.authId() }"),
("demo.user.model", "global type User { name: Text }"),
("demo.user.app", "type Identity { id: Text\nuser_id: model.id?\ntenant_id: model.id? }\nverify(claims: dever.auth.Claims) (value: Identity) { value = Identity { id = dever.auth.id(), user_id = null, tenant_id = null } }\nhealth() (value: Bool) { value = true }"),
("demo.user.api", "public get health = app.health"),
]).unwrap_err();
assert_eq!(error.code, "C014");
}
+202
View File
@@ -0,0 +1,202 @@
use super::*;
fn rest(model: &str, domain: &str) -> Result<Program, Diagnostic> {
packages(&[
("dever.database", ERROR),
("dever.api", "public type Error { error Unauthorized }"),
(
"dever.auth",
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
),
(
"text",
"public trim(input: Text) (value: Text) pure { value = input }",
),
(
"demo.account.model",
"global type Account { subject: Text\nsession: Text }",
),
("demo.account.model.profile", model),
("demo.account.domain", domain),
("demo.account.app", APP),
(
"demo.account.api.admin.session",
"public get health = app.health",
),
("demo.account.api.admin.profile", "rest model.profile"),
])
}
#[test]
fn rest_wire_reuses_schema_proof_without_skipping_json_validation() {
for (kind, decoder) in [
("Text", "decode_fixed"),
("Int?", "decode_fixed"),
("Json", "decode"),
("Json?", "decode"),
] {
let program = rest(&format!("type Profile {{ value: {kind} }}"), "").unwrap();
let generated = dever::compiler::emit::application::generate(&program);
assert!(!generated.contains("wire::decode_json("));
for operation in ["Create", "Replace"] {
assert!(
generated.contains(&format!(
"let args: Rest{operation}0 = input.decode(dever_runtime::wire::{decoder})"
)),
"{kind}/{operation}"
);
}
if kind == "Json" {
assert!(
generated.contains("field0: Box<dever_runtime::wire::serde_json::value::RawValue>")
);
assert!(generated.contains("Json::from_validated_raw(decoded.field0)"));
} else if kind == "Json?" {
assert!(
generated.contains(
"field0: Option<Box<dever_runtime::wire::serde_json::value::RawValue>>"
)
);
assert!(generated.contains(
"decoded.field0.map(dever_runtime::wire::json::Json::from_validated_raw)"
));
} else {
assert!(!generated.contains("Json::deserialize_validated"));
assert!(!generated.contains("Json::deserialize_optional_validated"));
}
}
}
#[test]
fn rest_json_output_projects_only_public_fields() {
let program = rest(
"type Profile { value: Json\nprivate secret: Text create = \"hidden\"\nextra: Json? }",
"",
)
.unwrap();
let generated = dever::compiler::emit::application::generate(&program);
let view = generated
.split_once("impl RestView0<'_> {")
.unwrap()
.1
.split_once("struct RestItems0")
.unwrap()
.0;
assert!(view.contains("field2"));
assert!(!view.contains("field3"));
assert!(view.contains("field4"));
assert!(!view.contains("secret"));
assert!(!view.contains("serialize_field"));
}
#[test]
fn rest_json_defaults_keep_their_typed_decoder() {
for (kind, value, decoder) in [
("Json", r#""{}""#, "deserialize_validated"),
("Json?", "null", "deserialize_optional_validated"),
] {
let program = rest(
&format!("type Profile {{ value: {kind} default {value} }}"),
"",
)
.unwrap();
let generated = dever::compiler::emit::application::generate(&program);
assert!(generated.contains(&format!("Json::{decoder}")));
assert!(generated.contains("#[serde(default=\"rest_create_0_default0\")]"));
}
}
#[test]
fn rest_bindings_use_checked_functions_and_preserve_owner_identity() {
let program = rest("type Profile { private owner user_id: model.id = dever.auth.userId()\nname: Text unique create = domain.normalize(input) replace = text.trim(input) search = input\nprivate source: Text create = \"source\" replace = \"replaced\" }","normalize(input: Text) (value: Text) pure { value = input }").unwrap();
let entry = &program.rest[0];
assert_eq!(entry.owner.as_ref().unwrap().position, 2);
assert!(!entry.read.contains(&2));
assert_eq!(
entry.create.iter().filter(|binding| binding.input).count(),
1
);
assert_eq!(entry.search.as_ref().unwrap().position, 3);
assert!(
entry
.functions()
.all(|function| program.entries().any(|entry| entry == function))
);
}
#[test]
fn rest_rejects_unsafe_field_sources_and_ambiguous_search() {
for model in [
"type Profile { private name: Text create = input }",
"type Profile { owner id_value: Int? = dever.auth.userId()\nname: Text }",
"type Profile { owner first_id: Int = dever.auth.userId()\nowner second_id: Int = dever.auth.tenantId()\nname: Text }",
"type Profile { name: Text search = input }",
"type Profile { name: Text index search = \"fixed\" }",
"type Profile { name: Text index search = input\nother_name: Text index search = input }",
"type Profile { private required: Text\nname: Text }",
"type Profile { name: Text create = domain.normalize(input) }",
] {
let error = rest(
model,
if model.contains("domain.normalize") {
"normalize(input: Text) (value: Text) { value = input }"
} else {
""
},
)
.unwrap_err();
assert_eq!(error.code, "C014", "{model}: {error:?}");
}
}
#[test]
fn rest_search_reserves_pagination_names_without_banning_storage_fields() {
for name in ["page", "size"] {
let model = format!("type Profile {{ {name}: Int index search = input }}");
let error = rest(&model, "").unwrap_err();
assert_eq!(error.code, "C014");
assert_eq!(
error.message,
"REST search field cannot use reserved pagination names"
);
let model = format!("type Profile {{ {name}: Int index }}");
assert!(
rest(&model, "").is_ok(),
"{name} remains a valid storage field"
);
}
}
#[test]
fn rest_preserves_required_private_fields_with_only_a_create_source() {
let program = rest(
"type Profile { private created_by: Text create = \"creator\"\nname: Text }",
"",
)
.unwrap();
let entry = &program.rest[0];
let source = entry
.create
.iter()
.find(|binding| binding.position == 2)
.unwrap();
assert!(source.function.is_some());
assert!(!source.input);
assert!(!entry.read.contains(&2));
assert!(entry.replace.iter().all(|binding| binding.position != 2));
let error = rest(
"type Profile { private created_by: Text replace = \"replacement\"\nname: Text }",
"",
)
.unwrap_err();
assert_eq!(error.code, "C014");
assert_eq!(
error.message,
"REST field is private and required without a source"
);
}
#[test]
fn rest_field_metadata_does_not_escape_into_handler_contracts() {
assert!(packages(&[("main","apply(callback: handler(value: Int create = input) (answer: Int)) (answer: Int) { answer = callback(1) }")]).is_err());
}
+171
View File
@@ -0,0 +1,171 @@
use super::{APP, ERROR};
use dever::{
compiler::{check, model::Program},
syntax::{diagnostic::Diagnostic, source::Source},
};
const API_ERROR: &str = "public type Error { error Invalid\nerror Unauthorized\nerror Forbidden\nerror NotFound\nerror Conflict\nerror TooManyRequests }";
fn checked(app: &str, api: &str) -> Result<Program, Diagnostic> {
let app = format!("{APP}\n{app}");
let api = format!("{api}\npublic get health = app.health");
let inputs = [
("dever.database", "database.dever", ERROR),
("dever.api", "api.dever", API_ERROR),
(
"dever.auth",
"values.dever",
include_str!("../../../../lib/dever/auth/values.dever"),
),
(
"dever.auth",
"capability.dever",
include_str!("../../../../lib/dever/auth/capability.dever"),
),
(
"demo.account.model",
"model.dever",
"global type Account { subject: Text\nsession: Text }",
),
("demo.account.app", "app.dever", app.as_str()),
("demo.account.api", "api.dever", api.as_str()),
];
let sources = inputs
.iter()
.enumerate()
.map(|(index, (_, path, text))| Source::new(index, *path, (*text).into()))
.collect::<Vec<_>>();
let modules = sources
.iter()
.map(dever::syntax::parse)
.collect::<Result<Vec<_>, _>>()?;
check::check(
modules,
&inputs
.iter()
.map(|(name, _, _)| (*name).into())
.collect::<Vec<_>>(),
)
}
#[test]
fn capability_operations_keep_original_types_and_write_effects() {
let program = checked("", "").unwrap();
for function in program.functions.iter().filter(|function| {
matches!(
function.signature.name.as_str(),
"register_capability" | "remove_capability"
)
}) {
let effects = function.signature.effects;
assert!(effects.identity && effects.authorization_write && effects.database_write);
}
let error = checked(
"register(id: Text) (key: Text) { key = dever.auth.registerCapability(id) }",
"post register = app.register",
)
.unwrap_err();
assert_eq!(error.code, "C005");
let error = checked(
"register(id: Uuid) (value: Bool) { value = dever.auth.registerCapability(id) }",
"post register = app.register",
)
.unwrap_err();
assert_eq!(error.code, "C005");
}
#[test]
fn capability_writes_require_protected_http_and_keep_transaction_rules() {
for api in [
"public post register = app.register",
"authenticated post register = app.register",
"get register = app.register",
"cmd register = app.register",
"long post register = app.register",
] {
let error = checked(
"register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
api,
)
.unwrap_err();
assert_eq!(error.code, "C014", "{api}: {error:?}");
}
let program = checked(
"transaction register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
"long post register = app.register",
)
.unwrap();
assert_tenant_requirement(&program, true);
}
#[test]
fn protected_role_query_is_authenticated_read_only_and_not_a_tenant_selector() {
let app =
"protected(user_id: Int) (value: Bool) { value = dever.auth.hasProtectedRoles(user_id) }";
for api in [
"get protected = app.protected",
"authenticated get protected = app.protected",
] {
let program = checked(app, api).unwrap();
assert_tenant_requirement(&program, true);
}
for api in [
"public get protected = app.protected",
"cmd protected = app.protected",
] {
assert_eq!(checked(app, api).unwrap_err().code, "C014");
}
assert_eq!(checked(
"protected(user_id: Int) (value: Bool) { value = dever.auth.hasProtectedRoles(user_id, 42) }",
"get protected = app.protected",
).unwrap_err().code, "C005");
}
fn assert_tenant_requirement(program: &Program, required: bool) {
let entry = program
.http
.iter()
.find(|entry| entry.access != dever::compiler::model::auth::Access::Public)
.unwrap();
assert_eq!(program.tenant_access(entry.function).required, required);
}
#[test]
fn protected_write_entries_cannot_replace_the_required_tenant() {
for (app, api) in [
(
"register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
"post register = app.register",
),
(
"remove(id: Uuid) () { dever.auth.removeCapability(id) }",
"delete remove = app.remove",
),
] {
assert_tenant_requirement(&checked(app, api).unwrap(), true);
}
}
#[test]
fn tenant_requirement_follows_reachable_calls_and_static_handlers() {
for (app, required) in [
(
"register(id: Uuid) (key: Text) { key = helper(id) }\nhelper(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
true,
),
(
"register(id: Uuid) (key: Text) { key = apply(dever.auth.registerCapability, id) }\napply(action: handler(id: Uuid) (key: Text), id: Uuid) (key: Text) { key = action(id) }",
true,
),
] {
assert_tenant_requirement(
&checked(app, "post register = app.register").unwrap(),
required,
);
}
let error = checked(
"register(id: Uuid) (key: Text) { key = \"unused\" }\nunused(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }",
"post register = app.register",
).unwrap_err();
assert_eq!(error.code, "C016");
}
+258
View File
@@ -0,0 +1,258 @@
use dever::compiler::{check, model::Program};
use dever::syntax::{diagnostic::Diagnostic, source::Source};
fn linked(text: &str) -> Result<Program, Diagnostic> {
let sources = sources(text);
let modules = sources
.iter()
.map(dever::syntax::parse)
.collect::<Result<Vec<_>, _>>()?;
check::check(
modules,
&["main".into(), "dever.bytes".into(), "text".into()],
)
}
fn sources(text: &str) -> [Source; 3] {
[
Source::new(0, "main.dever", text.into()),
Source::new(
1,
"lib/dever/bytes.dever",
include_str!("../../../lib/dever/bytes.dever").into(),
),
Source::new(
2,
"lib/text.dever",
include_str!("../../../lib/text.dever").into(),
),
]
}
fn generated_entry(text: &str) -> String {
let mut program = linked(text).unwrap();
let entry = program
.functions
.iter()
.position(|function| function.signature.name == "main")
.unwrap();
// 改名不改变内建操作身份,防止优化依赖 concat/from_text 的函数名称。
for (index, function) in program.functions.iter_mut().enumerate() {
function.signature.name = format!("renamed_{index}");
}
let generated = dever::compiler::emit::generate(&program, &sources(text), &[entry].into());
let start = generated.find(&format!("pub fn function{entry}(")).unwrap();
generated[start..]
.split("\npub ")
.next()
.unwrap()
.to_owned()
}
#[test]
fn borrowed_bytes_keep_their_shared_owner() {
let generated = generated_entry("main(bytes: Bytes) (answer: Bytes) { answer = bytes }");
assert!(
generated.contains("arg0: &dever_runtime::Bytes"),
"{generated}"
);
assert!(generated.contains(".to_owned()"), "{generated}");
assert!(!generated.contains("copy_from_slice"), "{generated}");
}
#[test]
fn text_byte_views_are_limited_to_non_escaping_iteration() {
let generated = generated_entry(
"add(item: Int, total: Int) (next: Int) { next = total + item }\nmain(text: Text) (answer: Int) { bytes = dever.bytes.fromText(text)\nanswer = reduce(add, bytes, 0) }",
);
assert!(generated.contains(".as_bytes()"), "{generated}");
assert!(!generated.contains("copy_from_slice"), "{generated}");
let generated = generated_entry(
"add(item: Int, total: Int) (next: Int) { next = total + item }\nmain(text: Text) (answer: Int) { bytes = dever.bytes.fromText(text)\nanswer = reduce(add, bytes, 0) + length(bytes) }",
);
assert!(!generated.contains("copy_from_slice"), "{generated}");
for body in [
"main(text: Text) (answer: Bytes) { bytes = dever.bytes.fromText(text)\nanswer = bytes }",
"identity(bytes: Bytes) (answer: Bytes) { answer = bytes }\nmain(text: Text) (answer: Bytes) { bytes = dever.bytes.fromText(text)\nanswer = identity(bytes) }",
] {
let generated = generated_entry(body);
assert!(generated.contains("copy_from_slice"), "{generated}");
}
}
#[test]
fn empty_separator_parts_are_fused_only_when_fully_concatenated() {
let join = "joinPart(item: Bytes, state: Bytes) (answer: Bytes) { answer = dever.bytes.concat(state, item) }\n";
let generated = generated_entry(
&(join.to_owned()
+ "main(text: Text, seed: Bytes) (answer: Bytes) { parts = each(dever.bytes.fromText, text.split(text, \"\"))\nanswer = reduce(joinPart, parts, seed) }"),
);
assert!(
generated.contains("state.extend_from_slice(sequence)"),
"{generated}"
);
assert!(!generated.contains("copy_from_slice"), "{generated}");
assert!(!generated.contains("collect::<Vec<_>>()"), "{generated}");
for body in [
"main(text: Text) (answer: List<Bytes>) { parts = each(dever.bytes.fromText, text.split(text, \"\"))\nanswer = parts }",
"main(text: Text, seed: Bytes) (answer: Bytes, count: Int) { parts = each(dever.bytes.fromText, text.split(text, \"\"))\ncount = length(parts)\nanswer = reduce(joinPart, parts, seed) }",
"main(text: Text, seed: Bytes) (answer: Bytes) { parts = each(dever.bytes.fromText, text.split(text, \",\"))\nanswer = reduce(joinPart, parts, seed) }",
"main(text: Text, separator: Text, seed: Bytes) (answer: Bytes) { parts = each(dever.bytes.fromText, text.split(text, separator))\nanswer = reduce(joinPart, parts, seed) }",
] {
let generated = generated_entry(&(join.to_owned() + body));
assert!(generated.contains("copy_from_slice"), "{generated}");
}
}
#[test]
fn literal_text_concat_allocates_only_the_final_buffer() {
for literal in [r#""""#, r#""prefix""#, r#""雪\u{0}🙂""#] {
let generated = generated_entry(&format!(
"main(suffix: Bytes) (answer: Bytes) {{ answer = dever.bytes.concat(dever.bytes.fromText({literal}), suffix) }}"
));
assert!(
generated.contains("value::bytes::concat(prefix, suffix)"),
"{generated}"
);
assert!(!generated.contains(".to_vec()"), "{generated}");
assert!(!generated.contains("extend_from_slice"), "{generated}");
}
}
#[test]
fn dynamic_text_and_owned_byte_accumulators_keep_their_existing_paths() {
for (source, operation) in [
(
"main(text: Text, suffix: Bytes) (answer: Bytes) { answer = dever.bytes.concat(dever.bytes.fromText(text), suffix) }",
"Bytes::copy_from_slice",
),
(
"main(suffix: Bytes) (answer: Bytes) { text = \"owned\"\n answer = dever.bytes.concat(dever.bytes.fromText(text), suffix) }",
".into_bytes()",
),
(
"main(suffix: Bytes) (answer: Bytes) { bytes = dever.bytes.fromText(\"owned\")\n answer = dever.bytes.concat(bytes, suffix) }",
"bytes.extend_from_slice(suffix)",
),
] {
let generated = generated_entry(source);
assert!(generated.contains(operation), "{generated}");
assert!(
!generated.contains("value::bytes::concat(prefix, suffix)"),
"{generated}"
);
}
}
#[test]
fn bytes_keep_their_declared_collection_contract() {
for source in [
"identity(item: Int) (answer: Int) { answer = item }\nmain(bytes: Bytes) (answer: List<Int>) { answer = each(identity, bytes) }",
"add(item: Int, state: Int) (next: Int) { next = item + state }\nmain(bytes: Bytes) (answer: Int) { answer = reduce(add, bytes, 0) }",
"main(values: List<Text>) (answer: List<Bytes>) { answer = each(dever.bytes.fromText, values) }",
"main(bytes: Bytes?) (answer: Bytes?) { answer = bytes }",
] {
let result = linked(source);
assert!(result.is_ok(), "{source}: {result:?}");
}
for source in [
"main(bytes: Bytes) (answer: Int) { answer = sum(bytes) }",
"identity(item: Int) (answer: Int) { answer = item }\nmain(bytes: Bytes) (answer: Int) { answer = sum(identity, bytes) }",
"yes(item: Int) (answer: Bool) { answer = true }\nmain(bytes: Bytes) (answer: List<Int>) { answer = filter(yes, bytes) }",
"yes(item: Int) (answer: Bool) { answer = true }\nmain(bytes: Bytes) (answer: Int?) { answer = find(yes, bytes) }",
"identity(item: Text) (answer: Text) { answer = item }\nmain(bytes: Bytes) (answer: List<Text>) { answer = each(identity, bytes) }",
"main(bytes: Bytes) (answer: List<Int>) { answer = bytes }",
"main(values: List<Int>) (answer: Bytes) { answer = values }",
"main(values: Map<Bytes, Int>) () {}",
] {
assert_eq!(linked(source).unwrap_err().code, "C005", "{source}");
}
}
#[test]
fn buffer_reductions_append_without_copying_the_accumulator() {
for (kind, body, method) in [
("Text", "state + item", "push_str"),
(
"Bytes",
"dever.bytes.concat(state, item)",
"extend_from_slice",
),
] {
let source = format!(
"appendPiece(item: {kind}, state: {kind}) (answer: {kind}) {{ answer = {body} }}\nmain(values: List<{kind}>, initial: {kind}) (answer: {kind}) {{ answer = reduce(appendPiece, values, initial) }}"
);
let generated = generated_entry(&source);
let iteration = generated.split("for item in sequence").nth(1).unwrap();
assert!(
iteration.contains(&format!("state.{method}(item)")),
"{generated}"
);
for copy in [".clone()", ".to_owned()", ".to_vec()"] {
assert!(!iteration.contains(copy), "{generated}");
}
}
}
#[test]
fn literal_buffer_seeds_do_not_roundtrip_through_immutable_bytes() {
for literal in [r#""""#, r#""seed""#, r#""雪\u{0}🙂""#] {
let generated = generated_entry(&format!(
"appendPiece(item: Bytes, state: Bytes) (answer: Bytes) {{ answer = dever.bytes.concat(state, item) }}\nmain(values: List<Bytes>) (answer: Bytes) {{ answer = reduce(appendPiece, values, dever.bytes.fromText({literal})) }}"
));
assert!(generated.contains("let mut state: Vec<u8>"), "{generated}");
assert!(!generated.contains("Bytes::from_static"), "{generated}");
assert!(!generated.contains("Vec::<u8>::from"), "{generated}");
}
}
#[test]
fn library_helpers_and_intrinsics_are_private() {
for source in [
"main() (answer: Bytes) { answer = dever.system.bytesFromText(\"x\") }",
"main() (answer: Bytes) { answer = dever.bytes.ready(dever.bytes.BufferResult.Ready(dever.bytes.fromText(\"x\"))) }",
"main() (answer: Text) { answer = dever.bytes.decoded(dever.bytes.DecodeResult.Decoded(\"x\")) }",
] {
assert_eq!(linked(source).unwrap_err().code, "C006", "{source}");
}
}
#[test]
fn byte_errors_keep_nominal_identity_and_obligations() {
assert_eq!(
linked("main(bytes: Bytes) () { captured = result(dever.bytes.toText(bytes)) }")
.unwrap_err()
.code,
"C012"
);
assert_eq!(linked("type Result { Decoded(text: Text)\n error Failed(message: Text) }\nmain(bytes: Bytes) (answer: Result) { answer = result(dever.bytes.toText(bytes)) }").unwrap_err().code, "C005");
assert!(linked("main(bytes: Bytes) (answer: dever.bytes.DecodeResult) { answer = result(dever.bytes.toText(bytes)) }").is_ok());
assert!(linked("type Result { Ready(text: Text)\n error Failed(message: Text) }\nidentity() (text: Text) { text = \"ok\" }\nmain() (answer: Result) { captured = result(identity())\n answer = captured }").is_ok());
}
#[test]
fn package_names_resolve_without_exposing_private_types() {
let library = Source::new(1, "sample.dever", "type Hidden { value: Int }\npublic type Visible { value: Int }\npublic read(value: Int) (answer: Int) { answer = value }".into());
for (source, accepted) in [
(
"main() (answer: sample.Visible) { answer = sample.Visible { value = sample.read(1) } }",
true,
),
("main(value: sample.Hidden) () {}", false),
("main() () { value = sample.Hidden { value = 1 } }", false),
(
"main() (answer: Visible) { answer = sample.Visible { value = 1 } }",
false,
),
] {
let source = Source::new(0, "main.dever", source.into());
let result = check::check(
vec![
dever::syntax::parse(&source).unwrap(),
dever::syntax::parse(&library).unwrap(),
],
&["main".into(), "sample".into()],
);
assert_eq!(result.is_ok(), accepted, "{result:?}");
}
}
+95
View File
@@ -0,0 +1,95 @@
use dever::{
compiler::{build, check, model::Program},
syntax::{diagnostic::Diagnostic, source::Source},
};
fn linked(text: &str) -> Result<Program, Diagnostic> {
let inputs = [
("main", text),
("dever.time", include_str!("../../../lib/dever/time.dever")),
(
"dever.time",
include_str!("../../../lib/dever/time/calendar.dever"),
),
(
"dever.time",
include_str!("../../../lib/dever/time/operations.dever"),
),
(
"dever.time",
include_str!("../../../lib/dever/time/clock.dever"),
),
];
let modules = inputs
.iter()
.enumerate()
.map(|(index, (_, text))| {
dever::syntax::parse(&Source::new(
index,
if index == 0 {
"main.dever".into()
} else {
format!("library{index}.dever")
},
(*text).into(),
))
})
.collect::<Result<Vec<_>, _>>()?;
check::check(
modules,
&inputs
.iter()
.map(|(package, _)| (*package).into())
.collect::<Vec<_>>(),
)
}
#[test]
fn calendar_types_are_distinct_and_only_clock_reads_have_effects() {
let program = linked("main() (answer: Text) pure { instant = dever.time.parseDatetime(\"1970-01-01T00:00:00Z\")\n answer = dever.time.formatDatetime(dever.time.add(instant, dever.time.duration(1))) }").unwrap();
let main = program
.functions
.iter()
.position(|function| function.signature.name == "main")
.unwrap();
assert_eq!(
program.functions[main].signature.effects,
Default::default()
);
assert_eq!(
build::runtime_features(&program, &program.reachable([main])),
["calendar"]
);
let clocks = linked("main() (value: Int) { value = dever.time.unixMillis() }").unwrap();
let main = clocks
.functions
.iter()
.position(|function| function.signature.name == "main")
.unwrap();
assert!(clocks.functions[main].signature.effects.clock);
assert!(!clocks.functions[main].signature.effects.timer);
assert!(!clocks.functions[main].signature.effects.suspend);
assert!(build::runtime_features(&clocks, &clocks.reachable([main])).is_empty());
for (source, code) in [
("main() (value: DateTime) { value = 0 }", "C005"),
(
"main(value: DateTime) (answer: DateTime) { answer = dever.time.add(value, 1) }",
"C005",
),
(
"main(value: DateTime) (answer: Bool) { answer = value < value }",
"C005",
),
(
"main() (value: DateTime) pure { value = dever.time.now() }",
"C013",
),
("main() () { value = dever.system.timeDuration(1) }", "C006"),
(
"main() () { lost = result(dever.time.parseDate(\"bad\")) }",
"C012",
),
] {
assert_eq!(linked(source).unwrap_err().code, code, "{source}");
}
}
+116
View File
@@ -0,0 +1,116 @@
use super::checked;
#[test]
fn unconstrained_parameters_do_not_expand_clause_products() {
let inputs = (0..40)
.map(|position| format!("flag{position}: Bool"))
.collect::<Vec<_>>()
.join(", ");
let body = (0..40)
.map(|position| format!("flag{position}"))
.collect::<Vec<_>>()
.join(" or ");
let ordinary = format!("choose({inputs}) (answer: Bool) {{ answer = {body} }}\n");
assert!(checked(&ordinary).is_ok());
let clause = |pattern: &str| {
format!("choose(test: {pattern}, {inputs}) (answer: Bool) {{ answer = {body} }}\n")
};
assert!(checked(&(clause("true") + &clause("false"))).is_ok());
assert_eq!(checked(&clause("true")).unwrap_err().code, "C009");
assert_eq!(
checked(&(clause("Bool") + &clause("false")))
.unwrap_err()
.code,
"C009"
);
assert!(checked("type State { One\n Two }\nvalue(input: State, optional: Bool?) (answer: Bool) { answer = optional == null }").is_ok());
}
#[test]
fn choice_coverage_and_payload_contracts() {
let prefix = "type State { Idle\n Working(value: Int) }\n";
let valid = "read(value: State.Idle) (answer: Int) { answer = 0 }\nread(value: State.Working(number)) (answer: Int) { answer = number }";
assert!(checked(&(prefix.to_owned() + valid)).is_ok());
for (body, code) in [
(
"read(value: State.Idle) (answer: Int) { answer = 0 }",
"C009",
),
(
"read(value: State) (answer: Int) { answer = 0 }\nread(value: State.Idle) (answer: Int) { answer = 1 }",
"C009",
),
(
"read(value: State.Working) (answer: Int) { answer = 0 }",
"C005",
),
(
"read(value: State.Working(first, second)) (answer: Int) { answer = first }",
"C005",
),
(
"read(value: State.Working(value)) (answer: Int) { answer = 0 }",
"C009",
),
(
"main() (answer: State) { answer = State.Working() }",
"C005",
),
(
"main() (answer: State) { answer = State.Working(true) }",
"C005",
),
(
"read(value: State) (answer: Int) { answer = value.value }",
"C005",
),
] {
let error = checked(&(prefix.to_owned() + body)).unwrap_err();
assert_eq!(error.code, code, "{body}: {error:?}");
}
let wildcard = "read(value: State.Working(_)) (answer: Int) { answer = 1 }\nread(value: other) (answer: Int) { answer = 0 }";
assert!(checked(&(prefix.to_owned() + wildcard)).is_ok());
assert_eq!(
checked("type Cycle { Next(value: Cycle) }")
.unwrap_err()
.code,
"C007"
);
assert_eq!(
checked("type State { Item(value: Row) }\ntype Row { state: State }")
.unwrap_err()
.code,
"C007"
);
}
#[test]
fn nullable_domains_and_complements_are_order_independent() {
for clauses in [
"read(value: Text) (answer: Int) { answer = length(value) }\nread(value: null) (answer: Int) { answer = -1 }",
"read(value: null) (answer: Int) { answer = -1 }\nread(value: Text) (answer: Int) { answer = length(value) }",
"read(value: other) (answer: Int) { answer = length(value) }\nread(value: null) (answer: Int) { answer = -1 }\nread(value: \"x\") (answer: Int) { answer = 1 }",
] {
assert!(checked(clauses).is_ok(), "{clauses}");
}
for clauses in [
"read(value: Text?) (answer: Int) { answer = 0 }\nread(value: null) (answer: Int) { answer = 1 }",
"read(value: \"x\") (answer: Int) { answer = 0 }\nread(value: null) (answer: Int) { answer = 1 }",
] {
assert_eq!(checked(clauses).unwrap_err().code, "C009");
}
assert!(checked("equal(value: Int?) (answer: Bool) { answer = value == null or null == value or value == 1 or 1 == value }").is_ok());
assert!(checked("number() (answer: Decimal?) { answer = 1 }").is_ok());
assert!(checked("number() (answer: Float?) { answer = 1.5 + 2.5 }").is_ok());
assert!(checked("values() (answer: List<Int>?) { answer = [] }").is_ok());
}
#[test]
fn payload_bindings_do_not_shadow_inputs_or_outputs() {
for binding in ["value", "answer"] {
let text = format!(
"type State {{ Item(number: Int) }}\nread(value: State.Item({binding})) (answer: Int) {{ answer = 1 }}"
);
assert_eq!(checked(&text).unwrap_err().code, "C002");
}
}
+179
View File
@@ -0,0 +1,179 @@
use super::checked;
const HANDLERS: &str = "double(value: Int) (answer: Int) { answer = value * 2 }\npositive(value: Int) (answer: Bool) { answer = value > 0 }\nadd(value: Int, state: Int) (next: Int) { next = value + state }\nstop(value: Int, state: Int) (next: Int, stop: Bool) { next = value + state\n stop = next >= 3 }\n";
const ERRORS: &str = "type Result { Ready(value: Int)\n error Failed(message: Text) }\nidentity(value: Result) (answer: Result) { answer = value }\nhandled(value: Result) (answer: Bool) recover(\"optional probe failure is explicitly discarded\") { answer = true }\ncontext(value: Int, state: Result) () recover(\"optional context probe is discarded\") {}\nkeep(value: Int, state: Result) (next: Result) { next = state }\nconsume(value: Result, state: Int) (next: Int) recover(\"count failed probes\") { next = state + 1 }\npartial(value: Result, state: Int) (next: Int, stop: Bool) recover(\"count failed probes\") { next = state + 1\n stop = true }\n";
#[test]
fn checks_collection_handler_types_and_cycles() {
for body in [
"main() (answer: List<Int>) { answer = each(double, []) }",
"main() (answer: List<Int>) { answer = filter(positive, [1, -1]) }",
"main() (answer: Int?) { answer = find(positive, []) }",
"main() (answer: Int) { answer = sum(double, []) }",
"main() (answer: Float) { answer = sum([]) }",
"main() (answer: Decimal?) { answer = sum([]) }",
"main() (answer: Int) { answer = reduce(add, [], 10) }",
"main() (answer: Int) { answer = reduceUntil(stop, [1, 2, 3], 0) }",
] {
let result = checked(&(HANDLERS.to_owned() + body));
assert!(result.is_ok(), "{body}: {result:?}");
}
for body in [
"main() (answer: List<Int>) { answer = each(positive, [1]) }",
"main() (answer: List<Int>) { answer = filter(double, [1]) }",
"main() (answer: Int?) { answer = find(positive, [\"one\"]) }",
"main() (answer: Int) { answer = sum(positive, [1]) }",
"main() (answer: Int) { answer = reduce(add, [1], true) }",
"main() (answer: Int) { answer = reduceUntil(add, [1], 0) }",
"main() (answer: Int) { answer = reduce(add, [1], 0, 1) }",
"main() (answer: List<Int>) { double = 2\n answer = each(double, [1]) }",
"main() () { handler = double }",
] {
assert!(checked(&(HANDLERS.to_owned() + body)).is_err(), "{body}");
}
assert_eq!(
checked("cycle(value: Int) (answer: Int) { answer = sum(cycle, [value]) }")
.unwrap_err()
.code,
"C007"
);
assert_eq!(checked("first(value: Int) (answer: Int) { answer = sum(second, [value]) }\nsecond(value: Int) (answer: Int) { answer = first(value) }").unwrap_err().code, "C007");
}
#[test]
fn collections_preserve_error_obligations() {
for body in [
"main(values: List<Result>) (answer: Int) { copies = each(identity, values)\n answer = length(copies) }",
"main(values: List<Result>) (answer: Result?) { answer = find(handled, values) }",
"main(values: List<Result>) (answer: Int) { answer = reduceUntil(partial, values, 0) }",
"main() (answer: Int) { answer = reduceUntil(partial, [Result.Failed(\"one\"), Result.Failed(\"two\")], 0) }",
"main(state: Result) () { each(context, [], state) }",
"main(values: List<Int>, state: Result) () { each(context, values, state) }",
"main(state: Result) (answer: Int) { kept = reduce(keep, [], state)\n answer = 1 }",
"main() (answer: List<Result>) { lost = Result.Failed(\"lost\")\n answer = each(identity, []) }",
] {
let result = checked(&(ERRORS.to_owned() + body));
assert_eq!(result.unwrap_err().code, "C012", "{body}");
}
for body in [
"main(values: List<Result>) (answer: List<Result>) { answer = each(identity, values) }",
"main(values: List<Result>) (answer: Int) { answer = reduce(consume, values, 0) }",
"main(state: Result) () { each(context, [1], state) }",
"main() (answer: Result?) { answer = find(handled, [Result.Failed(\"one\")]) }",
"main() (answer: Int) { answer = reduceUntil(partial, [Result.Failed(\"one\")], 0) }",
"main(state: Result) (answer: Result) { answer = reduce(keep, [], state) }",
] {
let result = checked(&(ERRORS.to_owned() + body));
assert!(result.is_ok(), "{body}: {result:?}");
}
}
#[test]
fn collection_error_summaries_survive_calls_and_repeated_state() {
let prefix = ERRORS.to_owned()
+ "copy(values: List<Result>) (answer: List<Result>) { answer = each(identity, values) }\nreplace(value: Result, state: Result) (next: Result) recover(\"last probe replaces previous probe\") { next = value }\n";
for body in [
"main(values: List<Result>) (answer: Int) { copies = copy(values)\n answer = length(copies) }",
"main(values: List<Result>, state: Result) (answer: Int) { last = reduce(replace, values, state)\n answer = 1 }",
"main(state: Result) () { copies = copy([])\n count = length(copies) }",
] {
assert_eq!(
checked(&(prefix.clone() + body)).unwrap_err().code,
"C012",
"{body}"
);
}
let source = prefix
+ "main(values: List<Result>, state: Result) (answer: Result) { answer = reduce(replace, values, state) }";
assert!(checked(&source).is_ok());
}
#[test]
fn optional_traversal_cannot_transfer_unvisited_obligations() {
let prefix = ERRORS.to_owned()
+ "takeContext(value: Int, context: Result) (answer: Result) { answer = context }\nfirst(value: Result, state: Result) (next: Result, stop: Bool) recover(\"replace previous state\") { next = value\n stop = true }\n";
for body in [
"main(values: List<Int>, state: Result) (answer: List<Result>) { answer = each(takeContext, values, state) }",
"main(values: List<Result>) (answer: Result) { answer = reduceUntil(first, values, Result.Ready(0)) }",
] {
assert_eq!(
checked(&(prefix.clone() + body)).unwrap_err().code,
"C012",
"{body}"
);
}
assert!(checked(&(prefix + "main(state: Result) (answer: List<Result>) { answer = each(takeContext, [1], state) }")).is_ok());
}
#[test]
fn replacing_record_fields_still_checks_discarded_failures() {
let source = ERRORS.to_owned()
+ "type Row { result: Result\n count: Int }\nreplace(value: Row) (answer: Row) { answer = value\n answer.result = Result.Ready(1)\n answer.count = 0 }";
assert_eq!(checked(&source).unwrap_err().code, "C012");
}
#[test]
fn known_length_map_builds_reserve_once_and_update_in_place() {
for (kind, item, value, sequence) in [
("Bytes", "Int", "value", "values"),
("List<Int>", "Int", "value", "values"),
(
"Map<Int, Int>",
"MapEntry<Int, Int>",
"value.value",
"entries(values)",
),
] {
let text = format!(
"insert(value: {item}, state: Map<Int, Int>) (next: Map<Int, Int>) {{ next = put(state, length(state), {value}) }}\nmain(values: {kind}) (answer: Map<Int, Int>) {{ answer = reduce(insert, {sequence}, {{}}) }}"
);
let source = dever::syntax::source::Source::new(0, "case.dever", text);
let program = checked(&source.text).unwrap();
let generated = dever::compiler::emit::generate(&program, &[source], &[1].into());
let main = generated.split("pub fn function1(").nth(1).unwrap();
let main = main.split("pub fn ").next().unwrap();
assert!(
main.contains("Map::with_capacity(sequence.len())"),
"{main}"
);
assert!(main.contains("state.insert("), "{main}");
assert!(!main.contains("state.clone()"), "{main}");
assert!(!main.contains(".to_owned()"), "{main}");
}
}
#[test]
fn record_reductions_keep_a_single_growing_buffer() {
let text = "type State { values: List<Int>\n count: Int }\nstep(value: Int, state: State) (next: State) { values = append(state.values, value)\n next = State { values = values\n count = state.count + 1 } }\nmain(input: Bytes) (answer: State) { answer = reduce(step, input, State { values = []\n count = 0 }) }";
let source = dever::syntax::source::Source::new(0, "case.dever", text.into());
let program = checked(text).unwrap();
let generated = dever::compiler::emit::generate(&program, &[source], &[1].into());
let main = generated.split("pub fn function1(").nth(1).unwrap();
let main = main.split("\npub ").next().unwrap();
assert!(!main.contains(".clone()"), "{main}");
let iteration = main.split("for byte in sequence").nth(1).unwrap();
let iteration = iteration.split("Type0 {").next().unwrap();
assert!(!iteration.contains(".reserve("), "{main}");
assert!(iteration.contains(".push(element)"), "{main}");
}
#[test]
fn map_selection_copies_only_matching_entries() {
for (operation, result) in [
("filter", "List<MapEntry<Text, Text>>"),
("find", "MapEntry<Text, Text>?"),
] {
let text = format!(
"matches(entry: MapEntry<Text, Text>, target: Text) (answer: Bool) {{ answer = entry.value == target }}\nmain(values: Map<Text, Text>, target: Text) (answer: {result}) {{ answer = {operation}(matches, entries(values), target) }}"
);
let source = dever::syntax::source::Source::new(0, "case.dever", text.clone());
let program = checked(&text).unwrap();
let generated = dever::compiler::emit::generate(&program, &[source], &[1].into());
let main = generated.split("pub fn function1(").nth(1).unwrap();
let main = main.split("\npub ").next().unwrap();
let iteration = main.split("for (key, value) in sequence").nth(1).unwrap();
let (inspection, selected) = iteration.split_once("if ").unwrap();
assert!(!inspection.contains(".clone()"), "{main}");
assert!(selected.contains("(key.clone(), value.clone())"), "{main}");
}
}
+95
View File
@@ -0,0 +1,95 @@
use dever::{
compiler::{check, model::Program},
syntax::{diagnostic::Diagnostic, source::Source},
};
fn linked(text: &str) -> Result<Program, Diagnostic> {
let inputs = [
("main", text),
(
"dever.crypto",
include_str!("../../../lib/dever/crypto.dever"),
),
("dever.time", include_str!("../../../lib/dever/time.dever")),
("dever.task", include_str!("../../../lib/dever/task.dever")),
];
let modules = inputs
.iter()
.enumerate()
.map(|(index, (_, text))| {
dever::syntax::parse(&Source::new(
index,
if index == 0 {
"main.dever".into()
} else {
format!("library{index}.dever")
},
(*text).into(),
))
})
.collect::<Result<Vec<_>, _>>()?;
check::check(
modules,
&inputs
.iter()
.map(|(package, _)| (*package).into())
.collect::<Vec<_>>(),
)
}
#[test]
fn secrets_cannot_be_compared_converted_or_hidden_in_error_payloads() {
for source in [
"main(value: Secret) (answer: Bool) { answer = value == value }",
"main(values: List<Secret>) (answer: Bool) { answer = values == values }",
"main(value: Secret) (answer: Int) { answer = length(value) }",
"main(value: Secret) (answer: Text) { answer = value }",
"type Invalid { error Failed(value: Secret) }\nmain() () {}",
"type Inner { key: Secret }\ntype Invalid { error Failed(values: List<Inner>) }\nmain() () {}",
"main(values: Map<Secret, Int>) () {}",
] {
assert_eq!(linked(source).unwrap_err().code, "C005", "{source}");
}
let error = linked("main() () { value = secret(\"password\") }").unwrap_err();
assert_eq!(error.code, "C006");
assert!(error.message.contains("compiler-owned Test source"));
assert!(linked("main(value: Secret) (answer: Secret?) { answer = value }").is_ok());
}
#[test]
fn crypto_randomness_and_password_work_keep_effect_boundaries() {
for source in [
"main() () pure { value = dever.crypto.token(16) }",
"main(password: Secret) (hash: Text) { dever.task.sleep(0)\n hash = dever.crypto.passwordHash(password) }",
"main(password: Secret, hash: Text) (valid: Bool) { dever.task.sleep(0)\n valid = dever.crypto.passwordVerify(password, hash) }",
] {
assert_eq!(linked(source).unwrap_err().code, "C013", "{source}");
}
assert!(linked("main(password: Secret) (hash: Text) { hash = blocking(dever.crypto.passwordHash(password)) }").is_ok());
assert!(linked("main(key: Secret, bytes: Bytes) (hash: Bytes) pure { hash = dever.crypto.hmacSha256(key, bytes) }").is_ok());
assert_eq!(
linked("main() () { value = dever.system.cryptoToken(16) }")
.unwrap_err()
.code,
"C006"
);
assert_eq!(
linked("main() () { lost = result(dever.crypto.token(0)) }")
.unwrap_err()
.code,
"C012"
);
}
#[test]
fn app_outputs_reject_nested_secret_types() {
for source in [
"read() (value: Secret?) { value = null }",
"type Envelope { values: List<Secret> }\nread() (value: Envelope) { value = Envelope { values = [] } }",
] {
let module = dever::syntax::parse(&Source::new(0, "app.dever", source.into())).unwrap();
let error = check::check(vec![module], &["shop.account.app".into()]).unwrap_err();
assert_eq!(error.code, "C006");
assert!(error.message.contains("Secret"));
}
}
+112
View File
@@ -0,0 +1,112 @@
use super::{ERROR, packages};
#[test]
fn text_policy_is_a_single_model_modifier_and_defaults_use_the_normalized_length() {
use dever::compiler::model::persistence::{TextMode, Value};
let program = packages(&[("demo.account.model", "type Account { name: Text(1, 2) lower default \" Ä \"\ncode: Text(2) upper default \" ß \"\nbody: Text raw default \" a \"\nnote: Text? }")]).unwrap();
let columns = &program.models[0].columns;
for (column, mode, expected) in [
(2, TextMode::Lower, "ä"),
(3, TextMode::Upper, "SS"),
(4, TextMode::Raw, " a "),
] {
assert_eq!(columns[column].text, mode);
assert!(matches!(&columns[column].default, Some(Value::Text(value)) if value == expected));
}
assert_eq!(columns[5].text, TextMode::Trim);
for field in [
"name: Text raw lower",
"name: Text lower upper",
"name: Text raw raw",
"name: Int lower",
"name: List<Text> raw",
"name: Text(1, 2) default \" \"",
"name: Text(1) upper default \"ß\"",
"name: Text trim",
] {
assert!(
packages(&[("demo.account.model", &format!("type Account {{ {field} }}"))]).is_err(),
"{field}"
);
}
assert!(packages(&[("demo.account.domain", "type Input { name: Text raw }")]).is_err());
let seeded = packages(&[(
"demo.account.model",
"type Account { name: Text(1, 2) lower unique }\nseed { { name = \" Ä \" } }",
)])
.unwrap();
assert!(matches!(&seeded.models[0].seeds[0].values[0].1, Value::Text(value) if value == "ä"));
}
#[test]
fn model_choice_options_preserve_labels_and_order() {
let program = packages(&[
("demo.account.model", "type Status { Active = \"启用\"\nDisabled = \"禁用\" }\ntype Account { status: Status }"),
("demo.account.app", "options() (values: Map<model.Status, Text>) { values = model.Status.options }"),
]).unwrap();
assert!(!program.functions[0].signature.effects.database);
assert!(packages(&[("main", "type Status { Active }\npublic main() (values: Map<Text, Text>) { values = Status.options }")]).is_err());
}
#[test]
fn checked_defaults_and_generated_columns_keep_static_types() {
let program = packages(&[
("dever.database", ERROR),
("demo.account.model", "type Status { Active = \"启用\"\nDisabled = \"禁用\" }\ntype Account { uid: Uuid generated unique\nstatus: Status default Status.Active\namount: Decimal(5, 2) default -12.30\nnote: Text? default null\nname: Text(1, 8) default \"雪\" }"),
("demo.account.app", "create() (id: model.id) { row = model.create({})\nid = row.id }"),
("demo.account.api", "cmd create = app.create"),
]).unwrap();
let columns = &program.models[0].columns;
assert!(columns[2].generated);
assert_eq!(
columns[3].choice.as_ref().unwrap(),
&[
("Active".into(), "启用".into()),
("Disabled".into(), "禁用".into())
]
);
assert!(
matches!(&columns[4].default, Some(dever::compiler::model::persistence::Value::Text(value)) if value == "098770")
);
}
#[test]
fn invalid_defaults_and_generated_writes_fail_before_database_access() {
for field in [
"value: Decimal(5, 2) default 0.001",
"value: Decimal(5, 2) default 1000",
"value: Text(2) default \"三字符\"",
"value: Int default 1.1",
"value: Int default 9223372036854775808",
"value: Float default 1e999",
"value: Bool default null",
"value: Uuid default \"broken\"",
"value: Date default \"2025-02-29\"",
"value: Time default \"24:00:00\"",
"value: Json default \"{\\\"x\\\":1,\\\"x\\\":2}\"",
"value: Int generated",
"value: Uuid? generated",
"value: Uuid generated default \"00112233-4455-6677-8899-aabbccddeeff\"",
] {
let model = format!("type Account {{ {field} }}");
let error = packages(&[("demo.account.model", &model)]).unwrap_err();
assert_eq!(error.code, "C014", "{field}: {error:?}");
}
for body in [
"row = model.create({ uid = uid })",
"row = model.update(id, { uid = uid })",
"row = model.get(id)\nrow.uid = uid",
] {
let app = format!(
"write(id: model.id, uid: Uuid) (value: model.id) {{ {body}\nvalue = row.id }}"
);
let error = packages(&[
("dever.database", ERROR),
("demo.account.model", "type Account { uid: Uuid generated }"),
("demo.account.app", &app),
])
.unwrap_err();
assert!(matches!(error.code, "C014" | "C005"), "{body}: {error:?}");
assert!(error.message.contains("read-only"), "{error:?}");
}
}
Loaded 100 of 1479 files, more files were not shown because too many files have changed in this diff. Show more