diff --git a/.gitignore b/.gitignore index ebfcc07..9c175d8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ config.js registration.yaml coverage db/ooye.db* -test/res/butterfly* +test/res/* +!test/res/lottie* diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 4849740..9067f9b 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -17,6 +17,8 @@ const eventToMessage = sync.require("../converters/event-to-message") const api = sync.require("../../matrix/api") /** @type {import("../../d2m/actions/register-user")} */ const registerUser = sync.require("../../d2m/actions/register-user") +/** @type {import("../converters/emoji-sheet")} */ +const emojiSheet = sync.require("../converters/emoji-sheet") /** * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message @@ -75,7 +77,7 @@ async function sendEvent(event) { // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it - let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, fetch}) + let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, fetch, mxcDownloader: emojiSheet.getAndConvertEmoji}) messagesToEdit = await Promise.all(messagesToEdit.map(async e => { e.message = await resolvePendingFiles(e.message) diff --git a/m2d/converters/emoji-sheet.js b/m2d/converters/emoji-sheet.js index 9e8703d..4b8c321 100644 --- a/m2d/converters/emoji-sheet.js +++ b/m2d/converters/emoji-sheet.js @@ -1,6 +1,7 @@ // @ts-check const assert = require("assert").strict +const fs = require("fs") const {pipeline} = require("stream").promises const sharp = require("sharp") const {GIFrame} = require("giframe") @@ -16,27 +17,11 @@ const IMAGES_ACROSS = Math.floor(RESULT_WIDTH / SIZE) /** * Composite a bunch of Matrix emojis into a kind of spritesheet image to upload to Discord. * @param {string[]} mxcs mxc URLs, in order + * @param {(mxc: string) => Promise} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock. * @returns {Promise} PNG image */ -async function compositeMatrixEmojis(mxcs) { - const buffers = await Promise.all(mxcs.map(async mxc => { - const abortController = new AbortController() - - const url = utils.getPublicUrlForMxc(mxc) - assert(url) - - /** @type {import("node-fetch").Response} */ - // If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing. - // If we were using connection pooling, we would be forced to download the entire GIF. - // So we set no agent to ensure we are not connection pooling. - // @ts-ignore the signal is slightly different from the type it wants (still works fine) - const res = await fetch(url, {agent: false, signal: abortController.signal}) - return convertImageStream(res.body, () => { - abortController.abort() - res.body.pause() - res.body.emit("end") - }) - })) +async function compositeMatrixEmojis(mxcs, mxcDownloader) { + const buffers = await Promise.all(mxcs.map(mxcDownloader)) // Calculate the size of the final composited image const totalWidth = Math.min(buffers.length, IMAGES_ACROSS) * SIZE @@ -65,6 +50,49 @@ async function compositeMatrixEmojis(mxcs) { return output.data } +/** + * Downloads the emoji from the web and converts to uncompressed PNG data. + * @param {string} mxc a single mxc:// URL + * @returns {Promise} uncompressed PNG data, or undefined if the downloaded emoji is not valid + */ +async function getAndConvertEmoji(mxc) { + const abortController = new AbortController() + + const url = utils.getPublicUrlForMxc(mxc) + assert(url) + + /** @type {import("node-fetch").Response} */ + // If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing. + // If we were using connection pooling, we would be forced to download the entire GIF. + // So we set no agent to ensure we are not connection pooling. + // @ts-ignore the signal is slightly different from the type it wants (still works fine) + const res = await fetch(url, {agent: false, signal: abortController.signal}) + return convertImageStream(res.body, () => { + abortController.abort() + res.body.pause() + res.body.emit("end") + }) +} + +/** + * MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data. + * @param {string} mxc a single mxc:// URL + * @returns {Promise} uncompressed PNG data, or undefined if the downloaded emoji is not valid +*/ +async function _mockGetAndConvertEmoji(mxc) { + const id = mxc.match(/\/([^./]*)$/)?.[1] + let s + if (fs.existsSync(`test/res/${id}.png`)) { + s = fs.createReadStream(`test/res/${id}.png`) + } else { + s = fs.createReadStream(`test/res/${id}.gif`) + } + return convertImageStream(s, () => { + s.pause() + s.emit("end") + }) +} + /** * @param {import("node-fetch").Response["body"]} streamIn * @param {() => any} stopStream @@ -128,4 +156,6 @@ async function convertImageStream(streamIn, stopStream) { } module.exports.compositeMatrixEmojis = compositeMatrixEmojis +module.exports.getAndConvertEmoji = getAndConvertEmoji +module.exports._mockGetAndConvertEmoji = _mockGetAndConvertEmoji module.exports._convertImageStream = convertImageStream diff --git a/m2d/converters/emoji-sheet.test.js b/m2d/converters/emoji-sheet.test.js index 026c53f..6096d0c 100644 --- a/m2d/converters/emoji-sheet.test.js +++ b/m2d/converters/emoji-sheet.test.js @@ -27,8 +27,9 @@ class Meter extends Transform { * @param {import("supertape").Test} t * @param {string} path * @param {number} totalSize + * @param {number => boolean} sizeCheck */ -async function runSingleTest(t, path, totalSize) { +async function runSingleTest(t, path, totalSize, sizeCheck) { const file = fs.createReadStream(path) const meter = new Meter() const p = file.pipe(meter) @@ -36,19 +37,23 @@ async function runSingleTest(t, path, totalSize) { file.pause() file.emit("end") }) - t.equal(result.subarray(1, 4).toString("ascii"), "PNG", `result was not a PNG file: ${result.toString("base64")}`) + t.equal(result.subarray(1, 4).toString("ascii"), "PNG", `test that this is a PNG file: ${result.toString("base64").slice(0, 100)}`) /* c8 ignore next 5 */ - if (meter.bytes < totalSize / 4) { // should download less than 25% of each file - t.pass("intentionally read partial file") + if (sizeCheck(meter.bytes)) { + t.pass("read the correct amount of the file") } else { - t.fail(`read more than 25% of file, read: ${meter.bytes}, total: ${totalSize}`) + t.fail(`read too much or too little of the file, read: ${meter.bytes}, total: ${totalSize}`) } } slow()("emoji-sheet: only partial file is read for APNG", async t => { - await runSingleTest(t, "test/res/butterfly.png", 2438998) + await runSingleTest(t, "test/res/butterfly.png", 2438998, n => n < 2438998 / 4) // should download less than 25% of the file }) slow()("emoji-sheet: only partial file is read for GIF", async t => { - await runSingleTest(t, "test/res/butterfly.gif", 781223) + await runSingleTest(t, "test/res/butterfly.gif", 781223, n => n < 781223 / 4) // should download less than 25% of the file +}) + +slow()("emoji-sheet: entire file is read for static PNG", async t => { + await runSingleTest(t, "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png", 11301, n => n === 11301) // should download entire file }) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index ae9be22..88b60d1 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -304,8 +304,9 @@ function getUserOrProxyOwnerID(mxid) { * @param {string} content * @param {{id: string, name: string}[]} attachments * @param {({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles + * @param {(mxc: string) => Promise} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock. */ -async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) { +async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, mxcDownloader) { if (!content.includes("<::>")) return content // No unknown emojis, nothing to do // Remove known and unknown emojis from the end of the message const r = /\s*$/ @@ -313,7 +314,7 @@ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) content = content.replace(r, "") } // Create a sprite sheet of known and unknown emojis from the end of the message - const buffer = await emojiSheet.compositeMatrixEmojis(endOfMessageEmojis) + const buffer = await emojiSheet.compositeMatrixEmojis(endOfMessageEmojis, mxcDownloader) // Attach it const name = "emojis.png" attachments.push({id: String(attachments.length), name}) @@ -421,7 +422,7 @@ const attachmentEmojis = new Map([ /** * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event * @param {import("discord-api-types/v10").APIGuild} guild - * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"]}} di simple-as-nails dependency injection for the matrix API + * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"], mxcDownloader: (mxc: string) => Promise}} di simple-as-nails dependency injection for the matrix API */ async function eventToMessage(event, guild, di) { /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */ @@ -717,7 +718,7 @@ async function eventToMessage(event, guild, di) { if (replyLine && content.startsWith("> ")) content = "\n" + content // SPRITE SHEET EMOJIS FEATURE: - content = await uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) + content = await uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, di?.mxcDownloader) } else { // Looks like we're using the plaintext body! content = event.content.body diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 1573a94..2a88734 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1,6 +1,7 @@ const assert = require("assert").strict const {test} = require("supertape") const {eventToMessage} = require("./event-to-message") +const {_mockGetAndConvertEmoji} = require("./emoji-sheet") const data = require("../../test/data") const {MatrixServerError} = require("../../matrix/mreq") const {db, select, discord} = require("../../passthrough") @@ -3534,7 +3535,7 @@ slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet" }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }) + }, {}, {mxcDownloader: _mockGetAndConvertEmoji}) const testResult = { content: messages.messagesToSend[0].content, fileName: messages.messagesToSend[0].pendingFiles[0].name, @@ -3559,7 +3560,7 @@ slow()("event2message: known emoji from an unreachable server at the end is reup }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }) + }, {}, {mxcDownloader: _mockGetAndConvertEmoji}) const testResult = { content: messages.messagesToSend[0].content, fileName: messages.messagesToSend[0].pendingFiles[0].name, @@ -3584,7 +3585,7 @@ slow()("event2message: known and unknown emojis in the end are reuploaded as a s }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }) + }, {}, {mxcDownloader: _mockGetAndConvertEmoji}) const testResult = { content: messages.messagesToSend[0].content, fileName: messages.messagesToSend[0].pendingFiles[0].name, @@ -3609,7 +3610,7 @@ slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet }, event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4", room_id: "!maggESguZBqGBZtSnr:cadence.moe" - }) + }, {}, {mxcDownloader: _mockGetAndConvertEmoji}) const testResult = { content: messages.messagesToSend[0].content, fileName: messages.messagesToSend[0].pendingFiles[0].name, @@ -3618,6 +3619,6 @@ slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet t.deepEqual(testResult, { content: "testing", fileName: "emojis.png", - fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAASAAAAAwCAYAAACxIqevAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOV9B1xUV9r3JMbEGBQLbRodhukDg2jWZP02" + fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOx9B3xT1/W/UkImYKZtLdt4a0uWMaQkzS9t" }) }) diff --git a/package-lock.json b/package-lock.json index 1ff65ce..880c150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,9 +38,10 @@ "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", + "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", - "supertape": "^8.3.0", + "supertape": "^10.3.0", "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" } }, @@ -163,6 +164,75 @@ "node": ">=14" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -173,12 +243,12 @@ } }, "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -209,17 +279,27 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@putout/cli-keypress": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-1.0.0.tgz", - "integrity": "sha512-w+lRVGZodRM4K214R4jvyOsmCUGA3OnaYDOJ2rpXj6a+O6n91zLlkb7JYsw6I0QCNmXjpNLJSoLgzGWTue6YIg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-2.0.0.tgz", + "integrity": "sha512-EXJv2HaXM+5scjoxE6Tf+o4+pxwL1tYJZJBDMygrF7cocjirGcU05GgNr9WHOaUPaVOpVjVU98ugYD7XJLmMkw==", "dev": true, "dependencies": { - "ci-info": "^3.1.1", + "ci-info": "^4.0.0", "fullstore": "^3.0.0" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/@putout/cli-validate-args": { @@ -236,9 +316,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@supertape/engine-loader": { @@ -279,19 +359,31 @@ } }, "node_modules/@supertape/formatter-progress-bar": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-4.1.0.tgz", - "integrity": "sha512-MYwso7kbiTE0DaZgbiSlNOikmEcFdL4RQUu1JvnW+cS6ZLl3fqNnmvKa1a14VChKyHzfaTKYLuqToN8zgUjP2g==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-5.0.2.tgz", + "integrity": "sha512-thJlWFQLVeZjK/YZktUNBU69LFaEjBNvRcg6otuXn0RHhorRrgNWHHkA7JLDHRExzc+xaGJJvrWwsslh47vN2g==", "dev": true, "dependencies": { - "chalk": "^4.1.0", - "ci-info": "^3.1.1", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", "cli-progress": "^3.8.2", "fullstore": "^3.0.0", "once": "^1.4.0" }, "engines": { - "node": ">=16" + "node": ">=18" + } + }, + "node_modules/@supertape/formatter-progress-bar/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@supertape/formatter-short": { @@ -312,10 +404,39 @@ "node": ">=16" } }, + "node_modules/@supertape/formatter-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@supertape/formatter-time/-/formatter-time-1.0.2.tgz", + "integrity": "sha512-QihQWA/3LSNuODHrL8MGNHkdRunaEqNQkuMUDGNgEQO8MYBB0d83WGlNxDFGjn4kRlq47hovw3Skq7Btb2i2JA==", + "dev": true, + "dependencies": { + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-progress": "^3.8.2", + "fullstore": "^3.0.0", + "once": "^1.4.0", + "timer-node": "^5.0.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@supertape/formatter-time/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@supertape/operator-stub": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.0.0.tgz", - "integrity": "sha512-LZ6E4nSMDMbLOhvEZyeXo8wS5EBiAAffWrohb7yaVHDVTHr+xkczzPxinkvcOBhNuAtC0kVARdMbHg+HULmozA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.1.0.tgz", + "integrity": "sha512-jzC56u1k+3DLRo854+J6v/DP/4SjRV2mAqfR6qzsyaAocC9OFe7NHYQQMmlJ4cUJwgFjUh7AVnjFfC0Z0XuH+g==", "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0" @@ -722,9 +843,9 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", "dev": true, "funding": [ { @@ -1061,9 +1182,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -1087,6 +1208,12 @@ "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1480,19 +1607,50 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1779,12 +1937,12 @@ } }, "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1982,25 +2140,43 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2141,15 +2317,18 @@ } }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -2160,6 +2339,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mixin-deep": { "version": "3.0.0", "resolved": "git+ssh://git@github.com/cloudrac3r/mixin-deep.git#2dd70d6b8644263f7ed2c1620506c9eb3f11d32a", @@ -2398,6 +2586,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -2454,12 +2667,12 @@ "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==" }, "node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -2652,12 +2865,12 @@ } }, "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -3103,6 +3316,42 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3125,9 +3374,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "dependencies": { "ansi-regex": "^6.0.1" @@ -3139,6 +3388,28 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3164,25 +3435,26 @@ } }, "node_modules/supertape": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-8.6.0.tgz", - "integrity": "sha512-zvAXZaliVu8qpGRx5KiYQfPZcQD9B361lmRtXb3zyilpHHc0/5ygQ9MfWYZEwXywxHDfve3w8ZukI/NKPT9PyA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.3.0.tgz", + "integrity": "sha512-z/HQbwB1+UMpmCQZXxxSU2zhzc+0yQq1Q9naCpyVDcRr2/cO/Bo3YhbSXJpEPVbvGy3BADl+vt5cfbXRJYFO0g==", "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0", - "@putout/cli-keypress": "^1.0.0", + "@putout/cli-keypress": "^2.0.0", "@putout/cli-validate-args": "^1.0.1", "@supertape/engine-loader": "^2.0.0", "@supertape/formatter-fail": "^3.0.0", "@supertape/formatter-json-lines": "^2.0.0", - "@supertape/formatter-progress-bar": "^4.0.0", + "@supertape/formatter-progress-bar": "^5.0.0", "@supertape/formatter-short": "^2.0.0", "@supertape/formatter-tap": "^3.0.0", + "@supertape/formatter-time": "^1.0.0", "@supertape/operator-stub": "^3.0.0", "cli-progress": "^3.8.2", "deep-equal": "^2.0.3", "fullstore": "^3.0.0", - "glob": "^8.0.3", + "glob": "^10.3.10", "jest-diff": "^29.0.1", "once": "^1.4.0", "resolve": "^1.17.0", @@ -3193,11 +3465,11 @@ "yargs-parser": "^21.0.0" }, "bin": { - "supertape": "bin/supertape.mjs", - "tape": "bin/supertape.mjs" + "supertape": "bin/tracer.mjs", + "tape": "bin/tracer.mjs" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/supports-color": { @@ -3378,6 +3650,12 @@ "node": ">= 6" } }, + "node_modules/timer-node": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.7.tgz", + "integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==", + "dev": true + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -3602,6 +3880,45 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", diff --git a/package.json b/package.json index 95d9f0a..40b81ae 100644 --- a/package.json +++ b/package.json @@ -44,15 +44,16 @@ "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", + "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", - "supertape": "^8.3.0", + "supertape": "^10.3.0", "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" }, "scripts": { "addbot": "node addbot.js", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", - "test-slow": "cross-env FORCE_COLOR=true SUPERTAPE_TIMEOUT=6000 supertape --no-check-assertions-count --format tap test/test.js -- --slow | tap-dot", + "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", "cover": "c8 --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -r html -r text supertape --no-check-assertions-count --format fail test/test.js -- --slow" } } diff --git a/test/test.js b/test/test.js index 3e5ba72..1e742a0 100644 --- a/test/test.js +++ b/test/test.js @@ -2,6 +2,7 @@ const fs = require("fs") const {join} = require("path") +const stp = require("stream").promises const sqlite = require("better-sqlite3") const migrate = require("../db/migrate") const HeatSync = require("heatsync") @@ -10,6 +11,7 @@ const data = require("./data") /** @type {import("node-fetch").default} */ // @ts-ignore const fetch = require("node-fetch") +const {green} = require("colorette") const config = require("../config") const passthrough = require("../passthrough") @@ -50,6 +52,48 @@ const file = sync.require("../matrix/file") file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) } ;(async () => { + /* c8 ignore start - maybe download some more test files in slow mode */ + if (process.argv.includes("--slow")) { + test("test files: download", async t => { + /** @param {{url: string, to: string}[]} files */ + async function allReporter(files) { + return new Promise(resolve => { + let resolved = 0 + const report = files.map(file => file.to.split("/").slice(-1)[0][0]) + files.map(download).forEach((p, i) => { + p.then(() => { + report[i] = green(".") + process.stderr.write("\r" + report.join("")) + if (++resolved === files.length) resolve(null) + }) + }) + }) + } + async function download({url, to}) { + if (await fs.existsSync(to)) return + const res = await fetch(url) + await stp.pipeline(res.body, fs.createWriteStream(to, {encoding: "binary"})) + } + await allReporter([ + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/bZFuuUSEebJYXUMSxuuSuLTa", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/qWmbXeRspZRLPcjseyLmeyXC", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/WbYqNlACRuicynBfdnPYtmvc", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/HYcztccFIPgevDvoaWNsEtGJ", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/lHfmJpzgoNyNtYHdAmBHxXix", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/MtRdXixoKjKKOyHJGWLsWLNU", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/HXfFuougamkURPPMflTJRxGc", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ikYKbkhGhMERAuPPbsnQzZiX", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"}, + {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"}, + {url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"}, + {url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.png"}, + ]) + }, {timeout: 60000}) + } + /* c8 ignore end */ + const p = migrate.migrate(db) test("migrate: migration works", async t => { await p @@ -64,26 +108,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not db.exec(fs.readFileSync(join(__dirname, "ooye-test-data.sql"), "utf8")) - /* c8 ignore start - maybe download some more test files in slow mode */ - if (process.argv.includes("--slow")) { - test("test files: download", async t => { - function download(url, to) { - return new Promise(async resolve => { - if (fs.existsSync(to)) return resolve(null) - const res = await fetch(url) - res.body.pipe(fs.createWriteStream(to, {encoding: "binary"})) - res.body.once("finish", resolve) - }) - } - await Promise.all([ - download("https://ezgif.com/images/format-demo/butterfly.png", "test/res/butterfly.png"), - download("https://ezgif.com/images/format-demo/butterfly.gif", "test/res/butterfly.gif") - ]) - t.pass("downloaded") - }) - } - /* c8 ignore end */ - require("../db/orm.test") require("../discord/utils.test") require("../matrix/kstate.test")