Add preliminary message backup harness

This commit is contained in:
Fedor Indutny 2024-03-15 07:20:33 -07:00 committed by GitHub
parent 231bf91a22
commit d85a1d5074
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2997 additions and 121 deletions

View file

@ -3,11 +3,15 @@
import assert from 'assert';
import fs from 'fs/promises';
import crypto from 'crypto';
import path from 'path';
import os from 'os';
import createDebug from 'debug';
import pTimeout from 'p-timeout';
import normalizePath from 'normalize-path';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
import type { Page } from 'playwright';
import type { Device, PrimaryDevice } from '@signalapp/mock-server';
import {
@ -18,6 +22,7 @@ import {
import { MAX_READ_KEYS as MAX_STORAGE_READ_KEYS } from '../services/storageConstants';
import * as durations from '../util/durations';
import { drop } from '../util/drop';
import type { RendererConfigType } from '../types/RendererConfig';
import { App } from './playwright';
import { CONTACT_COUNT } from './benchmarks/fixtures';
@ -93,7 +98,6 @@ for (const suffix of CONTACT_SUFFIXES) {
const MAX_CONTACTS = CONTACT_NAMES.length;
export type BootstrapOptions = Readonly<{
extraConfig?: Record<string, unknown>;
benchmark?: boolean;
linkedDevices?: number;
@ -104,7 +108,7 @@ export type BootstrapOptions = Readonly<{
contactPreKeyCount?: number;
}>;
type BootstrapInternalOptions = Pick<BootstrapOptions, 'extraConfig'> &
type BootstrapInternalOptions = BootstrapOptions &
Readonly<{
benchmark: boolean;
linkedDevices: number;
@ -114,6 +118,10 @@ type BootstrapInternalOptions = Pick<BootstrapOptions, 'extraConfig'> &
contactNames: ReadonlyArray<string>;
}>;
function sanitizePathComponent(component: string): string {
return normalizePath(component.replace(/[^a-z]+/gi, '-'));
}
//
// Bootstrap is a class that prepares mock server and desktop for running
// tests/benchmarks.
@ -149,8 +157,10 @@ export class Bootstrap {
private privPhone?: PrimaryDevice;
private privDesktop?: Device;
private storagePath?: string;
private backupPath?: string;
private timestamp: number = Date.now() - durations.WEEK;
private lastApp?: App;
private readonly randomId = crypto.randomBytes(8).toString('hex');
constructor(options: BootstrapOptions = {}) {
this.server = new Server({
@ -224,6 +234,9 @@ export class Bootstrap {
});
this.storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'mock-signal-'));
this.backupPath = await fs.mkdtemp(
path.join(os.tmpdir(), 'mock-signal-backup-')
);
debug('setting storage path=%j', this.storagePath);
}
@ -244,6 +257,26 @@ export class Bootstrap {
return path.join(this.storagePath, 'logs');
}
public getBackupPath(fileName: string): string {
assert(
this.backupPath !== undefined,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
return path.join(this.backupPath, fileName);
}
public async unlink(): Promise<void> {
assert(
this.storagePath !== undefined,
'Bootstrap has to be initialized first, see: bootstrap.init()'
);
// Note that backupPath must remain unchanged!
await fs.rm(this.storagePath, { recursive: true });
this.storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'mock-signal-'));
}
public async teardown(): Promise<void> {
debug('tearing down');
@ -252,6 +285,9 @@ export class Bootstrap {
this.storagePath
? fs.rm(this.storagePath, { recursive: true })
: Promise.resolve(),
this.backupPath
? fs.rm(this.backupPath, { recursive: true })
: Promise.resolve(),
this.server.close(),
this.lastApp?.close(),
]),
@ -259,10 +295,10 @@ export class Bootstrap {
]);
}
public async link(): Promise<App> {
public async link(extraConfig?: Partial<RendererConfigType>): Promise<App> {
debug('linking');
const app = await this.startApp();
const app = await this.startApp(extraConfig);
const provision = await this.server.waitForProvision();
@ -302,7 +338,9 @@ export class Bootstrap {
await app.close();
}
public async startApp(): Promise<App> {
public async startApp(
extraConfig?: Partial<RendererConfigType>
): Promise<App> {
assert(
this.storagePath !== undefined,
'Bootstrap has to be initialized first, see: bootstrap.init()'
@ -311,10 +349,10 @@ export class Bootstrap {
debug('starting the app');
const { port } = this.server.address();
const config = await this.generateConfig(port);
const config = await this.generateConfig(port, extraConfig);
let startAttempts = 0;
const MAX_ATTEMPTS = 5;
const MAX_ATTEMPTS = 4;
let app: App | undefined;
while (!app) {
startAttempts += 1;
@ -360,7 +398,7 @@ export class Bootstrap {
}
public async maybeSaveLogs(
test?: Mocha.Test,
test?: Mocha.Runnable,
app: App | undefined = this.lastApp
): Promise<void> {
const { FORCE_ARTIFACT_SAVE } = process.env;
@ -371,29 +409,18 @@ export class Bootstrap {
public async saveLogs(
app: App | undefined = this.lastApp,
pathPrefix?: string
testName?: string
): Promise<void> {
const { ARTIFACTS_DIR } = process.env;
if (!ARTIFACTS_DIR) {
// eslint-disable-next-line no-console
console.error('Not saving logs. Please set ARTIFACTS_DIR env variable');
const outDir = await this.getArtifactsDir(testName);
if (outDir == null) {
return;
}
await fs.mkdir(ARTIFACTS_DIR, { recursive: true });
const normalizedPrefix = pathPrefix
? `-${normalizePath(pathPrefix.replace(/[^a-z]+/gi, '-'))}-`
: '';
const outDir = await fs.mkdtemp(
path.join(ARTIFACTS_DIR, `logs-${normalizedPrefix}`)
);
// eslint-disable-next-line no-console
console.error(`Saving logs to ${outDir}`);
const { logsDir } = this;
await fs.rename(logsDir, outDir);
await fs.rename(logsDir, path.join(outDir, 'logs'));
const page = await app?.getWindow();
if (process.env.TRACING) {
@ -408,6 +435,77 @@ export class Bootstrap {
}
}
public async createScreenshotComparator(
app: App,
callback: (
page: Page,
snapshot: (name: string) => Promise<void>
) => Promise<void>,
test?: Mocha.Runnable
): Promise<(app: App) => Promise<void>> {
const snapshots = new Array<{ name: string; data: Buffer }>();
const window = await app.getWindow();
await callback(window, async (name: string) => {
debug('creating screenshot');
snapshots.push({ name, data: await window.screenshot() });
});
let index = 0;
return async (anotherApp: App): Promise<void> => {
const anotherWindow = await anotherApp.getWindow();
await callback(anotherWindow, async (name: string) => {
index += 1;
const before = snapshots.shift();
assert(before != null, 'No previous snapshot');
assert.strictEqual(before.name, name, 'Wrong snapshot order');
const after = await anotherWindow.screenshot();
const beforePng = PNG.sync.read(before.data);
const afterPng = PNG.sync.read(after);
const { width, height } = beforePng;
const diffPng = new PNG({ width, height });
const numPixels = pixelmatch(
beforePng.data,
afterPng.data,
diffPng.data,
width,
height,
{}
);
if (numPixels === 0) {
debug('no screenshot difference');
return;
}
debug('screenshot difference', numPixels);
const outDir = await this.getArtifactsDir(test?.fullTitle());
if (outDir != null) {
debug('saving screenshots and diff');
const prefix = `${index}-${sanitizePathComponent(name)}`;
await fs.writeFile(
path.join(outDir, `${prefix}-before.png`),
before.data
);
await fs.writeFile(path.join(outDir, `${prefix}-after.png`), after);
await fs.writeFile(
path.join(outDir, `${prefix}-diff.png`),
PNG.sync.write(diffPng)
);
}
assert.strictEqual(numPixels, 0, 'Expected no pixels to be different');
});
};
}
//
// Getters
//
@ -463,6 +561,28 @@ export class Bootstrap {
// Private
//
private async getArtifactsDir(
testName?: string
): Promise<string | undefined> {
const { ARTIFACTS_DIR } = process.env;
if (!ARTIFACTS_DIR) {
// eslint-disable-next-line no-console
console.error(
'Not saving artifacts. Please set ARTIFACTS_DIR env variable'
);
return undefined;
}
const normalizedPath = testName
? `${this.randomId}-${sanitizePathComponent(testName)}`
: this.randomId;
const outDir = path.join(ARTIFACTS_DIR, normalizedPath);
await fs.mkdir(outDir, { recursive: true });
return outDir;
}
private static async runBenchmark(
fn: (bootstrap: Bootstrap) => Promise<void>,
timeout: number
@ -486,7 +606,10 @@ export class Bootstrap {
}
}
private async generateConfig(port: number): Promise<string> {
private async generateConfig(
port: number,
extraConfig?: Partial<RendererConfigType>
): Promise<string> {
const url = `https://127.0.0.1:${port}`;
return JSON.stringify({
...(await loadCertificates()),
@ -510,7 +633,7 @@ export class Bootstrap {
directoryCDSIMRENCLAVE:
'51133fecb3fa18aaf0c8f64cb763656d3272d9faaacdb26ae7df082e414fb142',
...this.options.extraConfig,
...extraConfig,
});
}
}