From b18ae2dacb6a9b962bca0c04c8fea5aee89104f1 Mon Sep 17 00:00:00 2001 From: Alberto Ricart Date: Tue, 10 Dec 2024 19:00:23 -0600 Subject: [PATCH] Refactor code to improve maintainability and add new tests Commented out unused code, removed redundant implementations, and added new tests for Base64 encoding, JetStream consumers, and status handling. Enhanced validation for names and improved coverage tooling with a streamlined "coverage" task. Signed-off-by: Alberto Ricart --- .github/workflows/deno_checks.yml | 2 +- deno.json | 3 +- jetstream/src/jsapi_types.ts | 14 -- jetstream/src/jserrors.ts | 4 +- jetstream/src/jsmsg.ts | 58 ++++---- jetstream/src/jsutil.ts | 6 +- jetstream/tests/consume_test.ts | 6 + jetstream/tests/consumers_ordered_test.ts | 2 +- jetstream/tests/consumers_push_test.ts | 19 ++- jetstream/tests/jsm_test.ts | 6 +- jetstream/tests/jsmsg_test.ts | 27 ++++ jetstream/tests/jstest_util.ts | 67 +--------- jetstream/tests/status_test.ts | 155 ++++++++++++++++++++++ migration.md | 2 + obj/src/base64.ts | 2 +- obj/tests/b64_test.ts | 59 ++++++++ test_helpers/util.ts | 63 ++++++++- 17 files changed, 366 insertions(+), 129 deletions(-) create mode 100644 jetstream/tests/status_test.ts create mode 100644 obj/tests/b64_test.ts diff --git a/.github/workflows/deno_checks.yml b/.github/workflows/deno_checks.yml index e222a16b..f9eade38 100644 --- a/.github/workflows/deno_checks.yml +++ b/.github/workflows/deno_checks.yml @@ -36,7 +36,7 @@ jobs: CI: true run: | deno task test-${{ matrix.module }} - deno coverage --include=${{ matrix.module}}/src ./coverage --lcov > ./coverage/out.lcov + deno task coverage - name: Upload coverage uses: coverallsapp/github-action@v2 diff --git a/deno.json b/deno.json index de3234a0..4cd2fb0d 100644 --- a/deno.json +++ b/deno.json @@ -16,7 +16,8 @@ "test-obj": "deno test -A --parallel --reload --quiet --coverage=coverage obj/tests", "test-services": "deno test -A --parallel --reload --quiet --coverage=coverage services/tests", "test-transport-deno": "deno test -A --parallel --reload --quiet --coverage=coverage transport-deno/tests", - "cover": "deno coverage ./coverage --lcov > ./coverage/out.lcov && genhtml -o ./coverage/html ./coverage/out.lcov && open ./coverage/html/index.html", + "coverage": "deno coverage ./coverage --exclude=test_helpers --exclude=tests --exclude=examples --exclude=sha256 --lcov > ./coverage/out.lcov", + "cover": "deno task coverage && genhtml -o ./coverage/html ./coverage/out.lcov && open ./coverage/html/index.html", "lint": "deno task lint-core && deno task lint-jetstream && deno task lint-kv && deno task lint-obj && deno task lint-services && deno task lint-test_helpers", "lint-core": "cd core && deno lint", "lint-jetstream": "cd jetstream && deno lint", diff --git a/jetstream/src/jsapi_types.ts b/jetstream/src/jsapi_types.ts index e0f6cd88..9f9cba97 100644 --- a/jetstream/src/jsapi_types.ts +++ b/jetstream/src/jsapi_types.ts @@ -14,7 +14,6 @@ */ import type { Nanos } from "@nats-io/nats-core"; -import { nanos } from "@nats-io/nats-core"; import type { StoredMsg } from "./types.ts"; export type ApiPaged = { @@ -1053,19 +1052,6 @@ export enum PriorityPolicy { Overflow = "overflow", } -export function defaultConsumer( - name: string, - opts: Partial = {}, -): ConsumerConfig { - return Object.assign({ - name: name, - deliver_policy: DeliverPolicy.All, - ack_policy: AckPolicy.Explicit, - ack_wait: nanos(30 * 1000), - replay_policy: ReplayPolicy.Instant, - }, opts); -} - export type OverflowMinPending = { /** * The name of the priority_group diff --git a/jetstream/src/jserrors.ts b/jetstream/src/jserrors.ts index c4f4ba69..fb3f8248 100644 --- a/jetstream/src/jserrors.ts +++ b/jetstream/src/jserrors.ts @@ -193,8 +193,8 @@ export function isMessageNotFound(err: Error): boolean { } export class InvalidNameError extends Error { - constructor(name: string, message: string = "", opts?: ErrorOptions) { - super(`'${name} ${message}`, opts); + constructor(message: string = "", opts?: ErrorOptions) { + super(message, opts); this.name = "InvalidNameError"; } } diff --git a/jetstream/src/jsmsg.ts b/jetstream/src/jsmsg.ts index d0601785..c7f7813d 100644 --- a/jetstream/src/jsmsg.ts +++ b/jetstream/src/jsmsg.ts @@ -18,23 +18,21 @@ import type { MsgHdrs, MsgImpl, ProtocolHandler, - RequestOptions, } from "@nats-io/nats-core/internal"; import { - DataBuffer, deferred, millis, nanos, RequestOne, } from "@nats-io/nats-core/internal"; -import type { DeliveryInfo, PullOptions } from "./jsapi_types.ts"; +import type { DeliveryInfo } from "./jsapi_types.ts"; export const ACK = Uint8Array.of(43, 65, 67, 75); const NAK = Uint8Array.of(45, 78, 65, 75); const WPI = Uint8Array.of(43, 87, 80, 73); -const NXT = Uint8Array.of(43, 78, 88, 84); +const _NXT = Uint8Array.of(43, 78, 88, 84); const TERM = Uint8Array.of(43, 84, 69, 82, 77); -const SPACE = Uint8Array.of(32); +const _SPACE = Uint8Array.of(32); /** * Represents a message stored in JetStream @@ -100,19 +98,19 @@ export type JsMsg = { */ working(): void; - /** - * !! this is an experimental feature - and could be removed - * - * next() combines ack() and pull(), requires the subject for a - * subscription processing to process a message is provided - * (can be the same) however, because the ability to specify - * how long to keep the request open can be specified, this - * functionality doesn't work well with iterators, as an error - * (408s) are expected and needed to re-trigger a pull in case - * there was a timeout. In an iterator, the error will close - * the iterator, requiring a subscription to be reset. - */ - next(subj: string, ro?: Partial): void; + // /** + // * !! this is an experimental feature - and could be removed + // * + // * next() combines ack() and pull(), requires the subject for a + // * subscription processing to process a message is provided + // * (can be the same) however, because the ability to specify + // * how long to keep the request open can be specified, this + // * functionality doesn't work well with iterators, as an error + // * (408s) are expected and needed to re-trigger a pull in case + // * there was a timeout. In an iterator, the error will close + // * the iterator, requiring a subscription to be reset. + // */ + // next(subj: string, ro?: Partial): void; /** * Indicate to the JetStream server that processing of the message @@ -310,18 +308,18 @@ export class JsMsgImpl implements JsMsg { this.doAck(WPI); } - next(subj: string, opts: Partial = { batch: 1 }) { - const args: Partial = {}; - args.batch = opts.batch || 1; - args.no_wait = opts.no_wait || false; - if (opts.expires && opts.expires > 0) { - args.expires = nanos(opts.expires); - } - const data = new TextEncoder().encode(JSON.stringify(args)); - const payload = DataBuffer.concat(NXT, SPACE, data); - const reqOpts = subj ? { reply: subj } as RequestOptions : undefined; - this.msg.respond(payload, reqOpts); - } + // next(subj: string, opts: Partial = { batch: 1 }) { + // const args: Partial = {}; + // args.batch = opts.batch || 1; + // args.no_wait = opts.no_wait || false; + // if (opts.expires && opts.expires > 0) { + // args.expires = nanos(opts.expires); + // } + // const data = new TextEncoder().encode(JSON.stringify(args)); + // const payload = DataBuffer.concat(NXT, SPACE, data); + // const reqOpts = subj ? { reply: subj } as RequestOptions : undefined; + // this.msg.respond(payload, reqOpts); + // } term(reason = "") { let term = TERM; diff --git a/jetstream/src/jsutil.ts b/jetstream/src/jsutil.ts index 03d950de..c3c09ee1 100644 --- a/jetstream/src/jsutil.ts +++ b/jetstream/src/jsutil.ts @@ -13,6 +13,8 @@ * limitations under the License. */ +import { InvalidNameError } from "./jserrors.ts"; + export function validateDurableName(name?: string) { return minValidation("durable", name); } @@ -43,8 +45,8 @@ export function minValidation(context: string, name = "") { default: // nothing } - throw Error( - `invalid ${context} name - ${context} name cannot contain '${v}'`, + throw new InvalidNameError( + `${context} name ('${name}') cannot contain '${v}'`, ); } }); diff --git a/jetstream/tests/consume_test.ts b/jetstream/tests/consume_test.ts index 5043879b..8d367752 100644 --- a/jetstream/tests/consume_test.ts +++ b/jetstream/tests/consume_test.ts @@ -19,6 +19,7 @@ import { assert, assertEquals, assertExists, + assertFalse, assertRejects, } from "jsr:@std/assert"; import { initStream } from "./jstest_util.ts"; @@ -35,6 +36,8 @@ import type { PullConsumerMessagesImpl } from "../src/consumer.ts"; import { AckPolicy, DeliverPolicy, + isPullConsumer, + isPushConsumer, jetstream, jetstreamManager, } from "../src/mod.ts"; @@ -49,6 +52,9 @@ Deno.test("consumers - consume", async () => { const js = jetstream(nc, { timeout: 30_000 }); const c = await js.consumers.get(stream, consumer); + assert(isPullConsumer(c)); + assertFalse(isPushConsumer(c)); + const ci = await c.info(); assertEquals(ci.num_pending, count); const start = Date.now(); diff --git a/jetstream/tests/consumers_ordered_test.ts b/jetstream/tests/consumers_ordered_test.ts index 5fd3fcca..0aa7cc0d 100644 --- a/jetstream/tests/consumers_ordered_test.ts +++ b/jetstream/tests/consumers_ordered_test.ts @@ -859,7 +859,7 @@ Deno.test("ordered consumers - name prefix", async () => { return js.consumers.get("A", { name_prefix: "one.two" }); }, Error, - "invalid name_prefix name - name_prefix name cannot contain '.'", + "name_prefix name ('one.two') cannot contain '.'", ); await cleanup(ns, nc); diff --git a/jetstream/tests/consumers_push_test.ts b/jetstream/tests/consumers_push_test.ts index acd7d5c7..b9346077 100644 --- a/jetstream/tests/consumers_push_test.ts +++ b/jetstream/tests/consumers_push_test.ts @@ -1,12 +1,20 @@ import { AckPolicy, DeliverPolicy, + isPullConsumer, + isPushConsumer, jetstream, jetstreamManager, } from "../src/mod.ts"; +import { isBoundPushConsumerOptions } from "../src/types.ts"; import { cleanup, jetstreamServerConf, Lock, setup } from "test_helpers"; import { nanos } from "@nats-io/nats-core"; -import { assert, assertEquals, assertExists } from "jsr:@std/assert"; +import { + assert, + assertEquals, + assertExists, + assertFalse, +} from "jsr:@std/assert"; import type { PushConsumerMessagesImpl } from "../src/pushconsumer.ts"; import { delay } from "@nats-io/nats-core/internal"; @@ -21,16 +29,21 @@ Deno.test("push consumers - basics", async () => { js.publish("A.c"), ]); - await jsm.consumers.add("A", { + const opts = { durable_name: "B", deliver_subject: "hello", deliver_policy: DeliverPolicy.All, idle_heartbeat: nanos(5000), flow_control: true, ack_policy: AckPolicy.Explicit, - }); + }; + assert(isBoundPushConsumerOptions(opts)); + await jsm.consumers.add("A", opts); const c = await js.consumers.getPushConsumer("A", "B"); + assert(isPushConsumer(c)); + assertFalse(isPullConsumer(c)); + let info = await c.info(true); assertEquals(info.config.deliver_group, undefined); diff --git a/jetstream/tests/jsm_test.ts b/jetstream/tests/jsm_test.ts index 2d32d949..50c5075d 100644 --- a/jetstream/tests/jsm_test.ts +++ b/jetstream/tests/jsm_test.ts @@ -2125,7 +2125,7 @@ Deno.test("jsm - validate stream name in operations", async () => { if (v === "\t") v = "\\t"; const m = v === "" ? "stream name required" - : `invalid stream name - stream name cannot contain '${v}'`; + : `stream name ('${names[idx]}') cannot contain '${v}'`; assertEquals((err as Error).message, m, `${test.name} - ${m}`); } } @@ -2156,7 +2156,7 @@ Deno.test("jsm - validate consumer name", async () => { if (v === "\r") v = "\\r"; if (v === "\n") v = "\\n"; if (v === "\t") v = "\\t"; - const m = `invalid durable name - durable name cannot contain '${v}'`; + const m = `durable name ('${tests[idx]}') cannot contain '${v}'`; assertEquals((err as Error).message, m); } } @@ -2222,7 +2222,7 @@ Deno.test("jsm - validate consumer name in operations", async () => { if (v === "\t") v = "\\t"; const m = v === "" ? "durable name required" - : `invalid durable name - durable name cannot contain '${v}'`; + : `durable name ('${names[idx]}') cannot contain '${v}'`; assertEquals((err as Error).message, m, `${test.name} - ${m}`); } } diff --git a/jetstream/tests/jsmsg_test.ts b/jetstream/tests/jsmsg_test.ts index 70136ca5..6294330f 100644 --- a/jetstream/tests/jsmsg_test.ts +++ b/jetstream/tests/jsmsg_test.ts @@ -16,6 +16,7 @@ import { assert, assertEquals, assertExists, + assertNotEquals, assertRejects, fail, } from "jsr:@std/assert"; @@ -320,3 +321,29 @@ Deno.test("jsmsg - time and timestamp", async () => { await cleanup(ns, nc); }); + +Deno.test("jsmsg - reply/sid", async () => { + const { ns, nc } = await setup(jetstreamServerConf()); + const jsm = await jetstreamManager(nc) as JetStreamManagerImpl; + await jsm.streams.add({ + name: "A", + subjects: ["a.>"], + storage: StorageType.Memory, + allow_direct: true, + }); + + const js = jetstream(nc); + await js.publish("a.a", "hello"); + + await jsm.consumers.add("A", { + durable_name: "a", + ack_policy: AckPolicy.None, + }); + const oc = await js.consumers.get("A"); + const m = await oc.next() as JsMsgImpl; + assertNotEquals(m.reply, ""); + assert(m.sid > 0); + assertEquals(m.data, new TextEncoder().encode("hello")); + + await cleanup(ns, nc); +}); diff --git a/jetstream/tests/jstest_util.ts b/jetstream/tests/jstest_util.ts index 7853e3c8..fd67e55b 100644 --- a/jetstream/tests/jstest_util.ts +++ b/jetstream/tests/jstest_util.ts @@ -13,23 +13,11 @@ * limitations under the License. */ import { AckPolicy, jetstream, jetstreamManager } from "../src/mod.ts"; -import type { JsMsg, PubAck, StreamConfig } from "../src/mod.ts"; +import type { PubAck, StreamConfig } from "../src/mod.ts"; -import { assert } from "jsr:@std/assert"; import { Empty, nanos, nuid } from "@nats-io/nats-core"; -import type { NatsConnection, QueuedIterator } from "@nats-io/nats-core"; - -export async function consume(iter: QueuedIterator): Promise { - const buf: JsMsg[] = []; - await (async () => { - for await (const m of iter) { - m.ack(); - buf.push(m); - } - })(); - return buf; -} +import type { NatsConnection } from "@nats-io/nats-core"; export async function initStream( nc: NatsConnection, @@ -95,54 +83,3 @@ export function fill( return Promise.all(a); } - -export function time(): Mark { - return new Mark(); -} - -export class Mark { - measures: [number, number][]; - constructor() { - this.measures = []; - this.measures.push([Date.now(), 0]); - } - - mark() { - const now = Date.now(); - const idx = this.measures.length - 1; - if (this.measures[idx][1] === 0) { - this.measures[idx][1] = now; - } else { - this.measures.push([now, 0]); - } - } - - duration(): number { - const idx = this.measures.length - 1; - if (this.measures[idx][1] === 0) { - this.measures.pop(); - } - const times = this.measures.map((v) => v[1] - v[0]); - return times.reduce((result, item) => { - return result + item; - }); - } - - assertLess(target: number) { - const d = this.duration(); - assert( - target >= d, - `duration ${d} not in range - ${target} ≥ ${d}`, - ); - } - - assertInRange(target: number) { - const min = .50 * target; - const max = 1.50 * target; - const d = this.duration(); - assert( - d >= min && max >= d, - `duration ${d} not in range - ${min} ≥ ${d} && ${max} ≥ ${d}`, - ); - } -} diff --git a/jetstream/tests/status_test.ts b/jetstream/tests/status_test.ts new file mode 100644 index 00000000..32925b50 --- /dev/null +++ b/jetstream/tests/status_test.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2024 Synadia Communications, Inc + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isMessageNotFound, JetStreamStatus } from "../src/jserrors.ts"; +import type { StreamAPIImpl } from "../src/jsmstream_api.ts"; +import { Empty, type Msg, type Payload } from "@nats-io/nats-core/internal"; +import { + assert, + assertEquals, + assertRejects, +} from "https://deno.land/std@0.221.0/assert/mod.ts"; +import { MsgHdrsImpl } from "../../core/src/headers.ts"; +import { cleanup, setup } from "test_helpers"; +import { jetstreamServerConf } from "../../test_helpers/mod.ts"; +import { + type JetStreamApiError, + jetstreamManager, +} from "../src/internal_mod.ts"; + +Deno.test("js status - basics", async (t) => { + function makeMsg( + code: number, + description: string, + payload: Payload = Empty, + hdrs: [string, string[]][] = [], + ): Msg { + const h = new MsgHdrsImpl(code, description); + for (const [k, v] of hdrs) { + for (const e of v) { + h.append(k, e); + } + } + let data: Uint8Array = Empty; + if (typeof payload === "string") { + data = new TextEncoder().encode(payload); + } else { + data = payload; + } + + return { + subject: "foo", + reply: "", + headers: h, + data, + sid: 1, + respond: function () { + return this.reply !== ""; + }, + string: function () { + return new TextDecoder().decode(this.data); + }, + json: function () { + return JSON.parse(this.string()); + }, + }; + } + + function makeStatus( + code: number, + description: string, + payload: Payload = Empty, + hdrs?: [string, string[]][], + ): JetStreamStatus { + return new JetStreamStatus(makeMsg(code, description, payload, hdrs)); + } + + await t.step("debug", () => { + const s = makeStatus(404, "not found"); + s.debug(); + }); + + await t.step("empty description", () => { + const s = makeStatus(404, ""); + assertEquals(s.description, "unknown"); + }); + + await t.step("idle heartbeat", () => { + const s = makeStatus(100, "idle heartbeat", Empty, [["Nats-Last-Consumer", [ + "1", + ]], ["Nats-Last-Stream", ["10"]]]); + assert(s.isIdleHeartbeat()); + assertEquals(s.parseHeartbeat(), { + type: "heartbeat", + lastConsumerSequence: 1, + lastStreamSequence: 10, + }); + + assertEquals(s.description, "idle heartbeat"); + }); + + await t.step("idle heartbeats missed", () => { + const s = makeStatus(409, "idle heartbeats missed"); + assert(s.isIdleHeartbeatMissed()); + }); + + await t.step("request timeout", () => { + const s = makeStatus(408, "request timeout"); + assert(s.isRequestTimeout()); + }); + + await t.step("bad request", () => { + const s = makeStatus(400, ""); + assert(s.isBadRequest()); + }); + + await t.step("stream deleted", () => { + const s = makeStatus(409, "stream deleted"); + assert(s.isStreamDeleted()); + }); + + await t.step("exceeded maxwaiting", () => { + const s = makeStatus(409, "exceeded maxwaiting"); + assert(s.isMaxWaitingExceeded()); + }); + + await t.step("consumer is push based", () => { + const s = makeStatus(409, "consumer is push based"); + assert(s.isConsumerIsPushBased()); + }); +}); + +Deno.test("api error - basics", async () => { + const { ns, nc } = await setup(jetstreamServerConf()); + const jsm = await jetstreamManager(nc); + await jsm.streams.add({ + name: "test", + subjects: ["foo"], + allow_direct: true, + }); + + const api = jsm.streams as StreamAPIImpl; + const err = await assertRejects(() => { + return api._request(`$JS.API.STREAM.MSG.GET.test`, { seq: 1 }); + }); + const apiErr = err as JetStreamApiError; + assert(isMessageNotFound(err as Error)); + const data = apiErr.apiError(); + assertEquals(data.code, 404); + assertEquals(data.err_code, 10037); + assertEquals(data.description, "no message found"); + + await cleanup(ns, nc); +}); diff --git a/migration.md b/migration.md index 010dc697..788ea06a 100644 --- a/migration.md +++ b/migration.md @@ -144,6 +144,8 @@ To use JetStream, you must install and import `@nats/jetstream`. - The `ConsumerEvents` and `ConsumerDebugEvents` enum has been removed and replaced with `ConsumerNotification` which have a discriminating field `type`. The status objects provide a more specific API for querying those events. +- The JsMsg.next() API has been retracted as the simplified consumer `next()`, + and `consume()` provide the necessary functionality. ## Changes to KV diff --git a/obj/src/base64.ts b/obj/src/base64.ts index e40b2d53..6bc06ef1 100644 --- a/obj/src/base64.ts +++ b/obj/src/base64.ts @@ -46,7 +46,7 @@ export class Base64UrlPaddedCodec { } static decode(s: string, binary = false): Uint8Array | string { - return Base64UrlPaddedCodec.decode( + return Base64UrlCodec.decode( Base64UrlPaddedCodec.fromB64URLEncoding(s), binary, ); diff --git a/obj/tests/b64_test.ts b/obj/tests/b64_test.ts new file mode 100644 index 00000000..f3971dc2 --- /dev/null +++ b/obj/tests/b64_test.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Synadia Communications, Inc + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Base64Codec, + Base64UrlCodec, + Base64UrlPaddedCodec, +} from "../src/base64.ts"; +import { assert, assertEquals, assertFalse } from "jsr:@std/assert"; + +Deno.test("b64 - Base64Codec", () => { + // should match btoa + assertEquals(Base64Codec.encode("hello"), btoa("hello")); + assertEquals(Base64Codec.decode(btoa("hello")), "hello"); + + // binary input + const bin = new TextEncoder().encode("hello"); + assertEquals(Base64Codec.encode(bin), btoa("hello")); + assertEquals(Base64Codec.decode(Base64Codec.encode(bin), true), bin); +}); + +Deno.test("b64 - Base64UrlCodec", () => { + // URL encoding removes padding + const v = btoa(encodeURI("hello/world/one+two")).replaceAll("=", ""); + + assertEquals(Base64UrlCodec.encode("hello/world/one+two"), v); + assertEquals(Base64UrlCodec.decode(v), "hello/world/one+two"); + assertFalse(v.endsWith("=="), "expected padded"); + + // binary input + const bin = new TextEncoder().encode("hello/world/one+two"); + assertEquals(Base64UrlCodec.encode(bin), v); + assertEquals(Base64UrlCodec.decode(v, true), bin); +}); + +Deno.test("b64 - Base64UrlPaddedCodec", () => { + // URL encoding removes padding + const v = btoa(encodeURI("hello/world/one+two")); + assert(v.endsWith("=="), "expected padded"); + assertEquals(Base64UrlPaddedCodec.encode("hello/world/one+two"), v); + assertEquals(Base64UrlPaddedCodec.decode(v), "hello/world/one+two"); + + // binary input + const bin = new TextEncoder().encode("hello/world/one+two"); + assertEquals(Base64UrlPaddedCodec.encode(bin), v); + assertEquals(Base64UrlPaddedCodec.decode(v, true), bin); +}); diff --git a/test_helpers/util.ts b/test_helpers/util.ts index 692943b5..20840e4e 100644 --- a/test_helpers/util.ts +++ b/test_helpers/util.ts @@ -13,14 +13,14 @@ * limitations under the License. */ import { deferred, timeout } from "../core/src/internal_mod.ts"; -import type { Msg, Subscription } from "../core/src/internal_mod.ts"; +import { assert } from "jsr:@std/assert"; -export function consume(sub: Subscription, ms = 1000): Promise { - const to = timeout(ms); - const d = deferred(); - const msgs: Msg[] = []; +export function consume(iter: Iterable, ms = 1000): Promise { + const to = timeout(ms); + const d = deferred(); + const msgs: T[] = []; (async () => { - for await (const m of sub) { + for await (const m of iter) { msgs.push(m); } to.cancel(); @@ -31,3 +31,54 @@ export function consume(sub: Subscription, ms = 1000): Promise { return Promise.race([to, d]); } + +export function time(): Mark { + return new Mark(); +} + +export class Mark { + measures: [number, number][]; + constructor() { + this.measures = []; + this.measures.push([Date.now(), 0]); + } + + mark() { + const now = Date.now(); + const idx = this.measures.length - 1; + if (this.measures[idx][1] === 0) { + this.measures[idx][1] = now; + } else { + this.measures.push([now, 0]); + } + } + + duration(): number { + const idx = this.measures.length - 1; + if (this.measures[idx][1] === 0) { + this.measures.pop(); + } + const times = this.measures.map((v) => v[1] - v[0]); + return times.reduce((result, item) => { + return result + item; + }); + } + + assertLess(target: number) { + const d = this.duration(); + assert( + target >= d, + `duration ${d} not in range - ${target} ≥ ${d}`, + ); + } + + assertInRange(target: number) { + const min = .50 * target; + const max = 1.50 * target; + const d = this.duration(); + assert( + d >= min && max >= d, + `duration ${d} not in range - ${min} ≥ ${d} && ${max} ≥ ${d}`, + ); + } +}