Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: do not save diff when option light is true #10

Merged
merged 1 commit into from
Mar 29, 2017
Merged

Conversation

up73k
Copy link
Contributor

@up73k up73k commented Feb 20, 2017

No description provided.

@j0tunn
Copy link
Contributor

j0tunn commented Feb 21, 2017

Так нельзя: инстанс ImageError хранит поля isDiff и imagePath, которые в дальнейшем используются для отсеивания реальных диффов (когда все ретраи с одинаковым diff'ом). Если мы это выпилим, то в отчете будут лишние ошибки. По хорошему что нужно сделать:

  • не сохранять изображение перед созданием инстанса ImageError, а сохранить все данные для того, чтобы это изображение можно было сохранить позже по запросу
  • при отсеивании реальных диффов - сохранять изображения, и сравнивать между собой
  • поправить плагин, чтобы collect выполнялся не на END, которое синхронное, а на RUNNER_END, которое асинхронное

И да, про тесты не забудь.

@up73k
Copy link
Contributor Author

up73k commented Mar 7, 2017

на ES6 переведу отдельным коммитом после ревью основных изменений.

Copy link
Member

@DudaGod DudaGod left a comment

Choose a reason for hiding this comment

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

Давай сразу переведем отдельным коммитом на es6.

lib/plugin.js Outdated
@@ -15,6 +15,6 @@ module.exports = function (gemini, opts) {
data.equal || fails.addFail(data);
});

runner.on('end', fails.collect.bind(fails));
runner.on('endRunner', fails.collect.bind(fails));
});
};
Copy link
Member

Choose a reason for hiding this comment

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

А мы не хотим в рамках данного PR-а переписать тут на использование нового API Gemini? Уменьшится вложенность на один и не нужно будет хардкодить названия эвентов.
Я бы так же добавил опцию enabled как это сделано во всех остальных плагинах, чтобы было консистентно.

Copy link
Member

Choose a reason for hiding this comment

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

Еще у меня предложение обновить версию looksSame. В данном плагине версия - 2.2.2, а текущая версия - 3.2.0.
Так как за это время во первых мы научились работать с анти-алиасингом, а во вторых правильно обрабатывать каретку.

Copy link
Member

Choose a reason for hiding this comment

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

Сюда бы еще eslint наш вкрутить, а то тут ошибок дофига с кодстайлом.
И конфигпарсер как в остальных плагинах =)

Copy link
Contributor Author

@up73k up73k Mar 14, 2017

Choose a reason for hiding this comment

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

на использование нового API Gemini

Переписал.

return _.defaults(this.__base(), {
base64: this.base64
});
const super_ = this.__base.bind(this);
Copy link
Member

Choose a reason for hiding this comment

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

не понял, зачем тебе тут так биндить?

Copy link
Contributor Author

@up73k up73k Mar 11, 2017

Choose a reason for hiding this comment

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

иначе не работает модуль inherit

Copy link
Contributor

Choose a reason for hiding this comment

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

я уточню почему не работает: в модуле inherit __base переопределяется непосредственно перед вызовом каждого метода (и по моему затирается после вызова). Соответственно в следующем тике в this.__base уже нет ссылки на родительский метод.
Но все это фигня, т.к. при переписываении на es6 inherit можно выбросить

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 Author

Choose a reason for hiding this comment

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

inherit выгасил

}

return imageProcessor.pngToBase64(this.imagePath)
.then((base64) => _.extend(super_(), {base64}));
Copy link
Member

Choose a reason for hiding this comment

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

почему бы последние 2 строки не унести в приватный метод?
Тогда у тебя получится что-то вроде этого:

return this.light
  ? this.__base()
  : this._addBase64();

над названием метода нужно подумать )

return q(new ImageError(data, geminiConfig, {
path: imagePath,
saveImg: saveImg,
light: options.light
Copy link
Member

Choose a reason for hiding this comment

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

я бы пробрасывал весь объект опций, отдельным параметром. Но тут получается у тебя уже аж три параметра... и четвертый добавлять совсем не хочется. Нужно подумать.

@@ -59,8 +59,11 @@ module.exports = inherit({
}

function compareWith_(reference) {
const promise = reference.save();
Copy link
Member

Choose a reason for hiding this comment

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

давай пустой строкой отобьем

});
});

it('should not convert image to base64 if "light" option exist', function() {
Copy link
Member

Choose a reason for hiding this comment

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

exists


var failedTest = new ImageError(failedTestError, config, {base64: 'base64-value'}),
errorData = failedTest.getData();
it('should generate temporary path for image', function() {
Copy link
Member

Choose a reason for hiding this comment

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

этот тест должен быть в другом файле.

.then(function() {
assert.notCalled(imageProcessor.pngToBase64);
});
});
Copy link
Member

Choose a reason for hiding this comment

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

нужен еще тест на проверку того, что вернет метод getData если опци] light установлена


return failCollector.collect()
.then(function() {
assert.called(ImageError.prototype.save);
Copy link
Member

Choose a reason for hiding this comment

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

ты тут должен проверить, что save позвался два раза, т.е. для всех фейлов.
И тогда нужен еще один тест, который проверяет, что imageProcessor.compare позвался и с каким аргументами.

@@ -63,6 +64,20 @@ describe('fail-collector', function() {
failCollector = new FailCollector(config);
});

it('should save screenshot of the test failed with diff error type', function() {
Copy link
Member

Choose a reason for hiding this comment

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

should save screenshots for failed tests

lib/plugin.js Outdated
gemini.on(gemini.events.ERROR, (data) => fails.addFail(data));
gemini.on(gemini.events.RETRY, (data) => fails.addFail(data));
gemini.on(gemini.events.TEST_RESULT, (data) => data.equal || fails.addFail(data));
gemini.on(gemini.events.END_RUNNER, fails.collect.bind(fails));
Copy link
Member

Choose a reason for hiding this comment

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

зачем тебе тут bind?

Просто так сделай:

gemini.on(gemini.events.END_RUNNER, () => fails.collect());

Copy link
Member

Choose a reason for hiding this comment

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

и давай еще все подписки отобьем друг от друга пустой строкой - тогда будет проще читаться.

.then((fails) => this._filterDiffs(fails))
.then((fails) => this._getFailsData(fails))
.then((fails) => this._saveToFile(fails))
.catch((error) => console.error('Some error while collecting fails: ', error.stack));
Copy link
Member

Choose a reason for hiding this comment

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

тут можно template string юзнуть

Copy link
Member

Choose a reason for hiding this comment

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

И что-то мне не очень нравится сообщение - Some error...

.thru(q.all).value()
.then(_.compact);

function getRealFailedTestName_(fails) {
return _(fails)
.slice(1)
Copy link
Member

Choose a reason for hiding this comment

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

для чего тут этот slice?

Copy link
Contributor Author

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.

здесь можно оставить коммент

.thru(q.all)
.value()
.then((res) => _.zipObject(keys, res));
}
Copy link
Member

Choose a reason for hiding this comment

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

не понял.
Разве так работать не будет? :

return _(fails)
  .mapValues((failList) => {
    return _(failList)
      .map((fail) => fail.getData())
      .thru(q.all)
      .value();
  });

Copy link
Contributor Author

@up73k up73k Mar 23, 2017

Choose a reason for hiding this comment

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

неа (я проверил). я над этим местом 3 дня сидел )

Copy link
Member

Choose a reason for hiding this comment

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

Я к тому, что можно без zipObject-а обойтись:

return _(fails)
  .mapValues((failList) => {
    return _(failList)
      .map((fail) => fail.getData())
      .thru(q.all)
      .value();
  })
  .thru(q.all)
  .value();

Copy link
Contributor

Choose a reason for hiding this comment

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

я не уверен, что будет работать. q.all ожидает на вход массив промисов, а ты скармливаешь ему объект, значениями которого являются промисы. В принципе может прокатить, но на выходе скорее всего получится массив. Надо проверять

Copy link
Contributor Author

@up73k up73k Mar 24, 2017

Choose a reason for hiding this comment

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

zipObject нужен чтобы не терялся тайтл теста (входящий аргумент fails - это объект- коллекция массивов с ключами тайтлами тестов).

Copy link
Member

Choose a reason for hiding this comment

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

да, я понимаю для чего он нужен. Мне показалось, что так тоже должно работать и тогда не нужно будет выше сохранять ключи в переменную и в конце вызывать zipObject. Можешь пожалуйста проверить последний мой вариант. Что он возвращает?

Copy link
Contributor Author

@up73k up73k Mar 29, 2017

Choose a reason for hiding this comment

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

твой ваиант возвращает: { 'pager/first-page/1.plain.firefox': { state: 'pending' } }
то есть промисы в стэйте pending еще и не разрезолвлены. Скорее всего, второй q.all не отрабатывает.

.map((fail) => fail.getData())
.thru(q.all)
.value())
.thru(q.all)
Copy link
Member

Choose a reason for hiding this comment

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

зачем тут два раза через q.all прогонять?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

первый раз резовим промисы в каждом из массивов, второй раз резолвим конечный объект с ними.

if (!data.imagePath && !data.saveDiffTo) {
return q(new BaseError(data, geminiConfig));
return q(new BaseError({data, config}));
Copy link
Member

Choose a reason for hiding this comment

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

а зачем ты в один объект тут упаковал?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ниже ответил

}
this.message = opts.data.message || 'Image diff found'; // diff Error don't have "message" key;
this.browserId = opts.data.browserId;
this.sessionId = opts.data.sessionId;
Copy link
Member

Choose a reason for hiding this comment

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

теперь доступ к опциям слишком длинный получается - opts.data.some, а не просто opts.some

base64: base64
});
});
return q(new ImageError({data, config, img: {
Copy link
Member

Choose a reason for hiding this comment

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

кажется не лучшее решение все в один гигантский объект упаковывать.

Copy link
Member

Choose a reason for hiding this comment

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

я бы все таки лучше оставил как было до этого

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ты сам писал выше: "я бы пробрасывал весь объект опций, отдельным параметром. Но тут получается у тебя уже аж три параметра... и четвертый добавлять совсем не хочется" , поэтому и исправил.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

верну как было

Copy link
Member

Choose a reason for hiding this comment

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

ну ведь data и config это совершенно другие объекты. А я говорил, про объект options в котором у тебя сейчас хранится поле light

module.exports = ImageError;
_addBase64() {
return fs.exists(this.imagePath)
.then((isFileExists) => isFileExists ? _.noop() : q.fcall(this.save.bind(this)))
Copy link
Member

Choose a reason for hiding this comment

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

Я бы переменную назвал - isImgExists.
Так же я не понял зачем тебе тут q.fcall?
Ты же здесь можешь так написать:

.then((isImgExists) => isImgExists || this.save()

return fs.exists(this.imagePath)
.then((isFileExists) => isFileExists ? _.noop() : q.fcall(this.save.bind(this)))
.then(() => imageProcessor.pngToBase64(this.imagePath))
.then((base64) => _.extend(super.getData(), {base64}))
Copy link
Member

Choose a reason for hiding this comment

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

ты заметил, что ты получается super.getData зовешь в обоих случаях?
И когда this.light - true и когда false.
Может тогда будем вызывать его только в одном место? В this.getData?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

решил оставить так, чтобы не ломать промисы.

Copy link
Member

Choose a reason for hiding this comment

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

я считаю, что в этом случае нужно исправить.
Тем более я не пойму как ты их сломаешь, если super.getData() возвращает обычный объект?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_.extend не дожидается выполнения промиса _addBase64

Copy link
Member

Choose a reason for hiding this comment

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

ниче не понял.
В данном случае extend выполнится после того как разрезолвится вызов pngToBase64.
Вместо super.getData() ты должен туда просто подпихнуть переменную которую выше объявишь

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ок, прокинул через параметр в _addBase64

@@ -18,29 +18,29 @@ describe('errors/base-error', function() {
}),
config = mkConfigStub();
Copy link
Member

Choose a reason for hiding this comment

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

тут две переменные и каждая из них должна объявляться отдельно, а не через запятую

opts.data.suite.fullName,
opts.data.state && opts.data.state.name,
opts.data.browserId
]).join('.');
Copy link
Member

Choose a reason for hiding this comment

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

а почему у нас часть параметров public, а часть private?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

исторически сложилось, решил так оставить

var failedTestError = mkErrorStub(),
config = mkConfigStub(),
failedTest = new BaseError(failedTestError, config);
describe('getData()', () => {
Copy link
Member

Choose a reason for hiding this comment

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

а нафига нам скобочки в описании? Итак же понятно, что это метод

failedTest = new BaseError(failedTestError, config);
describe('getData()', () => {
['timestamp', 'message', 'sessionId', 'browserCapabilities', 'seleniumQuota'].forEach((key) => {
it('should return object with ' + key + ' value', () => {
Copy link
Member

Choose a reason for hiding this comment

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

давай тут template string юзнем

config = mkConfigStub(),
failedTest = new BaseError(failedTestError, config);
describe('getData()', () => {
['timestamp', 'message', 'sessionId', 'browserCapabilities', 'seleniumQuota'].forEach((key) => {
Copy link
Member

Choose a reason for hiding this comment

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

я бы так писал:

[
  'timestamp', 'message', 'sessionId', 'browserCapabilities', 'seleniumQuota'
].forEach((key) => {...});


var errorDefaults = {
const errorDefaults = {
suite: {fullName: 'suite-fullname'},
state: {name: 'state-name'},
browserId: 'browserId'
};
Copy link
Member

Choose a reason for hiding this comment

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

отступы поехали


var errorDefaults = {
const errorDefaults = {
suite: {fullName: 'suite-fullname'},
state: {name: 'state-name'},
browserId: 'browserId'
};

module.exports = {
Copy link
Member

Choose a reason for hiding this comment

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

зачем?
Почему бы просто не експортить каждый метод отдельно?

exports.mkErrorStub = () = {...}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

кажется, что разницы особой нет


describe('plugin', () => {
const sandbox = sinon.sandbox.create();
const geminiEvents = mkGeminiEvents();
Copy link
Member

Choose a reason for hiding this comment

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

почему бы сразу не сделать:

const geminiEvents = utils.mkGeminiEvents();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

убрал utils.mkGeminiEvents

equal: false,
saveDiffTo: sinon.stub().returns(q())
}, errorDefaults);
},
errorDefaults
Copy link
Member

Choose a reason for hiding this comment

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

это не должно на следующую строку переносится

RETRY: 'retry',
ERROR: 'err',
TEST_RESULT: 'testResult'
};
Copy link
Member

Choose a reason for hiding this comment

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

нужен ли нам для этого хелпер?
Если он по идеи только в plugin.js используется?

data.suite.fullName,
data.state && data.state.name,
data.browserId
]).join('.');
Copy link
Member

Choose a reason for hiding this comment

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

я бы все таки сделал все поля приватными и через геттер расшарил те которые нужны снаружи.
На сколько я понял снаружи используются только name и browserId.

А sessionId и message вообще снаружи не юзаются.
Т.е. достаточно написать так (для тех которые снаружи юзаются):

get name() {
  return this._name;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

задача не про это. Я так понял этот файл был затронут просто переводом на es6

sessionId: this.sessionId,
browserCapabilities: this._browserCapabilities,
seleniumQuota: this._quota
};
Copy link
Member

Choose a reason for hiding this comment

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

отступы поехали

path: imagePath,
saveImg,
light: options.light
}
Copy link
Member

Choose a reason for hiding this comment

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

теперь же логику про инициализацию imagePath и saveImg нужно уносить в ImageError.
Так как error-factory ничего про это знать не должен. Его задача создать инстансы ImageError и BaseError в зависимости от переданных параметров. А что происходит внутри его не должно касаться.
Т.е. здесь тебе просто нужно сделать так:

return new ImageError(data, config, options);

if (!data.imagePath && !data.saveDiffTo) {
return q(new BaseError(data, geminiConfig));
return q(new BaseError(data, config));
Copy link
Member

Choose a reason for hiding this comment

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

зачем нам тут оборачивать вызов конструктора BaseError в q? Так же само ниже мы оборачиваем конструктор ImageError в q. Здесь правильнее было бы обернуть сам вызов buildError если нужно.
Т.е. вот тут.

saveImg,
light: options.light
}
));
Copy link
Member

Choose a reason for hiding this comment

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

в итоге будет выглядеть это так:

exports.buildError = (data, config, options) => {
  return data.imagePath || data.saveDiffTo
    ? new ImageError(data, config, options)
    : new BaseError(data, config);
};

}

getData() {
const baseData = super.getData();
Copy link
Member

Choose a reason for hiding this comment

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

я бы эту переменную назвал baseErrData

});
return this.light
? baseData
: this._addBase64(baseData);
Copy link
Member

Choose a reason for hiding this comment

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

как вариант:

return this._opts.light ? baseErrData : this._addBase64(baseErrData);

module.exports = ImageError;
save() {
return this.saveImg(this.imagePath)
.catch((error) => console.error(error.stack || error));
Copy link
Member

Choose a reason for hiding this comment

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

Почему бы тут так не написать:

.catch((error) => console.error(`Error occurred while saving image: $(error)`));

Copy link
Contributor

Choose a reason for hiding this comment

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

  • $(error) -> ${error}
  • чаще всего хочется видеть стэк

.catch((error) => console.error(error.stack || error));
}

_addBase64(data) {
Copy link
Member

Choose a reason for hiding this comment

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

давай тут переименуем параметр который ты передаешь. Я бы вместо data назвал ее baseErrData

.thru(q.all)
.value()
.then((res) => _.zipObject(keys, res));
}
Copy link
Member

Choose a reason for hiding this comment

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

да, я понимаю для чего он нужен. Мне показалось, что так тоже должно работать и тогда не нужно будет выше сохранять ключи в переменную и в конце вызывать zipObject. Можешь пожалуйста проверить последний мой вариант. Что он возвращает?

suite: {
fullName: 'suite-fullname'
},
state: {
name: 'state-name'
},
browserId: 'browserId'
}),
config = mkConfigStub();
});
Copy link
Member

Choose a reason for hiding this comment

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

отступы уехали

assert.calledOnce(imageProcessor.pngToBase64);
});
.then((errorData) => {
errorData.saveImg();
Copy link
Member

Choose a reason for hiding this comment

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

этот тест уйдет в image-error если сделать как я выше предложил. Плюс станет понятнее.

it('should return extended data with "base64" key', () => {
imageProcessor.pngToBase64.returns(q.resolve('base64-value'));

return mkImgErrGetData({light: false})
Copy link
Member

Choose a reason for hiding this comment

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

зачем? Ты тем самым сделал его еще сложнее. Так как из названия теперь не сразу понятно, что оно делает. А если назвать mkImgErr - понятно, что создается инстанс и ты сразу дальше зовешь .getData. Плюс этот хелпер может юзаться в будущем для других методов.


module.exports = {
mkErrorStub: function(opts) {
mkErrorStub: (opts) => {
opts = opts || {};
return _.defaults(opts, errorDefaults);
Copy link
Member

Choose a reason for hiding this comment

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

давай хелперы консистентно друг другу сделаем.
У тебя в двух _.defaults сразу зовется, а в двух в самом конце.

.then((fails) => this._filterDiffs(fails))
.then((fails) => this._getFailsData(fails))
.then((fails) => this._saveToFile(fails))
.catch((error) => console.error(`Error occurred while collecting fails: $(error.stack)`));
Copy link
Contributor

Choose a reason for hiding this comment

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

$(error.stack) -> ${error.stack}. Но вообще я не понимаю зачем здесь менять то что было на template literals

}.bind(this))
.map(((fails) => fails.length === this._maxRuns(fails[0].browserId)
&& _.every(fails, {isDiff: true})
&& getRealFailedTestName_(fails)))
Copy link
Contributor

Choose a reason for hiding this comment

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

не надо так писать. Это уже далеко не однострочник

@DudaGod
Copy link
Member

DudaGod commented Mar 29, 2017

/ok

@up73k up73k merged commit bbb1a6d into master Mar 29, 2017
@up73k up73k deleted the light-error branch March 29, 2017 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants