diff --git a/package.json b/package.json index 15fe6bc..7bb7386 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qas-cli", - "version": "0.4.3", + "version": "0.4.4", "description": "QAS CLI is a command line tool for submitting your automation test results to QA Sphere at https://qasphere.com/", "type": "module", "main": "./build/bin/qasphere.js", diff --git a/src/api/run.ts b/src/api/run.ts index ad2ee66..3d4d872 100644 --- a/src/api/run.ts +++ b/src/api/run.ts @@ -1,4 +1,4 @@ -import { CreateResultRequest, ResourceId, RunTCase } from './schemas' +import { CreateResultsRequest, ResourceId, RunTCase } from './schemas' import { jsonResponse, withJson } from './utils' export interface CreateRunRequest { @@ -24,16 +24,16 @@ export const createRunApi = (fetcher: typeof fetch) => { fetcher(`/api/public/v0/project/${projectCode}/run/${runId}/tcase`) .then((r) => jsonResponse<{ tcases: RunTCase[] }>(r)) .then((r) => r.tcases), - createResultStatus: ( + + createResults: ( projectCode: ResourceId, runId: ResourceId, - tcaseId: ResourceId, - req: CreateResultRequest + req: CreateResultsRequest ) => - fetcher(`/api/public/v0/project/${projectCode}/run/${runId}/tcase/${tcaseId}/result`, { + fetcher(`/api/public/v0/project/${projectCode}/run/${runId}/result/batch`, { body: JSON.stringify(req), method: 'POST', - }).then((r) => jsonResponse<{ id: number }>(r)), + }).then((r) => jsonResponse<{ ids: number[] }>(r)), createRun: (projectCode: ResourceId, req: CreateRunRequest) => fetcher(`/api/public/v0/project/${projectCode}/run`, { diff --git a/src/api/schemas.ts b/src/api/schemas.ts index da1f961..903ff48 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -19,7 +19,12 @@ export interface RunTCase { folder: Folder } -export interface CreateResultRequest { +export interface CreateResultsRequestItem { + tcaseId: string status: ResultStatus comment?: string } + +export interface CreateResultsRequest { + items: CreateResultsRequestItem[] +} diff --git a/src/tests/result-upload.spec.ts b/src/tests/result-upload.spec.ts index fcc8b60..fa2e1fb 100644 --- a/src/tests/result-upload.spec.ts +++ b/src/tests/result-upload.spec.ts @@ -4,6 +4,7 @@ import { setupServer } from 'msw/node' import { HttpResponse, http } from 'msw' import { runTestCases } from './fixtures/testcases' import { countMockedApiCalls } from './utils' +import { setMaxResultsInRequest } from '../utils/result-upload/ResultUploader' const projectCode = 'TEST' const runId = '1' @@ -56,11 +57,11 @@ const server = setupServer( }) }), http.post( - new RegExp(`${baseURL}/api/public/v0/project/${projectCode}/run/${runId}/tcase/.+/result`), + new RegExp(`${baseURL}/api/public/v0/project/${projectCode}/run/${runId}/result/batch`), ({ request }) => { expect(request.headers.get('Authorization')).toEqual('ApiKey QAS_TOKEN') return HttpResponse.json({ - id: 0, + ids: [0], }) } ), @@ -83,12 +84,13 @@ afterAll(() => { afterEach(() => { server.resetHandlers() server.events.removeAllListeners() + setMaxResultsInRequest(50) }) const countFileUploadApiCalls = () => countMockedApiCalls(server, (req) => req.url.endsWith('/file')) const countResultUploadApiCalls = () => - countMockedApiCalls(server, (req) => new URL(req.url).pathname.endsWith('/result')) + countMockedApiCalls(server, (req) => new URL(req.url).pathname.endsWith('/result/batch')) const fileTypes = [ { @@ -116,22 +118,23 @@ fileTypes.forEach((fileType) => { ] for (const pattern of patterns) { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() await run(pattern) - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(5) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(1) // 5 results total } }) test('Passing correct Run URL pattern without https, should result in success', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() + setMaxResultsInRequest(1) await run( `${fileType.command} -r ${qasHost}/project/${projectCode}/run/${runId} ${fileType.dataBasePath}/matching-tcases.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(5) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(5) // 5 results total }) test('Passing incorrect Run URL pattern should result in failure', async () => { @@ -141,8 +144,8 @@ fileTypes.forEach((fileType) => { ] for (const pattern of patterns) { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() let isError = false try { @@ -151,105 +154,110 @@ fileTypes.forEach((fileType) => { isError = true } expect(isError).toBeTruthy() - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(0) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(0) } }) }) describe('Uploading test results', () => { test('Test cases on reports with all matching test cases on QAS should be successful', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() + setMaxResultsInRequest(2) await run( `${fileType.command} -r ${runURL} ${fileType.dataBasePath}/matching-tcases.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(5) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(3) // 5 results total }) test('Test cases on reports with a missing test case on QAS should throw an error', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() await expect( run( `${fileType.command} -r ${runURL} ${fileType.dataBasePath}/missing-tcases.${fileType.fileExtension}` ) ).rejects.toThrowError() - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(0) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(0) }) test('Test cases on reports with a missing test case on QAS should be successful when forced', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() + setMaxResultsInRequest(3) await run( `${fileType.command} -r ${runURL} --force ${fileType.dataBasePath}/missing-tcases.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(4) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(2) // 4 results total }) test('Test cases on reports with missing test cases should be successful with --ignore-unmatched', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() await run( `${fileType.command} -r ${runURL} --ignore-unmatched ${fileType.dataBasePath}/missing-tcases.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(4) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(1) // 4 results total }) test('Test cases from multiple reports should be processed successfully', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() + setMaxResultsInRequest(2) await run( `${fileType.command} -r ${runURL} --force ${fileType.dataBasePath}/missing-tcases.${fileType.fileExtension} ${fileType.dataBasePath}/missing-tcases.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(8) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(4) // 8 results total }) test('Test suite with empty tcases should not result in error and be skipped', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() await run( `${fileType.command} -r ${runURL} --force ${fileType.dataBasePath}/empty-tsuite.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(1) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(1) // 1 result total }) }) describe('Uploading with attachments', () => { test('Attachments should be uploaded', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() + setMaxResultsInRequest(3) await run( `${fileType.command} -r ${runURL} --attachments ${fileType.dataBasePath}/matching-tcases.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(5) - expect(tcaseUploadCount()).toBe(5) + expect(numFileUploadCalls()).toBe(5) + expect(numResultUploadCalls()).toBe(2) // 5 results total }) test('Missing attachments should throw an error', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() await expect( run( `${fileType.command} -r ${runURL} --attachments ${fileType.dataBasePath}/missing-attachments.${fileType.fileExtension}` ) ).rejects.toThrow() - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(0) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(0) }) test('Missing attachments should be successful when forced', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() + setMaxResultsInRequest(1) await run( `${fileType.command} -r ${runURL} --attachments --force ${fileType.dataBasePath}/missing-attachments.${fileType.fileExtension}` ) - expect(fileUploadCount()).toBe(4) - expect(tcaseUploadCount()).toBe(5) + expect(numFileUploadCalls()).toBe(4) + expect(numResultUploadCalls()).toBe(5) // 5 results total }) }) @@ -318,8 +326,8 @@ fileTypes.forEach((fileType) => { }) test('Should reuse existing run when run title is already used', async () => { - const fileUploadCount = countFileUploadApiCalls() - const tcaseUploadCount = countResultUploadApiCalls() + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() createRunTitleConflict = true await run( @@ -327,8 +335,8 @@ fileTypes.forEach((fileType) => { ) expect(lastCreatedRunTitle).toBe('duplicate run title') - expect(fileUploadCount()).toBe(0) - expect(tcaseUploadCount()).toBe(5) + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(1) // 5 results total }) test('Should use default name template when --run-name is not specified', async () => { diff --git a/src/utils/misc.ts b/src/utils/misc.ts index e485b0e..dd70186 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -40,6 +40,8 @@ export const twirlLoader = () => { if (timer) { clearInterval(timer) } + x = chars.length - 1 + update() process.stdout.write('\n') }, setText: (newText: string) => { diff --git a/src/utils/result-upload/ResultUploader.ts b/src/utils/result-upload/ResultUploader.ts index 7f11906..f6703f0 100644 --- a/src/utils/result-upload/ResultUploader.ts +++ b/src/utils/result-upload/ResultUploader.ts @@ -3,9 +3,12 @@ import chalk from 'chalk' import { RunTCase } from '../../api/schemas' import { parseRunUrl, printError, printErrorThenExit, twirlLoader } from '../misc' import { Api, createApi } from '../../api' -import { TestCaseResult } from './types' +import { Attachment, TestCaseResult } from './types' import { ResultUploadCommandArgs, UploadCommandType } from './ResultUploadCommandHandler' +const MAX_CONCURRENT_FILE_UPLOADS = 10 +let MAX_RESULTS_IN_REQUEST = 50 // Only updated from tests, otherwise it's a constant + export class ResultUploader { private api: Api private project: string @@ -137,38 +140,140 @@ ${chalk.yellow('To fix this issue, choose one of the following options:')} private uploadTestCases = async (results: TCaseWithResult[]) => { const loader = twirlLoader() - loader.start() try { - for (let i = 0; i < results.length; i++) { - const { tcase, result } = results[i] - let comment = result.message - loader.setText(`Uploading test case ${i + 1} of ${results.length}`) - if (this.args.attachments) { - const attachmentUrls: Array<{ name: string; url: string }> = [] - for (const attachment of result.attachments) { - if (attachment.buffer) { - const { url } = await this.api.file.uploadFile( - new Blob([attachment.buffer]), - attachment.filename - ) - attachmentUrls.push({ url, name: attachment.filename }) - } - } - if (attachmentUrls.length > 0) { - comment += `\n