Skip to content
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

refactor: streamline ReporterTestResult and imagesInfo types #526

Merged
merged 7 commits into from
Jan 9, 2024
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
67 changes: 31 additions & 36 deletions hermione.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import PQueue from 'p-queue';
import {CommanderStatic} from '@gemini-testing/commander';

import {cliCommands} from './lib/cli-commands';
import {hasFailedImages} from './lib/common-utils';
import {parseConfig} from './lib/config';
import {SKIPPED, SUCCESS, TestStatus, ToolName, UNKNOWN_ATTEMPT} from './lib/constants';
import {ToolName} from './lib/constants';
import {HtmlReporter} from './lib/plugin-api';
import {StaticReportBuilder} from './lib/report-builder/static';
import {formatTestResult, logPathToHtmlReport, logError} from './lib/server-utils';
import {formatTestResult, logPathToHtmlReport, logError, getExpectedCacheKey} from './lib/server-utils';
import {SqliteClient} from './lib/sqlite-client';
import {HtmlReporterApi, ImageInfoFull, ReporterOptions} from './lib/types';
import {HtmlReporterApi, ReporterOptions, TestSpecByPath} from './lib/types';
import {createWorkers, CreateWorkersRunner} from './lib/workers/create-workers';
import {SqliteImageStore} from './lib/image-store';
import {Cache} from './lib/cache';
import {ImagesInfoSaver} from './lib/images-info-saver';
import {getStatus} from './lib/test-adapter/hermione';

export = (hermione: Hermione, opts: Partial<ReporterOptions>): void => {
if (hermione.isWorker() || !opts.enabled) {
Expand Down Expand Up @@ -56,7 +59,17 @@ export = (hermione: Hermione, opts: Partial<ReporterOptions>): void => {

hermione.on(hermione.events.INIT, withMiddleware(async () => {
const dbClient = await SqliteClient.create({htmlReporter, reportPath: config.path});
staticReportBuilder = StaticReportBuilder.create(htmlReporter, config, {dbClient});
const imageStore = new SqliteImageStore(dbClient);
const expectedPathsCache = new Cache<[TestSpecByPath, string | undefined], string>(getExpectedCacheKey);

const imagesInfoSaver = new ImagesInfoSaver({
imageFileSaver: htmlReporter.imagesSaver,
expectedPathsCache,
imageStore,
reportPath: htmlReporter.config.path
});

staticReportBuilder = StaticReportBuilder.create(htmlReporter, config, {dbClient, imagesInfoSaver});

handlingTestResults = Promise.all([
staticReportBuilder.saveStaticFiles(),
Expand Down Expand Up @@ -90,39 +103,21 @@ async function handleTestResults(hermione: Hermione, reportBuilder: StaticReport
const queue = new PQueue({concurrency: os.cpus().length});
const promises: Promise<unknown>[] = [];

hermione.on(hermione.events.TEST_PASS, testResult => {
promises.push(queue.add(async () => {
const formattedResult = formatTestResult(testResult, SUCCESS, UNKNOWN_ATTEMPT, reportBuilder);
await reportBuilder.addSuccess(formattedResult);
}).catch(reject));
});

hermione.on(hermione.events.RETRY, testResult => {
promises.push(queue.add(async () => {
const status = hasFailedImages(testResult.assertViewResults as ImageInfoFull[]) ? TestStatus.FAIL : TestStatus.ERROR;

const formattedResult = formatTestResult(testResult, status, UNKNOWN_ATTEMPT, reportBuilder);

await reportBuilder.addFail(formattedResult);
}).catch(reject));
});

hermione.on(hermione.events.TEST_FAIL, testResult => {
promises.push(queue.add(async () => {
const status = hasFailedImages(testResult.assertViewResults as ImageInfoFull[]) ? TestStatus.FAIL : TestStatus.ERROR;

const formattedResult = formatTestResult(testResult, status, UNKNOWN_ATTEMPT, reportBuilder);
[
{eventName: hermione.events.TEST_PASS},
{eventName: hermione.events.RETRY},
{eventName: hermione.events.TEST_FAIL},
{eventName: hermione.events.TEST_PENDING}
].forEach(({eventName}) => {
type AnyHermioneTestEvent = typeof hermione.events.TEST_PASS;

await reportBuilder.addFail(formattedResult);
}).catch(reject));
});

hermione.on(hermione.events.TEST_PENDING, testResult => {
promises.push(queue.add(async () => {
const formattedResult = formatTestResult(testResult as HermioneTestResult, SKIPPED, UNKNOWN_ATTEMPT, reportBuilder);
hermione.on(eventName as AnyHermioneTestEvent, (testResult: HermioneTestResult) => {
promises.push(queue.add(async () => {
const formattedResult = formatTestResult(testResult, getStatus(eventName, hermione.events, testResult));

await reportBuilder.addSkipped(formattedResult);
}).catch(reject));
await reportBuilder.addTestResult(formattedResult);
}).catch(reject));
});
});
shadowusr marked this conversation as resolved.
Show resolved Hide resolved

hermione.on(hermione.events.RUNNER_END, () => {
Expand Down
33 changes: 33 additions & 0 deletions lib/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export class Cache<Key, Value> {
private _getKeyHash: (key: Key) => string;
private _cache: Map<string, Value>;

constructor(hashFn: (key: Key) => string) {
this._getKeyHash = hashFn;
this._cache = new Map();
}

has(key: Key): boolean {
const keyHash = this._getKeyHash(key);

return this._cache.has(keyHash);
}

get(key: Key): Value | undefined {
const keyHash = this._getKeyHash(key);

return this._cache.get(keyHash);
}

set(key: Key, value: Value): this {
const keyHash = this._getKeyHash(key);

if (value !== undefined) {
this._cache.set(keyHash, value);
} else {
this._cache.delete(keyHash);
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
}

return this;
}
}
61 changes: 54 additions & 7 deletions lib/common-utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import crypto from 'crypto';
import {pick, isEmpty} from 'lodash';
import {isEmpty, pick} from 'lodash';
import url from 'url';
import axios, {AxiosRequestConfig} from 'axios';
import {SUCCESS, FAIL, ERROR, SKIPPED, UPDATED, IDLE, RUNNING, QUEUED, TestStatus} from './constants';

import {UNCHECKED, INDETERMINATE, CHECKED} from './constants/checked-statuses';
import {ImageData, ImageBase64, ImageInfoFull, TestError, ImageInfoFail} from './types';
import {
ERROR,
FAIL,
HERMIONE_TITLE_DELIMITER,
IDLE, PWT_TITLE_DELIMITER,
QUEUED,
RUNNING,
SKIPPED,
SUCCESS,
TestStatus,
ToolName,
UPDATED
} from './constants';

import {CHECKED, INDETERMINATE, UNCHECKED} from './constants/checked-statuses';
import {
ImageBase64,
ImageBuffer,
ImageFile,
ImageInfoDiff,
ImageInfoFull,
ImageInfoWithState,
TestError
} from './types';
import {ErrorName, ImageDiffError, NoRefImageError} from './errors';
import {ReporterTestResult} from './test-adapter';

export const getShortMD5 = (str: string): string => {
return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7);
};
Expand Down Expand Up @@ -106,7 +127,7 @@ export const hasNoRefImageErrors = ({assertViewResults = []}: {assertViewResults

export const hasFailedImages = (imagesInfo: ImageInfoFull[] = []): boolean => {
return imagesInfo.some((imageInfo: ImageInfoFull) => {
return (imageInfo as ImageInfoFail).stateName &&
return (imageInfo as ImageInfoDiff).stateName &&
(isErrorStatus(imageInfo.status) || isFailStatus(imageInfo.status) || isNoRefImageError(imageInfo) || isImageDiffError(imageInfo));
});
};
Expand Down Expand Up @@ -151,7 +172,7 @@ export const determineStatus = (testResult: Pick<ReporterTestResult, 'status' |
return testResult.status;
};

export const isBase64Image = (image: ImageData | ImageBase64 | null | undefined): image is ImageBase64 => {
export const isBase64Image = (image: ImageFile | ImageBuffer | ImageBase64 | null | undefined): image is ImageBase64 => {
return Boolean((image as ImageBase64 | undefined)?.base64);
};

Expand Down Expand Up @@ -217,3 +238,29 @@ export const isCheckboxChecked = (status: number): boolean => Number(status) ===
export const isCheckboxIndeterminate = (status: number): boolean => Number(status) === INDETERMINATE;
export const isCheckboxUnchecked = (status: number): boolean => Number(status) === UNCHECKED;
export const getToggledCheckboxState = (status: number): number => isCheckboxChecked(status) ? UNCHECKED : CHECKED;

export const getTitleDelimiter = (toolName: ToolName): string => {
if (toolName === ToolName.Hermione) {
return HERMIONE_TITLE_DELIMITER;
} else if (toolName === ToolName.Playwright) {
return PWT_TITLE_DELIMITER;
} else {
return HERMIONE_TITLE_DELIMITER;
}
};

export function getDetailsFileName(testId: string, browserId: string, attempt: number): string {
return `${testId}-${browserId}_${Number(attempt) + 1}_${Date.now()}.json`;
}

export const getTestHash = (testResult: ReporterTestResult): string => {
return testResult.testPath.concat(testResult.browserId, testResult.attempt.toString()).join(' ');
};

export const isImageBufferData = (imageData: ImageBuffer | ImageFile | ImageBase64 | undefined): imageData is ImageBuffer => {
return Boolean((imageData as ImageBuffer).buffer);
};

export const isImageInfoWithState = (imageInfo: ImageInfoFull): imageInfo is ImageInfoWithState => {
return Boolean((imageInfo as ImageInfoWithState).stateName);
};
24 changes: 21 additions & 3 deletions lib/constants/database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {ValueOf} from 'type-fest';

// TODO: change to enums
export const DB_TYPES = {int: 'INT', text: 'TEXT'} as const;
export const DB_COLUMNS = {
Expand Down Expand Up @@ -40,7 +38,27 @@ export const DB_MAX_AVAILABLE_PAGE_SIZE = 65536; // helps to speed up queries
export const DB_SUITES_TABLE_NAME = 'suites';
export const LOCAL_DATABASE_NAME = 'sqlite.db';
export const DATABASE_URLS_JSON_NAME = 'databaseUrls.json';

// Precise numbers instead of just "number" make this possible: row[DB_COLUMN_INDEXES.name] === string
interface DbColumnIndexes {
[DB_COLUMNS.SUITE_PATH]: 0,
[DB_COLUMNS.SUITE_NAME]: 1,
[DB_COLUMNS.NAME]: 2,
[DB_COLUMNS.SUITE_URL]: 3,
[DB_COLUMNS.META_INFO]: 4,
[DB_COLUMNS.HISTORY]: 5,
[DB_COLUMNS.DESCRIPTION]: 6,
[DB_COLUMNS.ERROR]: 7,
[DB_COLUMNS.SKIP_REASON]: 8,
[DB_COLUMNS.IMAGES_INFO]: 9,
[DB_COLUMNS.SCREENSHOT]: 10,
[DB_COLUMNS.MULTIPLE_TABS]: 11,
[DB_COLUMNS.STATUS]: 12,
[DB_COLUMNS.TIMESTAMP]: 13,

}

export const DB_COLUMN_INDEXES = SUITES_TABLE_COLUMNS.reduce((acc: Record<string, number>, {name}, index) => {
acc[name] = index;
return acc;
}, {}) as { [K in ValueOf<typeof DB_COLUMNS>]: number };
}, {}) as unknown as DbColumnIndexes;
9 changes: 0 additions & 9 deletions lib/constants/errors.js

This file was deleted.

3 changes: 3 additions & 0 deletions lib/constants/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ERROR_TITLE_TEXT_LENGTH = 200;

export const NEW_ISSUE_LINK = 'https://github.com/gemini-testing/html-reporter/issues/new';
1 change: 1 addition & 0 deletions lib/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './browser';
export * from './database';
export * from './defaults';
export * from './diff-modes';
export * from './errors';
export * from './group-tests';
export * from './paths';
export * from './tests';
Expand Down
2 changes: 2 additions & 0 deletions lib/constants/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const HERMIONE_TITLE_DELIMITER = ' ';
export const PWT_TITLE_DELIMITER = ' › ';

export const UNKNOWN_ATTEMPT = -1;

export const UNKNOWN_SESSION_ID = 'unknown session id';
4 changes: 2 additions & 2 deletions lib/db-utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ export async function mergeDatabases(srcDbPaths: string[], reportPath: string):
}
}

export function getTestsTreeFromDatabase(toolName: ToolName, dbPath: string): Tree {
export function getTestsTreeFromDatabase(toolName: ToolName, dbPath: string, baseHost: string): Tree {
try {
const db = new Database(dbPath, {readonly: true, fileMustExist: true});
const testsTreeBuilder = StaticTestsTreeBuilder.create({toolName});
const testsTreeBuilder = StaticTestsTreeBuilder.create({toolName, baseHost});

const suitesRows = (db.prepare(commonSqliteUtils.selectAllSuitesQuery())
.raw()
Expand Down
12 changes: 6 additions & 6 deletions lib/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {CoordBounds} from 'looks-same';
import {DiffOptions, ImageData} from '../types';
import {DiffOptions, ImageFile} from '../types';
import {ValueOf} from 'type-fest';

export const ErrorName = {
Expand All @@ -17,18 +17,18 @@ export interface ImageDiffError {
stack: string;
stateName: string;
diffOpts: DiffOptions;
currImg: ImageData;
refImg: ImageData;
currImg: ImageFile;
refImg: ImageFile;
diffClusters: CoordBounds[];
diffBuffer?: ArrayBuffer;
diffImg?: ImageData;
diffImg?: ImageFile;
}

export interface NoRefImageError {
name: ErrorNames['NO_REF_IMAGE'];
stateName: string;
message: string;
stack?: string;
currImg: ImageData;
refImg?: ImageData;
currImg: ImageFile;
refImg: ImageFile;
}
Loading
Loading