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: prepare html-reporter for pwt GUI integration #522

Merged
Merged
Show file tree
Hide file tree
Changes from 10 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
85 changes: 0 additions & 85 deletions hermione.js

This file was deleted.

141 changes: 141 additions & 0 deletions hermione.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import os from 'os';
import path from 'path';
import Hermione, {TestResult as HermioneTestResult} from 'hermione';
import _ from 'lodash';
import PQueue from 'p-queue';
import {CommanderStatic} from '@gemini-testing/commander';

import {cliCommands} from './lib/cli-commands';
import {hasDiff} from './lib/common-utils';
import {parseConfig} from './lib/config';
import {FAIL, SUCCESS, 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 {SqliteClient} from './lib/sqlite-client';
import {HermioneTestAdapter, ReporterTestResult} from './lib/test-adapter';
import {TestAttemptManager} from './lib/test-attempt-manager';
import {HtmlReporterApi, ReporterConfig, ReporterOptions} from './lib/types';
import {createWorkers, CreateWorkersRunner} from './lib/workers/create-workers';

let workers: ReturnType<typeof createWorkers>;

export = (hermione: Hermione, opts: Partial<ReporterOptions>): void => {
if (hermione.isWorker()) {
return;
}

const config = parseConfig(opts);

if (!config.enabled) {
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const htmlReporter = HtmlReporter.create(config, {toolName: ToolName.Hermione});

(hermione as Hermione & HtmlReporterApi).htmlReporter = htmlReporter;

let isCliCommandLaunched = false;
let handlingTestResults: Promise<void>;

hermione.on(hermione.events.CLI, (commander: CommanderStatic) => {
_.values(cliCommands).forEach((command: string) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
require(path.resolve(__dirname, 'lib/cli-commands', command))(commander, config, hermione);

commander.prependListener(`command:${command}`, () => {
isCliCommandLaunched = true;
});
});
});

hermione.on(hermione.events.INIT, async () => {
if (isCliCommandLaunched) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

чет я не понимаю для чего это условие

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это нужно чтобы при запуске cli команд типа gui или любых других команд html-reporter не запускался обычный прогон hermione.

Ранее это было реализовано в PluginAdapter в виде this._run = _.noop. Сейчас — этим способом.

return;
}

const dbClient = await SqliteClient.create({htmlReporter, reportPath: config.path});
const testAttemptManager = new TestAttemptManager();
const staticReportBuilder = StaticReportBuilder.create(htmlReporter, config, {dbClient, testAttemptManager});

handlingTestResults = Promise.all([
staticReportBuilder.saveStaticFiles(),
handleTestResults(hermione, staticReportBuilder, config)
]).then(async () => {
await staticReportBuilder.finalize();
}).then(async () => {
await htmlReporter.emitAsync(htmlReporter.events.REPORT_SAVED, {reportPath: config.path});
});
});

hermione.on(hermione.events.RUNNER_START, (runner) => {
workers = createWorkers(runner as unknown as CreateWorkersRunner);
});

hermione.on(hermione.events.RUNNER_END, async () => {
try {
await handlingTestResults;

logPathToHtmlReport(config);
} catch (e: unknown) {
logError(e as Error);
}
});
};

async function handleTestResults(hermione: Hermione, reportBuilder: StaticReportBuilder, pluginConfig: ReporterConfig): Promise<void> {
const {path: reportPath} = pluginConfig;
const {imageHandler} = reportBuilder;

const failHandler = async (testResult: HermioneTestResult): Promise<ReporterTestResult> => {
const attempt = reportBuilder.testAttemptManager.registerAttempt({fullName: testResult.fullTitle(), browserId: testResult.browserId}, FAIL);
const formattedResult = formatTestResult(testResult, FAIL, attempt, reportBuilder);
sipayRT marked this conversation as resolved.
Show resolved Hide resolved

const actions: Promise<unknown>[] = [imageHandler.saveTestImages(formattedResult, workers)];

if (pluginConfig.saveErrorDetails && formattedResult.errorDetails) {
actions.push((formattedResult as HermioneTestAdapter).saveErrorDetails(reportPath));
}

await Promise.all(actions);

return formattedResult;
};

const addFail = (formattedResult: ReporterTestResult): ReporterTestResult => {
return hasDiff(formattedResult.assertViewResults as {name?: string}[])
sipayRT marked this conversation as resolved.
Show resolved Hide resolved
? reportBuilder.addFail(formattedResult)
: reportBuilder.addError(formattedResult);
};

return new Promise((resolve, reject) => {
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 attempt = reportBuilder.testAttemptManager.registerAttempt({fullName: testResult.fullTitle(), browserId: testResult.browserId}, FAIL);
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
const formattedResult = formatTestResult(testResult, SUCCESS, attempt, reportBuilder);
await imageHandler.saveTestImages(formattedResult, workers);

return reportBuilder.addSuccess(formattedResult);
}).catch(reject));
});

hermione.on(hermione.events.RETRY, testResult => {
promises.push(queue.add(() => failHandler(testResult).then(addFail)).catch(reject));
});

hermione.on(hermione.events.TEST_FAIL, testResult => {
promises.push(queue.add(() => failHandler(testResult).then(addFail)).catch(reject));
});

hermione.on(hermione.events.TEST_PENDING, testResult => {
promises.push(queue.add(() => failHandler(testResult as HermioneTestResult).then((testResult) => reportBuilder.addSkipped(testResult)).catch(reject)));
});

hermione.on(hermione.events.RUNNER_END, () => {
return Promise.all(promises).then(() => resolve(), reject);
sipayRT marked this conversation as resolved.
Show resolved Hide resolved
});
});
}
23 changes: 21 additions & 2 deletions lib/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {SUCCESS, FAIL, ERROR, SKIPPED, UPDATED, IDLE, RUNNING, QUEUED, TestStatu
import {UNCHECKED, INDETERMINATE, CHECKED} from './constants/checked-statuses';
import {ImageData, ImageBase64, ImageInfoFull, TestError, ImageInfoError} 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 All @@ -29,7 +30,7 @@ export const isErrorStatus = (status: TestStatus): boolean => status === ERROR;
export const isSkippedStatus = (status: TestStatus): boolean => status === SKIPPED;
export const isUpdatedStatus = (status: TestStatus): boolean => status === UPDATED;

export const determineStatus = (statuses: TestStatus[]): TestStatus | null => {
export const determineFinalStatus = (statuses: TestStatus[]): TestStatus | null => {
if (!statuses.length) {
return SUCCESS;
}
Expand Down Expand Up @@ -103,7 +104,7 @@ export const hasNoRefImageErrors = ({assertViewResults = []}: {assertViewResults
return assertViewResults.some((assertViewResult) => isNoRefImageError(assertViewResult));
};

const hasFailedImages = (result: {imagesInfo?: ImageInfoFull[]}): boolean => {
export const hasFailedImages = (result: {imagesInfo?: ImageInfoFull[]}): boolean => {
const {imagesInfo = []} = result;

return imagesInfo.some((imageInfo: ImageInfoFull) => {
Expand All @@ -128,6 +129,24 @@ export const hasDiff = (assertViewResults: {name?: string}[]): boolean => {
return assertViewResults.some((result) => isImageDiffError(result as {name?: string}));
};

/* This method tries to determine true status of testResult by using fields like error, imagesInfo */
export const determineStatus = (testResult: Pick<ReporterTestResult, 'status' | 'error' | 'imagesInfo'>): TestStatus => {
if (!hasFailedImages(testResult) && !isSkippedStatus(testResult.status) && isEmpty(testResult.error)) {
return SUCCESS;
}

const imageErrors = (testResult.imagesInfo ?? []).map(imagesInfo => (imagesInfo as {error: {name?: string}}).error ?? {});
if (hasDiff(imageErrors) || hasNoRefImageErrors({assertViewResults: imageErrors})) {
return FAIL;
}

if (!isEmpty(testResult.error)) {
return ERROR;
}

return testResult.status;
};

export const isBase64Image = (image: ImageData | ImageBase64 | null | undefined): image is ImageBase64 => {
return Boolean((image as ImageBase64 | undefined)?.base64);
};
Expand Down
6 changes: 3 additions & 3 deletions lib/db-utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {DbLoadResult, HandleDatabasesOptions} from './common';
import {DbUrlsJsonData, RawSuitesRow, ReporterConfig} from '../types';
import {Tree} from '../tests-tree-builder/base';
import {ReporterTestResult} from '../test-adapter';
import {SqliteAdapter} from '../sqlite-adapter';
import {SqliteClient} from '../sqlite-client';

export * from './common';

Expand Down Expand Up @@ -121,8 +121,8 @@ async function rewriteDatabaseUrls(dbPaths: string[], mainDatabaseUrls: string,
});
}

export const getTestFromDb = <T = unknown>(sqliteAdapter: SqliteAdapter, testResult: ReporterTestResult): T | undefined => {
return sqliteAdapter.query<T>({
export const getTestFromDb = <T = unknown>(dbClient: SqliteClient, testResult: ReporterTestResult): T | undefined => {
return dbClient.query<T>({
select: '*',
where: `${DB_COLUMNS.SUITE_PATH} = ? AND ${DB_COLUMNS.NAME} = ? AND ${DB_COLUMNS.STATUS} = ?`,
orderBy: DB_COLUMNS.TIMESTAMP,
Expand Down
5 changes: 0 additions & 5 deletions lib/errors/db-not-initialized-error.ts

This file was deleted.

Loading
Loading