При разработке очередного бота для группы в 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)
velvetcat
18.04.2017 17:59+1> Возможно есть другие, более удобные способы регулирования времени
Да, есть один очень хороший способ — не завязываться намертво на недетерминированные неконтролируемые зависимости, и больше не пытаться исправить то, что исправлять не следует.
j_wayne выше все правильно сказал.
saboteur_kiev
18.04.2017 19:11+1«Похоже, что Docker не настолько глубоко производит виртуализацию, как мне казалось.»
Ну собственно Docker это же виртуализация приложения, то есть в основном на уровне файловой системы/библиотек, а не ОС. Поэтому по идее на уровне интуиции можно было сразу начать искать что-то библиотечное, типа faketime.VolCh
18.04.2017 19:32Собственно Docker вообще не виртуализация, а изоляция процесса в контейнере от процессов вне его.
afunix
19.04.2017 05:37+3А как же модульная архитектура, mock-и, stub-ы, фабрики и все то, что изобрело человечество на данный момент в области computer science?
Rast1234
19.04.2017 12:50С virtualbox тоже было бы не очень просто, там можно задать смещение времени внутри машины относительно системного. При выключенной виртуалке. То есть чтобы каждый раз запускать какие-то тесты с нужным временем, нужно будет писать какую-то обвязку, чтобы посчитать и задать оффсет.
madkite
19.04.2017 13:36+1Тут надо отметить, что этот метод будет работать только если Вы получаете время через вызовы glibc. Если же у Вас в программе время получается по-другому (например, читается из procfs) или же glibc статически прилинкован к бинарнику, то трюк с LD_PRELOAD не прокатит. Попробуйте, например, протестировать так программу на go. ;)
Highstaker
19.04.2017 14:25Кстати это, как и многие другие ограничения, описано в пункте 2 их README. В таких случаях без mock'ов не обойтись (не уверен насчёт datefudge, не пробовал).
Но поскольку у меня питоновская прога от силы строк на 400, и время там получается банально через datetime.now() — так даже удобнее. Время «обманывается», и тестировочные классы городить не надо. XD
j_wayne
Немного оффтоп, т.к. не для процесса…
Но на практике, можно организовать классы/модули так, чтобы в unit-тестах просто подставлять нужное время без всяких костылей.
В теории можно в test env и для ручного тестирования похожий подход использовать.
Дело в том, что тут вся соль в тестировании алгоритмов самой вашей программы. Python от ОСи время может получать, это тестировать на мой взгляд — излишне.
kataklysm
Это как раз и называется «костылями». Зачем реализовывать классы/модули, без которых можно обойтись, да еще о которых надо помнить, особенно «другим» разработчикам.
Вот именно, что от ОСи и это надо тестировать.
Highstaker
Действительно, на мой взгляд, прикручивать отдельные классы чисто чтобы потестить небольшую фичу излишне. Тем более, что мой проект очень невелик.
В случае крупного проекта, где много фич завязано на время, это может быть оправдано. Но здесь… уж больно много мороки для в общем-то ерундовой задачи.
j_wayne
Мне бы было этого уже достаточно. Ну ладно, хозяин-барин. Удачи!
VolCh
По-моему, как раз подход с faketime лучше подходит для приемочного тестирования больших проектов, а для небольшого достаточно вынести зависимость модуля от времени как внешнюю и мокать её обычными средствами, а не системными.
j_wayne
Кому как.
Вызов условной статической 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
А то, как питон получает время от ОСи, протестировано разработчиками питона, зачем это тестировать?
batment
Специально для такого случая в питоне есть библиотека unittest.mock
j_wayne
Да, и в руби есть timecop, он еще удобнее, чем универсальный мокинг, я его использую.
Но в некоторых языках (java, C#) закрытая архитектура классов и нет манкипатчинга.
И вообще, я лично, предпочитаю mock-ам fake классы
http://www.yegor256.com/2014/09/23/built-in-fake-objects.html
Основной недостаток мока — код может сломаться, а мок это успешно скроет. Ну и вообще, магия…
VolCh
Это называется не костылями, а внедрением зависимости. Может открою вам секрет, но программы можно писать вообще не разбивая их код на модули, классы, функции/процедуры. Но если разбиваешь всё-таки, то многие считают, что лучше сначала тестировать части отдельно, как можно в большей степени изолированности от окружающего мира, а уж затем приложение целиком.