Add homework 01 guarantees assignment
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
|
||||
- [Канал](https://t.me/+jmiH8ATByBxjYmM6) и [чат](https://t.me/+7K6jQRKvhMZjMDky) курса
|
||||
- [Репозиторий с материалами курса](https://distsys.ru/course/hse-2026/)
|
||||
- [Таблица с оценками](https://docs.google.com/spreadsheets/d/1bas6MLplMmvbgl4X_syl4Tv5LPPT2pemjmDgWjs2KNo/)
|
||||
- [Записи занятий](https://disk.yandex.ru/d/h1-EWq4eNgP4_g)
|
||||
- [Таблица с оценками](https://docs.google.com/spreadsheets/d/1bas6MLplMmvbgl4X_syl4Tv5LPPT2pemjmDgWjs2KNo/)
|
||||
- [Политика использования LLM](llm-policy.md)
|
||||
- Форма для фидбека по курсу
|
||||
|
||||
## Занятия и преподаватели
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# Гарантии доставки
|
||||
|
||||
В этом задании вам предстоит реализовать различные гарантии доставки сообщений в распределённой системе.
|
||||
|
||||
В нашей системе есть два узла, и нам требуется организовать одностороннюю передачу текстовых сообщений между выполняющимися на узлах процессами. Процесс _Sender_ будет принимать сообщения от локального пользователя _S_ и отправлять их по сети процессу _Receiver_. Процесс _Receiver_ будет принимать сообщения от _Sender_ и доставлять их локальному пользователю _R_. Под доставкой подразумевается отправка локального сообщения, идентичного исходному сообщению от _S_.
|
||||
|
||||
Вам необходимо написать четыре реализации _Sender_ и _Receiver_, обеспечивающие следующие гарантии доставки сообщений:
|
||||
|
||||
1. _Не более одного раза (at most once)._ Каждое сообщение от _S_ должно быть или доставлено _R_ ровно один раз или не доставлено вовсе. Иными словами, нельзя допускать повторные доставки сообщений.
|
||||
2. _Не менее одного раза (at least once)._ Каждое сообщение от _S_ должно быть доставлено _R_, при этом допускаются повторы.
|
||||
3. _Ровно один раз (exactly once)._ Каждое сообщение от _S_ должно быть доставлено _R_ ровно один раз, то есть повторы не допускаются.
|
||||
4. _Ровно один раз и с сохранением порядка (exactly once + ordered)._ Каждое сообщение от _S_ должно быть доставлено _R_ ровно один раз и в порядке их отправки _S_.
|
||||
|
||||
Процессы могут взаимодействовать друг с другом путём обмена сообщениями через сетевой транспорт со следующими характеристиками: отдельное сообщение может быть потеряно, доставленные сообщения не искажаются, сообщения могут дублироваться, порядок их получения не гарантируется, все получаемые сообщения были кем-то отправлены (нет сообщений «из воздуха»). При этом сеть обладает свойством fair-loss: если отправитель неограниченно повторяет передачу одного и того же сообщения, хотя бы одна его копия рано или поздно достигнет получателя. Это предположение действует для передачи в обоих направлениях и соблюдается в тестах. Отказы узлов в данном задании отсутствуют. Все указанные гарантии рассматриваются только в рамках описанной модели.
|
||||
|
||||
Независимо от типа гарантий, если сеть ведёт себя надёжно (нет потерь сообщений), все сообщения от _S_ должны доставляться _R_. Это исключает, например, тривиальную реализацию гарантии 1, которая не отправляет ничего по сети.
|
||||
|
||||
Ваша реализация не должна делать предположений об уникальности содержимого доставляемых сообщений. Например, в разные моменты времени могут быть отправлены два сообщения с идентичным текстом. В этом случае для гарантии 2 требуется доставить эти сообщения пользователю не менее двух раз, а для гарантий 3 и 4 — ровно два раза. Также не следует делать предположений о размере сообщений — он может быть произвольным.
|
||||
|
||||
Несложно заметить, что реализация последней, самой сильной гарантии покрывает первые три гарантии. Тем не менее мы просим вас реализовать каждую гарантию отдельно. Так вы наглядно увидите, какие дополнительные ресурсы и накладные расходы требуются для поддержки той или иной гарантии. На практике не всегда требуются самые сильные гарантии: например, порядок доставки может быть неважен и «платить» за него будет расточительно. Поэтому, если вы реализуете только последнюю гарантию, мы не зачтём вам остальные.
|
||||
|
||||
На максимальный балл требуется также оптимизировать ресурсы, потребляемые для обеспечения каждой из гарантий. Во-первых, это объём памяти, используемой процессами, то есть размер хранимого ими состояния. Overhead-тесты сравнивают потребление памяти с заданными порогами: если бессрочно хранить все когда-либо обработанные сообщения или их идентификаторы, решение в эти пороги не уложится. При этом актуальные данные, например неподтверждённые сообщения, хранить можно и в некоторых гарантиях необходимо. Постарайтесь удалять лишнюю или неактуальную информацию (например, если доставлены все 100 предыдущих сообщений, то так ли необходимо хранить все 100 идентификаторов сообщений?). При оптимизации памяти учитывайте не только количество хранимых элементов, но и накладные расходы выбранного представления данных. Во-вторых, это число и суммарный объём сообщений, передаваемых по сети между _Sender_ и _Receiver_. Постарайтесь не передавать по сети лишние данные и не отправлять слишком много сообщений, особенно если они могут быть отброшены получателем, то есть переданы по сети зря. В процессе оптимизации вы должны осознать возникающие при такой оптимизации компромиссы между потреблением памяти, нагрузкой на сеть и скоростью доставки сообщений.
|
||||
|
||||
## Реализация
|
||||
|
||||
Для реализации и тестирования решения используется учебный фреймворк AnySystem (см. материалы первого семинара). В папке `solution` размещена заготовка для решения [guarantees.py](solution/guarantees.py). Вам надо доработать реализации классов `...Sender` и `...Receiver` для каждой гарантии так, чтобы они проходили все тесты.
|
||||
|
||||
Ваши реализации процессов AnySystem должны соблюдать правило [изоляции процессов](../readme.md#изоляция-процессов-anysystem): изменяемое состояние каждого процесса должно храниться только в `self`, а общая изменяемая память между процессами запрещена. Все взаимодействия между процессами должны происходить через сообщения, как и в реальной распределённой системе.
|
||||
|
||||
### Sender
|
||||
|
||||
Сообщения от _S_ передаются процессу с помощью локальных сообщений, см. метод `on_local_message()`. Все сообщения имеют тип `MESSAGE` и одинаковую структуру: в единственном поле `text` содержится строка с текстом сообщения. Для взаимодействия с _Receiver_ вы можете использовать сообщения произвольного типа и структуры. Приходящие от _Receiver_ сообщения следует обрабатывать в методе `on_message()`. Также вы можете устанавливать таймеры в любом из методов и обрабатывать их наступление в `on_timer()`.
|
||||
|
||||
### Receiver
|
||||
|
||||
Данный процесс не принимает локальные сообщения, поэтому метод `on_local_message()` не используется. Сетевые сообщения следует обрабатывать в методе `on_message()`. Также вы можете устанавливать таймеры в любом из методов и обрабатывать их наступление в `on_timer()`.
|
||||
|
||||
Важно правильно реализовать доставку сообщений локальному пользователю _R_, иначе тесты не будут проходить. Для этого вы должны отправить локальное сообщение с помощью метода `ctx.send_local()`. Сообщение должно быть полностью идентично исходному сообщению, принятому _Sender_ от пользователя _S_, то есть иметь тот же тип `MESSAGE` и поле `text` с тем же значением. Других полей в сообщении быть не должно.
|
||||
|
||||
## Оценивание
|
||||
|
||||
Компоненты задачи и их вклад в оценку:
|
||||
|
||||
- Корректная реализация гарантии _at most once_ - 2 балла
|
||||
- проходят все тесты на эту гарантию без "OVERHEAD..."
|
||||
- Корректная реализация гарантии _at least once_ - 2 балла
|
||||
- проходят все тесты на эту гарантию без "OVERHEAD..."
|
||||
- Корректная реализация гарантии _exactly once_ - 2 балла
|
||||
- проходят все тесты на эту гарантию без "OVERHEAD..."
|
||||
- Корректная реализация гарантии _exactly once + ordered_ - 2 балла
|
||||
- проходят все тесты на эту гарантию без "OVERHEAD..."
|
||||
- Оптимизация потребляемых ресурсов - 2 балла*
|
||||
- проходят тесты "OVERHEAD..." для всех гарантий (1 балл)
|
||||
- в отчёте описаны возникающие при оптимизации компромиссы и обоснованы используемые подходы (1 балл)
|
||||
- *засчитывается только при успешном выполнении всех предыдущих компонентов
|
||||
|
||||
Краткий отчёт с описанием решения в `solution/readme.md` обязателен и сдаётся вместе с решением до дедлайна. Без отчёта автоматические тесты запускаются, но защита не проводится и **решение не засчитывается**.
|
||||
|
||||
Автоматический `SCORE`, который выводят тесты, составляет не более 9 баллов. Ещё 1 балл за описание и обоснование оптимизаций выставляется отдельно по отчёту.
|
||||
|
||||
Штрафы:
|
||||
- Для гарантии A используется реализация более сильной гарантии B - минус 1 балл за гарантию A
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Локальное тестирование
|
||||
|
||||
Тесты находятся в папке `tests`. Есть два варианта их запуска.
|
||||
|
||||
Рекомендуемый вариант запуска тестов - через готовый Docker-образ. В этом случае используемое окружение будет аналогично тестирующей системе. Убедитесь, что на вашей машине установлен [Docker Engine](https://docs.docker.com/engine/install/) (можно использовать [Docker Desktop](https://docs.docker.com/desktop/)). Для запуска тестов выполните команду:
|
||||
|
||||
```commandline
|
||||
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/guarantees:latest [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
|
||||
```
|
||||
|
||||
Для запуска полного набора тестов с теми же параметрами, что и в тестирующей системе, выполните команду:
|
||||
|
||||
```commandline
|
||||
docker run --pull always --rm -t -v ./solution:/solution distsys.ru/course/guarantees:latest -m 100 -c -o
|
||||
```
|
||||
|
||||
Вы также можете запустить тесты, скомпилировав их локально с помощью компилятора Rust. Такой вариант может быть удобен, если вы хотите лучше изучить или доработать тесты. Убедитесь, что на вашей машине установлен [Rust](https://www.rust-lang.org/tools/install). Скомпилируйте тесты с помощью команды `cargo install --locked --path tests`. Для запуска тестов выполните команду:
|
||||
|
||||
```commandline
|
||||
distsys-guarantees [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
|
||||
```
|
||||
|
||||
Доступные опции тестов можно посмотреть с помощью флага `-h`. Опишем наиболее важные из них:
|
||||
- Флаг `-d` включает вывод трасс - последовательностей событий во время выполнения каждого из тестов. Его рекомендуется использовать при отладке решений.
|
||||
- Опция `-m` задает количество запусков рандомизированных тестов (chaos monkey). Значение по умолчанию - 0. Как только ваше решение будет проходить основные тесты, установите значение в 10 и убедитесь, что эти тесты проходят. Далее можно проверить решение на 100 запусках (`-d` лучше убрать для скорости) - такое значение используется в тестирующей системе. (Обратите внимание, что эти тесты хоть и рандомизированные, но детерминированные - при одном значении seed результат будет всегда одинаковый. Так что не стоит пытаться заново тестировать то же самое решение, надеясь что оно вдруг пройдет.)
|
||||
- Флаг `-c` включает тесты на model checking (см. первый семинар), по умолчанию они выключены. Как только ваше решение будет проходить основные тесты, добавьте этот флаг и убедитесь, что эти тесты также проходят.
|
||||
- Флаг `-o` включает тесты на потребление ресурсов (памяти и сети), по умолчанию они выключены. В этих тестах измеряются и выводятся максимальное потребление памяти объектами _Sender_ и _Receiver_, число переданных по сети сообщений, их суммарный объем (traffic) и отношение числа исходных сообщений к времени работы вашей реализации (throughput). Полученные значения сравниваются с пороговыми значениями, в которые укладывается с запасом авторское решение. Как только ваше решение будет проходить основные тесты, chaos monkey и model checking, включите эти тесты и при необходимости займитесь оптимизацией решения.
|
||||
- Опция `-t` позволяет прогнать только один конкретный тест, указав его имя (в точности как оно выводится в консоли, например `[AT MOST ONCE] NORMAL`).
|
||||
- Опция `-g` позволяет прогнать только тесты для одной из гарантий, указав её сокращение (`AMO`, `ALO`, `EO`, `EOO`).
|
||||
- Опция `-s` позволяет изменить используемый random seed (см. первый семинар). Можно использовать для дополнительной проверки вашего решения. В тестирующей системе используется значение по умолчанию (123).
|
||||
|
||||
Во время проверки решения в тестирующей системе используются опции `-m 100 -c -o` с лимитом времени в 5 минут. На авторском решении выполнение всех тестов с этими опциями занимает около 10 секунд.
|
||||
|
||||
Код тестов открыт и находится в `tests/src`. Вы можете обращаться к нему и использовать в своём решении информацию об условиях тестирования, например о минимальной и максимальной задержках в сети. При этом корректность решения не должна зависеть от конкретных значений задержек, вероятностей потери и дублирования сообщений: гарантии должны выполняться при любом поведении сети, соответствующем описанной выше модели.
|
||||
|
||||
Если вы найдете ошибки или требования из условий, которые не покрывают наши тесты, то вы можете получить за это бонусные баллы. Для этого надо включить в отчёт описание ситуации, которую не ловят тесты, добавив при необходимости пример решения с ошибкой. За это полагается 1 балл. Если вы также реализуете тесты, которые ловят найденную проблему, или хотя бы опишите их логику, то получите еще 1 балл.
|
||||
|
||||
### Проверка в тестирующей системе
|
||||
|
||||
Отправьте ваше решение в тестирующую систему следуя [инструкции](../readme.md) и дождитесь результатов.
|
||||
|
||||
## ЧаВо
|
||||
|
||||
**Как измеряется потребление памяти в тестах на overhead и что в него входит?**
|
||||
|
||||
См. [здесь](https://github.com/osukhoroslov/anysystem/blob/main/src/python/mod.rs#L282). Данной функции передаётся объект _Sender_ или _Receiver_. Измеряется потребление памяти структурами данных (атрибутами) внутри этих объектов. Функция вызывается периодически по ходу выполнения теста, и запоминается максимальное полученное значение, которое выводится в конце теста. Затраты на глобальные переменные и таймеры не учитываются.
|
||||
|
||||
**Что можно и что нельзя использовать для хранения состояния процесса?**
|
||||
|
||||
Можно и нужно использовать атрибуты объектов _Sender_ и _Receiver_.
|
||||
|
||||
Если вы претендуете на баллы за оптимизацию потребляемых ресурсов, также нельзя:
|
||||
- хранить полный набор идентификаторов сообщений в виде битовых значений в числе или битовом наборе (bitset); тесты проверяют наличие в коде операторов битового сдвига,
|
||||
- хранить содержимое сообщений в именах таймеров (допускается хранить номера сообщений),
|
||||
- отправлять из процесса самому себе сообщения с его состоянием.
|
||||
|
||||
Эти способы будут рассматриваться как попытка обойти измерение ресурсов, и пункт с оптимизацией потребляемых ресурсов засчитан не будет. Авторское решение не использует подобных ухищрений и хранит всё состояние в атрибутах процессов.
|
||||
|
||||
Для прохождения тестов на overhead не требуются сторонние библиотеки типа numpy, сжатие и распаковка данных, специальные бинарные форматы и т.п. Авторское решение не использует ничего из перечисленного.
|
||||
|
||||
Если вы не понимаете, как пройти тесты без вышеописанных хаков, перечитайте условие, там есть намёк.
|
||||
|
||||
**Допускается ли падение тестов на overhead с другим random seed?**
|
||||
|
||||
Да. Пороги в тестах указаны не вообще для всех возможных выполнений (где потребление ресурсов может довольно сильно отличаться), а только для запусков с дефолтным seed.
|
||||
@@ -0,0 +1,177 @@
|
||||
from anysystem import Context, Message, Process
|
||||
|
||||
|
||||
# AT MOST ONCE ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
class AtMostOnceSender(Process):
|
||||
def __init__(self, proc_id: str, receiver_id: str):
|
||||
self._id = proc_id
|
||||
self._receiver = receiver_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# receive message for delivery from local user
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver here
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
|
||||
|
||||
class AtMostOnceReceiver(Process):
|
||||
def __init__(self, proc_id: str):
|
||||
self._id = proc_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# not used in this task
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver
|
||||
# deliver message to local user with ctx.send_local()
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
|
||||
|
||||
# AT LEAST ONCE --------------------------------------------------------------------------------------------------------
|
||||
|
||||
class AtLeastOnceSender(Process):
|
||||
def __init__(self, proc_id: str, receiver_id: str):
|
||||
self._id = proc_id
|
||||
self._receiver = receiver_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# receive message for delivery from local user
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver here
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
|
||||
|
||||
class AtLeastOnceReceiver(Process):
|
||||
def __init__(self, proc_id: str):
|
||||
self._id = proc_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# not used in this task
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver
|
||||
# deliver message to local user with ctx.send_local()
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
|
||||
|
||||
# EXACTLY ONCE ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
class ExactlyOnceSender(Process):
|
||||
def __init__(self, proc_id: str, receiver_id: str):
|
||||
self._id = proc_id
|
||||
self._receiver = receiver_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# receive message for delivery from local user
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver here
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
|
||||
|
||||
class ExactlyOnceReceiver(Process):
|
||||
def __init__(self, proc_id: str):
|
||||
self._id = proc_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# not used in this task
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver
|
||||
# deliver message to local user with ctx.send_local()
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
|
||||
|
||||
# EXACTLY ONCE + ORDERED -----------------------------------------------------------------------------------------------
|
||||
|
||||
class ExactlyOnceOrderedSender(Process):
|
||||
def __init__(self, proc_id: str, receiver_id: str):
|
||||
self._id = proc_id
|
||||
self._receiver = receiver_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# receive message for delivery from local user
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver here
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
|
||||
|
||||
class ExactlyOnceOrderedReceiver(Process):
|
||||
def __init__(self, proc_id: str):
|
||||
self._id = proc_id
|
||||
|
||||
def on_local_message(self, msg: Message, ctx: Context):
|
||||
# not used in this task
|
||||
pass
|
||||
|
||||
def on_start(self, ctx: Context):
|
||||
pass
|
||||
|
||||
def on_message(self, msg: Message, sender: str, ctx: Context):
|
||||
# process messages from receiver
|
||||
# deliver message to local user with ctx.send_local()
|
||||
pass
|
||||
|
||||
def on_timer(self, timer_name: str, ctx: Context):
|
||||
# process fired timers here
|
||||
pass
|
||||
@@ -0,0 +1,5 @@
|
||||
target
|
||||
**/target
|
||||
**/__pycache__
|
||||
**/.pytest_cache
|
||||
*.pyc
|
||||
Generated
+990
@@ -0,0 +1,990 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anysystem"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01badd3bc0d042e4399f8f685c394a79a72e7f233b3d3b5eabb3b94734f4dbe"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"downcast-rs",
|
||||
"dyn-clone",
|
||||
"indexmap 2.14.0",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"ordered-float",
|
||||
"pyo3",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"regex",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simcore",
|
||||
"sugars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assertables"
|
||||
version = "3.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d84b91d63c67e6c0b1a74728dad0057c4a6124b74e671ff91b7fe9f8bbd671aa"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap 1.9.3",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distsys-guarantees"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anysystem",
|
||||
"assertables",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"pyo3",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"sugars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"portable-atomic",
|
||||
"pyo3-build-config",
|
||||
"pyo3-ffi",
|
||||
"pyo3-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
|
||||
dependencies = [
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"rstest_macros",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"relative-path",
|
||||
"rustc_version",
|
||||
"syn 2.0.106",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.143"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_type_name"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92acc4cd6ae20767c54d6cf1a075624e7f4d9e99d7ebc685398ff243144d8714"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simcore"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a20dfec85e94e569fa5b810b2d992c68b361f31bfba471447427536488c9c292"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"downcast-rs",
|
||||
"dyn-clone",
|
||||
"erased-serde",
|
||||
"futures",
|
||||
"log",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_type_name",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "sugars"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc0db74f9ee706e039d031a560bd7d110c7022f016051b3d33eeff9583e3e67a"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "distsys-guarantees"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
resolver = "3"
|
||||
|
||||
[dependencies]
|
||||
anysystem = "=0.3.0"
|
||||
indexmap = "2.12"
|
||||
assertables = "3.2.2"
|
||||
clap = { version = "3.1.17", features = ["cargo", "derive"] }
|
||||
env_logger = "0.9.0"
|
||||
log = "0.4.14"
|
||||
pyo3 = { version = "=0.29.2", features = ["auto-initialize"] }
|
||||
rand = "0.8.5"
|
||||
rand_pcg = "0.3.1"
|
||||
sugars = "3.0.0"
|
||||
@@ -0,0 +1,19 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM rust:1.97.1-bookworm@sha256:0e2bcaef56d041a486784e54104a81aebe0da44bd03019bd70bc0401e42e4a97 AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y build-essential python3-dev
|
||||
|
||||
WORKDIR /tests
|
||||
COPY . .
|
||||
RUN --mount=type=cache,id=distsys-course-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
|
||||
--mount=type=cache,id=distsys-course-cargo-target-rust-1-97-1,target=/tests/target,sharing=locked \
|
||||
cargo install --locked --path . --root /opt/course
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y python3-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /opt/course/bin/distsys-guarantees /usr/local/bin/distsys-guarantees
|
||||
|
||||
WORKDIR /solution
|
||||
ENTRYPOINT ["timeout", "-k", "10", "300", "distsys-guarantees", "-i", "guarantees.py"]
|
||||
@@ -0,0 +1,2 @@
|
||||
max_width = 120
|
||||
newline_style = "Unix"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,285 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use assertables::{assume, assume_eq};
|
||||
use sugars::boxed;
|
||||
|
||||
use anysystem::python::PyProcessFactory;
|
||||
use anysystem::test::TestResult;
|
||||
use anysystem::{Message, System};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TestConfig<'a> {
|
||||
pub impl_path: &'a str,
|
||||
pub sender_class: &'a str,
|
||||
pub receiver_class: &'a str,
|
||||
pub seed: u64,
|
||||
pub monkeys: u32,
|
||||
pub reliable: bool,
|
||||
pub once: bool,
|
||||
pub ordered: bool,
|
||||
}
|
||||
|
||||
pub fn build_system(config: &TestConfig, measure_max_size: bool) -> System {
|
||||
let mut sys = System::new(config.seed);
|
||||
sys.add_node("sender-node");
|
||||
sys.add_node("receiver-node");
|
||||
|
||||
let sender_f = PyProcessFactory::new(config.impl_path, config.sender_class);
|
||||
let mut sender = sender_f.build(("sender", "receiver"), config.seed);
|
||||
if measure_max_size {
|
||||
sender.set_max_size_freq(100);
|
||||
}
|
||||
sys.add_process("sender", boxed!(sender), "sender-node");
|
||||
|
||||
let receiver_f = PyProcessFactory::new(config.impl_path, config.receiver_class);
|
||||
let mut receiver = receiver_f.build(("receiver",), config.seed);
|
||||
if measure_max_size {
|
||||
receiver.set_max_size_freq(100);
|
||||
}
|
||||
sys.add_process("receiver", boxed!(receiver), "receiver-node");
|
||||
|
||||
sys
|
||||
}
|
||||
|
||||
pub fn generate_message_texts(sys: &mut System, message_count: usize) -> Vec<String> {
|
||||
if message_count == 5 {
|
||||
["distributed", "systems", "need", "some", "guarantees"]
|
||||
.map(String::from)
|
||||
.to_vec()
|
||||
} else {
|
||||
let mut messages = Vec::new();
|
||||
for _i in 0..message_count {
|
||||
let msg = if message_count == 10 {
|
||||
format!("{}C", sys.gen_range(20..30))
|
||||
} else {
|
||||
sys.random_string(100)
|
||||
};
|
||||
messages.push(msg);
|
||||
}
|
||||
messages
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_messages(sys: &mut System, message_count: usize) -> Vec<Message> {
|
||||
let texts = generate_message_texts(sys, message_count);
|
||||
let mut messages = Vec::new();
|
||||
for text in texts {
|
||||
let msg = Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#));
|
||||
sys.send_local_message("sender", msg.clone());
|
||||
if message_count <= 50 {
|
||||
let steps = sys.gen_range(0..2);
|
||||
if steps > 0 {
|
||||
sys.steps(steps);
|
||||
}
|
||||
} else {
|
||||
let duration = sys.gen_range(0.0..2.0);
|
||||
sys.step_for_duration(duration);
|
||||
};
|
||||
messages.push(msg);
|
||||
}
|
||||
messages
|
||||
}
|
||||
|
||||
pub fn check_delivered_messages(
|
||||
delivered: &[Message],
|
||||
expected_msg_count: &HashMap<String, i32>,
|
||||
expected_tip: &String,
|
||||
) -> Result<HashMap<String, i32>, String> {
|
||||
assert!(!expected_msg_count.is_empty());
|
||||
let mut delivered_msg_count = HashMap::default();
|
||||
for msg in delivered.iter() {
|
||||
// assuming all messages have the same type
|
||||
assume_eq!(msg.tip, *expected_tip, format!("Wrong message type {}", msg.tip))?;
|
||||
assume!(
|
||||
expected_msg_count.contains_key(&msg.data),
|
||||
format!("Wrong message data: {}", msg.data)
|
||||
)?;
|
||||
*delivered_msg_count.entry(msg.data.clone()).or_insert(0) += 1;
|
||||
}
|
||||
Ok(delivered_msg_count)
|
||||
}
|
||||
|
||||
pub fn check_message_delivery_reliable(
|
||||
delivered_msg_count: &HashMap<String, i32>,
|
||||
expected_msg_count: &HashMap<String, i32>,
|
||||
) -> TestResult {
|
||||
for (data, expected_count) in expected_msg_count {
|
||||
let delivered_count = delivered_msg_count.get(data).unwrap_or(&0);
|
||||
assume!(
|
||||
delivered_count >= expected_count,
|
||||
format!(
|
||||
"Message {} is not delivered (observed count {} < expected count {})",
|
||||
data, delivered_count, expected_count
|
||||
)
|
||||
)?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn check_message_delivery_once(
|
||||
delivered_msg_count: &HashMap<String, i32>,
|
||||
expected_msg_count: &HashMap<String, i32>,
|
||||
) -> TestResult {
|
||||
for (data, delivered_count) in delivered_msg_count {
|
||||
if expected_msg_count.contains_key(data) {
|
||||
let expected_count = expected_msg_count[data];
|
||||
assume!(
|
||||
*delivered_count <= expected_count,
|
||||
format!(
|
||||
"Message {} is delivered more than once (observed count {} > expected count {})",
|
||||
data, delivered_count, expected_count
|
||||
)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn check_message_delivery_ordered(delivered: &[Message], sent: &[Message]) -> TestResult {
|
||||
let mut next_idx = 0;
|
||||
for i in 0..delivered.len() {
|
||||
let msg = &delivered[i];
|
||||
let mut matched = false;
|
||||
while !matched && next_idx < sent.len() {
|
||||
if msg.data == sent[next_idx].data {
|
||||
matched = true;
|
||||
} else {
|
||||
next_idx += 1;
|
||||
}
|
||||
}
|
||||
assume!(
|
||||
matched,
|
||||
format!("Order violation: {} after {}", msg.data, &delivered[i - 1].data)
|
||||
)?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn check_guarantees(sys: &mut System, sent: &[Message], config: &TestConfig) -> TestResult {
|
||||
let mut expected_msg_count = HashMap::new();
|
||||
for msg in sent {
|
||||
*expected_msg_count.entry(msg.data.clone()).or_insert(0) += 1;
|
||||
}
|
||||
let delivered = sys.read_local_messages("receiver");
|
||||
|
||||
// check that delivered messages have expected type and data
|
||||
let delivered_msg_count = check_delivered_messages(&delivered, &expected_msg_count, &sent[0].tip)?;
|
||||
|
||||
// check delivered message count according to expected guarantees
|
||||
if config.reliable {
|
||||
check_message_delivery_reliable(&delivered_msg_count, &expected_msg_count)?;
|
||||
}
|
||||
if config.once {
|
||||
check_message_delivery_once(&delivered_msg_count, &expected_msg_count)?;
|
||||
}
|
||||
if config.ordered {
|
||||
check_message_delivery_ordered(&delivered, sent)?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn check_overhead(
|
||||
guarantee: &str,
|
||||
faulty: bool,
|
||||
message_count: usize,
|
||||
sender_mem: u64,
|
||||
receiver_mem: u64,
|
||||
net_message_count: u64,
|
||||
net_traffic: u64,
|
||||
throughput: f64,
|
||||
) -> TestResult {
|
||||
let (sender_mem_limit, receiver_mem_limit, net_message_count_limit, net_traffic_limit, throughput_limit) =
|
||||
match guarantee {
|
||||
"AMO" => match message_count {
|
||||
100 => {
|
||||
if !faulty {
|
||||
(800, 1500, 100, 20000, 0.6)
|
||||
} else {
|
||||
(800, 3500, 100, 20000, 0.6)
|
||||
}
|
||||
}
|
||||
1000 => {
|
||||
if !faulty {
|
||||
(800, 1500, 1000, 200000, 0.6)
|
||||
} else {
|
||||
(800, 30000, 1000, 200000, 0.6)
|
||||
}
|
||||
}
|
||||
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
|
||||
},
|
||||
"ALO" => match message_count {
|
||||
100 => {
|
||||
if !faulty {
|
||||
(1700, 600, 200, 20000, 0.6)
|
||||
} else {
|
||||
(5000, 600, 500, 40000, 0.6)
|
||||
}
|
||||
}
|
||||
1000 => {
|
||||
if !faulty {
|
||||
(2200, 600, 2000, 200000, 0.6)
|
||||
} else {
|
||||
(6000, 600, 5000, 400000, 0.6)
|
||||
}
|
||||
}
|
||||
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
|
||||
},
|
||||
"EO" => match message_count {
|
||||
100 => {
|
||||
if !faulty {
|
||||
(1700, 1500, 200, 20000, 0.6)
|
||||
} else {
|
||||
(5000, 2200, 500, 40000, 0.6)
|
||||
}
|
||||
}
|
||||
1000 => {
|
||||
if !faulty {
|
||||
(2200, 1500, 2000, 200000, 0.6)
|
||||
} else {
|
||||
(6000, 2200, 5000, 400000, 0.6)
|
||||
}
|
||||
}
|
||||
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
|
||||
},
|
||||
"EOO" => match message_count {
|
||||
100 => {
|
||||
if !faulty {
|
||||
(2900, 1200, 200, 25000, 0.4)
|
||||
} else {
|
||||
(9000, 2500, 500, 45000, 0.4)
|
||||
}
|
||||
}
|
||||
1000 => {
|
||||
if !faulty {
|
||||
(3400, 1200, 2000, 250000, 0.4)
|
||||
} else {
|
||||
(55000, 4000, 5000, 450000, 0.4)
|
||||
}
|
||||
}
|
||||
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
|
||||
},
|
||||
_ => (u64::MAX, u64::MAX, u64::MAX, u64::MAX, 0.),
|
||||
};
|
||||
assume!(
|
||||
sender_mem <= sender_mem_limit,
|
||||
format!("Sender memory > {}", sender_mem_limit)
|
||||
)?;
|
||||
assume!(
|
||||
receiver_mem <= receiver_mem_limit,
|
||||
format!("Receiver memory > {}", receiver_mem_limit)
|
||||
)?;
|
||||
assume!(
|
||||
net_message_count <= net_message_count_limit,
|
||||
format!("Message count > {}", net_message_count_limit)
|
||||
)?;
|
||||
assume!(
|
||||
net_traffic <= net_traffic_limit,
|
||||
format!("Traffic > {}", net_traffic_limit)
|
||||
)?;
|
||||
assume!(
|
||||
throughput >= throughput_limit,
|
||||
format!("Throughput < {}", throughput_limit)
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
mod common;
|
||||
mod shared_state;
|
||||
mod tests;
|
||||
mod tests_mc;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
|
||||
use clap::Parser;
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
|
||||
use anysystem::test::{TestResult, TestSuite};
|
||||
|
||||
use crate::common::TestConfig;
|
||||
use crate::tests::*;
|
||||
use crate::tests_mc::*;
|
||||
|
||||
/// Guarantees Homework Tests
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(about, long_about = None)]
|
||||
struct Args {
|
||||
/// Path to Python file with solution
|
||||
#[clap(long = "impl", short = 'i', default_value = "solution/guarantees.py")]
|
||||
solution_path: String,
|
||||
|
||||
/// Test to run (optional)
|
||||
#[clap(long = "test", short)]
|
||||
test: Option<String>,
|
||||
|
||||
/// Print execution trace
|
||||
#[clap(long, short)]
|
||||
debug: bool,
|
||||
|
||||
/// Guarantee to check
|
||||
#[clap(long, short, possible_values = ["AMO", "ALO", "EO", "EOO"])]
|
||||
guarantee: Option<String>,
|
||||
|
||||
/// Random seed used in tests
|
||||
#[clap(long, short, default_value = "123")]
|
||||
seed: u64,
|
||||
|
||||
/// Number of chaos monkey runs
|
||||
#[clap(long, short, default_value = "0")]
|
||||
monkeys: u32,
|
||||
|
||||
/// Run overhead tests
|
||||
#[clap(long, short)]
|
||||
overhead: bool,
|
||||
|
||||
/// Run model checking tests
|
||||
#[clap(long, short = 'c')]
|
||||
model_checking: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
if !shared_state::validate_or_report(
|
||||
&args.solution_path,
|
||||
&[
|
||||
"AtMostOnceSender",
|
||||
"AtMostOnceReceiver",
|
||||
"AtLeastOnceSender",
|
||||
"AtLeastOnceReceiver",
|
||||
"ExactlyOnceSender",
|
||||
"ExactlyOnceReceiver",
|
||||
"ExactlyOnceOrderedSender",
|
||||
"ExactlyOnceOrderedReceiver",
|
||||
],
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if args.debug {
|
||||
Builder::new()
|
||||
.filter(Some("anysystem"), LevelFilter::Debug)
|
||||
.format(|buf, record| writeln!(buf, "{}", record.args()))
|
||||
.init();
|
||||
}
|
||||
let guarantee = args.guarantee.as_deref();
|
||||
|
||||
env::set_var("PYTHONHASHSEED", args.seed.to_string());
|
||||
let mut config = TestConfig {
|
||||
impl_path: &args.solution_path,
|
||||
sender_class: "",
|
||||
receiver_class: "",
|
||||
seed: args.seed,
|
||||
monkeys: args.monkeys,
|
||||
reliable: false,
|
||||
once: false,
|
||||
ordered: false,
|
||||
};
|
||||
let mut tests = TestSuite::new();
|
||||
|
||||
// At most once
|
||||
if guarantee.is_none() || guarantee == Some("AMO") {
|
||||
config.sender_class = "AtMostOnceSender";
|
||||
config.receiver_class = "AtMostOnceReceiver";
|
||||
config.once = true;
|
||||
// without drops should be reliable
|
||||
config.reliable = true;
|
||||
tests.add("[AT MOST ONCE] NORMAL", test_normal, config);
|
||||
tests.add("[AT MOST ONCE] NORMAL NON-UNIQUE", test_normal_non_unique, config);
|
||||
tests.add("[AT MOST ONCE] DELAYED", test_delayed, config);
|
||||
tests.add("[AT MOST ONCE] DUPLICATED", test_duplicated, config);
|
||||
tests.add("[AT MOST ONCE] DELAYED+DUPLICATED", test_delayed_duplicated, config);
|
||||
tests.add("[AT MOST ONCE] OLD DUPLICATE", test_old_duplicate, config);
|
||||
// with drops is not reliable
|
||||
config.reliable = false;
|
||||
tests.add("[AT MOST ONCE] DROPPED", test_dropped, config);
|
||||
if args.monkeys > 0 {
|
||||
tests.add("[AT MOST ONCE] CHAOS MONKEY", test_chaos_monkey, config);
|
||||
}
|
||||
if args.overhead {
|
||||
config.reliable = true;
|
||||
tests.add(
|
||||
"[AT MOST ONCE] OVERHEAD NORMAL",
|
||||
|x| test_overhead(x, "AMO", false),
|
||||
config,
|
||||
);
|
||||
config.reliable = false;
|
||||
tests.add(
|
||||
"[AT MOST ONCE] OVERHEAD FAULTY",
|
||||
|x| test_overhead(x, "AMO", true),
|
||||
config,
|
||||
);
|
||||
}
|
||||
if args.model_checking {
|
||||
tests.add("[AT MOST ONCE] MODEL CHECKING", test_mc_reliable_network, config);
|
||||
tests.add(
|
||||
"[AT MOST ONCE] MODEL CHECKING MESSAGE DROPS",
|
||||
test_mc_message_drops,
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[AT MOST ONCE] MODEL CHECKING UNSTABLE NETWORK",
|
||||
test_mc_unstable_network,
|
||||
config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// At least once
|
||||
if guarantee.is_none() || guarantee == Some("ALO") {
|
||||
config.sender_class = "AtLeastOnceSender";
|
||||
config.receiver_class = "AtLeastOnceReceiver";
|
||||
config.reliable = true;
|
||||
config.once = false;
|
||||
tests.add("[AT LEAST ONCE] NORMAL", test_normal, config);
|
||||
tests.add("[AT LEAST ONCE] NORMAL NON-UNIQUE", test_normal_non_unique, config);
|
||||
tests.add("[AT LEAST ONCE] DELAYED", test_delayed, config);
|
||||
tests.add("[AT LEAST ONCE] DUPLICATED", test_duplicated, config);
|
||||
tests.add("[AT LEAST ONCE] DELAYED+DUPLICATED", test_delayed_duplicated, config);
|
||||
tests.add("[AT LEAST ONCE] DROPPED", test_dropped, config);
|
||||
if args.monkeys > 0 {
|
||||
tests.add("[AT LEAST ONCE] CHAOS MONKEY", test_chaos_monkey, config);
|
||||
}
|
||||
if args.overhead {
|
||||
tests.add(
|
||||
"[AT LEAST ONCE] OVERHEAD NORMAL",
|
||||
|x| test_overhead(x, "ALO", false),
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[AT LEAST ONCE] OVERHEAD FAULTY",
|
||||
|x| test_overhead(x, "ALO", true),
|
||||
config,
|
||||
);
|
||||
}
|
||||
if args.model_checking {
|
||||
tests.add("[AT LEAST ONCE] MODEL CHECKING", test_mc_reliable_network, config);
|
||||
tests.add(
|
||||
"[AT LEAST ONCE] MODEL CHECKING MESSAGE DROPS",
|
||||
test_mc_message_drops,
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[AT LEAST ONCE] MODEL CHECKING UNSTABLE NETWORK",
|
||||
test_mc_unstable_network,
|
||||
config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly once
|
||||
if guarantee.is_none() || guarantee == Some("EO") {
|
||||
config.sender_class = "ExactlyOnceSender";
|
||||
config.receiver_class = "ExactlyOnceReceiver";
|
||||
config.reliable = true;
|
||||
config.once = true;
|
||||
tests.add("[EXACTLY ONCE] NORMAL", test_normal, config);
|
||||
tests.add("[EXACTLY ONCE] NORMAL NON-UNIQUE", test_normal_non_unique, config);
|
||||
tests.add("[EXACTLY ONCE] DELAYED", test_delayed, config);
|
||||
tests.add("[EXACTLY ONCE] DUPLICATED", test_duplicated, config);
|
||||
tests.add("[EXACTLY ONCE] DELAYED+DUPLICATED", test_delayed_duplicated, config);
|
||||
tests.add("[EXACTLY ONCE] DROPPED", test_dropped, config);
|
||||
if args.monkeys > 0 {
|
||||
tests.add("[EXACTLY ONCE] CHAOS MONKEY", test_chaos_monkey, config);
|
||||
}
|
||||
if args.overhead {
|
||||
tests.add(
|
||||
"[EXACTLY ONCE] OVERHEAD NORMAL",
|
||||
|x| test_overhead(x, "EO", false),
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE] OVERHEAD FAULTY",
|
||||
|x| test_overhead(x, "EO", true),
|
||||
config,
|
||||
);
|
||||
}
|
||||
if args.model_checking {
|
||||
tests.add("[EXACTLY ONCE] MODEL CHECKING", test_mc_reliable_network, config);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE] MODEL CHECKING MESSAGE DROPS",
|
||||
test_mc_message_drops,
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE] MODEL CHECKING UNSTABLE NETWORK",
|
||||
test_mc_unstable_network,
|
||||
config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// EXACTLY ONCE ORDERED
|
||||
if guarantee.is_none() || guarantee == Some("EOO") {
|
||||
config.sender_class = "ExactlyOnceOrderedSender";
|
||||
config.receiver_class = "ExactlyOnceOrderedReceiver";
|
||||
config.reliable = true;
|
||||
config.once = true;
|
||||
config.ordered = true;
|
||||
tests.add("[EXACTLY ONCE ORDERED] NORMAL", test_normal, config);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE ORDERED] NORMAL NON-UNIQUE",
|
||||
test_normal_non_unique,
|
||||
config,
|
||||
);
|
||||
tests.add("[EXACTLY ONCE ORDERED] DELAYED", test_delayed, config);
|
||||
tests.add("[EXACTLY ONCE ORDERED] DUPLICATED", test_duplicated, config);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE ORDERED] DELAYED+DUPLICATED",
|
||||
test_delayed_duplicated,
|
||||
config,
|
||||
);
|
||||
tests.add("[EXACTLY ONCE ORDERED] DROPPED", test_dropped, config);
|
||||
if args.monkeys > 0 {
|
||||
tests.add("[EXACTLY ONCE ORDERED] CHAOS MONKEY", test_chaos_monkey, config);
|
||||
}
|
||||
if args.overhead {
|
||||
tests.add(
|
||||
"[EXACTLY ONCE ORDERED] OVERHEAD NORMAL",
|
||||
|x| test_overhead(x, "EOO", false),
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE ORDERED] OVERHEAD FAULTY",
|
||||
|x| test_overhead(x, "EOO", true),
|
||||
config,
|
||||
);
|
||||
}
|
||||
if args.model_checking {
|
||||
tests.add(
|
||||
"[EXACTLY ONCE ORDERED] MODEL CHECKING",
|
||||
test_mc_reliable_network,
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE ORDERED] MODEL CHECKING MESSAGE DROPS",
|
||||
test_mc_message_drops,
|
||||
config,
|
||||
);
|
||||
tests.add(
|
||||
"[EXACTLY ONCE ORDERED] MODEL CHECKING UNSTABLE NETWORK",
|
||||
test_mc_unstable_network,
|
||||
config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(test) = &args.test {
|
||||
tests.run_test(test);
|
||||
} else {
|
||||
let (_, results) = tests.run();
|
||||
let score = score(results);
|
||||
println!("SCORE: {score}\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn score(results: IndexMap<String, TestResult>) -> f32 {
|
||||
let guarantees = HashSet::from(["AT MOST ONCE", "AT LEAST ONCE", "EXACTLY ONCE", "EXACTLY ONCE ORDERED"]);
|
||||
let mut failed_guarantees: HashSet<&str> = HashSet::new();
|
||||
let mut failed_overheads: HashSet<&str> = HashSet::new();
|
||||
for (test, result) in results {
|
||||
if result.is_err() {
|
||||
for guarantee in guarantees.iter() {
|
||||
if test.contains(format!("[{guarantee}]").as_str()) {
|
||||
if test.contains("OVERHEAD") {
|
||||
failed_overheads.insert(guarantee);
|
||||
} else {
|
||||
failed_guarantees.insert(guarantee);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9. - failed_guarantees.len() as f32 * 2. - f32::from(!failed_overheads.is_empty())
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
use std::ffi::CString;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyList, PyModule};
|
||||
|
||||
const VALIDATOR_CODE: &str = include_str!("../shared_state_validator.py");
|
||||
|
||||
fn validator_module<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyModule>> {
|
||||
let code = CString::new(VALIDATOR_CODE).expect("validator source contains a null byte");
|
||||
PyModule::from_code(py, &code, c"shared_state_validator.py", c"shared_state_validator")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn validate_source(source: &str, filename: &str, class_names: &[&str]) -> Result<Vec<String>, String> {
|
||||
Python::attach(|py| {
|
||||
let module = validator_module(py).map_err(|error| error.to_string())?;
|
||||
let class_names = PyList::new(py, class_names).map_err(|error| error.to_string())?;
|
||||
module
|
||||
.getattr("validate_source")
|
||||
.and_then(|function| function.call1((source, filename, class_names)))
|
||||
.and_then(|result| result.extract::<Vec<String>>())
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_solution(path: &str, class_names: &[&str]) -> Result<Vec<String>, String> {
|
||||
Python::attach(|py| {
|
||||
let module = validator_module(py).map_err(|error| error.to_string())?;
|
||||
let class_names = PyList::new(py, class_names).map_err(|error| error.to_string())?;
|
||||
module
|
||||
.getattr("validate_solution")
|
||||
.and_then(|function| function.call1((path, class_names)))
|
||||
.and_then(|result| result.extract::<Vec<String>>())
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_or_report(path: &str, class_names: &[&str]) -> bool {
|
||||
match validate_solution(path, class_names) {
|
||||
Ok(violations) if violations.is_empty() => true,
|
||||
Ok(violations) => {
|
||||
println!("ERROR: shared state between AnySystem processes is forbidden");
|
||||
for violation in violations {
|
||||
println!("{violation}");
|
||||
}
|
||||
println!("\nSCORE: 0\n");
|
||||
false
|
||||
}
|
||||
Err(error) => panic!("failed to validate solution for shared state: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{validate_solution, validate_source};
|
||||
|
||||
const PROCESS: &[&str] = &["ProcessImpl"];
|
||||
|
||||
fn violations(source: &str) -> Vec<String> {
|
||||
validate_source(source, "solution.py", PROCESS).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_imports_helpers_constants_and_instance_state() {
|
||||
let source = r#"
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from types import MappingProxyType
|
||||
|
||||
LIMIT = 2 ** 10
|
||||
NAMES = ("a", "b")
|
||||
LOOKUP = MappingProxyType({"a": 1, "b": 2})
|
||||
|
||||
@dataclass
|
||||
class Helper:
|
||||
values: list[str] = field(default_factory=list)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Status:
|
||||
ok: bool
|
||||
|
||||
class Kind(Enum):
|
||||
READY = auto()
|
||||
|
||||
class ProcessImpl:
|
||||
RETRIES = 3
|
||||
READY = Status(True)
|
||||
|
||||
def __init__(self):
|
||||
self.values = []
|
||||
self.helper = Helper()
|
||||
|
||||
def choose(self):
|
||||
return random.choice(NAMES)
|
||||
"#;
|
||||
assert_eq!(violations(source), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_local_imports_without_executing_them() {
|
||||
let suffix = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"distsys-shared-state-validator-{}-{suffix}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
let entrypoint = directory.join("solution.py");
|
||||
std::fs::write(&entrypoint, "import helper\n\nclass ProcessImpl:\n pass\n").unwrap();
|
||||
std::fs::write(
|
||||
directory.join("helper.py"),
|
||||
"class Shared:\n data = {}\n\nraise RuntimeError('must not execute')\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = validate_solution(entrypoint.to_str().unwrap(), PROCESS);
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
|
||||
let violations = result.unwrap();
|
||||
assert_eq!(violations.len(), 1);
|
||||
assert!(violations[0].contains("Shared.data"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_shared_state_hidden_behind_helper_class() {
|
||||
let source = r#"
|
||||
from enum import Enum
|
||||
|
||||
class Shared:
|
||||
data = {}
|
||||
|
||||
@classmethod
|
||||
def data_ref(cls):
|
||||
return cls.data
|
||||
|
||||
class UnsafeKind(Enum):
|
||||
VALUES = []
|
||||
|
||||
class ProcessImpl:
|
||||
def write(self):
|
||||
Shared.data_ref()["key"] = "value"
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 2);
|
||||
assert!(violations.iter().any(|item| item.contains("Shared.data")));
|
||||
assert!(violations.iter().any(|item| item.contains("UnsafeKind.VALUES")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_module_and_process_class_state() {
|
||||
let source = r#"
|
||||
CACHE = {}
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrozenButMutable:
|
||||
values: list[int] = field(default_factory=list)
|
||||
|
||||
BROKEN = FrozenButMutable()
|
||||
|
||||
class Base:
|
||||
pending = set()
|
||||
|
||||
class ProcessImpl(Base):
|
||||
values = []
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 4);
|
||||
assert!(violations.iter().any(|message| message.contains("CACHE")));
|
||||
assert!(violations.iter().any(|message| message.contains("BROKEN")));
|
||||
assert!(violations.iter().any(|message| message.contains("Base.pending")));
|
||||
assert!(violations.iter().any(|message| message.contains("ProcessImpl.values")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_module_level_dynamic_shared_state() {
|
||||
let source = r#"
|
||||
def helper():
|
||||
pass
|
||||
|
||||
class ProcessImpl:
|
||||
COUNT = 0
|
||||
|
||||
alias = ProcessImpl
|
||||
alias.cache = {}
|
||||
helper.cache = []
|
||||
setattr(ProcessImpl, "pending", set())
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 3);
|
||||
assert!(violations
|
||||
.iter()
|
||||
.all(|message| message.contains("module-level shared state")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mutable_defaults_and_scope_state() {
|
||||
let source = r#"
|
||||
class ProcessImpl:
|
||||
def on_message(self, message, cache={}):
|
||||
global counter
|
||||
def next_value():
|
||||
nonlocal message
|
||||
return message
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_shared_but_accepts_local_closure_state() {
|
||||
let source = r#"
|
||||
def make_counter():
|
||||
count = 0
|
||||
def next_value():
|
||||
nonlocal count
|
||||
count += 1
|
||||
return count
|
||||
return next_value
|
||||
|
||||
SHARED_COUNTER = make_counter()
|
||||
|
||||
class ProcessImpl:
|
||||
def local_counter(self):
|
||||
count = 0
|
||||
def next_value():
|
||||
nonlocal count
|
||||
count += 1
|
||||
return count
|
||||
return next_value()
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 2);
|
||||
assert!(violations.iter().any(|message| message.contains("shared closure")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_getattr_eval_and_shared_cache_decorator() {
|
||||
let source = r#"
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache
|
||||
def cached_value(key):
|
||||
return key
|
||||
|
||||
class ProcessImpl:
|
||||
cache = ()
|
||||
|
||||
def on_message(self, message):
|
||||
getattr(type(self), "cache").clear()
|
||||
eval("globals()")
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 3);
|
||||
assert!(violations.iter().any(|message| message.contains("lru_cache")));
|
||||
assert!(violations.iter().any(|message| message.contains("clear")));
|
||||
assert!(violations.iter().any(|message| message.contains("eval")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_direct_aliased_and_reflective_mutation() {
|
||||
let source = r#"
|
||||
class ProcessImpl:
|
||||
COUNT = 0
|
||||
|
||||
def on_message(self, message):
|
||||
cls = type(self)
|
||||
state = cls.__dict__
|
||||
cls.COUNT += 1
|
||||
state.update({"x": 1})
|
||||
setattr(cls, "other", [])
|
||||
globals()["hidden"] = {}
|
||||
exec("hidden = {}")
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations.len() >= 5);
|
||||
assert!(violations.iter().any(|message| message.contains("assignment mutates")));
|
||||
assert!(violations.iter().any(|message| message.contains("setattr")));
|
||||
assert!(violations.iter().any(|message| message.contains("globals")));
|
||||
assert!(violations.iter().any(|message| message.contains("exec")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_spoofed_safe_names_and_aliased_cache_decorator() {
|
||||
let source = r#"
|
||||
def tuple():
|
||||
return []
|
||||
|
||||
def MappingProxyType(value):
|
||||
return value
|
||||
|
||||
def dataclass(*args, **kwargs):
|
||||
def wrap(cls):
|
||||
return cls
|
||||
return wrap
|
||||
|
||||
SHARED_TUPLE = tuple()
|
||||
SHARED_MAPPING = MappingProxyType({})
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Mutable:
|
||||
pass
|
||||
|
||||
SHARED_OBJECT = Mutable()
|
||||
|
||||
from functools import lru_cache as memo
|
||||
|
||||
@memo
|
||||
def cached(value):
|
||||
return value
|
||||
|
||||
class ProcessImpl:
|
||||
pass
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations.iter().any(|message| message.contains("SHARED_TUPLE")));
|
||||
assert!(violations.iter().any(|message| message.contains("SHARED_MAPPING")));
|
||||
assert!(violations.iter().any(|message| message.contains("SHARED_OBJECT")));
|
||||
assert!(violations.iter().any(|message| message.contains("lru_cache")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_imported_and_function_object_state() {
|
||||
let source = r#"
|
||||
import math
|
||||
import sys
|
||||
|
||||
SHARED = sys.modules
|
||||
holder = lambda: None
|
||||
module_alias = math
|
||||
math.shared = {}
|
||||
holder.shared = {}
|
||||
module_alias.other = {}
|
||||
|
||||
class ProcessImpl:
|
||||
def __init__(self):
|
||||
self.shared = sys.modules
|
||||
|
||||
def write(self):
|
||||
local_alias = math
|
||||
local_alias.more = {}
|
||||
self.shared["hidden"] = {}
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations.iter().any(|message| message.contains("SHARED")));
|
||||
assert!(violations.len() >= 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_constructor_as_mutable_default() {
|
||||
let source = r#"
|
||||
class Box:
|
||||
def __init__(self):
|
||||
self.values = {}
|
||||
|
||||
class ProcessImpl:
|
||||
def on_message(self, message, box=Box()):
|
||||
pass
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations
|
||||
.iter()
|
||||
.any(|message| message.contains("mutable default argument")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_dunder_and_class_namespace_reflection() {
|
||||
let source = r#"
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Frozen:
|
||||
value: int
|
||||
|
||||
SHARED = Frozen(1)
|
||||
|
||||
class ProcessImpl:
|
||||
locals()["shared"] = {}
|
||||
|
||||
def on_message(self, message):
|
||||
type.__setattr__(ProcessImpl, "cache", {})
|
||||
object.__setattr__(SHARED, "value", 2)
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations
|
||||
.iter()
|
||||
.any(|message| message.contains("class-level shared state")));
|
||||
assert!(violations.iter().any(|message| message.contains("__setattr__")));
|
||||
assert!(violations.len() >= 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mutating_frozen_dataclass_methods() {
|
||||
let source = r#"
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Frozen:
|
||||
value: int = 1
|
||||
|
||||
def __post_init__(self):
|
||||
object.__setattr__(self, "state", {})
|
||||
|
||||
def put(self, key, value):
|
||||
self.state[key] = value
|
||||
|
||||
GLOBAL = Frozen()
|
||||
|
||||
class ProcessImpl:
|
||||
def write(self):
|
||||
GLOBAL.put("x", 1)
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations.iter().any(|message| message.contains("GLOBAL")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_dunder_and_operator_item_mutation() {
|
||||
let source = r#"
|
||||
import math
|
||||
import operator
|
||||
|
||||
class ProcessImpl:
|
||||
def write(self):
|
||||
math.__dict__.__setitem__("shared", {})
|
||||
operator.setitem(math.__dict__, "other", {})
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations.len() >= 2);
|
||||
assert!(violations.iter().any(|message| message.contains("__setitem__")));
|
||||
assert!(violations.iter().any(|message| message.contains("operator")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_class_local_and_star_cache_decorators() {
|
||||
let source = r#"
|
||||
from functools import *
|
||||
|
||||
@lru_cache
|
||||
def module_cached():
|
||||
return []
|
||||
|
||||
class ProcessImpl:
|
||||
from functools import lru_cache as memo
|
||||
|
||||
@staticmethod
|
||||
@memo
|
||||
def shared():
|
||||
return []
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(
|
||||
violations
|
||||
.iter()
|
||||
.filter(|message| message.contains("lru_cache"))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_literal_dynamic_local_imports() {
|
||||
let suffix = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"distsys-dynamic-import-validator-{}-{suffix}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
let entrypoint = directory.join("solution.py");
|
||||
std::fs::write(
|
||||
&entrypoint,
|
||||
"from importlib import import_module as load\n\nPREFIX = 'hel'\nMODULE = PREFIX + 'per'\n\nclass ProcessImpl:\n def __init__(self):\n self.shared = load(MODULE).CACHE\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(directory.join("helper.py"), "CACHE = {}\n").unwrap();
|
||||
|
||||
let result = validate_solution(entrypoint.to_str().unwrap(), PROCESS);
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
|
||||
let violations = result.unwrap();
|
||||
assert_eq!(violations.len(), 1);
|
||||
assert!(violations[0].contains("CACHE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_shared_values_returned_by_wrappers_and_wrapped_cache() {
|
||||
let source = r#"
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
|
||||
def get_shared():
|
||||
return sys.modules
|
||||
|
||||
def memo(function):
|
||||
return lru_cache(function)
|
||||
|
||||
@memo
|
||||
def shared():
|
||||
return []
|
||||
|
||||
class ProcessImpl:
|
||||
def __init__(self):
|
||||
self.shared = get_shared()
|
||||
|
||||
def write(self):
|
||||
self.shared["key"] = shared()
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert!(violations.iter().any(|message| message.contains("assignment mutates")));
|
||||
assert!(violations.iter().any(|message| message.contains("memo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_aliased_mutator_callables() {
|
||||
let source = r#"
|
||||
import math
|
||||
import operator
|
||||
|
||||
class ProcessImpl:
|
||||
def write(self):
|
||||
mutate = operator.setitem
|
||||
mutate(math.__dict__, "first", {})
|
||||
bound_mutate = math.__dict__.__setitem__
|
||||
bound_mutate("second", {})
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 2);
|
||||
assert!(violations.iter().any(|message| message.contains("operator")));
|
||||
assert!(violations.iter().any(|message| message.contains("aliased")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unresolved_dynamic_local_imports() {
|
||||
let suffix = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"distsys-unresolved-import-validator-{}-{suffix}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
let entrypoint = directory.join("solution.py");
|
||||
std::fs::write(
|
||||
&entrypoint,
|
||||
"from importlib import import_module as load\n\nclass ProcessImpl:\n def write(self):\n name = ''.join(['helper'])\n state = load(name).CACHE\n state['key'] = 1\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(directory.join("helper.py"), "CACHE = {}\n").unwrap();
|
||||
|
||||
let result = validate_solution(entrypoint.to_str().unwrap(), PROCESS);
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
|
||||
let violations = result.unwrap();
|
||||
assert_eq!(violations.len(), 1);
|
||||
assert!(violations[0].contains("cannot be resolved statically"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_stateful_imported_api_but_allows_reviewed_calls() {
|
||||
let source = r#"
|
||||
import random
|
||||
from random import seed as direct_seed
|
||||
|
||||
random.seed(1)
|
||||
|
||||
class ProcessImpl:
|
||||
def write(self, value):
|
||||
direct_seed(value)
|
||||
reseed = random.seed
|
||||
reseed(value)
|
||||
|
||||
def read(self):
|
||||
return random.getstate()
|
||||
|
||||
def choose(self):
|
||||
return random.choice((1, 2))
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 4);
|
||||
assert!(violations.iter().any(|message| message.contains("random.seed")));
|
||||
assert!(violations.iter().any(|message| message.contains("random.getstate")));
|
||||
assert!(!violations.iter().any(|message| message.contains("random.choice")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_passthrough_and_lambda_shared_references() {
|
||||
let source = r#"
|
||||
import sys
|
||||
|
||||
def identity(value):
|
||||
return value
|
||||
|
||||
get_shared = lambda: sys.modules
|
||||
lambda_identity = lambda value: value
|
||||
|
||||
class ProcessImpl:
|
||||
def __init__(self):
|
||||
self.first = identity(sys.modules)
|
||||
self.second = get_shared()
|
||||
self.third = lambda_identity(sys.modules)
|
||||
|
||||
def write(self):
|
||||
self.first["first"] = {}
|
||||
self.second["second"] = {}
|
||||
self.third["third"] = {}
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 3);
|
||||
assert!(violations.iter().all(|message| message.contains("assignment mutates")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_getattr_hidden_imported_api_and_mutator() {
|
||||
let source = r#"
|
||||
import math
|
||||
import random
|
||||
|
||||
class ProcessImpl:
|
||||
def write(self, value):
|
||||
getattr(random, "seed")(value)
|
||||
getattr(math.__dict__, "__setitem__")("shared", {})
|
||||
"#;
|
||||
let violations = violations(source);
|
||||
assert_eq!(violations.len(), 2);
|
||||
assert!(violations.iter().any(|message| message.contains("random.seed")));
|
||||
assert!(violations.iter().any(|message| message.contains("__setitem__")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
use std::fs;
|
||||
|
||||
use assertables::assume;
|
||||
use rand::prelude::*;
|
||||
use rand_pcg::Pcg64;
|
||||
|
||||
use anysystem::test::TestResult;
|
||||
use anysystem::Message;
|
||||
|
||||
use crate::common::{build_system, check_guarantees, check_overhead, send_messages, TestConfig};
|
||||
|
||||
pub fn test_normal(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
let messages = send_messages(&mut sys, 5);
|
||||
sys.step_until_no_events();
|
||||
check_guarantees(&mut sys, &messages, config)?;
|
||||
// We expect no more than 5 messages from sender in normal network conditions
|
||||
let sent_count = sys.sent_message_count("sender");
|
||||
assume!(
|
||||
sent_count <= 5,
|
||||
format!("Sender sent {} messages, expected at most 5", sent_count)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn test_normal_non_unique(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
let messages = send_messages(&mut sys, 10);
|
||||
sys.step_until_no_events();
|
||||
check_guarantees(&mut sys, &messages, config)?;
|
||||
// We expect no more than 10 messages from sender in normal network conditions (stable delay, no loss).
|
||||
// If solution sends multiple messages without or with too small (<RTT) delay, this results in extra redundant
|
||||
// traffic. We want to avoid wasting network resources, assuming that normal conditions happen most of the time.
|
||||
let sent_count = sys.sent_message_count("sender");
|
||||
assume!(
|
||||
sent_count <= 10,
|
||||
format!("Sender sent {} messages, expected at most 10", sent_count)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn test_delayed(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
sys.network().set_delays(1., 3.);
|
||||
let messages = send_messages(&mut sys, 5);
|
||||
sys.step_until_no_events();
|
||||
check_guarantees(&mut sys, &messages, config)
|
||||
}
|
||||
|
||||
pub fn test_duplicated(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
sys.network().set_dupl_rate(0.3);
|
||||
let messages = send_messages(&mut sys, 5);
|
||||
sys.step_until_no_events();
|
||||
check_guarantees(&mut sys, &messages, config)
|
||||
}
|
||||
|
||||
pub fn test_delayed_duplicated(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
sys.network().set_delays(1., 3.);
|
||||
sys.network().set_dupl_rate(0.3);
|
||||
let messages = send_messages(&mut sys, 5);
|
||||
sys.step_until_no_events();
|
||||
check_guarantees(&mut sys, &messages, config)
|
||||
}
|
||||
|
||||
pub fn test_old_duplicate(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
sys.network().set_delays(1., 3.);
|
||||
sys.network().set_dupl_rate(1.);
|
||||
|
||||
let first = Message::new("MESSAGE", r#"{"text": "first"}"#);
|
||||
let mut messages = vec![first.clone()];
|
||||
sys.send_local_message("sender", first);
|
||||
|
||||
// Stop after the first copy is delivered.
|
||||
while sys.local_outbox("receiver").is_empty() {
|
||||
if !sys.step() {
|
||||
return Err("The first message was not delivered".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the old duplicate pending while newer messages are delivered first.
|
||||
// Zero-delay messages are processed before the delayed copy.
|
||||
sys.network().set_dupl_rate(0.);
|
||||
sys.network().set_delay(0.);
|
||||
for i in 0..50 {
|
||||
let msg = Message::new("MESSAGE", &format!(r#"{{"text": "message-{i}"}}"#));
|
||||
sys.send_local_message("sender", msg.clone());
|
||||
messages.push(msg);
|
||||
}
|
||||
|
||||
sys.step_until_no_events();
|
||||
check_guarantees(&mut sys, &messages, config)
|
||||
}
|
||||
|
||||
pub fn test_dropped(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
sys.network().set_drop_rate(0.3);
|
||||
let messages = send_messages(&mut sys, 5);
|
||||
sys.step_until_no_events();
|
||||
check_guarantees(&mut sys, &messages, config)
|
||||
}
|
||||
|
||||
pub fn test_chaos_monkey(config: &TestConfig) -> TestResult {
|
||||
let mut rand = Pcg64::seed_from_u64(config.seed);
|
||||
for i in 1..=config.monkeys {
|
||||
let mut run_config = *config;
|
||||
run_config.seed = rand.next_u64();
|
||||
println!("Run {} (seed: {})", i, run_config.seed);
|
||||
let mut sys = build_system(&run_config, false);
|
||||
sys.network().set_delays(1., 3.);
|
||||
sys.network().set_dupl_rate(0.3);
|
||||
sys.network().set_drop_rate(0.3);
|
||||
let messages = send_messages(&mut sys, 50);
|
||||
sys.step_until_no_events();
|
||||
let res = check_guarantees(&mut sys, &messages, &run_config);
|
||||
res.as_ref()?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn test_overhead(config: &TestConfig, guarantee: &str, faulty: bool) -> TestResult {
|
||||
for message_count in [100, 500, 1000] {
|
||||
let mut sys = build_system(config, true);
|
||||
if faulty {
|
||||
sys.network().set_delays(1., 3.);
|
||||
sys.network().set_dupl_rate(0.3);
|
||||
sys.network().set_drop_rate(0.3);
|
||||
}
|
||||
let messages = send_messages(&mut sys, message_count);
|
||||
sys.step_until_no_events();
|
||||
let res = check_guarantees(&mut sys, &messages, config);
|
||||
res.as_ref()?;
|
||||
let sender_mem = sys.max_size("sender");
|
||||
let receiver_mem = sys.max_size("receiver");
|
||||
let net_message_count = sys.network().network_message_count();
|
||||
let net_traffic = sys.network().traffic();
|
||||
let throughput = message_count as f64 / sys.time();
|
||||
println!(
|
||||
"{message_count:<6} Send Mem: {sender_mem:<8} Recv Mem: {receiver_mem:<8} Messages: {net_message_count:<8} Traffic: {net_traffic:<8} Throughput: {throughput:.3}"
|
||||
);
|
||||
check_overhead(
|
||||
guarantee,
|
||||
faulty,
|
||||
message_count,
|
||||
sender_mem,
|
||||
receiver_mem,
|
||||
net_message_count,
|
||||
net_traffic,
|
||||
throughput,
|
||||
)?;
|
||||
}
|
||||
let impl_code = fs::read_to_string(config.impl_path).unwrap();
|
||||
assume!(
|
||||
!impl_code.contains("<<") && !impl_code.contains(">>"),
|
||||
"Implementation contains bitwise shift operators"
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use sugars::boxed;
|
||||
|
||||
use anysystem::logger::LogEntry;
|
||||
use anysystem::mc::{
|
||||
predicates::{goals, invariants, prunes},
|
||||
strategies::Bfs,
|
||||
InvariantFn, ModelChecker, StrategyConfig,
|
||||
};
|
||||
use anysystem::test::TestResult;
|
||||
use anysystem::Message;
|
||||
|
||||
use crate::common::{
|
||||
build_system, check_delivered_messages, check_message_delivery_once, check_message_delivery_ordered,
|
||||
check_message_delivery_reliable, generate_message_texts, TestConfig,
|
||||
};
|
||||
|
||||
fn mc_invariant_guarantees(messages_expected: Vec<Message>, config: TestConfig) -> InvariantFn {
|
||||
boxed!(move |state| {
|
||||
let mut expected_msg_count = HashMap::new();
|
||||
for msg in &messages_expected {
|
||||
*expected_msg_count.entry(msg.data.clone()).or_insert(0) += 1;
|
||||
}
|
||||
let delivered = &state.node_states["receiver-node"].proc_states["receiver"].local_outbox;
|
||||
|
||||
// check that delivered messages have expected type and data
|
||||
let delivered_msg_count = check_delivered_messages(delivered, &expected_msg_count, &messages_expected[0].tip)?;
|
||||
|
||||
// check delivered message count according to expected guarantees
|
||||
if config.reliable && state.events.is_empty() {
|
||||
check_message_delivery_reliable(&delivered_msg_count, &expected_msg_count)?;
|
||||
}
|
||||
if config.once {
|
||||
check_message_delivery_once(&delivered_msg_count, &expected_msg_count)?;
|
||||
}
|
||||
if config.ordered {
|
||||
check_message_delivery_ordered(delivered, &messages_expected)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn test_mc_reliable_network(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
let messages: Vec<Message> = generate_message_texts(&mut sys, 2)
|
||||
.into_iter()
|
||||
.map(|text| Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#)))
|
||||
.collect();
|
||||
let strategy_config = StrategyConfig::default()
|
||||
.prune(prunes::sent_messages_limit(4))
|
||||
.goal(goals::got_n_local_messages("receiver-node", "receiver", 2))
|
||||
.invariant(invariants::all_invariants(vec![
|
||||
invariants::state_depth(20),
|
||||
mc_invariant_guarantees(messages.clone(), *config),
|
||||
]));
|
||||
let mut mc = ModelChecker::new(&sys);
|
||||
let res = mc.run_with_change::<Bfs>(strategy_config, move |sys| {
|
||||
for message in messages {
|
||||
sys.send_local_message("sender-node", "sender", message);
|
||||
}
|
||||
});
|
||||
if let Err(e) = res {
|
||||
e.print_trace();
|
||||
Err(e.message())
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_mc_message_drops(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
sys.network().set_drop_rate(0.1);
|
||||
let messages: Vec<Message> = generate_message_texts(&mut sys, 2)
|
||||
.into_iter()
|
||||
.map(|text| Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#)))
|
||||
.collect();
|
||||
let strategy_config = StrategyConfig::default()
|
||||
.prune(prunes::state_depth(7))
|
||||
.goal(goals::any_goal(vec![
|
||||
goals::got_n_local_messages("receiver-node", "receiver", 2),
|
||||
goals::no_events(),
|
||||
]))
|
||||
.invariant(mc_invariant_guarantees(messages.clone(), *config));
|
||||
let mut mc = ModelChecker::new(&sys);
|
||||
let res = mc.run_with_change::<Bfs>(strategy_config, move |sys| {
|
||||
for message in messages {
|
||||
sys.send_local_message("sender-node", "sender", message);
|
||||
}
|
||||
});
|
||||
if let Err(e) = res {
|
||||
e.print_trace();
|
||||
Err(e.message())
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_mc_unstable_network(config: &TestConfig) -> TestResult {
|
||||
let mut sys = build_system(config, false);
|
||||
sys.network().set_drop_rate(0.1);
|
||||
sys.network().set_dupl_rate(0.1);
|
||||
let msg_count = if config.ordered { 3 } else { 2 };
|
||||
let messages: Vec<Message> = generate_message_texts(&mut sys, msg_count)
|
||||
.into_iter()
|
||||
.map(|text| Message::new("MESSAGE", &format!(r#"{{"text": "{text}"}}"#)))
|
||||
.collect();
|
||||
let num_drops_allowed = 1;
|
||||
let num_duplication_allowed = 1;
|
||||
let goal = if config.reliable && config.once {
|
||||
goals::all_goals(vec![
|
||||
goals::got_n_local_messages("receiver-node", "receiver", msg_count),
|
||||
goals::no_events(),
|
||||
])
|
||||
} else {
|
||||
goals::no_events()
|
||||
};
|
||||
let mut invariants = vec![
|
||||
invariants::state_depth(20),
|
||||
mc_invariant_guarantees(messages.clone(), *config),
|
||||
];
|
||||
if config.ordered {
|
||||
invariants.push(invariants::time_limit(Duration::from_secs(80)))
|
||||
};
|
||||
let strategy_config = StrategyConfig::default()
|
||||
.prune(prunes::any_prune(vec![
|
||||
prunes::events_limit(LogEntry::is_mc_message_dropped, num_drops_allowed),
|
||||
prunes::events_limit(LogEntry::is_mc_message_duplicated, num_duplication_allowed),
|
||||
prunes::events_limit(LogEntry::is_mc_timer_fired, 1),
|
||||
prunes::events_limit(LogEntry::is_mc_message_received, msg_count + num_drops_allowed),
|
||||
]))
|
||||
.goal(goal)
|
||||
.invariant(invariants::all_invariants(invariants));
|
||||
let mut mc = ModelChecker::new(&sys);
|
||||
|
||||
let res = mc.run_with_change::<Bfs>(strategy_config, |sys| {
|
||||
for msg in messages {
|
||||
sys.send_local_message("sender-node", "sender", msg.clone());
|
||||
}
|
||||
});
|
||||
if let Err(e) = res {
|
||||
e.print_trace();
|
||||
Err(e.message())
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
+750
@@ -0,0 +1,750 @@
|
||||
"""Portable student CLI for registration and solution submission."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unicodedata
|
||||
import uuid
|
||||
import zipfile
|
||||
from collections.abc import Callable, Sequence
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
import httpx
|
||||
import yaml
|
||||
|
||||
DEFAULT_CONFIG = Path(__file__).resolve().with_name("cli.yaml")
|
||||
TOTAL_REQUEST_TIMEOUT_SECONDS = 120.0
|
||||
SUCCESS_FIELDS = frozenset({"username", "password", "repo", "token"})
|
||||
ERROR_FIELDS = frozenset({"code", "message", "request_id"})
|
||||
REGISTER_ERROR_CODES = {
|
||||
400: frozenset({"INVALID_REQUEST", "STUDENT_NOT_FOUND"}),
|
||||
401: frozenset({"INVALID_REGISTRATION_TOKEN"}),
|
||||
409: frozenset({"STUDENT_ALREADY_REGISTERED", "EMAIL_ALREADY_REGISTERED"}),
|
||||
413: frozenset({"PAYLOAD_TOO_LARGE"}),
|
||||
415: frozenset({"UNSUPPORTED_MEDIA_TYPE"}),
|
||||
500: frozenset({"INTERNAL_ERROR"}),
|
||||
502: frozenset({"GITEA_OPERATION_FAILED"}),
|
||||
503: frozenset({"CONFIG_INVALID"}),
|
||||
}
|
||||
SUBMIT_ERROR_CODES = {
|
||||
400: frozenset({"INVALID_REQUEST", "INVALID_ARCHIVE"}),
|
||||
401: frozenset({"ACCESS_TOKEN_REQUIRED", "INVALID_ACCESS_TOKEN"}),
|
||||
403: frozenset({"SUBMISSION_DEADLINE_EXPIRED"}),
|
||||
404: frozenset({"ASSIGNMENT_NOT_FOUND"}),
|
||||
409: frozenset({"SUBMISSION_IN_PROGRESS", "SOLUTION_UNCHANGED"}),
|
||||
413: frozenset({"PAYLOAD_TOO_LARGE"}),
|
||||
415: frozenset({"UNSUPPORTED_MEDIA_TYPE"}),
|
||||
500: frozenset({"INTERNAL_ERROR"}),
|
||||
502: frozenset({"GITEA_OPERATION_FAILED"}),
|
||||
503: frozenset({"CONFIG_INVALID"}),
|
||||
}
|
||||
SUBMIT_SUCCESS_FIELDS = frozenset(
|
||||
{"submission_id", "accepted_at", "late_submission", "workflow_run"}
|
||||
)
|
||||
ASSIGNMENT_ID_RE = re.compile(r"[a-z0-9][a-z0-9-]{0,63}\Z")
|
||||
_DRIVE_PATH_RE = re.compile(r"[A-Za-z]:")
|
||||
_UUID_RE = re.compile(
|
||||
r"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-"
|
||||
r"[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\Z"
|
||||
)
|
||||
_RFC3339_RE = re.compile(
|
||||
r"\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})\Z"
|
||||
)
|
||||
_COPY_CHUNK_SIZE = 64 * 1024
|
||||
MAX_PATH_SEGMENT_UTF8_BYTES = 255
|
||||
MAX_SOLUTION_PATH_UTF8_BYTES = 1024
|
||||
|
||||
|
||||
class CLIError(RuntimeError):
|
||||
"""A safe command failure with its stable process exit code."""
|
||||
|
||||
def __init__(self, message: str, exit_code: int) -> None:
|
||||
super().__init__(message)
|
||||
self.exit_code = exit_code
|
||||
|
||||
|
||||
class BackendRejected(CLIError):
|
||||
"""A successfully validated backend error envelope."""
|
||||
|
||||
def __init__(self, message: str, exit_code: int, backend_code: str) -> None:
|
||||
super().__init__(message, exit_code)
|
||||
self.backend_code = backend_code
|
||||
|
||||
|
||||
ClientFactory = Callable[[], httpx.Client]
|
||||
|
||||
|
||||
def normalize_server_url(value: str) -> str:
|
||||
"""Normalize an absolute root HTTP(S) backend URL without exposing user info."""
|
||||
if (
|
||||
not value
|
||||
or any(char.isspace() or ord(char) < 0x20 or ord(char) == 0x7F for char in value)
|
||||
or "?" in value
|
||||
or "#" in value
|
||||
):
|
||||
raise CLIError("server URL is invalid", 2)
|
||||
try:
|
||||
parsed = urlsplit(value)
|
||||
port = parsed.port
|
||||
except ValueError as exc:
|
||||
raise CLIError("server URL is invalid", 2) from exc
|
||||
if (
|
||||
parsed.scheme.lower() not in {"http", "https"}
|
||||
or not parsed.hostname
|
||||
or parsed.username is not None
|
||||
or parsed.password is not None
|
||||
or parsed.path not in {"", "/"}
|
||||
or parsed.query
|
||||
or parsed.fragment
|
||||
):
|
||||
raise CLIError("server URL is invalid", 2)
|
||||
host = parsed.hostname.lower()
|
||||
if ":" in host:
|
||||
host = f"[{host}]"
|
||||
netloc = host if port is None else f"{host}:{port}"
|
||||
normalized = urlunsplit((parsed.scheme.lower(), netloc, "", "", ""))
|
||||
try:
|
||||
httpx.URL(f"{normalized}/register")
|
||||
except httpx.InvalidURL as exc:
|
||||
raise CLIError("server URL is invalid", 2) from exc
|
||||
return normalized
|
||||
|
||||
|
||||
def _default_client() -> httpx.Client:
|
||||
return httpx.Client(
|
||||
timeout=httpx.Timeout(120.0, connect=10.0),
|
||||
follow_redirects=False,
|
||||
verify=True,
|
||||
)
|
||||
|
||||
|
||||
def _post_registration_with_deadline(
|
||||
client_factory: ClientFactory,
|
||||
url: str,
|
||||
payload: dict[str, str],
|
||||
total_timeout_seconds: float,
|
||||
) -> httpx.Response:
|
||||
outcomes: queue.SimpleQueue[httpx.Response | BaseException] = queue.SimpleQueue()
|
||||
|
||||
def send() -> None:
|
||||
try:
|
||||
with client_factory() as client:
|
||||
outcomes.put(client.post(url, json=payload))
|
||||
except BaseException as exc:
|
||||
outcomes.put(exc)
|
||||
|
||||
worker = threading.Thread(target=send, name="distsys-cli-request", daemon=True)
|
||||
worker.start()
|
||||
worker.join(total_timeout_seconds)
|
||||
if worker.is_alive():
|
||||
raise CLIError("network request failed", 4)
|
||||
outcome = outcomes.get()
|
||||
if isinstance(outcome, BaseException):
|
||||
raise outcome
|
||||
return outcome
|
||||
|
||||
|
||||
def _json_object(response: httpx.Response) -> dict[str, Any]:
|
||||
content_type = response.headers.get("content-type", "").split(";", 1)[0].strip().lower()
|
||||
if content_type != "application/json":
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
try:
|
||||
value = response.json()
|
||||
except ValueError as exc:
|
||||
raise CLIError("backend returned an invalid response", 4) from exc
|
||||
if not isinstance(value, dict):
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
return value
|
||||
|
||||
|
||||
def _request_id(response: httpx.Response) -> str:
|
||||
request_id = response.headers.get("X-Request-ID")
|
||||
if not request_id:
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
return str(request_id)
|
||||
|
||||
|
||||
def _validate_success(response: httpx.Response) -> tuple[dict[str, str], str]:
|
||||
request_id = _request_id(response)
|
||||
if response.headers.get("Cache-Control") != "no-store":
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
body = _json_object(response)
|
||||
if set(body) != SUCCESS_FIELDS or any(
|
||||
not isinstance(body.get(field), str) or not body[field] for field in SUCCESS_FIELDS
|
||||
):
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
repository = str(body["repo"])
|
||||
try:
|
||||
parsed_repository = urlsplit(repository)
|
||||
except ValueError as exc:
|
||||
raise CLIError("backend returned an invalid response", 4) from exc
|
||||
if (
|
||||
any(char.isspace() for char in repository)
|
||||
or not parsed_repository.scheme
|
||||
or not parsed_repository.netloc
|
||||
):
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
return {field: str(body[field]) for field in SUCCESS_FIELDS}, request_id
|
||||
|
||||
|
||||
def _raise_backend_error(
|
||||
response: httpx.Response,
|
||||
allowed_errors: dict[int, frozenset[str]] = REGISTER_ERROR_CODES,
|
||||
) -> None:
|
||||
request_id = _request_id(response)
|
||||
body = _json_object(response)
|
||||
if (
|
||||
set(body) != ERROR_FIELDS
|
||||
or not all(isinstance(body.get(field), str) and body[field] for field in ERROR_FIELDS)
|
||||
or body["request_id"] != request_id
|
||||
):
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
allowed_codes = allowed_errors.get(response.status_code)
|
||||
if allowed_codes is None or body["code"] not in allowed_codes:
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
status_class = response.status_code // 100
|
||||
exit_code = 3 if status_class == 4 else 4
|
||||
raise BackendRejected(
|
||||
f"{body['message']} (request ID: {request_id})",
|
||||
exit_code,
|
||||
str(body["code"]),
|
||||
)
|
||||
|
||||
|
||||
class _StrictLoader(yaml.SafeLoader):
|
||||
"""YAML loader that rejects duplicate mapping keys."""
|
||||
|
||||
|
||||
def _strict_mapping(loader: _StrictLoader, node: yaml.MappingNode, deep: bool = False) -> Any:
|
||||
loader.flatten_mapping(node)
|
||||
result: dict[Any, Any] = {}
|
||||
for key_node, value_node in node.value:
|
||||
key = loader.construct_object(key_node, deep=deep)
|
||||
try:
|
||||
duplicate = key in result
|
||||
except TypeError as exc:
|
||||
raise yaml.constructor.ConstructorError(
|
||||
None, None, "mapping key is not hashable", key_node.start_mark
|
||||
) from exc
|
||||
if duplicate:
|
||||
raise yaml.constructor.ConstructorError(
|
||||
None, None, "duplicate mapping key", key_node.start_mark
|
||||
)
|
||||
result[key] = loader.construct_object(value_node, deep=deep)
|
||||
return result
|
||||
|
||||
|
||||
_StrictLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _strict_mapping)
|
||||
|
||||
|
||||
def _load_config(config_path: Path | None) -> tuple[str, str]:
|
||||
path = DEFAULT_CONFIG if config_path is None else config_path
|
||||
if config_path is not None and not path.is_absolute():
|
||||
path = Path.cwd() / path
|
||||
try:
|
||||
documents = list(yaml.load_all(path.read_text(encoding="utf-8"), Loader=_StrictLoader))
|
||||
except (OSError, UnicodeError, yaml.YAMLError) as exc:
|
||||
raise CLIError("configuration is invalid", 2) from exc
|
||||
if len(documents) != 1 or not isinstance(documents[0], dict):
|
||||
raise CLIError("configuration is invalid", 2)
|
||||
value = documents[0]
|
||||
if set(value) - {"server_url", "access_token"} or "server_url" not in value:
|
||||
raise CLIError("configuration is invalid", 2)
|
||||
server_url = value.get("server_url")
|
||||
access_token = value.get("access_token")
|
||||
if not isinstance(server_url, str) or not isinstance(access_token, str) or not access_token:
|
||||
raise CLIError("configuration is invalid or has no access token", 2)
|
||||
return normalize_server_url(server_url), access_token
|
||||
|
||||
|
||||
def _normalized_segment(value: str) -> str:
|
||||
if (
|
||||
not value
|
||||
or value in {".", ".."}
|
||||
or "\\" in value
|
||||
or _DRIVE_PATH_RE.match(value)
|
||||
or any(unicodedata.category(character) == "Cc" for character in value)
|
||||
):
|
||||
raise CLIError("solution contains an unsupported path", 2)
|
||||
normalized = unicodedata.normalize("NFC", value)
|
||||
if (
|
||||
normalized in {"", ".", "..", ".git"}
|
||||
or len(normalized.encode("utf-8")) > MAX_PATH_SEGMENT_UTF8_BYTES
|
||||
):
|
||||
raise CLIError("solution contains an unsupported path", 2)
|
||||
return normalized
|
||||
|
||||
|
||||
def _validate_solution_path(path: tuple[str, ...]) -> None:
|
||||
if len("/".join(path).encode("utf-8")) > MAX_SOLUTION_PATH_UTF8_BYTES:
|
||||
raise CLIError("solution contains an unsupported path", 2)
|
||||
|
||||
|
||||
def _collect_solution_entries(
|
||||
source: Path,
|
||||
) -> list[tuple[Path, tuple[str, ...], bool, os.stat_result]]:
|
||||
entries: list[tuple[Path, tuple[str, ...], bool, os.stat_result]] = []
|
||||
normalized_paths: dict[tuple[str, ...], bool] = {}
|
||||
|
||||
def visit(directory: Path, relative: tuple[str, ...]) -> None:
|
||||
try:
|
||||
children = sorted(os.scandir(directory), key=lambda entry: entry.name)
|
||||
except OSError as exc:
|
||||
raise CLIError("solution directory cannot be read", 2) from exc
|
||||
for child in children:
|
||||
normalized = (*relative, _normalized_segment(child.name))
|
||||
_validate_solution_path(normalized)
|
||||
try:
|
||||
metadata = child.stat(follow_symlinks=False)
|
||||
except OSError as exc:
|
||||
raise CLIError("solution entry cannot be read", 2) from exc
|
||||
mode = metadata.st_mode
|
||||
if stat.S_ISLNK(mode):
|
||||
raise CLIError("solution contains a symbolic link", 2)
|
||||
directory_entry = stat.S_ISDIR(mode)
|
||||
regular = stat.S_ISREG(mode)
|
||||
if not directory_entry and not regular:
|
||||
raise CLIError("solution contains a non-regular entry", 2)
|
||||
if regular and getattr(metadata, "st_nlink", 1) > 1:
|
||||
raise CLIError("solution contains a hard-linked file", 2)
|
||||
if normalized in normalized_paths:
|
||||
raise CLIError("solution contains duplicate normalized paths", 2)
|
||||
for length in range(1, len(normalized)):
|
||||
ancestor = normalized[:length]
|
||||
if ancestor in normalized_paths and not normalized_paths[ancestor]:
|
||||
raise CLIError("solution contains a file/directory conflict", 2)
|
||||
normalized_paths[normalized] = directory_entry
|
||||
entries.append((Path(child.path), normalized, directory_entry, metadata))
|
||||
if directory_entry:
|
||||
visit(Path(child.path), normalized)
|
||||
|
||||
visit(source, ())
|
||||
if not any(not directory for _, _, directory, _ in entries):
|
||||
raise CLIError("solution directory contains no regular files", 2)
|
||||
return entries
|
||||
|
||||
|
||||
def _zip_info(name: str, mode: int, *, directory: bool) -> zipfile.ZipInfo:
|
||||
info = zipfile.ZipInfo(name + ("/" if directory else ""))
|
||||
info.create_system = 3
|
||||
file_type = stat.S_IFDIR if directory else stat.S_IFREG
|
||||
info.external_attr = (file_type | mode) << 16
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
return info
|
||||
|
||||
|
||||
def _source_is_executable(mode: int, platform_name: str = os.name) -> bool:
|
||||
"""Return whether this platform exposes any executable bit for a source file."""
|
||||
return platform_name == "posix" and bool(mode & 0o111)
|
||||
|
||||
|
||||
def _resolve_solution_directory(solution_dir: Path) -> Path:
|
||||
try:
|
||||
source = solution_dir.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise CLIError("solution directory does not exist", 2) from exc
|
||||
if not source.is_dir():
|
||||
raise CLIError("solution path is not a directory", 2)
|
||||
return source
|
||||
|
||||
|
||||
def build_solution_archive(solution_dir: Path, destination: Path) -> None:
|
||||
"""Build one normalized, link-free archive below a synthetic solution root."""
|
||||
source = _resolve_solution_directory(solution_dir)
|
||||
entries = _collect_solution_entries(source)
|
||||
try:
|
||||
with zipfile.ZipFile(destination, "w", allowZip64=True) as archive:
|
||||
archive.writestr(_zip_info("solution", 0o755, directory=True), b"")
|
||||
for path, relative, directory, selected_metadata in entries:
|
||||
member_name = "solution/" + "/".join(relative)
|
||||
if directory:
|
||||
archive.writestr(_zip_info(member_name, 0o755, directory=True), b"")
|
||||
continue
|
||||
executable = _source_is_executable(selected_metadata.st_mode)
|
||||
permissions = 0o755 if executable else 0o644
|
||||
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
||||
descriptor = os.open(path, flags)
|
||||
try:
|
||||
current = os.fstat(descriptor)
|
||||
if getattr(current, "st_nlink", 1) > 1:
|
||||
raise CLIError("solution contains a hard-linked file", 2)
|
||||
identity_changed = os.name == "posix" and (
|
||||
current.st_dev != selected_metadata.st_dev
|
||||
or current.st_ino != selected_metadata.st_ino
|
||||
)
|
||||
metadata_changed = (
|
||||
current.st_size != selected_metadata.st_size
|
||||
or current.st_mtime_ns != selected_metadata.st_mtime_ns
|
||||
)
|
||||
if not stat.S_ISREG(current.st_mode) or identity_changed or metadata_changed:
|
||||
raise CLIError("solution changed while it was archived", 2)
|
||||
with os.fdopen(descriptor, "rb", closefd=True) as source_file:
|
||||
descriptor = -1
|
||||
info = _zip_info(member_name, permissions, directory=False)
|
||||
with archive.open(info, "w") as output:
|
||||
while chunk := source_file.read(_COPY_CHUNK_SIZE):
|
||||
output.write(chunk)
|
||||
final = os.fstat(source_file.fileno())
|
||||
if (
|
||||
final.st_size != current.st_size
|
||||
or final.st_mtime_ns != current.st_mtime_ns
|
||||
):
|
||||
raise CLIError("solution changed while it was archived", 2)
|
||||
finally:
|
||||
if descriptor >= 0:
|
||||
os.close(descriptor)
|
||||
except CLIError:
|
||||
raise
|
||||
except (OSError, ValueError, zipfile.BadZipFile) as exc:
|
||||
raise CLIError("solution archive could not be created", 1) from exc
|
||||
|
||||
|
||||
def _post_archive_with_deadline(
|
||||
client_factory: ClientFactory,
|
||||
url: str,
|
||||
archive_path: Path,
|
||||
access_token: str,
|
||||
total_timeout_seconds: float,
|
||||
) -> httpx.Response:
|
||||
outcomes: queue.SimpleQueue[httpx.Response | BaseException] = queue.SimpleQueue()
|
||||
# Windows does not permit unlinking a file held open by the request worker.
|
||||
# Detach the request body from the temporary path before enforcing the outer
|
||||
# wall-clock deadline so timeout cleanup remains immediate.
|
||||
windows_content = archive_path.read_bytes() if os.name == "nt" else None
|
||||
headers = {
|
||||
"Authorization": f"token {access_token}",
|
||||
"Content-Type": "application/zip",
|
||||
}
|
||||
|
||||
def send() -> None:
|
||||
try:
|
||||
with client_factory() as client:
|
||||
if windows_content is not None:
|
||||
outcomes.put(client.post(url, content=windows_content, headers=headers))
|
||||
else:
|
||||
with archive_path.open("rb") as content:
|
||||
outcomes.put(client.post(url, content=content, headers=headers))
|
||||
except BaseException as exc:
|
||||
outcomes.put(exc)
|
||||
finally:
|
||||
_remove_temporary_archive(archive_path)
|
||||
|
||||
worker = threading.Thread(target=send, name="distsys-cli-request", daemon=True)
|
||||
worker.start()
|
||||
worker.join(total_timeout_seconds)
|
||||
if worker.is_alive():
|
||||
raise CLIError("network request failed", 4)
|
||||
outcome = outcomes.get()
|
||||
if isinstance(outcome, BaseException):
|
||||
raise outcome
|
||||
return outcome
|
||||
|
||||
|
||||
def _remove_temporary_archive(path: Path) -> None:
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _validate_submit_success(response: httpx.Response) -> dict[str, str | bool]:
|
||||
_request_id(response)
|
||||
body = _json_object(response)
|
||||
if set(body) != SUBMIT_SUCCESS_FIELDS:
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
submission_id = body.get("submission_id")
|
||||
accepted_at = body.get("accepted_at")
|
||||
late = body.get("late_submission")
|
||||
workflow_run = body.get("workflow_run")
|
||||
if not isinstance(submission_id, str) or not submission_id:
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
if not isinstance(accepted_at, str) or not accepted_at:
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
if not isinstance(workflow_run, str) or not workflow_run:
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
if not isinstance(late, bool):
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
if _UUID_RE.fullmatch(submission_id) is None or _RFC3339_RE.fullmatch(accepted_at) is None:
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
try:
|
||||
uuid.UUID(submission_id)
|
||||
normalized_time = accepted_at[:-1] + "+00:00" if accepted_at[-1] in "Zz" else accepted_at
|
||||
parsed_time = datetime.fromisoformat(normalized_time)
|
||||
parsed_url = urlsplit(workflow_run)
|
||||
workflow_port = parsed_url.port
|
||||
parsed_http_url = httpx.URL(workflow_run)
|
||||
except (UnicodeError, ValueError, TypeError, httpx.InvalidURL) as exc:
|
||||
raise CLIError("backend returned an invalid response", 4) from exc
|
||||
if (
|
||||
parsed_time.tzinfo is None
|
||||
or accepted_at.endswith("-00:00")
|
||||
or parsed_url.scheme not in {"http", "https"}
|
||||
or not parsed_url.netloc
|
||||
or parsed_url.hostname is None
|
||||
or parsed_url.username is not None
|
||||
or parsed_url.password is not None
|
||||
or workflow_port == 0
|
||||
or not parsed_http_url.host
|
||||
or any(
|
||||
character.isspace() or ord(character) < 0x20 or ord(character) == 0x7F
|
||||
for character in workflow_run
|
||||
)
|
||||
):
|
||||
raise CLIError("backend returned an invalid response", 4)
|
||||
return {
|
||||
"submission_id": submission_id,
|
||||
"accepted_at": accepted_at,
|
||||
"late_submission": late,
|
||||
"workflow_run": workflow_run,
|
||||
}
|
||||
|
||||
|
||||
def _format_accepted_at(accepted_at: str) -> str:
|
||||
normalized_time = accepted_at[:-1] + "+00:00" if accepted_at[-1] in "Zz" else accepted_at
|
||||
return datetime.fromisoformat(normalized_time).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def submit(
|
||||
assignment_id: str,
|
||||
solution_dir: Path,
|
||||
config_path: Path | None,
|
||||
*,
|
||||
client_factory: ClientFactory = _default_client,
|
||||
total_timeout_seconds: float = TOTAL_REQUEST_TIMEOUT_SECONDS,
|
||||
) -> None:
|
||||
if ASSIGNMENT_ID_RE.fullmatch(assignment_id) is None:
|
||||
raise CLIError("assignment ID is invalid", 2)
|
||||
server_url, access_token = _load_config(config_path)
|
||||
resolved_solution = _resolve_solution_directory(solution_dir)
|
||||
descriptor, archive_name = tempfile.mkstemp(prefix="distsys-solution-", suffix=".zip")
|
||||
os.close(descriptor)
|
||||
archive_path = Path(archive_name)
|
||||
try:
|
||||
try:
|
||||
archive_path.resolve().relative_to(resolved_solution)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise CLIError("temporary archive location overlaps the solution directory", 2)
|
||||
build_solution_archive(resolved_solution, archive_path)
|
||||
try:
|
||||
response = _post_archive_with_deadline(
|
||||
client_factory,
|
||||
f"{server_url}/submit/{assignment_id}",
|
||||
archive_path,
|
||||
access_token,
|
||||
total_timeout_seconds,
|
||||
)
|
||||
except (httpx.HTTPError, httpx.InvalidURL, OSError) as exc:
|
||||
raise CLIError("network request failed", 4) from exc
|
||||
if response.status_code != 201:
|
||||
try:
|
||||
_raise_backend_error(response, SUBMIT_ERROR_CODES)
|
||||
except BackendRejected as exc:
|
||||
if exc.backend_code == "SOLUTION_UNCHANGED":
|
||||
raise CLIError(f"{exc}; no grading run was created", 3) from None
|
||||
raise
|
||||
result = _validate_submit_success(response)
|
||||
finally:
|
||||
_remove_temporary_archive(archive_path)
|
||||
print(f"Submission ID: {result['submission_id']}")
|
||||
print(f"Accepted at: {_format_accepted_at(cast(str, result['accepted_at']))}")
|
||||
print(f"Late submission: {str(result['late_submission']).lower()}")
|
||||
print(f"Workflow run: {result['workflow_run']}")
|
||||
|
||||
|
||||
def _restrict_windows(path: Path) -> bool:
|
||||
creation_flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
try:
|
||||
identity = subprocess.run(
|
||||
["whoami"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
creationflags=creation_flags,
|
||||
)
|
||||
principal = identity.stdout.strip()
|
||||
if identity.returncode != 0 or not principal:
|
||||
return False
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"icacls",
|
||||
str(path),
|
||||
"/inheritance:r",
|
||||
"/grant:r",
|
||||
f"{principal}:(F)",
|
||||
],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=creation_flags,
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
return completed.returncode == 0
|
||||
|
||||
|
||||
def _write_config(destination: Path, server_url: str, access_token: str) -> bool:
|
||||
"""Synchronize a private sibling temporary file, then atomically replace."""
|
||||
payload = yaml.safe_dump(
|
||||
{"server_url": server_url, "access_token": access_token},
|
||||
allow_unicode=True,
|
||||
sort_keys=False,
|
||||
).encode("utf-8")
|
||||
descriptor = -1
|
||||
temporary: Path | None = None
|
||||
permission_warning = False
|
||||
try:
|
||||
descriptor, temporary_name = tempfile.mkstemp(
|
||||
prefix=f".{destination.name}.", dir=destination.parent
|
||||
)
|
||||
temporary = Path(temporary_name)
|
||||
with os.fdopen(descriptor, "wb", closefd=True) as stream:
|
||||
descriptor = -1
|
||||
stream.write(payload)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
if os.name == "posix":
|
||||
temporary.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
||||
if os.name == "nt" and not _restrict_windows(temporary):
|
||||
permission_warning = True
|
||||
os.replace(temporary, destination)
|
||||
temporary = None
|
||||
finally:
|
||||
if descriptor >= 0:
|
||||
os.close(descriptor)
|
||||
if temporary is not None:
|
||||
try:
|
||||
temporary.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
return permission_warning
|
||||
|
||||
|
||||
def _resolve_destination(value: Path | None) -> Path:
|
||||
destination = DEFAULT_CONFIG if value is None else value
|
||||
if value is not None and not destination.is_absolute():
|
||||
destination = Path.cwd() / destination
|
||||
try:
|
||||
destination = destination.resolve(strict=False)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
raise CLIError("configuration destination is not usable", 2) from exc
|
||||
if destination.exists() and destination.is_dir():
|
||||
raise CLIError("configuration destination is not a file", 2)
|
||||
return destination
|
||||
|
||||
|
||||
def _masked_email(email: str) -> str:
|
||||
local, separator, domain = email.rpartition("@")
|
||||
if not separator or not local or not domain:
|
||||
return "***"
|
||||
return f"{local[0]}***@{domain}"
|
||||
|
||||
|
||||
def register(
|
||||
server_url: str,
|
||||
config_path: Path | None,
|
||||
*,
|
||||
input_fn: Callable[[str], str] = input,
|
||||
password_fn: Callable[[str], str] = getpass.getpass,
|
||||
client_factory: ClientFactory = _default_client,
|
||||
total_timeout_seconds: float = TOTAL_REQUEST_TIMEOUT_SECONDS,
|
||||
) -> None:
|
||||
normalized_url = normalize_server_url(server_url)
|
||||
destination = _resolve_destination(config_path)
|
||||
registration_token = password_fn("Registration token: ")
|
||||
name = input_fn("Full name: ")
|
||||
email = input_fn("Email: ")
|
||||
try:
|
||||
response = _post_registration_with_deadline(
|
||||
client_factory,
|
||||
f"{normalized_url}/register",
|
||||
{"name": name, "email": email, "reg_token": registration_token},
|
||||
total_timeout_seconds,
|
||||
)
|
||||
except (httpx.HTTPError, httpx.InvalidURL) as exc:
|
||||
raise CLIError("network request failed", 4) from exc
|
||||
if response.status_code != 201:
|
||||
_raise_backend_error(response)
|
||||
result, request_id = _validate_success(response)
|
||||
try:
|
||||
permission_warning = _write_config(destination, normalized_url, result["token"])
|
||||
except OSError as exc:
|
||||
raise CLIError(
|
||||
"Registration succeeded, but credentials could not be stored. "
|
||||
f"Contact course staff with request ID: {request_id}",
|
||||
1,
|
||||
) from exc
|
||||
if permission_warning:
|
||||
print(
|
||||
"warning: could not restrict configuration permissions to the current user",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(f"Username: {result['username']}")
|
||||
print(f"Temporary password: {result['password']}")
|
||||
print(f"Repository: {result['repo']}")
|
||||
print(f"Registration email: Check {_masked_email(email)} for your Gitea account details.")
|
||||
print("If it does not arrive within 5 minutes, check spam and contact course staff.")
|
||||
print("Action required: Sign in to Gitea and change the temporary password before submitting.")
|
||||
|
||||
|
||||
def parse_args(arguments: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--config", type=Path, default=None)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
register_parser = subparsers.add_parser("register", help="create a student account")
|
||||
register_parser.add_argument("--config", type=Path, default=argparse.SUPPRESS)
|
||||
register_parser.add_argument("server_url")
|
||||
submit_parser = subparsers.add_parser("submit", help="submit a solution for grading")
|
||||
submit_parser.add_argument("--config", type=Path, default=argparse.SUPPRESS)
|
||||
submit_parser.add_argument("--assignment-id")
|
||||
submit_parser.add_argument("--solution-dir", type=Path)
|
||||
return parser.parse_args(arguments)
|
||||
|
||||
|
||||
def _resolve_submit_options(
|
||||
assignment_id: str | None, solution_dir: Path | None
|
||||
) -> tuple[str, Path]:
|
||||
if assignment_id is None and solution_dir is None:
|
||||
working_directory = Path.cwd()
|
||||
default_solution = working_directory / "solution"
|
||||
if not default_solution.is_dir():
|
||||
raise CLIError("submit without options requires a solution subdirectory", 2)
|
||||
return working_directory.name, default_solution
|
||||
if assignment_id is None or solution_dir is None:
|
||||
raise CLIError("--assignment-id and --solution-dir must be provided together", 2)
|
||||
return assignment_id, solution_dir
|
||||
|
||||
|
||||
def main(arguments: Sequence[str] | None = None) -> int:
|
||||
args = parse_args(arguments)
|
||||
try:
|
||||
if args.command == "register":
|
||||
register(args.server_url, args.config)
|
||||
elif args.command == "submit":
|
||||
assignment_id, solution_dir = _resolve_submit_options(
|
||||
args.assignment_id, args.solution_dir
|
||||
)
|
||||
submit(assignment_id, solution_dir, args.config)
|
||||
except CLIError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return exc.exit_code
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,92 @@
|
||||
# Домашние задания
|
||||
|
||||
Здесь находятся домашние задания по курсу. Если вы зачислены на курс и будете сдавать задания, прочитайте внимательно текст ниже. **Оценка за решение выставляется только в том случае, если оно сдано в тестирующую систему.** Если вы вольнослушатель, то сдача заданий вам недоступна, но вы можете тестировать решения локально (см. ниже).
|
||||
|
||||
## Самостоятельность выполнения
|
||||
|
||||
Домашние задания выполняются индивидуально. Можно обсуждать условие, общие идеи и подходы, но нельзя обмениваться кодом, отчётом или другими частями решений.
|
||||
|
||||
Нельзя передавать другим студентам своё решение или его части, просматривать решения других студентов, совместно разрабатывать, копировать или адаптировать их, в том числе если эти решения были сданы в прошлые годы.
|
||||
|
||||
При установленном нарушении академических норм за соответствующее домашнее задание выставляется оценка 0, а преподаватель направляет служебную записку декану факультета. Студенту, намеренно предоставившему своё решение для списывания, также может быть выставлена оценка 0. Подробнее см. [правила НИУ ВШЭ о нарушении академических норм](https://www.hse.ru/studyspravka/academnormy).
|
||||
|
||||
## Использование LLM
|
||||
|
||||
При выполнении домашних заданий можно использовать LLM. Если их применение повлияло на сданное решение, его необходимо описать в разделе `## Описание применения генеративных моделей` в файле `solution/readme.md`. Если раздел отсутствует, считается, что генеративные модели при подготовке решения не использовались. Во время устной защиты использовать LLM нельзя, если преподаватель явно не разрешил обратное. Подробные правила приведены в [политике использования LLM](../llm-policy.md).
|
||||
|
||||
## Настройка окружения
|
||||
|
||||
Убедитесь, что у вас на компьютере установлен Python. В случае работы на Windows не рекомендуется устанавливать Python из Microsoft Store – это может привести к ошибкам при запуске тестов, поэтому лучше использовать [официальный установщик](https://www.python.org/downloads).
|
||||
|
||||
Установите Python 3.12 или новее и необходимые зависимости: `pip3 install -r requirements.txt`. При работе на Windows замените здесь и далее `pip3` и `python3` на `pip` и `python` соответственно.
|
||||
|
||||
Также вам понадобится запускать Docker-контейнеры. Для этого надо установить [Docker Engine](https://docs.docker.com/engine/install/) или [Docker Desktop](https://docs.docker.com/desktop/) под вашу ОС.
|
||||
|
||||
Склонируйте репозиторий курса в папку на вашем компьютере. Далее обновляйте вашу копию репозитория с помощью `git pull` по мере размещения новых заданий и материалов. Во избежание проблем не рекомендуется менять структуру папок внутри репозитория и переносить их в другое место.
|
||||
|
||||
Откройте корень репозитория в вашей IDE и откройте файл `homework/01-guarantees/solution/guarantees.py`. Для автодополнения и проверки типов API AnySystem описан в [стабе](../typings/anysystem/__init__.pyi) — файле с объявлениями классов и методов без их реализации. В VS Code с Pylance он подключается через [настройки репозитория](../pyrightconfig.json), дополнительная установка модуля не нужна. Для другой IDE укажите папку `typings` как каталог стабов согласно её документации. Исходная реализация API доступна в [AnySystem 0.3.0](https://github.com/osukhoroslov/anysystem/blob/v0.3.0/python/anysystem.py).
|
||||
|
||||
В AnySystem 0.3.0 Python-модуль встроен в Rust-тестер: при запуске тестов локально или в Docker отдельный `anysystem.py` и настройка `PYTHONPATH` не требуются. Запускайте тесты из папки задания, чтобы относительные пути к решениям были корректны. Стаб служит только для IDE.
|
||||
|
||||
### Регистрация
|
||||
|
||||
(Пропустите эту часть, если вы вольнослушатель.)
|
||||
|
||||
Перед тем, как сдавать задания, надо пройти регистрацию. Откройте папку `homework` и выполните команду:
|
||||
|
||||
```
|
||||
python3 cli.py register https://distsys.ru
|
||||
```
|
||||
|
||||
Введите registration token, разосланный в канале курса, полное имя (Фамилия Имя Отчество) в точности как в списке студентов и личный адрес электронной почты. Если вас нет в списке студентов, зачисленных на курс, то вы не можете сдавать задания.
|
||||
|
||||
После успешной регистрации настройки и токен доступа будут сохранены в файле `cli.yaml`. Не публикуйте этот файл и не добавляйте его в Git. На указанный адрес придёт письмо с данными учётной записи Gitea.
|
||||
|
||||
Войдите в Gitea с временным паролем из вывода команды и сразу смените его. До смены пароля токен не работает. Если токен утрачен, создайте новый в Gitea: **Settings -> Applications**, имя `distsys-cli`, разрешение **User: Read**; затем замените только `access_token` в `cli.yaml`. Если недоступен адрес электронной почты или письмо не приходит, обратитесь к преподавателям.
|
||||
|
||||
## Локальное тестирование решения
|
||||
|
||||
Перед сдачей решения в тестирующую систему обязательно проведите локальное тестирование. В каждом задании будет инструкция как это сделать.
|
||||
|
||||
**Пожалуйста, не пренебрегайте локальным тестированием.** Все тесты для заданий открыты, в том числе для того, чтобы вы могли быстро протестировать решение локально после каждой правки, а не отправляли его каждый раз в тестирующую систему и ждали в очереди. Тем самым вы также снизите время ожидания для товарищей, которым действительно надо зафиксировать улучшение своего решения.
|
||||
|
||||
В каждом условии приведена Docker-команда для запуска тестов в окружении, аналогичном тестирующей системе. Выполняйте её из папки соответствующего задания.
|
||||
|
||||
В некоторых случаях время выполнения тестов, особенно в Docker, может зависеть от вашей системы, архитектуры процессора и т.п. Если в тестах к заданию есть лимит по времени, и при локальном тестировании ваше решение не укладывается в него, попробуйте сдать решение в систему. Возможно тесты там пройдут из-за отличий в скорости выполнения. Референсом в таких тестах служит сервер тестирующей системы. Также можно попробовать запустить тесты локально без Docker по инструкции из условия задания.
|
||||
|
||||
### ARM64 / Apple Silicon
|
||||
|
||||
Опубликованные образы тестов сейчас предназначены для `linux/amd64`. На компьютерах с ARM64, включая Apple Silicon, Docker запускает их через эмуляцию, поэтому тесты могут выполняться заметно медленнее. Особенно это важно для тестов с лимитом времени: их локальный результат может отличаться от результата в тестирующей системе.
|
||||
|
||||
Для заданий 1, 4, 6–9 можно собрать тестовый образ для архитектуры своей машины из открытого `tests/Dockerfile`. Выполните из папки задания:
|
||||
|
||||
```bash
|
||||
docker build --pull -t distsys-tests-local ./tests
|
||||
docker run --rm -t -v ./solution:/solution distsys-tests-local [ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]
|
||||
```
|
||||
|
||||
Для полного прогона замените `[ЗДЕСЬ МОЖНО УКАЗАТЬ ОПЦИИ]` параметрами из команды полного тестирования в условии соответствующего задания. Для быстрых итераций можно запускать тесты без Docker по инструкции из условия.
|
||||
|
||||
В заданиях 2, 3, 5 и 10 используйте приведённые в условиях инструкции локального запуска сервисов и тестов. Собирать внутренний Docker-in-Docker образ для этого не требуется.
|
||||
|
||||
Тестирующая система используется для итоговой проверки и служит эталоном для временных лимитов, но не должна заменять локальное тестирование после каждой правки.
|
||||
|
||||
## Сдача решения
|
||||
|
||||
После локального тестирования откройте папку задания, например `homework/01-guarantees`, и выполните команду:
|
||||
|
||||
```
|
||||
python3 ../cli.py submit
|
||||
```
|
||||
|
||||
Утилита определит идентификатор задания по имени текущей папки и отправит содержимое её поддиректории `solution`. Она выведет идентификатор и официальное время приёма решения, а также ссылку на запуск workflow. Откройте эту ссылку в Gitea, чтобы следить за проверкой и прочитать её журнал.
|
||||
|
||||
В состав решения для каждого задания должен входить краткий отчёт в файле `solution/readme.md`. Отчёт необходимо сдать вместе с остальными файлами решения до дедлайна задания. При его отсутствии автоматические тесты всё равно запускаются, но защита не проводится и **решение не засчитывается**. Требования к содержанию отчёта приведены в условии соответствующего задания.
|
||||
|
||||
## Изоляция процессов AnySystem
|
||||
|
||||
В заданиях на AnySystem каждый экземпляр `Process` моделирует отдельный процесс распределённой системы. Его изменяемое состояние должно храниться только в атрибутах экземпляра (`self`). Все взаимодействия между процессами должны происходить через сообщения, как и в реальной распределённой системе.
|
||||
|
||||
Запрещено использовать общую изменяемую память: изменяемые объекты на уровне модуля или любого класса, включая вспомогательные классы, изменяемые значения аргументов по умолчанию, `global`, разделяемые замыкания, атрибуты классов и рефлексию для изменения глобального состояния. Перед запуском тестов решение статически проверяется; при нарушении тестер выводит найденные места и выставляет `SCORE: 0`.
|
||||
|
||||
Импорты, вспомогательные функции и классы, а также неизменяемые константы разрешены. Изменяемое состояние вспомогательного объекта допустимо, если каждый процесс создаёт собственный экземпляр и хранит его в `self`; для полей `dataclass` используйте `field(default_factory=...)`. Неизменяемые последовательности и множества можно задавать через `tuple` и `frozenset`, а таблицы — через `types.MappingProxyType` с литералом словаря.
|
||||
@@ -0,0 +1,2 @@
|
||||
httpx>=0.28,<1
|
||||
PyYAML>=6.0.2,<7
|
||||
Reference in New Issue
Block a user