diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..af42e04 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,38 @@ +{ + // Use IntelliSense para saber los atributos posibles. + // Mantenga el puntero para ver las descripciones de los existentes atributos. + // Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "request": "launch", + "name": "Run Program", + "type": "node", + "program": "${workspaceFolder}/src/cli.ts", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "/home/juan/.deno/bin/deno", + "runtimeArgs": [ + "run", + "--unstable", + "--inspect", + "--allow-all" + ], + "attachSimplePort": 9229 + }, + { + "request": "launch", + "name": "Test Program", + "type": "node", + "program": "${workspaceFolder}/src/cli.ts", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "/home/juan/.deno/bin/deno", + "runtimeArgs": [ + "test", + "--unstable", + "--inspect", + "--allow-all" + ], + "attachSimplePort": 9229 + } + ] +} diff --git a/deno.jsonc b/deno.jsonc index eeee280..46d66be 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,7 @@ { "tasks": { "dev": "TEPI_NOT_EXIT=1 deno run -A --unstable --watch ./src/cli.ts", + "debug": "TEPI_NOT_EXIT=1 deno run --inspect-brk -A --unstable ./src/cli.ts ", // test "test": "NO_LOG=1 deno test -A --unstable", "test-watch": "FORCE_COLOR=1 deno test -A --unstable --watch test", diff --git a/http/needs.http b/http/needs.http index 1035384..c8adec1 100644 --- a/http/needs.http +++ b/http/needs.http @@ -45,5 +45,5 @@ x-quiet: true id: block5 needs: block_1 --- -GET /pong?status=204 +GET /pong?status=205 x-quiet: true diff --git a/http/needs.loop.http b/http/needs.loop.http index 9e1678c..42d8cc1 100644 --- a/http/needs.loop.http +++ b/http/needs.loop.http @@ -17,9 +17,11 @@ x-quiet: true --- id: block_2 needs: block_1 + --- POST /pong x-quiet: true +x-body-block1: <%= block_1.body || 'not found' %> <%= block_1.meta.id + '??' %> @@ -29,7 +31,6 @@ x-quiet: true --- id: block_3 needs: block_1 -# TODO DEBUG --- POST /pong x-quiet: true diff --git a/http/only.http b/http/only.http index c251af2..d7c528a 100644 --- a/http/only.http +++ b/http/only.http @@ -3,7 +3,7 @@ host: <%= Deno.env.get("HOST") || "https://faker.deno.dev "%> --- ### --- -id: 204 No Content +id: no ignored --- GET /pong?status=204 quiet: true @@ -12,7 +12,7 @@ HTTP/1.1 204 No Content ### --- -id: 203 Non-Authoritative Information +id: ignored only: <%= !!Deno.env.get('TEST_ONLY') %> --- diff --git a/src/cli.ts b/src/cli.ts index c675e32..2b331e1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -109,7 +109,7 @@ async function cli() { // resolves globs to file paths ///////////// - const globs: string = args._.length ? args._.join(" ") : "**/*.http"; + const globs: string = args._.length ? args._.join(" ") : 'http/need.loop.http'//"**/*.http"; const filePathsToRun = await globsToFilePaths(globs.split(" ")); // runner @@ -122,18 +122,18 @@ async function cli() { // warn only mode ///////////// - if (onlyMode.length) { + if (onlyMode.size) { if (getDisplayIndex(defaultMeta) > 0) { console.warn( fmt.yellow( `\n${fmt.bgYellow(fmt.bold(" ONLY MODE ")) - } ${onlyMode.length} tests are in "only" mode.`, + } ${onlyMode.size} tests are in "only" mode.`, ), ); if (!exitCode) { console.error( fmt.red( - `Failed because the ${fmt.bold('"only"')} option was used at ${onlyMode.join(", ") + `Failed because the ${fmt.bold('"only"')} option was used at ${[...onlyMode].join(", ") }`, ), ); diff --git a/src/print.ts b/src/print.ts index 4a0e1bc..2d69821 100644 --- a/src/print.ts +++ b/src/print.ts @@ -119,7 +119,8 @@ function metaToText(meta: Meta): string { return output; } -export function printErrorsSummary(blocks: Block[]): void { +export function printErrorsSummary(_blocks: Set): void { + const blocks = Array.from(_blocks); const blocksWidthErrors = blocks.filter((b) => b.error); if (blocksWidthErrors.length) { diff --git a/src/runner.ts b/src/runner.ts index c2a9fb2..9a91538 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -18,13 +18,13 @@ export async function runner( filePaths: string[], defaultMeta: Meta, failFast = false, -): Promise<{ files: File[]; exitCode: number; onlyMode: Set }> { +): Promise<{ files: File[]; exitCode: number; onlyMode: Set, blocksDone: Set }> { let successfulBlocks = 0; let failedBlocks = 0; let ignoredBlocks = 0; - const blocksDone: Block[] = []; + const blocksDone = new Set(); const startGlobalTime = Date.now(); const onlyMode = new Set(); @@ -46,7 +46,7 @@ export async function runner( }, _files: files, _blocksDone: {}, - _blocksAlreadyReferenced: {}, + _blocksAlreadyReferenced: new Set(), }; @@ -56,7 +56,7 @@ export async function runner( } catch (error) { console.error(`Error while parsing metadata`); console.error(error.message); - return { files, exitCode: 1, onlyMode }; + return { files, exitCode: 1, onlyMode, blocksDone }; } @@ -71,7 +71,7 @@ export async function runner( } for (const file of files) { - const relativePath = file.relativePath; + const relativePath = file.relativePath || ''; const path = fmt.dim(`running ${relativePath}`); const displayIndex = getDisplayIndex(defaultMeta); const pathSpinner = logPath(path, displayIndex); @@ -82,9 +82,7 @@ export async function runner( if (_isFirstBlock) { _isFirstBlock = false; } - const [...blocks] = await runBlock(block, globalData, relativePath); - blocksDone.push(...blocks); - + await runBlock(block, globalData, relativePath, blocksDone); if (block.meta._isIgnoredBlock) { ignoredBlocks++; } @@ -105,6 +103,7 @@ export async function runner( files, exitCode: status, onlyMode, + blocksDone }; } } @@ -130,123 +129,157 @@ export async function runner( ); } globalData._blocksDone = {}; // clean up blocks referenced - return { files, exitCode: failedBlocks, onlyMode }; + return { files, exitCode: failedBlocks, onlyMode, blocksDone }; } +function addToDone(blocksDone: Set, block: Block) { + if (blocksDone.has(block)) { + console.trace("Block already done: " + block.description); + throw new Error("Block already done: " + block.description); + } + if (block.meta._isDoneBlock) { + console.trace("Block already _isDoneBlock: " + block.description); + throw new Error("Block already _isDoneBlock"); + } + block.meta._isDoneBlock = true; + blocksDone.add(block); + // console.log(fmt.cyan("added to blocksDone"), block.description, block.meta._isEmptyBlock ? "empty" : block.meta._isIgnoredBlock ? "ignored" : block.meta._isFailedBlock ? "failed" : block.meta._isSuccessfulBlock ? "successful" : "?????"); +} async function runBlock( block: Block, globalData: GlobalData, currentFilePath: string, -): Promise { - const blocksDone = [block]; - if (block.meta._isDoneBlock) { - console.log("block is done", block.meta.id); - return []; - } - let spinner: Logger | undefined; + blocksDone: Set, +): Promise> { try { - if (block.meta.ignore) { - block.meta._isIgnoredBlock = true; - logBlock(block, currentFilePath).ignore(); + // console.group(); + // console.log(fmt.magenta("runBlock"), block.description); + + + if (blocksDone.has(block)) { + // console.log(fmt.green("already done"), block.description); return blocksDone; } - if (block.meta.needs) { - const blockReferenced = globalData._files.flatMap((file) => file.blocks) - .find((b) => b.meta.id === block.meta.needs); - if (!blockReferenced) { - logBlock(block, currentFilePath).fail(); - throw new Error(`Block referenced not found: ${block.meta.needs}`); - } else { - // Evict infinity loop - if ( - globalData - ._blocksAlreadyReferenced[blockReferenced.meta.id as string] - ) { - return []; - // throw new Error(`Block referenced already referenced: ${block.meta.needs}`); + let spinner: Logger | undefined; + try { + if (block.meta.ignore && !block.meta._isEmptyBlock) { + block.meta._isIgnoredBlock = true; + addToDone(blocksDone, block); + logBlock(block, currentFilePath).ignore(); + return blocksDone; + } + + if (block.meta.needs) { + const blockReferenced = globalData._files.flatMap((file) => file.blocks) + .find((b) => b.meta.id === block.meta.needs); + if (!blockReferenced) { + logBlock(block, currentFilePath).fail(); + throw new Error(`Block needed not found: ${block.meta.needs}`); + } else { + // Evict infinity loop + if (!globalData._blocksAlreadyReferenced.has(blockReferenced)) { + globalData._blocksAlreadyReferenced.add(blockReferenced); + await runBlock(blockReferenced, globalData, currentFilePath, blocksDone); + } else { + // console.log(fmt.brightRed("Evict Infinite loop"),blockReferenced.description,`${block.description} needs ${block.meta.needs}`); + throw new Error(`Infinite loop looking for needed blocks -> ${block.description} needs ${block.meta.needs}`); + // globalData._blocksDone[blockReferenced.meta.id] = blockReferenced; + // // return blocksDone; + // if (blocksDone.has(blockReferenced)) { + // return blocksDone; + // } + } } - globalData._blocksAlreadyReferenced[block.meta.needs as string] = - blockReferenced; - const [...blocks] = await runBlock(blockReferenced, globalData, currentFilePath); - blocksDone.push(...blocks); } - } - spinner = logBlock(block, currentFilePath); + if (blocksDone.has(block)) return blocksDone; + spinner = logBlock(block, currentFilePath); - block.meta = { - ...globalData.meta, - ...block.meta, - }; - block.request = await parseRequestFromText(block, { - ...globalData._blocksDone, - ...block, - ...assertions, - }); + block.meta = { + ...globalData.meta, + ...block.meta, + }; - spinner.update(); + block.request = await parseRequestFromText(block, { + ...globalData._blocksDone, + ...block, + ...assertions, + }); + spinner.update(); - if (block.meta._isFirstBlock && !block.request) { - globalData.meta = { ...globalData.meta, ...block.meta }; - } - if (!block.request) { - block.meta._isEmptyBlock = true; - spinner.empty(); - return blocksDone; - } + if (block.meta._isFirstBlock && !block.request) { + globalData.meta = { ...globalData.meta, ...block.meta }; + } + + if (!block.request) { + spinner.empty(); + addToDone(blocksDone, block); + block.meta._isEmptyBlock = true; + return blocksDone; + } - if (block.error) { - throw block.error; - } + if (block.error) { + throw block.error; + } - await fetchBlock(block); - block.expectedResponse = await parseResponseFromText( - block.text, - { - ...globalData._blocksDone, - ...block, - ...assertions, - body: await block.actualResponse?.getBody(), - // body: block.body, - response: block.response, - }, - ); + // console.log(fmt.brightBlue("fetch"), block.description, !!block.meta._isDoneBlock, blocksDone.has(block)); + await fetchBlock(block); - if (block.expectedResponse) { - await assertResponse(block); - } + block.expectedResponse = await parseResponseFromText( + block.text, + { + ...globalData._blocksDone, + ...block, + ...assertions, + body: await block.actualResponse?.getBody(), + // body: block.body, + response: block.response, + }, + ); + + if (block.expectedResponse) { + await assertResponse(block); + } - block.meta._isSuccessfulBlock = true; - spinner.pass(); - return blocksDone; - } catch (error) { - block.error = error; - block.meta._isFailedBlock = true; - spinner?.fail(); - return blocksDone; - } finally { - await printBlock(block); - await consumeBodies(block); - block.meta._isDoneBlock = true; - if (block.meta.id) { - const name = block.meta.id as string; - block.body = await block.actualResponse?.getBody(); - globalData._blocksDone[name] = block; + block.meta._isSuccessfulBlock = true; + spinner.pass(); + addToDone(blocksDone, block); + return blocksDone; + } catch (error) { + block.error = error; + block.meta._isFailedBlock = true; + spinner?.fail(); + addToDone(blocksDone, block); + return blocksDone; + } finally { + await printBlock(block); + await consumeBodies(block); + + + if (block.meta.id) { + const name = block.meta.id as string; + block.body = await block.actualResponse?.getBody(); + globalData._blocksDone[name] = block; + } } + } finally { + // console.groupEnd(); + + } } -async function processMetadata(files: File[], globalData: GlobalData, onlyMode: Set, mustBeImported: Set, blocksDone: Block[]) { +async function processMetadata(files: File[], globalData: GlobalData, onlyMode: Set, mustBeImported: Set, blocksDone: Set) { for (const file of files) { file.relativePath = './' + relative(Deno.cwd(), file.path); for (const block of file.blocks) { @@ -277,8 +310,8 @@ async function processMetadata(files: File[], globalData: GlobalData, onlyMode: }; } catch (error) { block.error = error; - block.meta._isDoneBlock = true; - blocksDone.push(block); + block.meta._isFailedBlock = true; + addToDone(blocksDone, block); } } } diff --git a/src/types.ts b/src/types.ts index a321520..8d460c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,6 +56,7 @@ export type Meta = { _filePath?: string; _relativeFilePath?: string; + _isEmptyBlock?: boolean; _isDoneBlock?: boolean; _isSuccessfulBlock?: boolean; _isFailedBlock?: boolean; @@ -116,9 +117,7 @@ export type File = { export type GlobalData = { meta: Meta; _files: File[]; - _blocksAlreadyReferenced: { - [key: string]: Block; - }; + _blocksAlreadyReferenced: Set _blocksDone: { [key: string]: Block; }; diff --git a/test/runner.test.ts b/test/runner.test.ts index ab3eccf..36aa11d 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -139,35 +139,55 @@ Deno.test("[runner] timeout", async () => { assertEquals(thirdBlock.error?.message, "Timeout of 100ms exceeded"); }); -Deno.test("[runner] needs", async () => { - const { files } = await runner(["http/needs.http"], { - display: "none", - }); +Deno.test("[runner] needs", - const firstBlock = files[0].blocks[1]; - assertEquals(firstBlock.meta.id, "block1"); - assertEquals(firstBlock.meta._isDoneBlock, true); - assertEquals(firstBlock.error, undefined); - assertEquals(await firstBlock.request?.getBody(), "RESPONSE!?"); -}); + async () => { + const { files, blocksDone } = await runner(["http/needs.http"], { + display: "none", + }); + + const firstBlock = files[0].blocks[1]; + assertEquals(firstBlock.meta.id, "block1"); + assertEquals(firstBlock.meta._isDoneBlock, true); + assertEquals(firstBlock.error, undefined); + assertEquals(await firstBlock.request?.getBody(), "RESPONSE!?"); + const blockInOrder = [...blocksDone] + + assertEquals(blockInOrder[0].description, "./http/needs.http:0"); + assertEquals(blockInOrder[1].description, "block3"); + assertEquals(blockInOrder[2].description, "block2"); + assertEquals(blockInOrder[3].description, "block1"); + assertEquals(blockInOrder[4].description, "block4"); + assertEquals(blockInOrder[5].description, "block5"); -Deno.test("[runner] needs loop", async () => { - const { files } = await runner(["http/needs.loop.http"], { - display: "none", }); - const firstBlock = files[0].blocks[1]; - assertEquals(firstBlock.meta.id, "block1"); - assertEquals(firstBlock.meta._isDoneBlock, true); - assertEquals(firstBlock.error, undefined); - assertEquals(await firstBlock.request?.getBody(), "block2?"); +Deno.test("[runner] needs loop", + async () => { + const { files, blocksDone } = await runner(["http/needs.loop.http"], { + display: "none", + }); - const secondBlock = files[0].blocks[1 + 1]; - assertEquals(secondBlock.meta.id, "block2"); - assertEquals(secondBlock.meta._isDoneBlock, true); - assertEquals(secondBlock.error, undefined); - assertEquals(await secondBlock.request?.getBody(), "block1??"); -}); + const firstBlock = files[0].blocks[1]; + assertEquals(firstBlock.meta.id, "block_1"); + assertEquals(firstBlock.meta._isDoneBlock, true); + assertEquals(firstBlock.error?.message.startsWith('Infinite loop'), true); + + const secondBlock = files[0].blocks[2]; + assertEquals(secondBlock.meta.id, "block_2"); + assertEquals(secondBlock.meta._isDoneBlock, true); + assertEquals(secondBlock.error, undefined); + assertEquals(await secondBlock.request?.getBody(), "block_1??"); + assertEquals(secondBlock.request?.headers.get('x-body-block1'), "not found"); + + const blockInOrder = [...blocksDone] + // console.log(blockInOrder.map((b) => b.description)); + assertEquals(blockInOrder[0].description, "./http/needs.loop.http:0"); + assertEquals(blockInOrder[1].description, "block_1"); + assertEquals(blockInOrder[2].description, "block_2"); + assertEquals(blockInOrder[3].description, "block_3"); + assertEquals(blockInOrder[4]?.description, undefined); + }); Deno.test( "[runner] redirect ", @@ -282,11 +302,11 @@ Deno.test( Deno.test( "[runner] logger", async () => { - const { exitCode } = await runner([ + const { exitCode } = await runner([ Deno.cwd() + "/http/timeout.http", ], { display: "none", }); - assertEquals(exitCode, 1) + assertEquals(exitCode, 2) }, );