Refactoring: decomposed ScreenUpdater and Tester processors#712
Refactoring: decomposed ScreenUpdater and Tester processors#712
Conversation
| static create(capture) { | ||
| return capture | ||
| ? new Updater(capture) | ||
| : new Updater(); |
There was a problem hiding this comment.
кажется не лучшее решение, возможно красивее будет передавать capture в метод save вторым параметром
There was a problem hiding this comment.
а зачем так делать?
почему не просто
return new Updater(capture);?
| fs.accessAsync.returns(Promise.reject()); | ||
| return assert.isRejected(tester.exec(capture, {}), NoRefImageError); | ||
| .then(() => assert.calledWith(imageStub.save, 'tmp/path')); | ||
| }); |
There was a problem hiding this comment.
тут тестов не хватает.
Нужно проверить еще, что компаратор не вызовется если refChecker.hasAccessTo вернет false и вызовется если вернет true.
tormozz48
left a comment
There was a problem hiding this comment.
В общем у меня есть существенное замечание про то, что использование классов в ходе данного рефактринга неоправданно. Лучше сделать отдельный модуль который бы экспортировал отдельные методы для проверки существования файла на файловой системе и пр.
| @@ -0,0 +1,13 @@ | |||
| 'use strict'; | |||
There was a problem hiding this comment.
А тебе тут точно нужен класс?
У тебя же нет никакого внутреннего состояния
| @@ -0,0 +1,16 @@ | |||
| 'use strict'; | |||
There was a problem hiding this comment.
Аналогичный вопрос по необходимость класса
| return new RefChecker(); | ||
| } | ||
|
|
||
| hasAccessTo(referencePath) { |
There was a problem hiding this comment.
Здесь ты по сути проверяешь существования файла на файловой системе.
И в название методы ты зашиваешь имя соответствующего нодового метода.
Это неправильно. Лучше назвать метод refExists или existsRef или existsRefImage
There was a problem hiding this comment.
Возможно мы не совсем понимаем идею, которую вкладывал @j0tunn в необходимый рефакторинг.
Сейчас ты создал кучу классов типа comparator, reference-checker и т д, которым свой контекст this вообще не нужен, то есть все это можно вынести в отдельный файл utils как простые методы.
Давайте с Тохой это обсудим)
| } | ||
|
|
||
| copy(currentPath, referencePath) { | ||
| return fs.copyAsync(currentPath, referencePath).thenReturn(true); |
There was a problem hiding this comment.
не нравится то, что функция вдруг boolean возвращает.
There was a problem hiding this comment.
Она и до этого boolean возвращала.
Так как вот тут - https://github.com/gemini-testing/gemini/blob/master/lib/state-processor/capture-processor/screen-updater/screen-updater.js#L16
нужно понимать обновился эталон или нет.
| save(referencePath) { | ||
| return fs.mkdirsAsync(path.dirname(referencePath)) | ||
| .then(() => this._capture.image.save(referencePath)) | ||
| .thenReturn(true); |
| static create(capture) { | ||
| return capture | ||
| ? new Updater(capture) | ||
| : new Updater(); |
There was a problem hiding this comment.
а зачем так делать?
почему не просто
return new Updater(capture);?
|
|
||
| save(referencePath) { | ||
| return fs.mkdirsAsync(path.dirname(referencePath)) | ||
| .then(() => this._capture.image.save(referencePath)) |
There was a problem hiding this comment.
а если this._capture === undefined ?
|
|
||
| afterEach(() => sandbox.restore()); | ||
|
|
||
| it('should have static factory creation method', () => { |
There was a problem hiding this comment.
Ну вот некоторые наши коллеги мне заворачивали такой тест по причине ненужности
| sandbox.stub(fs, 'copyAsync'); | ||
| }); | ||
|
|
||
| it('should save current image to reference path', () => { |
There was a problem hiding this comment.
save -> copy.
Ты же проверяешь и путь откуда копируется файл и путь назначения
| }; | ||
|
|
||
| it('should make directory before saving the image', () => { | ||
| const mediator = sinon.spy().named('mediator'); |
There was a problem hiding this comment.
А зачем так сложно?
Разве у тебя не отработает правильно без этого медиатора?
There was a problem hiding this comment.
Так я же могу в коде просто написать:
fs.mkdirsAsync(refPath);
capture.image.save(refPath);Т.е. сохранение изображение запустится сразу же не дожидаясь создалась директория или нет и мой тест без медиатора пройдет. Так как стабы вызовутся в правильном порядке.
| fs.mkdirsAsync.returns(Promise.resolve()); | ||
|
|
||
| return save_({refPath: '/ref/path'}) | ||
| .then(() => { |
| }); | ||
| }); | ||
|
|
||
| it('should save image with correct path', () => { |
There was a problem hiding this comment.
with correct -> with given
40e692f to
cdcf946
Compare
| canHaveCaret: capture.canHaveCaret, | ||
| tolerance: opts.tolerance, | ||
| pixelRatio: opts.pixelRatio | ||
| }; |
There was a problem hiding this comment.
кстати таки один класс можно создать, так как эта логика используется в двух местах.
Ну типо создать класс Comparator, который в конструкторе принимает capture и opts и инициализирует эти опции.
|
|
||
| const capture = {image: imageStub}; | ||
| const DiffScreenUpdater = proxyquire( | ||
| 'lib/state-processor/capture-processor/screen-updater/diff-screen-updater', |
| }); | ||
| }); | ||
|
|
||
| it('should save current image with "png" extension', () => { |
There was a problem hiding this comment.
название теста не соответствует тому, что проверяется в тесте.
Ты тут проверяешь, что temp.path вызывается с нужными аргментами, а не то, что картинка сохраняется.
То, что картинка сохраняется, ты проверяешь ниже в тесте
|
|
||
| afterEach(() => sandbox.restore()); | ||
|
|
||
| it('should not save current image if reference image does not exist', () => { |
There was a problem hiding this comment.
not save a current image if a reference image
| return exec_().then(() => assert.calledWith(temp.path, {suffix: '.png'})); | ||
| }); | ||
|
|
||
| it('should save current image to the temporary directory', () => { |
There was a problem hiding this comment.
to the temporary –> to a temporaty
| .then(() => assert.callOrder(fs.mkdirsAsync, mediator, imageStub.save)); | ||
| }); | ||
|
|
||
| it('should save image with given path', () => { |
| }); | ||
|
|
||
| it('should save image with given path', () => { | ||
| fs.mkdirsAsync.returns(Promise.resolve()); |
| }); | ||
|
|
||
| describe('existsRef', () => { | ||
| it('should fulfill promise if reference image exists', () => { |
|
|
||
| describe('existsRef', () => { | ||
| it('should fulfill promise if reference image exists', () => { | ||
| fs.accessAsync.returns(Promise.resolve()); |
| return assert.isFulfilled(utils.existsRef('/existent/path')); | ||
| }); | ||
|
|
||
| it('should reject with error if reference image does not exist', () => { |
There was a problem hiding this comment.
should be rejected with an error
j0tunn
left a comment
There was a problem hiding this comment.
Все равно ведь дублирование осталось. Моя мысль сводилась к тому, что у нас по сути есть всего две операции: проверка эталона и сравнение изображений, а различное поведение capture процессоров формируется в зависимости от реакции на эти две операции. Вот и хочется иметь какой-то набор функций/класов, а конкретный процессор формировать в фабрике под запрос. Типа такого:
exports.create = function(type) {
if (type === 'tester') {
return new CaptureProcessor()
.onNoReference((refPath) => throw new NoRefImageError(refPath))
.onEqual((refPath, currPath) => ({refPath, currPath, equal: true}))
.onDiff((refPath, currPath) => ({refPath, currPath, equal: false}));
}
const saveRef = (refPath, capture) => utils.saveRef(refPath, capture);
if (type === 'new-updater') {
return new CaptureProcessor()
.onNoReference(saveRef);
}
const updateRef = (refPath, currPath) => utils.copyImage(currPath, refPath);
if (type === 'diff-updater') {
return new CaptureProcessor()
.onDiff(updateRef);
}
if (type === 'meta-updater') {
return new CaptureProcessor()
.onNoReference(saveRef)
.onDiff(updateRef);
}
}А внутри CaptureProcessor уже реализуешь логику:
- когда нужно позвать тот или иной хэндлер
- выполняем сравнение (и зовем соответствующие хэндлеры) только если есть хоть один хэндлер
onEqualилиonDiff, и если существует эталон
Как-то так
| return fs.copyAsync(currentPath, referencePath).thenReturn(true); | ||
| return isEqual | ||
| ? false | ||
| : utils.copyImg(currentPath, referencePath); |
There was a problem hiding this comment.
.then((equal) => !equal && utils.copyImg(currentPath, refPath));8ea2ef0 to
4d65706
Compare
| return utils.saveRef(referencePath, capture) | ||
| .then((updated) => ({imagePath: referencePath, updated})); | ||
| }; | ||
| if (type === 'new-updater') { |
There was a problem hiding this comment.
Здесь лучше сделать пустую строку перед if
| return utils.copyImg(currentPath, referencePath) | ||
| .then((updated) => ({imagePath: referencePath, updated})); | ||
| }; | ||
| if (type === 'diff-updater') { |
There was a problem hiding this comment.
Здесь лучше сделать пустую строку перед if
| const CaptureProcessor = require('./'); | ||
| const utils = require('./utils'); | ||
|
|
||
| exports.create = (type) => { |
There was a problem hiding this comment.
Плохо читается:
Давай здесь перепишем по-другому:
const notUpdated = (referencePath) => ({imagePath: referencePath, updated: false});
const saveRef = (referencePath, capture) => {
return utils.saveRef(referencePath, capture)
.then((updated) => ({imagePath: referencePath, updated}));
};
const updateRef = (referencePath, currentPath) => {
return utils.copyImg(currentPath, referencePath)
.then((updated) => ({imagePath: referencePath, updated}));
};
exports.create = (type) => {
if (type === 'tester') {
....
}
if (type === 'new-updater') {
....
}
if (type === 'diff-updater') {
....
}
if (type === 'meta-updater') {
.....
}
}| exports.saveRef = (refPath, capture) => { | ||
| return fs.mkdirsAsync(path.dirname(refPath)) | ||
| .then(() => capture.image.save(refPath)) | ||
| .thenReturn(true) |
There was a problem hiding this comment.
Нет.
Давай не будем использовать вот такие штуки.
return fs.mkdirsAsync(path.dirname(refPath))
.then(() => capture.image.save(refPath))
.then(_.stubTrue)
.catch(_.stubFalse)There was a problem hiding this comment.
Ну мне не очень нравятся эти стабы из лодаша. Давай тогда так напишем:
return fs.mkdirsAsync(path.dirname(refPath))
.then(() => capture.image.save(refPath))
.then(() => true)
.catch(() => false)| }; | ||
|
|
||
| exports.existsRef = (refPath) => fs.accessAsync(refPath) | ||
| .thenReturn(true) |
| }; | ||
|
|
||
| function identifyUpdaterType(opts) { | ||
| let type; |
There was a problem hiding this comment.
ИМХО лишняя переменная.
Ты же можешь писать вот так:
if (opts.diff && !opts.new) {
return 'diff-updater';
}
if (!opts.diff && opts.new) {
return 'new-updater';
}
return 'meta-updater';| this._onDiffHandler = null; | ||
| } | ||
|
|
||
| onReference(handler) { |
There was a problem hiding this comment.
Хорошая идея. Я кстати такой подход использовал в dashboard при проектировании базового Job-а
| return utils.existsRef(referencePath) | ||
| .then((isRefExists) => { | ||
| if (!isRefExists) { | ||
| return this._onNoRefHandler |
There was a problem hiding this comment.
Вот тут непонятно. При каких условиях у тебя может быть или не быть обработчика this._onNoRefHandler ? Не переусложнил ли ты в этом месте код?
There was a problem hiding this comment.
по хорошему вместо проверок при вызове имеет смысл менять поведения объекта во время записи. Смотри, вызовы on* у тебя происходят только при создании объекта, а потом этот объект может несколько раз использоваться. В итоге можно просесть в производительности (да, в gemini у нас сейчас такие объекты одноразовые, но здесь тоже можно сделать какое-нибудь кэширование).
Т.е. лучше при построении создать объект, который уже никакие проверки внутри себя не делает
| : Promise.reject(new NoRefImageError(referencePath)); | ||
| } | ||
|
|
||
| if (!this._onEqualHandler && !this._onDiffHandler) { |
There was a problem hiding this comment.
Может быть здесь лучше написать вот так:
return (this._onEqualHandler && this._onDiffHandler)
? this._compareImages(capture, opts)
: this._onRefHandler(referencePath);There was a problem hiding this comment.
блин не заметил это замечание.
Спасибо, поправил.
| @@ -0,0 +1,4 @@ | |||
| global.assert.calledOnceWith = function() { | |||
| return processor.exec(capture, opts); | ||
| }; | ||
| }; | ||
| let exec_; |
| return assert.isRejected(exec_({refPath: '/non-existent/path'}), NoRefImageError); | ||
| }); | ||
|
|
||
| describe('should return image comparison result', () => { |
There was a problem hiding this comment.
Можно было бы вот так:
describe('should return image comparison result if images are', () => {
it('equal', () => {});
it('different', () => {});
});There was a problem hiding this comment.
И вообще здесь не совсем понятно, что проверяется:
assert.deepEqual(result, {
currentPath: '/temp/path',
referencePath: '/ref/path',
equal: true
});Предлагаю разбить на 3 теста:
- Ты проверяешь, что есть
currentPath: '/temp/path', иreferencePath: '/ref/path'` (а может быть даже в 2-х отдельных тестах) - Проверяешь, что equal=true для одинаковых изображений.
- Проверяешь, что equal=false для разных изображений.
There was a problem hiding this comment.
Я тут сразу проверяю весь результат который возвращается при сравнении изображений. Это позволяет быть уверенным, что объект не содержит каких то новых полей. Не уверен, что его нужно разбивать так. В итоге же получится везде одинаковый вызов, но проверка по кусочкам. Тем более тут же тест не сложный получается.
| exec_ = mkExecMethod(newUpdater); | ||
| }); | ||
|
|
||
| it('should not save a reference image if it is already exists', () => { |
There was a problem hiding this comment.
Ну приехали.
Ты проверяешь, что эталон не сохраняется и для этого у тебя есть assert.notCalled(utils.saveRef);
Зачем второй assert ?
There was a problem hiding this comment.
Или второй ассерт тоже "в тему"?
There was a problem hiding this comment.
Да, все-таки твой вариант правильный
| }); | ||
| }); | ||
|
|
||
| it('should save a reference image if it does not exist', () => { |
| }); | ||
| }); | ||
| }); | ||
| // 'use strict'; |
There was a problem hiding this comment.
А это что за закоментированный код ?
| it('should make a directory before saving the image', () => { | ||
| const mediator = sinon.spy().named('mediator'); | ||
|
|
||
| fs.mkdirsAsync.callsFake(() => Promise.delay(1).then(mediator)); |
There was a problem hiding this comment.
И опять непонятно про медиатор
There was a problem hiding this comment.
выше писал, для чего так делаю
d2ca22b to
90b67cf
Compare
| @@ -0,0 +1,44 @@ | |||
| 'use strict'; | |||
There was a problem hiding this comment.
я обычно фабрику кладу в index.js, а базовый класс в одноименный файл, т.е. можно переименовать: factory.js -> index.js, index.js -> capture-processor.j.
По большому счету входная точка-то здесь как раз фабрика, т.е. правильно будет просто подключить папку: require('./capture-processor')
| return utils.existsRef(referencePath) | ||
| .then((isRefExists) => { | ||
| if (!isRefExists) { | ||
| return this._onNoRefHandler |
There was a problem hiding this comment.
по хорошему вместо проверок при вызове имеет смысл менять поведения объекта во время записи. Смотри, вызовы on* у тебя происходят только при создании объекта, а потом этот объект может несколько раз использоваться. В итоге можно просесть в производительности (да, в gemini у нас сейчас такие объекты одноразовые, но здесь тоже можно сделать какое-нибудь кэширование).
Т.е. лучше при построении создать объект, который уже никакие проверки внутри себя не делает
| super({ | ||
| module: require.resolve('./capture-processor/tester'), | ||
| constructorArg: config.system.diffColor | ||
| module: require.resolve('./capture-processor/factory'), |
There was a problem hiding this comment.
родителю теперь достаточно передать только тип процессора, и можно обойтись без объекта
| return 'new-updater'; | ||
| } | ||
|
|
||
| return 'meta-updater'; |
There was a problem hiding this comment.
return opts.diff && !opts.new && 'diff-updater'
|| !opts.diff && opts.new && 'new-updater'
|| 'meta-updater'и можно в функцию не выносить
432895a to
a1fffe5
Compare
j0tunn
left a comment
There was a problem hiding this comment.
Еще файлы, которые затрагивал, на es6 переведи, и норм.
| if (type === 'diff-updater') { | ||
| return CaptureProcessor.create() | ||
| .onReference(_.noop) | ||
| .onNoReference(notUpdated) |
There was a problem hiding this comment.
а мы сейчас разве в этом случае не бросаем ошибку?
There was a problem hiding this comment.
Нет - https://github.com/gemini-testing/gemini/blob/master/lib/state-processor/capture-processor/screen-updater/diff-screen-updater.js#L12
Перед тем как влить еще раз внимательно проверю, что поведение не изменилось при вызове с любой из опций + прогоню интеграционно
| exports.create = (type) => { | ||
| if (type === 'tester') { | ||
| return CaptureProcessor.create() | ||
| .onReference(_.noop) |
There was a problem hiding this comment.
можно по идее по умолчанию в _onRefHandler записать _.noop (ну или даже во все хэндлеры по умолчанию записать _.noop). Тогда тебе вот это писать не нужно будет
| } | ||
| }; | ||
|
|
||
| function identifyUpdaterType(opts) { |
There was a problem hiding this comment.
ну я бы не выносил это в отдельную функцию. Просто в конструкторе сохранил бы переменную updaterType
5c4ca22 to
ffea13b
Compare
ec225cf to
9fa38b5
Compare
Currently ScreenUpdater and Tester processors have the same logic about saving reference images.
Proposed changes: