*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***
Показаны сообщения с ярлыком инструменты. Показать все сообщения
Показаны сообщения с ярлыком инструменты. Показать все сообщения

вторник, 22 ноября 2011 г.

TCP/IP proxy на Erlang'e

По мотивам недавнего поста про изучение новых языков, я таки добил версию на Erlang'е. Если тут есть спецы по языку, буду признателен за критику.

Программа по функциям идентична версии на Питоне за исключением отсутствия дублирования лога в файл и продвинутого разбора флагов командной строки.

И так: программа многопоточна, и журналирование также происходит в отдельном потоке для обеспечения целостности многострочных дампов.

Про Эрланг. После многократных и пока полностью неуспешных заходов на Хаскелл и после все еще неудачных попыток на Lisp или Scheme написать что-то более менее реальное и жизненное, Эрланг был реальным прорывом для меня.

Удивительно, невозможность изменять переменные (представьте, что программируя на С++ надо все переменные делать const) является фантастическим способом борьбы с опечатками при cut-and-paste. Также когда делаешь циклы через хвостовую рекурсию, сразу осознаешь, как эффективно работать со списками, чтобы их не копировать, а всегда "таскать" за хвост или голову.

Ну а концепция легких потоков и обмена сообщениями между ними (как в Go), приправленная глобальной иммутабельностью, позволяет легко писать надежные многотопочные программы.

Например, истересен способ реализации многопотокового TCP/IP сервера. Обычно при его программировании есть распростраенный прием: один главный поток, принимающий соединения, и когда соединение принято, создается новый поток-исполнитель, который обрабатывает соединение и после этого умирает.

В Эрланге можно сделать иначе (функция acceptor()). Поток, ожидающий входящего соединения, после его получения рождает свой клон для ожидания следующего соединения и затем сам обрабабатывает запрос.

Для меня это было немного необычно.

-module(tcp_proxy).

-define(WIDTH, 16).

main([ListenPort, RemoteHost, RemotePort]) ->
    ListenPortN = list_to_integer(ListenPort),
    start(ListenPortN, RemoteHost, list_to_integer(RemotePort));

main(_) -> usage().

usage() ->
    io:format("~ntcp_proxy.erl local_port remote_port remote_host~n~n", []),
    io:format("Example:~n~n", []),
    io:format("tcp_proxy.erl 50000 google.com 80~n~n", []).

start(ListenPort, CalleeHost, CalleePort) ->
    io:format("Start listening on port ~p and forwarding data to ~s:~p~n",
              [ListenPort, CalleeHost, CalleePort]),
    {ok, ListenSocket} = gen_tcp:listen(ListenPort, [binary, {packet, 0},
                                                    {reuseaddr, true},
                                                    {active, true}]),
    io:format("Listener socket is started ~s~n", [socket_info(ListenSocket)]),
    spawn(fun() -> acceptor(ListenSocket, CalleeHost, CalleePort, 0) end),
    register(logger, spawn(fun() -> logger() end)),
    wait().

% Infinine loop to make sure that the main thread doesn't exit.
wait() -> receive _ -> true end, wait().

format_socket_info(Info) ->
    {ok, {{A, B, C, D}, Port}} = Info,
    lists:flatten(io_lib:format("~p.~p.~p.~p:~p", [A, B, C, D, Port])).

peer_info(Socket) -> format_socket_info(inet:peername(Socket)).

socket_info(Socket) -> format_socket_info(inet:sockname(Socket)).

acceptor(ListenSocket, RemoteHost, RemotePort, ConnN) ->
    case gen_tcp:accept(ListenSocket) of
      {ok, LocalSocket} ->
          spawn(fun() -> acceptor(ListenSocket, RemoteHost, RemotePort, ConnN + 1) end),
          LocalInfo = peer_info(LocalSocket),
          logger ! {message, "~4.16.0B: Incoming connection from ~s~n", [ConnN, LocalInfo]},
          case gen_tcp:connect(RemoteHost, RemotePort, [binary, {packet, 0}]) of
            {ok, RemoteSocket} ->
              RemoteInfo = peer_info(RemoteSocket),
              logger ! {message, "~4.16.0B: Connected to ~s~n", [ConnN, RemoteInfo]},
              exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, 0),
              logger ! {message, "~4.16.0B: Finished~n", [ConnN]};
            {error, Reason} ->
              logger ! {message, "~4.16.0B: Unable to connect to ~s:~s (error: ~p)~n",
                       [ConnN, RemoteHost, RemotePort, Reason]}
          end;
      {error, Reason} ->
          logger ! {message, "Socket accept error '~w'~n", [Reason]}
    end.

exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN) ->
    receive
        {tcp, RemoteSocket, Bin} ->
            logger ! {received, ConnN, Bin, RemoteInfo, PacketN},
            gen_tcp:send(LocalSocket, Bin),
            logger ! {sent, ConnN, LocalInfo, PacketN},
            exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN + 1);
        {tcp, LocalSocket, Bin} ->
            logger ! {received, ConnN, Bin, LocalInfo, PacketN},
            gen_tcp:send(RemoteSocket, Bin),
            logger ! {sent, ConnN, RemoteInfo, PacketN},
            exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN + 1);
        {tcp_closed, RemoteSocket} ->
            logger ! {message, "~4.16.0B: Disconnected from ~s~n", [ConnN, RemoteInfo]};
        {tcp_closed, LocalSocket} ->
            logger ! {message, "~4.16.0B: Disconnected from ~s~n", [ConnN, LocalInfo]}
    end.

logger() ->
    receive
        {received, Pid, Msg, From, PacketN} ->
            io:format("~4.16.0B: Received (#~p) ~p byte(s) from ~s~n",
                      [Pid, PacketN, byte_size(Msg), From]),
            dump_bin(Pid, Msg),
            logger();
        {sent, Pid, ToSocket, PacketN} ->
            io:format("~4.16.0B: Sent (#~p) to ~s~n", [Pid, PacketN, ToSocket]),
            logger();
        {message, Format, Args} ->
            io:format(Format, Args),
            logger()
    end.

dump_list(Prefix, L, Offset) ->
    {H, T} = lists:split(lists:min([?WIDTH, length(L)]), L),
    io:format("~4.16.0B: ", [Prefix]),
    io:format("~4.16.0B: ", [Offset]),
    io:format("~-*s| ", [?WIDTH * 3, dump_numbers(H)]),
    io:format("~-*s", [?WIDTH, dump_chars(H)]),
    io:format("~n", []),
    if length(T) > 0 -> dump_list(Prefix, T, Offset + 16); true -> [] end.

dump_numbers(L) when (is_list(L)) ->
    lists:flatten([io_lib:format("~2.16.0B ", [X]) || X <- L]).

dump_chars(L) ->
    lists:map(fun(X) ->
                if X >= 32 andalso X < 128 -> X;
                   true -> $.
                end
              end, L).

dump_bin(Prefix, Bin) ->
    dump_list(Prefix, binary_to_list(Bin), 0).

В работе может выводить примерно следующее:

alexander:erlang/>./tcp_proxy.sh 50000 pop.yandex.ru 110
Start listening on port 50000 and forwarding data to pop.yandex.ru:110
Listener socket is started 0.0.0.0:50000
0000: Incoming connection from 127.0.0.1:51402
0000: Connected to 213.180.204.37:110
0000: Received (#0) 38 byte(s) from 213.180.204.37:110
0000: 0000: 2B 4F 4B 20 50 4F 50 20 59 61 21 20 76 31 2E 30 | +OK POP Ya! v1.0
0000: 0010: 2E 30 6E 61 40 32 35 20 67 55 62 44 54 51 64 5A | .0na@25 gUbDTQdZ
0000: 0020: 6D 6D 49 31 0D 0A                               | mmI1..
0000: Sent (#0) to 127.0.0.1:51402
0000: Received (#1) 11 byte(s) from 127.0.0.1:51402
0000: 0000: 55 53 45 52 20 74 65 73 74 0D 0A                | USER test..
0000: Sent (#1) to 213.180.204.37:110
0000: Received (#2) 23 byte(s) from 213.180.204.37:110
0000: 0000: 2B 4F 4B 20 70 61 73 73 77 6F 72 64 2C 20 70 6C | +OK password, pl
0000: 0010: 65 61 73 65 2E 0D 0A                            | ease...
0000: Sent (#2) to 127.0.0.1:51402
0000: Received (#3) 11 byte(s) from 127.0.0.1:51402
0000: 0000: 50 41 53 53 20 70 61 73 73 0D 0A                | PASS pass..
0000: Sent (#3) to 213.180.204.37:110
0000: Received (#4) 72 byte(s) from 213.180.204.37:110
0000: 0000: 2D 45 52 52 20 5B 41 55 54 48 5D 20 6C 6F 67 69 | -ERR [AUTH] logi
0000: 0010: 6E 20 66 61 69 6C 75 72 65 20 6F 72 20 50 4F 50 | n failure or POP
0000: 0020: 33 20 64 69 73 61 62 6C 65 64 2C 20 74 72 79 20 | 3 disabled, try
0000: 0030: 6C 61 74 65 72 2E 20 73 63 3D 67 55 62 44 54 51 | later. sc=gUbDTQ
0000: 0040: 64 5A 6D 6D 49 31 0D 0A                         | dZmmI1..
0000: Sent (#4) to 127.0.0.1:51402
0000: Disconnected from 213.180.204.37:110
0000: Finished
0001: Incoming connection from 127.0.0.1:51405
0001: Connected to 213.180.204.37:110
0001: Received (#0) 38 byte(s) from 213.180.204.37:110
0001: 0000: 2B 4F 4B 20 50 4F 50 20 59 61 21 20 76 31 2E 30 | +OK POP Ya! v1.0
0001: 0010: 2E 30 6E 61 40 33 30 20 70 55 62 41 72 52 33 74 | .0na@30 pUbArR3t
0001: 0020: 6A 65 41 31 0D 0A                               | jeA1..
0001: Sent (#0) to 127.0.0.1:51405
0001: Received (#1) 6 byte(s) from 127.0.0.1:51405
0001: 0000: 51 55 49 54 0D 0A                               | QUIT..
0001: Sent (#1) to 213.180.204.37:110
0001: Received (#2) 20 byte(s) from 213.180.204.37:110
0001: 0000: 2B 4F 4B 20 73 68 75 74 74 69 6E 67 20 64 6F 77 | +OK shutting dow
0001: 0010: 6E 2E 0D 0A                                     | n...
0001: Sent (#2) to 127.0.0.1:51405
0001: Disconnected from 213.180.204.37:110
0001: Finished

Вывод: Эрланг - прекрасный вариант для начала функциональной карьеры.

Посты по теме:

вторник, 19 апреля 2011 г.

re2c - компилятор регулярных выражений

Задача выделения из потока символов определенных лексем является весьма распространенной. Часто ее решают с помощью лексических анализаторов, конфигурируемых регулярными выражениями. Многие анализаторы построены по принципу генерации программного кода, который в свою очередь реализует логику регулярных выражений. Фактически, это компиляция языка регулярных выражений в код языка программирования.

Например, flex - это один из таких анализаторов. Старый, но проверенный годами.

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

Но вчера наткнулся на интересный проект - re2c. По сути, на этой штуке можно писать лексические анализаторы прямо на коленке за несколько минут.

Сразу рассмотрим пример.

Допустим, вам нужно из строки выделять некоторые команды, целые и дробные числа. Можно расчехлить flex, а можно написать так:

#include <stdio.h>
#include <stdlib.h>

enum {
  CMD, INT, FLOAT, SPACE, END
};

int scan(char** p, char** lex)
{
    char* marker;
    if (lex) *lex = *p;
/*!re2c
        re2c:define:YYCTYPE  = "unsigned char";
        re2c:define:YYCURSOR = *p;
        re2c:define:YYMARKER = marker;
        re2c:yyfill:enable   = 0;
        re2c:yych:conversion = 1;
        re2c:indent:top      = 1;
        "GET"|"PUT"|"EXIT" { return CMD; }
        [0-9]+             { return INT; }
        [0-9]+ '.' [0-9]*  { return FLOAT; }
        [ \t]+             { return SPACE; }
        [^]                { return END; }
*/
}

int main(int argc, char* argv[]) {
  char *p, *last;
  int token;
  if (argc < 2) return 1;

  p = argv[1];
  while ((token = scan(&p, &last)) != END) {
    int sz = p - last;
    switch (token) {
      case CMD: printf("Command: '%.*s'\n", sz, last); break;
      case INT: printf("Number: '%.*s'\n", sz, last); break;
      case FLOAT: printf("Float: '%.*s'\n", sz, last); break;
    }
  }

  return 0;
}

И все!

Понятно, что вся магия происходит в функции "scan()" между строками, ограниченных комментариями "/*!re2c" и "*/".

Итак, re2c - это компилятор регулярных выражений, который встраивает код прямо в текст программы.

Если прогнать наш исходник через re2c:

re2c.exe -is test.re2c >test.c

То получим вот такое:

/* Generated by re2c 0.13.5 on Tue Apr 19 21:08:57 2011 */
#include <stdio.h>
#include <stdlib.h>

enum {
  CMD, INT, FLOAT, SPACE, END
};

int scan(char** p, char** lex)
{
    char* marker;
    if (lex) *lex = *p;

    {
        unsigned char yych;

        yych = (unsigned char)**p;
        if (yych <= '9') {
            if (yych <= 0x1F) {
                if (yych == '\t') goto yy8;
                goto yy10;
            } else {
                if (yych <= ' ') goto yy8;
                if (yych <= '/') goto yy10;
                goto yy6;
            }
        } else {
            if (yych <= 'F') {
                if (yych == 'E') goto yy5;
                goto yy10;
            } else {
                if (yych <= 'G') goto yy2;
                if (yych == 'P') goto yy4;
                goto yy10;
            }
        }
yy2:
        yych = (unsigned char)*(marker = ++*p);
        if (yych == 'E') goto yy24;
yy3:
        { return END; }
yy4:
        yych = (unsigned char)*(marker = ++*p);
        if (yych == 'U') goto yy23;
        goto yy3;
yy5:
        yych = (unsigned char)*(marker = ++*p);
        if (yych == 'X') goto yy18;
        goto yy3;
yy6:
        ++*p;
        if ((yych = (unsigned char)**p) == '.') goto yy13;
        if (yych <= '/') goto yy7;
        if (yych <= '9') goto yy16;
yy7:
        { return INT; }
yy8:
        ++*p;
        yych = (unsigned char)**p;
        goto yy12;
yy9:
        { return SPACE; }
yy10:
        yych = (unsigned char)*++*p;
        goto yy3;
yy11:
        ++*p;
        yych = (unsigned char)**p;
yy12:
        if (yych == '\t') goto yy11;
        if (yych == ' ') goto yy11;
        goto yy9;
yy13:
        ++*p;
        yych = (unsigned char)**p;
        if (yych <= '/') goto yy15;
        if (yych <= '9') goto yy13;
yy15:
        { return FLOAT; }
yy16:
        ++*p;
        yych = (unsigned char)**p;
        if (yych == '.') goto yy13;
        if (yych <= '/') goto yy7;
        if (yych <= '9') goto yy16;
        goto yy7;
yy18:
        yych = (unsigned char)*++*p;
        if (yych == 'I') goto yy20;
yy19:
        *p = marker;
        goto yy3;
yy20:
        yych = (unsigned char)*++*p;
        if (yych != 'T') goto yy19;
yy21:
        ++*p;
        { return CMD; }
yy23:
        yych = (unsigned char)*++*p;
        if (yych == 'T') goto yy21;
        goto yy19;
yy24:
        ++*p;
        if ((yych = (unsigned char)**p) == 'T') goto yy21;
        goto yy19;
    }

}

int main(int argc, char* argv[]) {
  char *p, *last;
  int token;
  if (argc < 2) return 1;

  p = argv[1];
  while ((token = scan(&p, &last)) != END) {
    int sz = p - last;
    switch (token) {
      case CMD: printf("Command: '%.*s'\n", sz, last); break;
      case INT: printf("Number: '%.*s'\n", sz, last); break;
      case FLOAT: printf("Float: '%.*s'\n", sz, last); break;
    }
  }

  return 0;
}

Страшно? Да, код не для ручной правки, но это и не требуется.

Компилируем:

re2c.exe -is test.re2c >test.c && cl test.c

Запускаем:

test "GET 123.0 12344 PUT 10."

Результат:

Command: 'GET'
Float: '123.0'
Number: '12344'
Command: 'PUT'
Float: '10.'

Как говориться, быстро, дешево и сердито. Чтобы полностью овладеть re2c надо прочитать одну и единственную страничку документации.

Кстати, простота работы с re2c не означает, что на нем нельзя делать сложных анализаторов. В дистрибутиве есть примеры для грамматики токенов языков C и Rexx.

Если поиграться с флагами re2c, то можно вместо "if/else" генерировать код на основе "switch/case". Выбор стоит сделать на основе понимания, какой код ваш компилятор лучше оптимизирует.

Как я понимаю, анализатор, сгенерированный re2c должен быть весьма быстр, даже очень быстр. Хотя было бы интересно померить его против того же flex'а, ANTLR'а или Spirit'a.

Посты почти по теме:

среда, 23 февраля 2011 г.

MicroXP - легковесная верcия Windows XP

Задача - есть необходимость подключаться к некоторой удаленной сети по RDP. Для осуществления такого соединения на машине должен быть установлен специальный софт, создающий закрытый канал между клиентской машиной и удаленной сетью. После установки этого софта можно уже запускать не только клиент RDP (mstsc.exe), но и другие разрешенные программы (telnet/ssh, ftp, radmin и т.д.), которым нужен доступ в удаленную сеть.

И все бы ничего, когда клиентская машина работает на Windows. В этом случае ты просто заходишь на страничку, логин/пароль, прямо со страницы стартует activex, который ставит и запускает все что нужно, и можно работать.

Сложности начинаются, когда клиентская машина - это не Windows. Конечно, клиенты RDP уже есть не только под Windows, но это не особо помогает, так как без того особого софта для туннеля ничего работать все равно не будет. И как назло, нет возможности докупить серверную часть этого VPN'а для поддержки клиентов не только под Windows.

Что люди, пользователи Linux и Mac, вынуждены делать? Ставить Windows на виртуальной машине и запускать RDP в ней. Хорошее решение, но с одним недостатком. Виртуальная машина с обычными виндами расползается на пару гигов как минимум, и загружается не очень быстро.

Я немного погуглил, и наткнулся на MicroXP. Это сильно урезанная версия XP SP3. Дистрибутив занимает ~100 мегов, а в установленном виде ~250. Под VirtualBox'ом на Mac Air установка занимает минут пять, а уже установленная система статует секунд за 10. Виртуальная машина минимальна - динамический диск на 300 мегов (реально будет ~250) и 64 мегов памяти.

После установки MicroXP надо доустановить Virtual Box Guest Additions (~200кб) для возможности расшаривать каталоги и не мучаться с мышкой, и добавить клиента RDP (его, как раз, можно перенести через расшареный каталог).

В общем, если вам нужны минималистические ультрабыстрые XP, то MicroXP отличный кандидат.

P.S. Вопросы легальности этой сборки XP оставим на бортом. Поэтому ссылки ведут на торренты. Кстати, не ходите на сайт microxp.org. Это fake, для сбора почтовых адресов, видимо.

среда, 26 января 2011 г.

Fossil - контроль версий, баг-трекер и wiki в одном флаконе

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

Лично я люблю git для UNIX и mercurial для Windows. Каждая система имеет пачку удобных хостингов, как говорится, выбирай на вкус.

И вот у меня образовался закрытый проект, который мало того, что активно развивается, так еще и накапливает баг-репорты, приправленные файлами отчетов и картинками, и требует ведения документации.

Начал я его вести в mercurial, но когда начал утопать в письмах и документации, то осознал необходимость баг-трекера и wiki. Настраивать все это локально (на публичные готовые хостинги выложить не могу) как-то лень. И тут я вспомнил по fossil.

Fossil - это распределенный контроль версий, баг-трекер и wiki в одном флаконе. Более того, его автор -- ни кто иной, как автор SQLite, борец за минимализм, простоту и надежность. Как и в случае с небезызвестной базой данных, которую кто уже только не использует, все, что вам нужно - это один единственный файл -- fossil[.exe].

Для командной строки - это просто SCM, а будучи запущенной с параметром "ui", превращается в локальный веб-сервер, в котором есть "морда" для просмотра репозитория, баг-трекера и wiki. Более того, все данные живут также в одном единственном файле-репозитории, который по сути является SQLite-базой. Для переноса его в другое место, другую операционную систему или резервного копирования, нужно просто скопировать один файл.

fossil умеет импортировать и экспортировать в git, поэтому я сначала перегнал существующий репозиторий из mercurial в git, а потом импортировал из git в fossil.

В целом, fossil хорош. Вылизанный и минималистичный. Говорят, что из-за использования SQLite в качестве хранилища, с одной стороны получаешь надежность и транзакционность любых изменений (понятно, что хоть остальные системы работают просто с файлами, у них с целостностью тоже все в порядке), но с другой стороны, по скорости может радикально проигрывать git или mercurial на больших проектах. Но для небольших "домашних", но секретных проектов - сложно представить удобнее утилиты.

Даже в рамках компании, можно личный проект в два счета превратить в общий, просто запустив fossil в режиме сервера и дав коллегам его адрес. Домашняя страничка fossil по сути является сервером, работающим на fossil (там можно увидеть живой трекер и wiki). Не самое плохое доказательство уверенности автора fossil'а в своем детище.

Да, лицензия у fossil, конечно, BSD.

Итак, для дома для семьи -- очень удобно.

вторник, 28 декабря 2010 г.

Opengrok

Исходники – это основной источник информации для программиста, особенно если в компании (или проекте) размер кодовой базы переваливает за 200-300 тысяч строк.

Чтобы найти драгоценное зерно в такой куче, нужна правильная утилита для просмотра, а главное, поиска (типа «дай-ка я гляну, как народ эту функцию вызывает?» или «как там правильно создать экземпляр этого класса?» и т.д.).

Большинство систем контроля версий имеют веб-интерфейс для подобных целей. Также есть независимые системы, и одна из них называется Opengrok.


Это система с открытым кодом под лицензией CDDL. Может индексировать репозитории почти всех основных систем контроля версий, а для некоторых понимает и историю файлов. Множество критериев поиска. Крайне полезно, что можно одновременно подключать для индексирования несколько репозиториев, причем от разных VCS. Естественно, при просмотре исходник представляется гипертекстовым документом, через который можно двигаться дальше.

Кстати, можно вживую пощупать Opengrok на исходниках OpenSolaris’а.

В общем, лично у меня крайне положительный опыт работы с этой системой на весьма значительной по размеру, разнообразию языков и подключенных одновременно VCS кодовой базе. Всячески рекомендую.

P.S. Я как-то в целом давно не писал про всякие организационные примочки, облегчающие работу программиста.

Поэтому, небольшой списочек из старенького, но все еще актуального:

понедельник, 24 мая 2010 г.

Dropbox

Как-то осознал, что реально подсел на Dropbox.

Как это реально выглядит: просто у меня есть папка, которая автоматически синхронизируется через интернет на любом компьютере, где я залогинен в Dropbox. Не надо ничего вручную копировать и синхронизировать. Ты просто работаешь с локальными файлами, а все в фоне уходит на сервер и оттуда раздается на другие компьютеры.

Все файлы имеют историю ревизий, и их можно делать выборочно публичными. Почти как Google Docs, только есть автоматическое синхронизирование с локальными файлами.

Недавно вышел официальный клиент Dropbox для Android, так что теперь файлы еще и доступны прямо с телефона. Захотел в метро исходничек глянуть или pdf-ку - пожалуйста.

Конечно, класть в эту папку более менее конфиденциальные данные было бы глупо, хотя Dropbox гарантирует их сохранность, но для рабочих документов и исходников - это самое оно. Они и так у меня почти все лежат на Google Code.

суббота, 28 ноября 2009 г.

Электронная записная книга TiddlyWiki

Лично я очень люблю задавать вопросы, но я не люблю задавать один и тот же вопрос дважды. А иногда случается, что неожиданно сваливается большой поток новой информации (например, начинается проект), и голове все не удержать. Естественно, обычный бумажный блокнот позволяет производить начальную фиксацию информации, но когда объем записей, к которым необходимо часто возвращаться, переваливает за 8-10 исписанных страниц, то начинается каша.

Какой логичный выход? Использовать для записей компьютер.

Долгое время я использовал просто текстовый файл, например в Ворде. Например, начался проект или новая работа — сразу валит поток фактов, которые я в этот файл записываю. Обычно где-то раз в неделю я вбиваю записи из блокнота и тонны бумажек.

Так как информации много, то ее хочется как-то логично организовывать - главы, разделы, подразделы и т.д. Но со временем я заметил, что основной способ навигации впоследствии - это ни разу не оглавление, а просто поиск по словам. Например, мне нужно вспомнить, что мне там объясняли на тему как вбивать расходы или где смотреть отчеты по ночным сборкам. Я просто нажимаю CTRL-F и ввожу слово "расходы" или "ночные". Как говориться, "Don't sort. Search!"

Наличие всего в одном файле упрощает техническую сторону вопроса - хочешь в Ворде работай, хочешь в Vi. И бэкапить удобно или в контроль версий засунуть.

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

Что это такое? Поясню кратко, для тех кто не слышал. TiddlyWiki - это Wiki движок, который представляет собой один единственный html-файл. Как это работает: вы просто открываете этот файл в браузере, и движок на JavaScript позволяет создавать записи, редактировать их, ссылать записи друг на друга, удалять и, что важно, искать Но еще этот движок умеет делать самое интересное - сохранять все, что вы ввели в этот же файл. То есть когда вы откроете этот html-файл завтра, в нем будет все, что вы ввели раньше.

Я обычно работаю так: есть закладка в браузере, которая открывает локальный файл с TiddlyWiki. А так как браузера практически всегда открыт - запуск занимает мгновения. Так как этот Wiki, то доступны все удобства Wiki-разметки и взаимных ссылок (кстати, сам сайт www.tiddlywiki.com построен на TiddlyWiki). Создание новой заметки (tiddler'а) происходит мгновенно. Для навигации можно сделать удобное меню слева, но просто пользуюсь поиском. Когда данных много - это единственный способ что-то найти.

Что лично мне нравится TiddlyWiki-подходе в целом:

  • Нужен только браузер с установленым плагином Java и малюсенький файл "Saver.jar", который поставляется вместе с TiddlyWiki, и который надо просто положить в тот же каталог. Поэтому с wiki-файлом можно работать хоть в Windows, хоть в UNIX, хоть на Маке. Я лично проверял в Chrome, Firefox и IE).
  • Файл можно выложить на web-сервер, например в интранете, и таким образом публиковать записи. Конечно, для просматривающих его через web файл будет только для чтения.
  • Так как файл по сути своей текстовый (обычный html), то в случае чего, можно выдрать из него данные простым текстовым редактором, хотя у меня такой необходимости еще не было.
  • Движок TiddlyWiki при каждом самосохранении умеет делать копию текущего состояния. Я эти файлы обычно архивирую раз в неделю, и solid-архивация в один архив позволяет хранить всю историю с минимальным увеличением архива при каждом новом сохранении. И всегда можно вернуться к определенной старой версии.
  • И главное: удобный поиск!
Небольшой трюк для пользователей Хрома. Для нормальной работы TiddlyWiki нужно, чтобы были включены cookie. По умолчанию в Хроме при работе с локальными файлами cookie выключены, поэтому Хром следует запускать с ключом --enable-file-cookies.

Не спрашивайте второй раз одно и то же. Лучше запишите ответ в первый раз, а спросите что-нибудь новое.

четверг, 29 октября 2009 г.

codepad.org

Открыл для себя codepad.org, и начал пользоваться с завидной регулярностью, когда надо быстренько что-нибудь попробовать - откомпилировать и запустить, а потом еще и показать кому-нибудь.

вторник, 13 октября 2009 г.

Google C++ Mocking Framework для начинающих

В рамках проекта популяризации культуры разработки софта с активным использованием тестирования выложил перевод Google C++ Mocking for Dummies на русский язык - Google C++ Mocking Framework для начинающих.

Использование Mock-объектов является очень интересной темой. И владение ей позволяет перевести unit-тестирование на принципиально иной уровень.

Как рассказано в статье, языки программирования типа Python или Java благодаря встроенному механизму Reflection позволяют строить Mock-объекты почти автоматически. С++ не дает такой роскоши, но гугловцы проделали отличную работу, создав Google Mock. Практически все, что можно как-то упростить или автоматизировать в плане mock-дел в С++, сделано и сделано добротно.

Соглашусь, что поначалу вся эта тема с Mock-объектами выглядит несколько громоздко и сложновато, но тут как с эргономичной клавиатурой - надо сначала привыкнуть, а потом все окупится сполна.

Посты по теме:

суббота, 3 октября 2009 г.

Google C++ Testing Framework 1.4.0

Вчера гугловцы анонсировали новую версию моей любимой библиотеки тестирования Google C++ Testing Framework - 1.4.0.

Что новенького?

Одна из главных новых возможностей - это "The event listener API". А попросту говоря, возможность полностью контролировать процесс печати результатов тестирования. Это позволяет формировать отчеты по тестированию в нужном формате без изменения кода библиотеки.

Например, стандартный вывод при выполнении элементарного теста (файл runner.cpp):
#include "gtest/gtest.h"

TEST(One, Simple) {
EXPECT_EQ(1, 2);
}

int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
будет таким:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from One
[ RUN ] One.Simple
runner.cpp(4): error: Value of: 2
Expected: 1
[ FAILED ] One.Simple (15 ms)
[----------] 1 test from One (15 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (31 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] One.Simple

1 FAILED TEST
Для задания иного формата вывода нужно реализовать свой event listener (назовем его сервис печати). Например (файл runner.cpp):
#include "gtest/gtest.h"

using namespace ::testing;

// Данный класс заменит стандартный сервис печати.
class LaconicPrinter : public ::testing::EmptyTestEventListener {
// Вызывается до начала работы теста.
virtual void OnTestStart(const TestInfo& test_info) {
printf("*** Test %s.%s starting.\n",
test_info.test_case_name(), test_info.name());
}

// Вызывается при срабатывании какого-либо утверждения или явного вызова
// функции SUCCESS().
virtual void OnTestPartResult(const TestPartResult& test_part_result) {
printf("%s in %s:%d\n%s\n",
test_part_result.failed() ? "*** Failure" : "Success",
test_part_result.file_name(),
test_part_result.line_number(),
test_part_result.summary());
}

// Вызывается после выполнения теста.
virtual void OnTestEnd(const TestInfo& test_info) {
printf("*** Test %s.%s ending.\n",
test_info.test_case_name(), test_info.name());
}
};

TEST(One, Simple) {
EXPECT_EQ(1, 2);
}

int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);

// Получаем ссылку на список сервисов печати.
::testing::TestEventListeners& listeners =
::testing::UnitTest::GetInstance()->listeners();

// Удаляем стандартный сервис печати.
delete listeners.Release(listeners.default_result_printer());
// Добавляем наш сервис в список. Google Test самостоятельно удалит этот объект.
listeners.Append(new LaconicPrinter);

return RUN_ALL_TESTS();
}
Теперь отчет по работе тестов будет выглядеть так:
*** Test One.Simple starting.
*** Failure in runner.cpp:31
Value of: 2
Expected: 1
*** Test One.Simple ending.
Необходимо отметить, что одновременно может быть зарегистрировано несколько сервисов печати. Но в этом случае их выводы могут смешиваться и превращаться в кашу. Для избежания этого мы принудительно удаляем стандартный сервис печати, чтобы его вывод не мешал нашему.

Полностью интерфейс сервиса печати выглядит так:
class EmptyTestEventListener : public TestEventListener {
public:
// Вызывается при начале прогона всех тестов.
virtual void OnTestProgramStart(const UnitTest& unit_test);
// Вызывается при начале очередной итерации тестирования. Google Test
// позволяет управлять количеством итерации при прогоне тестов.
virtual void OnTestIterationStart(const UnitTest& unit_test, int iteration);
// Вызывается до функции Environment::SetUp(), устанавливающей необходимое
// окружение для работы всех тестов.
virtual void OnEnvironmentsSetUpStart(const UnitTest& unit_test);
// Вызывается после функции Environment::SetUp(), устанавливающей необходимое
// окружение для работы всех тестов.
virtual void OnEnvironmentsSetUpEnd(const UnitTest& unit_test);
// Вызывается при начале прогона группы тестов (у которых первый параметр
// макроса TEST/TEST_F одинаковый).
virtual void OnTestCaseStart(const TestCase& test_case);
// Вызывается при начале работы теста.
virtual void OnTestStart(const TestInfo& test_info);
// Вызывается при срабатывании утверждения в тесте или явного вызова
// функции SUCCESS().
virtual void OnTestPartResult(const TestPartResult& test_part_result);
// Вызывается после завершения работы теста.
virtual void OnTestEnd(const TestInfo& test_info);
// Вызывается после прогона группы тестов.
virtual void OnTestCaseEnd(const TestCase& test_case);
// Вызывается до функции Environment::TearDown(), производящей освобождение
// ресурсов, занятых Environment::StartUp().
virtual void OnEnvironmentsTearDownStart(const UnitTest& unit_test);
// Вызывается после функции Environment::TearDown().
virtual void OnEnvironmentsTearDownEnd(const UnitTest& unit_test);
// Вызывается после очередной итерации тестирования.
virtual void OnTestIterationEnd(const UnitTest& unit_test, int iteration);
// Вызывается после прогона всех тестов.
virtual void OnTestProgramEnd(const UnitTest& unit_test);
};
Также из значимого можно отметить новый ключ командной строки --gtest_shuffle, позволяющий запускать тесты в случайном порядке. Ключом --gtest_random_seed=SEED можно "управлять" случайностью этого порядка. Если SEED равен 0, то случайность будет действительно случайной, так как случайная последовательность будет определяться текущим временем.

Что приятно, теперь формат XML файлов, генерируемых при использовании ключа --gtest_output, полностью совместим с форматом JUnit. Это значит, что, например, система автоматической сборки, тестирования и интеграции Hudson теперь понимает отчеты Google Test без дополнительный конвертации.

Также теперь при работе в Visual Studio сообщения о сбойных тестах выводятся прямо в окно "Output", что позволяет, кликая на них, сразу находить строки, где сбоят тесты. Здорово, что данная фича основана на моем коде.

Еще, теперь время работы тестов печатается всегда, по умолчанию, то есть опция --gtest_print_time будто бы всегда включена.

Есть еще несколько незначительных улучшений:
  • поддержка CodeGear Studio
  • собственная реализация tuple для независимости от boost при использовании Combine()
  • множество улучшений для совместимости с Solaris, Windows Mobile и некоторыми другими платформами.
Радость, одно слово.

Я перестал что-то писать что-либо на C++ без тестов, а Google Test делает этот процесс простым и легким.

Я уже обновился до версии 1.4.0, а вы?

Ссылки по теме:

пятница, 4 сентября 2009 г.

Мультипотоковый отладчик TCP/IP соединений

Трассировка данных, передаваемых по TCP/IP, является весьма частой задачей при разработке сетевых приложений, особенно низкого уровня.

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

Несколько лет назад первые версии моей утилиты были на PHP, но текущая версия переписана на Питоне.

Итак, pyspy.

Исходник небольшой, а, как мне кажется, разглядывание исходников должно радовать большинство программистов, особенно, если есть что покритиковать, поэтому приведу его прямо здесь (см. ниже).

Ни разу не претендую на оптимальность или крутизну использования Питона, поэтому принимаю любую критику.

Основные особенности и возможности:
  • программа "слушает" на указанном порту и перенаправляет траффик на указанные адрес и порт
  • умеет сохранять лог в файл
  • программа является многопотоковой, то есть может принимать сразу несколько входящих содинений
  • механизм записи лога работает также в отдельном потоке, ускоряет работу
Пример использования (для работы по Windows есть специальный скрипт pyspy.cmd).

Запускаем сервер:
pyspy.cmd -a 10.44.5.138 -p 5467 -l 9999 -L trace.log
Запускаем клиента:
telnet localhost 9999
и вводим GET / HTTP/1.0<ENTER><ENTER>

В файле лога и в консоли получаем вот такое:
0000: Listen at port 9999, remote host ('10.44.5.138', 5467)
0000: Connection accepted from ('127.0.0.1', 15223), thread 1 launched
0001: Thread started
0001: Connecting to ('10.44.5.138', 5467)...
0001: Remote host: ('127.0.0.1', 15223)
0001: Recevied from ('127.0.0.1', 15223) (1)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 47 | G
0001: Sent to ('10.44.5.138', 5467) (1)
0001: Recevied from ('127.0.0.1', 15223) (13)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 45 54 20 2F 20 48 54 54 50 2F 31 2E 30 | ET / HTTP/1.0
0001: Sent to ('10.44.5.138', 5467) (13)
0001: Recevied from ('127.0.0.1', 15223) (2)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 0D 0A | ..
0001: Sent to ('10.44.5.138', 5467) (2)
0001: Recevied from ('127.0.0.1', 15223) (2)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 0D 0A | ..
0001: Sent to ('10.44.5.138', 5467) (2)
0001: Recevied from ('10.44.5.138', 5467) (379)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 48 54 54 50 2F 31 2E 31 20 33 30 32 20 46 6F 75 | HTTP/1.1 302 Fou
0001: 0010: 6E 64 0D 0A 44 61 74 65 3A 20 46 72 69 2C 20 30 | nd..Date: Fri, 0
0001: 0020: 34 20 53 65 70 20 32 30 30 39 20 30 38 3A 35 33 | 4 Sep 2009 08:53
0001: 0030: 3A 30 33 20 47 4D 54 0D 0A 53 65 72 76 65 72 3A | :03 GMT..Server:
0001: 0040: 20 41 70 61 63 68 65 0D 0A 50 72 61 67 6D 61 3A | Apache..Pragma:
0001: 0050: 20 6E 6F 2D 63 61 63 68 65 0D 0A 45 78 70 69 72 | no-cache..Expir
0001: 0060: 65 73 3A 20 46 72 69 2C 20 30 31 20 4A 61 6E 20 | es: Fri, 01 Jan
0001: 0070: 31 39 39 39 20 30 30 3A 30 30 3A 30 30 20 47 4D | 1999 00:00:00 GM
0001: 0080: 54 0D 0A 43 61 63 68 65 2D 63 6F 6E 74 72 6F 6C | T..Cache-control
0001: 0090: 3A 20 6E 6F 2D 63 61 63 68 65 2C 20 6E 6F 2D 63 | : no-cache, no-c
0001: 00A0: 61 63 68 65 3D 22 53 65 74 2D 43 6F 6F 6B 69 65 | ache="Set-Cookie
0001: 00B0: 22 2C 20 70 72 69 76 61 74 65 0D 0A 4C 6F 63 61 | ", private..Loca
...
[обрезано]
...
0001: 0100: 76 3D 31 0D 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A | v=1..Connection:
0001: 0110: 20 63 6C 6F 73 65 0D 0A 43 6F 6E 74 65 6E 74 2D | close..Content-
0001: 0120: 54 79 70 65 3A 20 74 65 78 74 2F 68 74 6D 6C 0D | Type: text/html.
0001: 0130: 0A 0D 0A 52 65 64 69 72 65 63 74 20 70 61 67 65 | ...Redirect page
0001: 0140: 3C 62 72 3E 3C 62 72 3E 0A 54 68 65 72 65 20 69 | <br><br>.There i
0001: 0150: 73 20 6E 6F 74 68 69 6E 67 20 74 6F 20 73 65 65 | s nothing to see
0001: 0160: 20 68 65 72 65 2C 20 70 6C 65 61 73 65 20 6D 6F | here, please mo
0001: 0170: 76 65 20 61 6C 6F 6E 67 2E 2E 2E | ve along...
0001: Sent to ('127.0.0.1', 15223) (379)
0001: Connection reset by ('10.44.5.138', 5467)
0001: Connection closed
Теперь, собственно, исходник:
#!/usr/bin/python

import socket, string, threading, os, select, sys, time, getopt
from sys import argv

def usage():
name = os.path.basename(argv[0])
print "usage:", name, "-l listen_port -a host -p port [-L file] [-c] [-h?]"
print " -a host - address/host to connect"
print " -p port - remote port to connect"
print " -l listen_port - local port to listen"
print " -L file - log file"
print " -c - supress console output"
print " -h or -? - this help"
print " -v - version"
sys.exit(1)

PORT = False
REMOTE_HOST = REMOTE_PORT = False

CONSOLE = True
LOGFILE = False

try:
opts, args = getopt.getopt(argv[1:], "l:a:p:L:ch?v")

for opt in opts:
opt, val = opt
if opt == "-l":
PORT = int(val)
elif opt == "-a":
REMOTE_HOST = val
elif opt == "-p":
REMOTE_PORT = int(val)
elif opt == "-L":
LOGFILE = val
elif opt == "-c":
CONSOLE = False
elif opt == "-?" or opt == "-h":
usage()
elif opt == "-v":
print "Python TCP/IP Spy Version 1.01 Copyright (c) 2009 by Alexander Demin"
sys.exit(1)
else:
usage()

if not PORT:
raise StandardError, "listen port is not given"

if not REMOTE_HOST:
raise StandardError, "remote host is not given"

if not REMOTE_PORT:
raise StandardError, "remote port is not given"

except Exception, e:
print "error:", e, "\n"
usage()

# Remote host
REMOTE = (REMOTE_HOST, REMOTE_PORT)

# Create logging contitional variable
log_cond = threading.Condition()

queue = []

def logger():
global queue
while 1:
log_cond.acquire()

while len(queue) == 0:
log_cond.wait()

if LOGFILE:
try:
logfile = open(LOGFILE, "a+")
logfile.writelines(map(lambda x: x+"\n", queue))
logfile.close()
except: pass

if CONSOLE:
for line in queue:
print line

queue = []
log_cond.release()

# Thread safe logger
def log(thread, msg):
if CONSOLE or LOGFILE:
log_cond.acquire()
queue.append("%04d: %s" % (thread, msg))
log_cond.notify()
log_cond.release()

def printable(ch):
return (int(ch < 32) and '.') or (int(ch >= 32) and chr(ch))

# Pre-build a printable characters map
printable_map = [ printable(x) for x in range(256) ]

# Thread safe dumper
def log_dump(thread, msg):

if CONSOLE or LOGFILE:
log_cond.acquire()

width = 16

header = reduce(lambda x, y: x + ("%02X-" % y), range(width), "")[0:-1]
queue.append("%04d: ----: %s" % (thread, header))
queue.append("%04d: %s" % (thread, '-' * width * 3))

i = 0
while 1:
line = msg[i:i+width]
if len(line) == 0: break
dump = reduce(lambda x, y: x + ("%02X " % ord(y)), line, "")
char = reduce(lambda x, y: x + printable_map[ord(y)], line, "")
queue.append("%04X: %04X: %-*s| %-*s" % (thread, i, width*3, dump, width, char))
i = i + width

log_cond.notify()
log_cond.release()

# Spy thread
def spy_thread(local, addr, thread_id):
log(thread_id, "Thread started")

try:
log(thread_id, "Connecting to %s..." % str(REMOTE))
remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote.connect(REMOTE)
except Exception, e:
log(thread_id, "Unable connect to %s -> %s" % (REMOTE, e))
local.close()
return

LOCAL = str(addr)

log(thread_id, "Remote host: " + LOCAL)

try:
running = 1;
while running == 1:

rd, wr, er = select.select([local, remote], [], [local, remote], 3600)

for sock in er:
if sock == local:
log(thread_id, "Connection error from " + LOCAL)
running = 0
if sock == remote:
log(thread_id, "Connection error from " + REMOTE)
running = 0

for sock in rd:
if sock == local:
val = local.recv(1024)
if val:
log(thread_id, "Recevied from %s (%d)" % (LOCAL, len(val)))
log_dump(thread_id, val)
remote.send(val)
log(thread_id, "Sent to %s (%d)" % (REMOTE, len(val)))
else:
log(thread_id, "Connection reset by %s" % LOCAL)
running = 0;

if sock == remote:
val = remote.recv(1024)
if val:
log(thread_id, "Recevied from %s (%d)" % (REMOTE, len(val)))
log_dump(thread_id, val)
local.send(val)
log(thread_id, "Sent to %s (%d)" % (LOCAL, len(val)))
else:
log(thread_id, "Connection reset by %s" % str(REMOTE))
running = 0;

except Exception, e:
log(thread_id, ("Connection terminated: " + str(e)))

remote.close()
local.close()

log(thread_id, "Connection closed")

try:
# Server socket
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(("", PORT))
except Exception, e:
print "error", e
sys.exit(1)

counter = 1

threading.Thread(target=logger, args=[]).start()

log(0, "Listen at port %d, remote host %s" % (PORT, REMOTE))

while 1:
srv.listen(1)
local, addr = srv.accept()
log(0, "Connection accepted from %s, thread %d launched" % (addr, counter))
threading.Thread(target=spy_thread, args=[local, addr, counter]).start()
counter = counter + 1
Лично я постоянно использую этот скрипт на Windows, Linux и Solaris.

Следующий шаг - это переписать все на чистом С в виде одного единственного файла, который можно было бы в течение минуты забросить на любой UNIX или Windows, скомпилить и получить готовую программу. Питон - это конечно здорово, но, например, для AIX или HP-UX Питон является небольшой загвоздкой, которую в пять секунд не решить.

А что стоит у вас на вооружении по этому вопросу?

понедельник, 4 мая 2009 г.

Автоматизация сборки продукта

Лично я убежден на 100%, что сборка более менее серьезного по размерам проекта/продукта должна производиться в командной строке, то есть никаких GUI-сред, в которые надо заходить, нажимать кнопки, смотреть в окна результатов и т.д. Все это здорово на этапе собственно написания кода, тестирования и отладки. Но когда код переходит в стадию “почти закончен”, все должно заканчиваться полностью авторизированной сборкой без вариантов “тут надо кликнуть, тут надо ввести путь, тут надо сделать clean-up и refresh, а то все съедет” и т.д. Прелесть командной строки в том, что вне зависимости с какого ты сегодня будуна, и что твоя голова с утра проходит в дверь только боком, ты набираешь “cd /my/super/project” и затем “make”. После этого ты откидываешься на стул в мыслях о пивасике, а проект тем временем собирается и тестируется. В идеале, конечно, вместо компиляции, ты должен просто скачать свежую автоматизированную ночную сборку, которая уже там, оттестирована и готова к употреблению.

Ладно, это была всем очевидная лирика.

Наш софт представляет собой монструозный симбиоз из С, С++, Java, BASIC (это наш собственный внутренний СУБД-ориентированный язык), Python’а и UNIX-скриптов. Система же сборки основана на GNU Make и по сути является огромным многоуровневым Makefile’ом. Необходимая при сборке логика, которую нельзя реализовать напрямую в GNU Make дополняется UNIX-скриптами и мини утилитками на C, которые компилируются прямо перед запуском. Java части используют Ant. Плагин Ivy используется для подкачки из репозитория двоичных модулей. Лично я против каких-либо двоичных файлов в проекте, и считаю, что в разы удобнее все компилировать из текстового представления (пусть это и дольше по времени), так как текстовики можно сравнивать, в них можно искать и т.д. Конечно, реальная жизнь сложнее, и иногда приходится использовать заранее собранные бинарники (например, OpenSSL, ICU, тучу сторонних jar’ов для Java и т.д.).

Итак, ясно, такая сборка со временем деградирует, становится сложнее, запутаннее, в ней сложно искать ошибки, а тем более узкие места.

Я пытался все перевести на Ant – возможностей много, но все крайне Ява-центричное, расширения надо тоже писать на ней же. Если мы все писали бы на Java, но у нас не тот случай.

Пробовал CMake. Очень неплохо, но обнаружились сложности скрещения с нашим собственным компилятором Бейсика.

Пробовал SCons. Пожалуй, это самая прикольная система. Недаром ее используют в Гугле для реально нетривиальных проектов типа Chrome и Native Client и т.д. По сути Makefile – это программа на Питоне, то есть ограничения на особую логику сборки (запуск тестов, фильтров, сборка документации, публикация результатов на FTP и т.д.) просто отсутствуют. Нужное просто пишется на полноценном языке программирования Питон. Удалось мне даже собрать нормально Питон для AIX и HPUX (с Windows, Linux, Solaris проблем нет вообще). Но и тут получилась ложна гов... дегтя. У меня есть необходимость конвертировать тысячи отчетов по тестам в формат jUnit. Мини утилитка на С, которая писалась на коленке, делает это менее чем за секунду. Все мои попытки на Питоне работали минуты. Получается, что идея опять не чиста, так как нужны опять сопровождающие утилиты, и уже не ясно, зачем что-то менять как оно есть сейчас.

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

 

Посты по теме:

воскресенье, 26 апреля 2009 г.

Система автоматизированной интеграции Hudson

Не секрет, что максимально автоматизированная "ночная" интеграция (сборка, тестирование, архивация) — это залог уверенности в том, что можно в любой момент выдать работоспособный релиз. А это одна из основ гибкого подхода к разработке.

Мы использует для этих нужд Hudson. Написано на Java, управляется через веб-интерфейс.

Несмотря на мою нелюбовь к Java, лично я мирно ужился с этой программой.

Что умеет Hudson, и для чего он нужен?

Hudson — это программа, которая умеет запускать скрипты локально или на удаленных машинах в зависимости от различных условий. Скрипты обычно производят сборку, тестирование, генерацию отчетов и документации, а события — это в основном поступление новых изменений в систему контроля версий.

Для начала, Hudson построен на плагинах, коих много, под разные запросы и желания.

Первый плагин, который нужен был конкретно нам — это плагин для работы с Perforce. Он умеет периодически опрашивать Perforce о наличии новых commit’ов и инициировать некоторые события, например сборку или запуск тестов.

Далее, ключевой момент для нас. Hudson имеет master-slave архитектуру, что позволяет с одного головного компьютера (master) проводить компиляцию, тестирование, архивирование и т.д. на множестве slave-машин. Так как наш софт поддерживается на нескольких разных типах UNIX и на Windows, по подобный подход очень упрощает управление таким зоопарком сборочных машин. Время разворачивания очередного slave’а — несколько минут (копирование slave.jar, запуск “java –jar slave.jar” и прописывание адреса этого slave’а на головной машине).

Еще мы активно используем плагины для посылки извещений по электропочте и для наглядного отображения результатов тестирования из формата jUnit. Наш софт состоит из смеси С, С++ и Java, поэтому пришлось выбрать единый формат представления журналов тестирования. Остановились на формате jUnit.

Каждая задача в Hudson может выполняться как на самом мастере, так и на заданном множестве slave-машин. Также  задача может являться условием запуска другой задачи. Например, если задача компиляции проекта прошла успешно, то инициируется задача тестирования. Естественно, на каждом этапе можно архивировать промежуточные результаты (логи, тестовые файлы и т.д.), к которым можно всегда вернуться позже.

Наш сценарий использования Hudson таков: каждые пять минут Hudson опрашивает Perforce. Если в какой-то ветке появились новые изменения, то запускается “чистая” сборка ветки с новыми кусками кода. Каждая такая сборка снабжается файлом, в котором перечислены изменения по сравнению с предыдущей сборкой (changelog). Если сборка прошла успешно, по запускается набор функциональных и приемочных тестов. Кроме этого каждую ночь делается сборка со всеми изменениями за день. Если тесты прошли успешно, что результат архивируется в виде очередного ночного билда и выкладывается на ftp.

Если какая-то задача оканчивается сбоем (например, компиляция, так как кто-то “сломал” сборку, забыв проверить новый код на другой платформе, или какой-то из тестов не работает), то посылаются извещения ответственным лицам и также виновнику сбоя.

При нашей интенсивности commit’ов крайне редко за пять минут появляются более одного. Чем это удобно? А тем, что если при очередной сборке кто-то сломал функциональный или приемочный тест, сразу выясняется кто и как это сделал. 

В целом, Hudson позволил нам сделать такую универсальную консоль в виде веб-странички, на которой в одном месте сразу видно все, что происходит в разработке, начиная от состояние функциональных и приемочных тестов, отчетов по покрытию и анализу качества кода и заканчивая списком незакрытых инцидентов в каждой ветке продукта.

А теперь реальный пример. В какой-то момент бета-тестеры начали сообщать, что система стала “как-то тормозить”. Точного момента никто не засек, а последняя "нормальная быстрая” сборка, которую эти тестеры имели, была сделана месяц назад. За этот месяц в ветку было внесено полсотни commit’ов. Искать среди них проблемный было бы занятием скучным (откат на определенную ревизию, сборка, тестирование, сравнение и т.д.).

Меня выручил Hudson. Я просмотрел отчеты по прогонам функциональных тестов за этот месяц и буквально сразу обнаружил, что в определенный день тесты сетевой подсистемы стали работать заметно медленнее. Область поиска сразу сузилась до четырех commit’ов сделанных в этот день. И только один из них был в сетевой подсистеме. Автор сего “улучшения” тоже нашелся сразу. Оказывается, человек что-то там оптимизировал в целях ускорения, а вышло наоборот.  Итого, около часа на полное разбирательство в проблеме, включая перебрасыванием электропочтой с участниками инцидента. Я думаю, ручной поиск занял был день-два. 

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

А вы какими программами пользуетесь для автоматической интеграции?

 

Посты по теме:

четверг, 19 марта 2009 г.

Google Test Framework 1.3.0

Сегодня вышла новая версия Google Test Framework1.3.0.

Радостно, что авторы воплотили мою идею, когда вся библиотека собирается всего в два файла: gtest-all.cc и gtest.h. Теперь для этого есть специальный скрипт на Питоне. Распаковываем архив gtest-1.30.zip и запускаем:

python scripts\fuse_gtest_files.py . fuse
После этого во вновь созданном подкаталоге fuse будет находиться "упакованная" версия библиотеки в виде двух файлов gtest/gtest-all.cc и gtest/gtest.h. Моя аналогичная, но ручная сборка для предыдущей версии больше неактуальна.

Опять таки приятно, что включили мой микропатч для возможности установки флагов командной строки прямо в исходниках тестов. Это очень удобно. Например, есть возможность печати времени работы тестов. Но по умолчанию эта функция выключена, и для ее включения надо в командной строке сказать --gtest_print_time. Неудобно постоянно таскать за собой этот ключ. Теперь же можно прямо в тексте тестов, например, в головном модуле, задать этот параметр:

#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
testing::GTEST_FLAG(print_time) = true;
return RUN_ALL_TESTS();
}
Итак, новые возможности версии 1.3.0:
  • поддержка так называемых смертельных тестов для Windows (раньше это работало только под Linux)
  • параметр командной строки --gtest_also_run_disabled_tests для принудительного запуска отключенных тестов
  • возможность распараллеливать запуск тестов на разных машинах
Небольшая программа ниже демонстрируем новые "вкусности" Google Test.

Файл runner.cpp:
#include "gtest/gtest.h"

#include <fstream>
#include <iostream>
#include <cstdlib>

// -------------------------------------------------------

// Данная функция, если файл не существует, печатает сообщение
// об ошибке и завершает программу с ненулевым кодом.
void openfile(const char* name) {
std::ifstream is(name);
if (!is) {
std::cerr << "Unable to open the file" << std::endl;
std::exit(1);
}
}

// Тест для функции openfile().
TEST(OpenFileDeathTest, ExitIfNoFile) {
// Задаем заведомо несуществующий файл и смотрим - завершилась
// ли программа с ненулевым кодом. Также проверяем регулярным
// выражением то, что программа напечатала при выходе.
// Мы ожидаем слово "open" среди остального вывода.
ASSERT_DEATH({ openfile("__nofile__"); }, ".*open.*");
}

// -------------------------------------------------------

// Данная функция должна падать с assert'ом, если делитель
// равен нулю.
int divide(int a, int b) {
assert(b != 0);
return a / b;
}

// Тест для assert'а в функции divide().
TEST(AssertDeathTest, DivideByZero) {
// Задаем нулевой делитель и смотрим - упала или нет.
// Вывод программы при падении не проверяем.
ASSERT_DEATH({ divide(1, 0); }, "");
}

// -------------------------------------------------------

// Данная функция должна при ненулевом коде завершать
// программу, прибавив к заданному коду ошибки 50.
void abandon(int code) {
if (code != 0) std::exit(code + 50);
}

// Тест для функции abandon().
TEST(AbandonDeathTest, ExitCode) {
// Вызываем функцию и смотрим код возврата.
// Вывод программы при выходе не проверяем.
ASSERT_EXIT(abandon(200), testing::ExitedWithCode(250), "");
}

// -------------------------------------------------------

// Заведомо неработающий “сломанный” тест.
// Если имя группы тестов или теста в отдельности предварить
// словом DISABLED_, то тест не будет участвовать с запуске.
// Это удобно, когда какой-то тест сломан, времени на его
// отладку нет, но убирать его из тестирования совсем нельзя.
// В это случае его можно отключить. Google Test при каждом
// запуске будет напоминать, сколько имеется отключенных тестов.
// В процессе же работы над тестом можно запускать программу
// с параметром "--gtest_also_run_disabled_tests", который
// будет проверять также и отключенные тесты.
TEST(BadTest, DISABLED_Test) {
FAIL();
}

// -------------------------------------------------------

int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
// Принудительно печатаем время работы тестов.
testing::GTEST_FLAG(print_time) = true;
return RUN_ALL_TESTS();
}
Компилируем в Visual Studio:
cl /EHsc /I. /Ferunner_vs2008.exe /DWIN32 runner.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 3 tests from 3 test cases.
[----------] Global test environment set-up.
[----------] 1 test from OpenFileDeathTest
[ RUN ] OpenFileDeathTest.ExitIfNoFile
[ OK ] OpenFileDeathTest.ExitIfNoFile (31 ms)
[----------] 1 test from OpenFileDeathTest (31 ms total)

[----------] 1 test from AssertDeathTest
[ RUN ] AssertDeathTest.DivideByZero
[ OK ] AssertDeathTest.DivideByZero (31 ms)
[----------] 1 test from AssertDeathTest (31 ms total)

[----------] 1 test from AbandonDeathTest
[ RUN ] AbandonDeathTest.ExitCode
[ OK ] AbandonDeathTest.ExitCode (32 ms)
[----------] 1 test from AbandonDeathTest (32 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 3 test cases ran. (94 ms total)
[ PASSED ] 3 tests.

YOU HAVE 1 DISABLED TEST
Отлично, все работает. Также не забудем, что у нас таки есть один отключенный тест. Его можно запустить принудительно, использовав ключ --gtest_also_run_disabled_tests:
runner_vs2008.exe --gtest_also_run_disabled_tests
Получим следующее:
[==========] Running 4 tests from 4 test cases.
[----------] Global test environment set-up.
[----------] 1 test from OpenFileDeathTest
[ RUN ] OpenFileDeathTest.ExitIfNoFile
[ OK ] OpenFileDeathTest.ExitIfNoFile (31 ms)
[----------] 1 test from OpenFileDeathTest (31 ms total)

[----------] 1 test from AssertDeathTest
[ RUN ] AssertDeathTest.DivideByZero
[ OK ] AssertDeathTest.DivideByZero (32 ms)
[----------] 1 test from AssertDeathTest (32 ms total)

[----------] 1 test from AbandonDeathTest
[ RUN ] AbandonDeathTest.ExitCode
[ OK ] AbandonDeathTest.ExitCode (31 ms)
[----------] 1 test from AbandonDeathTest (31 ms total)

[----------] 1 test from BadTest
[ RUN ] BadTest.DISABLED_Test
runner.cpp(72): error: Failed
[ FAILED ] BadTest.DISABLED_Test (0 ms)
[----------] 1 test from BadTest (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 4 test cases ran. (94 ms total)
[ PASSED ] 3 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] BadTest.DISABLED_Test

1 FAILED TEST
Под занавес отмечу, что появился еще один новый ключ командной строки --help для печати на экран всех весьма многочисленных параметров Google Test.

Я уже обновился до версии 1.3.0, а вы?