Обманываем время: о тестировании с «подставным» временем на Linux и Docker

  • Tutorial

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


Менять системное время глобально ой, как не хотелось. Муторно, плюс у меня в ней столько всего понаставлено, не дай Б-г что-то заглючит (вряд ли, но мало ли). Думал запустить VirtualBox, но уж больно лень было ставить «чистую» Убунту, расшаривать папки, и т. д., тем более что этот вариант жрёт, как троглодит серьёзно потребляет машинные ресурсы.


Но буквально недавно я начал ковырять Docker. «У него просто обязан быть механизм контроля системного времени внутри контейнера», — подумал я. Рассмотрим, что же в результате вышло.


Докер не выручил


Итак, создаём контейнер и залезаем в него:


docker run -it ubuntu bash

Сразу скажу, что в контейнере я работаю как root, поэтому sudo не требуется.


Пробую:


date --set='2017-04-20 23:59:50'

Выдаёт date: cannot set date: Operation not permitted


Пробую:


hwclock --set --date='2017-04-20 23:59:50'

Выдаёт hwclock: Cannot access the Hardware Clock via any known method.


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


Решение оказалось не докеровским


Ещё один вариант — в самой моей программе перехватывать вызовы к системному времени, но, опять же, муторно. Но буквально этажом выше есть ответ, указывающий на некую библиотеку libfaketime. С помощью неё можно подставить «фальшивое время» для запускаемого процесса. Итак, устанавливаю её в контейнер:


git clone https://github.com/wolfcw/libfaketime.git
cd libfaketime
make install

Далее, следуя инструкции в ответе, запускаю бота с заданными переменными окружения:


LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 FAKETIME_NO_CACHE=1 FAKETIME="2017-04-19 23:59:50" ./run.sh --debug#

Где в LD_PRELOAD подставляем свежеустановленную библиотеку, а в FAKETIME пишем время, которое хотим выставить для запускаемого процесса. FAKETIME_NO_CACHE использовался в примере и предположительно отключает кеширование, используемое для повышения производительности. Не испытывал, но полагаю, что этот параметр необязателен.


Итак, программа запустилась, и действительно время выставилось так, как я хотел. Лишь с одной проблемой — время остановилось. Сообщения дебага показывают постоянно [2017-04-19 23:59:50]. В этой библиотеке есть одна неинтуитивная особенность. Простое задание времени действительно задаёт и фиксирует его. Что бы время именно начиналось от данной точки, надо задать его, как FAKETIME='@2017-04-19 23:59:50'. И врёмя пойдёт от этой точки.


Аналог из репозиториев


Оказывается, всё даже проще. Чуть позже я обнаружил, что эта библиотека есть в стандартных репозиториях Ubuntu, и спокойно ставится через apt-get install faketime. А запускается так:


faketime -f '@2017-04-20 23:59:50' ./run.sh

Не забываем про @ перед временем, здесь такой же синтаксис, но в довольно кратком man это не сказано. Только в подробном описании на Гитхабе.


Вместо заключения


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


os.environ["FAKETIME"] = "2020-01-01"

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

Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 21

    0
    Возможно есть другие, более удобные способы регулирования времени для процесса?


    Немного оффтоп, т.к. не для процесса…
    Но на практике, можно организовать классы/модули так, чтобы в unit-тестах просто подставлять нужное время без всяких костылей.
    В теории можно в test env и для ручного тестирования похожий подход использовать.
    Дело в том, что тут вся соль в тестировании алгоритмов самой вашей программы. Python от ОСи время может получать, это тестировать на мой взгляд — излишне.
      0
      Но на практике, можно организовать классы/модули так, чтобы в unit-тестах просто подставлять нужное время без всяких костылей.

      Это как раз и называется «костылями». Зачем реализовывать классы/модули, без которых можно обойтись, да еще о которых надо помнить, особенно «другим» разработчикам.

      Python от ОСи время может получать, это тестировать на мой взгляд — излишне.

      Вот именно, что от ОСи и это надо тестировать.
        0
        Действительно, на мой взгляд, прикручивать отдельные классы чисто чтобы потестить небольшую фичу излишне. Тем более, что мой проект очень невелик.
        В случае крупного проекта, где много фич завязано на время, это может быть оправдано. Но здесь… уж больно много мороки для в общем-то ерундовой задачи.
          +1
          Этот бот в конце каждого дня отправляет (или, в зависимости от ряда условий, не отправляет) сообщение в чат и производит манипуляции с некоторыми предыдущими своими сообщениями (или, опять же, не производит).


          Мне бы было этого уже достаточно. Ну ладно, хозяин-барин. Удачи!
            +2

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

            +1
            Кому как.
            Вызов условной статической System.getTime() размазан по всему коду, от чего собственно и возник вопрос у ТС.

            https://dzone.com/articles/why-static-bad-and-how-avoid
            http://www.yegor256.com/2014/05/05/oop-alternative-to-utility-classes.html

            А то, как питон получает время от ОСи, протестировано разработчиками питона, зачем это тестировать?
              0
              Вызов условной статической System.getTime() размазан по всему коду

              Специально для такого случая в питоне есть библиотека unittest.mock
                0
                Да, и в руби есть timecop, он еще удобнее, чем универсальный мокинг, я его использую.
                Но в некоторых языках (java, C#) закрытая архитектура классов и нет манкипатчинга.
                И вообще, я лично, предпочитаю mock-ам fake классы

                http://www.yegor256.com/2014/09/23/built-in-fake-objects.html

                Основной недостаток мока — код может сломаться, а мок это успешно скроет. Ну и вообще, магия…
              +1
              Это как раз и называется «костылями». Зачем реализовывать классы/модули, без которых можно обойтись, да еще о которых надо помнить, особенно «другим» разработчикам.

              Это называется не костылями, а внедрением зависимости. Может открою вам секрет, но программы можно писать вообще не разбивая их код на модули, классы, функции/процедуры. Но если разбиваешь всё-таки, то многие считают, что лучше сначала тестировать части отдельно, как можно в большей степени изолированности от окружающего мира, а уж затем приложение целиком.

            0
            Круто. Интересно на винде для приложений есть такая фича.
              0
              Не так давно товарищ мучился подобной задачей. И выяснилось, что в винде все намного проще, системные вызовы перехватываются легче. А вот в линуксах или ядро патчить надо, или костыли городить
                0
                Спасибо. Видимо у товарища и у меня похожие интересы.)
              0
              Спасибо за статью, как раз нужно было «поиграться» с системным временем.
                +1
                > Возможно есть другие, более удобные способы регулирования времени

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

                j_wayne выше все правильно сказал.
                  +1
                  «Похоже, что Docker не настолько глубоко производит виртуализацию, как мне казалось.»

                  Ну собственно Docker это же виртуализация приложения, то есть в основном на уровне файловой системы/библиотек, а не ОС. Поэтому по идее на уровне интуиции можно было сразу начать искать что-то библиотечное, типа faketime.
                    0

                    Собственно Docker вообще не виртуализация, а изоляция процесса в контейнере от процессов вне его.

                    +1
                    Мы для таких целей используем datefudge
                      +3
                      А как же модульная архитектура, mock-и, stub-ы, фабрики и все то, что изобрело человечество на данный момент в области computer science?
                        0
                        С virtualbox тоже было бы не очень просто, там можно задать смещение времени внутри машины относительно системного. При выключенной виртуалке. То есть чтобы каждый раз запускать какие-то тесты с нужным временем, нужно будет писать какую-то обвязку, чтобы посчитать и задать оффсет.
                          +1

                          Тут надо отметить, что этот метод будет работать только если Вы получаете время через вызовы glibc. Если же у Вас в программе время получается по-другому (например, читается из procfs) или же glibc статически прилинкован к бинарнику, то трюк с LD_PRELOAD не прокатит. Попробуйте, например, протестировать так программу на go. ;)

                            0
                            Кстати это, как и многие другие ограничения, описано в пункте 2 их README. В таких случаях без mock'ов не обойтись (не уверен насчёт datefudge, не пробовал).

                            Но поскольку у меня питоновская прога от силы строк на 400, и время там получается банально через datetime.now() — так даже удобнее. Время «обманывается», и тестировочные классы городить не надо. XD

                          Only users with full accounts can post comments. Log in, please.