Skip to content
This repository was archived by the owner on Sep 21, 2022. It is now read-only.

Comments

Refactoring: decomposed ScreenUpdater and Tester processors#712

Merged
DudaGod merged 3 commits intomasterfrom
refactor/screen-updater
Aug 28, 2017
Merged

Refactoring: decomposed ScreenUpdater and Tester processors#712
DudaGod merged 3 commits intomasterfrom
refactor/screen-updater

Conversation

@DudaGod
Copy link
Member

@DudaGod DudaGod commented Jan 12, 2017

Currently ScreenUpdater and Tester processors have the same logic about saving reference images.

Proposed changes:

  • create set of utils which implement particular functionality:
    • RefChecker - checks exists reference or not
    • Comparator - compare current and reference images
    • Updater - save new reference image

static create(capture) {
return capture
? new Updater(capture)
: new Updater();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

кажется не лучшее решение, возможно красивее будет передавать capture в метод save вторым параметром

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а зачем так делать?

почему не просто

return new Updater(capture);

?

fs.accessAsync.returns(Promise.reject());
return assert.isRejected(tester.exec(capture, {}), NoRefImageError);
.then(() => assert.calledWith(imageStub.save, 'tmp/path'));
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут тестов не хватает.
Нужно проверить еще, что компаратор не вызовется если refChecker.hasAccessTo вернет false и вызовется если вернет true.

Copy link
Contributor

@tormozz48 tormozz48 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В общем у меня есть существенное замечание про то, что использование классов в ходе данного рефактринга неоправданно. Лучше сделать отдельный модуль который бы экспортировал отдельные методы для проверки существования файла на файловой системе и пр.

@@ -0,0 +1,13 @@
'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А тебе тут точно нужен класс?
У тебя же нет никакого внутреннего состояния

@@ -0,0 +1,16 @@
'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Аналогичный вопрос по необходимость класса

return new RefChecker();
}

hasAccessTo(referencePath) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Здесь ты по сути проверяешь существования файла на файловой системе.
И в название методы ты зашиваешь имя соответствующего нодового метода.

Это неправильно. Лучше назвать метод refExists или existsRef или existsRefImage

Copy link
Contributor

@eGavr eGavr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DudaGod

Возможно мы не совсем понимаем идею, которую вкладывал @j0tunn в необходимый рефакторинг.

Сейчас ты создал кучу классов типа comparator, reference-checker и т д, которым свой контекст this вообще не нужен, то есть все это можно вынести в отдельный файл utils как простые методы.

Давайте с Тохой это обсудим)

}

copy(currentPath, referencePath) {
return fs.copyAsync(currentPath, referencePath).thenReturn(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не нравится то, что функция вдруг boolean возвращает.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Она и до этого 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

и эта тоже.

static create(capture) {
return capture
? new Updater(capture)
: new Updater();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а зачем так делать?

почему не просто

return new Updater(capture);

?


save(referencePath) {
return fs.mkdirsAsync(path.dirname(referencePath))
.then(() => this._capture.image.save(referencePath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а если this._capture === undefined ?


afterEach(() => sandbox.restore());

it('should have static factory creation method', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну вот некоторые наши коллеги мне заворачивали такой тест по причине ненужности

sandbox.stub(fs, 'copyAsync');
});

it('should save current image to reference path', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save -> copy.
Ты же проверяешь и путь откуда копируется файл и путь назначения

};

it('should make directory before saving the image', () => {
const mediator = sinon.spy().named('mediator');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем так сложно?
Разве у тебя не отработает правильно без этого медиатора?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Так я же могу в коде просто написать:

fs.mkdirsAsync(refPath);
capture.image.save(refPath);

Т.е. сохранение изображение запустится сразу же не дожидаясь создалась директория или нет и мой тест без медиатора пройдет. Так как стабы вызовутся в правильном порядке.

fs.mkdirsAsync.returns(Promise.resolve());

return save_({refPath: '/ref/path'})
.then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

в одну строчку

});
});

it('should save image with correct path', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with correct -> with given

@DudaGod DudaGod force-pushed the refactor/screen-updater branch 3 times, most recently from 40e692f to cdcf946 Compare February 8, 2017 05:07
Copy link
Contributor

@eGavr eGavr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Посмотрел, по коду стало вроде лучше, но я не уверен, что прям реализована та идея рефакторинга, которую Антон вкладывал в ТЗ задачи.

canHaveCaret: capture.canHaveCaret,
tolerance: opts.tolerance,
pixelRatio: opts.pixelRatio
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

кстати таки один класс можно создать, так как эта логика используется в двух местах.

Ну типо создать класс Comparator, который в конструкторе принимает capture и opts и инициализирует эти опции.


const capture = {image: imageStub};
const DiffScreenUpdater = proxyquire(
'lib/state-processor/capture-processor/screen-updater/diff-screen-updater',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

зачем proxyquire ?

});
});

it('should save current image with "png" extension', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save a current

ниже тоже

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

название теста не соответствует тому, что проверяется в тесте.

Ты тут проверяешь, что temp.path вызывается с нужными аргментами, а не то, что картинка сохраняется.

То, что картинка сохраняется, ты проверяешь ниже в тесте


afterEach(() => sandbox.restore());

it('should not save current image if reference image does not exist', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to the temporary –> to a temporaty

.then(() => assert.callOrder(fs.mkdirsAsync, mediator, imageStub.save));
});

it('should save image with given path', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the given path

});

it('should save image with given path', () => {
fs.mkdirsAsync.returns(Promise.resolve());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тоже в beforeEach

});

describe('existsRef', () => {
it('should fulfill promise if reference image exists', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be fulfilled ...


describe('existsRef', () => {
it('should fulfill promise if reference image exists', () => {
fs.accessAsync.returns(Promise.resolve());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

в beforeEach

return assert.isFulfilled(utils.existsRef('/existent/path'));
});

it('should reject with error if reference image does not exist', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be rejected with an error

Copy link
Contributor

@j0tunn j0tunn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Все равно ведь дублирование осталось. Моя мысль сводилась к тому, что у нас по сути есть всего две операции: проверка эталона и сравнение изображений, а различное поведение 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.then((equal) => !equal && utils.copyImg(currentPath, refPath));

@DudaGod DudaGod force-pushed the refactor/screen-updater branch from 8ea2ef0 to 4d65706 Compare June 6, 2017 07:11
return utils.saveRef(referencePath, capture)
.then((updated) => ({imagePath: referencePath, updated}));
};
if (type === 'new-updater') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Здесь лучше сделать пустую строку перед if

return utils.copyImg(currentPath, referencePath)
.then((updated) => ({imagePath: referencePath, updated}));
};
if (type === 'diff-updater') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Здесь лучше сделать пустую строку перед if

const CaptureProcessor = require('./');
const utils = require('./utils');

exports.create = (type) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Плохо читается:
Давай здесь перепишем по-другому:

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нет.
Давай не будем использовать вот такие штуки.

return fs.mkdirsAsync(path.dirname(refPath))
    .then(() => capture.image.save(refPath))
    .then(_.stubTrue)
    .catch(_.stubFalse)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну мне не очень нравятся эти стабы из лодаша. Давай тогда так напишем:

return fs.mkdirsAsync(path.dirname(refPath))
    .then(() => capture.image.save(refPath))
    .then(() => true)
    .catch(() => false)

};

exports.existsRef = (refPath) => fs.accessAsync(refPath)
.thenReturn(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

См. комментарий выше

};

function identifyUpdaterType(opts) {
let type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ИМХО лишняя переменная.
Ты же можешь писать вот так:

if (opts.diff && !opts.new) {
    return 'diff-updater';
}

if (!opts.diff && opts.new) {
    return 'new-updater';
}

return 'meta-updater';

this._onDiffHandler = null;
}

onReference(handler) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хорошая идея. Я кстати такой подход использовал в dashboard при проектировании базового Job-а

return utils.existsRef(referencePath)
.then((isRefExists) => {
if (!isRefExists) {
return this._onNoRefHandler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот тут непонятно. При каких условиях у тебя может быть или не быть обработчика this._onNoRefHandler ? Не переусложнил ли ты в этом месте код?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А нет, все норм

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

по хорошему вместо проверок при вызове имеет смысл менять поведения объекта во время записи. Смотри, вызовы on* у тебя происходят только при создании объекта, а потом этот объект может несколько раз использоваться. В итоге можно просесть в производительности (да, в gemini у нас сейчас такие объекты одноразовые, но здесь тоже можно сделать какое-нибудь кэширование).
Т.е. лучше при построении создать объект, который уже никакие проверки внутри себя не делает

: Promise.reject(new NoRefImageError(referencePath));
}

if (!this._onEqualHandler && !this._onDiffHandler) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Может быть здесь лучше написать вот так:

return (this._onEqualHandler && this._onDiffHandler) 
    ? this._compareImages(capture, opts)
    : this._onRefHandler(referencePath);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//cc @DudaGod
А почему ты тут не исправил?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

блин не заметил это замечание.
Спасибо, поправил.

@@ -0,0 +1,4 @@
global.assert.calledOnceWith = function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return processor.exec(capture, opts);
};
};
let exec_;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Отбей пустой строкой

return assert.isRejected(exec_({refPath: '/non-existent/path'}), NoRefImageError);
});

describe('should return image comparison result', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно было бы вот так:

describe('should return image comparison result if images are', () => {
    it('equal', () => {});

    it('different', () => {});
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

И вообще здесь не совсем понятно, что проверяется:

assert.deepEqual(result, {
    currentPath: '/temp/path',
    referencePath: '/ref/path',
    equal: true
});

Предлагаю разбить на 3 теста:

  1. Ты проверяешь, что есть currentPath: '/temp/path', и referencePath: '/ref/path'` (а может быть даже в 2-х отдельных тестах)
  2. Проверяешь, что equal=true для одинаковых изображений.
  3. Проверяешь, что equal=false для разных изображений.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я тут сразу проверяю весь результат который возвращается при сравнении изображений. Это позволяет быть уверенным, что объект не содержит каких то новых полей. Не уверен, что его нужно разбивать так. В итоге же получится везде одинаковый вызов, но проверка по кусочкам. Тем более тут же тест не сложный получается.

exec_ = mkExecMethod(newUpdater);
});

it('should not save a reference image if it is already exists', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну приехали.
Ты проверяешь, что эталон не сохраняется и для этого у тебя есть assert.notCalled(utils.saveRef);
Зачем второй assert ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Или второй ассерт тоже "в тему"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, все-таки твой вариант правильный

});
});

it('should save a reference image if it does not exist', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

См. замечание выше

});
});
});
// 'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А это что за закоментированный код ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

забыл удалить. Удалил

it('should make a directory before saving the image', () => {
const mediator = sinon.spy().named('mediator');

fs.mkdirsAsync.callsFake(() => Promise.delay(1).then(mediator));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

И опять непонятно про медиатор

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

выше писал, для чего так делаю

@DudaGod DudaGod force-pushed the refactor/screen-updater branch from d2ca22b to 90b67cf Compare June 22, 2017 08:08
@@ -0,0 +1,44 @@
'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

я обычно фабрику кладу в 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

по хорошему вместо проверок при вызове имеет смысл менять поведения объекта во время записи. Смотри, вызовы on* у тебя происходят только при создании объекта, а потом этот объект может несколько раз использоваться. В итоге можно просесть в производительности (да, в gemini у нас сейчас такие объекты одноразовые, но здесь тоже можно сделать какое-нибудь кэширование).
Т.е. лучше при построении создать объект, который уже никакие проверки внутри себя не делает

super({
module: require.resolve('./capture-processor/tester'),
constructorArg: config.system.diffColor
module: require.resolve('./capture-processor/factory'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

родителю теперь достаточно передать только тип процессора, и можно обойтись без объекта

return 'new-updater';
}

return 'meta-updater';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return opts.diff && !opts.new && 'diff-updater'
    || !opts.diff && opts.new && 'new-updater'
    || 'meta-updater'

и можно в функцию не выносить

@DudaGod DudaGod force-pushed the refactor/screen-updater branch 2 times, most recently from 432895a to a1fffe5 Compare July 22, 2017 22:28
Copy link
Contributor

@j0tunn j0tunn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Еще файлы, которые затрагивал, на es6 переведи, и норм.

if (type === 'diff-updater') {
return CaptureProcessor.create()
.onReference(_.noop)
.onNoReference(notUpdated)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а мы сейчас разве в этом случае не бросаем ошибку?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нет - 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

можно по идее по умолчанию в _onRefHandler записать _.noop (ну или даже во все хэндлеры по умолчанию записать _.noop). Тогда тебе вот это писать не нужно будет

}
};

function identifyUpdaterType(opts) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ну я бы не выносил это в отдельную функцию. Просто в конструкторе сохранил бы переменную updaterType

@DudaGod DudaGod force-pushed the refactor/screen-updater branch 2 times, most recently from 5c4ca22 to ffea13b Compare August 23, 2017 04:51
@DudaGod DudaGod force-pushed the refactor/screen-updater branch 2 times, most recently from ec225cf to 9fa38b5 Compare August 28, 2017 07:12
@DudaGod DudaGod merged commit 39c5352 into master Aug 28, 2017
@DudaGod DudaGod deleted the refactor/screen-updater branch August 28, 2017 21:57
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants