UNPKG

@syngrisi/syngrisi

Version:
245 lines (207 loc) 9.51 kB
import { Check, CheckDocument } from '@models'; import { buildIdentObject, ident, errMsg, ApiError, prettyCheckParams } from '@utils'; import log from "@logger"; import { LogOpts, RequestUser } from '@types'; import { domSnapshotService } from './dom-snapshot.service'; import { TestDocument } from '@models/Test.model'; import { AppDocument } from '@models/App.model'; import { SuiteDocument } from '@models/Suite.model'; import { SnapshotDocument } from '@models/Snapshot.model'; import { HttpStatus } from '@utils'; import { prepareActualSnapshot, isNeedFiles } from './snapshot-file.service'; import { startSession, endSession, updateTestAfterCheck } from './test-run.service'; import * as BaselineService from './baseline.service'; import * as CheckService from './check.service'; import { compareCheck } from './comparison.service'; import { CreateCheckParams, CreateCheckParamsExtended } from '../../types/Check'; const createCheckParams = (checkParam: CreateCheckParams, suite: SuiteDocument, app: AppDocument, test: TestDocument, currentUser: RequestUser): CreateCheckParamsExtended => ({ test: test.id, name: checkParam.name, status: 'pending', viewport: checkParam.viewport, browserName: checkParam.browserName, browserVersion: checkParam.browserVersion, browserFullVersion: checkParam.browserFullVersion, os: checkParam.os, updatedDate: Date.now(), suite: suite.id, app: app.id, branch: checkParam.branch, domDump: checkParam.domDump, run: test.run.toString(), creatorId: currentUser._id.toString(), creatorUsername: currentUser.username, hashCode: checkParam.hashCode, failReasons: [], toleranceThreshold: checkParam.toleranceThreshold, }); import mongoose from 'mongoose'; import * as SnapshotService from './snapshot.service'; import fs, { promises as fsp } from 'fs'; import { config } from '@config'; import path from 'path'; /** * Check if the MongoDB deployment supports transactions (requires replica set or sharded cluster) */ async function supportsTransactions(): Promise<boolean> { try { if (!mongoose.connection.db) { log.warn('MongoDB connection not established. Transactions will be disabled.'); return false; } const adminDb = mongoose.connection.db.admin(); const serverStatus = await adminDb.serverStatus(); // Check if running as part of a replica set return serverStatus.repl !== undefined; } catch (e) { log.warn(`Failed to detect MongoDB replica set: ${errMsg(e)}. Transactions will be disabled.`); return false; } } const cleanupOrphanFiles = async ( actualSnapshot: SnapshotDocument | null, diffSnapshot: SnapshotDocument | null, logOpts: LogOpts ) => { if (actualSnapshot && actualSnapshot.filename === `${actualSnapshot.id}.png`) { const imagePath = path.join(config.defaultImagesPath, actualSnapshot.filename); try { if (fs.existsSync(imagePath)) { await fsp.unlink(imagePath); log.debug(`deleted orphan file: ${imagePath}`, logOpts); } } catch (err) { log.error(`failed to delete orphan file: ${imagePath}, error: ${errMsg(err)}`, logOpts); } } if (diffSnapshot && diffSnapshot.filename) { const imagePath = path.join(config.defaultImagesPath, diffSnapshot.filename); try { if (fs.existsSync(imagePath)) { await fsp.unlink(imagePath); log.debug(`deleted orphan diff file: ${imagePath}`, logOpts); } } catch (err) { log.error(`failed to delete orphan diff file: ${imagePath}, error: ${errMsg(err)}`, logOpts); } } }; const createCheck = async (checkParam: CreateCheckParams, test: TestDocument, suite: SuiteDocument, app: AppDocument, currentUser: RequestUser, skipSaveOnCompareError = false) => { const logOpts: LogOpts = { scope: 'createCheck', user: currentUser.username, itemType: 'check', }; let actualSnapshot: SnapshotDocument | null = null; let currentBaselineSnapshot: SnapshotDocument; let diffSnapshot: SnapshotDocument | null = null; const newCheckParams = createCheckParams(checkParam, suite, app, test, currentUser); const checkIdent = buildIdentObject(newCheckParams); let check: CheckDocument | null = null; const totalCheckHandleTime = 0; // Check if transactions are supported (requires replica set) const useTransactions = await supportsTransactions(); let session: mongoose.ClientSession | undefined; if (useTransactions) { session = await mongoose.startSession(); session.startTransaction(); log.debug('Using MongoDB transactions for createCheck', logOpts); } else { log.debug('MongoDB transactions not available, executing without session', logOpts); } try { const { needFilesStatus, snapshotFoundedByHashcode } = await isNeedFiles(checkParam, logOpts); if (needFilesStatus) { if (session) { await session.abortTransaction(); session.endSession(); } return { status: 'needFiles' }; } // update test with suite and creator // moved from controller to be part of transaction test.suite = suite.id; test.creatorId = currentUser._id; test.creatorUsername = currentUser.username; await test.save({ session }); actualSnapshot = await prepareActualSnapshot(checkParam, snapshotFoundedByHashcode, logOpts, session); newCheckParams.actualSnapshotId = actualSnapshot.id; log.info(`find a baseline for the check with identifier: '${JSON.stringify(checkIdent)}'`, logOpts); const storedBaseline = await BaselineService.getAcceptedBaseline(checkIdent); const inspectBaselineResult = await BaselineService.inspectBaseline(newCheckParams, storedBaseline, checkIdent, actualSnapshot, logOpts); Object.assign(newCheckParams, inspectBaselineResult.inspectBaselineParams); currentBaselineSnapshot = inspectBaselineResult.currentBaselineSnapshot; const compareResult = await compareCheck(currentBaselineSnapshot, actualSnapshot, newCheckParams, skipSaveOnCompareError, currentUser, session); Object.assign(newCheckParams, compareResult); if (compareResult.diffSnapshot) { diffSnapshot = compareResult.diffSnapshot; } log.debug(`create the new check document with params: '${prettyCheckParams(newCheckParams)}'`, logOpts); check = await CheckService.createCheckDocument(newCheckParams, session); const savedCheck = check; log.debug(`the check with id: '${check.id}', was created, will updated with data during creating process`, logOpts); logOpts.ref = String(check.id); await updateTestAfterCheck(test, check, logOpts, session); // Save DOM snapshot if provided (for RCA feature) if (checkParam.domDump) { try { await domSnapshotService.createDomSnapshot({ checkId: check.id, type: 'actual', content: checkParam.domDump, }); log.debug(`DOM snapshot created for check: '${check.id}'`, logOpts); } catch (domErr) { // DOM snapshot is non-critical, log and continue log.warn(`Failed to create DOM snapshot for check '${check.id}': ${errMsg(domErr)}`, logOpts); } } const lastSuccessCheck = await BaselineService.getLastSuccessCheck(checkIdent); if (session) { await session.commitTransaction(); session.endSession(); } const checkObject = savedCheck.toObject(); // Convert status from array to string for SDK compatibility if (checkObject.status && Array.isArray(checkObject.status)) { checkObject.status = checkObject.status[0] as any; } type CheckResult = (typeof checkObject) & { currentSnapshot: SnapshotDocument, expectedSnapshot: SnapshotDocument, diffSnapshot: SnapshotDocument, executeTime: number, lastSuccess: string, } const result: CheckResult = { ...checkObject, currentSnapshot: actualSnapshot, expectedSnapshot: currentBaselineSnapshot, diffSnapshot: compareResult.diffSnapshot, executeTime: totalCheckHandleTime, lastSuccess: lastSuccessCheck ? lastSuccessCheck.id : null, }; return result; } catch (e: unknown) { if (session) { await session.abortTransaction(); session.endSession(); } log.error(`${session ? 'transaction aborted' : 'operation failed'}, cleaning up files... error: ${errMsg(e)}`, logOpts); await cleanupOrphanFiles(actualSnapshot, diffSnapshot, logOpts); // Throw error instead of creating failed check - maintain data consistency throw new ApiError( HttpStatus.INTERNAL_SERVER_ERROR, `Failed to create check: ${errMsg(e)}` ); } }; const getIdent = () => ident; const getBaselines = BaselineService.getBaselines; export { startSession, endSession, createCheck, getIdent, getBaselines, };