10.09.2009Создание init-скриптов с помощью руби
Предисловие
Сегодня у меня кроме всего прочего появился повод похвастаться! :) Когда вы смотрите широко обсуждаемые Яндекс-Панорамы, знайте, что их производством в составе моей любимой компании neq4 занимался и я.
В частности, флэш-просмотрщик, который кроме панорам и улиц показывает ещё и номера домов в соответствии с информацией, полученной от сервера, был разработан мной и передан на поддержку Яндексу. Когда происходит полная передача проекта с исходниками, не совсем справедливо забирать всю славу себе. Но следует отметить, что мои исходники исчерпывающе документированы. И разработчики обратились ко мне с вопросами лишь два раза. Но, может быть, они всё уже переписали с нуля, и я зря тешу своё самолюбие :)
Так же я, безусловно, планирую раскрывать некоторые секреты. Кстати, практически все записи в этом блоге основаны на работе над этим проектом. Например, программа, которая управляет камерами во время движения, а так же получает данные от приборов, была так же написана мной. И теперь вы знаете, что написана она на руби. :) За исключением библиотеки для работы с камерами, о которой я уже писал.
Это и подводит нас вплотную к теме сегодняшней записи.
Введение
Безусловно, руби — лучший из языков. Для меня выбор в его пользу обусловлен прежде всего высокой скоростью разработки. Поэтому при возникновении какой-то задачи, особенно если её нужно сделать быстро, я пробую решить её на руби. В прошлый раз я рассказал об автоподстановке. А сегодня — создание init-скриптов
Скрипты эти располагаются в папке /etc/init.d и запускаются при старте в определённых условиях. Они должны принимать параметры start, stop, restart и status. Как правило, они служат для запуска и остановки служб.
Решение
Написать программу, которая принимает текстовый ключ на входе, не составляет труда (/etc/init.d/my_script):
#!/usr/bin/env ruby
APP_NAME = "my_script"
case ARGV.first
when "status"
if...
...
exit 0
elsif ...
...
exit 1
else
exit ...
end
when "start"
...
when "stop"
...
when "restart"
...
else
puts "Usage: #{APP_NAME} {start|stop|restart|status}"
end
Круто, но до настоящего init-скрипта не дотягивает. Когда система проходит свой цикл жизни от запуска до перезагрузки или выключения, она проходит через шесть стадий. Нужно указать, в каких стадиях наш скрипт следует запустить с параметром start, а когда — с параметром stop. Для этого используется спецификация LSB (Linux Standard Base). Вот так:
#!/usr/bin/env ruby
# Start/stop my daemon
#
### BEGIN INIT INFO
# Provides: my_script
# Required-Start: $all
# Required-Stop: $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Initializes my daemon at boot time
# Description: In a Galaxy far, far away...
### END INIT INFO
APP_NAME = "my_script"
case ARGV.first
.....
Теперь лежащий в /etc/init.d скрипт нужно подрегулировать и зарегистрировать:
sudo chown root:root /etc/init.d/my_script
sudo chmod 755 /etc/init.d/my_script
sudo update-rc.d my_script defaults
Таким образом, вовсе необязательно изучать bash-скрипт. Вот, кстати, оболочка-альтернатива всяким башам, использующая синтаксис руби. Для таких, как я. :)
Для самостоятельного изучения
02.09.2009Автоподстановка задач rake в терминале с помощью ruby
Введение
Периодически натыкаюсь на хвалебные отзывы о zsh и о совершенной необходимости перехода с bash на него всему прогрессивному человечеству. В качестве демонстрации удобства демонстрируют лёгкость создания скриптов автоподстановки, например для задач rake. Неужели из-за этого следует сменить оболочку терминала?
Задача
Найти решение для автоподстановки задач rake с помощью bash несложно. Для этого используется complete. Огромное число примеров можно посмотреть у себя же в системе в /etc/bash_completion.
При ближайшем рассмотрении выяснилось, что complete может использовать не только функцию, но и команду. То есть отдельный запускаемый скрипт, который может быть написан и на руби в том числе.
Можно ли из этого извлечь пользу?
Решение
Итак, чтобы подключить наш скрипт, нужно добавить в файл ~/.bashrc такую строчку.
complete -C ~/.bash/autocomplete/rake_complete -o default rake
Для начала положим по адресу ~/.bash/autocomplete/rake_complete элементарную реализацию автоподстановки, которая будет запускать rake и фильтровать результат.
Следует знать два момента:
- Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
- В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.
Элементарная реализация выглядит следующим образом:
#!/usr/bin/env ruby
class RakeComplete
def initialize(cmd)
@command = cmd[ /\s(.+)$/, 1 ] || ""
end
def search
`rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }.select{ |cmd| cmd[0, @command.length] == @command }.map{ |cmd| cmd.gsub(cmd_before_column, "") }
end
private
def cmd_before_column
@command[ /.+\:/ ] || ""
end
end
puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0
Вроде бы всё понятно: из результата вывода rake -T , отбросив паразитную первую строчку, вытаскиваем только названия задач, подбираем те, начало который совпадает с введенным названием задачи, выводим массив, предварительно удалив у элементов введённую до двоеточия часть. Но уж больно медленно работает. Конечно же, встает вопрос о кешировании.
Кеширование
Приведу сразу результат:
#!/usr/bin/env ruby
class RakeComplete
CACHE_NAME = ".rake_complete~"
def initialize(cmd)
@command = cmd[ /\s(.+)$/, 1 ] || ""
end
def search
exit 0 if rake_file.nil?
selected_tasks.map do |cmd|
cmd.gsub(cmd_before_column, "")
end
end
private
def cmd_before_column
@command[ /.+\:/ ] || ""
end
def rake_file
["Rakefile", "Rakefile.rb", "rakefile", "rakefile.rb"].detect do |name|
File.file?(File.join(Dir.pwd, name))
end
end
def cache_file
File.join(Dir.pwd, CACHE_NAME)
end
def generate_tasks
tasks = `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }
File.open(cache_file, "w") do |f|
tasks.each do |task|
f.puts task
end
end
tasks
end
def cached_tasks
File.read(cache_file).split("\n")
end
def cache_valid?
File.exist?(cache_file) && (File.mtime(cache_file) >= File.mtime(rake_file))
end
def tasks
cache_valid? ? cached_tasks : generate_tasks
end
def selected_tasks
tasks.select do |cmd|
cmd[0, @command.length] == @command
end
end
end
puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0
Для самостоятельного изучения
Думаю, теперь не составит труда написать подобное для задач capistrano.
27.08.2009Приёмы работы с массивами и блоками в качестве аргументов в руби
Введение
Главная причина, по которой я испытываю нежные чувства к руби — это гибкость синтаксиса. Сегодня я хочу рассказать об особенностях передачи блоков и массивов в качестве параметров. Базовые вещи можно прочитать по ссылкам в последнем параграфе.
Массивы
О том, как принять неограниченное число аргументов, знают все:
$KCODE = "utf-8"
def my_args(*args)
puts args.inspect
end
my_args(1, 2, "собачка", "котик") # => [1, 2, "собачка", "котик"]
Но что, если сами объекты, которые необходимо передать в качестве аргументов, уже находятся в массиве по какой-то причине?
arr = ["собачка", "котик", "ёжик", "медвежонок"]
selection = arr.select{ |a| a.chars.to_a.length > 5 }
my_args(selection) # => [["собачка", "медвежонок"]]
Внезапно массив (args) стал двумерным, что неудивительно, т.к. метод интерпретировал переданный ему массив как первый аргумент типа Array. В данном случае, чтобы передать массив как список аргументов, нужно использовать ту же звезду:
my_args(*selection) # => ["собачка", "медвежонок"]
Ура!
Кстати, звезду можно использовать и при получении значения от функции. Например, в таком случае:
def get_values
["красный", "зелёный", "синий"]
end
r, g, b = get_values
puts r.inspect # => "красный"
puts g.inspect # => "зелёный"
puts b.inspect # => "синий"
r, gb = get_values
puts r.inspect # => "красный"
puts gb.inspect # => "зелёный"
r, *gb = get_values
puts r.inspect # => "красный"
puts gb.inspect # => ["зелёный", "синий"]
По-моему, здорово!
Блоки
Бывают случаи, когда блок нужно сохранить на будущее, чтобы потом его использовать. Тогда можно указать его в списке параметров метода, что, конечно же, не делает его обязательным само по себе.
class Button
def on_click(&block)
if block_given?
@stored_block = block
else
puts "Блока нет, но жизнь продолжается"
end
end
def click(*args)
@stored_block.call(*args)
end
end
my_btn = Button.new
my_btn.on_click # => Блока нет, но жизнь продолжается
my_btn.on_click do |*args|
puts "Произошёл клик!"
end
my_btn.click # => Произошёл клик!
Теперь предположим, опять же, что по каким-то причинам блок у нас уже есть. Он определён до вызова метода.
handler = lambda{ |*args| puts "Вот аргументы: #{args.inspect}" }
my_btn.on_click(handler) # => wrong number of arguments (1 for 0) (ArgumentError)
То есть наш блок превратился в аргумент типа Proc. А метод совсем этого не ожидал. Можно модифицировать сам метод, чтобы он выдерживал подобные условия, но не для этого я всё это пишу :) Если мы чему-нибудь научились в предыдущей главке, то решение по аналогии приходит само:
my_btn.on_click(&handler)
my_btn.click("усы", "хвост") # => Вот аргументы: ["усы", "хвост"]
Надеюсь, что было полезно.
Материалы для самостоятельного изучения
19.08.2009Бинарные операции для работы с цветом в ActionScript
Введение
Закончился период отпусков и авральных возвращений к работе. Акклиматизация пройдена успешно, и настала пора возобновить ведение блога.
Задача
Возможно, это уже давно всем известно, но тем не менее, мне бы хотелось об этом написать. В ActionScript существует как минимум два способа обращаться с цветом. Если мы работаем с BitmapData, то используется ARGB, то есть прозрачность и цвет задаются одним числом типа uint. Кстати, 0xffffffff — непрозрачный белый цвет — максимальное число этого типа.
Когда же мы работаем с, например, Graphics, то цвет и прозрачность задаются отдельно друг от друга. Причём прозрачность в диапазоне от 0 до 1. Хотелось бы быстро раскладывать ARGB на части и снова собирать.
Решение
В одну сторону:
var argb:uint = 0xccabcdef;
var a:Number = (argb >>> 24) / 255.0;
var rgb:uint = argb & 0xffffff;
Аналогично, кстати, можно выделить и каналы по отдельности:
var red:uint =(argb & 0xff0000) >>> 16;
var green:uint =(argb & 0xff00) >>> 8;
var blue:uint =(argb & 0xff);
Оператор >>> вместо >> используется чтобы избежать проблем со знаком.
В обратную сторону, соответственно:
argb = (uint(a*255) << 24) | rgb;
argb = (uint(a*255) << 24) | (red << 16) | (green << 8) | blue;
Всё совершенно логично. Чтобы разделить, используется бинарное и, а чтобы объединить — бинарное или.
Для возобновления трансляции, как мне кажется, этого материала достаточно.
06.07.2009Упрощение работы с путями в руби
Введение
Недавно наткнулся на интересное решение объединения путей в питоне. Вспомнил, как недавно приходилось довольно много работать с файлами и путями. И решил подбить всё в одну библиотеку (конечно же, беззастенчиво позаимствовав такой способ объединения путей). В статье более подробно хочу остановиться на пути к текущему файлу.
Текущий путь
Довольно часто встречающаяся конструкция, после объединения путей, в моём случае — это:
File.dirname(__FILE__)
Если делать класс для работы с путями файлов, то он должен наследоваться от String, чтобы можно было сделать:
File.open(filepath)
А также должен уметь определять путь файла, в котором инициализируется или вызывается.
Начнём, конечно же, с тестов. Кроме всего прочего, я предпочитаю оперировать с развёрнутыми путями, т.к. если загружать библиотеку из разных мест, то пути могут не опознаваться как одинаковые, и интерпретатор ругается, например, на повторную инициализацию констант. Итак, тест с использованием RSpec:
describe FilePath do
it "should show correct current path" do
FilePath.current.should == File.expand_path(__FILE__)
end
end
Если использовать FILE внутри класса FilePath, то там окажется путь к файлу, в котором определяется класс.
Использование $0 так же не подходит, т.к. выдает путь только главного файла. В случае запуска теста $0 будет где-то в библиотеках.
Нам бы пригодилось что-нибудь вида:
eval("__FILE__", binding_of_caller)
Но binding_of_caller работало с помощью бага, который уже давно исправлен, а Binding.of_caller выглядит очень громоздко (можно там кликнуть на ссылочку Source). Мало того, что он ломает trace_func, так он требует, чтобы метод, в котором он используется, вызывали только внутри метода.
Можно ещё передавать внутрь метода пустой Proc, вытаскивая его binding, но требовать это от человека, использующего библиотеку для упрощения жизни, как-то нелепо.
Решение
На помощь спешит Kernel.caller, знакомый нам по трейсам ошибок. Если разобраться, как он работает, то решение приходит сразу:
caller(1)[0].split(":")[0]
Остальное можно посмотреть в исходниках file_path@github. Когда соберётся джем-библиотека, я обновлю инструкции и опубликую rdoc. Вдруг кому пригодится!
16.06.2009Управление фотокамерой с помощью руби
Введение
По заказу neq4 в рамках одного из проектов сделал библиотеку для работы с камерами. Точнее, это оболочка вокруг библиотеки на C (libgphoto2) для использования в руби.
Пришлось, конечно, изучать тему расширения руби с помощью C, но сегодня я хотел написать не об этом, а наоборот о том, как пользоваться библиотекой на руби. Что называется, программировал на C, чтобы другим не пришлось :)
Оболочка собрана в виде gem и работает на Linux и Mac OS. То есть везде, где работает gphoto2.
Установка
Для того, чтобы собрать gem, кроме средств сборки и версии руби для разработчиков, нужна установденная версия libgphoto2 для разработчиков:
sudo apt-get install ruby-dev build-essential libgphoto2-dev
Теперь можно ставить gem:
sudo gem i gphoto4ruby
Инициализация и настройки камеры
О том, какие камеры совместимы, и как их подключать для работы достаточно много написано у создателей gphoto2. Скажу лишь, что те камеры, с которыми я работал, для подключения нужно было переводить в PTP-mode. После подключения:
require "rubygems"
require "gphoto4ruby"
c = GPhoto2::Camera.new
Чтобы получить текущее значение настройки камеры и управлять состоянием конкретной настройки, нужно проделать примерно следующее.
cfg = c.config #{"capturemode" => "Single Shot",
# "exptime" => "0.125",
# "f-number" => "f/3.2", ...}
cfg.keys # возможные настройки
c["f-number", :all] # ["f/2.8", "f/3.2", "f/3.5", ...]
# возможные значения
c["f-number"] # текущее значение
c["f-number"] = "f/8" # вуаля! настройка на камере изменена
Не все настройки, имеющие несколько возможных значений, можно переключить с компьютера. Нельзя переключить то, для чего нужно что-то поворачивать или открывать на камере. Если вы что-то поменяли вручную на камере, то чтобы увидеть изменения параметра в объекте программы, нужно написать, например:
c["focusmode", :no_cache] # на Nikon D80 меняется переключателем
Съёмка
c.capture
c.capture.save.delete # сохранить снятую фотографию в текущую
# папку и удалить её с камеры
c.save :file => :last, :to_folder => "/home/sweet/home"
# сохранить последний из файлов в папку
c.save :file => c.files[3], :type => :preview, :new_name => "PREVIEW.JPG"
# загрузить превью фотографии под номером 4
# в списке файлов на карточке
При сохранении, если вы не делали съемку, могут быть тонкости связанные с тем, что нужно перейти в камере в нужную папку. Для этого есть ряд методов. Они хорошо описаны в документации по ссылке ниже.
Материалы для самостоятельного изучения
Библиотека libgphoto2 и инструмент для работы с камерами из командной строки gphoto2: есть FAQ. Документация оболочки gphoto4ruby: полный список классов и методов с примерами. Исходный код gphoto4ruby Расширение руби с помощью C
10.06.2009Ruby daemon, или как сделать демона на руби
Задача
Иногда в процессе работы с разными сторонними библиотеками, которые содержат тяжелые блокирующие методы, не обойтись обычным Thread. В таком случае на помощь приходят демоны :) Вот несколько приемов, которые я освоил в работе с нимим.
Базовый механизм
Запустить демона можно с помощью метода Kernel.fork, передав ему блок. Метод возвращает pid процесса, который можно записать в файл и просто использовать в дальнейшем.
pid = fork do
puts "from daemon"
exit!(1)
end
Определение статуса
Я не нашёл удобного способа определения методами руби, запущен ли процесс. Есть возможность фильтровать вывод ps ax, ища в нём pid процесса. Но есть метод Process.waitpid, который можно использовать хитрым образом. Так же для будущих задач, упакуем наш код в класс:
require 'timeout'
class Daemon
class << self
def start
@pid = fork do
puts "from daemon"
sleep 1
exit!(1)
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Сообщение об ошибках
Предположим, что у нашего демона есть некоторый процесс инициализации, и мы хотим знать, завершился ли он или произошла ошибка до того, как покинем метод Daemon.start.
Для передачи подобных сообщений хорошо подходит IO.pipe. Для двустороннего общения нужно создавать два канала, но нам хватит и одного:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
begin
dmn = new
@wr.write "ok"
@wr.close
sleep 1
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Остановка
У каждого демона (для того они обычно и создаются) есть циклическая часть. Нам хотелось бы запускать и останавливать процесс тогда, когда нам нужно. Если с запуском всё понятно, то для остановки потребуется ещё одна вещь. Ведь в тот момент, когда мы создали демона, он создает копии всех переменных и дальнейшее их изменение внутри и снаружи демона становится независимым. Ещё один способ общения с демоном — сигнал:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
@cnt = 0
end
def main_loop
@cnt += 1
puts "from daemon: running loop ##{@cnt}"
sleep 0.1
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
running = true
Signal.trap("TERM") do
running = false
end
begin
dmn = new
@wr.write "ok"
@wr.close
while running
dmn.main_loop
end
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def stop
unless @pid.nil?
Process.kill("TERM", @pid)
@pid = nil
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1
Daemon.stop
puts "running: #{Daemon.running?}"
Теперь, если наследовать от этого класса свой класс и переопределить методы initialize и main_loop, получится вполне себе демон :)
Для самостоятельного изучения
Есть, конечно, ещё недостатки. Например:
- Если ошибка возникает в main_loop, то канал вывода уже закрыт. А если не закрывать канал, то метод IO#read не позволит нам выйти из метода start. Что делать?
- Если нужно периодически общаться с демоном общирными объемами информации, что делать? (я в своей задаче использовал TCPSocket, но это, ведь, не панацея)
- Хорошо бы хранить pid в pid-файле на случай нашествия зомби. И, соответственно, обрабатывать возникающие зомбо-проблемы.
Но это я оставлю на самостоятельное решение пытливым читателям.
28.05.2009Тестирование в ActionScript с помощью AsUnit
Введение
Сейчас занимаюсь тестированием и отладкой достаточно большого проекта на флексе. При каждом обновлении продукт проходит длительный и подробный этап тестирования. Как нельзя более актуальной становится проблема «исправив одну ошибку не сделать новых».
Один из инструментов, который мог бы мне помочь, если бы я его использовал сразу — это тестирование, а точнее Unit Tests (изолированное тестирование).
Понятно, что ActionScript не самый удобный язык для изолированного тестирования. Сложно сделать связанные объекты независимыми друг от друга, чтобы тестировать их по отдельности. Да и в целом продукты на Flash и Flex имеют большую интерфейсную составляющую. И тестировать зачастую нужно впечатления пользователя. Но тем не менее, покрытие тестами, пусть и небольшое, — это удобно.
Задача
Протестировать объект, который получает данные из xml и выдает их в качестве собственных параметров. Попробуем сделать это в лучших традициях TDD
Ресурсы
Нам понадобится AsUnit — прекрасная библиотека с открытыми исходниками для изолированного тестирования (unit testing).
Так же используем лучшие традиции TDD:
- Никакого кода, пока нет провалившегося теста.
- Тест пишется до тех пор, пока он не начнет проваливаться.
- Кода нужно писать ровно столько, чтобы проваливающийся тест прошёл.
Для решения будем использовать 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
16.05.2009Эволюция алгоритма замены в строке ActionScript
В последнее время моя работа в текущем проекте заключается в отладке, оптимизации и отлове багов. Поэтому в самой работе практически не встречается того, о чём можно было бы здесь написать. Но на помощь пришла гугл-группа ruFlash и комьюнити молодых программистов. :)
Задача
Один из участников попросил составить выражение для удаления из текста ссылок с определенным текстом внутри целиком. Например, в выражении:
var str:String = '<a href="somelink">_some text_</a> ';
str += 'More text! ';
str += '<a href="anotherlink">**remove me**</a> ';
str += '<a href="yetanotherlink"><s>another text</s></a>';
Нужно удалить целиком ссылку, содержащую фразу remove me.
Понятно, что первое приходящее в голову выражение /<a.+?remove me.*?</a>/ захватит две первые ссылки. И «жадность» не поможет, т.к. поиск осуществляется по порядку, и, найдя первый <a, выражение не остановится до самого remove me.
Решение номер один
Поскольку к концу недели отладки и пересмотра одного и того же кода голова моя не была готова что-то изобретать, я последовал пути, предлагавшемуся одним из ответивших, слегка его доделав:
var re0:RegExp = /<a[^>]+>[^a]*remove me.*?<\/a>/g;
trace(str.replace(re0, "!removed!"));
Недостаток его очевиден. Хотя для приведенного примера он работает, но всё-таки, может и отказать, если встретит a между > и remove me. Например:
str += '<a href="anotherlink">eh! ah? **remove me**</a> ';
Решение номер два (рекурсивное)
Поскольку к этому моменту мозг ещё не расслабился окончательно и не готов был отказаться от выбранного способа думать о задаче, второе решение, пришедшее сразу за первым, было значительно сложнее.
Оно использовало возможность подсовывать функцию в качестве аргумента. Вот оно:
var re1:RegExp = /<a(.+?remove me.*?<\/a>)/g;
var replacer1:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("<a") > 0) {
return "<a" + s.replace(re1, replacer1);
} else {
return "!removed!";
}
}
trace(str.replace(re1, replacer1));
Здесь речь идёт о том, чтобы в группе (см. скобки), следующей после <a проверять наличие ещё одного <a. И в случае его наличия запускать ту же процедуру замены, но уже на группе.
Довольный собой, я запостил своё решение в ruFlash и поехал домой. По дороге домой мозг окончательно расслабился, и я смог увидеть задачу в отрыве от способа думать, который я выбрал изначально. И мне пришло
Решение номер три
Зачем городить рекурсию, когда можно просто перебирать все ссылки и заменять (удалять) только те, что нужно?
var re2:RegExp = /<a[^>]+>(.+?)<\/a>/g;
var replacer2:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("remove me") > 0) {
return "!removed!";
} else {
return arguments[0];
}
}
trace(str.replace(re2, replacer2));
Это ли не чудесно?
Выводы
- Решайте задачи.
- Решив (или не решив), записывайте то, что получилось, покажите кому-нибудь. Это позволит выкинуть решение из головы.
- Если есть решение лучше, то оно придет на освободившееся место.
07.05.2009ActionScript: асинхронная замена выражений в строке
Введение
Я снова вернулся к работе над флэшовым проектом. Поэтому немного об ActionScript. Описанная здесь задача сейчас мне не кажется такой сложной, какой она казалась, когда я впервые с ней столкнулся. Но тем не менее.
Задача
Имеется строка, содержащая разметку для замены её составляющих. Одна из разметок: #[some_url] должна быть заменена содержимым этого самого some_url. Для замены с помощью регулярных выражений в ActionScript 3 существует функция String#replace. Но всё, что связано с загрузкой из внешних источников, создает асинхронность. А любая попытка остановить код, сделать паузу, приводит к огромной потере производительности и ошибкам, которые генерирует плеер, когда долго не может завершить вызов. «Как быть?» — спросит меня пытливый читатель.
Решение
Решение состоит в том, чтобы заменять все вхождения ключевого выражения по-очереди и когда всё заменено, создавать событие.
Приведу основную часть. Файл AsyncStringReplaceExample.as
package {
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
public class AsyncStringReplaceExample {
public static const RE_URL:RegExp = /#\[([^\]]+)\]/g;
private var _str:String;
private var _currentExpr:String;
private var _ldr:URLLoader;
private var _loaded:Boolean;
public function AsyncStringReplaceExample(str:String) {
_str = str;
_ldr = new URLLoader();
_ldr.addEventListener(Event.COMPLETE, ldrCompleteHandler);
_loaded = false;
}
public function replace():Boolean {
_loaded = true;
_str = _str.replace(RE_URL, replaceURL);
return _loaded;
}
public function get string():String {
return _str;
}
private function replaceURL():String {
if (!_currentExpr) {
_loaded = false;
_currentExpr = arguments[0];
_ldr.load(new URLRequest(arguments[1]));
}
return arguments[0];
}
private function ldrCompleteHandler(evt:Event):void {
_str = _str.replace(_currentExpr, evt.target.data);
_currentExpr = null;
if (replace()) {
trace(_str); // здесь желанное событие
}
}
}
}
Теперь остается только использовать написанное нами богатство:
var str:String = "строка для примера\n";
str += "добавим: #[http://some.url/file.txt] или";
str += "ещё добавим: #[http://another.url/another/file.txt]! хватит?";
var asyncString:AsyncStringReplaceExample = new AsyncStringReplaceExample(str);
if (asyncString.replace()) {
trace(asyncString.string); //не нужно ничего заменять
} else {
asyncString.addEventListener(......)
}
...
// внутри обрабочика событий
trace(asyncString.string);
Выводы
Сразу видно, что последнее время я забросил ActionScript и занимался больше Ruby. Потому что подсветка синтаксиса в Ruby красивее. Но это не страшно. :)
Упражнения
Мне было бы интересно написать функцию, которая могла бы работать сколь угодно долго. Например, рекурсивную, или долгую по любой другой причине. Но ActionScript принудительно завершает процессы, которые долго не подают признаков жизни.
Как быть?