Compare commits

...

8 Commits

Author SHA1 Message Date
Daira Emma Hopwood e22101d24b Add dependencies and script for generating API documentation.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 22:51:09 +01:00
Daira Emma Hopwood 8918b71bdf More documentation.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 22:49:46 +01:00
Daira Emma Hopwood ffbc4792cf Add references for error and warning codes in .flake8
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 21:23:21 +01:00
Daira Emma Hopwood 97c49bd8b0 Run tests in CI.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 21:23:21 +01:00
Daira Emma Hopwood a4d1f24b43 Add tests for message-passing framework.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 21:23:21 +01:00
Daira Emma Hopwood e28b33c53e Minor refactoring to make the use of `Network` cleaner.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 20:32:29 +01:00
Daira Emma Hopwood f4f90f25f4 Add `check.sh` for convenience of running flake8 and tests before
submitting a PR.

Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 19:02:51 +01:00
Daira Emma Hopwood 70094bd0d2 Move documentation into a `doc/` directory.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-19 19:01:03 +01:00
14 changed files with 359 additions and 55 deletions

12
.flake8
View File

@ -1,4 +1,14 @@
[flake8]
exclude = .git, __pycache__
ignore = E302
# Justifications for each ignored error or warning:
# * E302: always requiring two blank lines rather than one between top-level items is too annoying and nitpicky.
# * E402: we want imports for tests to go between the main code and the tests.
# * W503, W504: these give false positives for the style of avoiding \ at the end of each line by using parens.
#
# References:
# - https://flake8.pycqa.org/en/latest/user/error-codes.html
# - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
ignore = E302, E402, W503, W504
max-line-length = 120

23
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: tests
on: pull_request
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install gnome-keyring
run: sudo apt-get install gnome-keyring
- name: Install poetry
run: pip install --user poetry
- name: Install dependencies
run: poetry install --no-root
- name: Run tests
# -p '[a-z]*.py' avoids running tests from __init__.py files twice.
run: poetry run python -m unittest discover -s simtfl -t . -p '[a-z]*.py' --verbose --buffer

View File

@ -23,30 +23,25 @@ Note the caveats: *experimental*, *simulator*, *research*, *potential*.
poetry run demo
## Programming patterns
## Documentation
The code makes use of the [simpy](https://simpy.readthedocs.io/en/latest/)
discrete event simulation library. This means that functions representing
processes are implemented as generators, so that the library can simulate
timeouts and asynchronous communication (typically faster than real time).
Design documentation is under the `doc/` directory:
We use the convention of putting "(process)" in the doc comment of these
functions. They either must use the `yield` construct, *or* return the
result of calling another "(process)" function (not both).
* [Programming patterns for use of simpy](doc/patterns.md).
Objects that implement processes typically hold the `simpy.Environment` in
an instance variable `self.env`.
To wait for another process `f()` before continuing, use `yield from f()`.
(If it is the last thing to do in a function with no other `yield`
statements, `return f()` can be used as an optimization.)
A "(process)" function that does nothing should `return skip()`, using
`simtfl.util.skip`.
You can also generate API documentation by running `./gendoc.sh`.
The starting point for the generated documentation is <apidoc/simtfl.html>.
## Contributing
Please check `poetry run flake8` before submitting a PR.
Please use `./check.sh` before submitting a PR. This currently runs `flake8`
and the unit tests locally.
You can use `./check.sh -k <substring>` to run `flake8` and then only tests
with names matching the given substring. This will not suppress output to
stdout or stderr (but `./check.sh -bk <substring>` will).
To see other options for running unit tests, use `poetry run python -m unittest -h`.
## License

12
check.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$(command -v -- "$0")")"
echo Running flake8...
poetry run flake8
echo
echo Running unit tests...
args="${*:---buffer}"
poetry run python -m unittest discover -s simtfl -t . -p '[a-z]*.py' --verbose ${args}

20
doc/patterns.md Normal file
View File

@ -0,0 +1,20 @@
# Programming patterns
The code makes use of the [simpy](https://simpy.readthedocs.io/en/latest/)
discrete event simulation library. This means that functions representing
processes are implemented as generators, so that the library can simulate
timeouts and asynchronous communication (typically faster than real time).
We use the convention of putting "(process)" in the doc comment of these
functions. They either must use the `yield` construct, *or* return the
result of calling another "(process)" function (not both).
Objects that implement processes typically hold the `simpy.Environment` in
an instance variable `self.env`.
To wait for another process `f()` before continuing, use `yield from f()`.
(If it is the last thing to do in a function with no other `yield`
statements, `return f()` can be used as an optimization.)
A "(process)" function that does nothing should `return skip()`, using
`simtfl.util.skip`.

5
gendoc.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
set -eu
poetry run pdoc simtfl -o apidoc --no-include-undocumented -d markdown

131
poetry.lock generated
View File

@ -17,6 +17,94 @@ mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.11.0,<2.12.0"
pyflakes = ">=3.1.0,<3.2.0"
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]]
name = "mccabe"
version = "0.7.0"
@ -29,16 +117,36 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "pdoc"
version = "14.1.0"
description = "API Documentation for Python Projects"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "pdoc-14.1.0-py3-none-any.whl", hash = "sha256:e8869dffe21296b3bd5545b28e7f07cae0656082aca43f8915323187e541b126"},
{file = "pdoc-14.1.0.tar.gz", hash = "sha256:3a0bd921a05c39a82b1505089eb6dc99d857b71b856aa60d1aca4d9086d0e18c"},
]
[package.dependencies]
Jinja2 = ">=2.11.0"
MarkupSafe = "*"
pygments = ">=2.12.0"
[package.extras]
dev = ["black", "hypothesis", "mypy", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"]
[[package]]
name = "pycodestyle"
version = "2.11.0"
version = "2.11.1"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"},
{file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"},
{file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
{file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
]
[[package]]
@ -53,6 +161,21 @@ files = [
{file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"},
]
[[package]]
name = "pygments"
version = "2.16.1"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
]
[package.extras]
plugins = ["importlib-metadata"]
[[package]]
name = "simpy"
version = "4.0.2"
@ -68,4 +191,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "c164f34aba3ecbda9fc9942d6e2bfad3eb11491c433ee46bdf0262fd8147209f"
content-hash = "d9f1f22d9f90f2ad4d00ec2785eee468e847dc1c8c4015615febe6094edf3e62"

View File

@ -12,6 +12,7 @@ simpy = "^4"
[tool.poetry.dev-dependencies]
flake8 = "^6"
pdoc = "^14"
[tool.poetry.scripts]
demo = "simtfl.demo:run"

View File

@ -0,0 +1,7 @@
"""
This is an experimental simulator for research into a potential [Trailing Finality
Layer](https://electriccoin.co/blog/the-trailing-finality-layer-a-stepping-stone-to-proof-of-stake-in-zcash/)
for Zcash.
See the [README](../README.md) for more information.
"""

View File

@ -58,14 +58,9 @@ def run():
"""
Runs the demo.
"""
env = Environment()
network = Network(env, delay=4)
network = Network(Environment(), delay=4)
for i in range(10):
network.add_node(PongNode(i, env, network))
network.add_node(PongNode())
network.add_node(PingNode(10, env, network))
for i in range(network.num_nodes()):
env.process(network.start_node(i))
env.run()
network.add_node(PingNode())
network.run_all()

View File

@ -1,12 +1,15 @@
"""
Base classes for messages.
"""
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class PayloadMessage:
"""
A message with an arbitrary payload.
"""
def __init__(self, payload):
"""
Constructs a `PayloadMessage` with the given payload.
"""
self.payload = payload
def __str__(self):
return f"{self.__class__.__name__}({self.payload})"
payload: Any
"""The payload."""

View File

@ -1,3 +1,7 @@
"""
Framework for message passing in a network of nodes.
"""
from .util import skip
@ -30,16 +34,34 @@ class Network:
"""
Adds a node with the next available ident.
"""
ident = self.num_nodes()
self.nodes.append(node)
node.initialize(ident, self.env, self)
def start_node(self, ident):
"""
(process) Start the node with the given ident.
"""
node = self.node(ident)
print(f"T{self.env.now:5d}: starting {node}")
print(f"T{self.env.now:5d}: starting {ident:2d}: {node}")
return node.run()
def start_processes(self):
"""
Start a process for each node.
"""
print()
for i in range(self.num_nodes()):
self.env.process(self.start_node(i))
def run_all(self, *args, **kwargs):
"""
Convenience method to start a process for each node, then start
the simulation. Takes the same arguments as `simpy.Environment.run`.
"""
self.start_processes()
self.env.run(*args, **kwargs)
def send(self, sender, target, message, delay=None):
"""
(process) Sends a message to the node with ident `target`, from the node

View File

@ -1,42 +1,57 @@
from .util import skip
"""
Base classes for node implementations.
"""
from collections import deque
from .util import skip
class PassiveNode:
"""
A node that sends no messages and does nothing with received messages.
This class is intended to be subclassed.
A node that processes messages concurrently. By default it sends no
messages and does nothing with received messages. This class is
intended to be subclassed.
Inherit from this class directly if all messages are to be processed
concurrently without blocking. If messages are to be processed
sequentially, it may be easier to inherit from `SequentialNode`.
Note that the simulation is deterministic regardless of which option
is selected.
"""
def __init__(self, ident, env, network):
def initialize(self, ident, env, network):
"""
Constructs a PassiveNode with the given simpy Environment and network.
Initializes a PassiveNode with the given ident, simpy Environment,
and network. Nodes are initialized when they are added to a network.
"""
self.ident = ident
self.env = env
self.network = network
def __str__(self):
return f"{self.ident:2d}: {self.__class__.__name__}"
return f"{self.__class__.__name__}"
def send(self, target, message):
"""
(process) This method can be overridden to intercept messages being sent
by this node. It should typically call `self.network.send`.
by this node. The implementation in this class calls `self.network.send`.
"""
return self.network.send(self.ident, target, message)
def receive(self, sender, message):
"""
(process) This method can be overridden to intercept messages being received
by this node. It should typically call `self.handle`.
by this node. The implementation in this class calls `self.handle`.
"""
return self.handle(sender, message)
def handle(self, message, sender):
def handle(self, sender, message):
"""
(process) Handles a message by doing nothing. Note that the handling of
each message, and the `run` method, are in separate simpy processes. That
is, yielding here will not block other incoming messages.
is, yielding here will not block other incoming messages for a direct
subclass of `PassiveNode`.
"""
return skip()
@ -49,13 +64,15 @@ class PassiveNode:
class SequentialNode(PassiveNode):
"""
A node that processes messages sequentially.
A node that processes messages sequentially. By default it sends no
messages and does nothing with received messages. This class is
intended to be subclassed.
"""
def __init__(self, ident, env, network):
def initialize(self, ident, env, network):
"""
Constructs a SequentialNode with the given simpy Environment and network.
Initializes a SequentialNode with the given simpy Environment and network.
"""
super().__init__(ident, env, network)
super().initialize(ident, env, network)
self._mailbox = deque()
self._wakeup = env.event()
@ -70,6 +87,16 @@ class SequentialNode(PassiveNode):
pass
return skip()
def handle(self, sender, message):
"""
(process) Handles a message by doing nothing. Messages are handled
sequentially; that is, handling of the next message will be blocked
on this process.
"""
# This is the same implementation as `PassiveNode`, but the documentation
# is different.
return skip()
def run(self):
"""
(process) Repeatedly handle incoming messages.
@ -87,3 +114,63 @@ class SequentialNode(PassiveNode):
# concurrency.
self._wakeup = self.env.event()
yield self._wakeup
__all__ = ['PassiveNode', 'SequentialNode']
from simpy import Environment
import unittest
from .message import PayloadMessage
from .network import Network
class PassiveReceiverTestNode(PassiveNode):
def __init__(self):
super().__init__()
self.received = deque()
def handle(self, sender, message):
self.received.append((sender, message, self.env.now))
yield self.env.timeout(3)
class SequentialReceiverTestNode(SequentialNode):
def __init__(self):
super().__init__()
self.received = deque()
def handle(self, sender, message):
self.received.append((sender, message, self.env.now))
yield self.env.timeout(3)
class SenderTestNode(PassiveNode):
def run(self):
for i in range(3):
yield from self.send(0, PayloadMessage(i))
yield self.env.timeout(1)
class TestFramework(unittest.TestCase):
def _test_node(self, receiver_node, expected):
network = Network(Environment())
network.add_node(receiver_node)
network.add_node(SenderTestNode())
network.run_all()
self.assertEqual(list(network.node(0).received), expected)
def test_passive_node(self):
self._test_node(PassiveReceiverTestNode(), [
(1, PayloadMessage(0), 1),
(1, PayloadMessage(1), 2),
(1, PayloadMessage(2), 3),
])
def test_sequential_node(self):
self._test_node(SequentialReceiverTestNode(), [
(1, PayloadMessage(0), 1),
(1, PayloadMessage(1), 4),
(1, PayloadMessage(2), 7),
])

View File

@ -2,6 +2,7 @@
Utilities.
"""
def skip():
"""
(process) Does nothing.