diff --git a/packages/plugin-testops/src/plugin.ts b/packages/plugin-testops/src/plugin.ts index 532e5b1cc30..f3a466a0017 100644 --- a/packages/plugin-testops/src/plugin.ts +++ b/packages/plugin-testops/src/plugin.ts @@ -38,6 +38,7 @@ export class TestOpsPlugin implements Plugin { #launchTags: string[] = []; #uploadedTestResultsIds: Set = new Set(); #autocloseLaunch: boolean = false; + #launchStarted: boolean = false; #gitFlow!: LaunchGitFlow; #enabledByConfig: boolean = false; @@ -369,7 +370,21 @@ export class TestOpsPlugin implements Plugin { return; } - await this.#client.createSession(env); + try { + await this.#client.createSession(env); + } catch (error) { + if (this.#client.isTestOpsClientError(error)) { + this.#logger.error(`Failed to create TestOps session: ${error.response.data.message}`); + this.#logger.debug(error.response?.data); + } else if (error instanceof Error) { + this.#logger.error(`Failed to create TestOps session: ${error.message}`); + } else { + this.#logger.error("Failed to create TestOps session"); + } + + return; + } + await this.#uploadGlobalAttachments(store); await this.#uploadGlobalErrors(store); await this.#uploadQualityGateResults(store); @@ -516,12 +531,30 @@ export class TestOpsPlugin implements Plugin { } } - async #startUpload() { + /** + * Creates the launch and starts the CI upload session. Unlike the per-content upload + * methods, a failure here means there's no launch to upload anything to at all, so it's + * caught and reported rather than left to crash the report generation for every plugin. + */ + async #startUpload(): Promise { const launchGitContext = this.#gitFlow.resolve(); - await this.#client.createLaunch(this.#launchName, this.#launchTags, launchGitContext); + try { + await this.#client.createLaunch(this.#launchName, this.#launchTags, launchGitContext); + await this.#client.startUpload(this.#ci!); + this.#launchStarted = true; + } catch (error) { + if (this.#client.isTestOpsClientError(error)) { + this.#logger.error(`Failed to create TestOps launch: ${error.response.data.message}`); + this.#logger.debug(error.response?.data); + } else if (error instanceof Error) { + this.#logger.error(`Failed to create TestOps launch: ${error.message}`); + } else { + this.#logger.error("Failed to create TestOps launch"); + } + } - await this.#client.startUpload(this.#ci!); + return this.#launchStarted; } async #stopUpload(status: TestStatus) { @@ -535,7 +568,10 @@ export class TestOpsPlugin implements Plugin { this.#logger.verbose("Starting upload…"); - await this.#startUpload(); + if (!(await this.#startUpload())) { + return; + } + await this.#upload(store, { context, stage: "start" }); this.#logger.info(`Allure TestOps Launch: ${this.#client.launchUrl}`); @@ -546,6 +582,11 @@ export class TestOpsPlugin implements Plugin { return; } + if (!this.#launchStarted) { + this.#logger.verbose("Skipping update: the TestOps launch was never started"); + return; + } + this.#logger.verbose("Updating (uploading new results)…"); await this.#upload(store, { context, stage: "update" }); @@ -556,6 +597,11 @@ export class TestOpsPlugin implements Plugin { return; } + if (!this.#launchStarted) { + this.#logger.verbose("Skipping finalization: the TestOps launch was never started"); + return; + } + const allTrs = await store.allTestResults({ filter: this.options.filter, includeRetries: false, @@ -566,7 +612,19 @@ export class TestOpsPlugin implements Plugin { this.#logger.verbose("Finalizing upload…"); await this.#upload(store, { context, stage: "done" }); - await this.#stopUpload(worstStatus || "unknown"); + + try { + await this.#stopUpload(worstStatus || "unknown"); + } catch (error) { + if (this.#client.isTestOpsClientError(error)) { + this.#logger.error(`Failed to stop TestOps upload: ${error.response.data.message}`); + this.#logger.debug(error.response?.data); + } else if (error instanceof Error) { + this.#logger.error(`Failed to stop TestOps upload: ${error.message}`); + } else { + this.#logger.error("Failed to stop TestOps upload"); + } + } const launchId = this.#client.launchId; diff --git a/packages/plugin-testops/test/plugin.test.ts b/packages/plugin-testops/test/plugin.test.ts index 86aecdc611b..a14386d45ee 100644 --- a/packages/plugin-testops/test/plugin.test.ts +++ b/packages/plugin-testops/test/plugin.test.ts @@ -1045,6 +1045,12 @@ describe("testops plugin", () => { }); it("should create new session", async () => { + // nothing to upload during start(), so the test result isn't marked as already uploaded + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); @@ -1057,6 +1063,12 @@ describe("testops plugin", () => { }); it("should upload test results", async () => { + // nothing to upload during start(), so the test result isn't marked as already uploaded + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); @@ -1129,6 +1141,9 @@ describe("testops plugin", () => { AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); AllureStoreMock.prototype.fixturesByTrId.mockResolvedValue([]); + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + await plugin.update({} as PluginContext, store); expect(TestOpsClientMock.prototype.createLaunch).toHaveBeenCalledTimes(0); @@ -1148,6 +1163,12 @@ describe("testops plugin", () => { plugin = new TestOpsPlugin({ filter } as TestOpsPluginOptions); + // nothing to upload during start(), so the target result isn't marked as already uploaded + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockImplementation(async (options: any = {}) => fixtures.testResults.filter(options.filter ?? (() => true)), ); @@ -1191,6 +1212,12 @@ describe("testops plugin", () => { store = new AllureStoreMock() as unknown as AllureStore; plugin = new TestOpsPlugin({} as TestOpsPluginOptions); + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + await plugin.done({ reportUuid: "test-uuid" } as PluginContext, store); expect(TestOpsClientMock.prototype.stopUpload).toHaveBeenCalledTimes(1); @@ -1237,6 +1264,12 @@ describe("testops plugin", () => { }); it("should create new session", async () => { + // nothing to upload during start(), so the test result isn't marked as already uploaded + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); @@ -1249,6 +1282,12 @@ describe("testops plugin", () => { }); it("should upload test results", async () => { + // nothing to upload during start(), so the test result isn't marked as already uploaded + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); @@ -1268,6 +1307,11 @@ describe("testops plugin", () => { }); it("should not call createLaunch on done", async () => { + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); @@ -1279,6 +1323,11 @@ describe("testops plugin", () => { }); it("should call closeLaunch when launchId is set", async () => { + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); @@ -1300,6 +1349,11 @@ describe("testops plugin", () => { }); it("should retry launch progress polling before closing", async () => { + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); @@ -1329,11 +1383,6 @@ describe("testops plugin", () => { }); it("should not check progress or close when autocloseLaunch is false", async () => { - AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); - AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); - AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); - AllureStoreMock.prototype.fixturesByTrId.mockResolvedValue([]); - (resolvePluginOptions as Mock).mockReturnValue({ accessToken: fixtures.accessToken, endpoint: fixtures.endpoint, @@ -1345,6 +1394,16 @@ describe("testops plugin", () => { plugin = new TestOpsPlugin({} as TestOpsPluginOptions); + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + + AllureStoreMock.prototype.allTestResults.mockResolvedValue(fixtures.testResults.slice(0, 1)); + AllureStoreMock.prototype.attachmentsByTrId.mockResolvedValue([]); + AllureStoreMock.prototype.attachmentContentById.mockResolvedValue(fixtures.attachmentContent); + AllureStoreMock.prototype.fixturesByTrId.mockResolvedValue([]); + await plugin.done({} as PluginContext, store); expect(TestOpsClientMock.prototype.checkLaunchProgress).not.toHaveBeenCalled(); @@ -1365,6 +1424,12 @@ describe("testops plugin", () => { plugin = new TestOpsPlugin({ filter } as TestOpsPluginOptions); + // nothing to upload during start(), so the target result isn't marked as already uploaded + AllureStoreMock.prototype.allTestResults.mockResolvedValue([]); + + await plugin.start({} as PluginContext, store); + vi.clearAllMocks(); + AllureStoreMock.prototype.allTestResults.mockImplementation(async (options: any = {}) => fixtures.testResults.filter(options.filter ?? (() => true)), );