LE Blog

Инженер с поэтической душой

02.09.2009 firtree_right Автоподстановка задач 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 и фильтровать результат.

Следует знать два момента:

  1. Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
  2. В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.

Элементарная реализация выглядит следующим образом:

#!/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 firtree_right Приёмы работы с массивами и блоками в качестве аргументов в руби

Введение

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

Массивы

О том, как принять неограниченное число аргументов, знают все:

$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("усы", "хвост")  # => Вот аргументы: ["усы", "хвост"]

Надеюсь, что было полезно.

Материалы для самостоятельного изучения

  1. Аргументы в руби
  2. Ещё про аргументы в руби

19.08.2009 firtree_right Бинарные операции для работы с цветом в 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 firtree_right Упрощение работы с путями в руби

Введение

Недавно наткнулся на интересное решение объединения путей в питоне. Вспомнил, как недавно приходилось довольно много работать с файлами и путями. И решил подбить всё в одну библиотеку (конечно же, беззастенчиво позаимствовав такой способ объединения путей). В статье более подробно хочу остановиться на пути к текущему файлу.

Текущий путь

Довольно часто встречающаяся конструкция, после объединения путей, в моём случае — это:

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 firtree_right Управление фотокамерой с помощью руби

Введение

По заказу 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.2009 firtree_right Ruby 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, получится вполне себе демон :)

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

Есть, конечно, ещё недостатки. Например:

  1. Если ошибка возникает в main_loop, то канал вывода уже закрыт. А если не закрывать канал, то метод IO#read не позволит нам выйти из метода start. Что делать?
  2. Если нужно периодически общаться с демоном общирными объемами информации, что делать? (я в своей задаче использовал TCPSocket, но это, ведь, не панацея)
  3. Хорошо бы хранить pid в pid-файле на случай нашествия зомби. И, соответственно, обрабатывать возникающие зомбо-проблемы.

Но это я оставлю на самостоятельное решение пытливым читателям.

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

16.05.2009 firtree_right Эволюция алгоритма замены в строке 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));

Это ли не чудесно?

Выводы

  1. Решайте задачи.
  2. Решив (или не решив), записывайте то, что получилось, покажите кому-нибудь. Это позволит выкинуть решение из головы.
  3. Если есть решение лучше, то оно придет на освободившееся место.

07.05.2009 firtree_right ActionScript: асинхронная замена выражений в строке

Введение

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

Как быть?

30.04.2009 firtree_right Тестирование Paperclip с помощью Factory Girl

Введение

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

Задача

Протестировать вновь изученный инструмент Paperclip с помощью вновь изученного инструмента Factory Girl.

Решение

Допустим, прикрепленный файл называется photo. То есть в модели написано следующее:

class Something < ActiveRecord::Base
  has_attached_file :photo
end

Кладем файл с фото, например, в папку с fixtures. Тогда файл test/factories.rb (или spec/factories.rb) будет содержать:

Factory.define :something do |smth|
  smth.photo File.new("path_to_file_in_fixtures", "rb")
end

И всё! Теперь в тестах, когда нужно создать объект пишем:

Factory.create(:something)
 # или
Factory.build(:something)

А в тестах контроллера пишем:

Factory.attributes_for(:something)

Выводы

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

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

Проекты в первом и втором случае значительно отличаются по объему. :)

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

Множество прекрасных инструментов от ThoughtBot