вторник, марта 28, 2006

Юнит-тесты рулят

Сегодня убедился, что юнит-тесты и в AS2 - вещь весьма полезная. Написал класс, к которому параллельно писал тесты. Не по методике TDD, но все же... И все тесты работают, а в приложении - нет. И тогда стал я писать тест, который показал бы мне таки красную полосу. И мне это удалось! После этого баг был локализован и выловлен. Баг оказался пустяковым - подумаешь по невнимательности в условной конструкции вместо == написал = :) Уж такого я на себя подумать не мог, да и со стороны все выглядело прилично. А потому стал думать а плясках с бубном, о собственной профнепригодности, о влиянии сложности приложения на нестабильно работающий класс, породившей баг, отладить который ввиду сложности приложения не представляется возможным. Не отчаивайтесь в моей ситуации - пишите тест. На кошках вылавливается легче.

Ну и под это дело как раз и хочу продемонстрировать пример. Но сначала, как известно, рабочая среда. Первым делом вам нужна библиотека AsUnit. Вам нужно скачать Framework. zip-архив содержит в себе три папки: as2, as25 и as3. Смысл примерно таков, что первая папка содержит все изначально написанную под AS2 библиотеку модульного тестирования. Пришло время AS3 и авторы переписали библиотеку путем портирования JUnit'а. А следом появилась бибилиотека AsUnit 2.5 - это портированная на AS2 библиотека версии AS3 :) В результате рекомендуется использовать 2.5. По сравнению с уже давно существующей AsUnit 2.0 там есть ряд преимуществ: в качестве тестов (единицы тестирования) считаются сами тестовые методы, а не assert'ы в методах, добавлены также улучшения по запуску тестов.

Так вот AsUnit 2.5 мы и используем в нашем проекте. Также понадобится компилятор mtasc, Apache Ant (о нем мы уже писали, а посему по вопросам установки отсылаю к соотвествующей статье) и задача Ant as2ant (на данный момент мною используется версия 1.5, просто копируем as2anr.jar в папку lib каталога с установкой Ant). После указания переменной окружения Path к компилятору mtasc можно считать рабочую среду подготовленной.

Приступим к самой теме. В качестве нашего предмета я выбрал некий абстрактный органайзер. Пускай у нас есть время, событие и некий флаг, показывающий, высок ли приоритет события или низок. Пример к жизни особого отношения не имеет - просто пример. Поэтому я позволи себе всякие вольности в его формулировке. Например, мы будем считать, что если мы планируем на одно и то же время сделать две вещи, то превращаем это в одно событие, а описания их просто конкатенируем. Но если они имеют разный приоритет, то и события разные. То есть предположим, на 14.00 я запланировал событие "Попить", "Поесть" и "Позвонить жене". И считаю, что приоритет первых двух невысок, а последнего - высок. Соотвественно я первые два объединяю в одно "Попить - поесть" с низким приоритетом, а последнее так и идет отдельно. То есть не факт, что я все это выполню, но если я посмторю, что у меня запланировано на 14.00, то увижу, что это два события: "Попить - поесть" и "Позвонит жене". Соотвественно хранит это дело у меня класс Organizer. В процессе разработки (через тестирование; это тема будущих статей и поэтому динамику разработки не показываю - только результат) у меня появился дополнительный класс Event для описания события. Его я тестировать не стал (а зря!) взяв на себя смелость утверждать, что он простой и тестирования класса Organizer вполне достаточно. Прежде чем привести результат, вкратце расскажу об устройстве модульных тестов. Модульный тест состоит, собственно, из теста. Тест - это некий метод некого тестового класса, тестирующий один из аспектов тестируемого класса. В нашем случае класс OrganizerTest будет содержать тесты (тестовые методы) для тестирования класса Organizer.

Класс OrganizerTest представляет из себя тестовый случай (Test Case) и потому расширяет класс TestCase. Класс TestCase устроен просто: он содержит в себе тесты (методы, начинающиеся со слова test), которые выполняются независимо. Перед выполнением каждого теста вызывается метод setUp, выполняющий некие начальные необходимые установки (создание экземпляра тестируемого класса, например). После вызова тестового метода вызывается метод tearDown (удаляет последствия). Результат каждого теста складывается из утверждений (assertion), которые либо выполняются, либо нет, а также исключений, которые могут выбрасываться тестируемым классом.

Несколько Test Cases объединяются в тестовый набор (Test Suite), для которых есть специальный класс (TestSuite). В нашем случае это AllTests, который содержит всего один тестовый случай. В тестовые наборы объединяют затем чтобы прогонять множество тестов сразу. При прогоне тестового набора автоматически прогоняются все тестовые набры, в него входящие (по шаблону Composite), а также все тестовые методы входящих Test Cases (определяются автоматически по началу на test).

Также в нашем проекте присутствует TestsRunner, запускающий тесты. Он выдает в качестве результата либо зеленую полосу (все тесты прошли удачно), либо красную - не сработал хотя бы один тест (утверждение, либо было выкинуто исключение). Красная полоса - это не плохо. Красная полоса - наш друг. Зеленая полоса зачастую заставляет задуматься, как будет показано дальше. Поэтому цель написания тестов - добиться красной полосы. Если полоса зеленая, то либо вы протестировали все, либо чего-то не протестировали :)

Также упомянем про наш build.xml. Этот ant-файл содержит две публичных цели: build и build.and.run. Цель build - цель по умолчанию. Она запускается при наборе команды ant без параметров в каталоге с билд-файлом. Она просто формирует билд нашего теста в виде swf-файла tests.swf в папке build. Цель build.and.run формирует билд и запускает его (надо указать правильный путь к flash-плееру в файле local.properties). Запускается просто: ant build.and.run.

Итак, наш класс Organizer:


/**
* @author Constantiner (constantiner at gmail dot com)
* @version Mar 27, 2006
*/

import constantiner.Event;

class constantiner.Organizer
{
private var events:Array;

/**
* Конструктор.
*/
public function Organizer ()
{
events = new Array ();
}

public function addEvent (eventDate : Date, eventDescription : String, isHiPriority:Boolean) : Void
{
var anEvent:Event = new Event (eventDate, eventDescription, isHiPriority);
var sameEvent:Event = getTheSame (anEvent);
if (sameEvent == null)
{
events.push (anEvent);
}
else
{
sameEvent.concat (anEvent);
}
}

public function getEventsNumber() : Number
{
return events.length;
}

public function getDescription (eventDate:Date, isHiPriority:Boolean):String
{
var anEvent:Event = new Event (eventDate, null, isHiPriority);
var sameEvent:Event = getTheSame (anEvent);
return sameEvent.getDescription ();
}

private function getTheSame (targetEvent:Event):Event
{
for (var i:Number = 0; i < events.length; i ++)
{
var tempEvent:Event = Event (events [i]);
if (targetEvent.equals (tempEvent))
{
return tempEvent;
}
}
return null;
}
}

Вспомогательный класс Event:


/**
* @author Constantiner (constantiner@gmail.com)
* @version Mar 27, 2006
*/

class constantiner.Event
{
private var eventDateMs:Number;
private var eventDescription:String;
private var isHiPriority : Boolean;

/**
* Конструктор.
*/
public function Event (eventDate:Date, eventDescription:String, isHiPriority:Boolean)
{
eventDateMs = eventDate.getTime ();
this.eventDescription = eventDescription;
this.isHiPriority = isHiPriority;
}

public function equals(targetEvent : Event) : Boolean
{
return Boolean ((targetEvent.isHiPriority == isHiPriority) && (targetEvent.eventDateMs = eventDateMs));
}

/**
* Метод, возвращающий строковую репрезентацию данного экземпляра (как правило, в целях отладки).
* @return Текстовая репрезентация данного экземпляра.
*/
public function toString() : String
{
var formattedDate:Date = new Date (eventDateMs);
return "constantiner.Event <date: " + formattedDate.toString() + ", isHiPriority: " + isHiPriority + ", eventDescription: " + eventDescription + ">";
}

public function concat(targetEvent:Event) : Void
{
eventDescription += " --- " + targetEvent.eventDescription;
}

/**
* @param
* @return
*/
public function getDescription ():String
{
return eventDescription;
}
}

Теперь наш тест:


/**
* @author Constantiner (constantiner@gmail.com)
* @version Mar 27, 2006
*/

import asunit.framework.TestCase;
import constantiner.Organizer;

class constantiner.tests.OrganizerTest extends TestCase
{
private var className:String = "constantiner.tests.OrganizerTest";
private var instance:Organizer;

public function setUp():Void
{
instance = new Organizer ();
}

public function tearDown():Void
{
delete instance;
}

public function testInstantiated():Void
{
assertTrue("target instantiated", instance instanceof Organizer);
}

public function testAddDate ():Void
{
instance.addEvent (new Date (1999, 5), "Hello event", true);
var eventsNumber:Number = instance.getEventsNumber ();
assertEquals ("Должно быть одно событие, а реально их " + eventsNumber, eventsNumber, 1);
instance.addEvent (new Date (2000, 5), "Next year hello", false);
eventsNumber = instance.getEventsNumber ();
assertEquals ("Должно быть два события, а реально их " + eventsNumber, eventsNumber, 2);
}

public function testAddTheSameDate ():Void
{
var firstDate:Date = new Date (2001, 5);
var secondDate:Date = new Date (firstDate.getTime());
instance.addEvent (firstDate, "Hello event", true);
instance.addEvent (secondDate, "And hello again", true);
var eventsNumber:Number = instance.getEventsNumber ();
assertEquals ("Должно быть одно событие, собранное из двух, а реально их " + eventsNumber, eventsNumber, 1);
}

public function testConcatDescription ():Void
{
var firstDate:Date = new Date (2001, 5);
var secondDate:Date = new Date (firstDate.getTime());
var firstDescription:String = "Hello event";
var secondDescription:String = "And hello again";
instance.addEvent (firstDate, firstDescription, true);
instance.addEvent (secondDate, secondDescription, true);
var newDescription:String = instance.getDescription (firstDate, true);
assertTrue ("Новое описание события должно быть длиннее их суммы. Новое описание: " + newDescription, (newDescription.length > (firstDescription.length + secondDescription.length)));
}
}

Запускаем наш Test Runner и видим зеленую полосу:

AsUnit 2.5 by Luke Bayes and Ali Mills
....

Time: 0.026

OK (4 tests)

Ура, говорим мы, наш класс работает! Внедряем его в проект - и находим ошибки (а вы видите ошибку в классах?). Но такого не может быть! Мы же все оттестировали! Обманчива эта иллюзия. Можно подумать, что окружающий код влияет пагубно, ошибку обнаружить нельзя итд. Но на самом деле хватит мумить! Только не мумить! Нам надо сломать тест! Красная полоса - наше спасение! Приступим!

Собственно я написал в итоге темт, дающий красную полосу. Вот он:


public function testMultiplyAdd ():Void
{
var count:Number = 10;
for (var i:Number = 0; i < count; i ++)
{
var firstDate:Date = new Date (2001 + i, 5);
var secondDate:Date = new Date (firstDate.getTime());
var firstDescription:String = "Hello event";
var secondDescription:String = "And hello again";
instance.addEvent (firstDate, firstDescription, true);
instance.addEvent (secondDate, secondDescription, false);
}
var eventsNumber:Number = instance.getEventsNumber ();
assertEquals ("Должно быть " + count * 2 + " событий, а реально их " + eventsNumber, count * 2, eventsNumber);
}
}

Запускаем и видим:


AsUnit 2.5 by Luke Bayes and Ali Mills
.....F

Time: 0.028
There was 1 failure:
0) constantiner.tests.OrganizerTest.testMultiplyAdd()
assertEquals.message: Должно быть 20 событий, а реально их 2

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

Красная полоса! Ура! Дальше можно написать тесты еще, можно написать тесты для класса Event. Но в таком простом и изолированном случае, каким является наш тест, мне проще оказалось найти методом пошаговой отладки тупой баг по невнимательности в методе equals класса Event. Сравнение - это == , а не =. Будьте внимательны!

Ну вот, собственно, мой первый урок использования модульного тестирования в AS2-разработке. Скажу, что статья далеко не полная и не охватывает важного аспекта модульного тестирования - методики TDD (разработка через тестирование). Но все еще впереди!

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

Удачных открытий!

воскресенье, марта 26, 2006

Eclipse/FDT Cheat Sheets

Если вдруг кто-нибудь пользуется FDT, то, возможно, небесполезными окажутся cheat sheets для работы в Eclipse с этим плагином. Повесить на стенку рядом с рабочим местом чтобы иногда поглядывать...

суббота, марта 25, 2006

Как обуздать Eclipse. Попытка первая.

Сидя вчера дома больной и опять безуспешно пытаясь добиться от нашего админа дать мне внешний доступ к нашему subversion-репозиторию, а также веселясь другим забавным накладкам, которые в это время происходили на работе, я решил разобраться с Эклипсом. Почему с ним надо разбираться? Все просто. Я хочу управлять сам плагинами Эклипса. Которые составляют конфликтующие конфигурации. Которые капризничают.

Далеко ходить не надо: Flex Builder 2 Beta 2 конфликтует, например, с FDT. Они оба добавляют отдельный content type для .as-файлов в результате чего некоторые горячие клавиши (например, Ctrl+7) в FDT не работают. И это раздражает. Также WTP конфликтует непонятно с чем и в результате корректно не работает. И это тоже раздражает. А человека больного все раздражает сильнее.

Как подобные проблемы решаются? Ну, во-первых, можно для каждого набора плагинов сделать отдельную инсталляцию Эклипса, которыми пользоваться. Недостатки? Полно! Тратится много места на диске (дублируется Эклипс и общие плагины). Сложность обновления как Эклипса, так и плагинов. Ведь обновлять надо в каждом каталоге. Сложность конфигурирования. Ведь замороченно как добавлять, так и удалять плагины чтобы получить нужную конфигурацию (об этом подробнее ниже).

Другой способ упоминал Antares в своем блоге. Это, скажем так, часть нашего решения и потому, во-первых, отошлю вас к первоисточнику и к первоисточнику первоисточника, а, во-вторых, вкратце изложу сам.

Вы организуете папочку для плагинов, которую я храню в Program Files рядом с папкой Eclipse и называю eclipse_config. В ней вы делаете папочку-прототип для всех плагинов и называете ее eclipse. Папочка eclipse должна в себе содержать папочки features и plugins, а также файл с названием .eclipseextension. И файл этот должен содержать в себе следующее:

name=Eclipse Platform
id=org.eclipse.platform
version=3.1.2

Понятно, что это касается версии 3.1.2 как у меня. Если у вас версия другая (версия чего? Гриба, то есть Эклипса), то сами поставите другие цифры.

Теперь для каждого плагина создаете в eclipse_config папку с именем, соотвествующим плагину (например, wtp), в которую копируете прототипную папочку eclipse. И уже внутрь этой папочки копируете ваш плагин.

Данная методика, изложенная в вышеприведенных источниках, предполагает дальнейшее управление плагинами через Help > Software Updates > Manage Configuration. Я легко допускаю, что я туп, но мне этот способ не понравился и вот почему. Во-первых, не всегда можно задисэйблить некоторый плагин. Если от него зависит кто-то еще, то надо сначала его задисэйблить итд. Во-вторых, я так и не смог на своем Эклипсе 3.1.2 включить плагин обратно. Enable на дисэйбленном пути к плагину у меня плагин сам не включает хоть ты тресни! Непредсказуемый результат! Ну и, вдобавок, я не имею конфигураций Эклипса как таковых. Я вынужден каждый раз производить шаманские операции по отключению того, включению этого и так далее. Утомительно, непредсказуемо, требует держать в памяти сочетания конфликтующих плагинов. В общем, не насладился я этим способом.

И я стал искать дальше и нашел приятный линк. И узнал я про способ с линками. И подумалось мне: а что если каждой конфигурации будет соотвествовать свой набор линков. Только вот как этим делом управлять чтобы сохранить и эклипс в единственном экземпляре, и плагины, и запускать легко было, и поддерживать не замороченно? Способов, сразу скажу, очень много. Это и ежу понятно. Я выбрал близкий себе способ, о котором уже однажды рассказывал, - Apache Ant.

Итак, что я хочу? Я хочу получить билд-файл, который бы имел цели запуска конфигураций плагинов, которые я ему определю. И вот тут начинается самая трудоемкая часть задачи. Сразу скажу, что поддерживать это дело будет гораздо легче. Тем более, что ant-редактор в Эклипсе очень хорош. И сам ant-скрипт это простой и естественный xml-диалект.

Приступим. Если вы не знакомы с Ant'ом - идите пока читать статью и устанавливать рабочую среду. Сразу скажу, что вам тут, по сути, понадобится только сам ant (и, понятно, jre, которая у вас, судя по тому, что вы работаете с Эклипсом, есть. Также по этой причине у вас есть и Ant, но в плагинах. Неплохо бы иметь и standalone-установку). Пока вы изучаете, я тем временем создам в папке eclipse_config папку links_store, куда сложу все мои link-файлы. Линк-файл может, например, называться asdt.link и содержать в себе строку:

path=C:\\Program Files\\eclipse_config\\ASDT\\

В корне моей папки eclipse_config я создаю билд-файл, который называю build.xml. Также создаю файл local.properties для локальных индивидуальных настроек. Приведу пример моего билд-файла:


<?xml version="1.0"?>
<!-- ======================================================================
24.03.2006 15:39:09

eclipse.config
Build file for eclipse configs launching

Constantiner
====================================================================== -->
<project name="eclipse.config" default="run.all.plugins" basedir=".">
<description>
Build file for eclipse configs launching
</description>

<!-- Define properties to override in local.properties file -->
<property file="local.properties" />
<!-- Define Eclipse folder (with eclipse executable and features/plugins folder). Override it in local.properties -->
<property name="eclipse.dir" location="."/>
<!-- Path to Eclipse executable -->
<property name="eclipse.path" location="${eclipse.dir}/eclipse.exe"/>

<!-- Folder to store all plugins links (in plugins configuration folder outside eclipse folder) -->
<property name="links.store" location="links_store/"/>
<!-- Target for links copying (in eclipse folder) -->
<property name="links" location="${eclipse.dir}/links/"/>

<!-- Define properties for link files names -->
<property name="all_the_news.link" value="all_the_news.link" />
<property name="asdt.link" value="asdt.link" />
<property name="CFEclipse.link" value="CFEclipse.link" />
<property name="eclipse.colorer.link" value="eclipse.colorer.link" />
<property name="flex.link" value="flex.link" />
<property name="gef.link" value="gef.link" />
<property name="misc.link" value="misc.link" />
<property name="PHPEclipse.link" value="PHPEclipse.link" />
<property name="subclipse.link" value="subclipse.link" />
<property name="uml2.link" value="uml2.link" />
<property name="ve.link" value="ve.link" />
<property name="winamp.link" value="winamp.link" />
<property name="wtp.link" value="wtp.link" />
<property name="XSD-SDO-EMF.link" value="XSD-SDO-EMF.link" />
<property name="PHPIDE.link" value="PHPIDE.link" />

<!-- Section defines pluginsets for every configuration -->
<fileset dir="${links.store}" id="core">
<patternset id="core.pattern">
<include name="${misc.link}"/>
<include name="${eclipse.colorer.link}"/>
<include name="${gef.link}"/>
<include name="${uml2.link}"/>
<include name="${winamp.link}"/>
<include name="${XSD-SDO-EMF.link}"/>
<include name="${subclipse.link}"/>
<include name="${ve.link}"/>
<include name="${all_the_news.link}"/>
</patternset>
</fileset>

<fileset dir="${links.store}" id="all">
<patternset id="all.pattern">
<patternset refid="core.pattern" />
<include name="${asdt.link}"/>
<include name="${CFEclipse.link}"/>
<include name="${flex.link}" />
<include name="${PHPEclipse.link}"/>
<include name="${wtp.link}"/>
<include name="${PHPIDE.link}"/>
</patternset>
</fileset>

<fileset dir="${links.store}" id="cold.fusion">
<patternset id="cold.fusion.pattern">
<patternset refid="core.pattern" />
<include name="${CFEclipse.link}"/>
</patternset>
</fileset>

<fileset dir="${links.store}" id="php">
<patternset id="php.pattern">
<patternset refid="core.pattern" />
<include name="${PHPEclipse.link}"/>
</patternset>
</fileset>

<fileset dir="${links.store}" id="flash.asdt">
<patternset id="flash.asdt.pattern">
<patternset refid="core.pattern" />
<include name="${asdt.link}"/>
</patternset>
</fileset>

<fileset dir="${links.store}" id="flex">
<patternset id="flex.pattern">
<patternset refid="core.pattern" />
<include name="${flex.link}"/>
</patternset>
</fileset>

<fileset dir="${links.store}" id="wtp">
<patternset id="wtp.pattern">
<patternset refid="core.pattern" />
<include name="${wtp.link}"/>
</patternset>
</fileset>

<fileset dir="${links.store}" id="php.ide">
<patternset id="php.ide.pattern">
<patternset refid="core.pattern" />
<include name="${wtp.link}"/>
<include name="${PHPIDE.link}"/>
</patternset>
</fileset>
<!-- End of pluginsets section -->

<!-- =================================
target: run.all.plugins
================================= -->
<target name="run.all.plugins" depends="clean,init,install.all,run.eclipse" description="--> Runs with all plugins">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.all
- - - - - - - - - - - - - - - - - -->
<target name="install.all">
<install.links links.id="all" />
</target>

<!-- =================================
target: run.php.ide
================================= -->
<target name="run.php.ide" depends="clean,init,install.php.ide,run.eclipse" description="--> Runs for PHP development with Zend PHP IDE">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.php.ide
- - - - - - - - - - - - - - - - - -->
<target name="install.php.ide">
<install.links links.id="php.ide" />
</target>

<!-- =================================
target: run.cold.fusion
================================= -->
<target name="run.cold.fusion" depends="clean,init,install.cold.fusion,run.eclipse" description="--> Runs for ColdFusion development with CFclipse">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.cold.fusion
- - - - - - - - - - - - - - - - - -->
<target name="install.cold.fusion">
<install.links links.id="cold.fusion" />
</target>

<!-- =================================
target: run.php
================================= -->
<target name="run.php" depends="clean,init,install.php,run.eclipse" description="--> Runs for PHP development with PHPEclipse">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.php
- - - - - - - - - - - - - - - - - -->
<target name="install.php">
<install.links links.id="php" />
</target>

<!-- =================================
target: run.flash.asdt
================================= -->
<target name="run.flash.asdt" depends="clean,init,install.flash.asdt,run.eclipse" description="--> Runs for Flash development with ASDT">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.flash.asdt
- - - - - - - - - - - - - - - - - -->
<target name="install.flash.asdt">
<install.links links.id="flash.asdt" />
</target>

<!-- =================================
target: run.flex
================================= -->
<target name="run.flex" depends="clean,init,install.flex,run.eclipse" description="--> Runs for Flex development with Flex Builder 2">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.flex
- - - - - - - - - - - - - - - - - -->
<target name="install.flex">
<install.links links.id="flex" />
</target>

<!-- =================================
target: run.wtp
================================= -->
<target name="run.wtp" depends="clean,init,install.wtp,run.eclipse" description="--> Runs for J2EE development with WTP">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.wtp
- - - - - - - - - - - - - - - - - -->
<target name="install.wtp">
<install.links links.id="wtp" />
</target>

<!-- - - - - - - - - - - - - - - - - -
target: clean
Clear links folder for configuration launching
- - - - - - - - - - - - - - - - - -->
<target name="clean">
<delete>
<fileset dir="${links}" includes="**/*.link"></fileset>
</delete>
</target>

<!-- - - - - - - - - - - - - - - - - -
target: init
Creates links folder (if not exists)
- - - - - - - - - - - - - - - - - -->
<target name="init">
<mkdir dir="${links}"/>
</target>

<!-- - - - - - - - - - - - - - - - - -
target: run.eclipse
Runs eclipse with workbench parameter.
- - - - - - - - - - - - - - - - - -->
<target name="run.eclipse" if="workbench" depends="run.eclipse.without.workbench">
<exec executable="${eclipse.path}" spawn="true">
<arg line="-data" />
<arg value="${workbench}" />
<arg line="-clean" />
<arg line="-refresh" />
<arg line="-nl en_US" />
</exec>
</target>

<!-- - - - - - - - - - - - - - - - - -
target: run.eclipse.without.workbench
- - - - - - - - - - - - - - - - - -->
<target name="run.eclipse.without.workbench" unless="workbench">
<exec executable="${eclipse.path}" spawn="true">
<arg line="-clean" />
<arg line="-refresh" />
<arg line="-nl en_US" />
</exec>
</target>

<!-- = = = = = = = = = = = = = = = = =
macrodef: install.links
= = = = = = = = = = = = = = = = = -->
<macrodef name="install.links">
<attribute name="links.id" />
<sequential>
<copy todir="${links}" >
<fileset refid="@{links.id}" />
</copy>
</sequential>
</macrodef>
</project>

Данный файл достаточно объемен, но, в общем-то, прост. Тем более что содержит комментарии на корявом английском. Заслуживает внимания секция перечисления всех интересующих нас линков, секция определения набора линков для каждой конфигурации, а также собственно цели запуска конфигураций. Заслуживает интереса наличие секции core, в которой я перечислил плагины, доступные в каждой конфигурации. Также обратите внимание на приватные цели run.eclipse и run.eclipse.without.workbench. Что это значит? Это значит, что вы можете определить свойство workbench, при наличии которого ваша конфигурация запустится в нужном вам workbench'е. Также при запуске Эклипса я добавил опции -clean (очищает кэш зависимостей плагинов), -refresh (обновляет проекты при запуске), а также -nl en_US (запускает Эклипс как если бы вы находились в США - необходимость этого описана в одном из моих постов). Свойство workbench можно задать двумя способами. Во-первых, в файле local.properties. При этом оно будет общим для всех конфигураций, что нам не очень-то интересно. Второй способ я опишу ниже, а сейчас приведу мой вариант файла local.properties:


eclipse.dir = C:\\Program Files\\Eclipse\\
#links.store = links_store

Здесь я просто задаю мой каталог с установкой Eclipse SDK. Там же можно задать многие другие свойства.

Если мы запустим теперь наш билд-файл в режиме помощи:

ant -projecthelp

То увидим все наши доступные публичные цели:

Buildfile: build.xml

Build file for eclipse configs launching

Main targets:

run.all.plugins --> Runs with all plugins
run.cold.fusion --> Runs for ColdFusion development with CFclipse
run.flash.asdt --> Runs for Flash development with ASDT
run.flex --> Runs for Flex development with Flex Builder 2
run.php --> Runs for PHP development with PHPEclipse
run.php.ide --> Runs for PHP development with Zend PHP IDE
run.wtp --> Runs for J2EE development with WTP

Default target: run.all.plugins

Цель run.php.ide меня вчера попросил добавить Михаил «Antares» Клишин с целью проверить совместимость текущей версии PHP IDE (0.5) с WTP 1.0 и Eclipse 3.1.2. Легко - сказал я. Я сделал папку Zend PHPIDE в моей eclipse_config, куда по описанным выше правилам скопировал данный плагин. В билд-файле я добавил свойство с именем линк-файла плагина (который тоже сделал в links_store):

<property name="PHPIDE.link" value="PHPIDE.link" />

Далее добавил набор линк-файлов данной конфигурации (включая wtp):

<fileset dir="${links.store}" id="php.ide">
<patternset id="php.ide.pattern">
<patternset refid="core.pattern" />
<include name="${wtp.link}"/>
<include name="${PHPIDE.link}"/>
</patternset>
</fileset>

И, собственно, сами цели, копирующие линки в папку Эклипса и запускающие ее:

<!-- ================================= 
target: run.php.ide
================================= -->
<target name="run.php.ide" depends="clean,init,install.php.ide,run.eclipse" description="--> Runs for PHP development with Zend PHP IDE">

</target>

<!-- - - - - - - - - - - - - - - - - -
target: install.php.ide
- - - - - - - - - - - - - - - - - -->
<target name="install.php.ide">
<install.links links.id="php.ide" />
</target>

Это заняло у меня совсем немного времени, зато в результате я узнал, что данная конфигурация вполне рабочая.

Как запускать эту конфигурацию? Просто наберите в командной строке:

ant run.php.ide

Эклипс запустится в данной конфигурации в последнем использовавшемся workbench'е.

Но тут есть два недостатка: во-первых, неудобно каждый раз набирать такие команды, а, во-вторых, хотелось бы для разных конфигураций задавать разные workbench'и (для flash-разработки - один, для php - другой). Эта проблема запросто решается с помощью bat-файлов на наиболее употребимые варианты. Например, для запуска PHP IDE мы делаем php.ide.bat, лежащий рядом с build.xml, который имеет следующий вид:

ant run.php.ide -Dworkbench=E:\Constantiner\!Projects\php_development

Поясню, что с помощью конструкции -Dproperty=value мы можем устанавливать значения свойств для Ant'а.

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

Я согласен с Мартином Фаулером, что таким ленивым людям, как он и я, необходимо немало потрудиться чтобы сократить работу в будущем.

Ну а теперь перечислю достоинства и недостатки данного способа. Достоинства:


  • Мы имеем по одному экземпляру Eclipse SDK и плагинов, что экономит место на диске.

  • Мы в любой момент можем сменить версию как SDK, так и плагинов без необходимости переставлять и чистить все.

  • Мы запускаем Eclipse с ограниченным набором плагинов, что так или иначе увеличивает производительность.

  • Мы просто управляем процессом конфигурирования среды Eclipse.

  • Мы избавляемся от конфликта плагинов.

  • Мы получаем удовлетворение от проделанных изысканий :)


Недостатки:


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

  • Возможны проблемы с восстановлением рабочей перспективы если в одном workbench'е запускать разные конфигурации. Можно забить. Но, возможно, кто-то предложит способ избежать этого.

  • Возможные проблемы с установкой плагинов. Это касается плагинов, устанавливаемых через update site и тех, которые устанавливаются инсталляторами (типа Flex Builder 2). Эти проблемы обходятся, но не очень изящно. Хотелось бы иметь простое решение.


Ну вот, собственно, и все, о чем я хотел вам поведать. Спасибо за внимание. Пишите свои пожелания, вопросы, замечания. Моя методика не претендует на полноту, идеальность и общеприменимость. Но если кого-то она наведет на гениальные идеи... То делитесь! :)