Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions packages/plugin-testops/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class TestOpsPlugin implements Plugin {
#launchTags: string[] = [];
#uploadedTestResultsIds: Set<string> = new Set();
#autocloseLaunch: boolean = false;
#launchStarted: boolean = false;
#gitFlow!: LaunchGitFlow;
#enabledByConfig: boolean = false;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<boolean> {
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) {
Expand All @@ -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}`);
Expand All @@ -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" });
Expand All @@ -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,
Expand All @@ -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;

Expand Down
75 changes: 70 additions & 5 deletions packages/plugin-testops/test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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)),
);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand All @@ -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)),
);
Expand Down
Loading