Skip to content

[INT-78] visual-snapshots: parallel processing of PDF pages #205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6159934
[INT-78] visual-snapshots: add parallel processing of PDF files
perf2711 Mar 12, 2025
fccd9e6
[INT-78] visual-snapshots: add parallel processing of PDF pages
perf2711 Mar 13, 2025
90394ff
[INT-78] visual-snapshots: fix error behavior for waitForEmptyQueue
perf2711 Mar 13, 2025
07fe61d
[INT-78] visual-snapshots: fix visual-api tests, make them more deter…
perf2711 Mar 13, 2025
60cbcc1
[INT-78] visual-snapshots: address MR comments
perf2711 Mar 14, 2025
4ba9a63
[INT-78] visual-snapshots: process pages before adding more files
perf2711 Mar 14, 2025
6eec823
[INT-78] visual-snapshots: add comment to setImmediate workers
perf2711 Mar 14, 2025
0cd5325
[INT-78] visual-snapshots: fix unit tests
perf2711 Mar 14, 2025
c5d52b7
[INT-78] visual-snapshots: move processing pdf file pages to workerpool
perf2711 Mar 17, 2025
a8821f8
[INT-78] visual-snapshots: remove old queue code
perf2711 Mar 17, 2025
1eb2a05
[INT-78] visual-snapshots: move clienVersion to a separate file
perf2711 Mar 20, 2025
5312c3e
[INT-78] visual-snapshots: refactor code, split responsibilities
perf2711 Mar 20, 2025
032efa8
[INT-78] visual-snapshots: add PdfCommandHandler tests
perf2711 Mar 20, 2025
d010c9b
[INT-78] visual-snapshots: rename LoadedPdfFile to PdfFile
perf2711 Mar 20, 2025
9a77a29
[INT-78] visual-snapshots: fix argv missing in worker
perf2711 Mar 20, 2025
57975c2
[INT-78] visual-snapshots: update path to PKG_VERSION file
perf2711 Mar 20, 2025
737188c
[INT-78] visual-snapshots: remove superfluous logging
perf2711 Mar 20, 2025
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
24 changes: 23 additions & 1 deletion visual-js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion visual-js/replace_pkg_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ FILE_MAP=(
["@saucelabs/nightwatch-sauce-visual-service"]="visual-nightwatch/src/utils/constants.ts"
["@saucelabs/visual-storybook"]="visual-storybook/src/api.ts"
["@saucelabs/wdio-sauce-visual-service"]="visual-wdio/src/SauceVisualService.ts"
["@saucelabs/visual-snapshots"]="visual-snapshots/src/index.ts"
["@saucelabs/visual-snapshots"]="visual-snapshots/src/version.ts"
["@saucelabs/visual-playwright"]="visual-playwright/src/api.ts"
# Add more mappings as needed
)
Expand Down
5 changes: 4 additions & 1 deletion visual-js/visual-snapshots/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@
},
"dependencies": {
"@saucelabs/visual": "^0.13.0",
"async-lock": "^1.4.1",
"commander": "^12.0.0",
"glob": "^11.0.1",
"pdf-to-img": "~4.4.0"
"pdf-to-img": "~4.4.0",
"workerpool": "^9.2.0"
},
"devDependencies": {
"@types/async-lock": "^1.4.2",
"@types/jest": "29.5.14",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
Expand Down
147 changes: 52 additions & 95 deletions visual-js/visual-snapshots/src/api/visual-snapshots-api.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,38 @@
import { BuildStatus, DiffingMethod, VisualApi } from "@saucelabs/visual";
import { formatString } from "../utils/format.js";
import path from "path";
import { PdfFile } from "../app/pdf-file.js";
import { __dirname } from "../utils/helpers.js";

export interface CreateVisualSnapshotsParams {
branch: string;
buildName: string;
defaultBranch: string;
project: string;
customId: string;
branch?: string;
buildName?: string;
defaultBranch?: string;
project?: string;
customId?: string;
buildId?: string;
suiteName?: string;
testName?: string;
snapshotName?: string;
}

export class VisualSnapshotsApi {
private api: VisualApi;

constructor(api: VisualApi) {
this.api = api;
}

public async generateAndSendPdfFileSnapshots(
pdfFiles: PdfFile[],
params: CreateVisualSnapshotsParams
) {
const buildId = params.buildId ?? (await this.createBuild(params));

for (const pdfFile of pdfFiles) {
console.info(`Processing file: ${pdfFile.path}`);

const filename = path.basename(pdfFile.path);
const testName = params.testName
? formatString(params.testName, { filename })
: undefined;

const snapshotFormat = this.getSnapshotFormat(params.snapshotName);

let pageNumber = 1;
for await (const pdfPageImage of pdfFile.convertPagesToImages()) {
const snapshotName = formatString(snapshotFormat, {
filename,
page: pageNumber,
});
export interface CreateBuildParams {
readonly buildName?: string;
readonly branch?: string;
readonly defaultBranch?: string;
readonly project?: string;
readonly customId?: string;
}

await this.uploadImageAndCreateSnapshot(
pdfPageImage,
buildId,
snapshotName,
testName,
params.suiteName
);
pageNumber++;
}
}
export interface UploadSnapshotParams {
readonly buildId: string;
readonly snapshot: Buffer;
readonly snapshotName: string;
readonly suiteName?: string;
readonly testName?: string;
}

await this.finishBuild(buildId);
}
export class VisualSnapshotsApi {
constructor(private readonly api: VisualApi) {}

private async createBuild(
params: CreateVisualSnapshotsParams
): Promise<string> {
public async createBuild(params: CreateBuildParams): Promise<string> {
const build = await this.api.createBuild({
name: params.buildName,
branch: params.branch,
Expand All @@ -73,62 +44,48 @@
return build.id;
}

private async uploadImageAndCreateSnapshot(
snapshot: Buffer,
buildId: string,
snapshotName: string,
testName?: string,
suiteName?: string
) {
const uploadId = await this.api.uploadSnapshot({
buildId,
image: { data: snapshot },
});

console.info(`Uploaded image to build ${buildId}: upload id=${uploadId}.`);

await this.api.createSnapshot({
buildId,
uploadId,
name: snapshotName,
diffingMethod: DiffingMethod.Balanced,
testName,
suiteName,
});

console.info(`Created a snapshot ${snapshotName} for build ${buildId}.`);
}

private async finishBuild(buildId: string) {
await this.api.finishBuild({
public async finishBuild(buildId: string) {
const { status: buildStatus } = await this.api.finishBuild({
uuid: buildId,
});
console.info(`Build ${buildId} finished.`);

const buildStatus = (await this.api.buildStatus(buildId))!;
if (
[BuildStatus.Running, BuildStatus.Queued].includes(buildStatus.status)
) {
if ([BuildStatus.Running, BuildStatus.Queued].includes(buildStatus)) {
console.info(
`Build ${buildId} finished but snapshots haven't been compared yet. Check the build status in a few moments.`
);
} else {
const { unapprovedCount, errorCount } = (await this.api.buildStatus(

Check warning on line 57 in visual-js/visual-snapshots/src/api/visual-snapshots-api.ts

View workflow job for this annotation

GitHub Actions / build

Forbidden non-null assertion
buildId
))!;
console.info(
`Build ${buildId} finished (status=${buildStatus.status}, unapprovedCount=${buildStatus.unapprovedCount}, errorCount=${buildStatus.errorCount}).`
`Build ${buildId} finished (status=${buildStatus}, unapprovedCount=${unapprovedCount}, errorCount=${errorCount}).`
);
}
}

private getSnapshotFormat(format: string | undefined) {
if (!format) {
return `page-{page}`;
}
public async uploadImageAndCreateSnapshot(params: UploadSnapshotParams) {
const uploadId = await this.api.uploadSnapshot({
buildId: params.buildId,
image: { data: params.snapshot },
});

// Page number is always required to make the snapshot names unique
if (!format.includes("{page}")) {
format = format += "-{page}";
}
console.info(
`Uploaded image to build ${params.buildId}: upload id=${uploadId}.`
);

await this.api.createSnapshot({
buildId: params.buildId,
uploadId,
name: params.snapshotName,
diffingMethod: DiffingMethod.Balanced,
testName: params.testName,
suiteName: params.suiteName,
});

console.info(
`Created a snapshot ${params.snapshotName} for build ${params.buildId}.`
);

return format;
return uploadId;
}
}
23 changes: 0 additions & 23 deletions visual-js/visual-snapshots/src/app/pdf-converter.ts

This file was deleted.

22 changes: 22 additions & 0 deletions visual-js/visual-snapshots/src/app/pdf-file-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { pdf } from "pdf-to-img";
import { PdfFile } from "./pdf-file.js";

export interface PdfFileLoader {
loadPdfFile(path: string): Promise<PdfFile>;
}

export class LibPdfFileLoader implements PdfFileLoader {
constructor(private readonly _pdf: typeof pdf = pdf) {}

public async loadPdfFile(pdfFilePath: string): Promise<PdfFile> {
const pdfFile = await this._pdf(pdfFilePath, {
scale: 1,
});

return {
path: pdfFilePath,
pages: pdfFile.length,
getPage: (page) => pdfFile.getPage(page),
};
}
}
3 changes: 2 additions & 1 deletion visual-js/visual-snapshots/src/app/pdf-file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface PdfFile {
readonly path: string;
convertPagesToImages(): AsyncGenerator<Buffer>;
readonly pages: number;
getPage(page: number): Promise<Buffer>;
}
11 changes: 11 additions & 0 deletions visual-js/visual-snapshots/src/app/pdf-files-snapshot-uploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface UploadPdfSnapshotsParams {
readonly buildId: string;
readonly pdfFilePaths: string[];
readonly suiteName?: string;
readonly testNameFormat?: string;
readonly snapshotNameFormat?: string;
}

export interface PdfSnapshotUploader {
uploadSnapshots(params: UploadPdfSnapshotsParams): Promise<void>;
}
36 changes: 22 additions & 14 deletions visual-js/visual-snapshots/src/app/pdf-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,38 @@ import {
CreateVisualSnapshotsParams,
VisualSnapshotsApi,
} from "../api/visual-snapshots-api.js";
import { initializeVisualApi } from "../api/visual-client.js";
import { PdfConverter } from "./pdf-converter.js";
import { VisualConfig } from "@saucelabs/visual";
import { getFiles } from "../utils/glob.js";
import { PdfSnapshotUploader } from "./pdf-files-snapshot-uploader.js";

export interface PdfCommandParams
extends VisualConfig,
CreateVisualSnapshotsParams {}
CreateVisualSnapshotsParams {
concurrency: number;
}

export class PdfCommandHandler {
private clientVersion: string;

constructor(clientVersion: string) {
this.clientVersion = clientVersion;
}
constructor(
private readonly visualSnapshotsApi: VisualSnapshotsApi,
private readonly pdfSnapshotUploader: PdfSnapshotUploader
) {}

public async handle(globsOrDirs: string[], params: PdfCommandParams) {
const visualApi = initializeVisualApi(params, this.clientVersion);
const visualSnapshots = new VisualSnapshotsApi(visualApi);
const pdfConverter = new PdfConverter();

const pdfFilePaths = await getFiles(globsOrDirs, "*.pdf");

const pdfFiles = pdfFilePaths.map((p) => pdfConverter.createPdfFile(p));
await visualSnapshots.generateAndSendPdfFileSnapshots(pdfFiles, params);
const buildId =
params.buildId ?? (await this.visualSnapshotsApi.createBuild(params));

await this.pdfSnapshotUploader.uploadSnapshots({
buildId,
pdfFilePaths,
suiteName: params.suiteName,
testNameFormat: params.testName,
snapshotNameFormat: params.snapshotName,
});

if (!params.buildId) {
await this.visualSnapshotsApi.finishBuild(buildId);
}
}
}
Loading