Skip to content
Open
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
153 changes: 153 additions & 0 deletions packages/cli/test/commands/open.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { spawn } from "node:child_process";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

import { epic, feature, label, story } from "allure-js-commons";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

const commandsDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(commandsDir, "../../../..");
const cliPath = join(repoRoot, "packages", "cli", "cli.js");
const yarnRcPath = join(repoRoot, ".yarnrc.yml");

const workspaces = new Set<string>();

const resolveYarnInvocation = async () => {
const { readFile, stat } = await import("node:fs/promises");
const yarnRc = await readFile(yarnRcPath, "utf-8");
const configuredYarnPath = /^yarnPath:\s+(.+)$/m.exec(yarnRc)?.[1]?.trim();

if (configuredYarnPath) {
const resolvedYarnPath = resolve(repoRoot, configuredYarnPath);

try {
await stat(resolvedYarnPath);

return {
command: process.execPath,
args: [resolvedYarnPath],
};
} catch {
// fall through
}
}

return {
command: process.platform === "win32" ? "yarn.cmd" : "yarn",
args: [],
};
};

const waitForOutput = (pattern: RegExp, stdout: NodeJS.ReadableStream | null, timeoutMs = 30_000) =>
new Promise<string>((resolve, reject) => {
let output = "";
const timeout = setTimeout(() => {
cleanup();
reject(new Error(`Timed out waiting for ${pattern}`));
}, timeoutMs);

const onData = (chunk: Buffer | string) => {
output += chunk.toString();
if (pattern.test(output)) {
cleanup();
resolve(output);
}
};

const onError = (error: Error) => {
cleanup();
reject(error);
};

const cleanup = () => {
clearTimeout(timeout);
stdout?.off("data", onData);
stdout?.off("error", onError);
};

stdout?.on("data", onData);
stdout?.on("error", onError);
});

afterEach(async () => {
await Promise.all(
[...workspaces].map(async (workspace) => {
await rm(workspace, { recursive: true, force: true });
}),
);
workspaces.clear();
});

describe("open command integration", () => {
beforeEach(async () => {
await epic("coverage");
await feature("cli-commands");
await story("open.integration");
await label("coverage", "cli-commands");
});

it("serve with historyPath keeps the HTTP server running", async () => {
const workspace = await mkdtemp(join(tmpdir(), "allure-serve-history-"));
workspaces.add(workspace);

await mkdir(join(workspace, "allure-results"), { recursive: true });
await writeFile(
join(workspace, "allure-results", "x-result.json"),
`${JSON.stringify({
uuid: "11111111-1111-1111-1111-111111111111",
historyId: "abc",
name: "dummy test",
status: "passed",
stage: "finished",
start: 1_784_900_000_000,
stop: 1_784_900_001_000,
labels: [],
steps: [],
parameters: [],
links: [],
})}\n`,
"utf-8",
);
await writeFile(
join(workspace, "allurerc.mjs"),
`export default {
name: "repro",
historyPath: "./allure-history.jsonl",
};
`,
"utf-8",
);

const yarnInvocation = await resolveYarnInvocation();
const child = spawn(
yarnInvocation.command,
[...yarnInvocation.args, "node", cliPath, "serve", "allure-results", "--cwd", workspace],
{
cwd: repoRoot,
env: {
...process.env,
NODE_NO_WARNINGS: "1",
},
stdio: ["ignore", "pipe", "pipe"],
},
);

try {
const output = await waitForOutput(/Allure is running on http:\/\/localhost:\d+/, child.stdout);
expect(output).toMatch(/Allure is running on http:\/\/localhost:\d+/);
expect(child.killed).toBe(false);
} finally {
child.kill("SIGTERM");
await new Promise<void>((resolve) => {
child.once("exit", () => resolve());
setTimeout(() => {
child.kill("SIGKILL");
resolve();
}, 5_000).unref();
});
}
}, 60_000);
});
11 changes: 8 additions & 3 deletions packages/core/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { once } from "node:events";
import { type FileHandle, mkdir, open } from "node:fs/promises";
import path from "node:path";
import readline from "node:readline/promises";
import { pipeline } from "node:stream/promises";
import { finished, pipeline } from "node:stream/promises";

import {
type AllureHistory,
Expand Down Expand Up @@ -137,8 +137,6 @@ export class AllureLocalHistory implements AllureHistory {
const { file: historyFile, exists: historyExists } = await this.#ensureFileOpenedToAppend(fullPath);

try {
const dst = historyFile.createWriteStream({ encoding: "utf-8", start: 0, autoClose: false });

if (limit === 0 && historyExists) {
await historyFile.truncate(0);
return;
Expand All @@ -148,6 +146,8 @@ export class AllureLocalHistory implements AllureHistory {
return;
}

const dst = historyFile.createWriteStream({ encoding: "utf-8", start: 0, autoClose: false });

if (historyExists) {
// move up to `limit-1` most recent entries to the beginning of the file
const start = await this.#findFirstEntryAddress(historyFile, limit ? limit - 1 : undefined);
Expand All @@ -164,6 +164,11 @@ export class AllureLocalHistory implements AllureHistory {
if (historyExists) {
await historyFile.truncate(dst.bytesWritten);
}

// FileHandle-backed write streams keep the handle ref'd until destroyed.
// Closing the handle before that makes Node exit with a pending appendHistory promise (see nodejs/node#48466).
await finished(dst);
dst.destroy();
} finally {
await historyFile.close();

Expand Down
54 changes: 52 additions & 2 deletions packages/core/test/history.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { constants } from "node:buffer";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import { appendFile, open, readFile, rm, writeFile } from "node:fs/promises";
import { appendFile, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path/posix";
import { promisify } from "node:util";

import type { HistoryDataPoint, TestCase, TestResult } from "@allurereport/core-api";
import { epic, feature, label, story } from "allure-js-commons";
Expand All @@ -11,6 +13,8 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from
import { AllureLocalHistory, createHistory } from "../src/history.js";
import { getDataPath } from "./utils.js";

const execFileAsync = promisify(execFile);

beforeEach(async () => {
await epic("coverage");
await feature("history");
Expand Down Expand Up @@ -283,7 +287,7 @@ describe("AllureLocalHistory", () => {
});

afterEach(async () => {
await rm(historyPath);
await rm(historyPath, { force: true });
});

it("should create empty file if limit is zero", async () => {
Expand All @@ -302,6 +306,52 @@ describe("AllureLocalHistory", () => {
await checkHistoryFile(["New entry"]);
});

it("should resolve appendHistory in a standalone node process", async () => {
const workDir = join(tmpdir(), randomUUID());
const historyFilePath = join(workDir, "history.jsonl");
const scriptPath = join(workDir, "append-history.mjs");
const line = `${JSON.stringify({ ...entry, name: "Standalone entry" })}\n`;

await mkdir(workDir, { recursive: true });
await writeFile(
scriptPath,
`
import { finished, pipeline } from "node:stream/promises";
import { mkdir, open } from "node:fs/promises";
import { dirname } from "node:path";

const historyPath = ${JSON.stringify(historyFilePath)};
await mkdir(dirname(historyPath), { recursive: true });
const historyFile = await open(historyPath, "w");

try {
const dst = historyFile.createWriteStream({ encoding: "utf-8", start: 0, autoClose: false });
await pipeline([${JSON.stringify(line)}], dst);
await finished(dst);
dst.destroy();
} finally {
await historyFile.close();
}

console.log("append-complete");
`.trim(),
"utf-8",
);

try {
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
cwd: workDir,
timeout: 5000,
env: { ...process.env, NODE_NO_WARNINGS: "1" },
});

expect(stdout.trim()).toBe("append-complete");
await expect(readFile(historyFilePath, "utf-8")).resolves.toBe(line);
} finally {
await rm(workDir, { recursive: true, force: true });
}
});

it("should write entry to new file if limit is positive", async () => {
const history = new AllureLocalHistory({ historyPath, limit: 1 });

Expand Down