
Test Applications on the Edge
Check streams of values from your app, including streams that never end, and always get an answer, using the closed world assumption. The long-term goal: testing how apps behave when they bootstrap, crash or recover.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
Check streams of values from your app, including streams that never end, and always get an answer, using the closed world assumption. The long-term goal: testing how apps behave when they bootstrap, crash or recover.
Arrays are bounded, so every, some or filter always give an answer. Streams are not: checking that every value of an infinite stream is positive never ends.
TAOTE gives streams an answer anyway. After a time budget (exhaustion), an unfinished stream is considered infinite, and the operation settles on a defined result:
every([1, 2, 3, 4, 5, 6, 7, 8, ┴], (e) => e < 10) // true
┴ is not false: it means something outside our control, like time or resources running out.
npm install taote
Usage
Streams are passed as generator functions, so TAOTE can restart them when needed.
import TAOTE from "taote";
// The app under test and its streams const app = { *naturals() { let n = 0; while (true) yield n++; }, *negatives() { let n = 0; while (true) yield n--; }, *fibonacci() { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; } }, };
const context = new TAOTE(app, 2); // 2 seconds before a stream counts as infinite
// Values while the condition holds, then how it ended [...context.until({ generator: app.fibonacci, condition: (v) => v < 1000 })]; // [0, 1, 1, 2, ..., 987, "halted"]
// One boolean [...context.every({ generator: app.naturals, condition: (v) => v >= 0 })]; // [true] - ran out of time [...context.some({ generator: app.naturals, condition: (v) => v > 300 })]; // [true] - found one [...context.some({ generator: app.naturals, condition: (v) => v < 0 })]; // [false] - ran out of time
// A holds until B holds (from Linear Temporal Logic) [...context.AUntilB({ generatorA: app.naturals, conditionA: (v) => v >= 0, generatorB: app.negatives, conditionB: (v) => v < -300, })];
console.log(context.last.outcome); // "satisfied"
How the last operation ended is kept in context.last.outcome: completed (the stream ended), halted (a condition failed), exhausted (time ran out) or satisfied (AUntilB reached B).
Operation What it does
until yields values while condition holds; optional transformer, process and window
every / some answer with one boolean, under the closed world assumption
AUntilB checks that A holds at every step until B holds
retry(resetMemory) replays the last operation, keeping or resetting its memory, like an app rebooting after a crash
repeater makes a stream endless by restarting it, optionally every round values
impurify perturbs a stream's structure to simulate failures (see below)
rateLimit / delayedGenerator async: pace a stream to one value per delay ms
Async streams
Real apps produce values asynchronously: events, network messages, logs. untilAsync, everyAsync, someAsync, AUntilBAsync and retryAsync accept async generators and any async iterable, such as a Node readable stream, as well as sync generators.
They enforce the time budget with a real timer, so a stream that stops producing is still declared exhausted on time instead of hanging the test:
// A server log that boots, then hangs without a word async function* serverLog() { yield "boot"; yield "ready"; await new Promise(() => {}); }
for await (const crashed of context.someAsync({ generator: serverLog, condition: (line) => line === "crash" })) console.log(crashed, context.last.outcome); // false "exhausted", after 2 seconds
// Node streams work too import { Readable } from "node:stream"; const lines = () => Readable.from(["boot", "ready", "crash"]); for await (const crashed of context.someAsync({ generator: lines, condition: (line) => line === "crash" })) console.log(crashed); // true
Conditions are still synchronous functions.
until can accumulate state through process, and remembers the last window values:
const sum = (v, memory) => { memory.result.sum = (memory.result.sum ?? 0) + v; };
[...context.until({ generator: app.naturals, process: sum, window: 3 })]; context.memory; // { result: { sum: ... }, window: [last 3 values] }
[...context.retry(false)]; // same run again, sum keeps growing [...context.retry(true)]; // fresh memory, sum restarts
Simulating failures with impurify
Pick one method. Each value comes back as [position, value], where position is its place in the original stream, so you can tell real values from noise:
src/main-browser.ts draws TAOTE streams with GeneratorCanvas, a canvas library driven by generators. Each row is a stream: blue cells are values, dark cells are lost values, red cells are injected values, and yellow shows how the stream ended.
git clone --recurse-submodules https://github.com/BasicHtmlCoder/TAOTE.git npm install npm test # run the tests npm run node_build # compile for Node npm start # run the Node demo (src/main-node.ts) npm run build # bundle the browser demo into dist/
Serve the repository root with any static server (e.g. npx http-server .) and open /dist/ to watch the visualization. See CONTRIBUTING.md before opening a pull request.
Exhaustion counts real time from the first value, including the time your code spends between values. A slow consumer runs out of time sooner.
The sync operations only check the time when a value arrives, so a sync generator that computes forever without yielding hangs them. Use the *Async operations for streams that may stall. A sync loop that never yields blocks JavaScript itself, so no timer can stop it.
On Node, TAOTE reports the operation that was running if the process crashes or is interrupted. Pass { lifecycle: false } to turn that off, or call context.dispose() when you are done.
Ideas for next steps
A value budget (maxValues) next to the time budget, so results don't depend on machine speed.
A seed for impurify, so a failing random run can be replayed exactly.
Adapters for common sources (event emitters, WebSockets, polling) and a fault-injection mode for impurify, to feed broken streams into a real app.
Why
Academia has plenty of work on testing systems with temporal logic, but little that is practical for everyday apps like web servers or web apps. TAOTE aims to test what happens at the edges of execution (start, restart, crash, overflow, unexpected values) without that level of formality.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。