Detached rulesets in LESS as a lambda-expressions

lambda-less-css-logoSurprisingly quietly and without much fanfare a groundbreaking change in the LESS world recently happened: so-called detached rulesets had arrived in version 1.7. In essence it means an ability to store a block of LESS code into variable or pass it as an argument to mixin.

According to documentation, a primary motivation for such a feature is @media queries with overlapping rules, but certainly there are many other useful applications possible.

For instance it alleviates a LESS’ inability to call a mixin indirectly by name – now we can supply a whole mixin invocation in curly braces instead of just a name.

The most interesting feature of the detached rulesets is their scope: the code inside them has access to both contexts – where ruleset was defined and where it’s used.

In combination with LESS’ distinctive lazy evaluation it allows us to use mixin ruleset arguments as some kind of “lambda-expressions” or “anonymous mixins” after similar concept of anonymous classes in java.

Let’s take iteration of lists or numeric ranges as an example. Perhaps that’s a little uncommon task in a day to day stylesheet authoring but it should be quite demonstrative.

Basic looping is trivially achieved with a recursive mixin. It’s really simple but only if fully implemented in each place. In pre-1.7 time any attempt to generalize the recursive part and move it to mixin library had to take the same design decisions: a loop body must be a mixin, no simple way to call mixin by name implies that name should be known beforehand (hardcoded in library mixin), that leads to clash of the same-named mixins when several loops are defined nearby, so wrapping each loop in additional block to limit its scope is suggested.

By the way, there exists a LESS-idiom for such scoping purposes – a nested rule with single parent selector: & { … }

Probably the most advanced of pre-1.7 iteration mixin libraries is less.curious from one of core LESS developers @seven-phases-max. That’s a really clever abuse of LESS’ syntax, source formatting and laziness. Here is an example from its documentation:

#icon {
    .for(home ok cancel error book); .-each(@name) {
        &-@{name} {
            background-image: url("../images/@{name}.png");

Now that we’ve got detached rulesets, we can easily write the code that produces exactly same results like this:

.foreach(home ok cancel error book, {
    #icon-@{item} {
        background-image: url("../images/@{item}.png");

Not having to name the loop body mixin, we can get rid of the wrapping block while retaining the ability to place several loops in the same context, as we can limit the scope of recursive mixins inside the library mixin.

That’s how such mixin might look:

.foreach(@array, @lambda)
		.iterator(@index) when (@index > 1)
			.iterator((@index - 1));
			@item: extract(@array, @index);
		@length: length(@array);

It takes two arguments, first one is a list to iterate and second one is a ruleset. Inside that ruleset following variables are available: @item – current list item, @index – its position in the list, @array – the list itself and @length – total amount of list items. Indices are starting from one, matching behavior of built-in “extract” function.

Caution! Names collision possible – it’s better to avoid the use of such variable names in other places.

When reffering to mixin or variable LESS search it in defining context before the usage context (quite illogical, in my opinion, but unlikely to change as it can broke many LESS users code in subtle ways and cause the debugging nightmare). So one can break all the loops in project simply by having a variable named “@item” defined in top context. Those collisions are probably the most significant drawback of this approach. It can be somewhat obviated by prefixing variable names though.

Here are some more usage examples in codepen:

// generic list iteration mixin for LESS 1.7
// arguments:
//   @array - list of items to iterate
//   @lambda - ruleset with loop body
// inside @lambda block folowwing variables are available:
//   @item – current list item 
//   @index – its position in the list
//   @array – the list itself and
//   @length – total amount of list items
// more info:
.foreach(@array, @lambda)
		.iterator(@index) when (@index > 1)
			.iterator((@index - 1));
			@item: extract(@array, @index);
		@length: length(@array);

// usage examples

.foreach(#a6c8e0 #fecc94 #c8e793 #b2e1d9 #f9f9ad,
	li:nth-child(@{length}n+@{index}) { background: @item; }

.foreach(home ok cancel error book, {
    #icon-@{item} {
        background-image: url("../images/@{item}.png");

See the Pen generic list iteration mixin for LESS 1.7 by bsl-zcs (@bsl-zcs) on CodePen.

Besides iteration, detached rulesets potentially can alter the ways people use LESS, exactly like lambdas in Java 8. Some places where mixins are usually used can be switched to rulesets and comparative simplicity of their use could lead to modularity increasing.

As LESS’ scope resolution logic prevents making mixins overridable in nested contexts, a ruleset variable with a default value can eventually overtake the role of the main instrument for optional style tuning.

Запробеливание™ в жабаскрипте

Сразу оговорюсь, дальше идёт практически эталонный образец бесполезной микрооптимизации.  Бессмысленной и беспощадной, ибо сказано: «premature optimization is the root of all evil».

Тем не менее.

Приспичило давеча сделать банальнейшую вещь: затереть строчку пробелами.  Иными словами, получив на вход строку, вернуть строку пробелов той же длины.  Происходит это при замене жабаскриптом регулярного выражения, поэтому вызываться может многократно, откуда и проистекает естественное желание сделать это побыстрее.

Казалось бы, что может быть проще: взять у строки длину и выдать пробелы в нужном количестве.  Оказывается, в жабаскрипте это – целая проблема, вызывающая бурные обсуждения.  Дело в том, что в силу врождённой убогости, жабаскрипт не имеет сколько-нибудь развитых средств работы с текстом, то есть аналога перлового " "x10, равно как и наскального " "*10, там попросту нет.  Простейший цикл с должным количеством конкатенаций упирается в крайне низкую производительность работы со строками.

Народное творчество на эту тему получило развитие в двух направлениях.  Первый вариант, более удобный для написания, базируется на операции объединения пустых элементов массива заданной длины с размножаемой строкой в качестве разделителя.  Второй вариант, быстрее работающий, построен на побитовом разложении числа повторений, удвоении длины путём объединения строки с самой собой, и добавлении дополнительной копии подстроки при установленном бите в числе повторений.  Звучит страшно, выглядит так же, зато работает с логарифмической сложностью.

Все эти варианты, и многие другие, можно посмотреть, например, тут:  На мой взгляд, глубоко неправилен уже сам факт того, что примитивная потребность способна вызывать настолько развёрнутую дискуссию, но не будем о грустном.

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

Один из них – дословно «затереть пробелами»: заменить каждый символ пробелом.  Табличной подстановки на манер перлового tr/// в жабаскрипте нет, да и простая замена подстроки срабатывает только однократно, но есть замена по регэкспу, в том числе с глобальным модификатором: str.replace(/./g, ' ');

Другой вариант заключается в накоплении стратегического запаса пробелов, и выделения из них части по требованию: spaces.substr(0, str.length);

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

Результат, в целом, предсказуем: регэкспы ожидаемо медленны, побитовая конкатенация ожидаемо быстра.  Что любопытно, получение подстроки из большой строчки пробелов на порядок быстрее всего остального.  Вероятно, современные жабаскриптовские движки не заморачиваются на копирование данных при получении подстрок, и ограничиваются созданием объектов ссылающихся в тот же буфер.  Из чего можно сделать далеко идущие выводы: переменная с полученной из большого буфера небольшой подстрокой может мешать сборщику мусора утилизировать весь буфер.  Если, конечно, такие случаи отдельно не отрабатываются. Теоретически с этим можно бороться, добавляя .valueOf() после .substring().

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

При всех своих достоинствах, стратегический запас пробелов всё-таки конечен.  На затирание очень большой строки его может просто не хватить. Следующий забег был посвящён поиску оптимального способа обработки больших строк.  С подробностями и результатами можно ознакомиться здесь:

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

Как ни странно, во всех протестированных браузерах кроме IE11 (показанного как other) скорость этого гибридного подхода (substr2, substr3) уступила побитовой конкатенации (repeat).


Поэтому, памятуя о том, что память всё равно разделяется всеми строчками, был добавлен вариант неограниченного увеличения буфера пробелов, и получения подстрок из него.  На гистограмме он показан синим цветом.  На больших строчках он отрабатывает многократно быстрее всех остальных вариантов, на мелких – незначительно медленнее получения подстрок из статического буфера.

Так что победителем этих тараканьих бегов объявляется следующая функция:

var spaces = "        ";
function erase(str){
	while(spaces.length < str.length) spaces += spaces;
	return  spaces.substr(0, str.length);

И да, она может сожрать вдвое больше памяти, чем надо.  А может и не сожрать.

О языке производственного общения

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

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

Простой пример: для того чтобы сложные проблемы были подъёмны, их разделяют на уровни абстракции – это позволяет определять одну функциональность в терминах другой, оставляя на каждом уровне приемлемое для анализа количество сложности. Это замечательно работает и позволяет разделять такие задачи по нескольким исполнителям, посредством чего доводить общую сложность систем до неприличных величин. Проблемы начинаются тогда, когда возникают нестыковки и приходится анализировать поведение механизмов, разделённых несколькими уровнями абстракции. Грубая аналогия: это чем-то похоже на попытку определения человеческих отношений в терминах биохимических процессов в мозге – приходится иметь в виду огромное количество деталей с крайне неочевидным влиянием на конечные результаты.

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

Зрелище само по себе завораживающее: гики собираются у какой-нибудь доски, что-то рисуют, кричат, перебивают друг друга, жестикулируют, слушают с предельно сосредоточенным видом, и видно как мозги у них шевелятся на максимальных оборотах, аж шерсть на затылках искрится.

Как-то я обратил внимание на то, что подобные дискуссии с участием представительниц прекрасного пола проходят с существенно меньшей эффективностью. Точных чисел с процентами у меня, разумеется, нет, но падение эффективности было достаточным для того, чтобы учитывать его при принятии решений о том, кому следует поручать разбор какой-либо проблемы. Причём, проявлялось это не только на уровне их персонального вклада в обсуждение, снижалась эффективность передачи информации и между остальными участниками дискуссии.

Внимательно рассмотрев, и большей частью отбросив, очевидные предположения о недостатке способностей у конкретно этих представительниц, пришёл к выводу, что это – проявление каких-то глобальных различий, источник которых лежит глубже, на уровне самого процесса коммуникации.

Как правило, в рассматриваемых случаях этот процесс осложняется отсутствием у обсуждающих общей терминологии – обычно абстракция успешно ограждает их от знания специфики реализации остальных слоёв. В результате, зачастую приходится вырабатывать понятийный аппарат прямо по ходу дискуссии. Что примечательно, эта терминология имеет смысл только в контексте данного обсуждения, плотно с ним ассоциирована (на манер анонимного внутреннего класса в какой-нибудь жабе) и прекратит своё существование тогда, когда исходная проблема будет решена. Поэтому, на формальное описание понятий никто не заморачивается и чаще всего дело ограничивается минимально достаточным для совместного распознавания описанием, которое временами принимает достаточно причудливые формы.

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

Очевидно, что эффективность процесса поиска общих ассоциаций напрямую зависит от широты общего для всех обсуждающих контекста (набора доступных для применения ассоциаций), способности участников разнообразить обсуждение запоминающимися выражениями (в достаточной степени, чтобы впоследствии ссылаться по ним на отдельные его этапы) и от размера применяемого словарного запаса.

Как выяснилось, собственно словарный запас и определяет гендерное различие эффективности коммуникаций: в присутствии дам участники обсуждения вынуждены воздерживаться от применения ненормативной лексики. А применяемая в подобных дискуссиях обсценная терминология качественно превосходит общеупотребительную как минимум по нескольким параметрам.

Во-первых, переопределение терминов в контексте обсуждения приводит к необходимости осознанно различать использование слова по назначению и в качестве ссылки на введённое понятие. При каждом упоминании слова, необходимо принять решение о том, буквально оно использовано или иносказательно. В то время как вокабулярий русского мата достаточно редко используется в буквальном смысле (для обозначения элементов репродуктивной системы, её процессов и девиаций), а подавляющее большинство его применений иносказательно. Таким образом, его и его производные можно рассматривать как набор универсальных, контекстно-свободных слов, допускающих применение для обозначения любого, даже самого тяжело формализуемого, понятия. И, что немаловажно, не требующих оценки буквальности при каждом применении.

Во-вторых, хоть в русском языке и имеются способы косвенно сослаться на предмет обсуждения, не называя его (указательными местоимениями «это», «он» и так далее), в нём напрочь отсутствует возможность аналогичным образом сослаться на обсуждаемое действие. Иными словами, имеется разыменование косвенных существительных, но отсутствует поддержка косвенных глаголов. Способность матерных корней к построению производных любых частей речи успешно восполняет это фундаментальный недостаток языка. Что особенно критично, когда описание действий многословно и, в свою очередь, оперирует понятиями, определёнными в том же контексте.

Таким образом, разработчики, лишённые из-за необходимости «фильтровать базар» выразительных возможностей мата, вынуждены нести дополнительную когнитивную нагрузку, чтобы это скомпенсировать. Разумеется, это сказывается на эффективности коммуникаций самым плачевным образом.

По результатам наблюдений были сделаны следующие практические выводы:

1. Дейкстра был прав: «Besides a mathematical inclination, an exceptionally good mastery of one’s native tongue is the most vital asset of a competent programmer». Способность витиевато изъясняться, как цензурно, так и наоборот, даёт серьёзный плюс к способности обсуждать запредельно сложные вещи.

2. Для эффективной коммуникации команда должна иметь общий контекст, отсылки к которому однозначно воспринимаются всей командой и не требуют дополнительных пояснений. Для расширения общего контекста команде нужно иметь должное количество совместного опыта, как проектного, так и неформального. Это хорошо согласуется с практическими наблюдениями процессов естественного командообразования. Для получения сыгранной команды требуется время, даже если собрать команду из профессионалов самого высокого уровня. Овладение контекстом (фактически, освоение местного фольклора) определяет нижнюю границу времени полноценного вхождения новых людей в команду.

3. Раздражающая многих привычка хохмить по поводу и без бывает в некоторых случаях полезна – удачная шутка или аллюзия на неё формирует надёжную ассоциацию и может в дальнейшем использоваться для привязки понятий.

4. По возможности следует воздерживаться от участия женщин в обсуждении действительно сложных проблем, например тех, которые требуют коммуникаций между разработчиками на нескольких уровнях абстракции. И ни в коем случае не подключать их к уже идущему обсуждению, чтобы не разрушить стихийно сформированный понятийный аппарат, лишив участников возможности применения половины понятий.

5. В качестве языка производственного общения в технических подразделениях организаций должен использоваться русский матерный.

MongoDB и изоляция

В последних проектах в качестве основного хранилища данных мы используем MongoDB. В целом, нас устраивает этот выбор. MongoDB позволяет быстро и просто хранить не слишком структурированные данные и поддерживает шардинг из коробки. Для большинства веб-проектов этого достаточно. Однако, как и все, относящееся к NoSQL, MongoDB решая одни проблемы (производительности, простоты, масштабирования), добавляет другие. Причем, иногда достаточно неожиданные. Попробую рассказать про последний такой случай. Заметьте, это реальный, а не выдуманный абстрактный пример.

Допустим, у нас есть некая коллекция и нам захотелось пронумеровать определенным образом записи в ней. В качестве языка запросов в MongoDB используется Javascript, поэтому вполне естественная мысль – написать простой скрипт. Запускаем mongo shell:

> db.coll.count()
> var counter = 0
> db.coll.find().sort({ “_id”: 1 }).forEach(function(x) { 
    db.coll.update({ _id: x._id }, { $set: { num: counter } }); 
> counter

Но ведь у нас в коллекции только 1000 записей! Почему наш счетчик дошел до 1010? Ответ прост, но не сразу очевиден. MongoDB не дает гарантию, что курсор не вернет одну и ту же запись дважды. В случае, если размеры записей меняются, может произойти сдвиг и некоторые записи переместятся в конец. Курсор просто идет по коллекции и возвращает все подряд, соответственно те записи, которые переместились в конец будут возвращены дважды. Что и приводит к ошибке в таком, казалось бы простом скрипте.
Оказывается, в MongoDB есть специальный костыль для таких случаев: cursor.snapshot():

> var counter = 0
> db.coll.find().snapshot().forEach(function(x) { 
    db.coll.update({ _id: x._id }, { $set: { num: counter } }); 
> counter

И как у любого костыля есть ограничения: нельзя использовать сортировку. В нашем случае это подходит, snapshot сам сортирует как раз по _id. Если нужна какая-то другая сортировка, придется заходить с другой стороны:

> var counter = 0
> var ids = db.coll.find({}, { _id: 1 }).sort({ date: 1 }).toArray()
> ids.forEach(function(x) { 
    db.coll.update({ _id: x._id }, { $set: { num: counter } }); 
> counter

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

Вот такие неочевидные грабли в таком, казалось бы, простом случае. Выкидывая SQL, мы выкидываем и ACID и должны уже сами думать об изоляции и транзакциях. Вообще вокруг NoSQL и MongoDB в частности сложился этакий модно-пионерский образ: “без схемы – это круто”, “SQL теперь не нужен совсем” и т.д. На самом деле схема ведь обычно никуда не девается, просто забота о ней перекладывается на плечи самих программистов. Причем вспомнить об этом придется в самый неподходящий момент. Тут еще нужно отметить, что в современных реляционных базах тоже хватает средств для хранения данных с динамической структурой (xml/xpath, json, различные key-value расширения).

К слову, в текущем проекте мы используем MongoDB скорее по историческим причинам, а не потому что у нас высокие нагрузки, требующие горизонтального масштабирования или потому что это сейчас модно. Просто мы выбрали ее на одном из предыдущих проектов, где она была действительно обоснована. И, поскольку, основные проблемы уже изучены, используем ее до сих пор и на других проектах. Но, как видим, бывает и новые проблемы появляются.

P.S. Уже после написания этого поста увидел практически аналогичный, местами более подробный, но с несколько другими акцентами.

Twenty twelve, google maps и корень зла

В очередной раз обновили вордпресс.  Поставив новую, ещё более минималистическую тему, привычно уже обнаружили то, что новая тема по-новому кривит встроенную на странице контактов карту.

Прошлогодняя тема twenty eleven ставила максимальную ширину картинкам, так что тайлы карты съёживались, открывая промежутки между ними (обсуждение можно найти, например, тут).

Новая тема twenty twelve добавляет картинкам элегантные тени, от чего карта начинает выглядеть так, будто её хранили в сложенном виде, и, расправив, не смогли разгладить на сгибах.

Если вдруг кому интересно, то лечится это, как и раньше, банальным добавлением стиля, с отменой этого эффекта у картинок – потомков контейнера с картой:

#map_canvas img { max-width: none; box-shadow: none; }

Сложностей, в принципе, никаких: всё решается за пару минут тыканья в каком-нибудь фаербаге.  Тем не менее, при всей своей незначительности, это – проявление настоящей проблемы, куда большей и на данный момент принципиально неразрешимой.

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

В частности, каскадность стилей (первая C из CSS) – вещь замечательная и незаменимая, но её наличие делает невозможным построение компонентов, которые гарантированно получится использовать повторно.  Просто потому, что отображение внутреннего содержимого компонента зависит и от того, где он помещён.

Даже если компонент явно укажет все используемые стили, в том числе и те, значения которых берутся по умолчанию, этого всё равно будет недостаточно – развитие технологий рано или поздно приведёт к появлению новых стилей, влияющих неизвестным заранее способом.  Причём, это происходит регулярно: производители каждого браузера постоянно добавляют свои возможности разной степени экспериментальности, с вендорными префиксами или без.

Некоторую надежду вселяет то, что попытки изобрести способ совместить несовместимое всё-таки предпринимаются.  Спецификация Web Components пока на самой ранней стадии обсуждения, но именно с ней связаны основные на данный момент надежды на решение, в том числе, и вышеозначенной проблемы изоляции компонента от контекста.  Правда, то, что там описано, судя по всему, потребует выворачивания наизнанку всей логики работы браузера, со всеми вытекающими последствиями.

Автоматизация функционального тестирования веб-приложений


ScalaTest FreeSpec, Selenium2 WebDriver, HtmlUnit без CSS везде где можно, встроенный прокси-сервер для аутентификации, ThreadLocal + ShutdownHook для запуска и остановки браузеров.

То же самое, но очень длинно

Конечная цель изысканий

На самом деле нужно было:

  1. Составить и согласовать с заказчиком сценарий приёмочного тестирования проекта.
  2. Отрабатывать его при каждом обновлении софта на боевых серверах.  По возможности, автоматически.

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


Инструментов для этого существует превеликое множество.  Я расскажу о том наборе, который использовали мы,  потому что результат нас вполне устроил.

Когда мы пишем на скале, для модульного и интеграционного тестирования используем ScalaTest.  Для своих целей весьма удобный инструмент, поводов его на что-либо менять пока не возникало. Поддерживает кучу методологий тестирования (TDD, BDD и т.д.) и при необходимости интегрируется с кучей библиотек (например, мы использовали через скалатестовскую обёртку EasyMock, это было действительно easy).  Кроме того, в последнее время он неплохо поддерживается идеей, что есть безусловный плюс.

WebDriver – сравнительно недавнее дополнение проекта Selenium.  Если первый селениум выполнял скрипты в собственном формате, записанные с действий пользователя в браузере, то второй селениум даёт возможность управлять браузером из сторонних программ через вебдрайвер. К нему есть библиотеки на разных языках, жабью вполне можно использовать из скалы.  У нас в конторе раньше применялся первый селениум, так что плюсы и минусы того подхода были известны, и то, что вместо этого стоит использовать вебдрайвер, было понятно сразу.

Выбор TestSuite

В конечном итоге мы хотим получить протокол тестирования, который можно будет использовать  в качестве официального документа – согласовывать, передавать в составе дистрибутива и так далее.  То есть, он должен быть в описательном виде, доступном неспециалисту.

Второй момент: у нас ожидается повторяемость и вложенность проверок.  Одним из побудительных мотивов для автоматизации как раз является то, что в тестируемом проекте интерфейс локализован на несколько языков, а поскольку на каждом используются свои справочники и данные из базы, приходится проверять работоспособность одинаковых функций на каждом языке отдельно.  То есть, нам надо иметь возможность группировать отдельные проверки, и выполнять эти группы несколько раз для разных урлов.

По смыслу к этим требованиям близки спецификации в BDD стиле.  Скалатест умеет несколько их разновидностей (FunSpec, FlatSpec, FreeSpec, WordSpec), тонкости их отличий оставим для истинных ценителей, для этой задачи важно только то, что часть из них использует сочетания волшебных BDD’шных слов should, when и then, как на уровне определения спецификации, так и при выдаче результатов.  Что нас не устраивает, поскольку нас интересует протокол на русском языке.  Поэтому выбираем FreeSpec, где синтаксис DSL сведён до абсолютного минимума и вложенность описывается самым естественным образом:

"группа проверок" -
	"что проверить" in { /* код проверки */ }

Это позволяет нам сразу же вынести несколько проверок в метод, и вызывать его для разных языков:

"[RUS]" - performTest("rus")
"[KAZ]" - performTest("kaz")

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

Выбор WebDriver’а

У селениума в комплекте имеются драйверы для большинства распространённых настоящих браузеров (включая пару мобильных) и для эрзац-браузера HtmlUnit.  Задачи автоматического тестирования кросс-браузерной совместимости у нас на данный момент нет, поэтому настоящий браузер мы взяли один, конкретно фаерфоксовский, поскольку он там родной и не требует никаких дополнительных движений.  Просто запускает отдельное окно фаерфокса с отключенными плагинами, в котором моргают загружаемые странички.

HtmlUnit, напротив, не имеет пользовательского интерфейса, только программный.  Он честно пытается изображать внутреннее устройство браузера, и получается это у него, хоть и посредственно, но зато очень быстро – быстрее настоящего браузера на десятичный порядок.  Поскольку на практике существенная часть тестов сводится к последовательности «загрузить страницу, убедиться, что в DOM есть нужный элемент, щёлкнуть по нему», суррогатный браузер, при всех своих ограничениях, оказывается весьма и весьма востребован.  Всё, что получается тестировать им, тестируется им.

Более того, имеет смысл даже отключить некоторые реализованные в HtmlUnit возможности, например жаваскрипт (он отключен по умолчанию) и поддержку CSS – просто потому, что понимает он их всё равно в ограниченном объёме (например, jQuery у него отработать не получается), а ресурсы на попытки тем не менее тратит. Разбирать современные таблицы стилей в несколько тысяч строк тоже довольно накладно, а по факту они используются только для попытки определения видимости элементов, и самое страшное, чем грозит их отключение – то, что эрзац-браузер будет соглашаться кликать на невидимые элементы, в то время как настоящий браузер отказывается.  Проверить совпадение результатов написанных тестов под разными драйверами полезно в любом случае, так что такие различия выявляются сразу же и проблем не создают.

Наскальная специфика

Возможность свободного переключения между драйверами несколько осложняется тем, что, по непонятным причинам, классы FirefoxDriver и HtmlUnitDriver не имеют общих предков, зато реализуют кучу интерфейсов на каждую возможность по отдельности.  Тем, кто пишет на жабе, это, наверное, доставляет неудобства необходимостью постоянного приведения типов. На скале с этим проще – можно определить алиас типа и использовать его:

type WebDriverFull = WebDriver with JavascriptExecutor
	with FindsById with FindsByLinkText with FindsByXPath
	with FindsByName with FindsByCssSelector with FindsByTagName
	with HasCapabilities with HasInputDevices

Некоторые части жабьего API вебдрайвера стоит спрятать под обёртками. Например, для конструкции, которую используют для ожидания появления чего-нибудь на странице,  можно сделать такую обёртку:

def waitFor[T](probe: WebDriver => T):T =
	new WebDriverWait(driver, 10).until(
		new ExpectedCondition[T]
			def apply(d:WebDriver):T = probe(d)

И использовать её станет гораздо проще:

val myDynamicElement =

Ещё селениумовским сообществом наработаны свои приёмы, в частности паттерн Page Object, который предполагает вынесение в отдельный класс методов, обращающихся с конкретной страницей, и описание логики остальных тестов уже в терминах этих методов.  Следуя при разработке принципу DRY, к подобной схеме можно прийти эволюционно: методы выделяются по мере повторного обращения, а когда возникает необходимость использования из других классов, выносятся в отдельный трейт.  В результате получается один трейт с методами для доступа к присутствующим на всех страницах полям общего шаблона оформления и трейты для отдельных  страниц. К конкретному TestSuite подмешиваются нужные из них.

Практические наблюдения

Имея экземпляр вебдрайвера, можно приступать к запрашиванию страниц и кликанию по ссылкам:


Идеологически правильным считается взаимодействие с браузером с пользовательской точки зрения, например, текст вводится путём эмуляции нажатия отдельных кнопок, ссылки лучше искать по тексту ссылки, а не по id тэга, и так далее.  На практике это получается далеко не всегда, и код тестов большей частью состоит из хитрых выборок по объектной модели документа.

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

Перебор элементов

Один из первых вопросов, которые возникают: как получить элемент по номеру?  Скажем, третий пункт меню, которое сделано в виде вложенных списков ссылок.

У вебдрайвера масса методов для поиска элементов разными способами: есть простые – по id, классу, имени тэга, тексту ссылки; а есть сложные – по CSS селектору и по XPath выражению.

В общем случае, сложные запросы рекомендуется делать CSS селекторами, потому как браузерам их отрабатывать проще.  Здесь был бы уместен псевдокласс :eq() как в jQuery, но в CSS его нет.  В CSS3 есть :nth-child() и :nth-of-type(), но у них другая семантика: аналог $(“#menu ul a:eq(2)”) – это скорее $(“#menu ul li:nth-child(3) a”), не каждый селектор можно переписать подобным образом.  И, что хуже всего, HtmlUnit не поддерживает CSS3 селекторы.

Можно написать это в виде XPath:


XPath – это вообще мощная штука. Им можно успешно задавать очень сложные условия, но некоторые из них тяжело задать правильно.  Например, отфильтровать элемент по заданному классу, если классов у него несколько и имя нужного класса полностью входит в другое имя, скажем, checked/unchecked.

На практике удобнее всего выбирать список, а по номеру из него доставать уже на стороне скалы:

driver.findElementsByCssSelector("#menu a")(2)

Можно произвольно смешивать обработку селениумом с обработкой в скале.  Но, хоть в случае использования HtmlUnit’а это и практически бесплатно, зато это ощутимо влияет на производительность в настоящем браузере.  Получение какого-нибудь атрибута у каждого элемента в большой коллекции выливается в кучу отдельных запросов по http к вебдрайверу. В результате, при количестве элементов, исчисляемом сотнями, тест вроде этого может задуматься на несколько секунд:

"У фасетов все ссылки различаются" in
	val links = driver.findElementsByCssSelector(".facets li a")
	val hrefs ="href")).toSet
	assert(links.size === hrefs.size)
	assert(links.size > 0)

Если нужно выбирать действительно много атрибутов в настоящих браузерах, имеет смысл отправить в браузер жабаскрипт, который выберет их там и вернёт все одним запросом:

val jsHrefs = driver.executeScript(
	""" return $.map( $('.main_facets ul li a'),
		function(e){ return $(e).attr('href') }); """


Самая большая проблема, с которой на данный момент столкнулись.

Перед выкатыванием сборки на боевой сервер, она разворачивается на staging сервере, и сначала тесты прогоняются на нём.  Staging сервер тоже в интернете – он используется для согласования изменений с заказчиком, но, по понятным причинам, не является общедоступным, и закрыт простейшим паролем. Базовой http-аутентификацией.

Как выяснилось, проходить аутентификацию селениум не обучен.  У них на трекере много лет висит открытая проблема.

Есть нормальное решение для HtmlUnit – он это может сам, поэтому при создании вебдрайвера, можно HtmlUnit у него получить и прописать ему нужные параметры.  В наскальном исполнении это может выглядеть примерно так:

new HtmlUnitDriver
	val client = getWebClient
	val provider = new DefaultCredentialsProvider();
	provider.addCredentials(login, password);

Для настоящих браузеров, того же фаерфокса, нормальных решений нет.  Народ предлагает обходные пути, основная масса которых сводится к дописыванию логина с паролем к каждой ссылке в стиле У некоторых это даже работает.  У остальных там срабатывают антифишинговые фильтры, которые пытаются хитро отключать, не работает аякс, и самое гадкое: это происходит через раз, то есть работает нестабильно.  Что совершенно неприемлемо для механизма, предназначенного стабильность обеспечивать.  Не говоря уже о необходимости учитывать наличие или отсутствие этой части при сравнении урлов в тестах.

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

В комментариях к той проблеме на их трекере один товарищ это недавно предлагал. Правда, сервер он использовал слишком суровый на наш взгляд – с тяжёлыми зависимостями и так далее.  Встраиваемых http прокси-серверов для жабы в природе существует несколько, разной степени продвинутости и заброшенности: littleproxy который был у него, exproxy, wpg-proxy.   Может и ещё есть.

Самым простым в использовании навскидку показался wpg-proxy. Со своей задачей он отлично справился, хотя и создал неочевидную проблему с перенаправлениями, но об этом позже. Запускаем его примерно так:

ProxyRegistry.addRequestProcessor(new HttpMessageProcessor
	def doContinue(msg: HttpMessage) = true
	def doSend(msg: HttpMessage) = true
	def process(msg: HttpMessage) =
		val encoded = Base64.encodeBase64String(
				(login + ":" +password).getBytes)
			.replace("\n", "").replace("\r", "")
		msg.addHeader("Authorization", "Basic "+encoded)
val proxy = new com.wpg.proxy.Proxy(
	InetAddress.getByName(""), proxyPort, 50)

При создании фаерфоксовского вебдрайвера ему передаём параметром пожелание иметь подключение через прокси-сервер:

val proxy = new org.openqa.selenium.Proxy();
val cap = new DesiredCapabilities()
cap.setCapability(CapabilityType.PROXY, proxy);
new FirefoxDriver(cap)

Обращаю внимание: класс Proxy в этом случае другой.

В результате аутентификация работает замечательно, и никаких изменений в коде тестов не требуется.

Скачивание файлов

Найти и щёлкнуть ссылку «download» можно запросто.  А вот объяснить браузеру куда следует сохранять скачиваемый файл через вебдрайвер нельзя – у многих браузеров там нативные диалоги, до которых скрипты вебдрайвера добраться просто не могут.  Народ с этим борется настройкой профилей браузерам, чтобы они сохраняли сразу туда, куда надо, и без лишних вопросов.  Потом ещё нужно будет определить момент завершения закачки.

Всех этих действий можно избежать, если скачивать файлы самостоятельно.

В простом случае достаточно получить href у ссылки и запросить.  Например, встроенными средствами JRE– new URL(href).openStream(), и так далее.   Либо с комфортом использовать какую-нибудь библиотеку. Мы взяли Resty – довольно удобно, аутентификация производится буквально одним вызовом метода.

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

Через HtmlUnit всё работало замечательно, а через настоящий браузер с прокси-сервером – нет. Вместо файла скачивалась страница с сообщением о перенаправлении, причём как с помощью Resty, так и вручную.  В конченом итоге выяснилось, что наш встроенный прокси-сервер переопределил стратегию отработки перенаправлений по умолчанию – сделал где-то у себя внутри HttpURLConnection.setFollowRedirects(false).

Можно было бы вернуть эту глобальную настройку обратно, но, очевидно, это было сделано с какой-то целью, и сложно предсказать на что это повлияет. Поскольку для нас такое поведение важно только при скачивании, разумно указать это для конкретного соединения.  Благо, при создании экземпляра Resty, ему можно передать подкласс Resty.Option, в котором есть метод, получающий каждое новое соединение для настройки.

new Resty(new Resty.Option
	override def apply(conn: URLConnection)
		conn match
			case c: HttpURLConnection => 
			case _ =>

Когда запускать и останавливать вебдрайверы

Запуск браузера – процедура достаточно длительная. При запуске фаерфокса селениум сначала берёт настоящий профиль, копирует в темповый каталог, потом чистит его от лишнего, дописывает туда свой плагин, запускает фаерфокс с этим профилем. Ощутимо легче становится, если переложить темповый каталог на ssd, возможно с рамдрайвом будет ещё лучше. HtmlUnit инициализируется намного быстрее, но тоже не моментально.  В общем, скорость запуска – достаточно веский довод в пользу того, чтобы лишний раз этого не делать.

Останавливать его тоже надо корректно: если вебдрайверу не вызвать метод close(), он оставит запущенный браузер открытым.  И при следующем запуске ещё один, и так далее.

У скалатеста имеются трейты BeforeAndAfter и BeforeAndAfterAll которые можно подмешать к Suite и получать управление до и после запуска отдельных тестов и всего Suite соответственно, но штатного способа ловить границы всей сессии тестирования, вроде бы, нет.  Во всяком случае, без написания собственной запускалки тестов, чего делать совершенно не хочется, поскольку сравнимого с идейской запускалкой удобства всё равно не достичь.

Сама сессия тестирования может варьироваться в широких пределах: это может быть отдельный тест, набор тестов (Suite), несколько тестов из разных наборов (такое часто происходит при запуске упавших в прошлый раз тестов), все наборы тестов в каком-нибудь пакете, несколько наборов, объединённых в группу (Suites).  Причём наборы в группе могут быть запущены параллельно (с ключом -c), и каждому потребуется собственный экземпляр драйвера. И закончиться тестирование может штатно, а может аварийно.

За отсутствием нормальных интерфейсов, получаем желаемое грубой силой – низкоуровневыми средствами жабьей платформы.

Заводим ThreadLocal  для каждого типа драйвера, и создаём экземпляры драйвера в методе initialValue.

new ThreadLocal[WebDriverFull]
	override def initialValue() = createBrowser()

То есть, создаётся максимум один вебдрайвер каждого типа на поток.  Многопоточная отработка тестов при этом проходит нормально: открывается нужное количество браузерных окон, в них всё вразнобой моргает.  Правда делать это стоит только тогда, когда результат интересует в двоичном виде – всё прошло или нет, потому что и лог, и дерево результатов в идейской запускалке от этого кривятся. Однопоточное тестирование открывает ровно одно окно на все тесты.

Регистрируем у жабьего рантайма ShutdownHook.  Закрытие драйверов и остановку прокси-сервера производим в нём.

Runtime.getRuntime.addShutdownHook(new Thread()
	override def run()

Все открытые окна после тестирования закрываются сами.  Правда, все разом – окна потоков, которые закончатся раньше других, будут висеть открытыми, пока не закончатся все остальные.


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

Выглядит примерно так:

Главная страница

На русском языке

  • Страница загружается
  • Присутствуют ключевые элементы страницы
    • Большая картинка в шапке
    • Форма полнотекстового поиска
    • Горизонтальный блок фасетов
  • Подсвечен пункт меню №1
  • Подсвечен нужный язык
  • Для всех локализуемых строк имеются строковые ресурсы
  • При смене языка выдаётся страница того же типа
  • Корректный заголовок окна

И дальше ещё много в том же духе.

Вроде бы всех устраивает.

Scala: вызов метода с tuple вместо набора параметров


Метод так вызвать нельзя, но можно сконвертировать его в функцию, а у них есть метод tupled.

То же самое, но длинно

Потребовался нам в проекте генератор тестовых данных.  Модель данных уже есть, типы и ограничения прописаны, можно спокойно брать и генерить.  Нашлась даже подходящая библиотека с набором методов для генерации примитивов – DataFactory от товарища Andy Gibson’а.

Получилось всё легко и просто, выглядит при этом в таком духе:

field match
	case f:Field.NumberField => df.getNumberBetween(0, 1000)
	case f:Field.StringField => genRandomString(100, 200)
	// и так далее

Для пущей правдоподобности полезно соблюсти границы значений,  которые прописаны в модели.  Правда, указаны они, понятно, не у всех полей, поэтому и границы по умолчанию следует указать.  Причём, в разных местах они отличаются, поэтому в класс самого поля жёстко их зашивать не хотелось бы.  Не проблема совершенно – пишется простенький метод, который принимает дефолтные границы, перебирает коллекцию валидаторов, и если находит подходящий – возвращает границы из него, а если нет, то дефолтные.  Возвращает естественно в виде tuple (слово «кортеж» меня коробит).

def limits(min:Int,  max:Int):(Int,  Int) =
		case v:Validator.Length => (v.min, v.max)
		case v:Validator.Range 	=> (v.from,
	}).getOrElse((min, max))

Только вот методы DataFactory принимают параметры по отдельности, и правильно делают.  Не говоря уже о том, что это вообще жабья библиотека, и понятием наскальных туплов она не владеет.

Затруднение с этим тоже не великое: наскальные экстракторы работают замечательно, вполне можно написать:

val (min, max) = limits(0, 10)

Но это же надо будет в каждом кейсе писать… Аж по две переменные два промежуточных значения… Или по одному, но с уродливыми ._1 и ._2. Неэстетично-с…

Именно для таких случаев у наскальных функций имеется метод tupled, который позволяет функцию от нескольких параметров вызвать с одним tuple.

Процесс преобразования метода в функцию в скале принято называть звучным термином из лямбда-исчисления «eta expansion».  По сути это означает, что компилятор сгенерирует вложенный класс-обёртку, унаследованный от FunctionN (в нашем случае Function2), который в методе apply вызывает нужный метод.

Синтаксически это оформляется подчёркиванием после имени метода.  Кстати, это совпадает с синтаксисом частичного применения функций (curring), что есть весьма консистентно.

С использованием вышеперечисленного, вызов – тема этого поста выглядит следующим образом:

(df.getNumberBetween _).tupled(limits(0, 1000))

Можно пойти ещё дальше и часть этого кода обобщить.  Например, в виде такого метода:

def limited[A](f:(Int,  Int) => A)(min:Int,  max:Int):A =
	f.tupled(limits(min, max))

Более того, в случае передачи метода в качестве параметра типа «функция», компилятор способен произвести eta expansion самостоятельно безо всяких подчёркиваний.  Конечный результат может выглядеть так:

field match
	case f:Field.NumberField => limited(df.getNumberBetween)(0, 1000)
	case f:Field.StringField => limited(genRandomString)(100, 200)

Стоило оно того или нет – это вопрос, но знать в любом случае полезно.

Scala REPL, IntelliJ IDEA, Console2

Update: с момента написания этого текста идейская наскальная консоль стала намного лучше, и для репла стоит пользоваться ей.  Использовать Console2 теперь актуальнее для запуска sbt.  Потому что при перегенерации идейского проекта плагином sbt-idea проект перегружается, и все открытые в идее консоли, включая sbt’шную, закрываются.  Так что гораздо удобнее запускать sbt во внешней консоли.  Настраивается так же, только надо ещё текущим каталогом указать $ProjectFileDir$.

Идейский наскальный плагин далеко не идеален, но в целом юзабелен. В числе прочего, у него имеется встроенная запускалка наскального репла, который сам по себе тоже бывает весьма полезен. Проблема в том, что интерактивность в набортных идейских консолях оставляет желать лучшего. В особенности, когда консольный софт хочет каких-нибудь сочетаний клавиш, которые идея старательно порывается отработать самостоятельно. Благо использование этой консоли сугубо опционально, потому как запустить scala interpreter несложно и руками.

Правда, несколько мелких препятствий всё-таки имеется.

Первое: конторские машины у нас под виндами, а родная виндовая консоль тоже убога до невозможности. Глубоко убеждён, что отказывать пользователю даже в растягивании окошка в ширину мышкой в наше время просто неприлично. Не говоря уже про прочие радости, вроде строго блочного выделения текста. Хорошо, что существуют альтернативные консоли, в частности Console2, которой мы и воспользуемся. К слову, в интернетах попадается ещё такая штука, как ScalaConsole с гуём на свинге, но, несмотря на название, это не консоль с командной строкой и историей, а редактор для наскальных скриптов с возможностью их запуска, каковой в идее реализован гораздо лучше.

Второе: привычный jar-hell вполне распространяется и на scala interpreter. Вольное обращение с автоматический подгрузкой зависимостей каким-нибудь мавеном с лёгкостью приводит classpath в состояние полной непотребности. Померил в одном из наших проектов: длинный список jar’ок, каждая с полным путём по пакетам в локальном мавеновском репозитории, в общей сложности на 8 килобайт. Приложенная к экрану линейка и нехитрая арифметика показывают, что, будучи записанным в строчку скромным 12 кеглем, этот classpath протянется на абсурдные 15-20 метров. Очевидно, что ручное редактирование командной строки длиной в 20 метров – занятие весьма и весьма некомфортное. А поскольку изменение зависимостей в активно разрабатывающемся проекте – действие регулярное, то и classpath реплу придётся постоянно обновлять. Бороться с этим будем получая его у самой идеи, через макроподстановку в параметрах внешнего инструмента.

Третье: помимо зависимостей от библиотек, у проекта ещё, как минимум, есть зависимость от конкретных версий JDK и самой скалы. Вынужден признать: я так и не нашёл приемлемого способа получения от идеи каталога lib выбранного в проекте компилятора скалы. Более того, учитывая то, что подключается наскальный sdk в виде глобальной библиотеки, в которой некоторых репловских jar’ок нет, подозреваю, что и сами они их для своей запускалки получают как-нибудь затейливо, а может и просто без них обходятся. Впрочем, версии жабы и скалы обновляются значительно реже, необходимости разрабатывать сразу под несколько версий у нас, слава Богу, пока нет, поэтому счёл приемлемым вариант ручного изменения путей в окружении или настройках console2.

В конечном итоге всё сводится к следующей последовательности действий:

  1. поставить console2
  2. настроить в ней scala interpreter одним из шеллов
  3. подключить это в идею в качестве external tool.

Console2 нужной редакции качается с родного соурсфорджа, последняя на момент написания версия 2.00 beta 147, вполне работоспособна. Настраивается по вкусу. Отдельно стоит упомянуть о горячих клавишах, которых она по умолчанию себе хочет штук тридцать. Из них стоит отключить или переназначить хотя бы те, которые конфликтуют с наскальным реплом. А можно и все сразу, потому как их функции в массе своей ни разу не актуальны, и в любом случае доступны из меню. Вообще, чем меньше консоль позволяет себе самодеятельности, тем лучше.

В разделе настроек с неинтуитивным названием Tabs нужно добавить элемент, указать имя вкладки (оно потом понадобится при вызове),  и прописать ему в поле Shell вызов нужного scala interpreter с нужным JDK.



У меня это выглядит так:

C:\Tools\Java\jdk1.6.0_24\bin\java -Dfile.encoding=UTF-8 -classpath

В настройках идеи нас интересует кнопка «Add…» в разделе External Tools.

В этом окне надо поснимать лишние галочки, прописать имя инструмента, указать экзешник console2 и такие параметры:

-t scala -r "-classpath \"$Classpath$\""

Ключом -t передаётся имя консольского tab с нашим шеллом.

В результате всех этих действий, в меню Tools должен появиться пункт Scala Console,запускающий в Console2 Scala REPL с classpath теущего проекта.

Hello world!

Спонтанно решили завести себе конторский бложек.

До большого сайта “как у взрослых” руки не доходят уже много лет, да и необходимости в нём особенной не было.  На мой взгляд, у мелких контор между сайтом и бизнесом имеется обратная зависимость: пока есть заказы – нету времени на сайт, заказы кончаются – появляется время для сайта, с сайта приходят новые заказы – на сайт снова не хватает времени.  Поэтому, отсутствие сайта у мелкой айтишной конторы в целом можно считать хорошим признаком.

Тем не менее, периодически хочется записать результат каких-нибудь произведённых раскопок, просто чтобы не потерялось.  Сильно привязанные к проектам вещи разумно отмечать на конторской вики, а то, что более или менее воспринимается отдельно, вполне можно выкладывать в интернеты.  Чем мы, собственно, и займёмся.

Поскольку некоторые из нас отличаются радикальностью взглядов по многим аспектам разработки софта, здесь же будем выкладывать нравоучительные истории, иллюстрирующие нашу точку зрения.

Такие, безусловно, полезные вещи, как оформление, продвижение, сео, монетизацию и прочие атрибуты типового сайтика, мы намерены злостно проигнорировать.  Если у нас вдруг напишется что-нибудь стоящее, народ рано или поздно на это выйдет с поисковиков, а если не напишется, то и смотреть нечего.

Обещать регулярности обновлений разумеется не будем.  Равно как и того, что написанное будет хоть кому-нибудь кроме нас интересно.  Но если вас всё-таки угораздило забрести на наш бложек – милости просим, чувствуйте себя как дома.