23 июня 2016, 23:16

Тестирование сервера Node.js

Темы: tdd, bdd, javascript, node, pattern

Вводная

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

Для тестирования кода на ноде я использую жасмин. Для запуска задач — галп.

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

Staged reality

Кто запускает сервер

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

if (require.main === module) {
	app.use('/', require(path.join(__dirname, 'routes', 'main'))());
	var server = app.listen(5000, function () {
		...
	});
} else {
	exports = module.exports = function (options) {
		app.use('/', require(path.join(__dirname, 'routes', 'main'))(options));
		return app;
	};
}

Таким образом, уже в тестовом фреймворке мы будем запускать:

app = require('index.js');
server = app(...).listen(6000, function (err) {...});

перед каждым тестом, а после каждого теста:

server.close(function (err) {...});

Очень удобно!

Имитация библиотек

Второе, что бы хотелось сделать при тестировании сервера, — имитировать используемые им библиотеки, чтобы тестировать только код сервера, а не библиотеки. Связки тоже важны, но только тогда, когда мы этого сами хотим. И тут появляется интересный момент. Если мы хотим имитировать библиотеки при тестировании, нам нужно использовать определённый паттерн при программировании. Паттерн фабрик. То есть мы не просто тестируем свой код, но устраиваем архитектуру и стиль так, чтобы его удобнее было тестировать.

В случае с паттерном фабрики мы возвращаем в качестве модуля не объект, а функцию, которая создаёт объект в соответствии с нашими параметрами:

exports = module.exports = function (options) {
	options = options || {};
	var customLib = options.customLib || require(...);

	/* GET main page. */
	router.get('/', function (req, res, next) {
		res.end('Real server text. CustomLib: ' + customLib.name);
	});

	return router;
};

Как видно в вызовах выше, мы передаём сквозным образом наши опции при тестировании, а при нормальном запуске, не передаём ничего.

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

Для самостоятельного изучения

Полный код примера в сборе

Комментарии 0 >>

09 июня 2016, 22:18

Куда уходят программисты

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

Missing people

Без следа

Недавно я проникся новым молодым языком программирования elm. Читал статьи, потом решил потрогать руками. Установил подсветку синтаксиса для текстового редактора, поставил библиотеки, и вдруг — беда! Поскольку язык молодой (максимум 4 года ему), то синтаксис немного поменялся в последнем релизе. И оказалось, что подсветка не работает с новым синтаксисом.

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

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

Уйти красиво

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

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

Почему

Первой же историей, которая обратила мой внимание на то, что люди отделены от своего виртуального персонажа, была история Why the Lucky Stiff. По его учебнику я изучал руби. Но в какой-то момент он решил уйти из интернета. Перед тем, как уйти в оффлайн, он написал: «Программирование весьма неблагодарно. Через год ты видишь, как твою работу замещает лучше сделанная, а через несколько лет её уже и не запустить». Это он сказал в 2009-м.

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

Комментарии 0 >>

26 мая 2016, 15:36

Создание своих правил для udev

Темы: bash, udev, linux, script

Магия

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

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

Вставил диск

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

Sorting robot

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

udevadm info -a --name=/dev/sdd1
udevadm info --query=env --name=/dev/sdd1

как уже говорил, имея инструкцию, как подготовить диск для копирования, можно всегда иметь одинаковую метку тома. Чем и пользуемся: создаём файлик /etc/udev/rules.d/90-my-storage-copy.rules, куда пишем:

ACTION=="add", ENV{ID_FS_USAGE}=="filesystem", ENV{ID_FS_TYPE}=="ext4", ENV{ID_FS_LABEL_ENC}=="storage-copy", RUN+="/usr/local/bin/storage-copy-mount.sh"
ACTION=="remove", ENV{ID_FS_USAGE}=="filesystem", ENV{ID_FS_TYPE}=="ext4", ENV{ID_FS_LABEL_ENC}=="storage-copy", RUN+="/usr/local/bin/storage-copy-umount.sh"

Всё, что выдавалось нам с параметром --query=env будет в параметрах окружения нашего скрипта /usr/local/bin/storage-copy-mount.sh:

#!/bin/sh
mount_point="/mnt/myrules/$(basename $DEVNAME)"
mkdir -p $mount_point
mount -t $ID_FS_TYPE -o ro $DEVNAME $mount_point

/usr/local/bin/storage-copy-umount.sh:

#!/bin/sh
mount_point="/mnt/myrules/$(basename $DEVNAME)"
umount -l -f $mount_point
rmdir $mount_point

Некоторые гайды не рекомендуют вызывать команду mount из правил udev, но когда это останавливало настоящих волшебников? :)

Массив без массива

Второй случай — половина рэйд-массива. Нужно собирать и разбирать массивы так, чтобы это не пересекалось с работой остальной системы. Некоторые хранилища задают метки тома своим разделам с информацией, а некоторые можно определить только по номеру партиции. /etc/udev/rules.d/90-my-storage-rais.rules:

ACTION=="add", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_PART_ENTRY_NUMBER}=="3", ENV{ID_FS_LABEL_ENC}!="system*", RUN+="/usr/local/bin/storage-raid-mount.sh"
ACTION=="add", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_FS_LABEL_ENC}=="DiskStation*", RUN+="/usr/local/bin/storage-raid-mount.sh"
ACTION=="remove", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_PART_ENTRY_NUMBER}=="3", ENV{ID_FS_LABEL_ENC}!="system*", RUN+="/usr/local/bin/storage-raid-umount.sh"
ACTION=="remove", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_FS_LABEL_ENC}=="DiskStation*", RUN+="/usr/local/bin/storage-raid-umount.sh"

То есть, говоря человеческим языком, это или рэйд-партиция с номером 3, название которой не начинается с system или рэйд-партиция с именем, начинающимся с DiskStation. Теперь нам нужно собрать массив так, чтобы у него было уникальное имя, но при этом однозначно связанное с устройством, чтобы не плодить лишних сущностей. Для этого я решил точку для монтирования называть так же как имя устройства в /dev, а номер рейда брать из кода последней буквы. /usr/local/bin/storage-raid-mount.sh:

#!/bin/sh

mount_point="/mnt/myrules/$(basename $DEVNAME)"
num=$(printf %d "'$(echo $DEVNAME | head -c8 | tail -c1)")
raid_device="/dev/md$num"

mkdir -p $mount_point
mdadm -S $raid_device
mdadm -A -R $raid_device $DEVNAME
mount -o ro $raid_device $mount_point

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

echo $DEVNAME | head -c8 | tail -c1

Выдаёт нам восьмую букву имени устройства, то есть «d» для «/dev/sdd3», например, и «f» для «/dev/sdf5».

printf %d "'d"

Выдаёт нам 100, а в случае с «f» — 102. И мы получаем имя «/dev/md100», под которым насильно поднимаем raid1 на одном диске из двух. И обратно то же самое. /usr/local/bin/storage-raid-umount.sh

#!/bin/sh

mount_point="/mnt/myrules/$(basename $DEVNAME)"
num=$(printf %d "'$(echo $DEVNAME | head -c8 | tail -c1)")
raid_device="/dev/md$num"

umount -l -f $mount_point
mdadm -S $raid_device
rmdir $mount_point

Понятно, что с вытаскиванием сложнее, даже если монтировать, как это делаю я, только для чтения. Это всё актуально, если после работы с диском прошло значительное время. И я предпочитаю хотя бы размонтировать вручную. Но при этом совершенно прекрасно то, что все наши устройства будут создавать папки и появляться в /mnt/myrules, как флэшки появляются в /media на десктопных версиях Убунту.

Для самостоятельного изучения

1. man udev

Комментарии 0 >>

11 мая 2016, 23:13

Как показать Яндекс Панорамы где угодно

Темы: javascript, hack, ajax, security

История

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

Со временем, сменился плеер и появился код вставки вида:

<script src="//panoramas.api-maps....."></script>

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

Hack

Параметры

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

ll=37.61782676%2C55.75074572

Это же долгота и широта (в таком порядке) через запятую! Второе, с чем сразу же хочется повозиться — это параметр:

size=690%2C495

Это ширина и высота окна через запятую. Если посмотреть на то, как работает скрипт, и что он оставляет после себя на странице, то совсем не обязательно лезть и деобфусцировать код. Всё понятно: скрипт создаёт вместо себя тег iframe и другой тег script, а себя удаляет. Параметр size и задаёт размеры айфрейма.

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

var latlng = L.latLng(55.75074572, 37.61782676);
var $map = L.map('map', {center: latlng, zoom: 15});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo($map);

function openPanoramaAt(latlng) {
	var panoDiv = $('#panorama');
	var panoScript = document.createElement('script');
	panoScript.type = 'text/javascript';
	panoScript.src = 'https://panoramas.api-maps.yandex.ru/embed/1.x/?lang=ru&ll=' + latlng.lng + '%2C' + latlng.lat + '&ost=dir%3A0.0%2C0.0~span%3A130%2C70.26418362927674&size=' + panoDiv.width() + '%2C' + panoDiv.height() + '&l=stv';
	panoDiv.empty();
	panoDiv[0].appendChild(panoScript);
}

openPanoramaAt(latlng);

$map.on('click', function (me) {
	$map.panTo(me.latlng);
	openPanoramaAt(me.latlng);
});

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

Но нам нужно идти дальше. Ведь внутри панорам можно переходить по стрелкам, а у нас это никак не отображается на карте. Что же делать?

Человек посередине

Чтобы определить, что делает скрипт, у нас и так уже открыт инспектор страницы. Теперь, дорогой читатель, давай переключимся во вкладку «сеть».

Переходя по стрелкам мы увидим, что, кроме всего прочего, плеер запрашивает файлик по адресу https://panoramas.api-maps.yandex.ru/panorama/1.x/?l=stv&lang=ru_RU&...&format=json. Тут уже нет широты и долготы, а присутствует идентификатор, но если открыть этот джейсон в новой вкладке, то внутри него мы увидим нужные нам координаты:

JSON.parse(response).data.Data.Point.coordinates

То, что браузер видит это запрос, означает, что скорее всего в основе лежит XMLHttpRequest. Так как заголовка Content-Security-Policy не видно (по правде сказать, настраивать его довольно сложно, и обычно если кто и прописывает такой заголовок, то там среди прочего есть unsafe-inline), то мы попробуем подслушать, о чём говорит плеер с сервером.

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

    Сразу видно несколько недостатков:
  1. Размеры панорамы не адаптируются при изменении размеров окна.
  2. Невозможно отследить направление взгляда, чтобы показать его на карте. Я рылся в объектах, но ничего не нашёл.
  3. Пришла беда, откуда не ждали: случился прогресс!

Правильный API

Тема этого топика была запланирована у меня некоторое время назад. И основные фишки были опробованы и сделаны тоже некоторое время назад. Но когда я сел писать конкретный код, то обнаружил, что в стандартной поставке API карт уже есть панорамы. Произошло это в прошлой версии 2.1.38 от 31 марта 2016. Сейчас я работал с 2.1.39. Всего 42 дня как можно ставить панорамы на карты!

Конечно же, я собрал такой же пример на API Яндекс Карт. (Всё-таки синтаксис лифлета намного изящнее, извините). Это настолько новое явление, что даже не входит в стандартный полный набор модулей. Заметьте во внешних ресурсах слева я написал для загрузки (иначе не работает):

load=package.full,panorama.isSupported,panorama.locate,panorama.createPlayer,panorama.Player

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

Так что с точки зрения дальнейшего использования, моё решение утратило свою актуальность. Но с точки зрения образовательной — очень даже отличный материал, я считаю!

Отдельно ссылки:

  1. Как приспособить код для вставки Яндекс Панорам на свои карты (jsfiddle);
  2. Как показать панорамы с помощью API Яндекс Карт (jsfiddle);
  3. Intercept all ajax calls (stackoverflow);
  4. API Яндекс Карт;
  5. Лучший API для картографических сервисов Leaflet.

Комментарии 0 >>

27 апреля 2016, 23:24

Стрелки для krpano на несущетсвующем языке

Темы: javascript, xml, syntax, antique

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

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

Стрелки

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

Mayalanguage

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

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

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

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

Сжатие

Для тестирования я решил использовать микро-сервер node.js, а вместе с ним инструмент для исполнения задач gulp. Одна такая задача — убирать лишние пробелы в придуманном скриптовом языке krpano. Не столько для обфускации или экономии трафика, сколько опять же для интереса.

Для сжатия xml я нашёл pretty-data, а недостающий кусок дописал:

var gulp = require('gulp'),
	prettyData = require('gulp-pretty-data');

// Minify krpano action
function minifyAction() {
	function dry (file, cb) {
		file.contents = new Buffer(String(file.contents).replace(/(<action.+?>)([\s\S]+?)(<\/action>)/ig, function (str, opentag, cnt, closetag) {
			return opentag + cnt.replace(/\s*(^|[;,=!])\s*/g, "$1") + closetag;
		}));
		cb(null, file);
	}

	return require('event-stream').map(dry);
}

// Minify plugin xml
gulp.task('xml', function () {
	return gulp.src('dev/arrows.xml')
		.pipe(prettyData({type: 'minify'}))
		.pipe(minifyAction())
		.pipe(gulp.dest('arrows/example/'));
});

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

Ссылки

  1. Виртуальный тур по Арктике с моими стрелками.
  2. Репозиторий krpano-arrows на гитхабе.
  3. Страница плагина на krpano.com.

Комментарии 0 >>