Category Archives: Ноу-хау

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));
		}
		.iterator(@index) 
		{
			@item: extract(@array, @index);
			@lambda();
		}
		@length: length(@array);
		.iterator(@length);
	};
}

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: http://www.pnml.kz/2014/06/detached-rulesets-in-less-as-a-lambda-expressions/
.foreach(@array, @lambda)
{
	&
	{
		.iterator(@index) when (@index > 1)
		{
			.iterator((@index - 1));
		}
		.iterator(@index) 
		{
			@item: extract(@array, @index);
			@lambda();
		}
		@length: length(@array);
		.iterator(@length);
	};
}



// 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, там попросту нет.  Простейший цикл с должным количеством конкатенаций упирается в крайне низкую производительность работы со строками.

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

Все эти варианты, и многие другие, можно посмотреть, например, тут: http://stackoverflow.com/questions/202605/repeat-string-javascript.  На мой взгляд, глубоко неправилен уже сам факт того, что примитивная потребность способна вызывать настолько развёрнутую дискуссию, но не будем о грустном.

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

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

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

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

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

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

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

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

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

jsperf

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

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

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

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

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

tl;dr

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 =
	waitFor(_.findElement(By.id("myDynamicElement")))

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

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

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

driver.get("http://localhost:8080/")
driver.findElementByPartialLinkText("Home").click()

Идеологически правильным считается взаимодействие с браузером с пользовательской точки зрения, например, текст вводится путём эмуляции нажатия отдельных кнопок, ссылки лучше искать по тексту ссылки, а не по 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:

driver.findElementByXPath("id('menu')/ul/li[3]/a")

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

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

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

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

"У фасетов все ссылки различаются" in
{
	val links = driver.findElementsByCssSelector(".facets li a")
	val hrefs = links.map(_.getAttribute("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') }); """
).asInstanceOf[java.util.List[String]].toSeq

HTTP-аутентификация

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

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

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

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

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

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

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

В комментариях к той проблеме на их трекере один товарищ это недавно предлагал. Правда, сервер он использовал слишком суровый на наш взгляд – с тяжёлыми зависимостями и так далее.  Встраиваемых 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)
		msg
	}
})
val proxy = new com.wpg.proxy.Proxy(
	InetAddress.getByName("127.0.0.1"), proxyPort, 50)
proxy.start()

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

val proxy = new org.openqa.selenium.Proxy();
proxy.setHttpProxy("localhost:"+proxyPort)
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 => 
				c.setInstanceFollowRedirects(true)
			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()
	{
		createdBrowsers.foreach(_.close())
		proxyHolder.foreach(_.shutdown())
	}
})

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

Результат

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

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

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

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

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

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

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

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

tl;dr

Метод так вызвать нельзя, но можно сконвертировать его в функцию, а у них есть метод 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) =
{
	field.validators.collectFirst(
	{
		case v:Validator.Length => (v.min, v.max)
		case v:Validator.Range 	=> (v.from, v.to)
	}).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
"C:\Tools\Scala\scala-2.9.0.1\lib\scala-compiler.jar;
C:\Tools\Scala\scala-2.9.0.1\lib\scala-library.jar;
C:\Tools\Scala\scala-2.9.0.1\lib\jline.jar;"
scala.tools.nsc.MainGenericRunner

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


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

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

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


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