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;
Всё совершенно логично. Чтобы разделить, используется бинарное и, а чтобы объединить — бинарное или.
Для возобновления трансляции, как мне кажется, этого материала достаточно.
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));
Это ли не чудесно?
Выводы
- Решайте задачи.
- Решив (или не решив), записывайте то, что получилось, покажите кому-нибудь. Это позволит выкинуть решение из головы.
- Если есть решение лучше, то оно придет на освободившееся место.