원문 캡처 · github.com
원문 캡처 · github.com

TAOTE는 끝나지 않을 수 있는 값 스트림을 시간 예산 안에서 검사하는 라이브러리다. 앱이 시작되거나 멈추고 복구될 때의 동작을 테스트하는 것이 장기 목표다.

TAOTE는 닫힌 세계 가정(closed world assumption)을 적용한다. 배열은 끝이 정해져 있지만 스트림은 무한할 수 있어, 모든 값이 조건을 만족하는지 확인하는 작업이 끝나지 않을 수 있다. TAOTE는 제한 시간이 지나도 스트림이 끝나지 않으면 무한 스트림으로 간주하고 결과를 정한다. 이때 ┴는 false가 아니라 시간이나 자원처럼 통제할 수 없는 요인을 뜻한다.

javascript
every([1, 2, 3, 4, 5, 6, 7, 8, ┴], (e) => e < 10)  // true 

사용 방법과 제한

npm으로 설치한다.

bash
npm install taote

스트림은 필요할 때 다시 시작할 수 있도록 제너레이터 함수로 전달한다. 아래 예시에서는 스트림이 2초 동안 계속되면 무한한 것으로 처리한다.

javascript
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"

마지막 연산의 결과는 context.last.outcome에서 확인한다. 스트림이 끝나면 completed, 조건이 실패하면 halted, 시간이 다 되면 exhausted를 기록한다. AUntilB에서 B 조건에 도달하면 satisfied가 된다.

주요 연산은 다음과 같다.

  • until은 조건이 참인 동안 값을 내보내며, transformer, process, window를 선택적으로 받는다.
  • every와 some은 닫힌 세계 가정 아래 불리언 하나를 반환한다.
  • AUntilB는 B가 참이 될 때까지 모든 단계에서 A가 참인지 확인한다.
  • retry(resetMemory)는 마지막 연산을 다시 실행하며, 메모리를 유지하거나 초기화할 수 있다.
  • repeater는 스트림을 다시 시작해 끝없이 이어 붙인다. 선택적으로 round개 값마다 재시작할 수 있다.
  • impurify는 스트림 구조를 바꿔 실패 상황을 모의한다.
  • rateLimit과 delayedGenerator는 비동기 스트림을 delay밀리초마다 값 하나씩 내보내도록 조절한다.

비동기 스트림에는 untilAsync, everyAsync, someAsync, AUntilBAsync, retryAsync를 쓸 수 있다. 비동기 제너레이터와 Node 읽기 스트림 같은 비동기 이터러블을 받으며, 동기 제너레이터도 사용할 수 있다. 실제 타이머로 제한 시간을 적용하므로 스트림이 값 생성을 멈춰도 테스트가 멈춘 채 기다리지 않고 시간이 지나면 exhausted로 처리한다. 조건 함수는 동기 함수여야 한다.

동기 연산은 값이 도착할 때만 시간을 확인한다. 따라서 값을 내보내지 않고 무한히 계산하는 동기 제너레이터는 연산을 멈추게 한다. 이런 스트림에는 *Async 연산을 사용해야 한다. 값을 내보내지 않는 동기 루프는 자바스크립트 실행 자체를 막으므로 타이머로 중단할 수 없다. 제한 시간은 첫 값이 도착한 시점부터 실제 시간으로 계산하며, 값 사이에 코드가 실행되는 시간도 포함한다. 소비 속도가 느리면 더 빨리 시간이 소진된다.

메모리와 실패 모의

until은 process로 상태를 누적하고 window에 마지막 값들을 보관할 수 있다. retry(false)는 메모리를 유지해 같은 연산을 다시 실행하고, retry(true)는 메모리를 새로 시작한다.

javascript
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

impurify에는 다음 설정 가운데 하나를 선택한다. 각 값은 [position, value]로 반환되며, position은 원래 스트림에서 해당 값이 있던 위치다. 따라서 원래 값과 추가된 잡음을 구분할 수 있다.

  • endBeforeStart: true: 첫 값이 나오기 전에 스트림을 끝낸다.
  • ignoreSome: n: 처음 n개 값을 잃는다.
  • hop: n: n번째마다 값을 잃는다.
  • expander: n: 두 값마다 임의의 값 n개를 추가한다. 추가된 값의 위치는 -1이다.
  • shrinker: n: 두 값마다 n개 값을 잃는다.

Node에서 프로세스가 충돌하거나 중단되면 TAOTE는 실행 중이던 연산을 보고한다. 이를 끄려면 { lifecycle: false }를 전달하거나 작업이 끝난 뒤 context.dispose()를 호출한다.

시각화와 개발

src/main-browser.ts는 제너레이터 기반 캔버스 라이브러리 GeneratorCanvas로 TAOTE 스트림을 그린다. 각 행은 스트림 하나를 나타낸다. 파란색 칸은 값, 어두운 칸은 사라진 값, 빨간색 칸은 추가된 값이며, 노란색은 스트림이 끝난 방식을 보여준다.

저장소를 복제하고 의존성을 설치한 뒤 테스트를 실행하거나 Node·브라우저 데모를 빌드할 수 있다.

bash
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/

정적 서버로 저장소 루트를 제공한 뒤 /dist/를 열면 시각화를 볼 수 있다. 예시로 npx http-server .를 사용할 수 있다. 풀 리퀘스트를 열기 전에는 CONTRIBUTING.md를 확인하도록 안내한다.

향후 작업 아이디어로는 실행 속도에 따라 결과가 달라지지 않도록 시간 예산과 함께 쓰는 값 예산 maxValues, 실패한 무작위 실행을 정확히 재현하는 impurify용 seed, 이벤트 이미터·WebSocket·폴링용 어댑터, 실제 앱에 깨진 스트림을 전달하는 장애 주입 모드가 제시돼 있다. 현재 목표는 웹 서버나 웹 앱 같은 일상적인 앱에서 시작·재시작·충돌·넘침·예상치 못한 값 등 실행의 경계 상황을 형식적 접근 없이 테스트하는 것이다. 프로젝트는 시간 논리로 시스템을 테스트하는 연구는 많지만 일상적인 앱에 적용하기 쉬운 도구는 적다는 점을 배경으로 든다.

MIT 라이선스