При разработке очередного бота для группы в 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"

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

Поделиться с друзьями
-->

Комментарии (21)


  1. j_wayne
    18.04.2017 14:58

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


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


    1. kataklysm
      18.04.2017 15:58

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

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

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

      Вот именно, что от ОСи и это надо тестировать.


      1. Highstaker
        18.04.2017 16:01

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


        1. j_wayne
          18.04.2017 16:09
          +1

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


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


        1. VolCh
          18.04.2017 18:24
          +2

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


      1. j_wayne
        18.04.2017 16:07
        +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

        А то, как питон получает время от ОСи, протестировано разработчиками питона, зачем это тестировать?


        1. batment
          18.04.2017 18:30

          Вызов условной статической System.getTime() размазан по всему коду

          Специально для такого случая в питоне есть библиотека unittest.mock


          1. j_wayne
            18.04.2017 18:34

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

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

            Основной недостаток мока — код может сломаться, а мок это успешно скроет. Ну и вообще, магия…


      1. VolCh
        18.04.2017 18:20
        +1

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

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


  1. f9k56
    18.04.2017 16:32

    Круто. Интересно на винде для приложений есть такая фича.


    1. ushliy
      18.04.2017 21:57

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


      1. f9k56
        19.04.2017 16:16

        Спасибо. Видимо у товарища и у меня похожие интересы.)


  1. jetu
    18.04.2017 16:32

    Спасибо за статью, как раз нужно было «поиграться» с системным временем.


  1. velvetcat
    18.04.2017 17:59
    +1

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

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

    j_wayne выше все правильно сказал.


  1. saboteur_kiev
    18.04.2017 19:11
    +1

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

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


    1. VolCh
      18.04.2017 19:32

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


  1. crazylh
    18.04.2017 22:10
    +1

    Мы для таких целей используем datefudge


  1. afunix
    19.04.2017 05:37
    +3

    А как же модульная архитектура, mock-и, stub-ы, фабрики и все то, что изобрело человечество на данный момент в области computer science?


  1. Rast1234
    19.04.2017 12:50

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


  1. madkite
    19.04.2017 13:36
    +1

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


    1. Highstaker
      19.04.2017 14:25

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

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