UNPKG

@syngrisi/syngrisi

Version:
537 lines (470 loc) 22.1 kB
/* eslint-disable camelcase */ const { promises: fs } = require('fs'); const stringTable = require('string-table'); const fss = require('fs'); const { config } = require('../../../config'); const { subDays, dateToISO8601 } = require('../utils'); const { ProgressBar } = require('../utils/utils'); const { Snapshot, Check, Test, Run, Log, Suite, User, Baseline, } = require('../models'); const $this = this; $this.logMeta = { scope: 'app_service', msgType: 'APP', }; function taskOutput(msg, res) { res.write(`${msg.toString()}\n`); log.debug(msg.toString(), $this); } function parseHrtimeToSeconds(hrtime) { return (hrtime[0] + (hrtime[1] / 1e9)).toFixed(3); } const status = async (currentUser) => { 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 = fss.readdirSync(config.defaultImagesPath); return files; }; const loadTestUser = async () => { const logOpts = { itemType: 'user', msgType: 'LOAD', ref: 'Administrator', }; if (process.env.SYNGRISI_TEST_MODE !== '1') { 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', $this, logOpts); const adminData = JSON.parse(fss.readFileSync('./src/server/lib/testAdmin.json')); const admin = await User.create(adminData); log.info(`test Administrator with id: '${admin._id}' was created`, $this, logOpts); return admin; } log.info(`test admin is exists: ${JSON.stringify(testAdmin, null, 2)}`, $this, logOpts); return { msg: `already exist '${testAdmin}'` }; }; const task_handle_database_consistency = async (options, res) => { // this header to response with chunks data res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Content-Encoding': 'none', 'x-no-compression': 'true', }); // res.setHeader('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 fs.readdir(config.defaultImagesPath, { withFileTypes: true })) .filter((item) => !item.isDirectory()) .map(((x) => x.name)) .filter((x) => 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); // eslint-disable-next-line const abandonedSnapshots = allSnapshotsBefore.filter((sn) => { return !fss.existsSync(`${config.defaultImagesPath}/${sn.filename}`); }); taskOutput('> calculate abandoned files', res); const snapshotsUniqueFiles = Array.from(new Set(allSnapshotsBefore.map((x) => x.filename))); const abandonedFiles = []; const progress = new ProgressBar(allFilesBefore.length); // eslint-disable-next-line no-restricted-syntax for (const [index, file] of allFilesBefore.entries()) { setTimeout(() => { progress.writeIfChange(index, allFilesBefore.length, taskOutput, res); }, 10); if (!(snapshotsUniqueFiles.includes(file.toString()))) { abandonedFiles.push(file); } } // we don't remove the abandoned checks yet, need more statistics taskOutput('> calculate abandoned checks', res); const allSnapshotsBeforeIds = allSnapshotsBefore.map((x) => x._id.valueOf()); const allChecksBeforeLight = allChecksBefore.map((x) => ({ _id: x._id.valueOf(), baselineId: x.baselineId.valueOf(), actualSnapshotId: x.actualSnapshotId.valueOf(), })); const abandonedChecks = []; const progressChecks = new ProgressBar(allChecksBefore.length); // eslint-disable-next-line no-restricted-syntax 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) => x.valueOf()); const emptyTests = []; // eslint-disable-next-line no-restricted-syntax,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) => x.valueOf()); const emptyRuns = []; // eslint-disable-next-line no-restricted-syntax for (const run of allRunsBefore) { // eslint-disable-next-line no-await-in-loop 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) => x.valueOf()); const emptySuites = []; // eslint-disable-next-line no-restricted-syntax for (const suite of allSuitesBefore) { // eslint-disable-next-line no-await-in-loop 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) => fs.unlink(`${config.defaultImagesPath}/${filename}`))); const allFilesAfter = fss.readdirSync(config.defaultImagesPath, { withFileTypes: true }) .filter((item) => !item.isDirectory()) .map(((x) => x.name)) .filter((x) => 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) { log.error(e.stack || e.toString()); taskOutput(e.stack || e, res); } finally { res.end(); } }; const task_remove_old_logs = async (options, res) => { // this header to response with chunks data 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, res) => { // this header to response with chunks data 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 fs.readdir(config.defaultImagesPath, { withFileTypes: true })) .filter((item) => !item.isDirectory()) .map(((x) => x.name)) .filter((x) => 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) => x.baselineId) .filter((x) => x); taskOutput('>>> collect all actualSnapshotId for old Checks ', res); const oldSnapshotsActualSnapshotIdIds = oldChecks.map((x) => x.actualSnapshotId) .filter((x) => x); taskOutput('>>> collect all diffId for old Checks ', res); const oldSnapshotsDiffIds = oldChecks.map((x) => x.diffId) .filter((x) => x); taskOutput('>>> calculate all unique snapshots ids for old Checks ', res); const allOldSnapshotsUniqueIds = Array.from( new Set([...oldSnapshotsBaselineIdIds, ...oldSnapshotsActualSnapshotIdIds, ...oldSnapshotsDiffIds]) ) .map((x) => 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')); // get baselineIds after removing 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); // here we give all old checks and then exclude all baselines // and all checks related to new checks with actual and baseline snapshots with such baselineId 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) => 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, arr2) => arr1.filter((x) => arr2.includes(x)); const filesInterception = arrayIntersection(allCurrentSnapshotsFilenames, oldSnapshotsUniqueFilenames); taskOutput(`>> found: ${filesInterception.length}`, res); taskOutput('>> calculate filenames to remove', res); const arrayDiff = (arr1, arr2) => arr1.filter((x) => !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) => fs.unlink(`${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 fs.readdir(config.defaultImagesPath, { withFileTypes: true })) .filter((item) => !item.isDirectory()) .map(((x) => x.name)) .filter((x) => 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) { log.error(e.stack.toString() || e); taskOutput(e.stack || e, res); } finally { res.end(); } }; const task_test = async (options = 'empty', req, res) => { // this header to response with chunks data 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) { // eslint-disable-next-line no-await-in-loop 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', $this); res.flush(); return res.end(); } } return res.end(); }; module.exports = { task_test, task_handle_old_checks, task_handle_database_consistency, task_remove_old_logs, status, loadTestUser, screenshots, };