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


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

понедельник, 31 декабря 2012 г.

Apache JMeter: Создаем 500 юзеров на Moodle автоматом

Задача возникла - для целей тестирования нагрузки надо создать 500 временных пользователей  на Moodle и заасайнить их на тренинг.

Ну что начнем.

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

Итак я запустил свой C:\Java\apache-jmeter-2.6\bin\jmeter.bat

После Test Plan -> Add -> Thread Group



После Test Group -> Add -> Logic Control -> Recording Controller



Добавил HTTP Cookie Manager Test Plan -> Add -> Config Element -> HTTP Cookie Manager



Далее WorkBench -> Add -> Non-Test Elements -> HTTP Proxy Server



После связал Recording Cpntroller с Proxy Server (порт 8080 выставил)



После натравил мой Firefox на прокси (на порт 8080 localhost'а)



Запустил Proxy



И зашел в браузере на Moodle. Запись пошла...

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



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

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



Добавил подглядывалку (Add-> listener -> View result tree)



После остановил прокси. Запустил свой сценарий, нажав зеленую кнопочку (или Ctrl-R) и изучил то, что мне вывел мой новый друг View result tree.



Response data я вставил как html файл на рабочий стол и посомтрел что отрендерит браузер



Так я столкнулся с бедой, под названием user session id.



Дело в том, что Moodle после залогинивания передает пользователю ключ, с которым он ходит по страничкам (передает через GET/POST) и так Moodle определяет что можно позволить пользователю, а что нет. И при каждом новом залогинивании это ключ, естественно, другой. Обычно используются куки (для этого и нужен был в прошлой статье Add -> Config Element -> HTTP Cookie Manager), но не в моем случае. А вот как выкусить sessionid и встраивать потом во все линки подскажет умная статья.

Значит ищем то место, после выполнения которого настраничке появится хоть один линк с sesskey - думаю это случится после залогинивания. Как минимум один линк (Logout) на домашне страничке Moodle модержит его.



Мы могли бы добавить процессор непосредственно в тот запрос, в котором он требуется, и тогда (пре)процессор обработает результат предыдущего запроса, найдет там sesskey и вставит его куда надо.

Но так как нам sesskey может понадобиться и в других местах, то я его вынесу на уровень выше - сразу для всех страниц. Значит добавляем к первому запросу на Recording Control -> Add-> pre processor-> HTTP URL Re-writing Modifier



Далее указываем, что нас интересует именно sesskey и ставим галочку!



А потом во всех запросах, в параметрах меняем sesskey на символ *



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



Но это не все. Дло в том, что после создания пользователя в момент его Enroll'a так же фигурирует айдишка, которая от пользователя к пользователю будет меняться.



Добавим на эту страничку такой же препроцессор  Add-> pre processor-> HTTP URL Re-writing Modifier



И встроим его в параметр userid запроса




Важно! Препроцессор  будет искать параметр userid=xxx на результате выполнения предыдущего запроса, а значит, если на прошлой страничке его нет, то он вставит * - но это мы можем легко проверить, посмотрев результат выполнения...



Оно-то вставило, но вставило не совсем того юзера. Айдишку 5 имеет юзер, под которым мы вошли. Значит, надо придумать другую историю...

Попробуем так. Добавляем переменную, в которой будет храниться имя нового пользователя. затем воспользуемся ею при создании пользователя. Затем найдем пользователя по имени а из результирующей странички выкусим где-то айдишку новосозданного пользователя.

Итак добавляем новую переменную Thread Group -> Add -> Coding Element -> user DefinaedVariables



Добавляем туда переменную



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



Проверить все ли правильно сработало можно после запуска.



Супер! Теперь надо анйти на Moodle запрос, который вернет по имени пользователя его айдишку... Я нашел соответсвующий скрин, записал (через Recording Controller+Proxy Server) как я с помощью фильтра по email получаю своего юзера. И вставил страничку перед той, в которой мне понадобится айдишка нового пользователя. Что классно, так это то, что он автоматом подставил переменную user_name за меня. Ну а в sesskey я записал символ '*' сам.



Теперь осталось выкусить айдишку пользователя из внутренностей. Я выбрал suspend=xxx потому как это едиснтвенный параметр с таким именем и используется он для того чтобы указать айдишку пользователя, который надо спрятать (фича Moodle)...



Супер! Теперь на страничке, на которой нам нужна эта айдишка надо настроить препроцессор так, как мы это делали раньше...



Настроить его на выкусывание suspend



И в параметры запроса прописать вставку его значения



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



Затем я подобрал опытным путем необходимый элемент (пользовался плагином Firefox Firebug+Firepath)



Но надо получить подстроку, а потому xpath выражение будет таким



Теперь настроим постпроцессор



И наконец, запустив порадуемся результату!



Теперь весь наш сценарий работает!

Но еще не все :) Поскольку нам надо создать 500 юзеров, то необходимосделать цикл в котором будет меняться наша user_name переменная... Но как? Google подскзывает...



А как достучаться до переменной цикла? Создадим счетчик Recording Controller -> Add -> Coding Element -> Counter



Далее стоит удалить созданный ранее (Thread Group -> Add -> Coding Element -> user DefinaedVariables), которые как оказалось играют роль констант и вставить локальные переменные (Recording Controller -> Add -> Pre Processors -> User Parameters)



Скопировать туда старое значение user_name немного модифицировав его



Теперь если запустить и посмотреть на результат выполнения, то увидим, что все ок!



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

А вот и тест план, что у меня получился в результате. Может кому-то пригодится.

P.S. Немного улучшенная модификация скрипта выглядит немного по другому. Тут в группа (1) запускается только 1 раз, тут странички логина (2) и логаута (5) выполняются только один раз. Весь остальной контент циклически выполняется в цикле (3) со своим хитрым синтаксисом проверки каунтера. Так же сюда пришлось добавить обнуление фильтра (4) в котором искали юзера, поскольку фильтр сохраняется для залогиненного пользователя. Вцелом это работает на треть быстрее.


Теперь точно все! Иду потихоньку за стол, праздновать ДР. Сейчас уже 23:24 :)

воскресенье, 30 декабря 2012 г.

Сила геймификации: Мой первый опыт подсчета XP

Всю ночь вчера и от обеда денем сегодня я делал эту фичу -->

Суть в том, что ребята (как тренера так и трейни) на тренинге будут получать опыт за всевозможные действия. Пока только за code review - тренер, сделавший code review и трейни, отреагировавний на него получают по 3xp за раз. Все суммируется и выводится в отдельном блоке на курсе Moodle.

К слову о Moodle - кодить на на php/sql, местами квесты попадаются еще те, но в целом api очень продумано.  Не могу нарадоваться и как разработчик и как пользователь этой чудной LMS. Спасибо!

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

После этого стоит продумать о уровнях: Level 1, Level 2, ... - например на первый уровень можно зачислять за 100 очков, а на второй уже за 500...

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

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

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

Вот точно знаю, чем буду заниматься в новом году! 


суббота, 29 декабря 2012 г.

Сила геймификации: Билд, поднимающий настроение

Сегодня решил выделить пол дня под изучение курса геймификации на Сoursera. Просто будет неприлично это оттягивать еще хотьраз на завтра. И в ходе увлекательного рассказа Kevin Werbach я стал вспомниать о том, как впервые написал код с целью принести fun в несовсем веселом процессе - билд системы.

Любой столкнувшийся с билдом большой системы знает, что за время пока соберется приложение можно два раза сходить попить чай и прочитать массу интересного в сети. Редко, кто смотрит во время билда в консоль. А зря, поскольку иногда билд валится в самом начале, а узнаем мы это спустя ожидаемых 10-15-20 минут (через то время на сколько обычно оставили билдится систему). 

А так как я автоматизировал билд для нашей системы (тогда еще на Ant), то я решил вставлять в консоль смешные цитаты собранные из сети. Так каждые 15 секунд в консоль выплевывалось что-то новое выбранное рендомом из тысяч вариантов, сохраненных из всевозможных уголков сети.

Код сейчас наверное уже и не достать... Но я попробую...

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

пятница, 28 декабря 2012 г.

Сила геймификации: Клиент ожидает

Вчера вечером запилил одну не сложную, но очень действенню фичу.

Дело в том, что тренера у нас делают ревью своим трейни. И так, как привязки тренйи+тренер, а тренинг можетпройти любой желающий, и тренеров, в свою очередь, не много, то случается так, что трейни подолгу остаются без code review.

Говорить ребятам "эй, там некоторые трейни ждут вашего ревью" - как-то неэкологично, поскольку тренера стараются, и стоит говорить им о том, что они сделали клево, а не о том,что еще осталось сделать. В общем не стал, но написал немного кода, который как бы сообщит об этом автоматически.

Вот как скрин выглядел сегодня с утра.


А вот как он выглядел уже через пару часов



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


Благодарности. Спасибо Диме Масленко за идею с использованиям смайликов в качестве обратной связи. Спасибо тренерам нашего тренинга за то, что помагаете трейни с code review. Спасбио разработчикам Moodle!

А еще я начал играть в Diablo III специально, чтобы поучиться у мастеров геймификации. Только прошу не беспокойтесь, как показала практика больше пары часов игарть не могу (как раньше, в годы студенческие) - у меня сейчас реальные квесты поинтереснее, чем виртуальный мир монстров и героев.

Будет еще...

понедельник, 24 декабря 2012 г.

Варианты всегда есть, даже если кажется, что рассмотрены все возможные

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

Наталья - так звали кассиршу - ответила, что есть две возможности: если кто-то сдаст билет, а потому стоит подойти за 5 минут до начала или пойти на последний сеанс через 3,5 часа. Ну вот! Спасибо. Так и сделаем. Но не успел я отойти, как слышу зовут - Молодой человек! Не поверите. Сдали билет. Вот и ладненько! Давайте его. И что любопытно - в самом центре зала.

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

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

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

А фильм мне очень понравился. Рекомендую всем, кто любит чудеса. Фильм длинный, а потому стоит предвариетльно подготовиться. Смотреть его рекомендую в IMAX - особенно красиво.

вторник, 18 декабря 2012 г.

Synthesia - Aerosmith - I don't want to miss a thing - Intro

Утро, все спят, а я получил задание - подобрать припевчик одной песни с 23й секунды.
Нашел, как подобное играет парень. Красиво играет.

Вот подобрал и решил выложить, поскольку в сети нигде этого нет

Вот midi.

воскресенье, 16 декабря 2012 г.

Интерпретатор BrainFuck

Все говорят, что с этим языком можно поломать мозг. И я решил попробовать, настолько ли страшен BrainFuck как его рисуют. Хороший способ разобраться с языком - написать интерпретатор.

Могу сказать, что BrainFuck - очень легкая версия ассемблера, поскольку в ассемблере купа регистров, память с адресами, разные команды, а в BrainFuck - память, один регистр, указывающий на ячейку памяти с которой работаем и пару простых операций:
> - увеличение регистра на 1 (перемещение по ленте-памяти вправо)
< - уменьшение регистра на 1 (перемещение по ленте-памяти вправо)
+ - увеличение значения ячейки памяти на 1
- - уменьшение значения ячейки памяти на 1
. - ввод данных в ячейку памяти
, - вывод данных из ячейки памяти
[ - начало цикла, если текущая ячейка памяти не 0 то заходим в цикл (следующая команда)
] - конец цикла, если текущая ячейка памяти равна 0 - то выходим из цикла, иначе идем к следующей команде после открывающей [

Я добавил в свой интерпретатор еще пару отладочных команд.
@ - остановить программу в этом месте
# - записать состояние памяти и указатель на память (потом можно будет вывести все)

Интерфейс у меня вышел таким.
public interface Interpreter {
    Interpreter compile(String program, String input);

    String getOutput();

    String printMemory();

    List<String> getTrace();

    Interpreter compile(String program);

    Interpreter run();

    Interpreter useBreakpoint();

    Interpreter withMemory(int... memory);
}
А использую я его как-то так (это тесты, которые мне помогли в разработке)
public class BrainFuckInterpreterTest {

    private static final String SYMBOL_Q = "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++";

    protected Interpreter getInterpreter() {
        return new BrainFuckInterpreter();
    }

    @Test
    public void shouldPrintHelloWorld() {
        assertEquals("Hello World!\n",
                getInterpreter().compile(
                        "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>" +
                                "++.>+.+++++++..+++.>++.<<++++++++++" +
                                "+++++.>.+++.------.--------.>+.>.").run().getOutput());
    }

    @Test
    public void shouldPrintHelloWorld2() {
        assertEquals("Hello World!",
                getInterpreter().compile(
                    ">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.>>>++++++++[<++++>-]" +
                    "<.>>>++++++++++[<+++++++++>-]<---.<<<<.+++.------.--------.>>+.").run().getOutput());
    }

    @Test
    public void shouldOut() {
        assertEquals("Q",
                getInterpreter().compile(
                    SYMBOL_Q + ".").run().getOutput());
    }

    @Test
    public void shouldIncrease() {
        assertEquals("P",
                getInterpreter().compile(
                    SYMBOL_Q + "-.").run().getOutput());
    }

    @Test
    public void shouldIncreaseTwice() {
        assertEquals("O",
                getInterpreter().compile(
                        SYMBOL_Q + "--.").run().getOutput());
    }

    @Test
    public void shouldNextPreviousVar() {
        assertEquals("QP",
                getInterpreter().compile(
                        SYMBOL_Q + "->" + SYMBOL_Q + "><.<.").run().getOutput());
    }

    @Test
    public void shouldPrintFirst() {
        assertEquals("\u0000",
                getInterpreter().compile(
                        ".").run().getOutput());
    }

    @Test
    public void shouldIncreaseFromCurrentPosition() {
        assertEquals("QS",
                getInterpreter().compile(
                        SYMBOL_Q + "<<<<" + SYMBOL_Q + "++>>>>.<<<<.").run().getOutput());
    }

    @Test
    public void shouldLoop() {
        assertEquals("QQ",
                getInterpreter().compile(
                        SYMBOL_Q + ".[>+<-]>.").run().getOutput());
    }

    @Test
         public void shouldInput() {
        assertEquals("R",
                getInterpreter().compile(
                        ",+.", "Q").run().getOutput());
    }

    @Test
    public void shouldInputFromEmpty() {
        assertEquals("\u0000Q",
                getInterpreter().compile(
                        ",>,.<.", "Q").run().getOutput());
    }

    @Test
    public void shouldPrintMemory() {
        assertEquals("0 1 2 3 >4 ",
                getInterpreter().compile(
                        ">+>++>+++>++++").run().printMemory());
    }

    @Test
    public void shouldBreakpointAt() {
        assertEquals("0 1 2 >1 ",
                getInterpreter().compile(
                        ">+>++>+@++>++++").useBreakpoint().run().printMemory());
    }

    @Test
    public void shouldMoveRight() {
        assertEquals("\u0003",
                getInterpreter().compile(
                        "+++" + BrainFuckUtils.MOVE_RIGHT + ">.").run().getOutput());
    }

    @Test
    public void shouldCopyValue() {
        assertEquals("\u0003\u0003",
                getInterpreter().compile(
                        "+++" + BrainFuckUtils.COPY_RIGHT + ".>.").run().getOutput());

    }

    @Test
    public void shouldCopyInputToOutput() {
        assertEquals("sdgsjmfgl",
                getInterpreter().compile(
                        BrainFuckUtils.READ_ALL_INPUT +
                        BrainFuckUtils.GOTO_TO_MEMORY_START +
                        BrainFuckUtils.WRITE_ALL_OUTPUT, "sdgsjmfgl").run().getOutput());

    }

    @Test
    public void shouldWithMemory() {
        assertEquals(">14 ", getInterpreter()
                .withMemory(14).printMemory());
    }

    @Test
    public void shouldMultiple() {
        assertEquals(
               "[>2 0 3 , " +
                "2 0 >3 , " +
                "2 0 >3 3 0 , " +
                ">2 0 3 3 0 , " +
                "2 0 >3 3 0 , " +
                "2 3 >0 3 0 , " +
                "2 3 0 >3 0 , " +
                "2 3 3 >0 0 , " +
                "2 3 >3 0 0 , " +
                "2 3 >3 3 0 , " +
                ">1 3 3 3 0 , " +
                "1 3 >3 3 0 , " +
                "1 6 >0 3 0 , " +
                "1 6 0 >3 0 , " +
                "1 6 3 >0 0 , " +
                "1 6 >3 0 0 , " +
                "1 6 >3 3 0 , " +
                ">0 6 3 3 0 , " +
                "0 >6 3 3 0 ]"
        , getInterpreter()
        .withMemory(2, 0, 3)
        .useBreakpoint()
        .compile("#>>#" + COPY_RIGHT + "#<<#[>># [<+>-]# ># " +
                "[<+>-]# <# [>+>+<<-]>>[<<+>>-]<<# <<-#]>#")
        .run().getTrace().toString());
    }

    @Test
    public void shouldNotIgnoreNextCommandAfterCycle() {
        assertEquals("<0 0 1", getInterpreter()
                .withMemory(0, 0, 1)
                .compile("<[-]>")
                .run().printMemory());
    }
}
Что касается реализации, то ее можно найти тут.

В коде я специально оставил класс BrainFuckInterpreterNOOOP, который можно, по моему, использовать для проверки того как трейни разобрался в ООП - попросив его зарефакторить этот класс со множеством ответственностей. Взглянуть только на его список полей
public class BrainFuckInterpreterNOOOP implements Interpreter {
    public List<Integer> memory;
    public int index;
    private String program;
    private String input;
    private String output;
    private boolean useBreakpoint;
    private List<String> trace;
Но не интерпретатор был целью, а кодинг на самом языке. А потому я написал небольшую подборку простеньких алгоритмов, которые можно повторно использовать.
public class BrainFuckUtils {
    public static final String MOVE_RIGHT = "[>+<-]";
    public static final String MOVE_LEFT = "[<+>-]";
    public static final String COPY_RIGHT = "[>+>+<<-]>>[<<+>>-]<<";
    public static final String COPY_LEFT = "[<+<+>>-]<<[>>+<<-]>>";
    public static final String GOTO_TO_MEMORY_START = "<[<]>";
    public static final String READ_ALL_INPUT = ">,[>,]";
    public static final String WRITE_ALL_OUTPUT = "[.>]";
    public static final String CLEAN = "[-]";
    public static final String MULTIPLE =
            COPY_LEFT + ">>" + COPY_RIGHT + "<<[>>" + MOVE_LEFT + ">" +
            MOVE_LEFT + "<" +COPY_RIGHT + "<<-]>>" + ">" + CLEAN +
                    "<<<<" + MOVE_RIGHT + ">>";

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

пятница, 14 декабря 2012 г.

Позитивная песенка


Аутотренинг прям :)

четверг, 13 декабря 2012 г.

Андрей Макаревич - Вагонные споры




Вагонные споры - последнее дело,
Когда больше нечего пить,
Но поезд идет, бутыль опустела,
И тянет поговорить.

И двое сошлись не на страх, а на совесть,
Колеса прогнали сон:
Один говорил: "Наша жизнь - это поезд",
Другой говорил: "Перрон".

Один утверждал: "На пути нашем чисто",
Другой возражал: "Не до жиру".
Один говорил, мол, мы - машинисты,
Другой говорил: "Пассажиры!"

Один говорил: "Нам свобода - награда,
Мы поезд куда надо ведем".
Другой говорил: "Задаваться не надо:
Как сядем в него, так и сойдем".

А первый кричал: "Нам открыта дорога
На много, на много лет".
Второй отвечал: "Не так уж и много -
Все дело в цене на билет".

А первый кричал: "Куда хотим - туда едем,
И можем, если надо, свернуть",
Второй отвечал, что поезд проедет
Лишь там, где проложен путь.

И оба сошли где-то под Таганрогом,
Среди бескрайних полей,
И каждый пошел своею дорогой,
А поезд пошел своей.

четверг, 6 декабря 2012 г.

Кто мы? Homo Sapiens? Не уверен...

Есть два фильма, которые очень хорошо определяют нас как большую ошибку природы. Но начну с Джорджа Карлина, чтобы немного поржать над нами...



А теперь жестяк (взято тут)



И еще больший жестяк (взято тут)



Вот так вот... Будет ли конец света? Уверен мы его застанем очень скоро... Тот, кто писал календарик Майя немного ошибся, лет так на 100. Не будет конец света в этом году, но если ничего не менять - скоро будет так, как иллюстрируют во многих фантастических фильмах - кислород стоит деньги, всем управляют корпорации, смертельные вирусы, живого почти нет, вокруг смог и грязь, нет чистой воды, нет чистого воздуха, мутанты и т.д. Мы живем в интересное время. Если прорвемся, нашим внукам будет что рассказать...

понедельник, 3 декабря 2012 г.

Успеть все

Все сразу - кажется нереальным. Но сделав что-то простое на пути к нереальному, вскоре понимаешь, что можешь и больше. Хотя и по чуть-чуть. Только, блин, это все надо было делать еще вчера.

Я Сегодня - это весь мой вчерашний опыт. Я выучу сегодня новую мелодию - я завтра буду ее знать. Просто у меня будет такое же Сегодня + одна новая мелодия. День Сурка - не иначе.

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

А столько хочется успеть.

Не говори мне "завтра"! Я не знаю, что будет завтра. Лучше планировать на вчера - оно точно никогда больше не случится. Сегодня. Сейчас. Уже.

Вот бы как в матрице - дискетку вставили и вжых - "я знаю Кунг Фу!"

среда, 21 ноября 2012 г.

Очаровательная музыка

Вот понравилась мелодия Ephemeral Feeling - Sad Beautiful Piano, решил воспроизвести midi и потом по ней учиться.


Пока вот что получилось.

Может кому-то пригодится.

"Если учителя можно заменить машиной, то такого учителя стоит заменить"

Валерка, спасибо за линк!
Очень понравилось "Вопрос от экспериментатора бухгалтерше: - а ты не могла бы подтянуть детей по биотехнологии? - но как, я ведь в этом ничего не понимаю - не важно. используй бабушкин метод - как это? - тебе просто стоит стать у них за спиной и бесконечно восхищаться ими"

воскресенье, 18 ноября 2012 г.

Как мокать Bean'ы внутри Spring context запущеного Jetty?

Есть Web приложение на Spring (MVC + IoC). Есть Jetty сервер, на котором оно ранится. Web приложение в своей Model содержит ряд сервисов (Spring Beans), которыми пользуются контроллера.

Задача: Написать функциональный тест с испольованием WebDriver, но так, чтобы из теста была возможность мокать реальные сервиса модели.

Updated: Есть более продвинутая имплементация этой задачи, вот тут. Все что дальше по тексту этого поста - на память :)

Например в приложении есть 1 сервис MyService
package com.services;

import org.springframework.stereotype.Component;

/**
 * User: apofig
 * Date: 11/18/12
 * Time: 9:49 PM
 */
@Component("myService")
public class MyService {

    public String getString() {
        return "Hello from " + MyService.class.getSimpleName() + "!";
    }

}
Он инджектится в контроллер MyController с помощью @Autowired аннотации Spring'ом.
package com.controller;

import com.services.MyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * User: apofig
 * Date: 9/20/12
 * Time: 1:37 PM
 */
@Controller
@RequestMapping("/")
public class MyController {

    @Autowired
    private MyService service;

    @RequestMapping(method = RequestMethod.GET)
    public String savePlayerGame(Model model) {
        model.addAttribute("string", service.getString());
        return "view";
    }
}
Контроллер отправляет данные на jsp, которая рисует сообщение на экране
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;">
    <title>Main page</title>
</head>
<body>
    <c:if test="${string!=null}">
        <span id="message">${string}</span>
    </c:if>
<body>
</html>
Во время старта приложения я хочу, чтобы там был не реальный MyService, а его мок (или шпион), которым я буду рулить из теста вот так
public class IntegrationTest {
    private String url = "http://localhost:8080/";
    private WebDriver driver = new HtmlUnitDriver(true);
    private MyService service;

    @Test
    public void shouldGetMessageFromService() throws IOException, InterruptedException {
        when(service.getString()).thenReturn("Hi from mock!");

        driver.get(url);

        String message = driver.findElement(By.id("message")).getText();
        assertEquals("Hi from mock!", message);
    }
}
Зачем это делать? Если писать полностью функиональные тесты, тогда чтобы протестить, скажем, удаление пользователя из системы надо будет вначале создать этого пользователя по всем правилам. Но если иметь возможность мокать модель, тогда лишние действия не потребуются - мы всего лишь проверим, что до модели дошел запрос о удалении юзера и все.

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

Как я решал проблему? При старте Jetty сервера есть возможность подслушать момент инициплизации контекста.
    Server server = new Server(0);
    WebAppContext context = new WebAppContext("src/main/webapp", "");
    context.addEventListener(new ServletContextListener() {
        @Override
        public void contextInitialized(ServletContextEvent sce)
            ServletContext servletContext = sce.getServletContext(); 
            // вот тут       
        }
    }
    server.setHandler(context);
    server.start();
    int port = server.getConnectors()[0].getLocalPort();
    String url = "http://localhost:" + port;
Я так же знаю, что после окончательного старта сервера в ServletContext будет находиться Spring'овый WebApplicationContext, но в момент отработки ServletContextListener его там еще нет.

После старта сервера я могу его получить вот так
    WebApplicationContext applicationContext = 
        WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
Имея на руках Spring'овый контекст я могу поковырять его - могу удалить существующий бин и заменить его моком, так что всякий раз когда клиент будет делать context.getBean(name) он будет получать мой мок. Вперед!
    private static Object mocking(WebApplicationContext webAppContext, String beanName, boolean isMockOfSpy) {
        Object realBean = webAppContext.getBean(beanName);

        AbstractRefreshableApplicationContext context = (AbstractRefreshableApplicationContext) webAppContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();

        Object newBean = null;
        if (isMockOfSpy) {
            newBean = mock(realBean.getClass());
        } else {
            newBean = spy(realBean);
        }

        beanFactory.removeBeanDefinition(beanName);
        beanFactory.registerSingleton(beanName, newBean);
        return beanFactory.getBean(beanName);
    }
Конечно, тут не помешает пару проверок (для ClassCast), потому как контекст мог быть не обязательно XmlWebApplicationContext как у меня - а там иди знай, что вернется.

Но это не все. В момент, когда я могу провернуть такую махинацию Spring уже проинджектил все бины в поля зависимых классов по аннотации @Autowired. И хоть я могу получить мок с помощью context.getBean(name), меня это не устраивает.

Несколько часов дебага с кондишенал брейкопинтами и я поймал момент, когда WebApplicationContext уже имеет на руках все бины, но еще не успел расставить их куда велено. Любопытно, что этот момент - такой же как и мой обработчик ServletContextListener.

В момент когда я добавляю свой обработчик, список обработчиков WebAppContext пуст (т.е. я первым слушаю) - тут нигде нет никакого WebApplicationContext. Даже в момент, когда мой обработчик отрабатывает, WebApplicationContext'а так же нигде нет.
    Server server = new Server(0);
    WebAppContext context = new WebAppContext("src/main/webapp", "");
    // Spring контекст недуступен
    context.addEventListener(new ServletContextListener() {
        @Override
        public void contextInitialized(ServletContextEvent sce)
            ServletContext servletContext = sce.getServletContext();        
            // Spring контекст так же недуступен
        }
    }
    server.setHandler(context);
    server.start();
    int port = server.getConnectors()[0].getLocalPort();
    String url = "http://localhost:" + port;
Обыно он хранится в _contextAttributes, но тут пока пусто
Но я точно знаю, что он там будет спустя некоторое время (ибо додебажился).
Через некоторое время, в очередь, после моего ServletContextListener обработчика, выстрится ряд других обработчиков. Последним из них будет Spring'овый. И вот когда он отработает, то WebApplicationContext появится внутри ServletContext.В этот момент бины будут уже готовые, но еще пока не расставленные по зависимым классам.

Мне надо как-то стать пятым в очереди, и сделал я это так
    private ServletContext servletContext;
    private WebApplicationContext applicationContext;

    public String start() throws Exception {
        server = new Server(0);
        final WebAppContext context = loadWebContext();
        context.addEventListener(new ServletContextListener() {
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                servletContext = sce.getServletContext();

                context.addEventListener(new ServletContextListener() {
                    @Override
                    public void contextInitialized(ServletContextEvent sce) {
                        applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
                        // тут бины тепленькие, и все мои!!!
                    }

                    @Override
                    public void contextDestroyed(ServletContextEvent sce) {
                    }
                });
            }

            @Override
            public void contextDestroyed(ServletContextEvent sce) {
            }
        });
        server.setHandler(context);
        server.start();
        int port = server.getConnectors()[0].getLocalPort();

        String url = "http://localhost:" + port;
        return url;
    }
Дальше дело техники, вызываем метод
    private static Object mocking(WebApplicationContext webAppContext, String beanName, boolean isMockOfSpy) {
И сразу после того, как отработчик отработает Spring полезет деливерить бины по зависимым клиентам (@Autowired).

Я в дамках!

Вот исходный код простенького Spring Web проекта с демонстрацией этой возможности.

четверг, 15 ноября 2012 г.

Отчет о Tetris Coding Dojo #3 Киев DataArt XPDays

Привет! Мы в очередной раз собрались, чтобы погаматься в тетрис.
Спасибо Коле Алименкову, офису DataArt, а именно Антону Михайлюку и Саше Лылке которые помагали нам с организационной стороны, а так же и всем участникам этой встречи.

Ребят было достаточно много и теперь не только джависты, но и C# программисты. Кодили на лиспе, пхп, питоне...


Ребята не жалели бумагу


Уходили глубоко в себя :)


Но первым на Level 2 перешел AlexeyZ и тут же вся комната наполнилась смехом, потому как алгоритм не умел расскладывать квадратики, и относился к ним как к палочкам - вот и получилась занятная лесенка.


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


А вот Игорь потирает руки. Мы-то с Серегой знаем, что он бывал на всех прошлых встречах и у него в загашнике есть хороший рабочий алгоритм - долго ждать не пришлось :) Игорь вскоре запустил его.


Мы понаблюдали-понаблюдали, а когда алгоритм стал портить статистику всех играющих, Игорь получил в бан. Хорошо, что часть фреймворка написана на скриптовом языке - простой фикс и справедливость восстановлена!



Призами на этот раз были игрушки из нашего детства



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

Саша и Леша находясь на втором уровне сложности умудрились заработать столько очков, что вышли на третье место среди ребят находящихся на 5 уровне.
За то, что они поделились секретом как они это сделали - мы вручили им один тетрис на двоих :)

Получили по тетрису так же тройка лидеров.

Призеры писали на php в паре. Тут они еще не знают, что попадут в тройку.

Двое ребят не побоявшиеся писать на Closure - им достались не такие тетрисы, как всем...
...в партии, что мы купили к сегодняшней встрече как раз было два тетриса, которые отличались от всех остальных. Так и на встрече - все писали на С# и Java, а ребята решили на Closure. И молоды! Клиенты мы потом у них заберем и к делу подошьем :)

А вот эти ребята писали тесты и потому немного отстали от всех :) Просто тесты имеют такое свойство - вначале ты их, а потомо ни тебе.



А вот вкратце вся игра, ускоренная в 10 раз


Остальные фотки тут