From 5aba0c9df41704ed4b6f89828f1cc23507a43147 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Wed, 26 Nov 2025 11:14:46 -0500 Subject: [PATCH 1/4] Make a comment truer. With compression, a board size of 45 no longer crashes. Move the talk of limits to the place where we --- examples/game-of-life.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/game-of-life.py b/examples/game-of-life.py index e2944d1..5b0741e 100644 --- a/examples/game-of-life.py +++ b/examples/game-of-life.py @@ -15,10 +15,8 @@ app = Flask(__name__) -# Board width and height. We assume a square board for now. -# 45 crashes. Viceroy will pass us no more than 1936 bytes of the board. (Or -# maybe the entire URL gets truncated at 1965b.) If you change this, change the -# f"{i:010000b}" format string below to be the new value squared. +# Board width and height. We assume a square board for now. If you change this, +# change the f"{i:010000b}" format string below to be the new value squared. WIDTH = HEIGHT = 50 @@ -26,6 +24,10 @@ def decompressed_board(compressed: str) -> str: """Decompress the board representation sent from JS, returning a B&W board string ("10011011"...). + Viceroy will pass us no more than 1936 bytes of the board. (Or maybe the + entire URL gets truncated at 1965b.) This overcomes that (for the board + sizes we're interested in). + :arg compressed: A urlsafe_b64encode()d representation of the bit-packed black-and-white board. (We don't need color info in order to compute the next board.) From d515dda2d9e8ff6d5efe5eddb884287732c3d135 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Wed, 26 Nov 2025 11:52:25 -0500 Subject: [PATCH 2/4] Rename Bottle tests to make them more descriptive and less favored. --- tests/{test_app.py => test_bottle_example.py} | 6 +++--- tests/test_flask_example.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename tests/{test_app.py => test_bottle_example.py} (95%) diff --git a/tests/test_app.py b/tests/test_bottle_example.py similarity index 95% rename from tests/test_app.py rename to tests/test_bottle_example.py index 0bf4d30..ae48203 100644 --- a/tests/test_app.py +++ b/tests/test_bottle_example.py @@ -1,9 +1,9 @@ -"""Tests for the Fastly Compute Python service (app.wasm functionality).""" - from fastly_compute.testing import ViceroyTestBase -class TestFastlyComputeApp(ViceroyTestBase): +class TestBottleApp(ViceroyTestBase): + """Tests for the Bottle framework example""" + def test_hello_endpoint(self): """Test the hello endpoint returns expected content.""" response = self.get("/hello/test") diff --git a/tests/test_flask_example.py b/tests/test_flask_example.py index 3dd19b7..aaa2411 100644 --- a/tests/test_flask_example.py +++ b/tests/test_flask_example.py @@ -1,10 +1,10 @@ -"""Tests for the Flask example application.""" +"""Tests for the Flask example application""" from fastly_compute.testing import ViceroyTestBase class TestFlaskApp(ViceroyTestBase): - """Integration tests for the Flask example application.""" + """Integration tests for the Flask example application""" WASM_FILE = "build/flask-app.composed.wasm" From 6dce28f1357485cebb0becd27db9a34feef11606 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Wed, 26 Nov 2025 11:16:31 -0500 Subject: [PATCH 3/4] =?UTF-8?q?Deal=20with=20the=20case=20when=20`await=5F?= =?UTF-8?q?request()`=20returns=20`None`=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …indicating "there are no more requests this session". It's an `optional` in the WIT, though it never returned `None` before. I suspect multi-request is never happening anymore. Perhaps something changed in Viceroy, but this crash happens in Viceroy's `erik/python-sdk-compatible-2` and `main` (80c9e1486e5bbe559ec0194714bb668988984993). The crash happens *after* the request is served, so CI didn't catch it. It does now. Also add tests for Game Of Life. --- examples/game-of-life.py | 5 ++- fastly_compute/wsgi.py | 2 ++ tests/test_game_of_life_example.py | 49 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 tests/test_game_of_life_example.py diff --git a/examples/game-of-life.py b/examples/game-of-life.py index 5b0741e..b5bb860 100644 --- a/examples/game-of-life.py +++ b/examples/game-of-life.py @@ -43,9 +43,8 @@ def decompressed_board(compressed: str) -> str: @app.route("/board/") def board(compressed_board: str): - """Return the next frame of the Game Of Life, given the current one. If a "" - board is given, return a new random board. - """ + """Return the next frame of the Game Of Life, given the current one. If "none" + is given instead, return a new random board.""" cells = decompressed_board(compressed_board) # Random board on start: diff --git a/fastly_compute/wsgi.py b/fastly_compute/wsgi.py index 13717fd..37e3934 100644 --- a/fastly_compute/wsgi.py +++ b/fastly_compute/wsgi.py @@ -208,6 +208,8 @@ def handle(self, request: Any, body: Any) -> None: # Something went wrong. raise else: + if not result: + break request, body = result serve_wsgi_request( request, diff --git a/tests/test_game_of_life_example.py b/tests/test_game_of_life_example.py new file mode 100644 index 0000000..15e04f8 --- /dev/null +++ b/tests/test_game_of_life_example.py @@ -0,0 +1,49 @@ +import re +from time import sleep + +from fastly_compute.testing import ViceroyTestBase + + +class TestGameOfLife(ViceroyTestBase): + """Integration tests for the Game Of Life example""" + + WASM_FILE = "build/game-of-life.composed.wasm" + + def test_root(self): + """Show that the page full of JS at least loads.""" + response = self.get("/") + assert response.status_code == 200 + assert "async function startAnimation" in response.text + + def test_random_board(self): + """Show that random boards are generated for the first frame.""" + response = self.get("/board/none") + assert response.status_code == 200 + assert re.match(r"[01]+", response.text) + + def test_evolved_board(self): + """Show that a new board is correctly computed from an old one.""" + response = self.get( + "/board/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAEAAAAAAABAAAAAHAAQAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAKAAAAAAAEgAAAAAAAwAADAAAAAAAN6AAAAAACscAAAAAAoOAGAAAAGACBgAAAAAAgAAAAAAAgAAAAAAAsAAAAAAAIAwAAAAAAgSAAAAAAAEgAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAAQAAAAAAAOADAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + ) + assert response.status_code == 200 + assert ( + response.text + == "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000001210000000000000000000000000000000002000000000000000000000000000000000000000000000000010000000000010000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000202000000000000000000000000000000000000000000000020020000000000000000000000000000000000000000000000022000000000000000000010010000000000000000000000000000000000000000000231000300110000000000000000000000000000000000000003030001100020000000000000000000000000000000000000020300000200100000000033000000000000000000000000000032000000100000000000330000000000000000000000000000000000000000100000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000001100000022000000000000000000000000000000000000000000000002002000000000000000000000000000000000000000000000020020000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000232000000000003300000000000000000000000000000000000100000000000033000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + + def test_reuse_sandboxes(self): + """Make sure attempting to issue multiple requests to a single sandbox doesn't crash. + + This does not test whether a single sandbox actually served multiple + requests; 2 sandboxes could have served 1 request each. + """ + response = self.get("/board/none") + assert response.status_code == 200 + response = self.get("/board/none") + assert response.status_code == 200 + + # The error about failed sandbox reuse comes *after* the request has + # succeeded. And it seems to take forever to show up. + sleep(4) # 2 is not enough. I am sad. + assert "WebAssembly trapped" not in "\n".join(self.server.output_lines) From b1e49f208851aeda70db70ffa90aeecc1fc9d623 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Mon, 1 Dec 2025 10:26:30 -0500 Subject: [PATCH 4/4] Shorten the sleep in `test_reuse_sandboxes()`. Today, it seems not to need as long. The unpredictability displeases me. --- tests/test_game_of_life_example.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_game_of_life_example.py b/tests/test_game_of_life_example.py index 15e04f8..deeaf8d 100644 --- a/tests/test_game_of_life_example.py +++ b/tests/test_game_of_life_example.py @@ -36,14 +36,15 @@ def test_reuse_sandboxes(self): """Make sure attempting to issue multiple requests to a single sandbox doesn't crash. This does not test whether a single sandbox actually served multiple - requests; 2 sandboxes could have served 1 request each. + requests, though it tries to provoke that. Still, 2 sandboxes could have + served 1 request each. """ response = self.get("/board/none") assert response.status_code == 200 response = self.get("/board/none") assert response.status_code == 200 - # The error about failed sandbox reuse comes *after* the request has - # succeeded. And it seems to take forever to show up. - sleep(4) # 2 is not enough. I am sad. + # Reports about crashers in the post-response code come *after* the + # request has succeeded. And it seems to take awhile to show up. + sleep(0.5) # .3 is not enough. assert "WebAssembly trapped" not in "\n".join(self.server.output_lines)