Skip to content

Commit 80d587a

Browse files
Merge pull request #467 from gemini-testing/HERMIONE-781.undo
feat: undo screen accept
2 parents a19d96b + 312e0eb commit 80d587a

File tree

34 files changed

+1047
-68
lines changed

34 files changed

+1047
-68
lines changed

lib/common-utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
const crypto = require('crypto');
34
const {pick} = require('lodash');
45
const url = require('url');
56
const axios = require('axios');
@@ -15,6 +16,10 @@ const {
1516
QUEUED
1617
} = require('./constants/test-statuses');
1718

19+
exports.getShortMD5 = (str) => {
20+
return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7);
21+
};
22+
1823
const statusPriority = [
1924
// non-final
2025
RUNNING, QUEUED,

lib/gui/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ module.exports = class App {
5555
return this._toolRunner.updateReferenceImage(failedTests);
5656
}
5757

58+
undoAcceptImages(imageIds) {
59+
return this._toolRunner.undoAcceptImages(imageIds);
60+
}
61+
5862
async findEqualDiffs(data) {
5963
return this._toolRunner.findEqualDiffs(data);
6064
}

lib/gui/server.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ exports.start = async ({paths, hermione, guiApi, configs}) => {
8989
.catch(({message}) => res.status(INTERNAL_SERVER_ERROR).send({error: message}));
9090
});
9191

92+
server.post('/undo-accept-images', (req, res) => {
93+
app.undoAcceptImages(req.body)
94+
.then((updated) => res.json(updated))
95+
.catch(({message}) => res.status(INTERNAL_SERVER_ERROR).send({error: message}));
96+
});
97+
9298
server.post('/get-find-equal-diffs-data', (req, res) => {
9399
try {
94100
const data = app.getImageDataToFindEqualDiffs(req.body);

lib/gui/tool-runner/index.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const {logger} = require('../../common-utils');
1515
const reporterHelper = require('../../reporter-helpers');
1616
const {UPDATED} = require('../../constants/test-statuses');
1717
const {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} = require('../../constants/database');
18-
const {formatId, getShortMD5, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} = require('./utils');
18+
const {getShortMD5} = require('../../common-utils');
19+
const {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} = require('./utils');
1920
const {getTestsTreeFromDatabase} = require('../../db-utils/server');
2021

2122
module.exports = class ToolRunner {
@@ -120,6 +121,19 @@ module.exports = class ToolRunner {
120121
});
121122
}
122123

124+
async undoAcceptImages(imageIds) {
125+
const {
126+
referencesToRemove,
127+
referencesToUpdate,
128+
...rest
129+
} = await this._reportBuilder.undoAcceptImages(imageIds, this._reportPath);
130+
131+
await reporterHelper.overwriteReferenceImage(referencesToUpdate, this._reportPath);
132+
await reporterHelper.removeReferenceImage(referencesToRemove);
133+
134+
return rest;
135+
}
136+
123137
async findEqualDiffs(images) {
124138
const [selectedImage, ...comparedImages] = images;
125139
const {tolerance, antialiasingTolerance} = this.config;

lib/gui/tool-runner/utils.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const _ = require('lodash');
44
const path = require('path');
55
const fs = require('fs-extra');
66
const chalk = require('chalk');
7-
const crypto = require('crypto');
87
const Database = require('better-sqlite3');
98

109
const {logger} = require('../../common-utils');
@@ -13,10 +12,6 @@ const {mergeTables} = require('../../db-utils/server');
1312

1413
exports.formatId = (hash, browserId) => `${hash}/${browserId}`;
1514

16-
exports.getShortMD5 = (str) => {
17-
return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7);
18-
};
19-
2015
exports.mkFullTitle = ({suite, state}) => {
2116
return suite.path.length > 0
2217
? `${suite.path.join(' ')} ${state.name}`

lib/report-builder/gui.js

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
'use strict';
22

33
const _ = require('lodash');
4+
const path = require('path');
5+
46
const StaticReportBuilder = require('./static');
57
const GuiTestsTreeBuilder = require('../tests-tree-builder/gui');
68
const {IDLE, RUNNING, SKIPPED, FAIL, SUCCESS, UPDATED} = require('../constants/test-statuses');
79
const {hasResultFails, hasNoRefImageErrors} = require('../static/modules/utils');
8-
const {isSkippedStatus} = require('../common-utils');
9-
const {getConfigForStaticFile} = require('../server-utils');
10+
const {isSkippedStatus, isUpdatedStatus} = require('../common-utils');
11+
const {getConfigForStaticFile, deleteFile} = require('../server-utils');
12+
const {DB_COLUMNS} = require('../constants/database');
1013

1114
module.exports = class GuiReportBuilder extends StaticReportBuilder {
1215
static create(...args) {
@@ -103,6 +106,78 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder {
103106
return isUpdated ? attempt : attempt + 1;
104107
}
105108

109+
async undoAcceptImages(imageIds, reportPath) {
110+
const updatedImages = [], removedResults = [], referencesToRemove = [], referencesToUpdate = [];
111+
112+
for (const imageId of imageIds) {
113+
const {
114+
updatedImage,
115+
removedResult,
116+
referenceToRemove,
117+
referenceToUpdate
118+
} = await this._undoAcceptImage(imageId, reportPath);
119+
120+
updatedImage && updatedImages.push(updatedImage);
121+
removedResult && removedResults.push(removedResult);
122+
referenceToRemove && referencesToRemove.push(referenceToRemove);
123+
referenceToUpdate && referencesToUpdate.push(referenceToUpdate);
124+
}
125+
126+
return {updatedImages, removedResults, referencesToRemove, referencesToUpdate};
127+
}
128+
129+
async _undoAcceptImage(imageId, reportPath) {
130+
const {
131+
suitePath,
132+
browserName,
133+
stateName,
134+
status,
135+
timestamp,
136+
image,
137+
previousImage,
138+
resultIdToRemove
139+
} = this._testsTree.getResultDataToUnacceptImage(imageId);
140+
141+
if (!isUpdatedStatus(status)) {
142+
return {};
143+
}
144+
145+
const fullTitle = suitePath.join(' ');
146+
const previousExpectedImgPath = _.get(previousImage, 'expectedImg.path', null);
147+
const refImgPath = previousImage.refImg.path;
148+
const currentExpectedImgPath = image.expectedImg.path;
149+
const shouldRemoveReference = _.isNull(previousImage.refImg.size);
150+
let updatedImage, removedResult, referenceToRemove, referenceToUpdate;
151+
152+
await this._removeImageFromReport(reportPath, currentExpectedImgPath);
153+
this._updateTestExpectedPath(fullTitle, browserName, stateName, previousExpectedImgPath);
154+
155+
if (shouldRemoveReference) {
156+
referenceToRemove = refImgPath;
157+
} else {
158+
referenceToUpdate = {path: refImgPath, source: previousExpectedImgPath};
159+
}
160+
161+
if (resultIdToRemove) {
162+
this._testsTree.removeTestResult(resultIdToRemove);
163+
this._decreaseTestAttemptNumber(fullTitle, browserName);
164+
165+
removedResult = resultIdToRemove;
166+
} else {
167+
updatedImage = this._testsTree.updateImageInfo(imageId, previousImage);
168+
}
169+
170+
this._deleteTestResultFromDb({where: [
171+
`${DB_COLUMNS.SUITE_PATH} = ?`,
172+
`${DB_COLUMNS.NAME} = ?`,
173+
`${DB_COLUMNS.STATUS} = ?`,
174+
`${DB_COLUMNS.TIMESTAMP} = ?`,
175+
`json_extract(${DB_COLUMNS.IMAGES_INFO}, '$[0].stateName') = ?`
176+
].join(' AND ')}, JSON.stringify(suitePath), browserName, status, timestamp, stateName);
177+
178+
return {updatedImage, removedResult, referenceToRemove, referenceToUpdate};
179+
}
180+
106181
_addTestResult(formattedResult, props, opts = {}) {
107182
super._addTestResult(formattedResult, props);
108183

@@ -156,4 +231,10 @@ module.exports = class GuiReportBuilder extends StaticReportBuilder {
156231
});
157232
}
158233
}
234+
235+
async _removeImageFromReport(reportPath, imgPath) {
236+
const imageAbsolutePath = path.resolve(process.cwd(), reportPath, imgPath);
237+
238+
await deleteFile(imageAbsolutePath);
239+
}
159240
};

lib/report-builder/static.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ module.exports = class StaticReportBuilder {
143143
this._sqliteAdapter.write({testResult, suitePath, suiteName});
144144
}
145145

146+
_deleteTestResultFromDb({where, orderBy, orderDescending, limit}, ...args) {
147+
this._sqliteAdapter.delete({where, orderBy, orderDescending, limit}, ...args);
148+
}
149+
150+
_updateTestExpectedPath(fullTitle, browserId, stateName, expectedPath) {
151+
TestAdapter.updateCacheExpectedPath(fullTitle, browserId, stateName, expectedPath);
152+
}
153+
154+
_decreaseTestAttemptNumber(fullTitle, browserName) {
155+
TestAdapter.decreaseAttemptNumber(fullTitle, browserName);
156+
}
157+
146158
async finalize() {
147159
this._sqliteAdapter.close();
148160

lib/reporter-helpers.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ exports.updateReferenceImage = (testResult, reportPath, stateName) => {
1616
utils.copyFileAsync(src, utils.getReferenceAbsolutePath(testResult, reportPath, stateName))
1717
]);
1818
};
19+
20+
exports.removeReferenceImage = (refImgPaths) => {
21+
return Promise.map([].concat(refImgPaths), (refImgPath) => utils.deleteFile(refImgPath));
22+
};
23+
24+
exports.overwriteReferenceImage = (referencesToUpdate, reportPath) => {
25+
return Promise.map([].concat(referencesToUpdate), (referenceToUpdate) => {
26+
return utils.copyFileAsync(path.resolve(reportPath, referenceToUpdate.source), referenceToUpdate.path);
27+
});
28+
};

lib/server-utils.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ function copyFileAsync(srcPath, destPath, reportDir = '') {
5252
return makeDirFor(resolvedDestPath).then(() => fs.copy(srcPath, resolvedDestPath));
5353
}
5454

55+
/**
56+
* @param {String} filePath
57+
* @returns {Promise}
58+
*/
59+
function deleteFile(filePath) {
60+
return fs.remove(filePath);
61+
}
62+
5563
/**
5664
* @param {String} destPath
5765
*/
@@ -268,6 +276,7 @@ module.exports = {
268276
getDiffAbsolutePath,
269277

270278
copyFileAsync,
279+
deleteFile,
271280
makeDirFor,
272281

273282
logPathToHtmlReport,

lib/sqlite-adapter.js

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const Database = require('better-sqlite3');
66
const NestedError = require('nested-error-stacks');
77
const debug = require('debug')('html-reporter:sqlite-adapter');
88

9-
const {getShortMD5} = require('./gui/tool-runner/utils.js');
9+
const {getShortMD5} = require('./common-utils');
1010
const {createTablesQuery} = require('./db-utils/server');
1111
const {DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, LOCAL_DATABASE_NAME, DATABASE_URLS_JSON_NAME} = require('./constants/database');
1212

@@ -62,27 +62,21 @@ module.exports = class SqliteAdapter {
6262
* @param {string} queryParams.where - `WHERE ${where}`
6363
* @param {string} queryParams.orderBy - `ORDER BY ${orderBy}`
6464
* @param {boolean} queryParams.orderDescending - ASCending / DESCending
65+
* @param {number} queryParams.limit - `LIMIT ${limit}`
6566
* @param {boolean} queryParams.noCache - disables caching for equal queries
6667
* @param {...string} queryArgs
6768
*/
68-
query({select, where, orderBy, orderDescending, noCache = false} = {}, ...queryArgs) {
69+
query({select, where, orderBy, orderDescending, limit, noCache = false} = {}, ...queryArgs) {
6970
const cacheKey = !noCache && getShortMD5(`${select}#${where}#${orderBy}${orderDescending}${queryArgs.join('#')}`);
7071

7172
if (!noCache && this._queryCache.has(cacheKey)) {
7273
return this._queryCache.get(cacheKey);
7374
}
7475

75-
let query = `SELECT ${select || '*'} FROM ${DB_SUITES_TABLE_NAME}`;
76+
const sentence = `SELECT ${select || '*'} FROM ${DB_SUITES_TABLE_NAME}`
77+
+ this._createSentence({where, orderBy, orderDescending, limit});
7678

77-
if (where) {
78-
query += ` WHERE ${where}`;
79-
}
80-
81-
if (orderBy) {
82-
query += ` ORDER BY ${orderBy} ${orderDescending ? 'DESC' : 'ASC'}`;
83-
}
84-
85-
const result = this._db.prepare(query).get(...queryArgs);
79+
const result = this._db.prepare(sentence).get(...queryArgs);
8680

8781
if (!noCache) {
8882
this._queryCache.set(cacheKey, result);
@@ -99,6 +93,40 @@ module.exports = class SqliteAdapter {
9993
this._db.prepare(`INSERT INTO ${DB_SUITES_TABLE_NAME} VALUES (${placeholders})`).run(...values);
10094
}
10195

96+
/**
97+
* Delete records from database
98+
* @param {Object} deleteParams
99+
* @param {string} deleteParams.where - `WHERE ${where}`
100+
* @param {string} deleteParams.orderBy - `ORDER BY ${orderBy}`
101+
* @param {boolean} deleteParams.orderDescending - ASCending / DESCending
102+
* @param {number} deleteParams.limit - `LIMIT ${limit}`
103+
* @param {...string} deleteArgs
104+
*/
105+
delete({where, orderBy, orderDescending, limit} = {}, ...deleteArgs) {
106+
const sentence = `DELETE FROM ${DB_SUITES_TABLE_NAME}`
107+
+ this._createSentence({where, orderBy, orderDescending, limit});
108+
109+
this._db.prepare(sentence).run(...deleteArgs);
110+
}
111+
112+
_createSentence({where, orderBy, orderDescending, limit}) {
113+
let sentence = '';
114+
115+
if (where) {
116+
sentence += ` WHERE ${where}`;
117+
}
118+
119+
if (orderBy) {
120+
sentence += ` ORDER BY ${orderBy} ${orderDescending ? 'DESC' : 'ASC'}`;
121+
}
122+
123+
if (limit) {
124+
sentence += ` LIMIT ${limit}`;
125+
}
126+
127+
return sentence;
128+
}
129+
102130
_parseTestResult({suitePath, suiteName, testResult}) {
103131
const {
104132
name,

0 commit comments

Comments
 (0)