Compare commits

...

16 Commits

Author SHA1 Message Date
Daira Emma Hopwood e726693d5a
Merge pull request #18 from daira/doc-and-tests
Doc and test improvements
2023-10-21 00:22:25 +01:00
Daira Emma Hopwood 6e2686eae1 Consistently use "message propagation delay".
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 23:02:30 +01:00
Daira Emma Hopwood 224faeebb7 Fix an issue with overriding the network delay, and test it.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 22:40:04 +01:00
Daira Emma Hopwood 3d4a647bd0 Add comments to explain the `Node` tests.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 22:29:35 +01:00
Daira Emma Hopwood 92c8082193 Refactor starting of nodes.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 22:00:04 +01:00
Daira Emma Hopwood d5c637954d Update the simtfl module doc comment to point to tfl-book.
Co-authored-by: Nathan Wilcox <nathan-at-least@users.noreply.github.com>
2023-10-20 22:00:04 +01:00
Daira Emma Hopwood de53ac847c Add `apidoc/` to `.gitignore` and clarify README on generating docs.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:56:27 +01:00
Daira Emma Hopwood 70c9ee523d Add dependencies and script for generating API documentation.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:56:27 +01:00
Daira Emma Hopwood d8c282c73c More documentation.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:56:27 +01:00
Daira Emma Hopwood df251635bf Add references for error and warning codes in .flake8
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:56:27 +01:00
Daira Emma Hopwood 44fc6685a5 Run tests in CI.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:56:27 +01:00
Daira Emma Hopwood f971c8b299 Add tests for message-passing framework.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:09:28 +01:00
Daira Emma Hopwood c7e29c924f Minor refactoring to make the use of `Network` cleaner.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:09:28 +01:00
Daira Emma Hopwood ffc2ecbc64 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-20 21:09:28 +01:00
Daira Emma Hopwood 67de1f9841 Move documentation into a `doc/` directory.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:09:28 +01:00
Daira Emma Hopwood f9188f73f1 Use `_name` for attributes that are intended to be private.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-20 21:09:28 +01:00
16 changed files with 409 additions and 72 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

View File

@ -10,6 +10,7 @@ jobs:
- uses: actions/checkout@v2
- name: Install gnome-keyring
# https://github.com/python-poetry/poetry/issues/2692
run: sudo apt-get install gnome-keyring
- name: Install poetry

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

@ -0,0 +1,24 @@
name: tests
on: pull_request
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install gnome-keyring
# https://github.com/python-poetry/poetry/issues/2692
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

3
.gitignore vendored
View File

@ -1,6 +1,9 @@
*.swp
*.save
# API docs
apidoc/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View File

@ -23,30 +23,26 @@ 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`. This assumes
that you have run `poetry install` as shown above. 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://electric-coin-company.github.io/tfl-book/)
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
@ -8,7 +12,7 @@ class Network:
def __init__(self, env, nodes=None, delay=1):
"""
Constructs a Network with the given `simpy.Environment`, and optionally
a set of initial nodes and a message delay.
a set of initial nodes and a message propagation delay.
"""
self.env = env
self.nodes = nodes or []
@ -30,21 +34,47 @@ 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(self, node):
"""
Starts a process for the given node (which is assumed to
have already been added to this `Network`).
"""
print(f"T{self.env.now:5d}: starting {node.ident:2d}: {node}")
self.env.process(node.run())
def start_node(self, ident):
"""
(process) Start the node with the given ident.
Starts a process for the node with the given ident.
A given node should only be started once.
"""
node = self.node(ident)
print(f"T{self.env.now:5d}: starting {node}")
return node.run()
self._start(self.nodes[ident])
def start_all_nodes(self):
"""
Starts a process for each node.
A given node should only be started once.
"""
print()
for node in self.nodes:
self._start(node)
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_all_nodes()
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
with ident `sender`. The message delay is normally given by `self.delay`,
but can be overridden by the `delay` parameter.
with ident `sender`. The message propagation delay is normally given by
`self.delay`, but can be overridden by the `delay` parameter.
"""
if delay is None:
delay = self.delay
@ -60,7 +90,7 @@ class Network:
def convey(self, delay, sender, target, message):
"""
(process) Conveys a message to the node with ident `target`, from the node
with ident `sender`, after waiting for the given transmission delay.
with ident `sender`, after waiting for the given message propagation delay.
This normally should not be called directly because it *may* only complete
after the message has been handled by the target node. The caller should
not depend on when it completes.

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):
def send(self, target, message, delay=None):
"""
(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)
return self.network.send(self.ident, target, message, delay=delay)
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,27 +64,39 @@ 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)
self.mailbox = deque()
self.wakeup = env.event()
super().initialize(ident, env, network)
self._mailbox = deque()
self._wakeup = env.event()
def receive(self, sender, message):
"""
(process) Add incoming messages to the mailbox.
"""
self.mailbox.append((sender, message))
self._mailbox.append((sender, message))
try:
self.wakeup.succeed()
self._wakeup.succeed()
except RuntimeError:
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.
@ -78,12 +105,91 @@ class SequentialNode(PassiveNode):
implementation.
"""
while True:
while len(self.mailbox) > 0:
(sender, message) = self.mailbox.popleft()
while len(self._mailbox) > 0:
(sender, message) = self._mailbox.popleft()
print(f"T{self.env.now:5d}: handling {sender:2d} -> {self.ident:2d}: {message}")
yield from self.handle(sender, message)
# This naive implementation is fine because we have no actual
# concurrency.
self.wakeup = self.env.event()
yield self.wakeup
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.handled = deque()
def handle(self, sender, message):
# Record when each message is handled.
self.handled.append((sender, message, self.env.now))
# The handler takes 3 time units.
yield self.env.timeout(3)
class SequentialReceiverTestNode(SequentialNode):
def __init__(self):
super().__init__()
self.handled = deque()
def handle(self, sender, message):
# Record when each message is handled.
self.handled.append((sender, message, self.env.now))
# The handler takes 3 time units.
yield self.env.timeout(3)
class SenderTestNode(PassiveNode):
def run(self):
# We send messages at times 0, 1, 2. Since the message
# propagation delay is 1 (the default), they will be
# received at times 1, 2, 3.
for i in range(3):
yield from self.send(0, PayloadMessage(i))
yield self.env.timeout(1)
# Test overriding the propagation delay. This message
# is sent at time 3 and received at time 11.
yield from self.send(0, PayloadMessage(3), delay=8)
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).handled), expected)
def test_passive_node(self):
# A PassiveNode subclass does not block on handling of
# previous messages, so it handles each message immediately
# when it is received.
self._test_node(PassiveReceiverTestNode(), [
(1, PayloadMessage(0), 1),
(1, PayloadMessage(1), 2),
(1, PayloadMessage(2), 3),
(1, PayloadMessage(3), 11),
])
def test_sequential_node(self):
# A SequentialNode subclass *does* block on handling of
# previous messages. It handles the messages as soon as
# possible after they are received subject to that blocking,
# so they will be handled at intervals of 3 time units.
self._test_node(SequentialReceiverTestNode(), [
(1, PayloadMessage(0), 1),
(1, PayloadMessage(1), 4),
(1, PayloadMessage(2), 7),
(1, PayloadMessage(3), 11),
])

View File

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