Add homework 01 guarantees assignment
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user