@syngrisi/syngrisi
Version:
Syngrisi - Visual Testing Tool
467 lines (398 loc) • 20.7 kB
text/typescript
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable camelcase */
import { Response } from "express";
import { env } from "@/server/envConfig";
interface StringTable {
create(data: { [key: string]: any }[]): string;
}
import fs, { promises as fsp } from 'fs';
// @ts-ignore
import st from 'string-table';
import { config } from '@config';
import { subDays, dateToISO8601 } from '@utils';
import { ProgressBar } from '@utils';
import log from "../lib/logger";
import testAdminUser from '../../seeds/testAdmin.json'
const stringTable: StringTable = st;
import {
Snapshot,
Check,
Test,
Run,
Suite,
User,
Log,
Baseline,
} from '@models';
import { ExtRequest } from '@types';
import path from "path";
function taskOutput(msg: any, res: any) {
res.write(`${msg.toString()}\n`);
log.debug(msg.toString());
}
function parseHrtimeToSeconds(hrtime: any) {
return (hrtime[0] + (hrtime[1] / 1e9)).toFixed(3);
}
const status = async (currentUser: any) => {
const count = await User.countDocuments().exec();
log.silly(`server status: check users counts: ${count}`);
if (count > 1) {
return { alive: true, currentUser: currentUser?.username };
}
return { alive: false };
};
const screenshots = async () => {
const files = fs.readdirSync(config.defaultImagesPath);
return files;
};
const loadTestUser = async () => {
const logOpts = {
itemType: 'user',
msgType: 'LOAD',
ref: 'Administrator',
};
if (!env.SYNGRISI_TEST_MODE) {
return { message: 'the feature works only in test mode' };
}
const testAdmin = await User.findOne({ username: 'Test' }).exec();
if (!testAdmin) {
log.info('create the test Administrator', logOpts);
const admin = await User.create(testAdminUser);
log.info(`test Administrator with id: '${admin._id}' was created`, logOpts);
return admin;
}
log.info(`test admin is exists: ${JSON.stringify(testAdmin, null, 2)}`, logOpts);
return { msg: `already exist '${testAdmin}'` };
};
const task_handle_database_consistency = async (options: any, res: any) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Content-Encoding': 'none',
'x-no-compression': 'true',
});
try {
const startTime = process.hrtime();
taskOutput('- starting...\n', res);
taskOutput('---------------------------------', res);
taskOutput('STAGE #1: Calculate Common stats', res);
taskOutput('get runs data', res);
const allRunsBefore = await Run.find().exec();
taskOutput('get suites data', res);
const allSuitesBefore = await Suite.find().exec();
taskOutput('get tests data', res);
const allTestsBefore = await Test.find().lean().exec();
taskOutput('get checks data', res);
const allChecksBefore = await Check.find().lean().exec();
taskOutput('get snapshots data', res);
const allSnapshotsBefore = await Snapshot.find().lean().exec();
taskOutput('get files data', res);
const allFilesBefore = (await fsp.readdir(config.defaultImagesPath, { withFileTypes: true }))
.filter((item: any) => !item.isDirectory())
.map((x: any) => x.name)
.filter((x: any) => x.includes('.png'));
taskOutput('-----------------------------', res);
const beforeStatTable = stringTable.create([
{ item: 'suites', count: allSuitesBefore.length },
{ item: 'runs', count: allRunsBefore.length },
{ item: 'tests', count: allTestsBefore.length },
{ item: 'checks', count: allChecksBefore.length },
{ item: 'snapshots', count: allSnapshotsBefore.length },
{ item: 'files', count: allFilesBefore.length },
]);
res.flush();
taskOutput(beforeStatTable, res);
taskOutput('---------------------------------', res);
taskOutput('STAGE #2: Calculate Inconsistent Items', res);
taskOutput('> calculate abandoned snapshots', res);
const abandonedSnapshots = allSnapshotsBefore.filter((sn: any) => !fs.existsSync(path.join(config.defaultImagesPath, sn.filename)));
taskOutput('> calculate abandoned files', res);
const snapshotsUniqueFiles = Array.from(new Set(allSnapshotsBefore.map((x: any) => x.filename)));
const abandonedFiles: any[] = [];
const progress = new ProgressBar(allFilesBefore.length);
for (const [index, file] of allFilesBefore.entries()) {
setTimeout(() => {
progress.writeIfChange(index, allFilesBefore.length, taskOutput, res);
}, 10);
if (!snapshotsUniqueFiles.includes(file.toString())) {
abandonedFiles.push(file);
}
}
taskOutput('> calculate abandoned checks', res);
const allSnapshotsBeforeIds = allSnapshotsBefore.map((x: any) => x._id.valueOf());
const allChecksBeforeLight = allChecksBefore.map((x: any) => ({
_id: x._id.valueOf(), baselineId: x.baselineId.valueOf(), actualSnapshotId: x.actualSnapshotId.valueOf(),
}));
const abandonedChecks: any[] = [];
const progressChecks = new ProgressBar(allChecksBefore.length);
for (const [index, check] of allChecksBeforeLight.entries()) {
progressChecks.writeIfChange(index, allChecksBeforeLight.length, taskOutput, res);
if (!allSnapshotsBeforeIds.includes(check.baselineId) || !allSnapshotsBeforeIds.includes(check.actualSnapshotId.valueOf())) {
abandonedChecks.push(check._id.valueOf());
}
}
taskOutput('> calculate empty tests', res);
const checksUniqueTests = (await Check.find().lean().distinct('test').exec()).map((x: any) => x.valueOf());
const emptyTests: any[] = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [index, test] of allTestsBefore.entries()) {
if (!checksUniqueTests.includes(test._id.valueOf())) {
emptyTests.push(test._id.valueOf());
}
}
taskOutput('> calculate empty runs', res);
const checksUniqueRuns = (await Check.find().distinct('run').exec()).map((x: any) => x.valueOf());
const emptyRuns: any[] = [];
for (const run of allRunsBefore) {
if (!checksUniqueRuns.includes(run._id.valueOf())) {
emptyRuns.push(run._id.valueOf());
}
}
taskOutput('> calculate empty suites', res);
const checksUniqueSuites = (await Check.find().distinct('suite').exec()).map((x: any) => x.valueOf());
const emptySuites: any[] = [];
for (const suite of allSuitesBefore) {
if (!checksUniqueSuites.includes(suite._id.valueOf())) {
emptySuites.push(suite._id.valueOf());
}
}
taskOutput('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', res);
taskOutput('Current inconsistent items:', res);
const inconsistentStatTable = stringTable.create([
{ item: 'empty suites', count: emptySuites.length },
{ item: 'empty runs', count: emptyRuns.length },
{ item: 'empty tests', count: emptyTests.length },
{ item: 'abandoned checks', count: abandonedChecks.length },
{ item: 'abandoned snapshots', count: abandonedSnapshots.length },
{ item: 'abandoned files', count: abandonedFiles.length },
]);
taskOutput(inconsistentStatTable, res);
if (options.clean) {
taskOutput('---------------------------------', res);
taskOutput('STAGE #3: Remove non consistent items', res);
taskOutput('> remove empty suites', res);
await Suite.deleteMany({ _id: { $in: emptySuites } });
taskOutput('> remove empty runs', res);
await Run.deleteMany({ _id: { $in: emptyRuns } });
taskOutput('> remove empty tests', res);
await Test.deleteMany({ _id: { $in: emptyTests } });
taskOutput('> remove abandoned checks', res);
await Check.deleteMany({ _id: { $in: abandonedChecks } });
taskOutput('> remove abandoned snapshots', res);
await Snapshot.deleteMany({ _id: { $in: abandonedSnapshots } });
taskOutput('> remove abandoned files', res);
await Promise.all(abandonedFiles.map((filename) => fsp.unlink(path.join(config.defaultImagesPath, filename))));
const allFilesAfter = fs.readdirSync(config.defaultImagesPath, { withFileTypes: true })
.filter((item: any) => !item.isDirectory())
.map((x: any) => x.name)
.filter((x: any) => x.includes('.png'));
taskOutput('STAGE #4: Calculate Common stats after cleaning', res);
taskOutput('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', res);
taskOutput('Current items:', res);
const afterStatTable = stringTable.create([
{ item: 'suites', count: await Suite.countDocuments() },
{ item: 'runs', count: await Run.countDocuments() },
{ item: 'tests', count: await Test.countDocuments() },
{ item: 'checks', count: await Check.countDocuments() },
{ item: 'snapshots', count: await Snapshot.countDocuments() },
{ item: 'files', count: allFilesAfter.length },
]);
taskOutput(afterStatTable, res);
}
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
taskOutput(`> Done in ${elapsedSeconds} seconds, ${elapsedSeconds / 60} min`, res);
taskOutput('- end...\n', res);
} catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : String(e);
log.error(errMsg);
taskOutput(errMsg, res);
} finally {
res.end();
}
};
const task_remove_old_logs = async (options: any, res: any) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Content-Encoding': 'none',
});
const trashHoldDate = subDays(new Date(), parseInt(options.days, 10));
const filter = { timestamp: { $lt: trashHoldDate } };
const allLogsCountBefore = await Log.find({}).countDocuments();
const oldLogsCount = await Log.find(filter).countDocuments();
taskOutput(`- the count of all documents is: '${allLogsCountBefore}'\n`, res);
taskOutput(`- the count of documents to be removed is: '${oldLogsCount}'\n`, res);
if (options.statistics === 'false') {
taskOutput(`- will remove all logs older that: '${options.days}' days, '${dateToISO8601(trashHoldDate)}'\n`, res);
await Log.deleteMany(filter);
const allLogsCountAfter = await Log.find({}).countDocuments();
taskOutput(`- the count of all documents now is: '${allLogsCountAfter}'\n`, res);
}
taskOutput('> Done', res);
res.end();
};
const task_handle_old_checks = async (options: any, res: any) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Content-Encoding': 'none',
});
try {
const startTime = process.hrtime();
taskOutput('- starting...\n', res);
taskOutput('STAGE #1 Calculate common stats', res);
const trashHoldDate = subDays(new Date(), parseInt(options.days, 10));
taskOutput('> get all checks data', res);
const allChecksBefore = await Check.find().lean().exec();
taskOutput('> get snapshots data', res);
const allSnapshotsBefore = await Snapshot.find().lean().exec();
taskOutput('> get files data', res);
const allFilesBefore = (await fsp.readdir(config.defaultImagesPath, { withFileTypes: true }))
.filter((item: any) => !item.isDirectory())
.map((x: any) => x.name)
.filter((x: any) => x.includes('.png'));
taskOutput('> get old checks data', res);
const oldChecks = await Check.find({ createdDate: { $lt: trashHoldDate } }).lean().exec();
taskOutput('>>> collect all baselineIds for old Checks ', res);
const oldSnapshotsBaselineIdIds = oldChecks.map((x: any) => x.baselineId).filter((x: any) => x);
taskOutput('>>> collect all actualSnapshotId for old Checks ', res);
const oldSnapshotsActualSnapshotIdIds = oldChecks.map((x: any) => x.actualSnapshotId).filter((x: any) => x);
taskOutput('>>> collect all diffId for old Checks ', res);
const oldSnapshotsDiffIds = oldChecks.map((x: any) => x.diffId).filter((x: any) => x);
taskOutput('>>> calculate all unique snapshots ids for old Checks ', res);
const allOldSnapshotsUniqueIds = Array.from(new Set([...oldSnapshotsBaselineIdIds, ...oldSnapshotsActualSnapshotIdIds, ...oldSnapshotsDiffIds]))
.map((x: any) => x.valueOf());
taskOutput('>>> collect all old snapshots', res);
const oldSnapshots = await Snapshot.find({ _id: { $in: allOldSnapshotsUniqueIds } }).lean();
const outTable = stringTable.create([
{ item: 'all checks', count: allChecksBefore.length },
{ item: 'all snapshots', count: allSnapshotsBefore.length },
{ item: 'all files', count: allFilesBefore.length },
{ item: `checks older than: '${options.days}' days`, count: oldChecks.length },
{ item: 'old snapshots baseline ids', count: oldSnapshotsBaselineIdIds.length },
{ item: 'old snapshots actual snapshotId', count: oldSnapshotsActualSnapshotIdIds.length },
{ item: 'old snapshots diffIds', count: oldSnapshotsDiffIds.length },
{ item: 'all old snapshots unique Ids', count: allOldSnapshotsUniqueIds.length },
{ item: 'all old snapshots', count: oldSnapshots.length },
]);
taskOutput(outTable, res);
if (options.remove === 'true') {
taskOutput(`STAGE #2 Remove checks that older that: '${options.days}' days, '${dateToISO8601(trashHoldDate)}'\n`, res);
taskOutput('> remove checks', res);
const checkRemovingResult = await Check.deleteMany({ createdDate: { $lt: trashHoldDate } });
taskOutput(`>>> removed: '${checkRemovingResult.deletedCount}'`, res);
taskOutput('> remove snapshots', res);
taskOutput('>> collect data to removing', res);
taskOutput('>>> get all baselines snapshots id`s', res);
const baselinesSnapshotsIds = (await Baseline.find({}).distinct('snapshootId'));
taskOutput('>>> get all checks snapshots baselineId', res);
const checksSnapshotsBaselineId = (await Check.find({}).distinct('baselineId'));
taskOutput('>>> get all checks snapshots actualSnapshotId', res);
const checksSnapshotsActualSnapshotId = (await Check.find({}).distinct('actualSnapshotId'));
taskOutput('>> remove baselines snapshots', res);
taskOutput('>> remove all old snapshots that not related to new baseline and check items', res);
const removedByBaselineSnapshotsResult = await Snapshot.deleteMany({
$and: [
{ _id: { $nin: checksSnapshotsBaselineId } },
{ _id: { $nin: checksSnapshotsActualSnapshotId } },
{ _id: { $nin: baselinesSnapshotsIds } },
{ _id: { $in: oldSnapshotsBaselineIdIds } },
],
});
taskOutput(`>>> removed: '${removedByBaselineSnapshotsResult.deletedCount}'`, res);
taskOutput('>> remove actual snapshots', res);
taskOutput('>> remove all old snapshots that not related to new baseline and check items', res);
const removedByActualSnapshotsResult = await Snapshot.deleteMany({
$and: [
{ _id: { $nin: checksSnapshotsBaselineId } },
{ _id: { $nin: checksSnapshotsActualSnapshotId } },
{ _id: { $nin: baselinesSnapshotsIds } },
{ _id: { $in: oldSnapshotsActualSnapshotIdIds } },
],
});
taskOutput(`>>> removed: '${removedByActualSnapshotsResult.deletedCount}'`, res);
taskOutput('>> remove all old diff snapshots', res);
const removedByDiffSnapshotsResult = await Snapshot.deleteMany({
$and: [
{ _id: { $in: oldSnapshotsDiffIds } },
],
});
taskOutput(`>>> removed: '${removedByDiffSnapshotsResult.deletedCount}'`, res);
taskOutput('> remove files', res);
taskOutput('>>> collect all old snapshots filenames', res);
const oldSnapshotsUniqueFilenames = Array.from(new Set(oldSnapshots.map((x: any) => x.filename)));
taskOutput(`>> found: ${oldSnapshotsUniqueFilenames.length}`, res);
taskOutput('> get all current snapshots filenames', res);
const allCurrentSnapshotsFilenames = await Snapshot.find().distinct('filename').exec();
taskOutput('>> calculate interception between all current snapshot filenames and old shapshots filenames', res);
const arrayIntersection = (arr1: any, arr2: any) => arr1.filter((x: any) => arr2.includes(x));
const filesInterception = arrayIntersection(allCurrentSnapshotsFilenames, oldSnapshotsUniqueFilenames);
taskOutput(`>> found: ${filesInterception.length}`, res);
taskOutput('>> calculate filenames to remove', res);
const arrayDiff = (arr1: any, arr2: any) => arr1.filter((x: any) => !arr2.includes(x));
const filesToDelete = arrayDiff(oldSnapshotsUniqueFilenames, filesInterception);
taskOutput(`>> found: ${filesToDelete.length}`, res);
taskOutput(`>> remove these files: ${filesToDelete.length}`, res);
await Promise.all(filesToDelete.map((filename: string) => fsp.unlink(path.join(config.defaultImagesPath, filename))));
taskOutput(`>> done: ${filesToDelete.length}`, res);
taskOutput('STAGE #3 Calculate common stats after Removing', res);
taskOutput('> get all checks data', res);
const allChecksAfter = await Check.find().lean().exec();
taskOutput('> get snapshots data', res);
const allSnapshotsAfter = await Snapshot.find().lean().exec();
taskOutput('> get files data', res);
const allFilesAfter = (await fsp.readdir(config.defaultImagesPath, { withFileTypes: true }))
.filter((item: any) => !item.isDirectory())
.map((x: any) => x.name)
.filter((x: any) => x.includes('.png'));
const outTableAfter = stringTable.create([
{ item: 'all checks', count: allChecksAfter.length },
{ item: 'all snapshots', count: allSnapshotsAfter.length },
{ item: 'all files', count: allFilesAfter.length },
]);
taskOutput(outTableAfter, res);
}
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
taskOutput(`> done in ${elapsedSeconds} seconds ${elapsedSeconds / 60} min`, res);
} catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : String(e);
log.error(errMsg);
taskOutput(errMsg, res);
} finally {
res.end();
}
};
const task_test = async (options = 'empty', req: ExtRequest, res: Response) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Content-Encoding': 'none',
});
const x = 1000;
// const interval = 30;
let isAborted = false;
req.on('close', () => {
isAborted = true;
});
for (let i = 0; i < x; i += 1) {
// await new Promise((r) => setTimeout(() => r(), interval));
taskOutput(`- Task Output: '${i}', options: ${options}\n`, res);
if (isAborted) {
taskOutput('the task was aborted\n', res);
log.warn('the task was aborted');
(res as any).flush();
return res.end();
}
}
return res.end();
};
export {
task_test,
task_handle_old_checks,
task_handle_database_consistency,
task_remove_old_logs,
status,
loadTestUser,
screenshots,
};