Skip to content

Commit bed8801

Browse files
authored
add more tests for reader package (via #574)
1 parent 3f6e06f commit bed8801

9 files changed

Lines changed: 289 additions & 10 deletions

File tree

packages/reader/src/xcresult/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ export const readXcResultBundle = async (visitor: ResultsVisitor, directory: str
3535
return false;
3636
};
3737

38+
export const isXcResultToolAvailable = async () => {
39+
try {
40+
await version();
41+
return true;
42+
} catch {
43+
return false;
44+
}
45+
};
46+
3847
const maybeGetXcResultToolVersion = async () => {
3948
try {
4049
return await version();

packages/reader/src/xcresult/xcresulttool/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const parseWithExportedAttachments = async (
1919
try {
2020
attachmentsDir = await mkdtemp(path.join(tmpdir(), "allure-reader-xcresult-"));
2121
await exportAttachments(xcResultPath, attachmentsDir);
22-
await fn(createAttachmentFileFactoryFn(attachmentsDir));
22+
await fn(createAttachmentFileFactory(attachmentsDir));
2323
} finally {
2424
if (attachmentsDir) {
2525
try {
@@ -55,7 +55,7 @@ const timestampToString = (timestamp: number) => {
5555
});
5656
};
5757

58-
const createAttachmentFileFactoryFn =
58+
export const createAttachmentFileFactory =
5959
(attachmentsDir: string): AttachmentFileFactory =>
6060
async (attachmentUuid, uniqueFileName) => {
6161
const fileExtension = path.extname(uniqueFileName);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { BufferResultFile } from "@allurereport/reader-api";
2+
import { attachment, step } from "allure-js-commons";
3+
import { describe, expect, it } from "vitest";
4+
5+
import { attachments } from "../src/attachments/index.js";
6+
import { mockVisitor } from "./utils.js";
7+
8+
describe("attachments reader", () => {
9+
it("should pass attachment files through to the visitor", async () => {
10+
const visitor = mockVisitor();
11+
12+
const resultFile = await step("prepare an attachment result file", async () => {
13+
const file = new BufferResultFile(Buffer.from("attachment content", "utf-8"), "example.txt");
14+
await attachment("attachment-input.txt", Buffer.from("attachment content", "utf-8"), "text/plain");
15+
return file;
16+
});
17+
18+
const read = await step("read the attachment file", async () => {
19+
return await attachments.read(visitor, resultFile);
20+
});
21+
22+
await step("verify the attachment was forwarded unchanged", async () => {
23+
expect(read).toBe(true);
24+
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(1);
25+
expect(visitor.visitAttachmentFile).toHaveBeenCalledWith(resultFile, { readerId: "attachments" });
26+
});
27+
});
28+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { ensureArray, ensureBoolean, ensureInt, ensureObject, ensureString } from "../src/utils.js";
4+
5+
describe("reader utility helpers", () => {
6+
describe("ensureBoolean", () => {
7+
it("should keep boolean values and fall back for non-boolean values", () => {
8+
expect(ensureBoolean(true)).toBe(true);
9+
expect(ensureBoolean(false)).toBe(false);
10+
expect(ensureBoolean("true")).toBeUndefined();
11+
expect(ensureBoolean("true", true)).toBe(true);
12+
});
13+
});
14+
15+
describe("ensureInt", () => {
16+
it("should parse numbers and integer-like strings", () => {
17+
expect(ensureInt(42)).toBe(42);
18+
expect(ensureInt("42")).toBe(42);
19+
expect(ensureInt("42px")).toBe(42);
20+
expect(ensureInt("nope")).toBeUndefined();
21+
});
22+
});
23+
24+
describe("ensureString", () => {
25+
it("should keep strings and fall back for non-string values", () => {
26+
expect(ensureString("value")).toBe("value");
27+
expect(ensureString(42)).toBeUndefined();
28+
expect(ensureString(42, "fallback")).toBe("fallback");
29+
});
30+
});
31+
32+
describe("ensureArray", () => {
33+
it("should keep arrays and fall back for non-array values", () => {
34+
expect(ensureArray([1, 2, 3])).toEqual([1, 2, 3]);
35+
expect(ensureArray("value")).toBeUndefined();
36+
expect(ensureArray("value", ["fallback"])).toEqual(["fallback"]);
37+
});
38+
});
39+
40+
describe("ensureObject", () => {
41+
it("should keep non-null objects and fall back otherwise", () => {
42+
expect(ensureObject({ foo: "bar" })).toEqual({ foo: "bar" });
43+
expect(ensureObject(null)).toBeUndefined();
44+
expect(ensureObject(["not", "object"])).toEqual(["not", "object"]);
45+
expect(ensureObject(undefined, { fallback: true })).toEqual({ fallback: true });
46+
});
47+
});
48+
});

packages/reader/test/utils.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,57 @@ export const readResults = async (
4040
files: Record<string, string> = {},
4141
result: boolean = true,
4242
) => {
43-
return step("readResults", async () => {
43+
return step(`read ${reader.readerId()} results`, async () => {
4444
const visitor = mockVisitor();
45+
46+
await attachment(
47+
`${reader.readerId()}-inputs.json`,
48+
Buffer.from(
49+
JSON.stringify(
50+
{
51+
readerId: reader.readerId(),
52+
expectedResult: result,
53+
files: Object.entries(files).map(([resourcePath, originalFileName]) => ({
54+
resourcePath,
55+
originalFileName,
56+
})),
57+
},
58+
null,
59+
2,
60+
),
61+
"utf-8",
62+
),
63+
"application/json",
64+
);
65+
4566
for (const filesKey in files) {
46-
const resultFile = await readResourceAsResultFile(filesKey, files[filesKey]);
47-
await attachResultFile(resultFile);
48-
const read = await reader.read(visitor, resultFile);
49-
expect(read).toBe(result);
67+
await step(`read ${filesKey}`, async () => {
68+
const resultFile = await readResourceAsResultFile(filesKey, files[filesKey]);
69+
await attachResultFile(resultFile);
70+
const read = await reader.read(visitor, resultFile);
71+
expect(read).toBe(result);
72+
});
5073
}
74+
75+
await attachment(
76+
`${reader.readerId()}-visitor-summary.json`,
77+
Buffer.from(
78+
JSON.stringify(
79+
{
80+
testResults: visitor.visitTestResult.mock.calls.length,
81+
attachmentFiles: visitor.visitAttachmentFile.mock.calls.length,
82+
metadata: visitor.visitMetadata.mock.calls.length,
83+
fixtures: visitor.visitTestFixtureResult.mock.calls.length,
84+
globals: visitor.visitGlobals.mock.calls.length,
85+
},
86+
null,
87+
2,
88+
),
89+
"utf-8",
90+
),
91+
"application/json",
92+
);
93+
5194
return visitor;
5295
});
5396
};

packages/reader/test/xcresult.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { step } from "allure-js-commons";
77
import { beforeAll, describe, expect, it } from "vitest";
88

99
import { IS_MAC } from "../src/xcresult/bundle.js";
10-
import { readXcResultBundle } from "../src/xcresult/index.js";
10+
import { isXcResultToolAvailable, readXcResultBundle } from "../src/xcresult/index.js";
1111
import { attachResultDir, buildResourcePath, mockVisitor } from "./utils.js";
1212

1313
const filenamePatterns = {
1414
unnamed: /public\.data_\d_[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/,
1515
bar: /bar_\d_[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/,
1616
};
17+
const HAS_XCRESULTTOOL = IS_MAC && (await isXcResultToolAvailable());
1718

1819
const readXcResultResource = async (resourcePath: string, expectedResult: boolean = true) => {
1920
return await step("readXcResultBundle", async () => {
@@ -47,7 +48,16 @@ describe.skipIf(IS_MAC)("Not a MAC machine", () => {
4748
});
4849
});
4950

50-
describe.skipIf(!IS_MAC)("A MAC machine", { timeout: 10_000 }, () => {
51+
describe.skipIf(!IS_MAC || HAS_XCRESULTTOOL)("A MAC machine without xcresulttool", () => {
52+
it("should treat xcresult parsing as unsupported instead of failing downstream assertions", async () => {
53+
const result = await readXcResultResource("outcomes/passed.xcresult");
54+
55+
expect(result.visitAttachmentFile).not.toBeCalled();
56+
expect(result.visitTestResult).not.toBeCalled();
57+
});
58+
});
59+
60+
describe.skipIf(!HAS_XCRESULTTOOL)("A MAC machine with xcresulttool", { timeout: 10_000 }, () => {
5161
describe("attachments", () => {
5262
it("should parse a nameless test attachment", async () => {
5363
const result = await readXcResultResource("attachments/nameless.xcresult");
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import console from "node:console";
2+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import path from "node:path";
5+
6+
import { afterEach, describe, expect, it, vi } from "vitest";
7+
8+
import { findBundleInfoFile, followsXcResultNaming, isMostProbablyXcResultBundle } from "../src/xcresult/bundle.js";
9+
import { createAttachmentFileFactory, mapWellKnownAttachmentName } from "../src/xcresult/xcresulttool/utils.js";
10+
11+
describe("xcresult bundle helpers", () => {
12+
const tempDirs: string[] = [];
13+
14+
afterEach(async () => {
15+
await Promise.all(
16+
tempDirs.map(async (directory) => {
17+
await import("node:fs/promises").then(({ rm }) => rm(directory, { recursive: true, force: true }));
18+
}),
19+
);
20+
tempDirs.length = 0;
21+
});
22+
23+
const createTempDir = async (name: string) => {
24+
const parent = await mkdtemp(path.join(tmpdir(), "allure-reader-bundle-"));
25+
const directory = path.join(parent, name);
26+
await mkdir(directory, { recursive: true });
27+
tempDirs.push(directory);
28+
return directory;
29+
};
30+
31+
it("should detect xcresult bundles by naming convention and bundle metadata", async () => {
32+
const namedBundle = await createTempDir("named.xcresult");
33+
const bundleWithInfo = await createTempDir("bundle");
34+
const plainDirectory = await createTempDir("plain");
35+
36+
await mkdir(path.join(bundleWithInfo, "Contents"), { recursive: true });
37+
await writeFile(path.join(bundleWithInfo, "Contents", "Info.plist"), "plist", "utf-8");
38+
39+
expect(followsXcResultNaming(namedBundle)).toBe(true);
40+
expect(await findBundleInfoFile(bundleWithInfo)).toBe(path.join(bundleWithInfo, "Contents", "Info.plist"));
41+
expect(await isMostProbablyXcResultBundle(namedBundle)).toBe(true);
42+
expect(await isMostProbablyXcResultBundle(bundleWithInfo)).toBe(true);
43+
expect(await isMostProbablyXcResultBundle(plainDirectory)).toBe(false);
44+
});
45+
});
46+
47+
describe("xcresult attachment helpers", () => {
48+
const tempDirs: string[] = [];
49+
50+
afterEach(async () => {
51+
await Promise.all(
52+
tempDirs.map(async (directory) => {
53+
await import("node:fs/promises").then(({ rm }) => rm(directory, { recursive: true, force: true }));
54+
}),
55+
);
56+
tempDirs.length = 0;
57+
vi.restoreAllMocks();
58+
});
59+
60+
const createTempDir = async () => {
61+
const directory = await mkdtemp(path.join(tmpdir(), "allure-reader-attachments-"));
62+
tempDirs.push(directory);
63+
return directory;
64+
};
65+
66+
it("should read exported attachments by exact file path and extension fallback", async () => {
67+
const attachmentsDir = await createTempDir();
68+
const factory = createAttachmentFileFactory(attachmentsDir);
69+
70+
await writeFile(path.join(attachmentsDir, "uuid-plain"), "plain text", "utf-8");
71+
await writeFile(path.join(attachmentsDir, "uuid-image.png"), "png data", "utf-8");
72+
73+
const plain = await factory("uuid-plain", "plain.txt");
74+
const image = await factory("uuid-image", "image.png");
75+
76+
expect(plain?.getOriginalFileName()).toBe("plain.txt");
77+
expect(await plain?.asUtf8String()).toBe("plain text");
78+
expect(image?.getOriginalFileName()).toBe("image.png");
79+
expect(await image?.asUtf8String()).toBe("png data");
80+
});
81+
82+
it("should return undefined and log an actionable error when an attachment cannot be read", async () => {
83+
const attachmentsDir = await createTempDir();
84+
const factory = createAttachmentFileFactory(attachmentsDir);
85+
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
86+
87+
await expect(factory("missing-uuid", "missing.txt")).resolves.toBeUndefined();
88+
expect(consoleError).toHaveBeenCalledWith(
89+
"Can't read attachment",
90+
"missing-uuid",
91+
"in",
92+
attachmentsDir,
93+
":",
94+
expect.anything(),
95+
expect.anything(),
96+
);
97+
});
98+
99+
it("should map well-known automatic attachment names to readable titles", () => {
100+
expect(mapWellKnownAttachmentName("kXCTAttachmentScreenRecording", undefined)).toBe("Screen Recording");
101+
expect(mapWellKnownAttachmentName("kXCTAttachmentLegacyScreenImageData", undefined)).toBe("Screenshot");
102+
expect(mapWellKnownAttachmentName("kXCTAttachmentLegacyScreenImageData", 1_717_171_717_171)).toContain(
103+
"Screenshot at",
104+
);
105+
expect(mapWellKnownAttachmentName("Custom Attachment", undefined)).toBe("Custom Attachment");
106+
});
107+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import {
4+
cleanBadXmlCharacters,
5+
isBadXmlCharacter,
6+
isEmptyElement,
7+
isStringAnyRecord,
8+
isStringAnyRecordArray,
9+
} from "../src/xml-utils.js";
10+
11+
describe("xml utility helpers", () => {
12+
it("should recognize empty xml elements and plain string-keyed records", () => {
13+
expect(isEmptyElement("")).toBe(true);
14+
expect(isEmptyElement("not-empty")).toBe(false);
15+
expect(isStringAnyRecord({ foo: "bar" })).toBe(true);
16+
expect(isStringAnyRecord(["foo"])).toBe(false);
17+
expect(isStringAnyRecordArray([{ foo: "bar" }, { bar: "baz" }])).toBe(true);
18+
expect(isStringAnyRecordArray([{ foo: "bar" }, ["baz"]])).toBe(false);
19+
});
20+
21+
it("should identify and replace invalid xml characters", () => {
22+
expect(isBadXmlCharacter(0)).toBe(true);
23+
expect(isBadXmlCharacter(9)).toBe(false);
24+
expect(isBadXmlCharacter(10)).toBe(false);
25+
expect(isBadXmlCharacter(13)).toBe(false);
26+
27+
const input = Buffer.from([65, 0, 66, 9, 67, 255]);
28+
const cleaned = cleanBadXmlCharacters(Buffer.from(input));
29+
30+
expect(Array.from(cleaned)).toEqual([65, 32, 66, 9, 67, 255]);
31+
});
32+
});

packages/reader/vitest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { createRequire } from "node:module";
2+
import { env } from "node:process";
23
import { defineConfig } from "vitest/config";
34

45
const require = createRequire(import.meta.url);
6+
const resultsDir = env.ALLURE_RESULTS_DIR ?? "./out/allure-results";
57

68
export default defineConfig({
79
test: {
@@ -12,7 +14,7 @@ export default defineConfig({
1214
[
1315
"allure-vitest/reporter",
1416
{
15-
resultsDir: "./out/allure-results",
17+
resultsDir,
1618
globalLabels: [
1719
{ name: "module", value: "reader" },
1820
],

0 commit comments

Comments
 (0)