LE Blog

Инженер от души

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

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

28.05.2009 firtree_right Тестирование в ActionScript с помощью AsUnit

Введение

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

Один из инструментов, который мог бы мне помочь, если бы я его использовал сразу — это тестирование, а точнее Unit Tests (изолированное тестирование).

Понятно, что ActionScript не самый удобный язык для изолированного тестирования. Сложно сделать связанные объекты независимыми друг от друга, чтобы тестировать их по отдельности. Да и в целом продукты на Flash и Flex имеют большую интерфейсную составляющую. И тестировать зачастую нужно впечатления пользователя. Но тем не менее, покрытие тестами, пусть и небольшое, — это удобно.

Задача

Протестировать объект, который получает данные из xml и выдает их в качестве собственных параметров. Попробуем сделать это в лучших традициях TDD

Ресурсы

Нам понадобится AsUnit — прекрасная библиотека с открытыми исходниками для изолированного тестирования (unit testing).

Так же используем лучшие традиции TDD:

  1. Никакого кода, пока нет провалившегося теста.
  2. Тест пишется до тех пор, пока он не начнет проваливаться.
  3. Кода нужно писать ровно столько, чтобы проваливающийся тест прошёл.

Для решения будем использовать Adobe Flash CS3 в качестве редактора.

Решение

Итак, скачиваем AsUnit с сайта по ссылке выше. Создаем среду для тестирования. ConfigTest.as:

package {
  import asunit.framework.TestCase;

  public class ConfigTest extends TestCase {
    private var instance:Config;

    /**
     * Запускается перед каждый тестом
     */
    protected override function setUp():void {
      instance = new Config();
    }

    /**
     * Запускается после каждого теста
     */
    protected override function tearDown():void {
      instance = null;
    }

    /**
     * Тест-проверка, что созданный объект нужного класса
     */
    public function testIsConfig():void {
      assertTrue("Example is Config", instance is Config);
    }
  }
}

Это набор тестов для нашего будущего класса, который будет называться Config. Теперь, если бы у нас было несколько классов и несколько наборов тестов, их нужно было бы собрать воедино. AllTests.as:

package {
import asunit.framework.TestSuite;

  public class AllTests extends TestSuite {
    public function AllTests() {
      super();
      addTest(new ConfigTest());
    }
  }
}

Теперь нам нужен, собственно, тестировщик. AsUnitRunner.as:

package {
import asunit.textui.TestRunner;

  public class AsUnitRunner extends TestRunner {
    public function AsUnitRunner() {
      start(AllTests);
    }
  }
}

Теперь создаем AsUnitRunner.fla, в настройках File->Publish Settings->Flash->Settings прописываем в Document class базовый класс AsUnitRunner и добавляем путь к asunnit/as3/src в Classpath.

Попробуем запустить (ctrl+enter) — неудача! :) Можно сказать, провалившийся тест. Чтобы тест прошёл достаточно создать описание класса. Config.as:

package {
  public class Config {
  }
}

И теперь когда мы запускаем наш тестировщик мы видим:

AsUnit 3.0 by Luke Bayes and Ali Mills

Flash Player version: WIN 9,0,115,0

.

Time: 0.024

OK (1 test)


Time Summary:

23ms : ConfigTest

Все тесты проходят. Пора закончить писать код и снова перейти к написанию тестов. Добавим тестирование желаемого функционала. Я хочу загружать в Config xml и получать значения из него в виде заданных параметров. В описание ConfigTest.as добавим метод:

    /**
     * Тест-проверка, что из xml получаются параметры x и y
     */
    public function testParsesCoordinates():void {
      var xml:XML = <root>
                      <point>
                        <x>10</x>
                        <y>20</y>
                      </point>
                    </root>
      instance.fromXML(xml);
      assertEquals("X property should equal 10", 10, instance.x);
      assertEquals("Y property should equal 20", 20, instance.y);
    }

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

package {
  public class Config {
    public function fromXML(xml:XML):void {
    }
    public function get x():int {
      return 0;
    }
    public function get y():int {
      return 0;
    }
  }
}

Результат тестирования теперь выглядит так:

AsUnit 3.0 by Luke Bayes and Ali Mills

Flash Player version: WIN 9,0,115,0

..F

Time: 0.037
There was 1 failure:
0) ConfigTest.testParsesCoordinates
AssertionFailedError: X property should equal 10 expected:<10> but was:<0>
    at asunit.framework::Assert$/fail()
    at asunit.framework::Assert$/failNotEquals()
    at asunit.framework::Assert$/assertEquals()
    at ConfigTest/testParsesCoordinates()
    at asunit.framework::TestCase/runMethod()
    at asunit.framework::TestCase/runBare()
    at Function/http://adobe.com/AS3/2006/builtin::apply()
    at <anonymous>()
    at SetIntervalTimer/onTimer()
    at flash.utils::Timer/_timerDispatch()
    at flash.utils::Timer/tick()

FAILURES!!!
Tests run: 2,  Failures: 1,  Errors: 0


Time Summary:

36ms : ConfigTest

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

package {
  public class Config {
    private var _x:int;
    private var _y:int;
    public function fromXML(xml:XML):void {
      _x = int(xml.point.x);
      _y = int(xml.point.y);
    }
    public function get x():int {
      return _x;
    }
    public function get y():int {
      return _y;
    }
  }
}

А результат теста так:

AsUnit 3.0 by Luke Bayes and Ali Mills

Flash Player version: WIN 9,0,115,0

..

Time: 0.037

OK (2 tests)


Time Summary:

36ms : ConfigTest

Ураа!! Можно перестать писать код и написать ещё тесты! :)

Выводы

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

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

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

Естественно, изолированное тестирование не оставляет без работы тестеров-людей. Но позволяет им сосредоточиться на тестировании именно того, что нельзя протестировать автоматически.

Материалы для изучения

Прекрасное видео про tdd в руби и просто прекрасное видео Документация и примеры AsUnit

14.04.2009 firtree_right Фильтрация rss-потоков с помощью Sinatra и HTTParty

Задача

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

Надо: собрать воедино несколько единообразных rss-потоков, отфильтровав только нужное, и выдать единый rss-поток.

Для удобства предположим, что потоки имеют одинаковый формат — atom. Адреса нужных нам потоков будут находиться в текстовом файле, разделенные переносом строки. Так же как и необходимые нам ключевые слова. Так же допустим, что наличие ключевых слов будем отслеживать в заголовках.

Ресурсы

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

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

sudo gem i sinatra
sudo gem i httparty

Сбор и фильтрация

Создадим библиотечный файл feed_fetcher.rb:

require 'rubygems'
require 'httparty'

class FeedFetcher
  include HTTParty
  format :xml # позволяет получть результат сразу расфасованный
              # в Hash

  def self.get_items
    urls = nil # будет массив адресов
    titles = nil # будет массив нужных частей заголовков
    items = [] # будет массив записей
    File.open("path_to_feed_urls_file") do |f|
      urls = f.readlines.each(&:strip!)
    end
    File.open("path_to_titles_file") do |f|
      titles = f.readlines.each(&:strip!)
    end

    # составим единое регулярное выражение для фильтрации
    retitles = Regexp.union(titles.reject(&:empty?).map { |t| %r{\b#{Regexp.escape(t)}\b}i })

    # соберём записи со всех адресов в единый масив
    urls.each do |u|
      items += get(u)["rss"]["channel"]["item"] unless u.empty?
    end

    # отфильтруем по регулярному выражению и упорядочим по дате
    items.select { |i| i["title"] =~ retitles }.sort do |x, y|
      DateTime.parse(y["pubDate"]) <=> DateTime.parse(x["pubDate"])
    end
  end
end

Выдача результата

Результат будем так же выдавать в формате atom, поэтому нам понадобится builder, который, например, входит в состав active_support. Но можно установить его и отдельно.

Файл feed_filter.rb:

require 'rubygems'
require 'sinatra'
require 'active_support'
require 'feed_fetcher.rb'

get '/' do
  content_type 'application/xml', :charset => 'utf-8'
  @items = FeedFetcher.get_items
  builder :index
end

По-умолчанию Sinatra хранит шаблоны в папке views. Файл views/index.builder:

xml.instruct!
xml.rss "version" => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
  xml.channel do
    xml.title "My Filtered Feed"
    xml.link "http://lonelyelk.com"
    xml.pubDate CGI::rfc1123_date Time.parse(@items.first["pubDate"]) if @items.any?
    xml.description "Some description"
    @items.each do |item|
      xml.item do
        item.each_pair do |key, value|
          xml.tag!(key, value)
        end
      end
    end
  end
end

Запуск приложения с помощью passenger

Для запуска приложения мы будем использовать Passenger, который поддерживает не только rails, но и rack. Для этого нам понадобится создать папку public и указать к ней путь.

В установках виртуального сервера для apache:

<VirtualHost *:80>
  ServerAdmin webmaster@mydomain.ru
  ServerName feedfilter.mydomain.ru
  DocumentRoot /path/to/feed_filter/public
  ...
</VirtualHost>

А в папке приложения нужно создать файл config.ru:

require 'rubygems'
require 'sinatra'

Sinatra::Application.set(:run, false)
Sinatra::Application.set(:environment, ENV['RACK_ENV'])

require 'feed_filter'
run Sinatra::Application

Вот и всё. Естественно, ещё следует написать тесты. Так же для публикации можно использовать capistrano. Но это, я думаю, всем под силу.

Материалы для изучения

Первое знакомство с фреймворком Sinatra

11.03.2009 firtree_right Использование GEdit для разработки на Ruby и Flex

Сегодня я хочу поделиться своими ресурсами для разработки. В частности, хочу рассказать про IDE. Основную часть рабочего времени я провожу под кубунту. После долгих метаний и проб я остановился на GEdit, как на универсальном средстве разработки.

Установка

Для работы нам понадобятся плагины для GEdit. Они содержат основной набор вкусностей: code snippets, file browser pane, terminal pane и тому подобное. Поэтому устанавливаем:

sudo apt-get install gedit gedit-plugins

Дополнительные файлы, которые понадобятся, находятся в теле поста.

Ruby и Ruby on Rails

Подсветка синтаксиса и сниппеты для ruby уже поставляются из коробки. Единственное, если вы откроете файл со спецификацией подсветки синтаксиса для ruby, то обнаружите там пометку FIXME, касающуюся подсветки интерполяции внутри строки. Это связано с особенностями обработки правил подсветки самим редактором. Эти особенности удалось обойти, присвоив этому правилу измененный цвет фона.

Подсветку синтаксиса Ruby положить в /usr/share/gtksourceview-2.0/language-specs/ Для корректной работы подсветки, потребуется определить стиль для ruby:interpolation. Я использую тему darkmate, в которой определил необходимые дополнительные цвета на свой вкус. Положить следует в /usr/share/gtksourceview-2.0/styles/, а затем выбрать эту тему в установках редактора.

Язык для описания подсветки синтаксиса достаточно прост. Кроме всего прочего он позволяет ссылаться из правил одного языка на правила другого. Что, собственно, нам и понадобится для подсветки языка темплейтов erb. Ведь erb — это по сути html со вставками ruby. Теперь, когда у нас есть оба описания, берём описание для html и вставляем в него ссылку на ruby.

Так же следует определить mime-type для erb. Правила подсветки erb предполагают, что в системе определен mime-type text/erb. В четвертом KDE описать свои типы файлов можно в System->System Settings->Advanced->File Associacions

Мой файл использует цвет erb:background, который так же определен в файле с темой.

Подсмотрев code snippets для других языков, а так же в процессе использования, вполне можно самостоятельно написать пару-тройку полезных выражений.

Flash и Flex

Для работы с flex нам понадобится flex sdk, а так же отладочная версия standalone flash player. Я уж не буду вдаваться в подробности, как там написать компилирующий или запускающий скрипт. А выложу сразу подсветку синтаксиса.

Если определить тип (mime-type) text/x-shockwave, то можно использовать правила для подсветки ActionScript3. Которые следует положить по адресу, указаному выше.

Так же, используя описанное выше знание, легко создать описание подсветки синтаксиса mxml, зная, что это xml, со вставками actionscript. Для корректной работы следует определить mime-type text/mxml.

Заключение

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

02.03.2009 firtree_right Пространства имён XML в ActionScript 3

В последнее время достаточно плотно сталкивался с обработкой XML в ActionScript 3. В третьей версии ActionScript для обработки XML реализована спецификация ECMAScript for XML (E4X). Это значительно упрощает работу. Обращение к дочерним элементам происходит так же, как обращение к свойствам объекта. Об этом я и хочу рассказать в этот раз, взяв для примера что-то более сложное, чем в стандартный примерах.

var xml:XML =
    <robot xmlns="http://robots.org/2069/body/"
              xmlns:color="http://robots.org/2075/bodycolor"
              xmlns:extra="http://extraparts.org/2080/xml"
              xmlns:uni="http://universal.org/2010/robots/xml">
    <body>
      <head>
        <brain uni:id="abc123456" />
        <brain uni:id="abc987654" />
        <casing color:rgb="cfa499" />
      </head>
      <extra:additional name="mono">
        <extra:arm>
          <extra:claw extra:size="12" color:pantone="134f" />
        </extra:arm>
      </extra:additional>
    </body>
    </robot>;

То есть в данном примере присутствует четыре спецификации, которые описывают различные (или возможные) элементы или атрибуты XML. Что же будет, если мы попробуем что-нибудь поискать:

trace(xml.body.length()); // 0

Это произошло потому, что сам XML имеет своё пространство имён (namespace). Поэтому можно сделать так:

trace(xml.body.length()); // 0
var mainNS:Namespace = new Namespace("http://robots.org/2069/body/");
trace(xml.mainNS::body.length()); // 1
default xml namespace = mainNS;
trace(xml.body.length()); // 1

Остальные пространства имён:

var colorNS:Namespace = new Namespace("color", "http://robots.org/2075/bodycolor");
var extraNS:Namespace = new Namespace("extra", "http://extraparts.org/2080/xml");
var uniNS:Namespace = new Namespace("uni", "http://universal.org/2010/robots/xml");

Объект, возвращаемый при выборке имеет тип XMLList. Но не смотря на это, дальнейшую выборку можно продолжать. Так же для примера рассмотрим обращение к атрибутам:

trace(xml.body.head.casing.@colorNS::rgb); // cfa499

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

trace(xml.body[0].head[0].casing[0].@colorNS::rgb[0]); // cfa499

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

trace(xml..mainNS::brain.(@uniNS::id.indexOf("abc") >= 0).@uniNS::id.toXMLString());
            //abc123456
            //abc987654

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

trace(xml..mainNS::brain.(@uniNS::id.indexOf("abc") >= 0)[1].@uniNS::id);
            //abc987654

Естественно, что XMLList может быть использован для образования цикла с помощью for .. in.

Ещё один способ обращаться к элементам и атрибутам — это с помощью методов класса XML и объекта QName. Это прекрасный способ, если не нужно «хардкодить» имена элементов. Ещё это может пригодиться, если элементы xml имеют имена, совпадающие со служебными или ключевыми словами ActionScript.

var claw:QName = new QName(extraNS, "claw");
var pantone:QName = new QName(colorNS, "pantone");
trace(xml.descendants(claw)[0].attribute(pantone)); // 134f

Материалы для изучения

Документация класса XML со ссылками Руководство пользователя на тему XML в ActionScript 3 Для хардкорных читателей