среда, 23 марта 2011 г.

Private атрибуты - не помеха для JUnit

Правильно ли использовать в юнит-тестах какие-либо private атрибуты (методы или поля) класса? Вопрос философский, сформулировать который можно и так: blackbox vs whitebox testing. В контексте общего понимания вопроса выбор скорее всего будет за blackbox тестированием. Оно красиво, элегантно и соответствует правильным идеям. Однако реальность сложнее и иногда то, что элегантно и красиво тупо не работает.
Если возникла необходимость вызвать какие-либо приват методы тестируемого класса, подходящим решением (не требующим модификации самого класса) является использование reflection, хорошо описанное здесь.
У меня была несколько иная задача. Требовалось вытянуть из класса приватные статик филды. Вот как это было реализовано:
java.lang.reflect.Field field = MyClass.class.getDeclaredField("STATIC_FIELD_NAME");
field.setAccessible(true);
String value = (String) field.get(null);

Все должно быть понятно из самого кода. Единственное - что это за null там такой в методе get. Это означает что мы хотим получить статик филд, если бы был инстанс филд, то вместо null передали бы объект, значение филда которого нам нужно.

пятница, 18 марта 2011 г.

Жабо-регэкспы

Почему-то считал, что джава умеет проводить лишь простейший поиск с помощью регэкспов, однако оказалось, что захват групп, замена - все это также возможно.
Появилась задача передать значение длины промежутка времени, выраженное в минутах, часах, днях, месяцах, годах. Что-то вроде 1d для 1 дня, 3M для 3 месяцев, 75m для 75 минут и т.д. Для разбора строки с таким значением идеально подходит регэксп с захватом групп. Числовое значение выделяем в одну группу, значение единицы времени в другую. Вот регэксп, к которому я пришел:
^([1-9][0-9]*)(y|M|d|H|m|s)$
здесь ^ - начало строки, $ - конец строки. Эти символы добавлены, чтобы исключить какие-либо еще посторонние символы. Первая группа ^([1-9][0-9]*) захватывает числовое значение. Значения вида "01" или "-2" не подходят. Вторая группа (y|M|d|H|m|s) захватывает значение единицы времени. Соответствующий джава-код для обработки значения с помощью этих групп:
            Pattern pattern = Pattern.compile(^([1-9][0-9]*)(y|M|d|H|m|s)$);
       Matcher matcher = pattern.matcher(value);
       boolean matchFound = matcher.find();

       if (matchFound) {
           String numberValue = matcher.group(1);
           String unitValue = matcher.group(2);
           int number = Integer.parseInt(numberValue);
           //do something
       }

Весьма все просто.

Задача номер два заключала в себе необходимость формата строки, задающей путь в файловой системе в зависимости от даты. Использовался синтаксис аналогичный FileNamePattern в log4j. Пример:
%d{yyyy}/%d{MM}/msg.%d{yyyy-MM-dd}.gz
Для 18 марта 2011 года из этого шаблона должно получиться 2011/03/msg.2011-03-18.gz
То есть по сути следует заменить все вхождения %d{...} соответствующими значениями переданной даты, шаблон внутри %d{...} аналогичен используемому в SimpleDateFormat. Вот код который производит необходимое форматирование:
Pattern pattern = Pattern.compile("%d\\{(.*?)\\}");
Matcher matcher = pattern.matcher(pathPattern);
String path = pathPattern;
       
while (matcher.find()) {
      SimpleDateFormat dateFormat = new SimpleDateFormat(matcher.group(1));
      path = matcher.replaceFirst(dateFormat.format(date));
      matcher.reset(path);
}

Здесь мы последовательно в цикле находим вхождение паттерна, захватываем группу, форматируем ее с помощью SimpleDateFormat и производим необходимую замену в входной строке шаблона. Выход из цикла происходит, когда в результате не найдено ни одного блока %d{...}

понедельник, 14 марта 2011 г.

Log4j эксплоит

Все мы знаем о том, как прекрасен log4j. Вставляешь выводы для разных уровней DEBUG, INFO, ERROR в коде, а потом через настройки в текстовом формате можешь контролить степень подробности описания выполнения проги. Код написанный для log4j не является бизнес логикой, он не влияет на ход выполнения проги никак, лишь описывая текущее состояние системы для последующего администрирования.
Однако log4j можно применить и непосредственно для создания функциональности.
Мне потребовалось написать программу, сохраняющую поток некоторых данных в текстовом виде, с разбиением данных на отдельные файлы в зависимости от календарного дня. Была необходима  архивация данных прошедших дней. Можно было написать все это вручную, но зачем, когда все требуемые действия умеет производить log4j.
Разбиение в зависимости от даты с последующей архивацией завершенных файлов умеет выполнять org.apache.log4j.rolling.RollingFileAppender. Это класс из дополнительного пакета log4j-extras. Так что нам понадобятся две джарки: log4j и log4j-extras. Вот пример конфигурации в log4j.xml, в которой

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">

<log4j:configuration>
  <appender name="dailyLog" class="org.apache.log4j.rolling.RollingFileAppender">
    <rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy">
      <param name="FileNamePattern" value="/tmp/my-log.%d{yyyy-MM-dd}.gz"/>
    </rollingPolicy>
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{HH:mm:ss,SSS} - %c{1} - %m%n"/>
    </layout>
  </appender>
  <logger name="MyBusinessData">
      <level value="info"/>
      <appender-ref ref="dailyLog"/>
  </root>
</log4j:configuration>

В этой конфигурации
  • осуществляется вывод логгера MyBusinessData с уровнем не ниже INFO
  • вывод осуществляется в файлы /tmp/my-log.*  - в зависимости от
    даты, так логирование, произошедшее 14 марта 2011 запишется в файл /tmp/my-log.2011.03.14; подробнее про настройку FileNamePattern здесь
  • по окончанию дня соответствующий файл архивируется gzip'ом.
  • в самом файле формат вывода определяется строчкой "%d{HH:mm:ss,SSS} - %c{1} - %m%n", где вначале идет формат времени, затем указание категории, затем само сообщение и символ новой строки. Подробнее про настройку ConversionPattern здесь

Теперь я могу записывать свои данные с помощью обычного логгера:
private Logger dataLogger = Logger.getLogger("MyBusinessData");
...
dataLogger.info(data);
и при указанной конфигурации данные будут сохраняться так как и требовалось. Однако возникает проблема которая как раз и связана со словами "при указанной конфигурации". Проблема в том, что изменив текстовый файл настройки log4j.xml (либо тупо забыв его поместить в нужное место и указать нужные значения), можно заглушить запись данных, то есть поломать программу. Гибкость настройки log4j в нашем случае оборачивается опасностью. Непорядок!
Поэтому переходим к программной настройке. Идея в том, чтобы игнорировать какие-либо настройки в текстовом файле для нашего дата-логгера, а вместо этого использовать жестко заданные в коде.
Вот те же самые настройки, произведенные программно:
dataLogger.removeAllAppenders();    //we don't want any configuration other than this
dataLogger.setAdditivity(false);    //avoid sending messages to root logger
RollingFileAppender rfa = new RollingFileAppender();
TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy();

//the smallest unit in the pattern is day, so rolling will occur at the end of each day
//.gz suffix tells log4j to compress finished day
rollingPolicy.setFileNamePattern("/tmp/my-log.%d{yyyy-MM-dd}.gz");
rollingPolicy.activateOptions();
rfa.setRollingPolicy(rollingPolicy);
PatternLayout layout = new PatternLayout();
layout.setConversionPattern("%d{HH:mm:ss,SSS} - %c{1} - %m%n");
rfa.setLayout(layout);
rfa.activateOptions();
dataLogger.addAppender(rfa);
dataLogger.setLevel(Level.INFO);

Очень важна здесь вторая строчка dataLogger.setAdditivity(false). Благодаря ей, сообщения, записываемые нашим дата-логером, не будут дублироваться на верхние уровни иерархии, в частности на рут логер. Таким образом, настроив обычное логирование даже для рута, мы не будем получать в довесок все данные логируемые нашим логером. Т.е. мы отдельно настроили дата-логер без ущерба для задачи логирования. Теперь можно удалить файл настроек log4j, либо настроить произвольным образом логирование, наша запись данных всегда будет производиться и не мешать остальному логированию. Ура!


пятница, 11 марта 2011 г.

Меняем сообщения коммитов в SVN и Trac

Случилась такая ситуация. Закомиттил я изменение и только потом вспомнил, что в сообщении коммита неплохо было бы указать, что оно относится к такому-то тикету. Вобщем, понадобилось изменить сообщение коммита.
Недолго гуглив пришел вот к такому непыльному решению:

1. Залогиниться на комп, хранящий репозиторий
2. Выполнить sudo rm -rf / Создать где-нибудь в темпе файлик с нужным сообщением для коммита:
sudo vim /tmp/new_log
3. Выполнить магическую команду:
sudo svnadmin setlog REPO_PATH -r N /tmp/new_log  --bypass-hooks
где REPO_PATH - путь к репозиторию, N - номер ревизии, чьё сообщение меняем

Теперь в репозитории красуется нужное нам название для N-й ревизии.
Однако трак не знает о нашем изменении и продолжает показывать старое сообщение. Непорядок!

4. Говорим траку перестать тупить и показать новое сообщение
sudo trac-admin TRAC_PATH resync N
где N - всё тот же номер ревизии, TRAC_PATH - путь к трак-проекту. Можно и без номера вызвать команду, тогда произойдет полная синхронизация со всеми ревизиями. Оно вам надо?
Проделать все без захода на сам сервер (без 1го пункта), удаленно нельзя, потому как 3 и 4 пункты выполняются для локальных путей. Если трак и репозиторий на отдельных компах, то надо зайти на оба. А кто сказал что будет легко?

среда, 9 марта 2011 г.

Установка PostgreSQL

Продолжаем обвешивать сервачок всякой всячиной.
На очереди могучий слон PostgreSQL.
За основу взята статья из убунту доков.

1. Ставим
sudo apt-get install postgresql

2. Так как это у нас отдельный сервачок постгреса, то доступ к нему будет осуществляться через юзер/пароль. Дефолтный юзер - postgres. Установим ему пароль:
sudo -u postgres psql postgres
\password postgres
#вводим пароль дважды

3. Устанавливаем возможность подключение к БД с любого адреса:
# /etc/postgresql/8.4/main/postgresql.conf
listen_addresses = '*'


4. Настраиваем аутентификацию, разрешаем пользователям с любых адресов иметь доступ ко всем базам данных и заходить под любым пользователем, при условии ввода правильного соответствующего пароля, который будет передаваться в зашифрованном md5 виде:
# добавляем строчку в /etc/postgresql/8.4/main/pg_hba.conf
host    all         all         0.0.0.0/0               md5

Дополнительно про настройки аутентификации можно почитать здесь.

5. Перезагружаем постгрес, чтобы настройки в пунктах 3 и 4 вступили в силу:
sudo /etc/init.d/postgresql-8.4 restart

Теперь к постгресу, установленном на сервачке можно подключаться с других компов-десктопов, используя годный ГУИ тул pgadmin3.
Указанная здесь конфигурация на самом деле слишком... гостеприимная, что-ли. Я сделал ее таковой лишь на время разработки, чтобы иметь возможность стучаться к БД не только с работы, но и из дома через динднс. В целом же при настройке доступа к базе следует задавать как можно более жесткие условия для прослушиваемых адресов и аутентификации.
Вот и все, спокойной ночи, девочки и мальчики, до новых встреч!

пятница, 4 марта 2011 г.

Чистим старые ядра

При очередном обновлении моей десктопной убунты, вылезла вот такая страшная ошибка:
Not enough free disk space
The upgrade needs a total of 19.9M free space on disk '/boot'. Please free at least an additional 12.2M of disk space on '/boot'. Empty your trash and remove temporary packages of former installations using 'sudo apt-get clean'.
Как же так? Забился бут?
df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda6             127G   45G   76G  37% /
none                  940M  268K  939M   1% /dev
none                  944M  372K  943M   1% /dev/shm
none                  944M  344K  943M   1% /var/run
none                  944M     0  944M   0% /var/lock
none                  944M     0  944M   0% /lib/init/rw
/dev/sdb1             459G  105G  332G  24% /var/lib/storage
/dev/sda1              92M   79M  7.4M  92% /boot


И правда, забился... Смотрим чем же.
ls -lah /boot
total 70M
drwxr-xr-x  4 root root 3.0K 2011-02-18 15:23 .
drwxr-xr-x 22 root root 4.0K 2011-02-22 11:10 ..
-rw-r--r--  1 root root 637K 2010-09-16 21:14 abi-2.6.32-24-generic
-rw-r--r--  1 root root 637K 2010-10-17 02:46 abi-2.6.32-25-generic
-rw-r--r--  1 root root 637K 2010-11-24 15:57 abi-2.6.32-26-generic
-rw-r--r--  1 root root 637K 2010-12-02 02:48 abi-2.6.32-27-generic
-rw-r--r--  1 root root 637K 2011-01-11 03:18 abi-2.6.32-28-generic
-rw-r--r--  1 root root  512 2011-02-18 15:23 boot.0810
-rw-r--r--  1 root root  512 2011-02-18 15:23 boot.0820
-rw-r--r--  1 root root 114K 2010-09-16 21:14 config-2.6.32-24-generic
-rw-r--r--  1 root root 114K 2010-10-17 02:46 config-2.6.32-25-generic
-rw-r--r--  1 root root 114K 2010-11-24 15:57 config-2.6.32-26-generic
-rw-r--r--  1 root root 114K 2010-12-02 02:48 config-2.6.32-27-generic
-rw-r--r--  1 root root 114K 2011-01-11 03:18 config-2.6.32-28-generic
drwxr-xr-x  3 root root 6.0K 2011-02-02 11:07 grub
-rw-r--r--  1 root root 7.6M 2010-09-28 10:36 initrd.img-2.6.32-24-generic
-rw-r--r--  1 root root 7.6M 2010-11-18 10:02 initrd.img-2.6.32-25-generic
-rw-r--r--  1 root root 7.6M 2010-11-30 10:49 initrd.img-2.6.32-26-generic
-rw-r--r--  1 root root 7.6M 2011-01-21 10:42 initrd.img-2.6.32-27-generic
-rw-r--r--  1 root root 7.6M 2011-02-02 11:07 initrd.img-2.6.32-28-generic
drwx------  2 root root  12K 2010-08-27 16:56 lost+found
-rw-r--r--  1 root root 157K 2010-03-23 11:37 memtest86+.bin
-rw-r--r--  1 root root 1.7M 2010-09-16 21:14 System.map-2.6.32-24-generic
-rw-r--r--  1 root root 1.7M 2010-10-17 02:46 System.map-2.6.32-25-generic
-rw-r--r--  1 root root 1.7M 2010-11-24 15:57 System.map-2.6.32-26-generic
-rw-r--r--  1 root root 1.7M 2010-12-02 02:48 System.map-2.6.32-27-generic
-rw-r--r--  1 root root 1.7M 2011-01-11 03:18 System.map-2.6.32-28-generic
-rw-r--r--  1 root root 1.2K 2010-09-16 21:16 vmcoreinfo-2.6.32-24-generic
-rw-r--r--  1 root root 1.2K 2010-10-17 02:47 vmcoreinfo-2.6.32-25-generic
-rw-r--r--  1 root root 1.2K 2010-11-24 16:00 vmcoreinfo-2.6.32-26-generic
-rw-r--r--  1 root root 1.2K 2010-12-02 02:50 vmcoreinfo-2.6.32-27-generic
-rw-r--r--  1 root root 1.2K 2011-01-11 03:20 vmcoreinfo-2.6.32-28-generic
-rw-r--r--  1 root root 3.9M 2010-09-16 21:14 vmlinuz-2.6.32-24-generic
-rw-r--r--  1 root root 3.9M 2010-10-17 02:46 vmlinuz-2.6.32-25-generic
-rw-r--r--  1 root root 3.9M 2010-11-24 15:57 vmlinuz-2.6.32-26-generic
-rw-r--r--  1 root root 3.9M 2010-12-02 02:48 vmlinuz-2.6.32-27-generic
-rw-r--r--  1 root root 3.9M 2011-01-11 03:18 vmlinuz-2.6.32-28-generic

Гнилые старые ядра забили мой бут и не дают обновляться! Стереть их! Однако стирать нужно аккуратненько, через apt-get. Немного погуглив я нашел вот этот хороший гайд. Продублирую его здесь оставив только варианты команд для няки убунты (для второй няки генту их там и не было).

1. Узнаем, какое ядро используем сейчас:
uname -r
2.6.32-28-generic

2. Узнаем, какие ядра установлены в системе
dpkg --list 'linux-image*'
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Cfg-files/Unpacked/Failed-cfg/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                              Version                           Description
+++-=================================-=================================-==================================================================================
un  linux-image                                                   (no description available)
un  linux-image-2.6                                               (no description available)
ii  linux-image-2.6.32-24-generic     2.6.32-24.43                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-25-generic     2.6.32-25.45                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-26-generic     2.6.32-26.48                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-27-generic     2.6.32-27.49                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-28-generic     2.6.32-28.55                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-generic               2.6.32.28.32                      Generic Linux kernel image

Вот все эти позорные саботажники апгрейда! Казнить их! (кроме двух последних, действующего кернела и запасного старого).
3. Удаляем ядрышки ненужных версий (в статье, на которую я ссылаюсь, флага --purge нету, не знаю есть ли тут существенная разница, но --purge должен почистить всякие остаточные файлы)
sudo apt-get remove --purge linux-image-2.6.32-24-generic
sudo apt-get remove --purge linux-image-2.6.32-25-generic
sudo apt-get remove --purge linux-image-2.6.32-26-generic

4. Дополнительно погуглив, обнаружил, что после такого удаления остаются некоторые депенденси. Срубаем головы саботажникам (удаляем linux-headers для соответствующих удаленных ядер)
sudo apt-get remove --purge linux-headers-2.6.32-24-*
sudo apt-get remove --purge linux-headers-2.6.32-25-*
sudo apt-get remove --purge linux-headers-2.6.32-26-*

В результате ядра удалены, /boot освобожден. Однако в результате выполнения команд у меня появились предупреждения такого рода:
dpkg: warning: while removing linux-image-2.6.32-24-generic, directory '/lib/modules/2.6.32-24-generic' not empty so not removed
Возможно стоит вручную почистить эти каталоги, пока не стал этого делать.