From 1a38384786b137f38dd1f66a1c48468be6c1ca10 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Tue, 2 Sep 2025 22:12:17 +0000 Subject: [PATCH 01/13] Update WIT definitions, add tests & CI infra --- .github/workflows/python-ci.yml | 39 + .gitignore | 2 +- Makefile | 24 +- README.md | 25 +- app.py | 65 +- pyproject.toml | 57 +- tests/test_integration.py | 118 + wit/deps/cli/command.wit | 5 +- wit/deps/cli/environment.wit | 4 + wit/deps/cli/exit.wit | 13 + wit/deps/cli/imports.wit | 28 +- wit/deps/cli/run.wit | 2 + wit/deps/cli/stdio.wit | 15 +- wit/deps/cli/terminal.wit | 13 + wit/deps/clocks/monotonic-clock.wit | 21 +- wit/deps/clocks/timezone.wit | 55 + wit/deps/clocks/wall-clock.wit | 6 +- wit/deps/clocks/world.wit | 7 +- wit/deps/fastly-adapter/adapter.wit | 112 + wit/deps/fastly/compute.wit | 3327 ++++++++++++++---------- wit/deps/filesystem/preopens.wit | 7 +- wit/deps/filesystem/types.wit | 66 +- wit/deps/filesystem/world.wit | 5 +- wit/deps/http/proxy.wit | 40 +- wit/deps/http/types.wit | 8 +- wit/deps/io/error.wit | 18 +- wit/deps/io/poll.wit | 34 +- wit/deps/io/streams.wit | 34 +- wit/deps/io/world.wit | 6 +- wit/deps/random/insecure-seed.wit | 4 +- wit/deps/random/insecure.wit | 5 +- wit/deps/random/random.wit | 5 +- wit/deps/random/world.wit | 8 +- wit/deps/sockets/instance-network.wit | 4 +- wit/deps/sockets/ip-name-lookup.wit | 13 +- wit/deps/sockets/network.wit | 28 +- wit/deps/sockets/tcp-create-socket.wit | 5 +- wit/deps/sockets/tcp.wit | 56 +- wit/deps/sockets/udp-create-socket.wit | 5 +- wit/deps/sockets/udp.wit | 36 +- wit/deps/sockets/world.wit | 10 +- wit/viceroy.wit | 8 +- 42 files changed, 2868 insertions(+), 1475 deletions(-) create mode 100644 .github/workflows/python-ci.yml create mode 100644 tests/test_integration.py create mode 100644 wit/deps/clocks/timezone.wit create mode 100644 wit/deps/fastly-adapter/adapter.wit diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000..f7c3965 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,39 @@ +name: Python CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Rust Toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: '1.86.0' + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install uv + run: pip install uv + - name: Install dependencies + run: uv sync + - name: Install viceroy + run: cargo install --git https://github.com/fastly/Viceroy.git --branch sunfishcode/sync-wit viceroy + - name: Build WebAssembly component + run: make app.wasm + - name: Install dependencies + run: uv sync --extra dev --extra test + - name: Check formatting + run: make format-check + - name: Run linting + run: make lint + - name: Run tests + run: make test diff --git a/.gitignore b/.gitignore index 407389a..19f7e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ *.egg-info uv.lock -/hello_compute/ +/stubs/ __pycache__ app.wasm diff --git a/Makefile b/Makefile index c8499b7..1341be9 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,25 @@ all: app.wasm +STUBS_DIR := stubs + app.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit app.py - rm -rf hello_compute - uv run componentize-py -d wit -w compute bindings hello_compute - uv run componentize-py -d wit -w compute componentize --stub-wasi app -o app.wasm + rm -rf ${STUBS_DIR} + uv run componentize-py -d wit -w viceroy bindings ${STUBS_DIR} + uv run componentize-py -d wit -w viceroy componentize app -o app.wasm serve: app.wasm - viceroy --adapt app.wasm + viceroy serve app.wasm + +test: + uv run --extra test pytest -m integration + +lint: + uv run --extra dev ruff check . + +format: + uv run --extra dev ruff format . + +format-check: + uv run --extra dev ruff format --check . -.PHONY: all serve +.PHONY: all serve test lint format format-check diff --git a/README.md b/README.md index 085ad81..4f2315e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,30 @@ Currently, this demonstrates… # Build and Run 1. `make serve` -2. Visit http://127.0.0.1:7676/hello/fred in a browser. +2. Visit http://127.0.0.1:7676/hello/world or http://127.0.0.1:7676/info in a browser. You are seeing Bottle, a simple Python web framework, run on a Fastly Compute worker! + +# Testing + +```bash +# Install dependencies and run tests +uv sync --extra dev --extra test +make test +``` + +The integration tests automatically build the WebAssembly component, start viceroy, and verify all endpoints work correctly with the WIT APIs. + +# Development + +```bash +# Format code +make format + +# Check formatting +make format-check + +# Run linting +make lint +``` diff --git a/app.py b/app.py index 20a5846..50c46a4 100644 --- a/app.py +++ b/app.py @@ -1,40 +1,59 @@ from urllib.parse import urlparse -import art -import bottle -from bottle import template, Bottle - -from wit_world.exports import Reactor as BaseReactor -from wit_world.imports import log, http_req, http_resp, http_body +from bottle import Bottle +from wit_world.exports import HttpIncoming as BaseHttpIncoming +from wit_world.imports import http_body, http_resp, log from wit_world.imports.http_resp import send_downstream - # Enable a bit more debug logging from the framework. -bottle.debug(True) - app = Bottle() app.catchall = False # bottle backtrace causes issues; use our own. @app.route("/hello/") -def index(name): - return template("Hello
{{name}}
", name=art.text2art(name)) +def hello(name): + return f"Hello {name}!" + + +@app.route("/info") +def info(): + """Return JSON with request information we can test against""" + from bottle import request + + # Get some runtime info we can test + vcpu_time = None + try: + from wit_world.imports import compute_runtime + + vcpu_time = compute_runtime.get_vcpu_ms() + except Exception: + pass + + return { + "service": "fastly-compute-python", + "status": "ok", + "message": "Hello from Fastly Compute!", + "vcpu_time_ms": vcpu_time, + "request_method": request.environ.get("REQUEST_METHOD"), + "path_info": request.environ.get("PATH_INFO"), + } def print(*args): # hack to allow print locally; so far, monkeypatching # sys.stdout/sys.stderr hasn't panned out, so more # research required. - log.write(log_ep, " ".join(args).encode()) + log_ep.write(" ".join(args).encode()) def init(): global log_ep - log_ep = log.endpoint_get("") + log_ep = log.Endpoint.get("") + class StdErr: """File-like object to receive errors and direct them to our logging endpoint""" - + def write(self, data: str): print(f"wsgi-error: {data}") @@ -46,7 +65,7 @@ def serve_wsgi_request(req, body, app): """Pass a WSGI application a single request, and adapt its behavior back to the Fastly API.""" - response = http_resp.new() + response = http_resp.Response.new() response_body = http_body.new() def write(body_data: bytes): @@ -56,14 +75,14 @@ def write(body_data: bytes): def start_response(status: str, headers: list[tuple], exc_info=None): code, _description = status.split(" ", 1) - http_resp.status_set(response, int(code)) + response.set_status(int(code)) for header, value in headers: - http_resp.header_append(response, header.encode(), value.encode()) + response.append_header(header, value.encode()) return write - url = urlparse(http_req.uri_get(req, 2048)) + url = urlparse(req.get_uri(2048)) environ = { - "REQUEST_METHOD": http_req.method_get(req, 12), + "REQUEST_METHOD": req.get_method(12), "PATH_INFO": url.path, "QUERY_STRING": url.query, "SERVER_NAME": url.hostname, @@ -74,14 +93,14 @@ def start_response(status: str, headers: list[tuple], exc_info=None): # TODO: this would be a good place to stream, but for now we just # write to the buffer and send once the handler is done. write(body_chunk) - send_downstream(response, response_body, False) + send_downstream(response, response_body) -class Reactor(BaseReactor): - def serve(self, req: int, body: int) -> None: +class HttpIncoming(BaseHttpIncoming): + def handle(self, request, body): init() try: - serve_wsgi_request(req, body, app) + serve_wsgi_request(request, body, app) except Exception as e: log_exception(e) diff --git a/pyproject.toml b/pyproject.toml index 23cf320..6ec9ed9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,64 @@ [project] -name = "compute-sdk-python" +name = "fastly-compute" version = "0.1.0" description = "Python SDK for Fastly Compute" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "art", "bottle", "componentize-py", ] + +[project.optional-dependencies] +test = [ + "pytest>=7.0.0", + "requests>=2.28.0", +] +dev = [ + "ruff>=0.1.0", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-v", + "--tb=short", + "--strict-markers", +] +markers = [ + "integration: marks tests as integration tests (deselect with '-m \"not integration\"')", +] + +[tool.ruff] +target-version = "py312" +line-length = 88 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by formatter +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +py-modules = ["app"] diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..07d92da --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,118 @@ +"""Integration tests for the Fastly Compute Python service.""" + +import subprocess +import time +from pathlib import Path + +import pytest +import requests + + +@pytest.mark.integration +class TestFastlyComputeService: + """Integration tests for the Fastly Compute service running under viceroy.""" + + BASE_URL = "http://127.0.0.1:7676" + REQUEST_TIMEOUT = 10 + + @pytest.fixture(scope="class", autouse=True) + def build_service(self): + """Build the WebAssembly component before running tests.""" + print("Building WebAssembly component...") + result = subprocess.run( + ["make", "app.wasm"], + cwd=Path(__file__).parent.parent, + capture_output=True, + text=True, + ) + if result.returncode != 0: + pytest.fail(f"Failed to build service: {result.stderr}") + + @pytest.fixture(scope="class") + def viceroy_server(self): + """Start viceroy server for the duration of the test class.""" + print("Starting viceroy server...") + + # Start viceroy in the background + process = subprocess.Popen( + ["make", "serve"], + cwd=Path(__file__).parent.parent, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + # Wait for server to start + # TODO: key off some other signal or logs to speed this up... + time.sleep(10) + + # Check if process is still running + if process.poll() is not None: + stdout, stderr = process.communicate() + pytest.fail(f"Viceroy failed to start: {stderr}") + + yield process + + # Cleanup: terminate the process + print("Stopping viceroy server...") + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + + def _get(self, path: str) -> requests.Response: + """Make a GET request to the service.""" + return requests.get(f"{self.BASE_URL}{path}", timeout=self.REQUEST_TIMEOUT) + + def test_hello_endpoint(self, viceroy_server): + """Test the hello endpoint returns expected content.""" + response = self._get("/hello/test") + + assert response.status_code == 200 + assert response.text == "Hello test!" + + def test_hello_endpoint_with_different_name(self, viceroy_server): + """Test the hello endpoint with a different name parameter.""" + response = self._get("/hello/world") + + assert response.status_code == 200 + assert response.text == "Hello world!" + + def test_info_endpoint(self, viceroy_server): + """Test the info endpoint returns expected JSON with WIT data.""" + response = self._get("/info") + + assert response.status_code == 200 + assert response.headers.get("content-type", "").startswith("application/json") + + data = response.json() + + # Check basic service info + assert data["service"] == "fastly-compute-python" + assert data["status"] == "ok" + assert "message" in data + + # Check WIT API data + assert "vcpu_time_ms" in data + assert isinstance(data["vcpu_time_ms"], int | type(None)) + + # Check request data + assert data["request_method"] == "GET" + assert data["path_info"] == "/info" + + def test_nonexistent_endpoint(self, viceroy_server): + """Test that nonexistent endpoints return 404.""" + response = self._get("/nonexistent") + + assert response.status_code == 404 + + def test_service_health(self, viceroy_server): + """Test that the service is healthy and responsive.""" + # Make multiple requests to ensure stability + for _ in range(3): + response = self._get("/info") + assert response.status_code == 200 + + data = response.json() + assert data["status"] == "ok" diff --git a/wit/deps/cli/command.wit b/wit/deps/cli/command.wit index d8005bd..6d3cc83 100644 --- a/wit/deps/cli/command.wit +++ b/wit/deps/cli/command.wit @@ -1,7 +1,10 @@ -package wasi:cli@0.2.0; +package wasi:cli@0.2.6; +@since(version = 0.2.0) world command { + @since(version = 0.2.0) include imports; + @since(version = 0.2.0) export run; } diff --git a/wit/deps/cli/environment.wit b/wit/deps/cli/environment.wit index 7006523..2f449bd 100644 --- a/wit/deps/cli/environment.wit +++ b/wit/deps/cli/environment.wit @@ -1,3 +1,4 @@ +@since(version = 0.2.0) interface environment { /// Get the POSIX-style environment variables. /// @@ -7,12 +8,15 @@ interface environment { /// Morally, these are a value import, but until value imports are available /// in the component model, this import function should return the same /// values each time it is called. + @since(version = 0.2.0) get-environment: func() -> list>; /// Get the POSIX-style arguments to the program. + @since(version = 0.2.0) get-arguments: func() -> list; /// Return a path that programs should use as their initial current working /// directory, interpreting `.` as shorthand for this. + @since(version = 0.2.0) initial-cwd: func() -> option; } diff --git a/wit/deps/cli/exit.wit b/wit/deps/cli/exit.wit index d0c2b82..427935c 100644 --- a/wit/deps/cli/exit.wit +++ b/wit/deps/cli/exit.wit @@ -1,4 +1,17 @@ +@since(version = 0.2.0) interface exit { /// Exit the current instance and any linked instances. + @since(version = 0.2.0) exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); } diff --git a/wit/deps/cli/imports.wit b/wit/deps/cli/imports.wit index 083b84a..d9fd017 100644 --- a/wit/deps/cli/imports.wit +++ b/wit/deps/cli/imports.wit @@ -1,20 +1,36 @@ -package wasi:cli@0.2.0; +package wasi:cli@0.2.6; +@since(version = 0.2.0) world imports { - include wasi:clocks/imports@0.2.0; - include wasi:filesystem/imports@0.2.0; - include wasi:sockets/imports@0.2.0; - include wasi:random/imports@0.2.0; - include wasi:io/imports@0.2.0; + @since(version = 0.2.0) + include wasi:clocks/imports@0.2.6; + @since(version = 0.2.0) + include wasi:filesystem/imports@0.2.6; + @since(version = 0.2.0) + include wasi:sockets/imports@0.2.6; + @since(version = 0.2.0) + include wasi:random/imports@0.2.6; + @since(version = 0.2.0) + include wasi:io/imports@0.2.6; + @since(version = 0.2.0) import environment; + @since(version = 0.2.0) import exit; + @since(version = 0.2.0) import stdin; + @since(version = 0.2.0) import stdout; + @since(version = 0.2.0) import stderr; + @since(version = 0.2.0) import terminal-input; + @since(version = 0.2.0) import terminal-output; + @since(version = 0.2.0) import terminal-stdin; + @since(version = 0.2.0) import terminal-stdout; + @since(version = 0.2.0) import terminal-stderr; } diff --git a/wit/deps/cli/run.wit b/wit/deps/cli/run.wit index a70ee8c..655346e 100644 --- a/wit/deps/cli/run.wit +++ b/wit/deps/cli/run.wit @@ -1,4 +1,6 @@ +@since(version = 0.2.0) interface run { /// Run the program. + @since(version = 0.2.0) run: func() -> result; } diff --git a/wit/deps/cli/stdio.wit b/wit/deps/cli/stdio.wit index 31ef35b..cb8aea2 100644 --- a/wit/deps/cli/stdio.wit +++ b/wit/deps/cli/stdio.wit @@ -1,17 +1,26 @@ +@since(version = 0.2.0) interface stdin { - use wasi:io/streams@0.2.0.{input-stream}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream}; + @since(version = 0.2.0) get-stdin: func() -> input-stream; } +@since(version = 0.2.0) interface stdout { - use wasi:io/streams@0.2.0.{output-stream}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{output-stream}; + @since(version = 0.2.0) get-stdout: func() -> output-stream; } +@since(version = 0.2.0) interface stderr { - use wasi:io/streams@0.2.0.{output-stream}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{output-stream}; + @since(version = 0.2.0) get-stderr: func() -> output-stream; } diff --git a/wit/deps/cli/terminal.wit b/wit/deps/cli/terminal.wit index 38c724e..d305498 100644 --- a/wit/deps/cli/terminal.wit +++ b/wit/deps/cli/terminal.wit @@ -3,8 +3,10 @@ /// In the future, this may include functions for disabling echoing, /// disabling input buffering so that keyboard events are sent through /// immediately, querying supported features, and so on. +@since(version = 0.2.0) interface terminal-input { /// The input side of a terminal. + @since(version = 0.2.0) resource terminal-input; } @@ -13,37 +15,48 @@ interface terminal-input { /// In the future, this may include functions for querying the terminal /// size, being notified of terminal size changes, querying supported /// features, and so on. +@since(version = 0.2.0) interface terminal-output { /// The output side of a terminal. + @since(version = 0.2.0) resource terminal-output; } /// An interface providing an optional `terminal-input` for stdin as a /// link-time authority. +@since(version = 0.2.0) interface terminal-stdin { + @since(version = 0.2.0) use terminal-input.{terminal-input}; /// If stdin is connected to a terminal, return a `terminal-input` handle /// allowing further interaction with it. + @since(version = 0.2.0) get-terminal-stdin: func() -> option; } /// An interface providing an optional `terminal-output` for stdout as a /// link-time authority. +@since(version = 0.2.0) interface terminal-stdout { + @since(version = 0.2.0) use terminal-output.{terminal-output}; /// If stdout is connected to a terminal, return a `terminal-output` handle /// allowing further interaction with it. + @since(version = 0.2.0) get-terminal-stdout: func() -> option; } /// An interface providing an optional `terminal-output` for stderr as a /// link-time authority. +@since(version = 0.2.0) interface terminal-stderr { + @since(version = 0.2.0) use terminal-output.{terminal-output}; /// If stderr is connected to a terminal, return a `terminal-output` handle /// allowing further interaction with it. + @since(version = 0.2.0) get-terminal-stderr: func() -> option; } diff --git a/wit/deps/clocks/monotonic-clock.wit b/wit/deps/clocks/monotonic-clock.wit index 4e4dc3a..f3bc839 100644 --- a/wit/deps/clocks/monotonic-clock.wit +++ b/wit/deps/clocks/monotonic-clock.wit @@ -1,4 +1,4 @@ -package wasi:clocks@0.2.0; +package wasi:clocks@0.2.6; /// WASI Monotonic Clock is a clock API intended to let users measure elapsed /// time. /// @@ -7,38 +7,43 @@ package wasi:clocks@0.2.0; /// /// A monotonic clock is a clock which has an unspecified initial value, and /// successive reads of the clock will produce non-decreasing values. -/// -/// It is intended for measuring elapsed time. +@since(version = 0.2.0) interface monotonic-clock { - use wasi:io/poll@0.2.0.{pollable}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; /// An instant in time, in nanoseconds. An instant is relative to an /// unspecified initial value, and can only be compared to instances from /// the same monotonic-clock. + @since(version = 0.2.0) type instant = u64; /// A duration of time, in nanoseconds. + @since(version = 0.2.0) type duration = u64; /// Read the current value of the clock. /// /// The clock is monotonic, therefore calling this function repeatedly will /// produce a sequence of non-decreasing values. + @since(version = 0.2.0) now: func() -> instant; /// Query the resolution of the clock. Returns the duration of time /// corresponding to a clock tick. + @since(version = 0.2.0) resolution: func() -> duration; /// Create a `pollable` which will resolve once the specified instant - /// occured. + /// has occurred. + @since(version = 0.2.0) subscribe-instant: func( when: instant, ) -> pollable; - /// Create a `pollable` which will resolve once the given duration has - /// elapsed, starting at the time at which this function was called. - /// occured. + /// Create a `pollable` that will resolve after the specified duration has + /// elapsed from the time this function is invoked. + @since(version = 0.2.0) subscribe-duration: func( when: duration, ) -> pollable; diff --git a/wit/deps/clocks/timezone.wit b/wit/deps/clocks/timezone.wit new file mode 100644 index 0000000..ca98ad1 --- /dev/null +++ b/wit/deps/clocks/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.2.6; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/wit/deps/clocks/wall-clock.wit b/wit/deps/clocks/wall-clock.wit index 440ca0f..76636a0 100644 --- a/wit/deps/clocks/wall-clock.wit +++ b/wit/deps/clocks/wall-clock.wit @@ -1,4 +1,4 @@ -package wasi:clocks@0.2.0; +package wasi:clocks@0.2.6; /// WASI Wall Clock is a clock API intended to let users query the current /// time. The name "wall" makes an analogy to a "clock on the wall", which /// is not necessarily monotonic as it may be reset. @@ -13,8 +13,10 @@ package wasi:clocks@0.2.0; /// monotonic, making it unsuitable for measuring elapsed time. /// /// It is intended for reporting the current date and time for humans. +@since(version = 0.2.0) interface wall-clock { /// A time and date in seconds plus nanoseconds. + @since(version = 0.2.0) record datetime { seconds: u64, nanoseconds: u32, @@ -33,10 +35,12 @@ interface wall-clock { /// /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.2.0) now: func() -> datetime; /// Query the resolution of the clock. /// /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.2.0) resolution: func() -> datetime; } diff --git a/wit/deps/clocks/world.wit b/wit/deps/clocks/world.wit index c022457..5c53c51 100644 --- a/wit/deps/clocks/world.wit +++ b/wit/deps/clocks/world.wit @@ -1,6 +1,11 @@ -package wasi:clocks@0.2.0; +package wasi:clocks@0.2.6; +@since(version = 0.2.0) world imports { + @since(version = 0.2.0) import monotonic-clock; + @since(version = 0.2.0) import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; } diff --git a/wit/deps/fastly-adapter/adapter.wit b/wit/deps/fastly-adapter/adapter.wit new file mode 100644 index 0000000..4ceadf3 --- /dev/null +++ b/wit/deps/fastly-adapter/adapter.wit @@ -0,0 +1,112 @@ +/// Interfaces available to the component adapter, which are not otherwise +/// part of the Fastly Compute platform. +package fastly:adapter; + +/// Adapter functions formerly of `fastly:compute/http-req`. +/// +/// These functions depend on the host maintaining an implicit downstream +/// request. They were deprecated and replaced by functions in the +/// `http-downstream` interface which do the same thing but take an explicit +/// `request` handle. +/// +/// We could almost polyfill these functions in the adapter, by having the +/// adapter remember the downstream request handle passed in and calling the +/// `http-downstream` versions with it, but not quite. Guest programs can call +/// `send` and pass it the downstream handle, which consumes the downstream +/// handle. If guest programs do that and later call one of these functions, +/// the polyfill no longer has a valid handle it can pass in. +/// +/// So instead, we moved them to be private functions, still implemented by +/// the host, and still accessible through the component adapter, but not +/// accessible to public Wit users. +interface adapter-http-req { + use fastly:compute/types.{error, ip-address}; + use fastly:compute/http-req.{client-cert-verify-result}; + + downstream-client-ip-addr: func() -> option; + downstream-server-ip-addr: func() -> option; + downstream-client-h2-fingerprint: func(max-len: u64) -> result; + downstream-client-request-id: func(max-len: u64) -> result; + downstream-client-oh-fingerprint: func(max-len: u64) -> result; + downstream-client-ddos-detected: func() -> result; + downstream-tls-cipher-openssl-name: func(max-len: u64) -> result, error>; + downstream-tls-protocol: func(max-len: u64) -> result, error>; + downstream-tls-client-hello: func(max-len: u64) -> result, error>; + downstream-tls-client-cert-verify-result: func() -> result; + downstream-tls-ja3-md5: func() -> result, error>; + downstream-tls-ja4: func(max-len: u64) -> result; + downstream-compliance-region: func(max-len: u64) -> result; + + /// Deprecated, because it doesn't return `error.optional-none` on an empty certificate. + downstream-tls-raw-client-certificate-deprecated: func(max-len: u64) -> result, error>; + + get-original-header-names: func( + max-len: u64, + cursor: u32, + ) -> result>, error>; + + original-header-count: func() -> result; + + fastly-key-is-valid: func() -> result; + + /// Deprecated; use `redirect-to-websocket-proxy` instead. + redirect-to-websocket-proxy-deprecated: func(backend: string) -> result<_, error>; + + /// Deprecated; use `redirect-to-grip-proxy` instead. + redirect-to-grip-proxy-deprecated: func(backend: string) -> result<_, error>; +} + +interface adapter-http-downstream { + use fastly:compute/types.{error}; + use fastly:compute/http-req.{request}; + + /// Deprecated, because it doesn't return `error.optional-none` on an empty certificate. + downstream-tls-raw-client-certificate-deprecated: func( + ds-request: borrow, + max-len: u64 + ) -> result, error>; +} + +/// User-agent string parsing (deprecated). +/// +/// This was public in the Witx ABI, but it was deprecated, so now it's a +/// fastly-private API, available to existing code using the adapter, but +/// not available publicly. +interface adapter-uap { + use fastly:compute/types.{error}; + + resource user-agent { + family: func(max-len: u64) -> result; + major: func(max-len: u64) -> result; + minor: func(max-len: u64) -> result; + patch: func(max-len: u64) -> result; + } + + /// Parses a user agent string. + parse: func(user-agent: list) -> result; +} + +/// A world that just imports all the deprecated APIs, split out from the main +/// world below so that we can refer to it in tests. +world adapter-imports { + import adapter-http-req; + import adapter-http-downstream; + import adapter-uap; +} + +/// The `fastly:compute/service` world plus the deprecated interfaces. +world adapter-service { + // Make this world a superset of the public `service` world. + include fastly:compute/service; + + // And, add all the deprecated interfaces. + include adapter-imports; +} + +/// Like `adapter-service`, but only includes the imports, and not the +/// exports (`http-incoming.handle`), so that it can be used by library components +/// that don't have their own `main` function. +world adapter-service-imports { + include fastly:compute/service-imports; + include adapter-imports; +} diff --git a/wit/deps/fastly/compute.wit b/wit/deps/fastly/compute.wit index 18934c7..0346749 100644 --- a/wit/deps/fastly/compute.wit +++ b/wit/deps/fastly/compute.wit @@ -1,45 +1,60 @@ -package fastly:api; +/// This is a [Wit] file defining the APIs of the [Fastly Compute platform]. +/// +/// This file defines the `fastly:compute/service` world, which defines the +/// set of interfaces available to, and expected of, Fastly Compute service +/// applications. +/// +/// [Wit]: https://component-model.bytecodealliance.org/design/wit.html +/// [Fastly Compute platform]: https://www.fastly.com/documentation/guides/compute/ +package fastly:compute; +/// Types used by many interfaces in this package. interface types { - // TODO: split this up into function-specific error enums + /// A common error type used by many functions in this package. + /// + /// TODO: In the future this should be split up into more-specific error + /// enums so that it better documents which errors each function can actually + /// return and what they mean. variant error { - /// Unknown error value. - /// It should be an internal error if this is returned. - unknown-error, /// Generic error value. - /// This means that some unexpected error occurred during a hostcall. + /// + /// This means that some unexpected error occurred. generic-error, /// Invalid argument. invalid-argument, /// Invalid handle. - /// Thrown when a handle is not valid. E.G. No dictionary exists with the given name. + /// + /// Returned when a handle is not valid, for example when no dictionary exists with the given + /// name. bad-handle, /// Buffer length error. - /// Thrown when a buffer is the wrong size. + /// + /// Returned when a buffer is the wrong size. /// Includes the buffer length that would allow the operation to succeed. buffer-len(u64), /// Unsupported operation error. - /// This error is thrown when some operation cannot be performed, because it is not supported. + /// + /// This error is returned when some operation cannot be performed, because it is not supported. unsupported, - /// Alignment error. - /// This is thrown when a pointer does not point to a properly aligned slice of memory. - bad-align, /// Invalid HTTP error. - /// This can be thrown when a method, URI, header, or status is not valid. This can also - /// be thrown if a message head is too large. + /// + /// This can be returned when a method, URI, header, or status is not valid. This can also + /// be returned if a message head is too large. http-invalid, /// HTTP user error. - /// This is thrown in cases where user code caused an HTTP error. For example, attempt to send + /// + /// This is returned in cases where user code caused an HTTP error. For example, attempt to send /// a 1xx response code, or a request with a non-absolute URI. This can also be caused by /// an unexpected header: both `content-length` and `transfer-encoding`, for example. http-user, /// HTTP incomplete message error. - /// This can be thrown when a stream ended unexpectedly. + /// + /// This can be returned when a stream ended unexpectedly. http-incomplete, - /// A `None` error. + /// A “none” error. + /// /// This status code is used to indicate when an optional value did not exist, as opposed to /// an empty value. - /// Note, this value should no longer be used, as we have explicit optional types now. optional-none, /// Message head too large. http-head-too-large, @@ -49,217 +64,666 @@ interface types { /// /// This is returned when an attempt to allocate a resource has exceeded the maximum number of /// resources permitted. For example, creating too many response handles. - limit-exceeded + limit-exceeded, } - /// A handle to an individual secret. - type secret-handle = u32; -} - -interface http-types { + /// IPv4 addresses. + type ipv4-address = tuple; - use types.{secret-handle}; + /// IPv6 addresses. + type ipv6-address = tuple; - /// A handle to an HTTP request or response body. - type body-handle = u32; + /// IPv4 or IPv6 addresses. + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } +} - /// A handle to an HTTP request. - type request-handle = u32; - /// A handle to a currently-pending asynchronous HTTP request. - type pending-request-handle = u32; - /// A handle to an HTTP response. - type response-handle = u32; - type request = tuple; - type response = tuple; +/// Types used by HTTP interfaces in this package. +interface http-types { - /// A tag indicating HTTP protocol versions. + /// HTTP protocol versions. enum http-version { + /// HTTP/0.9 http09, + /// HTTP/1.0 http10, + /// HTTP/1.1 http11, + /// HTTP/2.0 h2, + /// HTTP/3.0 h3 } + /// HTTP [content encoding] flags + /// + /// [content encoding]: https://www.rfc-editor.org/rfc/rfc9110.html#field.content-encoding flags content-encodings { + /// [Gzip coding] + /// + /// [Gzip coding]: https://www.rfc-editor.org/rfc/rfc9110.html#gzip.coding gzip } - /// Adjust how this requests's framing headers are determined. + /// Determines how the framing headers (`Content-Length`/`Transfer-Encoding`) are set for a + /// request or response. enum framing-headers-mode { + /// Determine the framing headers automatically based on the message body, and discard any + /// framing headers already set in the message. This is the default behavior. + /// + /// In automatic mode, a `Content-Length` is used when the size of the body can be determined + /// before it is sent. Requests/responses sent in streaming mode, where headers are sent + /// immediately but the content of the body is streamed later, will receive a + /// `Transfer-Encoding: chunked` to accommodate the dynamic generation of the body. automatic, + + /// Use the exact framing headers set in the message, falling back to `automatic` if invalid. + /// + /// In “from headers” mode, any `Content-Length` or `Transfer-Encoding` headers will be honored. + /// You must ensure that those headers have correct values permitted by the + /// [HTTP/1.1 specification]. If the provided headers are not permitted by the spec, the headers + /// will revert to automatic mode and a log diagnostic will be issued about what was wrong. If a + /// `Content-Length` is permitted by the spec, but the value doesn't match the size of the + /// actual body, the body will either be truncated (if it is too long), or the connection will + /// be hung up early (if it is too short). + /// + /// [HTTP/1.1 specification]: https://www.rfc-editor.org/rfc/rfc7230#section-3.3.1 manually-from-headers } + /// [Transport Layer Security] (TLS) version + /// + /// [Transport Layer Security]: https://www.rfc-editor.org/rfc/rfc8446.html enum tls-version { + /// TLS 1.0 tls1, + /// TLS 1.1 tls11, + /// TLS 1.2 tls12, + /// TLS 1.3 tls13 } - flags backend-config-options { - reserved, - host-override, - connect-timeout, - first-byte-timeout, - between-bytes-timeout, - use-tls, - tls-min-version, - tls-max-version, - cert-hostname, - ca-cert, - ciphers, - sni-hostname, - dont-pool, - client-cert, - grpc, - keepalive, - } - - /// Create a backend for later use - record dynamic-backend-config { - host-override: string, - connect-timeout: u32, - first-byte-timeout: u32, - between-bytes-timeout: u32, - tls-min-version: option, - tls-max-version: option, - cert-hostname: string, - ca-cert: string, - ciphers: string, - sni-hostname: string, - client-cert: string, - client-key: secret-handle, - http-keepalive-time-ms: u32, - tcp-keepalive-enable: u32, - tcp-keepalive-interval-secs: u32, - tcp-keepalive-probes: u32, - tcp-keepalive-time-secs: u32, - } - - /// HTTP status codes. + /// HTTP [status codes]. + /// + /// [status codes]: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml type http-status = u16; } -/// Fastly UAP -interface uap { +/// HTTP bodies. +interface http-body { use types.{error}; - resource user-agent { - family: func(max-len: u64) -> result; - major: func(max-len: u64) -> result; - minor: func(max-len: u64) -> result; - patch: func(max-len: u64) -> result; - } + /// An HTTP request or response body. + use async-io.{pollable as body}; - /// Parse a user agent string. - parse: func(user-agent: list) -> result; -} + /// Creates a new empty body that can be used for outgoing requests and responses. + new: func() -> result; -/// Fastly HTTP Body -interface http-body { + /// Appends the contents of the body `src` to the body `dest`. + append: func(dest: borrow, src: body) -> result<_, error>; - use types.{error}; - use http-types.{body-handle}; + /// Reads from a body. + read: func(body: borrow, chunk-size: u32) -> result, error>; + + /// Writes to a body. + write: func(body: borrow, buf: list, end: write-end) -> result; + /// Which side of a body to write to. enum write-end { + /// Write to the back of the body; that is, append to it. back, + + /// Write to the front of the body; that is, prepend to it. front } - append: func(dest: body-handle, src: body-handle) -> result<_, error>; - - new: func() -> result; - - read: func(h: body-handle, chunk-size: u32) -> result, error>; - - write: func(h: body-handle, buf: list, end: write-end) -> result; - - /// Frees the body on the host. + /// Frees a body. + /// + /// This releases resources associated with the body. /// - /// For streaming bodies, this is a _successful_ stream termination, which will signal + /// For streaming bodies, this is a *successful* stream termination, which will signal /// via framing that the body transfer is complete. - close: func(h: body-handle) -> result<_, error>; - - /// Frees a streaming body on the host _unsuccessfully_, so that framing makes clear that - /// the body is incomplete. - abandon: func(h: body-handle) -> result<_, error>; + /// + /// If a handle is dropped without calling `close`, it's an *unsuccessful* stream + /// termination. + close: func(body: body) -> result<_, error>; - /// Returns a u64 body length if the length of a body is known, or `FastlyStatus::None` - /// otherwise. + /// Returns a `u64` body length if the length of a body is known, or `none` otherwise. /// /// If the length is unknown, it is likely due to the body arising from an HTTP/1.1 message with /// chunked encoding, an HTTP/2 or later message with no `content-length`, or being a streaming /// body. /// - /// Note that receiving a length from this function does not guarantee that the full number of + /// Receiving a length from this function does not guarantee that the full number of /// bytes can actually be read from the body. For example, when proxying a response from a /// backend, this length may reflect the `content-length` promised in the response, but if the /// backend connection is closed prematurely, fewer bytes may be delivered before this body /// handle can no longer be read. - known-length: func(h: body-handle) -> result; + get-known-length: func(body: borrow) -> option; - trailer-append: func( - h: body-handle, - name: list, + /// Adds a body trailing header with given value. + append-trailer: func( + body: borrow, + name: string, value: list, ) -> result<_, error>; - trailer-names-get: func( - h: body-handle, + /// Gets the names of the trailers associated with this body. + /// + /// The first `cursor` names are skipped. The remaining names are encoded successively with + /// a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + /// names don't fit, the returned `option` is the index of the first name that didn't fit, + /// or `none` if all the remaining names fit. If `max-len` is too small to fit any name, an + /// `error.buffer-len` error is returned, providing a recommended buffer size. + get-trailer-names: func( + body: borrow, max-len: u64, cursor: u32, - ) -> result, option>>, error>; + ) -> result>, error>; - trailer-value-get: func( - h: body-handle, - name: list, + /// Gets the value for the trailer with the given name, or `none` if the trailer is not present. + /// + /// If there are multiple values for this header, only one is returned, which may be + /// any of the values. See `get-trailer-values` if you need to get all of the values. + /// + /// This functions returns `ok(some(v))` if the trailer with the given name is present, + /// and `ok(none)` if no trailer with the given name is present. If `max-len` is too + /// small to fit the value, an `error.buffer-len` error is returned, providing a + /// recommended buffer size. + get-trailer-value: func( + body: borrow, + name: string, max-len: u64, ) -> result>, error>; - trailer-values-get: func( - h: body-handle, - name: list, + /// Gets multiple values associated with the trailer with the given name. + /// + /// As opposed to `get-trailer-value`, this function returns all of the values for this trailer. + /// + /// The first `cursor` values are skipped. The remaining values are encoded successively with + /// a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + /// values don't fit, the returned `option` is the index of the first value that didn't + /// fit, or `none` if all the remaining values fit. If `max-len` is too small to fit any value, + /// an `error.buffer-len` error is returned, providing a recommended buffer size. + get-trailer-values: func( + body: borrow, + name: string, max-len: u64, cursor: u32 - ) -> result, option>>, error>; + ) -> result, option>, error>; } -/// Fastly Log +/// Low-level interface to Fastly's [Real-Time Log Streaming] endpoints. +/// +/// [Real-Time Log Streaming]: https://docs.fastly.com/en/guides/about-fastlys-realtime-log-streaming-features interface log { use types.{error}; - /// A handle to a logging endpoint. - type handle = u32; + /// A logging endpoint. + resource endpoint { + /// Tries to get an endpoint by name. + /// + /// Currently, the conditions on an endpoint name are: + /// - It must not be empty. + /// - It must not contain newlines (`\n`) or colons (`:`). + /// - It must not be `stdout` or `stderr`, which are reserved for debugging. + /// + /// Names are case sensitive. Calling `get-endpoint` with a name that doesn't correspond to any + /// logging endpoint available in your service will still return a usable endpoint, and writes + /// to that endpoint will succeed. Refer to your service dashboard to diagnose missing log + /// events. + get: static func(name: string) -> result; + + /// Writes a data to the given endpoint. + /// + /// Each call to `write` produces a single log event. On success, the number of bytes written + /// is returned. + write: func(msg: list) -> result; + } +} + +/// HTTP downstream requests and metadata. +/// +/// “Downstream” here refers to incoming HTTP requests. +interface http-downstream { + use types.{error, ip-address}; + use http-req.{ + request, client-cert-verify-result, error-with-detail, cache-override, request-promise, + request-with-body, + }; + + /// Configuration for `next-request`. + record next-request-options { + timeout-ms: option, + + /// Additional options may be added in the future via this resource type. + extra: option>, + } + + /// Extensibility for `next-request-options` + resource extra-next-request-options {} + + /// Starts waiting for the next request. + next-request: func( + options: next-request-options, + ) -> result; + + /// Waits until the next request is available, and then returns the resulting + /// request and body. + await-next-request: func( + pending: request-promise, + ) -> result; + + next-request-abandon: func( + pending: request-promise, + ) -> result<_, error>; + + /// Returns the client request's header names exactly as they were originally received. + /// + /// This includes both the original header name characters' cases, as well as the original order + /// of the received headers. + /// + /// The first `cursor` names are skipped. The remaining names are encoded successively with + /// a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + /// names don't fit, the returned `option` is the index of the first name that didn't fit, + /// or `none` if all the remaining names fit. If `max-len` is too small to fit any name, + /// an `error.buffer-len` error is returned, providing a recommended buffer size. + downstream-original-header-names: func( + ds-request: borrow, + max-len: u64, + cursor: u32, + ) -> result>, error>; + + /// Returns the number of headers in the client request as originally received. + downstream-original-header-count: func( + ds-request: borrow + ) -> result; + + /// Returns the IP address of the client making the HTTP request, if known. + downstream-client-ip-addr: func( + ds-request: borrow + ) -> option; + + /// Returns the IP address on which this server received the HTTP request, if known. + downstream-server-ip-addr: func( + ds-request: borrow + ) -> option; + + /// Gets the HTTP/2 fingerprint of client request if available. + downstream-client-h2-fingerprint: func( + ds-request: borrow, + max-len: u64 + ) -> result; + + /// Gets the id of the current request if available. + downstream-client-request-id: func( + ds-request: borrow, + max-len: u64 + ) -> result; + + /// Gets the fingerprint of client request headers if available. + downstream-client-oh-fingerprint: func( + ds-request: borrow, + max-len: u64 + ) -> result; + + /// Returns whether the request was tagged as contributing to a DDoS attack. + downstream-client-ddos-detected: func( + ds-request: borrow + ) -> result; + + /// Gets the cipher suite used to secure the downstream client TLS connection. + /// + /// The value returned will be consistent with the [OpenSSL name] for the cipher suite. + /// + /// [OpenSSL name]: https://testssl.sh/openssl-iana.mapping.html + downstream-tls-cipher-openssl-name: func( + ds-request: borrow, + max-len: u64 + ) -> result, error>; + + /// Gets the TLS protocol version used to secure the downstream client TLS connection. + downstream-tls-protocol: func( + ds-request: borrow, + max-len: u64 + ) -> result, error>; + + /// Gets the raw bytes sent by the client in the TLS ClientHello message. + /// + /// See [RFC 5246] for details. + /// + /// [RFC 5246]: https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2 + downstream-tls-client-hello: func( + ds-request: borrow, + max-len: u64 + ) -> result, error>; + + /// Gets the raw client certificate used to secure the downstream client mTLS connection. + /// + /// The value returned will be based on PEM format. + /// + /// Returns `error.optional-none` if the connection is not mTLS or is unavailable. + downstream-tls-raw-client-certificate: func( + ds-request: borrow, + max-len: u64 + ) -> result, error>; + + /// Returns the `client-cert-verify-result` from the downstream client mTLS handshake. + /// + /// Returns `none` if not available. + downstream-tls-client-cert-verify-result: func( + ds-request: borrow + ) -> result; + + /// Gets the JA3 hash of the TLS ClientHello message. + downstream-tls-ja3-md5: func( + ds-request: borrow + ) -> result, error>; - endpoint-get: func(name: string) -> result; + /// Gets the JA4 hash of the TLS ClientHello message. + downstream-tls-ja4: func( + ds-request: borrow, + max-len: u64 + ) -> result; + + /// Gets the compliance region that the client IP address is in. + downstream-compliance-region: func( + ds-request: borrow, + max-len: u64 + ) -> result; - write: func(h: handle, msg: list) -> result; + /// Returns whether or not the original client request arrived with a + /// Fastly-Key belonging to a user with the rights to purge content on this + /// service. + fastly-key-is-valid: func( + ds-request: borrow, + ) -> result; } -/// Fastly HTTP Req +/// HTTP requests. interface http-req { - use types.{error}; - use http-types.{ - body-handle, request-handle, http-version, response, pending-request-handle, - content-encodings, framing-headers-mode, backend-config-options, - dynamic-backend-config - }; + use types.{error, ip-address}; + use http-types.{http-version, content-encodings, framing-headers-mode, tls-version}; + use http-resp.{response}; + use http-body.{body}; + use secret-store.{secret}; + use http-resp.{response-with-body}; + + /// Handle that can be used to wait for a sent request. + use async-io.{pollable as pending-request}; + + /// Handle that can be used to wait for incoming requests. + use async-io.{pollable as request-promise}; + + /// An HTTP request. + resource request { + /// Creates a new `request` with no method, URL, or headers, and an empty body. + new: static func() -> result; + + /// Sets the cache override behavior for this request. + /// + /// This setting will override any cache directive headers returned in response to this request. + set-cache-override: func( + cache-override: cache-override, + ) -> result<_, error>; + + /// Reads the request's header names via a buffer of the provided size. + /// + /// The first `cursor` names are skipped. The remaining names are encoded successively with + /// a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + /// names don't fit, the returned `option` is the index of the first name that didn't fit, + /// or `none` if all the remaining names fit. If `max-len` is too small to fit any name, + /// an `error.buffer-len` error is returned, providing a recommended buffer size. + get-header-names: func( + max-len: u64, + cursor: u32, + ) -> result>, error>; + + /// Gets the value of a header, or `none` if the header is not present. + /// + /// If there are multiple values for the header, only one is returned. See + /// `get-header-values` if you need to get all of the values. + /// + /// If header name requires more than `max-len` bytes, this will return an `error.buffer-len` + /// containing the required size. + get-header-value: func( + name: string, + max-len: u64, + ) -> result>, error>; + + /// Gets multiple header values for the given `name` via a buffer of the provided size. + /// + /// As opposed to `get-header-value`, this function returns all of the values for this header. + /// + /// The first `cursor` values are skipped. The remaining values are encoded successively with + /// a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + /// values don't fit, the returned `option` is the index of the first value that didn't + /// fit, or `none` if all the remaining values fit. If `max-len` is too small to fit any value, + /// an `error.buffer-len` error is returned, providing a recommended buffer size. + get-header-values: func( + name: string, + max-len: u64, + cursor: u32 + ) -> result, option>, error>; + + /// Sets the values for the given header name, replacing any headers that previously existed for + /// that name. + set-header-values: func( + name: string, + /// contains multiple values each terminated by `\0` and concatenated + values: list + ) -> result<_, error>; + + /// Sets a request header to the given value, discarding any previous values for the given + /// header name. + insert-header: func(name: string, value: list) -> result<_, error>; + + /// Adds a request header with given value. + /// + /// Unlike `set-header-values`, this does not discard existing values for the same header name. + append-header: func( + name: string, + value: list, + ) -> result<_, error>; + + /// Removes all request headers of the given name + /// + /// Returns `ok` if any headers were successfully removed. + remove-header: func(name: string) -> result<_, error>; + + /// Gets the request method. + get-method: func(max-len: u64) -> result; + + /// Sets the request method. + set-method: func(method: string) -> result<_, error>; + + /// Gets the request URI. + get-uri: func(max-len: u64) -> result; + + /// Sets the request URI. + set-uri: func(uri: string) -> result<_, error>; + + /// Gets the HTTP version of this request. + get-version: func() -> result; + + /// Sets the HTTP version of this request. + set-version: func(version: http-version) -> result<_, error>; + + /// Sets the content encodings to automatically decompress responses to this request. + /// + /// If the response to this request is encoded by one of the encodings set by this method, the + /// response will be presented to the Compute program in decompressed form with the + /// `Content-Encoding` and `Content-Length` headers removed. + set-auto-decompress-response: func( + encodings: content-encodings, + ) -> result<_, error>; + + /// Passes the WebSocket directly to a backend. + /// + /// This can only be used on services that have the WebSockets feature enabled and on requests + /// that are valid WebSocket requests. + /// + /// The sending completes in the background. Once this method has been called, no other response + /// can be sent to this request, and the application can exit without affecting the send. + /// + /// See the [WebSockets passthrough] documentation for a high-level description of this feature. + /// + /// [WebSockets passthrough]: https://www.fastly.com/documentation/guides/concepts/real-time-messaging/websockets-tunnel/ + redirect-to-websocket-proxy: func( + backend: string, + ) -> result<_, error>; + + /// Sets how the framing headers `Content-Length` and `Transfer-Encoding` will be determined + /// when sending this request. + set-framing-headers-mode: func( + mode: framing-headers-mode, + ) -> result<_, error>; + + /// Inspects request HTTP traffic using the [NGWAF] lookaside service. + /// + /// Returns a JSON-encoded string. + /// + /// [NGWAF]: https://docs.fastly.com/en/ngwaf/ + inspect: func( + body: borrow, + options: inspect-options, + max-len: u64 + ) -> result; + + /// Instead of having this request cache in this service's space, use the + /// cache of the named service + on-behalf-of: func( + service: string, + ) -> result<_, error>; + + redirect-to-grip-proxy: func( + backend: string, + ) -> result<_, error>; + } + + /// Retrieves a response for the request, either from cache or by sending it + /// to the given backend server. + /// + /// Returns once the response headers have been received, or an error occurs. + send: func( + request: request, + body: body, + backend: string, + ) -> result; + + /// Sends the request directly to the backend server without performing any + /// caching or inserting any cache-related headers in the response. + /// + /// Returns once the response headers have been received, or an error occurs. + send-uncached: func( + request: request, + body: body, + backend: string, + ) -> result; + + /// Begins sending the request to the given backend server, and returns a + /// `pending-request` that can yield the backend response or an error. + /// + /// This method returns as soon as the request begins sending to the backend, + /// and transmission of the request body and headers will continue in the + /// background. + /// + /// This method allows for sending more than one request at once and receiving + /// their responses in arbitrary orders. See `pending-request` for more + /// details on how to wait on, poll, or select between pending requests. + /// + /// This method is also useful for sending requests where the response is + /// unimportant, but the request may take longer than the Compute program is + /// able to run, as the request will continue sending even after the program + /// that initiated it exits. + send-async: func( + request: request, + body: body, + backend: string + ) -> result; + + /// This is to `send-async` as `send-uncached` is to `send`. + /// + /// As with `send-uncached`, this function sends the request directly to the + /// backend server without performing any caching or inserting any + /// cache-related headers in the response. + send-async-uncached: func( + request: request, + body: body, + backend: string, + ) -> result; + + /// Begins sending the request to the given backend server, and returns a + /// `pending-request` that can yield the backend response or an error. + /// + /// The `body` argument is not consumed, so that it can accept further data to send. + /// + /// The backend connection is only closed once `http-body.close` is called. The + /// `pending-request` will not yield a `response` until the body is finished. + /// + /// This method is most useful for programs that do some sort of processing or + /// inspection of a potentially-large client request body. Streaming allows the + /// program to operate on small parts of the body rather than having to read it all + /// into memory at once. + /// + /// This method returns as soon as the request begins sending to the backend, + /// and transmission of the request body and headers will continue in the + /// background. + send-async-streaming: func( + request: request, + body: borrow, + backend: string, + ) -> result; + + /// This is to `send-async-streaming` as `send-uncached` is to `send`. + /// + /// As with `send-uncached`, this function sends the request directly to the + /// backend server without performing any caching or inserting any + /// cache-related headers in the response. + send-async-uncached-streaming: func( + request: request, + body: borrow, + backend: string, + ) -> result; + + type request-with-body = tuple; + + /// Optional override for response caching behavior. + variant cache-override { + /// Do not override the behavior specified in the origin response’s cache control headers. + none, - /// An override for response caching behavior. - /// A zero value indicates that the origin response's cache control headers should be used. - flags cache-override-tag { - /// Do not cache the response to this request, regardless of the origin response's headers. + /// Do not cache the response to this request, regardless of the origin response’s headers. pass, - ttl, - stale-while-revalidate, - pci, + + /// Override particular cache control settings. + override(cache-override-details) + } + + /// The fields for the `override` arm of `cache-override`. + /// + /// The origin response’s cache control headers will be used for ttl and + /// `stale-while-revalidate` if `none`. + record cache-override-details { + ttl: option, + stale-while-revalidate: option, + pci: bool, + surrogate-key: option>, + + /// Additional options may be added in the future via this resource type. + extra: option>, } + /// Extensibility for `cache-override-details` + resource extra-cache-override-details {} + /// TLS client certificate verified result from downstream. enum client-cert-verify-result { /// Success value. @@ -269,7 +733,7 @@ interface http-req { /// bad certificate error. /// /// This error means the certificate is corrupt - /// (e.g., the certificate signatures do not verify correctly). + /// (for example, when the certificate signatures do not verify correctly). bad-certificate, /// certificate revoked error. /// @@ -306,13 +770,13 @@ interface http-req { /// hostname. dns-timeout, /// The system encountered a DNS error when trying to find an IP address for the backend - /// hostname. The fields $dns_error_rcode and $dns_error_info_code may be set in the + /// hostname. The fields `dns-error-rcode` and `dns-error-info-code` may be set in the /// $send_error_detail. dns-error, /// The system cannot determine which backend to use, or the specified backend was invalid. destination-not-found, - /// The system considers the backend to be unavailable; e.g., recent attempts to communicate - /// with it may have failed, or a health check may indicate that it is down. + /// The system considers the backend to be unavailable, for example when recent attempts to + /// communicate with it may have failed, or a health check may indicate that it is down. destination-unavailable, /// The system cannot find a route to the next-hop IP address. destination-ip-unroutable, @@ -345,8 +809,9 @@ interface http-req { /// The process of negotiating an upgrade of the HTTP version between the system and the /// backend failed. http-upgrade-failed, - /// The system encountered an HTTP protocol error when communicating with the backend. This - /// error will only be used when a more specific one is not defined. + /// The system encountered an HTTP protocol error when communicating with the backend. + /// + /// This error will only be used when a more specific one is not defined. http-protocol-error, /// An invalid cache key was provided for the request. http-request-cache-key-invalid, @@ -354,7 +819,7 @@ interface http-req { http-request-uri-invalid, /// The system encountered an unexpected internal error. internal-error, - /// The system received a TLS alert from the backend. The field $tls_alert_id may be set in + /// The system received a TLS alert from the backend. The field `tls-alert-id` may be set in /// the $send_error_detail. tls-alert-received, /// The system encountered a TLS error when communicating with the backend, either during @@ -362,19 +827,11 @@ interface http-req { tls-protocol-error, } - flags send-error-detail-mask { - reserved, - dns-error-rcode, - dns-error-info-code, - tls-alert-id, - } - record send-error-detail { tag: send-error-detail-tag, - mask: send-error-detail-mask, - dns-error-rcode: u16, - dns-error-info-code: u16, - tls-alert-id: u8, + dns-error-rcode: option, + dns-error-info-code: option, + tls-alert-id: option, } record error-with-detail { @@ -382,625 +839,714 @@ interface http-req { error: error, } - flags inspect-config-options { - reserved, - corp, - workspace, - } + /// Configuration for inspecting a `request` using Security. + record inspect-options { + corp: option, + workspace: option, - record inspect-config { - corp: string, - workspace: string, + /// Additional options may be added in the future via this resource type. + extra: option>, } - cache-override-set: func( - h: request-handle, - tag: cache-override-tag, - ttl: u32, - stale-while-revalidate: u32, - ) -> result<_, error>; - - cache-override-v2-set: func( - h: request-handle, - tag: cache-override-tag, - ttl: u32, - stale-while-revalidate: u32, - sk: option> - ) -> result<_, error>; - - downstream-client-ip-addr: func() -> result, error>; - - downstream-server-ip-addr: func() -> result, error>; + /// Extensibility for `inspect-options` + resource extra-inspect-options {} - downstream-client-h2-fingerprint: func(max-len: u64) -> result, error>; + /// Waits until the request is completed, and then returns the resulting + /// response and body. + await-request: func( + pending: pending-request + ) -> result; - downstream-client-request-id: func(max-len: u64) -> result, error>; + /// Closes the `request`, releasing any associated resources. + /// + /// A `request` is automatically consumed when you send a request. You should call `close` + /// only if you have a `request` you don't intend to use anymore. + close: func(request: request) -> result<_, error>; - downstream-client-oh-fingerprint: func(max-len: u64) -> result, error>; + upgrade-websocket: func(backend: string) -> result<_, error>; - downstream-client-ddos-detected: func() -> result; + /// Create a backend for later use + register-dynamic-backend: func( + prefix: string, + target: string, + options: dynamic-backend-options, + ) -> result<_, error>; - downstream-tls-cipher-openssl-name: func(max-len: u64) -> result, error>; + /// Create a backend for later use + resource dynamic-backend-options { + constructor(); + + host-override: func(value: string); + connect-timeout: func(value: u32); + first-byte-timeout: func(value: u32); + between-bytes-timeout: func(value: u32); + use-tls: func(value: bool); + tls-min-version: func(value: tls-version); + tls-max-version: func(value: tls-version); + cert-hostname: func(value: string); + ca-cert: func(value: string); + ciphers: func(value: string); + sni-hostname: func(value: string); + client-cert: func(client-cert: string, key: borrow); + http-keepalive-time-ms: func(value: u32); + tcp-keepalive-enable: func(value: u32); + tcp-keepalive-interval-secs: func(value: u32); + tcp-keepalive-probes: func(value: u32); + tcp-keepalive-time-secs: func(value: u32); + pooling: func(value: bool); + grpc: func(value: bool); + } +} - downstream-tls-protocol: func(max-len: u64) -> result, error>; +/// HTTP responses. +interface http-resp { + use types.{error, ip-address}; - downstream-tls-client-hello: func(max-len: u64) -> result, error>; + use http-types.{ + http-version, http-status, + framing-headers-mode + }; + use http-body.{body}; - downstream-tls-raw-client-certificate: func(max-len: u64) -> result, error>; + /// An HTTP response. + resource response { + /// Create a new `response`. + /// + /// The new `response` is created with status code 200 OK, no headers, and an empty body. + new: static func() -> result; - downstream-tls-client-cert-verify-result: func() -> result; + /// Read the response's header names via a buffer of the provided size. + /// + /// The first `cursor` names are skipped. The remaining names are encoded successively with + /// a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + /// names don't fit, the returned `option` is the index of the first name that didn't fit, + /// or `none` if all the remaining names fit. If `max-len` is too small to fit any name, + /// an `error.buffer-len` error is returned, providing a recommended buffer size. + get-header-names: func( + max-len: u64, + cursor: u32, + ) -> result>, error>; + + /// Gets the value of a header, or `none` if the header is not present. + /// + /// If there are multiple values for the header, only one is returned. See + /// `get-header-values` if you need to get all of the values. + /// + /// If header name requires more than `max-len` bytes, this will return an `error.buffer-len` + /// containing the required size. + get-header-value: func( + name: string, + max-len: u64, + ) -> result>, error>; + + /// Gets multiple header values for the given `name` via a buffer of the provided size. + /// + /// As opposed to `get-header-value`, this function returns all of the values for this header. + /// + /// The first `cursor` values are skipped. The remaining values are encoded successively with + /// a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + /// values don't fit, the returned `option` is the index of the first value that didn't + /// fit, or `none` if all the remaining values fit. If `max-len` is too small to fit any value, + /// an `error.buffer-len` error is returned, providing a recommended buffer size. + get-header-values: func( + name: string, + max-len: u64, + cursor: u32 + ) -> result, option>, error>; + + /// Sets the values for the given header name, replacing any headers that previously existed for + /// that name. + set-header-values: func( + name: string, + /// contains multiple values each terminated by `\0` and concatenated + values: list + ) -> result<_, error>; + + /// Sets a response header to the given value, discarding any previous values for the given + /// header name. + insert-header: func( + name: string, + value: list, + ) -> result<_, error>; + + /// Add a response header with given value. + /// + /// Unlike `set-header-values`, this does not discard existing values for the same header name. + append-header: func( + name: string, + value: list, + ) -> result<_, error>; - downstream-tls-ja3-md5: func() -> result, error>; + /// Remove all response headers of the given name + /// + /// Returns `ok` if any headers were successfully removed. + remove-header: func(name: string) -> result<_, error>; - downstream-tls-ja4: func(max-len: u64) -> result, error>; + /// Gets the HTTP version of this response. + get-version: func() -> result; - downstream-compliance-region: func(max-len: u64) -> result, error>; + /// Sets the HTTP version of this response. + set-version: func(version: http-version) -> result<_, error>; - new: func() -> result; + /// Gets the HTTP status code of the response. + get-status: func() -> result; - header-names-get: func( - h: request-handle, - max-len: u64, - cursor: u32, - ) -> result, option>>, error>; + /// Sets the HTTP status code of the response. + set-status: func(status: http-status) -> result<_, error>; - original-header-names-get: func( - max-len: u64, - cursor: u32, - ) -> result, option>>, error>; + /// Sets how the framing headers `Content-Length` and `Transfer-Encoding` will be determined + /// when sending this response. + set-framing-headers-mode: func(mode: framing-headers-mode) -> result<_, error>; - original-header-count: func() -> result; + /// Adjust the response's connection reuse mode. + set-http-keepalive-mode: func(mode: keepalive-mode) -> result<_, error>; - header-value-get: func( - h: request-handle, - name: list, - max-len: u64, - ) -> result>, error>; + /// Gets the destination IP address used for this response, if known. + get-remote-ip-addr: func() -> option; - header-values-get: func( - h: request-handle, - name: list, - max-len: u64, - cursor: u32 - ) -> result, option>>, error>; + /// Gets the destination port used for this response, if known. + get-remote-port: func() -> option; + } - header-values-set: func( - h: request-handle, - name: list, - /// contains multiple values separated by \0 - values: list + /// Sends a response to the client that made the request passed to `http-incoming.handle`. + /// + /// This method returns as soon as the response header begins sending to the client, and + /// transmission of the response will continue in the background. + /// + /// Data for the body must be written before calling this function. To start a response + /// and write data to it afterwards, use `send-downstream-streaming` instead. + send-downstream: func( + response: response, + body: body, ) -> result<_, error>; - header-insert: func(h: request-handle, name: list, value: list) -> result<_, error>; - - header-append: func( - h: request-handle, - name: list, - value: list, + /// Starts a response to the client that made the request passed to `http-incoming.handle`. + /// + /// The body is left open, allowing data to be written after calling this function. + send-downstream-streaming: func( + response: response, + body: borrow, ) -> result<_, error>; - header-remove: func(h: request-handle, name: list) -> result<_, error>; + /// Closes the `response`, releasing any associated resources. + /// + /// A `response` is consumed when you send a response to a client or stream one to a + /// client. You should call `close` only if you have a `response` you don't intend + /// to use anymore. + close: func(response: response) -> result<_, error>; - method-get: func(h: request-handle, max-len: u64) -> result; + type response-with-body = tuple; - method-set: func(h: request-handle, method: string) -> result<_, error>; + enum keepalive-mode { + automatic, + no-keepalive, + } +} - uri-get: func(h: request-handle, max-len: u64) -> result; +/// [Compute Dictionaries] (deprecated in favor of `config-store`) +/// +/// [Compute Dictionaries]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#dictionaries +interface dictionary { - uri-set: func(h: request-handle, uri: string) -> result<_, error>; + use types.{error}; - version-get: func(h: request-handle) -> result; + /// A Compute Dictionary. + resource dictionary { + /// Opens a dictionary, given its name. + /// + /// Names are case sensitive. + open: static func(name: string) -> result; - version-set: func(h: request-handle, version: http-version) -> result<_, error>; + /// Tries to look up a value in this dictionary. + /// + /// If the lookup is successful, this function returns `ok(s)` containing the found string + /// `s`, or `err(error.optional-none)` if no entry with the given key was found. + get: func( + key: string, + max-len: u64, + ) -> result, error>; + } +} - send: func( - h: request-handle, - b: body-handle, - backend: string, - ) -> result; +/// [Geographic data] for IP addresses. +/// +/// [Geographic data]: https://www.fastly.com/blog/improve-performance-and-gain-better-end-user-intelligence-geoip-geography-detection +interface geo { + use types.{error, ip-address}; - send-v2: func( - h: request-handle, - b: body-handle, - backend: string, - ) -> result; + /// Looks up the geographic data associated with a particular IP address. + /// + /// Returns a list of bytes containing JSON-encoded geographic data. See [here] for descriptions + /// of the JSON fields. + /// + /// [here]: https://www.fastly.com/documentation/reference/vcl/variables/geolocation/ + lookup: func(ip-addr: ip-address, max-len: u64) -> result; +} - send-v3: func( - h: request-handle, - b: body-handle, - backend: string, - ) -> result; +/// Device detection based on the User-Agent header. +interface device-detection { + use types.{error}; - send-async: func(h: request-handle, b: body-handle, backend: string) -> -result; + /// Looks up the data associated with a particular User-Agent string. + /// + /// Returns a list of bytes containing JSON-encoded device data. See [here] for descriptions + /// of the JSON fields. + /// + /// [here]: https://www.fastly.com/documentation/reference/vcl/variables/client-request/client-identified/ + lookup: func(user-agent: string, max-len: u64) -> result, error>; +} - send-async-v2: func( - req-handle: request-handle, - body-handle: body-handle, - backend: string, - streaming: bool, - ) -> result; - - send-async-streaming: func( - h: request-handle, - b: body-handle, - backend: string - ) -> result; - - pending-req-poll: func( - h: pending-request-handle, - ) -> result, error>; - - pending-req-poll-v2: func( - h: pending-request-handle, - ) -> result, error-with-detail>; - - pending-req-wait: func(h: pending-request-handle) -> result; - - pending-req-wait-v2: func( - h: pending-request-handle - ) -> result; - - pending-req-select: func( - h: list - ) -> result, error>; - - pending-req-select-v2: func( - h: list - ) -> result>, error>; - - /// Returns whether or not the original client request arrived with a - /// Fastly-Key belonging to a user with the rights to purge content on this - /// service. - fastly-key-is-valid: func() -> result; - - close: func(h: request-handle) -> result<_, error>; - - auto-decompress-response-set: func( - h: request-handle, - encodings: content-encodings, - ) -> result<_, error>; - - upgrade-websocket: func(backend: string) -> result<_, error>; - - redirect-to-websocket-proxy: func(backend: string) -> result<_, error>; - - redirect-to-websocket-proxy-v2: func( - h: request-handle, - backend: string, - ) -> result<_, error>; - - redirect-to-grip-proxy: func(backend: string) -> result<_, error>; - - redirect-to-grip-proxy-v2: func( - h: request-handle, - backend: string, - ) -> result<_, error>; - - /// Adjust how this requests's framing headers are determined. - framing-headers-mode-set: func( - h: request-handle, - mode: framing-headers-mode, - ) -> result<_, error>; - - /// Create a backend for later use - register-dynamic-backend: func( - prefix: string, - target: string, - options: backend-config-options, - config: dynamic-backend-config, - ) -> result<_, error>; - - /// Hostcall for Fastly Compute guests to inspect request HTTP traffic - /// using the NGWAF lookaside service. - inspect: func( - h: request-handle, - b: body-handle, - options: inspect-config-options, - info: inspect-config, - max-len: u64 - ) -> result, error>; - - /// Instead of having this request cache in this service's space, use the - /// cache of the named service - on-behalf-of: func( - h: request-handle, - service: string, - ) -> result<_, error>; -} - -/// Fastly HTTP Resp -interface http-resp { - use types.{error}; - - use http-types.{ - response-handle, body-handle, http-version, http-status, - framing-headers-mode - }; - - new: func() -> result; - - header-names-get: func( - h: response-handle, - max-len: u64, - cursor: u32, - ) -> result, option>>, error>; - - header-value-get: func( - h: response-handle, - name: list, - max-len: u64, - ) -> result>, error>; - - header-values-get: func( - h: response-handle, - name: list, - max-len: u64, - cursor: u32 - ) -> result, option>>, error>; - - header-values-set: func( - h: response-handle, - name: list, - /// contains multiple values separated by \0 - values: list - ) -> result<_, error>; - - header-insert: func( - h: response-handle, - name: list, - value: list, - ) -> result<_, error>; - - header-append: func( - h: response-handle, - name: list, - value: list, - ) -> result<_, error>; - - header-remove: func( - h: response-handle, - name: list, - ) -> result<_, error>; - - version-get: func(h: response-handle) -> result; - - version-set: func( - h: response-handle, - version: http-version, - ) -> result<_, error>; - - send-downstream: func( - h: response-handle, - b: body-handle, - streaming: bool, - ) -> result<_, error>; - - status-get: func(h: response-handle) -> result; - - status-set: func(h: response-handle, status: http-status) -> result<_, error>; - - close: func(h: response-handle) -> result<_, error>; - - /// Adjust how this response's framing headers are determined. - framing-headers-mode-set: func(h: response-handle, mode: framing-headers-mode) --> result<_, error>; - - enum keepalive-mode { - automatic, - no-keepalive, - } - - /// Adjust the response's connection reuse mode. - http-keepalive-mode-set: func(h: response-handle, mode: keepalive-mode) -> -result<_, error>; - - /// Hostcall for getting the destination IP used for this request. - /// - /// The buffer for the IP address must be 16 bytes. `addr_octets_out` - /// will be set to 4 for IPv4 addresses, and 16 for IPv6. - get-addr-dest-ip: func(h: response-handle) -> result, error>; - - /// Hostcall for getting the destination port used for this request. - get-addr-dest-port: func(h: response-handle) -> result; -} - -/// Fastly Dictionary -interface dictionary { - - use types.{error}; - - /// A handle to an Edge Dictionary. - type handle = u32; - - open: func(name: string) -> result; - - get: func( - h: handle, - key: string, - max-len: u64, - ) -> result, error>; -} - -/// Fastly Geo -interface geo { - use types.{error}; - - lookup: func(addr-octets: list, max-len: u64) -> result, error>; -} - -/// Fastly device detection -interface device-detection { - use types.{error}; - - lookup: func(user-agent: string, max-len: u64) -> result>, error>; -} - -/// Fastly edge-rate-limiter +/// [Edge rate limiting] API. +/// +/// [Edge rate limiting]: https://docs.fastly.com/products/edge-rate-limiting interface erl { use types.{error}; + /// Increments an entry in a rate counter and check if the client has exceeded some average number + /// of requests per second (RPS) over the window. + /// + /// If the client is over the rps limit for the window, add to the penaltybox for ttl. Valid ttl + /// span is 1m to 1h and TTL value is truncated to the nearest minute. check-rate: func( - rc: string, + rate-counter: string, entry: string, delta: u32, window: u32, limit: u32, - pb: string, + penalty-box: string, ttl: u32, ) -> result; + /// Increments an entry in the ratecounter by `delta`. ratecounter-increment: func( - rc: string, + rate-counter: string, entry: string, delta: u32, ) -> result<_, error>; + /// Looks up the current rate for entry in the ratecounter for a window. ratecounter-lookup-rate: func( - rc: string, + rate-counter: string, entry: string, window: u32, ) -> result; + /// Looks up the current count for entry in the ratecounter for duration. ratecounter-lookup-count: func( - rc: string, + rate-counter: string, entry: string, duration: u32, ) -> result; + /// Add `entry` to a the penaltybox for the duration of ttl. + /// + /// Valid ttl span is 1m to 1h and TTL value is truncated to the nearest minute. penaltybox-add: func( - pb: string, + penalty-box: string, entry: string, ttl: u32, ) -> result<_, error>; + /// Checks if `entry` is in the penaltybox. penaltybox-has: func( - pb: string, + penalty-box: string, entry: string, - ) -> result; + ) -> result; } -/// Fastly Object Store +/// Object Store (deprecated in favor of `kv-store`) interface object-store { use types.{error}; - use http-types.{body-handle}; + use http-body.{body}; - /// (DEPRECATED) A handle to an Object Store. - type handle = u32; - /// (DEPRECATED) A handle to a pending Object Store lookup. - type pending-lookup-handle = u32; - /// (DEPRECATED) A handle to a pending Object Store insert. - type pending-insert-handle = u32; - /// (DEPRECATED) A handle to a pending Object Store delete. - type pending-delete-handle = u32; + /// (DEPRECATED) An Object Store. + resource store { + open: static func(name: string) -> result, error>; - open: func(name: string) -> result, error>; + lookup: func( + key: string, + ) -> result, error>; - lookup: func( - store: handle, - key: string, - ) -> result, error>; + lookup-async: func( + key: string, + ) -> result; - lookup-async: func( - store: handle, - key: string, - ) -> result; - - pending-lookup-wait: func( - handle: pending-lookup-handle, - ) -> result, error>; - - insert: func( - store: handle, - key: string, - body-handle: body-handle, - ) -> result<_, error>; + insert: func( + key: string, + body: body, + ) -> result<_, error>; - insert-async: func( - store: handle, - key: string, - body-handle: body-handle, - ) -> result; + insert-async: func( + key: string, + body: body, + ) -> result; - pending-insert-wait: func( - handle: pending-insert-handle, + delete-async: func( + key: string, + ) -> result; + } + /// (DEPRECATED) A pending Object Store lookup. + resource pending-lookup {} + /// (DEPRECATED) A pending Object Store insert. + resource pending-insert {} + /// (DEPRECATED) A pending Object Store delete. + resource pending-delete {} + + await-pending-lookup: func( + handle: pending-lookup, + ) -> result, error>; + + await-pending-insert: func( + handle: pending-insert, ) -> result<_, error>; - delete-async: func( - store: handle, - key: string, - ) -> result; - - pending-delete-wait: func( - handle: pending-delete-handle, + await-pending-delete: func( + handle: pending-delete, ) -> result<_, error>; } -/// Fastly KV Store (adapter) +/// Interface to Fastly's [Compute KV Store]. +/// +/// For a high-level introduction to this feature, see this [blog post]. /// -/// -- find the component version in github.com:fastly/edge-storage +/// [Compute KV Store]: https://www.fastly.com/documentation/guides/concepts/edge-state/data-stores/#kv-stores +/// [blog post]: https://www.fastly.com/blog/introducing-the-compute-edge-kv-store-global-persistent-storage-for-compute-functions interface kv-store { use types.{error}; - use http-types.{body-handle}; - - /// A handle to an KV Store. - type handle = u32; - /// A handle to a KV Store lookup. - type lookup-handle = u32; - /// A handle to a KV Store insert. - type insert-handle = u32; - /// A handle to a KV Store delete. - type delete-handle = u32; - /// A handle to a KV Store list. - type list-handle = u32; - - enum kv-status { - /// There was no error. - ok, - /// KV store cannot or will not process the request due to something that is perceived to be a client error - /// This will map to the api's 400 codes + use http-body.{body}; + + /// A KV Store. + resource store { + /// Opens the KV Store with the given name. + /// + /// If there is no store by that name, this returns `ok(none)`. + open: static func(name: string) -> result, error>; + + /// Looks up a value in the KV Store. + /// + /// Returns `ok(some(v))` with the value `v` that was found, `ok(none)` if no value was + /// found, or `err(e)` indicating the error `e` occurred. + /// + /// This function waits until the operation completes. + lookup: func( + key: string, + ) -> result, kv-error>; + + /// Look up a value in the KV Store asynchronously. + /// + /// This function initiates an async lookup of a value in the KV Store. Use + /// `await-lookup` to finish the lookup. + lookup-async: func( + key: string, + ) -> result; + + /// Inserts a value into the KV Store. + /// + /// If the KV Store already contains a value for this key, the `mode` field + /// of the `options` argument specifies how the existing value is handled. + /// + /// This function waits until the operation completes. + insert: func( + key: string, + body: body, + options: insert-options, + ) -> result<_, kv-error>; + + /// Insert a value into the KV Store asynchronously. + /// + /// If the KV Store already contains a value for this key, the `mode` field + /// of the `options` argument specifies how the existing value is handled. + /// + /// This function initiates an async insert of a value in the KV Store. Use + /// `await-insert` to finish the lookup. + insert-async: func( + key: string, + body: body, + options: insert-options, + ) -> result; + + /// Deletes a value in the KV Store. + /// + /// Returns `ok(true)` if a value was successfully deleted, `ok(false)` if no value was + /// found, or `err(e)` indicating the error `e` occurred. + /// + /// This function waits until the operation completes. + delete: func( + key: string, + ) -> result; + + /// Delete of a value in the KV Store. + /// + /// This function initiates an async delete of a value in the KV Store. Use + /// `await-delete` to finish the lookup. + delete-async: func( + key: string, + ) -> result; + + /// Lists keys in the KV Store. + /// + /// Returns `ok(b)` with the body `b` on success, or `err(e)` indicating the error `e` + /// occurred. + /// + /// This function waits until the operation completes. + %list: func( + options: list-options, + ) -> result; + + /// List of keys in the KV Store. + /// + /// This function initiates an async list value in the KV Store. Use + /// `await-list` to finish the lookup. + list-async: func( + options: list-options, + ) -> result; + } + + /// An asynchronous KV Store lookup. Use `await-lookup` to resolve. + use async-io.{pollable as pending-lookup}; + + /// An asynchronous KV Store insert. Use `await-insert` to resolve. + use async-io.{pollable as pending-insert}; + + /// An asynchronous KV Store delete. Use `await-delete` to resolve. + use async-io.{pollable as pending-delete}; + + /// An asynchronous KV Store list. Use `await-list` to resolve. + use async-io.{pollable as pending-list}; + + /// A value indicating the status of a KV store operation. + enum kv-error { + /// KV store cannot or will not process the request due to something that is perceived to be a + /// client error. + /// + /// This will map to the api's 400 codes. bad-request, - /// KV store cannot find the requested resource - /// This will map to the api's 404 codes - not-found, - /// KV store cannot fulfill the request, as definied by the client's prerequisites (ie. if-generation-match) - /// This will map to the api's 412 codes + /// KV store cannot fulfill the request, as defined by the client's prerequisites, for example + /// `if-generation-match`. + /// + /// This will map to the api's 412 codes. precondition-failed, /// The size limit for a KV store key was exceeded. - /// This will map to the api's 413 codes + /// + /// This will map to the api's 413 codes. payload-too-large, /// The system encountered an unexpected internal error. - /// This will map to all remaining http error codes + /// + /// This will map to all remaining http error codes. internal-error, /// Too many requests have been made to the KV store. - /// This will map to the api's 429 codes + /// + /// This will map to the api's 429 codes. too-many-requests, + /// Generic error value. + /// + /// This means that some unexpected error occurred. + generic-error, } - open: func(name: string) -> result, error>; + /// Wait on the async lookup of a value in the KV Store. + /// + /// Returns `ok(some(v))` with the value `v` that was found, `ok(none)` if no value was + /// found, or `err(e)` indicating the error `e` occurred. + await-lookup: func( + handle: pending-lookup, + ) -> result, kv-error>; - lookup: func( - store: handle, - key: list, - ) -> result; + /// Wait on the async insert of a value in the KV Store. + /// + /// Returns `ok` if the `insert` succeeded, or an error code on failure. + await-insert: func( + handle: pending-insert, + ) -> result<_, kv-error>; + + /// Wait on the async delete of a value in the KV Store. + /// + /// Returns `ok(true)` if a value was successfully deleted, `ok(false)` if no value was + /// found, or `err(e)` indicating the error `e` occurred. + await-delete: func( + handle: pending-delete, + ) -> result; + + /// Wait on the async list of keys in the KV Store. + /// + /// Returns `ok(b)` with the body `b` on success, or `err(e)` indicating the error `e` + /// occurred. + await-list: func( + handle: pending-list, + ) -> result; + + /// A response from a KV Store Lookup operation. + /// + /// This type holds the `body`, metadata, and generation of found key. + resource entry { + /// Take and return the body from this `entry`, if it has one; otherwise return `none`. + /// + /// After calling this method, this entry will no longer have a body. + take-body: func() -> option; - resource lookup-result { - body: func() -> body-handle; + /// Read the metadata of the KV Store item. metadata: func(max-len: u64) -> result, error>; + + /// Read the current generation of the KV Store item. generation: func() -> u64; } - lookup-wait: func( - handle: lookup-handle, - ) -> result, kv-status>, error>; - + /// Selects the behavior for an insert when the new key matches an existing key. + /// + /// A KV store maintains the property that its keys are unique from each other. If an insert + /// has a key that doesn't match any key already in the store, then the pair of the key and the + /// new value is inserted into the store. However, if the insert's key does match a key already + /// in the store, then no new key-value pair is inserted, and the insert's `insert-mode.mode` + /// determines what it does instead. enum insert-mode { + /// Updates the existing key's value by overwriting it with the new value. + /// + /// This is the default mode. overwrite, + + /// Fails, leaving the existing key's value unmodified. + /// + /// With this mode, the insert fails with a code of `kv-error.precondition-failed`, and + /// does not modify the existing value. Inserts with this mode will only “add” new key-value + /// pairs; they are prevented from modifying any existing ones. add, + + /// Updates the existing key's value by appending the new value to it. append, + + /// Updates the existing key's value by prepending the new value to it. prepend, } - flags insert-config-options { - reserved, - background-fetch, - if-generation-match, - metadata, - time-to-live-sec, - } + /// Options for configuring the behavior of the `insert` function. + record insert-options { + /// If set, allows fetching from the origin to occur in the background, enabling a faster + /// response with stale content. The cache will be updated with fresh content after the request + /// is completed. + background-fetch: bool, - record insert-config { - mode: insert-mode, - if-generation-match: u64, - metadata: string, - time-to-live-sec: u32, - } + /// Requests for keys will return a “generation” header specific to the version of a key. The + /// generation header is a unique, non-serial 64-bit unsigned integer that can be used for + /// testing against a specific KV store value. + if-generation-match: option, - insert: func( - store: handle, - key: list, - body-handle: body-handle, - mask: insert-config-options, - config: insert-config, - ) -> result; + /// Sets an arbitrary data field which can contain up to 2000B of data. + metadata: option, - insert-wait: func( - handle: insert-handle, - ) -> result; + /// Sets a time for the key to expire. Deletion will take place up to 24 hours after the ttl + /// reaches 0. + time-to-live-sec: option, - delete: func( - store: handle, - key: list, - ) -> result; + /// Select the behavior in the case when the new key matches an existing key. + mode: insert-mode, - delete-wait: func( - handle: delete-handle, - ) -> result; + /// Additional options may be added in the future via this resource type. + extra: option>, + } + /// Extensibility for `insert-options` + resource extra-insert-options {} + + /// Modes of KV Store list operations. + /// + /// This type serves to facilitate alternative methods of cache interactions with list operations. enum list-mode { + /// Performs an un-cached list on every invocation. + /// + /// This is the default method of listing. strong, - eventual, - } - flags list-config-options { - reserved, - cursor, - limit, - prefix, + /// Returns a cached list response to improve performance. + /// + /// The data may be slightly out of sync with the store, but repeated calls are faster. + /// + /// The word “eventual” here refers to eventual consistency. + eventual, } - record list-config { + record list-options { mode: list-mode, - cursor: string, - limit: u32, - prefix: string, - } + cursor: option, + limit: option, + prefix: option, - %list: func( - store: handle, - mask: list-config-options, - options: list-config, - ) -> result; + /// Additional options may be added in the future via this resource type. + extra: option>, + } - list-wait: func( - handle: list-handle, - ) -> result, kv-status>, error>; + /// Extensibility for `list-options` + resource extra-list-options {} } -/// Fastly Secret Store +/// [Secret Store] API. +/// +/// [Secret Store]: https://www.fastly.com/documentation/reference/api/services/resources/secret-store/ interface secret-store { - use types.{error, secret-handle}; - - /// A handle to a Secret Store. - type store-handle = u32; + use types.{error}; - open: func(name: string) -> result; + /// An individual secret. + resource secret { + /// Creates a new “secret” from the given memory. + /// + /// This is *not* the suggested way to create `secret`s; instead, we suggest using `get`. + /// This secret will *NOT* be shared with other sessions. + /// + /// This method can be used for data that should be secret, but is being obtained by + /// some other means than the secret store. New “secrets” created this way use plaintext + /// only, and live in the session's memory unencrypted for much longer than secrets + /// generated by `get`. They should thus only be used in situations in which an API requires + /// a `secret`, but you cannot (for whatever reason) use a `store` to store them. + /// + /// As the early note says, this `secret` will be local to the current session, and + /// will not be shared with other sessions of this service. + from-bytes: static func(bytes: list) -> result; + + /// Returns the plaintext value of this secret. + plaintext: func( + max-len: u64 + ) -> result>, error>; + } - get: func( - store: store-handle, - key: string, - ) -> result, error>; + /// A Secret Store. + resource store { + /// Opens the Secret Store with the given name. + open: static func(name: string) -> result; - plaintext: func( - secret: secret-handle, - max-len: u64 - ) -> result>, error>; - - from-bytes: func(bytes: list) -> result; + /// Tries to look up a Secret by name in this secret store. + /// + /// If successful, this method returns `ok(some(s))` containing the found secret `s` if the + /// secret is found, or `ok(none)` if the secret was not found. + get: func( + key: string, + ) -> result, error>; + } } -/// Fastly ACL +/// Blocklists using [Access Control Lists] (ACLs) +/// +/// [Access Control Lists]: https://www.fastly.com/documentation/reference/api/acls/ interface acl { - use types.{error}; - use http-types.{body-handle}; + use types.{error, ip-address}; + use http-body.{body}; - /// A handle to an ACL. - type acl-handle = u32; + /// An ACL. + resource acl { + /// Opens an ACL linked to the current service with the given link name. + open: static func(name: string) -> result; + /// Performs a lookup of the given IP address in the ACL. + /// + /// If no matches are found, then `ok(none)` is returned. + lookup: func( + ip-addr: ip-address, + ) -> result, acl-error>, error>; + } + + /// Errors returned on ACL lookup failure. enum acl-error { /// The $acl_error has not been initialized. uninitialized, @@ -1013,17 +1559,27 @@ interface acl { /// Too many requests have been made. too-many-requests, } - - open: func(name: string) -> result; - - lookup: func( - acl: acl-handle, - ip-octets: list, - ip-len: u64, - ) -> result, acl-error>, error>; } -/// Fastly backend +/// [Backends] API. +/// +/// A backend represents a service that the application can send requests to, potentially +/// caching the responses received. +/// +/// Backends come in one of two flavors: +/// * **Static Backends**: These backends are created using the Fastly UI or API, +/// and are predefined by the user. Static backends typically have short names that are +/// usable across every session of a service. +/// * **Dynamic Backends**: These backends are created programmatically using the +/// `register-dynamic-backend` API. They are defined at runtime, and may or may not +/// be shared across sessions depending on how they are configured. +/// +/// To use a backend, pass it to a `send*` function. +/// +/// Future versions of this function may return an error if your service does not have a backend +/// with this name. +/// +/// [Backends]: https://www.fastly.com/documentation/guides/integrations/non-fastly-services/developer-guide-backends/ interface backend { use types.{error}; use http-types.{tls-version}; @@ -1032,6 +1588,7 @@ interface backend { type timeout-secs = u32; type probe-count = u32; + /// Returns `true` if a backend with this name exists. exists: func(backend: string) -> result; enum backend-health { @@ -1040,217 +1597,468 @@ interface backend { unhealthy, } + /// Return the health of the backend if configured and currently known. + /// + /// For backends without a configured healthcheck, this will always return + /// `backend-health.unknown`. is-healthy: func(backend: string) -> result; - /// Returns `true` if the backend is a "dynamic" backend. + /// Returns `true` if the backend is a “dynamic” backend. is-dynamic: func(backend: string) -> result; - /// Get the host of this backend. + /// Gets the host of this backend. get-host: func(backend: string, max-len: u64) -> result; - /// Get the "override host" for this backend. + /// Gets the “override host” for this backend. + /// + /// This is used to change the `Host` header sent to the backend. See + /// [the Fastly documentation on override hosts]. /// - /// This is used to change the `Host` header sent to the backend. See the - /// Fastly documentation oh this topic here: https://docs.fastly.com/en/guides/specifying-an-override-host + /// [the Fastly documentation on override hosts]: https://docs.fastly.com/en/guides/specifying-an-override-host> get-override-host: func( backend: string, max-len: u64, ) -> result>, error>; - /// Get the remote TCP port of the backend connection for the request. + /// Gets the remote TCP port of the backend connection for the request. get-port: func(backend: string) -> result; - /// Get the connection timeout of the backend. - get-connect-timeout-ms: func(backend: string) -> result; + /// Gets the connection timeout of the backend. + get-connect-timeout-ms: func(backend: string) -> result; - /// Get the first byte timeout of the backend. - get-first-byte-timeout-ms: func(backend: string) -> result; + /// Gets the first byte timeout of the backend. + /// + /// This timeout applies between the time of connection and the time we get the first byte back. + get-first-byte-timeout-ms: func(backend: string) -> result; - /// Get the between byte timeout of the backend. - get-between-bytes-timeout-ms: func(backend: string) -> result; + /// Gets the between byte timeout of the backend. + /// + /// This timeout applies between any two bytes we receive across the wire. + get-between-bytes-timeout-ms: func(backend: string) -> result; /// Returns `true` if the backend is configured to use TLS. is-tls: func(backend: string) -> result; - /// Get the minimum TLS version this backend will use. + /// Gets the minimum TLS version this backend will use. get-tls-min-version: func(backend: string) -> result, error>; - /// Get the maximum SSL version this backend will use. + /// Gets the maximum TLS version this backend will use. get-tls-max-version: func(backend: string) -> result, error>; + /// Returns the time for this backend to hold onto an idle HTTP keepalive connection + /// after it was last used before closing it. get-http-keepalive-time: func( backend: string, ) -> result; + /// Returns `true` if TCP keepalives have been enabled for this backend. get-tcp-keepalive-enable: func( backend: string, ) -> result; + /// Returns the time to wait in between sending each TCP keepalive probe to this backend. get-tcp-keepalive-interval: func( backend: string, ) -> result; + /// Returns the time to wait after the last data was sent before starting to send TCP keepalive + /// probes to this backend. get-tcp-keepalive-probes: func( backend: string, ) -> result; + /// Returns the time to wait after the last data was sent before starting to send TCP keepalive + /// probes to this backend. get-tcp-keepalive-time: func( backend: string, ) -> result; } -/// Fastly Async IO +/// Async IO support. +/// +/// This module provides several utilities for performing I/O asynchronously. +/// See the documentation for `async-io.pollable` for a description of the kinds +/// of events it supports. +/// +/// In the future, this interface is expected to be replaced by +/// [integrated async features]. +/// +/// [integrated async features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md#-async-explainer interface async-io { - use types.{error}; - - /// A handle to an object supporting generic async operations. - /// Can be either a `BodyHandle` or a `PendingRequestHandle`. + /// An object supporting generic async operations. + /// + /// Can be a `http-body.body`, `http-req.pending-request`, `http-req.request-promise`, + /// `cache.pending-entry`. `kv-store.pending-lookup`, `kv-store.pending-insert`, + /// `kv-store.pending-delete`, or `kv-store.pending-list`. /// /// Each async item has an associated I/O action: /// - /// * Pending requests: awaiting the response headers / `Response` object + /// * Pending requests: awaiting the response headers / `response` object /// * Normal bodies: reading bytes from the body /// * Streaming bodies: writing bytes to the body /// - /// For writing bytes, note that there is a large host-side buffer that bytes can eagerly be written - /// into, even before the origin itself consumes that data. - type handle = u32; + /// For writing bytes, there is a large buffer associated with the handle that bytes + /// can eagerly be written into, even before the origin itself consumes that data. + resource pollable { + /// Make a nonblocking attempt to complete the I/O operation. + /// + /// Returns `true` if the given async item is “ready” for its associated I/O action, `false` + /// otherwise. + /// + /// If an object is ready, the I/O action is guaranteed to complete without blocking. + /// + /// Valid object handles includes bodies and pending requests. See the `async-io.pollable` + /// definition for more details, including what I/O actions are associated with each handle + /// type. + is-ready: func() -> bool; - /// Blocks until one of the given objects is ready for I/O, or the optional timeout expires. + /// Create a new trivial `pollable` which reports being immediately ready. + new-ready: static func() -> pollable; + } + + /// Blocks until one of the given objects is ready for I/O. /// - /// Valid object handles includes bodies and pending requests. See the `async_item_handle` + /// If an object is ready, the I/O action is guaranteed to complete without blocking. + /// + /// Valid object handles includes bodies and pending requests. See the `async-io.pollable` /// definition for more details, including what I/O actions are associated with each handle /// type. /// - /// The timeout is specified in milliseconds, or 0 if no timeout is desired. + /// Returns the *index* (not handle!) of the first object that is ready. /// - /// Returns the _index_ (not handle!) of the first object that is ready, or - /// none if the timeout expires before any objects are ready for I/O. - select: func(hs: list, timeout-ms: u32) -> result, error>; + /// Traps if the list is empty. + select: func(handles: list>) -> u32; - /// Returns 1 if the given async item is "ready" for its associated I/O action, 0 otherwise. + /// Blocks until one of the given objects is ready for I/O, or the timeout expires. /// /// If an object is ready, the I/O action is guaranteed to complete without blocking. /// - /// Valid object handles includes bodies and pending requests. See the `async_item_handle` + /// Valid object handles includes bodies and pending requests. See the `async-io.pollable` /// definition for more details, including what I/O actions are associated with each handle /// type. - is-ready: func(handle: handle) -> result; + /// + /// The timeout is specified in milliseconds. + /// + /// Returns the *index* (not handle!) of the first object that is ready, or `none` if the + /// timeout expires before any objects are ready for I/O. + select-with-timeout: func(handles: list>, timeout-ms: u32) -> option; } -/// Fastly Purge -/// -/// See the [Fastly purge documentation] for details. +/// [Cache Purging] API. /// -/// [Fastly purge documentation]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/ +/// [Cache Purging]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/ interface purge { - use types.{error}; + use types.{error}; + + record purge-options { + /// Perform a [soft purge] instead of a hard purge. + /// + /// [soft purge]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/#soft-vs-hard-purging + soft-purge: bool, + + /// Additional options may be added in the future via this resource type. + extra: option>, + } + + /// Extensibility for `purge-options` + resource extra-purge-options {} + + /// Purge a surrogate key for the current service. + /// + /// A surrogate key can be a max of 1024 characters. + /// A surrogate key must contain only printable ASCII characters (those between `0x21` and `0x7E`, + /// inclusive). + /// + /// Never returns `error.optional-none`. + purge-surrogate-key: func( + surrogate-keys: string, + purge-options: purge-options, + ) -> result<_, error>; + + /// Purge a surrogate key for the current service, and return the purge id. + /// + /// This is similar to `purge-surrogate-key`, but on success, returns a + /// [JSON purge response] containing an ASCII alphanumeric string identifying + /// a purging. + /// + /// Never returns `error.optional-none`. + /// + /// [JSON purge response]: https://developer.fastly.com/reference/api/purging/#purge-tag + purge-surrogate-key-verbose: func( + surrogate-keys: string, + purge-options: purge-options, + max-len: u64, + ) -> result; +} + +/// [Core Cache] API +/// +/// [Core Cache]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/#core-cache +interface cache { + + use types.{error}; + use http-body.{body}; + use http-req.{request}; + + /// The outcome of a cache lookup (either bare or as part of a cache transaction) + resource entry { + /// Performs a non-request-collapsing cache lookup. + /// + /// Returns a result without waiting for any request collapsing that may be ongoing. + lookup: static func( + key: list, + options: lookup-options, + ) -> result; + + /// The entrypoint to the request-collapsing cache transaction API. + /// + /// This operation always participates in request collapsing and may return stale objects. To + /// bypass request collapsing, use `lookup` and `insert` instead. + transaction-lookup: static func( + key: list, + options: lookup-options, + ) -> result; + + /// The entrypoint to the request-collapsing cache transaction API, returning instead of waiting + /// on busy. + /// + /// This operation always participates in request collapsing and may return stale objects. To + /// bypass request collapsing, use `lookup` and `insert` instead. + transaction-lookup-async: static func( + key: list, + options: lookup-options, + ) -> result; + + /// Insert an object into the cache with the given metadata. + /// + /// Can only be used in if the cache handle state includes the `must-insert-or-update` flag. + /// + /// The returned handle is to a streaming body that is used for writing the object into + /// the cache. + transaction-insert: func( + options: write-options, + ) -> result; + + /// Insert an object into the cache with the given metadata, and return a readable stream of the + /// bytes as they are stored. + /// + /// This helps avoid the “slow reader” problem on a teed stream, for example when a program + /// wishes to store a backend request in the cache while simultaneously streaming to a client + /// in an HTTP response. + /// + /// The returned body handle is to a streaming body that is used for writing the object *into* + /// the cache. The returned cache handle provides a separate transaction for reading out the + /// newly cached object to send elsewhere. + transaction-insert-and-stream-back: func( + options: write-options, + ) -> result, error>; + + /// Update the metadata of an object in the cache without changing its data. + /// + /// Can only be used in if the cache handle state includes both of the flags: + /// - `found` + /// - `must-insert-or-update` + transaction-update: func( + options: write-options, + ) -> result<_, error>; + + get-state: func() -> result; + + /// Gets the user metadata of the found object, returning `none` if no object + /// was found. + get-user-metadata: func(max-len: u64) -> result>, error>; + + /// Gets a range of the found object body, returning the `optional-none` error if there + /// was no found object. + /// + /// The returned `body` must be closed before calling this function again on the same + /// `entry`. + /// + /// Note: until the CacheD protocol is adjusted to fully support this functionality, + /// the body of objects that are past the stale-while-revalidate period will not + /// be available, even when other metadata is. + get-body: func( + options: get-body-options, + ) -> result; + + /// Gets the content length of the found object, returning the `error.optional-none` error if + /// there was no found object, or no content length was provided. + get-length: func() -> result; + + /// Gets the configured max age of the found object, returning the `error.optional-none` error + /// if there was no found object. + get-max-age-ns: func() -> result; + + /// Gets the configured stale-while-revalidate period of the found object, returning the + /// `error.optional-none` error if there was no found object. + get-stale-while-revalidate-ns: func() -> result; + + /// Gets the age of the found object, returning the `error.optional-none` error if there + /// was no found object. + get-age-ns: func() -> result; + + /// Gets the number of cache hits for the found object, returning the `error.optional-none` + /// error if there was no found object. + get-hits: func() -> result; + + /// Cancel an obligation to provide an object to the cache. + /// + /// Useful if there is an error before streaming is possible, for example if a backend is + /// unreachable. + transaction-cancel: func() -> result<_, error>; + } + /// Handle that can be used to check whether or not a cache lookup is waiting on another client. + use async-io.{pollable as pending-entry}; + + /// A replace operation. + type replace-entry = entry; + + /// The entrypoint to the replace API. + /// + /// This operation always participates in request collapsing and may return stale objects. + replace: func( + key: list, + options: replace-options, + ) -> result; + + /// Replace an object in the cache with the given metadata + /// + /// The returned handle is to a streaming body that is used for writing the object into + /// the cache. + replace-insert: func( + handle: borrow, + options: write-options, + ) -> result; + + /// Gets the age of the existing object during replace, returning + /// `none` if there was no object. + replace-get-age-ns: func( + handle: borrow, + ) -> result, error>; + + /// Gets a range of the existing object body, returning `none` if there + /// was no existing object. + /// + /// The returned `body` must be closed before calling this function + /// again on the same `replace-entry`. + replace-get-body: func( + handle: borrow, + options: get-body-options, + ) -> result, error>; + + /// Gets the number of cache hits for the existing object during replace, + /// returning `none` if there was no object. + replace-get-hits: func( + handle: borrow, + ) -> result, error>; - flags purge-options-mask { - soft-purge, - /// all ret_buf fields must be populated - ret-buf - } + /// Gets the content length of the existing object during replace, + /// returning `none` if there was no object, or no content + /// length was provided. + replace-get-length: func( + handle: borrow, + ) -> result, error>; - /// Purge a surrogate key for the current service. - /// - /// A surrogate key can be a max of 1024 characters. - /// A surrogate key must contain only printable ASCII characters (those between `0x21` and `0x7E`, inclusive). - /// - /// Returns a JSON purge response as in https://developer.fastly.com/reference/api/purging/#purge-tag - purge-surrogate-key: func( - surrogate-keys: string, - purge-options: purge-options-mask, - max-len: u64, - ) -> result, error>; -} + /// Gets the configured max age of the existing object during replace, + /// returning the `error.optional-none` error if there was no object. + replace-get-max-age-ns: func( + handle: borrow, + ) -> result, error>; -/// Fastly Cache -interface cache { + /// Gets the configured stale-while-revalidate period of the existing + /// object during replace, returning the `error.optional-none` error if there was no + /// object. + replace-get-stale-while-revalidate-ns: func( + handle: borrow, + ) -> result, error>; - use types.{error}; - use http-types.{body-handle, request-handle}; + /// Gets the lookup state of the existing object during replace, returning + /// the `error.optional-none` error if there was no object. + replace-get-state: func( + handle: borrow, + ) -> result, error>; + + /// Gets the user metadata of the existing object during replace, returning + /// the `error.optional-none` error if there was no object. + replace-get-user-metadata: func( + handle: borrow, + max-len: u64, + ) -> result>, error>; - /// The outcome of a cache lookup (either bare or as part of a cache transaction) - type handle = u32; - /// Handle that can be used to check whether or not a cache lookup is waiting on another client. - type busy-handle = u32; - type cache-replace-handle = u32; type object-length = u64; type duration-ns = u64; type cache-hit-count = u64; - flags lookup-options-mask { - reserved, - request-headers, - service-id, - always-use-requested-range, - } - - /// Extensible options for cache lookup operations; currently used for both `lookup` and `transaction_lookup`. + /// Options for cache lookup operations; currently used for both `lookup` and + /// `transaction-lookup`. record lookup-options { /// A full request handle, but used only for its headers - request-headers: request-handle, + /// + /// May be `none` if the `request-headers` option isn't enabled. + /// + request-headers: option>, - service-id: string, - } + service-id: option, - /// Options mask for `http_cache_write_options`. - flags write-options-mask { - reserved, - /// Only allowed for non-transactional `insert` - request-headers, - vary-rule, - initial-age-ns, - stale-while-revalidate-ns, - surrogate-keys, - length, - user-metadata, - sensitive-data, - edge-max-age-ns, - service-id, + always-use-requested-range: bool, + + /// Additional options may be added in the future via this resource type. + extra: option>, } - /// Configuration for several hostcalls that write to the cache: + /// Extensibility for `lookup-options` + resource extra-lookup-options {} + + /// Configuration for several functions that write to the cache: /// - `insert` /// - `transaction-insert` /// - `transaction-insert-and-stream-back` /// - `transaction-update` /// - /// Some options are only allowed for certain of these hostcalls; see `cache-write-options-mask`. + /// Some options are only allowed for certain of these hostcalls; see the comments + /// on the fields. record write-options { - /// this is a required field; there's no flag for it + /// this is a required field max-age-ns: duration-ns, /// a full request handle, but used only for its headers - request-headers: request-handle, + /// + /// Only allowed for non-transactional `insert` + request-headers: option>, /// a list of header names separated by spaces - vary-rule: string, + vary-rule: option, /// The initial age of the object in nanoseconds (default: 0). /// /// This age is used to determine the freshness lifetime of the object as well as to /// prioritize which variant to return if a subsequent lookup matches more than one vary rule - initial-age-ns: duration-ns, - stale-while-revalidate-ns: duration-ns, + initial-age-ns: option, + stale-while-revalidate-ns: option, /// a list of surrogate keys separated by spaces - surrogate-keys: string, - length: object-length, - user-metadata: list, - edge-max-age-ns: duration-ns, - service-id: string, + surrogate-keys: option, + length: option, + user-metadata: option>, + edge-max-age-ns: option, + service-id: option, + sensitive-data: bool, + + /// Additional options may be added in the future via this resource type. + extra: option>, } - flags get-body-options-mask { - reserved, - %from, - to, - } + /// Extensibility for `write-options` + resource extra-write-options {} record get-body-options { - %from: u64, - to: u64, + %from: option, + to: option, + + /// Additional options may be added in the future via this resource type. + extra: option>, } + /// Extensibility for `get-body-options` + resource extra-get-body-options {} + /// The status of this lookup (and potential transaction) flags lookup-state { /// a cached object was found @@ -1263,327 +2071,335 @@ interface cache { must-insert-or-update, } - /// Performs a non-request-collapsing cache lookup. - /// - /// Returns a result without waiting for any request collapsing that may be ongoing. - lookup: func( - key: list, - mask: lookup-options-mask, - options: lookup-options, - ) -> result; - /// Performs a non-request-collapsing cache insertion (or update). /// /// The returned handle is to a streaming body that is used for writing the object into /// the cache. insert: func( key: list, - options-mask: write-options-mask, options: write-options, - ) -> result; - - /// The entrypoint to the request-collapsing cache transaction API. - /// - /// This operation always participates in request collapsing and may return stale objects. To bypass - /// request collapsing, use `lookup` and `insert` instead. - transaction-lookup: func( - key: list, - mask: lookup-options-mask, - options: lookup-options, - ) -> result; - - /// The entrypoint to the request-collapsing cache transaction API, returning instead of waiting on busy. - /// - /// This operation always participates in request collapsing and may return stale objects. To bypass - /// request collapsing, use `lookup` and `insert` instead. - transaction-lookup-async: func( - key: list, - mask: lookup-options-mask, - options: lookup-options, - ) -> result; + ) -> result; /// Continues the lookup transaction from which the given busy handle was returned, /// waiting for the leader transaction if request collapsed, and returns a cache handle. - cache-busy-handle-wait: func( - handle: busy-handle, - ) -> result; - - /// Insert an object into the cache with the given metadata. - /// - /// Can only be used in if the cache handle state includes the `must-insert-or-update` flag. - /// - /// The returned handle is to a streaming body that is used for writing the object into - /// the cache. - transaction-insert: func( - handle: handle, - mask: write-options-mask, - options: write-options, - ) -> result; - - /// Insert an object into the cache with the given metadata, and return a readable stream of the - /// bytes as they are stored. - /// - /// This helps avoid the "slow reader" problem on a teed stream, for example when a program wishes - /// to store a backend request in the cache while simultaneously streaming to a client in an HTTP - /// response. - /// - /// The returned body handle is to a streaming body that is used for writing the object _into_ - /// the cache. The returned cache handle provides a separate transaction for reading out the - /// newly cached object to send elsewhere. - transaction-insert-and-stream-back: func( - handle: handle, - mask: write-options-mask, - options: write-options, - ) -> result, error>; - - /// Update the metadata of an object in the cache without changing its data. - /// - /// Can only be used in if the cache handle state includes both of the flags: - /// - `found` - /// - `must-insert-or-update` - transaction-update: func( - handle: handle, - mask: write-options-mask, - options: write-options, - ) -> result<_, error>; - - /// Cancel an obligation to provide an object to the cache. - /// - /// Useful if there is an error before streaming is possible, e.g. if a backend is unreachable. - transaction-cancel: func(handle: handle) -> result<_, error>; + await-entry: func( + handle: pending-entry, + ) -> result; - /// Close an interaction with the cache that has not yet finished request collapsing. - close-busy: func(handle: busy-handle) -> result<_, error>; + /// Closes an interaction with the cache that has not yet finished request collapsing. + close-pending-entry: func(handle: pending-entry) -> result<_, error>; - /// Close an ongoing interaction with the cache. + /// Closes an ongoing interaction with the cache. /// /// If the cache handle state includes the `must-insert-or-update` (and hence no insert or /// update has been performed), closing the handle cancels any request collapsing, potentially /// choosing a new waiter to perform the insertion/update. - close: func(handle: handle) -> result<_, error>; - - get-state: func(handle: handle) -> result; - - /// Gets the user metadata of the found object, returning None if no object - /// was found. - get-user-metadata: func(handle: handle, max-len: u64) -> result>, error>; - - /// Gets a range of the found object body, returning the `optional-none` error if there - /// was no found object. - /// - /// The returned `body_handle` must be closed before calling this function again on the same - /// `cache_handle`. /// - /// Note: until the CacheD protocol is adjusted to fully support this functionality, - /// the body of objects that are past the stale-while-revalidate period will not - /// be available, even when other metadata is. - get-body: func( - handle: handle, - mask: get-body-options-mask, - options: get-body-options, - ) -> result; - - /// Gets the content length of the found object, returning the `$none` error if there - /// was no found object, or no content length was provided. - get-length: func(handle: handle) -> result; - - /// Gets the configured max age of the found object, returning the `$none` error if there - /// was no found object. - get-max-age-ns: func(handle: handle) -> result; - - /// Gets the configured stale-while-revalidate period of the found object, returning the - /// `$none` error if there was no found object. - get-stale-while-revalidate-ns: func(handle: handle) -> result; - - /// Gets the age of the found object, returning the `$none` error if there - /// was no found object. - get-age-ns: func(handle: handle) -> result; - - /// Gets the number of cache hits for the found object, returning the `$none` error if there - /// was no found object. - get-hits: func(handle: handle) -> result; - - flags replace-options-mask { - reserved, - request-headers, - replace-strategy, - service-id, - always-use-requested-range, - } + /// This may be passed either an `entry` or a `replace-entry`. + close: func(handle: entry) -> result<_, error>; - /// Extensible options for cache replace operations + /// Options for cache replace operations record replace-options { /// a full request handle, but used only for its headers - request-headers: request-handle, - replace-strategy: replace-strategy, - service-id: string, + request-headers: option>, + replace-strategy: option, + service-id: option, + always-use-requested-range: bool, + + /// Additional options may be added in the future via this resource type. + extra: option>, } + /// Extensibility for `replace-options` + resource extra-replace-options {} + enum replace-strategy { + /// Immediately start the replace and do not wait for any other pending requests for the same + /// object, including insert requests. + /// + /// With this strategy a replace will race all other pending requests to update the object. + /// + /// The existing object will be accessible until this replace finishes providing the replacement + /// object. + /// + /// This is the default replace strategy. immediate, + + /// Immediate, but remove the existing object immediately + /// + /// Requests for the same object that arrive after this replace starts will wait until this + /// replace starts providing the replacement object. immediate-force-miss, + + /// Join the wait list behind other pending requests before starting this request. + /// + /// With this strategy this replace request will wait for an in-progress replace or insert + /// request before starting. + /// + /// This strategy allows implementing a counter, but may cause timeouts if too many requests + /// are waiting for in-progress and waiting updates to complete. wait, } - - /// The entrypoint to the replace API. - /// - /// This operation always participates in request collapsing and may return stale objects. - replace: func( - key: list, - mask: replace-options-mask, - options: replace-options, - ) -> result; - - /// Replace an object in the cache with the given metadata - /// - /// The returned handle is to a streaming body that is used for writing the object into - /// the cache. - replace-insert: func( - h: cache-replace-handle, - mask: write-options-mask, - options: write-options, - ) -> result; - - /// Gets the age of the existing object during replace, returning - /// `None` if there was no object. - replace-get-age-ns: func( - h: cache-replace-handle, - ) -> result, error>; - - /// Gets a range of the existing object body, returning `None` if there - /// was no existing object. - /// - /// The returned `body_handle` must be closed before calling this function - /// again on the same `cache_replace_handle`. - replace-get-body: func( - h: cache-replace-handle, - mask: get-body-options-mask, - options: get-body-options, - ) -> result, error>; - - /// Gets the number of cache hits for the existing object during replace, - /// returning `None` if there was no object. - replace-get-hits: func( - h: cache-replace-handle, - ) -> result, error>; - - /// Gets the content length of the existing object during replace, - /// returning `None` if there was no object, or no content - /// length was provided. - replace-get-length: func( - h: cache-replace-handle, - ) -> result, error>; - - /// Gets the configured max age of the existing object during replace, - /// returning the `$none` error if there was no object. - replace-get-max-age-ns: func( - h: cache-replace-handle, - ) -> result, error>; - - /// Gets the configured stale-while-revalidate period of the existing - /// object during replace, returning the `$none` error if there was no - /// object. - replace-get-stale-while-revalidate-ns: func( - h: cache-replace-handle, - ) -> result, error>; - - /// Gets the lookup state of the existing object during replace, returning - /// the `$none` error if there was no object. - replace-get-state: func( - h: cache-replace-handle, - ) -> result, error>; - - /// Gets the user metadata of the existing object during replace, returning - /// the `$none` error if there was no object. - replace-get-user-metadata: func( - h: cache-replace-handle, - max-len: u64, - ) -> result>, error>; - } -/// Proposed hostcall interface for the HTTP Cache API +/// [HTTP Cache] API. /// /// Overall, this should look very familiar to users of the Core Cache API. The primary differences /// are: /// -/// - HTTP `request_handle`s and `response_handle`s are used rather than relying on the user to -/// encode headers, status codes, etc in `user_metadata`. +/// - HTTP `request`s and `response`s are used rather than relying on the user to +/// encode headers, status codes, etc in `user-metadata`. /// -/// - Convenience functions specific to HTTP semantics are provided, such as `is_request_cacheable`, -/// `get_suggested_backend_request`, `get_suggested_cache_options`, and -/// `transaction_record_not_cacheable`. +/// - Convenience functions specific to HTTP semantics are provided, such as `is-request-cacheable`, +/// `get-suggested-backend-request`, `get-suggested-write-options`, and +/// `transaction-record-not-cacheable`. /// /// The HTTP-specific behavior of these functions is intended to support applications that match the -/// normative guidance in RFC 9111. For example, `is_request_cacheable` returns `false` for `POST` +/// normative guidance in [RFC 9111]. For example, `is-request-cacheable` returns `false` for `POST` /// requests. However, this answer along with those of many of these functions explicitly provide -/// _suggestions_; they do not necessarily need to be followed if custom behavior is required, such +/// *suggestions*; they do not necessarily need to be followed if custom behavior is required, such /// as caching `POST` responses when the application author knows that to be safe. /// -/// The starting points for this API are `lookup` (no request collapsing) and `transaction_lookup` +/// The starting points for this API are `lookup` (no request collapsing) and `transaction-lookup` /// (request collapsing). +/// +/// [HTTP Cache]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/cache-freshness/ +/// [RFC 9111]: https://www.rfc-editor.org/rfc/rfc9111.html interface http-cache { use types.{error}; - use http-types.{request-handle, response-handle, body-handle}; + use http-body.{body}; + use http-req.{request}; + use http-resp.{response}; use cache.{lookup-state, object-length, duration-ns, cache-hit-count}; - /// A handle to an HTTP Cache transaction. - type cache-handle = u32; + /// An HTTP Cache transaction. + resource entry { + /// (DEPRECATED) Use transaction-lookup + lookup: static func( + req-handle: borrow, + options: lookup-options, + ) -> result; + + /// Performs a cache lookup based on the given request. + /// + /// This operation always participates in request collapsing and may return an obligation to + /// insert or update responses, and/or stale responses. To bypass request collapsing, use + /// `lookup` instead. + /// + /// The request is not consumed. + transaction-lookup: static func( + req-handle: borrow, + options: lookup-options, + ) -> result; + + /// Inserts a response into the cache with the given options, returning a streaming body handle + /// that is ready for writing or appending. + /// + /// Can only be used if the cache handle state includes the `must-insert-or-update` flag. + /// + /// The response is consumed. + transaction-insert: func( + resp-handle: response, + options: write-options, + ) -> result; + + /// Inserts a response into the cache with the given options, and return a fresh cache handle + /// that can be used to retrieve and stream the response while it's being inserted. + /// + /// This helps avoid the “slow reader” problem on a teed stream, for example when a program + /// wishes to store a backend request in the cache while simultaneously streaming to a client + /// in an HTTP response. + /// + /// The response is consumed. + transaction-insert-and-stream-back: func( + resp-handle: response, + options: write-options, + ) -> result, error>; + + /// Updates freshness lifetime, response headers, and caching settings without updating the + /// response body. + /// + /// Can only be used in if the cache handle state includes both of the flags: + /// - `found` + /// - `must-insert-or-update` + /// + /// The response is consumed. + transaction-update: func( + resp-handle: response, + options: write-options, + ) -> result<_, error>; + + /// Updates freshness lifetime, response headers, and caching settings without updating the + /// response body, and return a fresh cache handle that can be used to retrieve and stream the + /// stored response. + /// + /// Can only be used in if the cache handle state includes both of the flags: + /// - `found` + /// - `must-insert-or-update` + /// + /// The response is consumed. + transaction-update-and-return-fresh: func( + resp-handle: response, + options: write-options, + ) -> result; + + /// Disables request collapsing and response caching for this cache entry. + /// + /// In Varnish terms, this function stores a hit-for-pass object. + /// + /// Only the max age and, optionally, the vary rule are read from the `options` + /// for this function. + transaction-record-not-cacheable: func( + options: write-options, + ) -> result<_, error>; + + /// Prepares a suggested request to make to a backend to satisfy the looked-up request. + /// + /// If there is a stored, stale response, this suggested request may be for revalidation. If the + /// looked-up request is ranged, the suggested request will be unranged in order to try caching + /// the entire response. + get-suggested-backend-request: func() -> result; + + /// Prepares a suggested set of cache write options for a given request and response pair. + /// + /// The response is not consumed. + get-suggested-write-options: func( + response: borrow, + ) -> result; + + /// Adjusts a response into the appropriate form for storage and provides a storage action + /// recommendation. + /// + /// For example, if the looked-up request contains conditional headers, this function will + /// interpret a `304 Not Modified` response for revalidation by updating headers. + /// + /// In addition to the updated response, this function returns the recommended storage action. + prepare-response-for-storage: func( + response: borrow, + ) -> result, error>; + + /// Retrieves a stored response from the cache, returning the `error.optional-none` error if + /// there was no response found. + /// + /// If `transform-for-client` is set, the response will be adjusted according to the looked-up + /// request. For example, a response retrieved for a range request may be transformed into a + /// `206 Partial Content` response with an appropriate `content-range` header. + get-found-response: func( + transform-for-client: u32, + ) -> result, error>; + + /// Gets the state of a cache transaction. + /// + /// Primarily useful after performing the lookup to determine what subsequent operations are + /// possible and whether any insertion or update obligations exist. + get-state: func( + ) -> result; + + /// Gets the length of the found response, returning the `error.optional-none` error if there + /// was no response found or no length was provided. + get-length: func() -> result; + + /// Gets the configured max age of the found response in nanoseconds, returning the + /// `error.optional-none` error if there was no response found. + get-max-age-ns: func() -> result; + + /// Gets the configured stale-while-revalidate period of the found response in nanoseconds, + /// returning the `error.optional-none` error if there was no response found. + get-stale-while-revalidate-ns: func( + ) -> result; + + /// Gets the age of the found response in nanoseconds, returning the `error.optional-none` error + /// if there was no response found. + get-age-ns: func() -> result; + + /// Gets the number of cache hits for the found response, returning the `error.optional-none` + /// error if there was no response found. + /// + /// This figure only reflects hits for a stored response in a particular cache server + /// or cluster, not the entire Fastly network. + get-hits: func() -> result; + + /// Gets whether a found response is marked as containing sensitive data, returning the + /// `error.optional-none` error if there was no response found. + get-sensitive-data: func() -> result; + + /// Gets the surrogate keys of the found response, returning the `error.optional-none` error if + /// there was no response found. + /// + /// The output is a list of surrogate keys separated by spaces. + /// + /// If the full list requires more than `max-len` bytes, an `error.buffer-len` + /// error is returned containing the required size. + get-surrogate-keys: func( + max-len: u64, + ) -> result; + + /// Gets the vary rule of the found response, returning the `error.optional-none` error if there + /// was no response found. + /// + /// The output is a list of header names separated by spaces. + /// + /// If the full list requires more than `max-len` bytes, an `error.buffer-len` + /// error is returned containing the required size. + get-vary-rule: func( + max-len: u64, + ) -> result; + + /// Abandons an obligation to provide a response to the cache. + /// + /// Useful if there is an error before streaming is possible, for example if a backend is + /// unreachable. + /// + /// If there are other requests collapsed on this transaction, one of those other requests will + /// be awoken and given the obligation to provide a response. If subsequent requests + /// are unlikely to yield cacheable responses, this may lead to undesired serialization of + /// requests. Consider using `transaction-record-not-cacheable` to make lookups for this request + /// bypass the cache. + transaction-abandon: func() -> result<_, error>; + } /// The suggested action to take for spec-recommended behavior following - /// `prepare_response_for_storage`. + /// `prepare-response-for-storage`. enum storage-action { - /// Insert the response into cache (`transaction_insert*`). + /// Insert the response into cache (for `transaction-insert` and + /// `transaction-insert-and-stream-back`). insert, - /// Update the stale response in cache (`transaction_update*`). + /// Update the stale response in cache (for `transaction-update` and + /// `transaction-update-and-return-fresh`). update, /// Do not store this response. do-not-store, /// Do not store this response, and furthermore record its non-cacheability for other pending - /// requests (`transaction_record_not_cacheable`). + /// requests (`transaction-record-not-cacheable`). record-uncacheable, } /// Non-required options for cache lookups. - /// - /// This record is always provided along with an `http_cache_lookup_options_mask` value that - /// indicates which of the fields in this record are valid. - record cache-lookup-options { + record lookup-options { /// Cache key to use in lieu of the automatically-generated cache key based on the request's /// properties. - override-key: list, - } + override-key: option>, + /// Backend name that will be used for the eventual request. + backend-name: option, - /// Options mask for `http_cache_lookup_options`. - flags cache-lookup-options-mask { - reserved, - override-key, + /// Additional options may be added in the future via this resource type. + extra: option>, } - flags cache-write-options-mask { - reserved, - vary-rule, - initial-age-ns, - stale-while-revalidate-ns, - surrogate-keys, - length, - sensitive-data, - } + /// Extensibility for `lookup-options` + resource extra-lookup-options {} /// Options for cache insertions and updates. - /// - /// This record is always provided along with an `http_cache_write_options_mask` value that - /// indicates which of the fields in this record are valid. - record cache-write-options { + record write-options { /// The maximum age of the response before it is considered stale, in nanoseconds. /// - /// This field is required; there is no flag for it in `http_cache_write_options_mask`. + /// This field is required. max-age-ns: duration-ns, /// A list of header names to use when calculating variants for this response. /// /// The format is a string containing header names separated by spaces. - vary-rule: string, + vary-rule: option, /// The initial age of the response in nanoseconds. /// @@ -1591,23 +2407,23 @@ interface http-cache { /// /// This age is used to determine the freshness lifetime of the response as well as to /// prioritize which variant to return if a subsequent lookup matches more than one vary rule - initial-age-ns: duration-ns, + initial-age-ns: option, - /// The maximum duration after `max_age` during which the response may be delivered stale + /// The maximum duration after `max-age` during which the response may be delivered stale /// while being revalidated, in nanoseconds. /// /// If this field is not set, the default value is zero. - stale-while-revalidate-ns: duration-ns, + stale-while-revalidate-ns: option, /// A list of surrogate keys that may be used to purge this response. /// - /// The format is a string containing [valid surrogate - /// keys](https://www.fastly.com/documentation/reference/http/http-headers/Surrogate-Key/) - /// separated by spaces. + /// The format is a string containing [valid surrogate keys] separated by spaces. /// /// If this field is not set, no surrogate keys will be associated with the response. This /// means that the response cannot be purged except via a purge-all operation. - surrogate-keys: string, + /// + /// [valid surrogate keys]: https://www.fastly.com/documentation/reference/http/http-headers/Surrogate-Key/ + surrogate-keys: option, /// The length of the response body. /// @@ -1616,341 +2432,146 @@ interface http-cache { /// When possible, this field should be set so that other clients waiting to retrieve the /// body have enough information to synthesize a `content-length` even before the complete /// body is inserted to the cache. - length: object-length, + length: option, + + /// Enable or disable PCI/HIPAA-compliant non-volatile caching. + /// + /// See the [Fastly PCI-Compliant Caching and Delivery documentation] for details. + /// + /// [Fastly PCI-Compliant Caching and Delivery documentation]: https://docs.fastly.com/products/pci-compliant-caching-and-delivery + sensitive-data: bool, + + /// Additional options may be added in the future via this resource type. + extra: option>, } - /// Determine whether a request is cacheable per conservative RFC 9111 semantics. + /// Extensibility for `write-options` + resource extra-write-options {} + + /// Determines whether a request is cacheable per conservative [RFC 9111] semantics. /// /// In particular, this function checks whether the request method is `GET` or `HEAD`, and /// considers requests with other methods uncacheable. Applications where it is safe to cache /// responses to other methods should consider using their own cacheability check instead of /// this function. - is-request-cacheable: func(req-handle: request-handle) -> result; + /// + /// [RFC 9111]: https://www.rfc-editor.org/rfc/rfc9111.html + is-request-cacheable: func(request: borrow) -> result; /// Retrieves the default cache key for the request. /// - /// The `$key_out` parameter must point to an array of size `key_out_len`. - /// - /// If the guest-provided output parameter is not long enough to contain the full key, - /// the required size is written by the host to `nwritten_out` and the `$buflen` - /// error is returned. + /// If the full key requires more than `max-len` bytes, an `error.buffer-len` + /// error is returned containing the required size. /// /// At the moment, HTTP cache keys must always be 32 bytes. get-suggested-cache-key: func( - req-handle: request-handle, + request: borrow, max-len: u64, ) -> result, error>; - /// Perform a cache lookup based on the given request without participating in request - /// collapsing. - /// - /// The request is not consumed. - lookup: func( - req-handle: request-handle, - options-mask: cache-lookup-options-mask, - options: cache-lookup-options, - ) -> result; - - /// Perform a cache lookup based on the given request. - /// - /// This operation always participates in request collapsing and may return an obligation to - /// insert or update responses, and/or stale responses. To bypass request collapsing, use - /// `lookup` instead. - /// - /// The request is not consumed. - transaction-lookup: func( - req-handle: request-handle, - options-mask: cache-lookup-options-mask, - options: cache-lookup-options, - ) -> result; - - /// Insert a response into the cache with the given options, returning a streaming body handle - /// that is ready for writing or appending. - /// - /// Can only be used if the cache handle state includes the `$must_insert_or_update` flag. - /// - /// The response is consumed. - transaction-insert: func( - handle: cache-handle, - resp-handle: response-handle, - options-mask: cache-write-options-mask, - options: cache-write-options, - ) -> result; - - /// Insert a response into the cache with the given options, and return a fresh cache handle - /// that can be used to retrieve and stream the response while it's being inserted. - /// - /// This helps avoid the "slow reader" problem on a teed stream, for example when a program wishes - /// to store a backend request in the cache while simultaneously streaming to a client in an HTTP - /// response. - /// - /// The response is consumed. - transaction-insert-and-stream-back: func( - handle: cache-handle, - resp-handle: response-handle, - options-mask: cache-write-options-mask, - options: cache-write-options, - ) -> result, error>; - - /// Update freshness lifetime, response headers, and caching settings without updating the - /// response body. - /// - /// Can only be used in if the cache handle state includes both of the flags: - /// - `$found` - /// - `$must_insert_or_update` - /// - /// The response is consumed. - transaction-update: func( - handle: cache-handle, - resp-handle: response-handle, - options-mask: cache-write-options-mask, - options: cache-write-options, - ) -> result<_, error>; - - /// Update freshness lifetime, response headers, and caching settings without updating the - /// response body, and return a fresh cache handle that can be used to retrieve and stream the - /// stored response. - /// - /// Can only be used in if the cache handle state includes both of the flags: - /// - `$found` - /// - `$must_insert_or_update` - /// - /// The response is consumed. - transaction-update-and-return-fresh: func( - handle: cache-handle, - resp-handle: response-handle, - options-mask: cache-write-options-mask, - options: cache-write-options, - ) -> result; - - /// Disable request collapsing and response caching for this cache entry. - /// - /// In Varnish terms, this function stores a hit-for-pass object. - /// - /// Only the max age and, optionally, the vary rule are read from the options mask and struct - /// for this function. - transaction-record-not-cacheable: func( - handle: cache-handle, - options-mask: cache-write-options-mask, - options: cache-write-options, - ) -> result<_, error>; - - /// Abandon an obligation to provide a response to the cache. - /// - /// Useful if there is an error before streaming is possible, e.g. if a backend is unreachable. - /// - /// If there are other requests collapsed on this transaction, one of those other requests will - /// be awoken and given the obligation to provide a response. Note that if subsequent requests - /// are unlikely to yield cacheable responses, this may lead to undesired serialization of - /// requests. Consider using `transaction_record_not_cacheable` to make lookups for this request - /// bypass the cache. - transaction-abandon: func( - handle: cache-handle, - ) -> result<_, error>; - - /// Close an ongoing interaction with the cache. + /// Closes an ongoing interaction with the cache. /// - /// If the cache handle state includes `$must_insert_or_update` (and hence no insert or update + /// If the cache handle state includes `must-insert-or-update` (and hence no insert or update /// has been performed), closing the handle cancels any request collapsing, potentially choosing /// a new waiter to perform the insertion/update. close: func( - handle: cache-handle, + handle: entry, ) -> result<_, error>; - /// Prepare a suggested request to make to a backend to satisfy the looked-up request. - /// - /// If there is a stored, stale response, this suggested request may be for revalidation. If the - /// looked-up request is ranged, the suggested request will be unranged in order to try caching - /// the entire response. - get-suggested-backend-request: func( - handle: cache-handle, - ) -> result; - - resource suggested-cache-options { - max-age-ns: func() -> duration-ns; - vary-rule: func(max-len: u64) -> result, error>; - initial-age-ns: func() -> duration-ns; - stale-while-revalidate-ns: func() -> duration-ns; - surrogate-keys: func(max-len: u64) -> result, error>; - length: func() -> option; - sensitive-data: func() -> bool; + /// The methods in this resource return values that correspond to the fields in a + /// `write-options`. This type is used when a `write-options` value would + /// be returned, so that it can use `max-len` parameters when returning + /// dynamically-sized data, and so that it excludes the `extra` field, since borrowed + /// handles cannot be returned from functions. + resource suggested-write-options { + /// Returns the suggested value for the `write-options.max-age-ns` field. + get-max-age-ns: func() -> duration-ns; + /// Returns the suggested value for the `write-options.vary-rule` field. + get-vary-rule: func(max-len: u64) -> result; + /// Returns the suggested value for the `write-options.initial-age-ns` field. + get-initial-age-ns: func() -> duration-ns; + /// Returns the suggested value for the `write-options.stale-while-revalidate-ns` field. + get-stale-while-revalidate-ns: func() -> duration-ns; + /// Returns the suggested value for the `write-options.surrogate-keys` field. + get-surrogate-keys: func(max-len: u64) -> result; + /// Returns the suggested value for the `write-options.length` field. + get-length: func() -> option; + /// Returns the suggested value for the `write-options.sensitive-data` field. + get-sensitive-data: func() -> bool; } - - /// Prepare a suggested set of cache write options for a given request and response pair. - /// - /// The ABI of this function includes several unusual types of input and output parameters. - /// - /// The bits set in the `options_mask` input parameter describe which cache options the guest is - /// requesting that the host provide. - /// - /// The `options` input parameter allows the guest to provide output parameters for - /// pointer/length options. When the corresponding bit is set in `options_mask`, the pointer and - /// length should be set in this record to be used by the host to provide the output. - /// - /// The `options_mask_out` output parameter is only used by the host to indicate the status of - /// pointer/length data in the `options_out` record. The flag for a given pointer/length - /// parameter is set by the host if the corresponding flag was set in `options_mask`, and the - /// value is present in the suggested options. If the host returns a status of `$buflen`, the - /// same set of flags will be set, but the length value of the corresponding fields in - /// `options_out` are set to the lengths that would be required to read the full value from the - /// host on a subsequent call. - /// - /// The `options_out` output parameter is where the host writes the suggested options that were - /// requested by the guest in `options_mask`. For pointer/length data, if there was enough room - /// to write the suggested option, the length field will contain the length of the data actually - /// written, while the pointer field will match the input pointer. - /// - /// The response is not consumed. - get-suggested-cache-options: func( - handle: cache-handle, - response: response-handle, - ) -> result; - - /// Adjust a response into the appropriate form for storage and provides a storage action recommendation. - /// - /// For example, if the looked-up request contains conditional headers, this function will - /// interpret a `304 Not Modified` response for revalidation by updating headers. - /// - /// In addition to the updated response, this function returns the recommended storage action. - prepare-response-for-storage: func( - handle: cache-handle, - response: response-handle, - ) -> result, error>; - - /// Retrieve a stored response from the cache, returning the `$none` error if there was no found - /// response. - /// - /// If `transform_for_client` is set, the response will be adjusted according to the looked-up - /// request. For example, a response retrieved for a range request may be transformed into a - /// `206 Partial Content` response with an appropriate `content-range` header. - get-found-response: func( - handle: cache-handle, - transform-for-client: u32, - ) -> result, error>; - - /// Get the state of a cache transaction. - /// - /// Primarily useful after performing the lookup to determine what subsequent operations are - /// possible and whether any insertion or update obligations exist. - get-state: func( - handle: cache-handle, - ) -> result; - - /// Get the length of the found response, returning the `$none` error if there was no found - /// response or no length was provided. - get-length: func(handle: cache-handle) -> result; - - /// Get the configured max age of the found response in nanoseconds, returning the `$none` error - /// if there was no found response. - get-max-age-ns: func(handle: cache-handle) -> result; - - /// Get the configured stale-while-revalidate period of the found response in nanoseconds, - /// returning the `$none` error if there was no found response. - get-stale-while-revalidate-ns: func( - handle: cache-handle, - ) -> result; - - /// Get the age of the found response in nanoseconds, returning the `$none` error if there was - /// no found response. - get-age-ns: func(handle: cache-handle) -> result; - - /// Get the number of cache hits for the found response, returning the `$none` error if there - /// was no found response. - /// - /// Note that this figure only reflects hits for a stored response in a particular cache server - /// or cluster, not the entire Fastly network. - get-hits: func(handle: cache-handle) -> result; - - /// Get whether a found response is marked as containing sensitive data, returning the `$none` - /// error if there was no found response. - get-sensitive-data: func(handle: cache-handle) -> result; - - /// Get the surrogate keys of the found response, returning the `$none` error if there was no - /// found response. - /// - /// The output is a list of surrogate keys separated by spaces. - /// - /// If the guest-provided output parameter is not long enough to contain the full list of - /// surrogate keys, the required size is written by the host to `nwritten_out` and the `$buflen` - /// error is returned. - get-surrogate-keys: func( - handle: cache-handle, - max-len: u64, - ) -> result, error>; - - /// Get the vary rule of the found response, returning the `$none` error if there was no found - /// response. - /// - /// The output is a list of header names separated by spaces. - /// - /// If the guest-provided output parameter is not long enough to contain the full list of - /// surrogate keys, the required size is written by the host to `nwritten_out` and the `$buflen` - /// error is returned. - get-vary-rule: func( - handle: cache-handle, - max-len: u64, - ) -> result, error>; } +/// [Config Store] API. +/// +/// [Config Store]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#config-stores interface config-store { use types.{error}; - /// A handle to an Config Store. - type handle = u32; - - /// Attempt to open the named config store. - open: func(name: string) -> result; - - /// Fetch a value from the config store, returning `None` if it doesn't exist. - get: func( - store: handle, - key: string, - max-len: u64, - ) -> result, error>; + /// A Config Store. + resource store { + /// Attempts to open the named config store. + /// + /// Names are case sensitive. + open: static func(name: string) -> result; + + /// Fetches a value from the config store, returning `none` if it doesn't exist. + get: func( + key: string, + max-len: u64, + ) -> result, error>; + } } +/// [Shielding] API. +/// +/// [Shielding]: https://www.fastly.com/documentation/guides/concepts/shielding/ interface shielding { use types.{error}; shield-info: func( name: string, max-len: u64, - ) -> result, error>; - - flags shield-backend-options-mask { - reserved, - cache-key, - } + ) -> result; record shield-backend-options { - cache-key: string, + cache-key: option, + + /// Additional options may be added in the future via this resource type. + extra: option>, } + /// Extensibility for `shield-backend-options` + resource extra-shield-backend-options {} + backend-for-shield: func( name: string, - options-mask: shield-backend-options-mask, options: shield-backend-options, max-len: u64, ) -> result; } -/// Fastly Image Optimizer +/// [Image Optimizer] API. +/// +/// [Image Optimizer]: https://www.fastly.com/documentation/guides/full-site-delivery/image-optimization/about-fastly-image-optimizer/ interface image-optimizer { - use http-types.{body-handle, request-handle, response}; + use http-body.{body}; + use http-req.{request}; + use http-resp.{response-with-body}; use types.{error}; - flags image-optimizer-transform-config-options { - reserved, - sdk-claims-opts, - } - - record image-optimizer-transform-config { + record image-optimizer-transform-options { /// Contains any Image Optimizer API parameters that were set /// as well as the Image Optimizer region the request is meant for. - sdk-claims-opts: string, + sdk-claims-opts: option, + + /// Additional options may be added in the future via this resource type. + extra: option>, } + /// Extensibility for `image-optimizer-transform-options` + resource extra-image-optimizer-transform-options {} + enum image-optimizer-error-tag { uninitialized, ok, @@ -1964,57 +2585,81 @@ interface image-optimizer { } transform-image-optimizer-request: func( - origin-image-request: request-handle, - origin-image-request-body: body-handle, + origin-image-request: borrow, + origin-image-request-body: option, origin-image-request-backend: string, - io-transform-config-mask: image-optimizer-transform-config-options, - io-transform-config: image-optimizer-transform-config, + io-transform-options: image-optimizer-transform-options, io-error-detail: image-optimizer-error-detail, - ) -> result; + ) -> result; } - -interface reactor { - use http-types.{request-handle, body-handle}; - - /// Serve the given request - /// - /// response handle not currently returned, because in the case of a streamed response - /// send downstream must be fully streamed due to the run to completion semantics. - serve: func(req: request-handle, body: body-handle) -> result; +/// The exported interface. +/// +/// The `handle` function serves as the main entrypoint to applications. Unlike the +/// rest of the interfaces in this package, this `http-incoming` interface is exported by +/// applications rather than imported, which means that this is a function defined +/// by the application and called from the outside, rather than a function called +/// by the application into the outside. +interface http-incoming { + use http-body.{body}; + use http-req.{request}; + + /// Handle the given request. + /// + /// Conceptually, `send` returns a response to the given request, however this isn't + /// modeled as a literal return value in this API. Instead, the `send-downstream` + /// function is used to send the response. This allows for the option of streaming the + /// response body, since that requires the program to continue executing after the + /// response has been initiated. + handle: func(request: request, body: body) -> result; } +/// Features for interacting with the Compute runtime. interface compute-runtime { - use types.{error}; - + /// A timestamp in milliseconds. type vcpu-ms = u64; - get-vcpu-ms: func() -> result; + /// Gets the amount of vCPU time that has passed since this instance was started, in milliseconds. + /// + /// This function returns only time spent running on a vCPU, and does not include time spent + /// performing any I/O operations. However, it is based on clock time passing, and so will include + /// time spent executing hostcalls, is heavily affected by what core of what CPU is running the + /// code, and can even be influenced by the state of the CPU. + /// + /// As a result, this function *should not be used in benchmarking across runs*. It can be used, + /// with caution, to compare the runtime of different operations within the same session. + get-vcpu-ms: func() -> vcpu-ms; +} + +/// WASI interfaces used by `fastly:compute/service`. +world wasi-imports { + import wasi:clocks/wall-clock@0.2.6; + import wasi:clocks/monotonic-clock@0.2.6; + import wasi:io/error@0.2.6; + import wasi:io/streams@0.2.6; + import wasi:random/random@0.2.6; + import wasi:cli/environment@0.2.6; + import wasi:cli/exit@0.2.6; + import wasi:cli/stdout@0.2.6; + import wasi:cli/stderr@0.2.6; + import wasi:cli/stdin@0.2.6; } -world compute { - import wasi:clocks/wall-clock@0.2.0; - import wasi:clocks/monotonic-clock@0.2.0; - import wasi:io/error@0.2.0; - import wasi:io/streams@0.2.0; - import wasi:random/random@0.2.0; - import wasi:cli/environment@0.2.0; - import wasi:cli/exit@0.2.0; - import wasi:cli/stdout@0.2.0; - import wasi:cli/stderr@0.2.0; - import wasi:cli/stdin@0.2.0; - - // public interfaces +/// Custom interfaces used by `fastly:compute/service`. +world custom-imports { import acl; import async-io; import backend; import cache; import compute-runtime; + import config-store; import dictionary; import geo; import device-detection; import erl; import http-body; + import http-cache; + import http-downstream; import http-req; import http-resp; import image-optimizer; @@ -2023,12 +2668,32 @@ world compute { import object-store; import purge; import secret-store; - import config-store; - import uap; - - // experimental interfaces - import http-cache; import shielding; +} + +world custom-exports { + // Export the `http-incoming` interface. + export http-incoming; +} + +/// Interfaces that a Fastly Compute service may import. +/// +/// This contains the imports used in the `service` world, factored out into a +/// separate world so that it can be used by library components. Library components +/// are components that do not export anything themselves. +world service-imports { + include wasi-imports; + include custom-imports; +} - export reactor; +/// A Fastly Compute service. +/// +/// This defines the set of interfaces available to, and expected of, +/// Fastly Compute service applications. +/// +/// This `service` world includes all the `service-imports` imports, and adds the +/// `http-incoming` exports. +world service { + include service-imports; + include custom-exports; } diff --git a/wit/deps/filesystem/preopens.wit b/wit/deps/filesystem/preopens.wit index da801f6..f228479 100644 --- a/wit/deps/filesystem/preopens.wit +++ b/wit/deps/filesystem/preopens.wit @@ -1,8 +1,11 @@ -package wasi:filesystem@0.2.0; +package wasi:filesystem@0.2.6; +@since(version = 0.2.0) interface preopens { + @since(version = 0.2.0) use types.{descriptor}; - /// Return the set of preopened directories, and their path. + /// Return the set of preopened directories, and their paths. + @since(version = 0.2.0) get-directories: func() -> list>; } diff --git a/wit/deps/filesystem/types.wit b/wit/deps/filesystem/types.wit index 11108fc..75c1904 100644 --- a/wit/deps/filesystem/types.wit +++ b/wit/deps/filesystem/types.wit @@ -1,4 +1,4 @@ -package wasi:filesystem@0.2.0; +package wasi:filesystem@0.2.6; /// WASI filesystem is a filesystem API primarily intended to let users run WASI /// programs that access their files on their existing filesystems, without /// significant overhead. @@ -23,16 +23,21 @@ package wasi:filesystem@0.2.0; /// [WASI filesystem path resolution]. /// /// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.2.0) interface types { - use wasi:io/streams@0.2.0.{input-stream, output-stream, error}; - use wasi:clocks/wall-clock@0.2.0.{datetime}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream, output-stream, error}; + @since(version = 0.2.0) + use wasi:clocks/wall-clock@0.2.6.{datetime}; /// File size or length of a region within a file. + @since(version = 0.2.0) type filesize = u64; /// The type of a filesystem object referenced by a descriptor. /// /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.2.0) enum descriptor-type { /// The type of the descriptor or file is unknown or is different from /// any of the other types specified. @@ -56,6 +61,7 @@ interface types { /// Descriptor flags. /// /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.2.0) flags descriptor-flags { /// Read mode: Data can be read. read, @@ -77,7 +83,7 @@ interface types { /// WASI. At this time, it should be interpreted as a request, and not a /// requirement. data-integrity-sync, - /// Requests that reads be performed at the same level of integrety + /// Requests that reads be performed at the same level of integrity /// requested for writes. This is similar to `O_RSYNC` in POSIX. /// /// The precise semantics of this operation have not yet been defined for @@ -99,6 +105,7 @@ interface types { /// File attributes. /// /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.2.0) record descriptor-stat { /// File type. %type: descriptor-type, @@ -125,6 +132,7 @@ interface types { } /// Flags determining the method of how paths are resolved. + @since(version = 0.2.0) flags path-flags { /// As long as the resolved path corresponds to a symbolic link, it is /// expanded. @@ -132,6 +140,7 @@ interface types { } /// Open flags used by `open-at`. + @since(version = 0.2.0) flags open-flags { /// Create file if it does not exist, similar to `O_CREAT` in POSIX. create, @@ -144,9 +153,11 @@ interface types { } /// Number of hard links to an inode. + @since(version = 0.2.0) type link-count = u64; /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.2.0) variant new-timestamp { /// Leave the timestamp set to its previous value. no-change, @@ -248,6 +259,7 @@ interface types { } /// File or memory access pattern advisory information. + @since(version = 0.2.0) enum advice { /// The application has no advice to give on its behavior with respect /// to the specified data. @@ -271,6 +283,7 @@ interface types { /// A 128-bit hash value, split into parts because wasm doesn't have a /// 128-bit integer type. + @since(version = 0.2.0) record metadata-hash-value { /// 64 bits of a 128-bit hash value. lower: u64, @@ -281,6 +294,7 @@ interface types { /// A descriptor is a reference to a filesystem object, which may be a file, /// directory, named pipe, special file, or other object on which filesystem /// calls may be made. + @since(version = 0.2.0) resource descriptor { /// Return a stream for reading from a file, if available. /// @@ -290,6 +304,7 @@ interface types { /// file and they do not interfere with each other. /// /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + @since(version = 0.2.0) read-via-stream: func( /// The offset within the file at which to start reading. offset: filesize, @@ -301,6 +316,7 @@ interface types { /// /// Note: This allows using `write-stream`, which is similar to `write` in /// POSIX. + @since(version = 0.2.0) write-via-stream: func( /// The offset within the file at which to start writing. offset: filesize, @@ -311,12 +327,14 @@ interface types { /// May fail with an error-code describing why the file cannot be appended. /// /// Note: This allows using `write-stream`, which is similar to `write` with - /// `O_APPEND` in in POSIX. + /// `O_APPEND` in POSIX. + @since(version = 0.2.0) append-via-stream: func() -> result; /// Provide file advisory information on a descriptor. /// /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.2.0) advise: func( /// The offset within the file to which the advisory applies. offset: filesize, @@ -332,6 +350,7 @@ interface types { /// opened for writing. /// /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.2.0) sync-data: func() -> result<_, error-code>; /// Get flags associated with a descriptor. @@ -340,6 +359,7 @@ interface types { /// /// Note: This returns the value that was the `fs_flags` value returned /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) get-flags: func() -> result; /// Get the dynamic type of a descriptor. @@ -352,12 +372,14 @@ interface types { /// /// Note: This returns the value that was the `fs_filetype` value returned /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) get-type: func() -> result; /// Adjust the size of an open file. If this increases the file's size, the /// extra bytes are filled with zeros. /// /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.2.0) set-size: func(size: filesize) -> result<_, error-code>; /// Adjust the timestamps of an open file or directory. @@ -365,6 +387,7 @@ interface types { /// Note: This is similar to `futimens` in POSIX. /// /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.2.0) set-times: func( /// The desired values of the data access timestamp. data-access-timestamp: new-timestamp, @@ -383,6 +406,7 @@ interface types { /// In the future, this may change to return a `stream`. /// /// Note: This is similar to `pread` in POSIX. + @since(version = 0.2.0) read: func( /// The maximum number of bytes to read. length: filesize, @@ -399,6 +423,7 @@ interface types { /// In the future, this may change to take a `stream`. /// /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.2.0) write: func( /// Data to write buffer: list, @@ -415,6 +440,7 @@ interface types { /// This always returns a new stream which starts at the beginning of the /// directory. Multiple streams may be active on the same directory, and they /// do not interfere with each other. + @since(version = 0.2.0) read-directory: func() -> result; /// Synchronize the data and metadata of a file to disk. @@ -423,11 +449,13 @@ interface types { /// opened for writing. /// /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.2.0) sync: func() -> result<_, error-code>; /// Create a directory. /// /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.2.0) create-directory-at: func( /// The relative path at which to create the directory. path: string, @@ -442,6 +470,7 @@ interface types { /// modified, use `metadata-hash`. /// /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) stat: func() -> result; /// Return the attributes of a file or directory. @@ -451,6 +480,7 @@ interface types { /// discussion of alternatives. /// /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) stat-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -464,6 +494,7 @@ interface types { /// /// Note: This was called `path_filestat_set_times` in earlier versions of /// WASI. + @since(version = 0.2.0) set-times-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -477,7 +508,12 @@ interface types { /// Create a hard link. /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.2.0) link-at: func( /// Flags determining the method of how the path is resolved. old-path-flags: path-flags, @@ -491,12 +527,6 @@ interface types { /// Open a file or directory. /// - /// The returned descriptor is not guaranteed to be the lowest-numbered - /// descriptor not currently open/ it is randomized to prevent applications - /// from depending on making assumptions about indexes, since this is - /// error-prone in multi-threaded contexts. The returned descriptor is - /// guaranteed to be less than 2**31. - /// /// If `flags` contains `descriptor-flags::mutate-directory`, and the base /// descriptor doesn't have `descriptor-flags::mutate-directory` set, /// `open-at` fails with `error-code::read-only`. @@ -507,6 +537,7 @@ interface types { /// `error-code::read-only`. /// /// Note: This is similar to `openat` in POSIX. + @since(version = 0.2.0) open-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -524,6 +555,7 @@ interface types { /// filesystem, this function fails with `error-code::not-permitted`. /// /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.2.0) readlink-at: func( /// The relative path of the symbolic link from which to read. path: string, @@ -534,6 +566,7 @@ interface types { /// Return `error-code::not-empty` if the directory is not empty. /// /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.2.0) remove-directory-at: func( /// The relative path to a directory to remove. path: string, @@ -542,6 +575,7 @@ interface types { /// Rename a filesystem object. /// /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.2.0) rename-at: func( /// The relative source path of the file or directory to rename. old-path: string, @@ -557,6 +591,7 @@ interface types { /// `error-code::not-permitted`. /// /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.2.0) symlink-at: func( /// The contents of the symbolic link. old-path: string, @@ -568,6 +603,7 @@ interface types { /// /// Return `error-code::is-directory` if the path refers to a directory. /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + @since(version = 0.2.0) unlink-file-at: func( /// The relative path to a file to unlink. path: string, @@ -579,6 +615,7 @@ interface types { /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. /// wasi-filesystem does not expose device and inode numbers, so this function /// may be used instead. + @since(version = 0.2.0) is-same-object: func(other: borrow) -> bool; /// Return a hash of the metadata associated with a filesystem object referred @@ -590,7 +627,7 @@ interface types { /// replaced. It may also include a secret value chosen by the /// implementation and not otherwise exposed. /// - /// Implementations are encourated to provide the following properties: + /// Implementations are encouraged to provide the following properties: /// /// - If the file is not modified or replaced, the computed hash value should /// usually not change. @@ -600,12 +637,14 @@ interface types { /// computed hash. /// /// However, none of these is required. + @since(version = 0.2.0) metadata-hash: func() -> result; /// Return a hash of the metadata associated with a filesystem object referred /// to by a directory descriptor and a relative path. /// /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.2.0) metadata-hash-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -615,8 +654,10 @@ interface types { } /// A stream of directory entries. + @since(version = 0.2.0) resource directory-entry-stream { /// Read a single directory entry from a `directory-entry-stream`. + @since(version = 0.2.0) read-directory-entry: func() -> result, error-code>; } @@ -630,5 +671,6 @@ interface types { /// /// Note that this function is fallible because not all stream-related /// errors are filesystem-related errors. + @since(version = 0.2.0) filesystem-error-code: func(err: borrow) -> option; } diff --git a/wit/deps/filesystem/world.wit b/wit/deps/filesystem/world.wit index 663f579..65597f9 100644 --- a/wit/deps/filesystem/world.wit +++ b/wit/deps/filesystem/world.wit @@ -1,6 +1,9 @@ -package wasi:filesystem@0.2.0; +package wasi:filesystem@0.2.6; +@since(version = 0.2.0) world imports { + @since(version = 0.2.0) import types; + @since(version = 0.2.0) import preopens; } diff --git a/wit/deps/http/proxy.wit b/wit/deps/http/proxy.wit index 687c24d..5bd9f99 100644 --- a/wit/deps/http/proxy.wit +++ b/wit/deps/http/proxy.wit @@ -1,32 +1,50 @@ -package wasi:http@0.2.0; +package wasi:http@0.2.6; -/// The `wasi:http/proxy` world captures a widely-implementable intersection of -/// hosts that includes HTTP forward and reverse proxies. Components targeting -/// this world may concurrently stream in and out any number of incoming and -/// outgoing HTTP requests. -world proxy { +/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. +/// It is intended to be `include`d in other worlds. +@since(version = 0.2.0) +world imports { /// HTTP proxies have access to time and randomness. - include wasi:clocks/imports@0.2.0; - import wasi:random/random@0.2.0; + @since(version = 0.2.0) + import wasi:clocks/monotonic-clock@0.2.6; + @since(version = 0.2.0) + import wasi:clocks/wall-clock@0.2.6; + @since(version = 0.2.0) + import wasi:random/random@0.2.6; /// Proxies have standard output and error streams which are expected to /// terminate in a developer-facing console provided by the host. - import wasi:cli/stdout@0.2.0; - import wasi:cli/stderr@0.2.0; + @since(version = 0.2.0) + import wasi:cli/stdout@0.2.6; + @since(version = 0.2.0) + import wasi:cli/stderr@0.2.6; /// TODO: this is a temporary workaround until component tooling is able to /// gracefully handle the absence of stdin. Hosts must return an eof stream /// for this import, which is what wasi-libc + tooling will do automatically /// when this import is properly removed. - import wasi:cli/stdin@0.2.0; + @since(version = 0.2.0) + import wasi:cli/stdin@0.2.6; /// This is the default handler to use when user code simply wants to make an /// HTTP request (e.g., via `fetch()`). + @since(version = 0.2.0) import outgoing-handler; +} + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +@since(version = 0.2.0) +world proxy { + @since(version = 0.2.0) + include imports; /// The host delivers incoming HTTP requests to a component by calling the /// `handle` function of this exported interface. A host may arbitrarily reuse /// or not reuse component instance when delivering incoming HTTP requests and /// thus a component must be able to handle 0..N calls to `handle`. + @since(version = 0.2.0) export incoming-handler; } diff --git a/wit/deps/http/types.wit b/wit/deps/http/types.wit index 755ac6a..e174c3d 100644 --- a/wit/deps/http/types.wit +++ b/wit/deps/http/types.wit @@ -2,10 +2,10 @@ /// HTTP Requests and Responses, both incoming and outgoing, as well as /// their headers, trailers, and bodies. interface types { - use wasi:clocks/monotonic-clock@0.2.0.{duration}; - use wasi:io/streams@0.2.0.{input-stream, output-stream}; - use wasi:io/error@0.2.0.{error as io-error}; - use wasi:io/poll@0.2.0.{pollable}; + use wasi:clocks/monotonic-clock@0.2.6.{duration}; + use wasi:io/streams@0.2.6.{input-stream, output-stream}; + use wasi:io/error@0.2.6.{error as io-error}; + use wasi:io/poll@0.2.6.{pollable}; /// This type corresponds to HTTP standard Methods. variant method { diff --git a/wit/deps/io/error.wit b/wit/deps/io/error.wit index 22e5b64..784f74a 100644 --- a/wit/deps/io/error.wit +++ b/wit/deps/io/error.wit @@ -1,6 +1,6 @@ -package wasi:io@0.2.0; - +package wasi:io@0.2.6; +@since(version = 0.2.0) interface error { /// A resource which represents some error information. /// @@ -11,16 +11,15 @@ interface error { /// `wasi:io/streams/stream-error` type. /// /// To provide more specific error information, other interfaces may - /// provide functions to further "downcast" this error into more specific - /// error information. For example, `error`s returned in streams derived - /// from filesystem types to be described using the filesystem's own - /// error-code type, using the function - /// `wasi:filesystem/types/filesystem-error-code`, which takes a parameter - /// `borrow` and returns - /// `option`. + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. /// /// The set of functions which can "downcast" an `error` into a more /// concrete type is open. + @since(version = 0.2.0) resource error { /// Returns a string that is suitable to assist humans in debugging /// this error. @@ -29,6 +28,7 @@ interface error { /// It may change across platforms, hosts, or other implementation /// details. Parsing this string is a major platform-compatibility /// hazard. + @since(version = 0.2.0) to-debug-string: func() -> string; } } diff --git a/wit/deps/io/poll.wit b/wit/deps/io/poll.wit index ddc67f8..7f71183 100644 --- a/wit/deps/io/poll.wit +++ b/wit/deps/io/poll.wit @@ -1,22 +1,26 @@ -package wasi:io@0.2.0; +package wasi:io@0.2.6; /// A poll API intended to let users wait for I/O events on multiple handles /// at once. +@since(version = 0.2.0) interface poll { /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) resource pollable { - /// Return the readiness of a pollable. This function never blocks. - /// - /// Returns `true` when the pollable is ready, and `false` otherwise. - ready: func() -> bool; + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; - /// `block` returns immediately if the pollable is ready, and otherwise - /// blocks until ready. - /// - /// This function is equivalent to calling `poll.poll` on a list - /// containing only this pollable. - block: func(); + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); } /// Poll for completion on a set of pollables. @@ -27,8 +31,9 @@ interface poll { /// The result `list` contains one or more indices of handles in the /// argument list that is ready for I/O. /// - /// If the list contains more elements than can be indexed with a `u32` - /// value, this function traps. + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. /// /// A timeout can be implemented by adding a pollable from the /// wasi-clocks API to the list. @@ -36,6 +41,7 @@ interface poll { /// This function does not return a `result`; polling in itself does not /// do any I/O so it doesn't fail. If any of the I/O sources identified by /// the pollables has an error, it is indicated by marking the source as - /// being reaedy for I/O. + /// being ready for I/O. + @since(version = 0.2.0) poll: func(in: list>) -> list; } diff --git a/wit/deps/io/streams.wit b/wit/deps/io/streams.wit index 6d2f871..c5da38c 100644 --- a/wit/deps/io/streams.wit +++ b/wit/deps/io/streams.wit @@ -1,19 +1,26 @@ -package wasi:io@0.2.0; +package wasi:io@0.2.6; /// WASI I/O is an I/O abstraction API which is currently focused on providing /// stream types. /// /// In the future, the component model is expected to add built-in stream types; /// when it does, they are expected to subsume this API. +@since(version = 0.2.0) interface streams { + @since(version = 0.2.0) use error.{error}; + @since(version = 0.2.0) use poll.{pollable}; /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) variant stream-error { /// The last operation (a write or flush) failed before completion. /// /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. last-operation-failed(error), /// The stream is closed: no more input will be accepted by the /// stream. A closed output-stream will return this error on all @@ -29,6 +36,7 @@ interface streams { /// available, which could even be zero. To wait for data to be available, /// use the `subscribe` function to obtain a `pollable` which can be polled /// for using `wasi:io/poll`. + @since(version = 0.2.0) resource input-stream { /// Perform a non-blocking read from the stream. /// @@ -56,6 +64,7 @@ interface streams { /// is not possible to allocate in wasm32, or not desirable to allocate as /// as a return value by the callee. The callee may return a list of bytes /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) read: func( /// The maximum number of bytes to read len: u64 @@ -63,6 +72,7 @@ interface streams { /// Read bytes from a stream, after blocking until at least one byte can /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) blocking-read: func( /// The maximum number of bytes to read len: u64 @@ -72,6 +82,7 @@ interface streams { /// /// Behaves identical to `read`, except instead of returning a list /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) skip: func( /// The maximum number of bytes to skip. len: u64, @@ -79,6 +90,7 @@ interface streams { /// Skip bytes from a stream, after blocking until at least one byte /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) blocking-skip: func( /// The maximum number of bytes to skip. len: u64, @@ -90,6 +102,7 @@ interface streams { /// The created `pollable` is a child resource of the `input-stream`. /// Implementations may trap if the `input-stream` is dropped before /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) subscribe: func() -> pollable; } @@ -102,6 +115,11 @@ interface streams { /// promptly, which could even be zero. To wait for the stream to be ready to /// accept data, the `subscribe` function to obtain a `pollable` which can be /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) resource output-stream { /// Check readiness for writing. This function never blocks. /// @@ -112,6 +130,7 @@ interface streams { /// When this function returns 0 bytes, the `subscribe` pollable will /// become ready when this function will report at least 1 byte, or an /// error. + @since(version = 0.2.0) check-write: func() -> result; /// Perform a write. This function never blocks. @@ -127,6 +146,7 @@ interface streams { /// /// returns Err(closed) without writing if the stream has closed since /// the last call to check-write provided a permit. + @since(version = 0.2.0) write: func( contents: list ) -> result<_, stream-error>; @@ -155,6 +175,7 @@ interface streams { /// // Check for any errors that arose during `flush` /// let _ = this.check-write(); // eliding error handling /// ``` + @since(version = 0.2.0) blocking-write-and-flush: func( contents: list ) -> result<_, stream-error>; @@ -169,14 +190,16 @@ interface streams { /// writes (`check-write` will return `ok(0)`) until the flush has /// completed. The `subscribe` pollable will become ready when the /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) flush: func() -> result<_, stream-error>; /// Request to flush buffered output, and block until flush completes /// and stream is ready for writing again. + @since(version = 0.2.0) blocking-flush: func() -> result<_, stream-error>; /// Create a `pollable` which will resolve once the output-stream - /// is ready for more writing, or an error has occured. When this + /// is ready for more writing, or an error has occurred. When this /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an /// error. /// @@ -185,6 +208,7 @@ interface streams { /// The created `pollable` is a child resource of the `output-stream`. /// Implementations may trap if the `output-stream` is dropped before /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) subscribe: func() -> pollable; /// Write zeroes to a stream. @@ -193,6 +217,7 @@ interface streams { /// preconditions (must use check-write first), but instead of /// passing a list of bytes, you simply pass the number of zero-bytes /// that should be written. + @since(version = 0.2.0) write-zeroes: func( /// The number of zero-bytes to write len: u64 @@ -222,6 +247,7 @@ interface streams { /// // Check for any errors that arose during `flush` /// let _ = this.check-write(); // eliding error handling /// ``` + @since(version = 0.2.0) blocking-write-zeroes-and-flush: func( /// The number of zero-bytes to write len: u64 @@ -229,7 +255,7 @@ interface streams { /// Read from one stream and write to another. /// - /// The behavior of splice is equivelant to: + /// The behavior of splice is equivalent to: /// 1. calling `check-write` on the `output-stream` /// 2. calling `read` on the `input-stream` with the smaller of the /// `check-write` permitted length and the `len` provided to `splice` @@ -240,6 +266,7 @@ interface streams { /// /// This function returns the number of bytes transferred; it may be less /// than `len`. + @since(version = 0.2.0) splice: func( /// The stream to read from src: borrow, @@ -252,6 +279,7 @@ interface streams { /// This is similar to `splice`, except that it blocks until the /// `output-stream` is ready for writing, and the `input-stream` /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) blocking-splice: func( /// The stream to read from src: borrow, diff --git a/wit/deps/io/world.wit b/wit/deps/io/world.wit index 5f0b43f..84c85c0 100644 --- a/wit/deps/io/world.wit +++ b/wit/deps/io/world.wit @@ -1,6 +1,10 @@ -package wasi:io@0.2.0; +package wasi:io@0.2.6; +@since(version = 0.2.0) world imports { + @since(version = 0.2.0) import streams; + + @since(version = 0.2.0) import poll; } diff --git a/wit/deps/random/insecure-seed.wit b/wit/deps/random/insecure-seed.wit index 47210ac..d3dc03a 100644 --- a/wit/deps/random/insecure-seed.wit +++ b/wit/deps/random/insecure-seed.wit @@ -1,8 +1,9 @@ -package wasi:random@0.2.0; +package wasi:random@0.2.6; /// The insecure-seed interface for seeding hash-map DoS resistance. /// /// It is intended to be portable at least between Unix-family platforms and /// Windows. +@since(version = 0.2.0) interface insecure-seed { /// Return a 128-bit value that may contain a pseudo-random value. /// @@ -21,5 +22,6 @@ interface insecure-seed { /// This will likely be changed to a value import, to prevent it from being /// called multiple times and potentially used for purposes other than DoS /// protection. + @since(version = 0.2.0) insecure-seed: func() -> tuple; } diff --git a/wit/deps/random/insecure.wit b/wit/deps/random/insecure.wit index c58f4ee..d4d0284 100644 --- a/wit/deps/random/insecure.wit +++ b/wit/deps/random/insecure.wit @@ -1,8 +1,9 @@ -package wasi:random@0.2.0; +package wasi:random@0.2.6; /// The insecure interface for insecure pseudo-random numbers. /// /// It is intended to be portable at least between Unix-family platforms and /// Windows. +@since(version = 0.2.0) interface insecure { /// Return `len` insecure pseudo-random bytes. /// @@ -12,11 +13,13 @@ interface insecure { /// There are no requirements on the values of the returned bytes, however /// implementations are encouraged to return evenly distributed values with /// a long period. + @since(version = 0.2.0) get-insecure-random-bytes: func(len: u64) -> list; /// Return an insecure pseudo-random `u64` value. /// /// This function returns the same type of pseudo-random data as /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.2.0) get-insecure-random-u64: func() -> u64; } diff --git a/wit/deps/random/random.wit b/wit/deps/random/random.wit index 0c017f0..a0ff956 100644 --- a/wit/deps/random/random.wit +++ b/wit/deps/random/random.wit @@ -1,8 +1,9 @@ -package wasi:random@0.2.0; +package wasi:random@0.2.6; /// WASI Random is a random data API. /// /// It is intended to be portable at least between Unix-family platforms and /// Windows. +@since(version = 0.2.0) interface random { /// Return `len` cryptographically-secure random or pseudo-random bytes. /// @@ -16,11 +17,13 @@ interface random { /// This function must always return fresh data. Deterministic environments /// must omit this function, rather than implementing it with deterministic /// data. + @since(version = 0.2.0) get-random-bytes: func(len: u64) -> list; /// Return a cryptographically-secure random or pseudo-random `u64` value. /// /// This function returns the same type of data as `get-random-bytes`, /// represented as a `u64`. + @since(version = 0.2.0) get-random-u64: func() -> u64; } diff --git a/wit/deps/random/world.wit b/wit/deps/random/world.wit index 3da3491..099f47b 100644 --- a/wit/deps/random/world.wit +++ b/wit/deps/random/world.wit @@ -1,7 +1,13 @@ -package wasi:random@0.2.0; +package wasi:random@0.2.6; +@since(version = 0.2.0) world imports { + @since(version = 0.2.0) import random; + + @since(version = 0.2.0) import insecure; + + @since(version = 0.2.0) import insecure-seed; } diff --git a/wit/deps/sockets/instance-network.wit b/wit/deps/sockets/instance-network.wit index e455d0f..5f6e6c1 100644 --- a/wit/deps/sockets/instance-network.wit +++ b/wit/deps/sockets/instance-network.wit @@ -1,9 +1,11 @@ /// This interface provides a value-export of the default network handle.. +@since(version = 0.2.0) interface instance-network { + @since(version = 0.2.0) use network.{network}; /// Get a handle to the default network. + @since(version = 0.2.0) instance-network: func() -> network; - } diff --git a/wit/deps/sockets/ip-name-lookup.wit b/wit/deps/sockets/ip-name-lookup.wit index 8e639ec..ee6419e 100644 --- a/wit/deps/sockets/ip-name-lookup.wit +++ b/wit/deps/sockets/ip-name-lookup.wit @@ -1,9 +1,10 @@ - +@since(version = 0.2.0) interface ip-name-lookup { - use wasi:io/poll@0.2.0.{pollable}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) use network.{network, error-code, ip-address}; - /// Resolve an internet host name to a list of IP addresses. /// /// Unicode domain names are automatically converted to ASCII using IDNA encoding. @@ -24,8 +25,10 @@ interface ip-name-lookup { /// - /// - /// - + @since(version = 0.2.0) resolve-addresses: func(network: borrow, name: string) -> result; + @since(version = 0.2.0) resource resolve-address-stream { /// Returns the next address from the resolver. /// @@ -40,12 +43,14 @@ interface ip-name-lookup { /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + @since(version = 0.2.0) resolve-next-address: func() -> result, error-code>; /// Create a `pollable` which will resolve once the stream is ready for I/O. /// - /// Note: this function is here for WASI Preview2 only. + /// Note: this function is here for WASI 0.2 only. /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) subscribe: func() -> pollable; } } diff --git a/wit/deps/sockets/network.wit b/wit/deps/sockets/network.wit index 9cadf06..6ca98b6 100644 --- a/wit/deps/sockets/network.wit +++ b/wit/deps/sockets/network.wit @@ -1,8 +1,12 @@ - +@since(version = 0.2.0) interface network { + @unstable(feature = network-error-code) + use wasi:io/error@0.2.6.{error}; + /// An opaque resource that represents access to (a subset of) the network. /// This enables context-based security for networking. /// There is no need for this to map 1:1 to a physical network interface. + @since(version = 0.2.0) resource network; /// Error codes. @@ -17,6 +21,7 @@ interface network { /// - `concurrency-conflict` /// /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.2.0) enum error-code { /// Unknown error unknown, @@ -103,6 +108,20 @@ interface network { permanent-resolver-failure, } + /// Attempts to extract a network-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// network-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are network-related errors. + @unstable(feature = network-error-code) + network-error-code: func(err: borrow) -> option; + + @since(version = 0.2.0) enum ip-address-family { /// Similar to `AF_INET` in POSIX. ipv4, @@ -111,14 +130,18 @@ interface network { ipv6, } + @since(version = 0.2.0) type ipv4-address = tuple; + @since(version = 0.2.0) type ipv6-address = tuple; + @since(version = 0.2.0) variant ip-address { ipv4(ipv4-address), ipv6(ipv6-address), } + @since(version = 0.2.0) record ipv4-socket-address { /// sin_port port: u16, @@ -126,6 +149,7 @@ interface network { address: ipv4-address, } + @since(version = 0.2.0) record ipv6-socket-address { /// sin6_port port: u16, @@ -137,9 +161,9 @@ interface network { scope-id: u32, } + @since(version = 0.2.0) variant ip-socket-address { ipv4(ipv4-socket-address), ipv6(ipv6-socket-address), } - } diff --git a/wit/deps/sockets/tcp-create-socket.wit b/wit/deps/sockets/tcp-create-socket.wit index c7ddf1f..eedbd30 100644 --- a/wit/deps/sockets/tcp-create-socket.wit +++ b/wit/deps/sockets/tcp-create-socket.wit @@ -1,6 +1,8 @@ - +@since(version = 0.2.0) interface tcp-create-socket { + @since(version = 0.2.0) use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) use tcp.{tcp-socket}; /// Create a new TCP socket. @@ -23,5 +25,6 @@ interface tcp-create-socket { /// - /// - /// - + @since(version = 0.2.0) create-tcp-socket: func(address-family: ip-address-family) -> result; } diff --git a/wit/deps/sockets/tcp.wit b/wit/deps/sockets/tcp.wit index 5902b9e..beefd7b 100644 --- a/wit/deps/sockets/tcp.wit +++ b/wit/deps/sockets/tcp.wit @@ -1,10 +1,15 @@ - +@since(version = 0.2.0) interface tcp { - use wasi:io/streams@0.2.0.{input-stream, output-stream}; - use wasi:io/poll@0.2.0.{pollable}; - use wasi:clocks/monotonic-clock@0.2.0.{duration}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.6.{duration}; + @since(version = 0.2.0) use network.{network, error-code, ip-socket-address, ip-address-family}; + @since(version = 0.2.0) enum shutdown-type { /// Similar to `SHUT_RD` in POSIX. receive, @@ -27,8 +32,8 @@ interface tcp { /// - `connect-in-progress` /// - `connected` /// - `closed` - /// See - /// for a more information. + /// See + /// for more information. /// /// Note: Except where explicitly mentioned, whenever this documentation uses /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. @@ -37,6 +42,7 @@ interface tcp { /// In addition to the general error codes documented on the /// `network::error-code` type, TCP socket methods may always return /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.2.0) resource tcp-socket { /// Bind the socket to a specific network on the provided IP address and port. /// @@ -76,13 +82,15 @@ interface tcp { /// - /// - /// - + @since(version = 0.2.0) start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) finish-bind: func() -> result<_, error-code>; /// Connect to a remote endpoint. /// /// On success: - /// - the socket is transitioned into the `connection` state. + /// - the socket is transitioned into the `connected` state. /// - a pair of streams is returned that can be used to read & write to the connection /// /// After a failed connection attempt, the socket will be in the `closed` @@ -121,7 +129,9 @@ interface tcp { /// - /// - /// - + @since(version = 0.2.0) start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) finish-connect: func() -> result, error-code>; /// Start listening for new connections. @@ -149,7 +159,9 @@ interface tcp { /// - /// - /// - + @since(version = 0.2.0) start-listen: func() -> result<_, error-code>; + @since(version = 0.2.0) finish-listen: func() -> result<_, error-code>; /// Accept a new client socket. @@ -178,6 +190,7 @@ interface tcp { /// - /// - /// - + @since(version = 0.2.0) accept: func() -> result, error-code>; /// Get the bound local address. @@ -196,6 +209,7 @@ interface tcp { /// - /// - /// - + @since(version = 0.2.0) local-address: func() -> result; /// Get the remote address. @@ -208,16 +222,19 @@ interface tcp { /// - /// - /// - + @since(version = 0.2.0) remote-address: func() -> result; /// Whether the socket is in the `listening` state. /// /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.2.0) is-listening: func() -> bool; /// Whether this is a IPv4 or IPv6 socket. /// /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) address-family: func() -> ip-address-family; /// Hints the desired listen queue size. Implementations are free to ignore this. @@ -229,6 +246,7 @@ interface tcp { /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. /// - `invalid-argument`: (set) The provided value was 0. /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + @since(version = 0.2.0) set-listen-backlog-size: func(value: u64) -> result<_, error-code>; /// Enables or disables keepalive. @@ -240,7 +258,9 @@ interface tcp { /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. /// /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.2.0) keep-alive-enabled: func() -> result; + @since(version = 0.2.0) set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. @@ -253,7 +273,9 @@ interface tcp { /// /// # Typical errors /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) keep-alive-idle-time: func() -> result; + @since(version = 0.2.0) set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; /// The time between keepalive packets. @@ -266,7 +288,9 @@ interface tcp { /// /// # Typical errors /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) keep-alive-interval: func() -> result; + @since(version = 0.2.0) set-keep-alive-interval: func(value: duration) -> result<_, error-code>; /// The maximum amount of keepalive packets TCP should send before aborting the connection. @@ -279,7 +303,9 @@ interface tcp { /// /// # Typical errors /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) keep-alive-count: func() -> result; + @since(version = 0.2.0) set-keep-alive-count: func(value: u32) -> result<_, error-code>; /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. @@ -288,7 +314,9 @@ interface tcp { /// /// # Typical errors /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) hop-limit: func() -> result; + @since(version = 0.2.0) set-hop-limit: func(value: u8) -> result<_, error-code>; /// The kernel buffer space reserved for sends/receives on this socket. @@ -301,9 +329,13 @@ interface tcp { /// /// # Typical errors /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) receive-buffer-size: func() -> result; + @since(version = 0.2.0) set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) send-buffer-size: func() -> result; + @since(version = 0.2.0) set-send-buffer-size: func(value: u64) -> result<_, error-code>; /// Create a `pollable` which can be used to poll for, or block on, @@ -318,11 +350,12 @@ interface tcp { /// `subscribe` only has to be called once per socket and can then be /// (re)used for the remainder of the socket's lifetime. /// - /// See - /// for a more information. + /// See + /// for more information. /// - /// Note: this function is here for WASI Preview2 only. + /// Note: this function is here for WASI 0.2 only. /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) subscribe: func() -> pollable; /// Initiate a graceful shutdown. @@ -335,7 +368,7 @@ interface tcp { /// associated with this socket will be closed and a FIN packet will be sent. /// - `both`: Same effect as `receive` & `send` combined. /// - /// This function is idempotent. Shutting a down a direction more than once + /// This function is idempotent; shutting down a direction more than once /// has no effect and returns `ok`. /// /// The shutdown function does not close (drop) the socket. @@ -348,6 +381,7 @@ interface tcp { /// - /// - /// - + @since(version = 0.2.0) shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; } } diff --git a/wit/deps/sockets/udp-create-socket.wit b/wit/deps/sockets/udp-create-socket.wit index 0482d1f..e8eeacb 100644 --- a/wit/deps/sockets/udp-create-socket.wit +++ b/wit/deps/sockets/udp-create-socket.wit @@ -1,6 +1,8 @@ - +@since(version = 0.2.0) interface udp-create-socket { + @since(version = 0.2.0) use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) use udp.{udp-socket}; /// Create a new UDP socket. @@ -23,5 +25,6 @@ interface udp-create-socket { /// - /// - /// - + @since(version = 0.2.0) create-udp-socket: func(address-family: ip-address-family) -> result; } diff --git a/wit/deps/sockets/udp.wit b/wit/deps/sockets/udp.wit index d987a0a..9dbe693 100644 --- a/wit/deps/sockets/udp.wit +++ b/wit/deps/sockets/udp.wit @@ -1,9 +1,12 @@ - +@since(version = 0.2.0) interface udp { - use wasi:io/poll@0.2.0.{pollable}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) use network.{network, error-code, ip-socket-address, ip-address-family}; /// A received datagram. + @since(version = 0.2.0) record incoming-datagram { /// The payload. /// @@ -19,6 +22,7 @@ interface udp { } /// A datagram to be sent out. + @since(version = 0.2.0) record outgoing-datagram { /// The payload. data: list, @@ -33,9 +37,8 @@ interface udp { remote-address: option, } - - /// A UDP socket handle. + @since(version = 0.2.0) resource udp-socket { /// Bind the socket to a specific network on the provided IP address and port. /// @@ -63,7 +66,9 @@ interface udp { /// - /// - /// - + @since(version = 0.2.0) start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) finish-bind: func() -> result<_, error-code>; /// Set up inbound & outbound communication channels, optionally to a specific peer. @@ -106,6 +111,7 @@ interface udp { /// - /// - /// - + @since(version = 0.2.0) %stream: func(remote-address: option) -> result, error-code>; /// Get the current bound address. @@ -124,6 +130,7 @@ interface udp { /// - /// - /// - + @since(version = 0.2.0) local-address: func() -> result; /// Get the address the socket is currently streaming to. @@ -136,11 +143,13 @@ interface udp { /// - /// - /// - + @since(version = 0.2.0) remote-address: func() -> result; /// Whether this is a IPv4 or IPv6 socket. /// /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) address-family: func() -> ip-address-family; /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. @@ -149,7 +158,9 @@ interface udp { /// /// # Typical errors /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) unicast-hop-limit: func() -> result; + @since(version = 0.2.0) set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; /// The kernel buffer space reserved for sends/receives on this socket. @@ -162,18 +173,24 @@ interface udp { /// /// # Typical errors /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) receive-buffer-size: func() -> result; + @since(version = 0.2.0) set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) send-buffer-size: func() -> result; + @since(version = 0.2.0) set-send-buffer-size: func(value: u64) -> result<_, error-code>; /// Create a `pollable` which will resolve once the socket is ready for I/O. /// - /// Note: this function is here for WASI Preview2 only. + /// Note: this function is here for WASI 0.2 only. /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) subscribe: func() -> pollable; } + @since(version = 0.2.0) resource incoming-datagram-stream { /// Receive messages on the socket. /// @@ -198,15 +215,18 @@ interface udp { /// - /// - /// - + @since(version = 0.2.0) receive: func(max-results: u64) -> result, error-code>; /// Create a `pollable` which will resolve once the stream is ready to receive again. /// - /// Note: this function is here for WASI Preview2 only. + /// Note: this function is here for WASI 0.2 only. /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) subscribe: func() -> pollable; } + @since(version = 0.2.0) resource outgoing-datagram-stream { /// Check readiness for sending. This function never blocks. /// @@ -255,12 +275,14 @@ interface udp { /// - /// - /// - + @since(version = 0.2.0) send: func(datagrams: list) -> result; /// Create a `pollable` which will resolve once the stream is ready to send again. /// - /// Note: this function is here for WASI Preview2 only. + /// Note: this function is here for WASI 0.2 only. /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) subscribe: func() -> pollable; } } diff --git a/wit/deps/sockets/world.wit b/wit/deps/sockets/world.wit index f8bb92a..e86f02c 100644 --- a/wit/deps/sockets/world.wit +++ b/wit/deps/sockets/world.wit @@ -1,11 +1,19 @@ -package wasi:sockets@0.2.0; +package wasi:sockets@0.2.6; +@since(version = 0.2.0) world imports { + @since(version = 0.2.0) import instance-network; + @since(version = 0.2.0) import network; + @since(version = 0.2.0) import udp; + @since(version = 0.2.0) import udp-create-socket; + @since(version = 0.2.0) import tcp; + @since(version = 0.2.0) import tcp-create-socket; + @since(version = 0.2.0) import ip-name-lookup; } diff --git a/wit/viceroy.wit b/wit/viceroy.wit index 646dd32..7c68bd5 100644 --- a/wit/viceroy.wit +++ b/wit/viceroy.wit @@ -1,5 +1,7 @@ package fastly:viceroy; -world compute { - include fastly:api/compute; -} \ No newline at end of file +world viceroy { + include fastly:compute/custom-imports; + include fastly:adapter/adapter-imports; + include fastly:compute/custom-exports; +} From 08afbdf7f83522dc707f49684fe65a5acbed07a4 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 3 Sep 2025 17:07:09 +0000 Subject: [PATCH 02/13] Constrain dependency versions more tightly --- pyproject.toml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6ec9ed9..293e8fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,11 +11,11 @@ dependencies = [ [project.optional-dependencies] test = [ - "pytest>=7.0.0", - "requests>=2.28.0", + "pytest (>=8.4.0,<9.0.0)", + "requests (>=2.32.5,<3.0.0)", ] dev = [ - "ruff>=0.1.0", + "ruff (>=0.12.11,<0.13.0)", ] [tool.pytest.ini_options] @@ -57,7 +57,10 @@ skip-magic-trailing-comma = false line-ending = "auto" [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = [ + "setuptools (>=80.9.0,<81.0.0)", + "wheel (>=0.45.0,<0.46.0)" +] build-backend = "setuptools.build_meta" [tool.setuptools] From 985b2932886c67845ab3235a8a928cc7c1b6a0f3 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 3 Sep 2025 17:07:35 +0000 Subject: [PATCH 03/13] tests: avoid using the default viceroy port Find an available port and use that for a given test run to avoid interference with other running viceroy instances or services on the local machine. --- tests/test_integration.py | 44 +++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 07d92da..67415ab 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,20 +1,38 @@ """Integration tests for the Fastly Compute Python service.""" +import socket import subprocess import time +from dataclasses import dataclass from pathlib import Path import pytest import requests +@dataclass +class ViceroyServer: + """Represents a running viceroy server instance.""" + + process: subprocess.Popen + base_url: str + + @pytest.mark.integration class TestFastlyComputeService: """Integration tests for the Fastly Compute service running under viceroy.""" - BASE_URL = "http://127.0.0.1:7676" REQUEST_TIMEOUT = 10 + @staticmethod + def _find_free_port() -> int: + """Find an available port on localhost.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('', 0)) + s.listen(1) + port = s.getsockname()[1] + return port + @pytest.fixture(scope="class", autouse=True) def build_service(self): """Build the WebAssembly component before running tests.""" @@ -33,9 +51,13 @@ def viceroy_server(self): """Start viceroy server for the duration of the test class.""" print("Starting viceroy server...") - # Start viceroy in the background + # Find an available port + port = self._find_free_port() + base_url = f"http://127.0.0.1:{port}" + + # Start viceroy in the background with the specific port process = subprocess.Popen( - ["make", "serve"], + ["viceroy", "serve", "app.wasm", "--addr", f"127.0.0.1:{port}"], cwd=Path(__file__).parent.parent, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -51,7 +73,7 @@ def viceroy_server(self): stdout, stderr = process.communicate() pytest.fail(f"Viceroy failed to start: {stderr}") - yield process + yield ViceroyServer(process=process, base_url=base_url) # Cleanup: terminate the process print("Stopping viceroy server...") @@ -61,27 +83,27 @@ def viceroy_server(self): except subprocess.TimeoutExpired: process.kill() - def _get(self, path: str) -> requests.Response: + def _get(self, path: str, server: ViceroyServer) -> requests.Response: """Make a GET request to the service.""" - return requests.get(f"{self.BASE_URL}{path}", timeout=self.REQUEST_TIMEOUT) + return requests.get(f"{server.base_url}{path}", timeout=self.REQUEST_TIMEOUT) def test_hello_endpoint(self, viceroy_server): """Test the hello endpoint returns expected content.""" - response = self._get("/hello/test") + response = self._get("/hello/test", viceroy_server) assert response.status_code == 200 assert response.text == "Hello test!" def test_hello_endpoint_with_different_name(self, viceroy_server): """Test the hello endpoint with a different name parameter.""" - response = self._get("/hello/world") + response = self._get("/hello/world", viceroy_server) assert response.status_code == 200 assert response.text == "Hello world!" def test_info_endpoint(self, viceroy_server): """Test the info endpoint returns expected JSON with WIT data.""" - response = self._get("/info") + response = self._get("/info", viceroy_server) assert response.status_code == 200 assert response.headers.get("content-type", "").startswith("application/json") @@ -103,7 +125,7 @@ def test_info_endpoint(self, viceroy_server): def test_nonexistent_endpoint(self, viceroy_server): """Test that nonexistent endpoints return 404.""" - response = self._get("/nonexistent") + response = self._get("/nonexistent", viceroy_server) assert response.status_code == 404 @@ -111,7 +133,7 @@ def test_service_health(self, viceroy_server): """Test that the service is healthy and responsive.""" # Make multiple requests to ensure stability for _ in range(3): - response = self._get("/info") + response = self._get("/info", viceroy_server) assert response.status_code == 200 data = response.json() From 090ca4fd375492bc7c0b88512da09ab74bfbcf7f Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 3 Sep 2025 17:18:07 +0000 Subject: [PATCH 04/13] tests: removed fixed blocking time for viceroy startup We key off extra output included when running the server in verbose mode to know when viceroy is ready. --- tests/test_integration.py | 68 ++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 67415ab..f8a1559 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,9 +2,11 @@ import socket import subprocess +import threading import time from dataclasses import dataclass from pathlib import Path +from queue import Queue, Empty import pytest import requests @@ -28,7 +30,7 @@ class TestFastlyComputeService: def _find_free_port() -> int: """Find an available port on localhost.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) + s.bind(("", 0)) s.listen(1) port = s.getsockname()[1] return port @@ -55,23 +57,65 @@ def viceroy_server(self): port = self._find_free_port() base_url = f"http://127.0.0.1:{port}" - # Start viceroy in the background with the specific port + # Start viceroy in the background with the specific port and verbose output process = subprocess.Popen( - ["viceroy", "serve", "app.wasm", "--addr", f"127.0.0.1:{port}"], + ["viceroy", "serve", "app.wasm", "--addr", f"127.0.0.1:{port}", "-v"], cwd=Path(__file__).parent.parent, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stderr=subprocess.STDOUT, # Redirect stderr to stdout for easier monitoring text=True, ) - # Wait for server to start - # TODO: key off some other signal or logs to speed this up... - time.sleep(10) - - # Check if process is still running - if process.poll() is not None: - stdout, stderr = process.communicate() - pytest.fail(f"Viceroy failed to start: {stderr}") + # Use a queue to communicate between threads; this extra work is mostly + # so that we have a solution that will work well enough on windows. + output_queue = Queue() + + def read_output(): + """Read process output in a separate thread.""" + try: + for line in iter(process.stdout.readline, ""): + output_queue.put(line.strip()) + if not line: + break + except Exception: + pass + + # Start the output reading thread + output_thread = threading.Thread(target=read_output, daemon=True) + output_thread.start() + + # Wait for server to start by monitoring output + timeout = 15 # Maximum wait time in seconds + start_time = time.time() + server_ready = False + + while time.time() - start_time < timeout: + # Check if process is still running + if process.poll() is not None: + # Collect any remaining output + remaining_lines = [] + try: + while True: + line = output_queue.get_nowait() + remaining_lines.append(line) + except Empty: + pass + all_output = "\n".join(remaining_lines) + pytest.fail(f"Viceroy failed to start. Output:\n{all_output}") + + # Check for output indicating server is ready + try: + line = output_queue.get(timeout=0.1) + if "Listening on" in line: + print(f"Server ready: {line}") + server_ready = True + break + except Empty: + continue + + if not server_ready: + process.terminate() + pytest.fail(f"Viceroy server did not start within {timeout} seconds") yield ViceroyServer(process=process, base_url=base_url) From add9eaf888fcacc93b481af6965a4e6971c88e8c Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 3 Sep 2025 17:26:24 +0000 Subject: [PATCH 05/13] Cleanup app and tests A lot of the extra stuff we had for logging exceptions and such aren't required any longer as the WASI bits are working as they ought to, so we can gut all of that. Also, fixed vcpu_time which does return an integer number of milliseconds in the viceroy impl, so assert on that. --- app.py | 54 +++------------------------------------ tests/test_integration.py | 12 +-------- 2 files changed, 4 insertions(+), 62 deletions(-) diff --git a/app.py b/app.py index 50c46a4..a8932d0 100644 --- a/app.py +++ b/app.py @@ -2,12 +2,11 @@ from bottle import Bottle from wit_world.exports import HttpIncoming as BaseHttpIncoming -from wit_world.imports import http_body, http_resp, log +from wit_world.imports import compute_runtime, http_body, http_resp, log from wit_world.imports.http_resp import send_downstream # Enable a bit more debug logging from the framework. app = Bottle() -app.catchall = False # bottle backtrace causes issues; use our own. @app.route("/hello/") @@ -21,13 +20,7 @@ def info(): from bottle import request # Get some runtime info we can test - vcpu_time = None - try: - from wit_world.imports import compute_runtime - - vcpu_time = compute_runtime.get_vcpu_ms() - except Exception: - pass + vcpu_time = compute_runtime.get_vcpu_ms() return { "service": "fastly-compute-python", @@ -39,18 +32,6 @@ def info(): } -def print(*args): - # hack to allow print locally; so far, monkeypatching - # sys.stdout/sys.stderr hasn't panned out, so more - # research required. - log_ep.write(" ".join(args).encode()) - - -def init(): - global log_ep - log_ep = log.Endpoint.get("") - - class StdErr: """File-like object to receive errors and direct them to our logging endpoint""" @@ -98,33 +79,4 @@ def start_response(status: str, headers: list[tuple], exc_info=None): class HttpIncoming(BaseHttpIncoming): def handle(self, request, body): - init() - try: - serve_wsgi_request(request, body, app) - except Exception as e: - log_exception(e) - - -def log_exception(e): - """Pretty-print an exception to our logging endpoint. - - Do it without callling format_exc(), which calls stat() to determine whether - we're in a tty and what its width is. stat() and other fd routines currently - crash when they try to access stdout or stderr, probably because they are - not in the preopens. - """ - try: - print(f"Exception {type(e).__name__} - {e}") - print("--- Traceback Follows ---") - - current_tb = e.__traceback__ - while current_tb: - frame = current_tb.tb_frame - print( - f" File: {frame.f_code.co_filename}, " - f"Function: {frame.f_code.co_name}, " - f"Line: {frame.f_lineno}" - ) - current_tb = current_tb.tb_next - except Exception as e2: - print(f"print_exc failed {e2}") + serve_wsgi_request(request, body, app) diff --git a/tests/test_integration.py b/tests/test_integration.py index f8a1559..eb8179e 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -161,7 +161,7 @@ def test_info_endpoint(self, viceroy_server): # Check WIT API data assert "vcpu_time_ms" in data - assert isinstance(data["vcpu_time_ms"], int | type(None)) + assert isinstance(data["vcpu_time_ms"], int) # Check request data assert data["request_method"] == "GET" @@ -172,13 +172,3 @@ def test_nonexistent_endpoint(self, viceroy_server): response = self._get("/nonexistent", viceroy_server) assert response.status_code == 404 - - def test_service_health(self, viceroy_server): - """Test that the service is healthy and responsive.""" - # Make multiple requests to ensure stability - for _ in range(3): - response = self._get("/info", viceroy_server) - assert response.status_code == 200 - - data = response.json() - assert data["status"] == "ok" From a2349828b2fbc991ff26c21bbd8cb41271faf549 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 3 Sep 2025 18:12:20 +0000 Subject: [PATCH 06/13] Fix lint errors and add ruff --fix helper --- Makefile | 3 +++ app.py | 2 +- tests/test_integration.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1341be9..95ed677 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,9 @@ test: lint: uv run --extra dev ruff check . +lint-fix: + uv run --extra dev ruf check --fix . + format: uv run --extra dev ruff format . diff --git a/app.py b/app.py index a8932d0..e633495 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,7 @@ from bottle import Bottle from wit_world.exports import HttpIncoming as BaseHttpIncoming -from wit_world.imports import compute_runtime, http_body, http_resp, log +from wit_world.imports import compute_runtime, http_body, http_resp from wit_world.imports.http_resp import send_downstream # Enable a bit more debug logging from the framework. diff --git a/tests/test_integration.py b/tests/test_integration.py index eb8179e..8adce18 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,7 +6,7 @@ import time from dataclasses import dataclass from pathlib import Path -from queue import Queue, Empty +from queue import Empty, Queue import pytest import requests From ef3406b5d74de498c9ac3d4db219b25d241e6ad9 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 3 Sep 2025 18:12:32 +0000 Subject: [PATCH 07/13] Use sys.stderror directly for wsgi.errors --- app.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app.py b/app.py index e633495..4d6a4e4 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,4 @@ +import sys from urllib.parse import urlparse from bottle import Bottle @@ -32,16 +33,6 @@ def info(): } -class StdErr: - """File-like object to receive errors and direct them to our logging endpoint""" - - def write(self, data: str): - print(f"wsgi-error: {data}") - - def flush(self): - pass - - def serve_wsgi_request(req, body, app): """Pass a WSGI application a single request, and adapt its behavior back to the Fastly API.""" @@ -68,7 +59,7 @@ def start_response(status: str, headers: list[tuple], exc_info=None): "QUERY_STRING": url.query, "SERVER_NAME": url.hostname, "SERVER_PORT": str(url.port), - "wsgi.errors": StdErr(), + "wsgi.errors": sys.stderr, } for body_chunk in app(environ, start_response): # TODO: this would be a good place to stream, but for now we just From 5a9ef31d85208cce08c96709b9688cf617b26d34 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 3 Sep 2025 20:32:55 +0000 Subject: [PATCH 08/13] testing: export pytest viceroy helpers as part of sdk We are using these helpers internally and it probably makes sense to have some sort of easier setup for end users. This is the first stab at making that available. Along with those changes, I also incorporated a few chanes to reduce the code for viceroy setup using asyncio (still a bit verbose but is working) and a few other small tweaks. The plugin to get Viceroy output on failure is a hack and we may scrap or have to revisit other approaches (e.g. just immediately outputting to host stdout/stderr) but it is functional and may be a reasonable approach (if ugly in implementation). --- Makefile | 4 +- app.py | 6 + fastly_compute/__init__.py | 7 + fastly_compute/pytest_plugin.py | 31 +++ fastly_compute/testing.py | 352 ++++++++++++++++++++++++++++++++ tests/README.md | 91 +++++++++ tests/__init__.py | 1 + tests/conftest.py | 4 + tests/test_app.py | 74 +++++++ tests/test_integration.py | 174 ---------------- tests/test_testing.py | 110 ++++++++++ 11 files changed, 678 insertions(+), 176 deletions(-) create mode 100644 fastly_compute/__init__.py create mode 100644 fastly_compute/pytest_plugin.py create mode 100644 fastly_compute/testing.py create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_app.py delete mode 100644 tests/test_integration.py create mode 100644 tests/test_testing.py diff --git a/Makefile b/Makefile index 95ed677..406eec7 100644 --- a/Makefile +++ b/Makefile @@ -10,14 +10,14 @@ app.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit app.py serve: app.wasm viceroy serve app.wasm -test: +test: app.wasm uv run --extra test pytest -m integration lint: uv run --extra dev ruff check . lint-fix: - uv run --extra dev ruf check --fix . + uv run --extra dev ruff check --fix . format: uv run --extra dev ruff format . diff --git a/app.py b/app.py index 4d6a4e4..d564a76 100644 --- a/app.py +++ b/app.py @@ -33,6 +33,12 @@ def info(): } +@app.route("/error") +def error(): + """Endpoint that intentionally raises an exception to test error handling.""" + raise RuntimeError("This is an intentional error for testing purposes") + + def serve_wsgi_request(req, body, app): """Pass a WSGI application a single request, and adapt its behavior back to the Fastly API.""" diff --git a/fastly_compute/__init__.py b/fastly_compute/__init__.py new file mode 100644 index 0000000..84534fc --- /dev/null +++ b/fastly_compute/__init__.py @@ -0,0 +1,7 @@ +"""Fastly Compute SDK for Python. + +This package provides a Python SDK for building Fastly Compute services. +""" + +# Testing utilities are available but not imported by default +# Users can import them explicitly: from fastly_compute.testing import ViceroyTestBase diff --git a/fastly_compute/pytest_plugin.py b/fastly_compute/pytest_plugin.py new file mode 100644 index 0000000..16b54cf --- /dev/null +++ b/fastly_compute/pytest_plugin.py @@ -0,0 +1,31 @@ +"""Pytest plugin for automatic viceroy output on test failures.""" + +import sys + +import pytest + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + """Hook to display viceroy output on test failure. + + This hook automatically displays recent viceroy server output + when any test fails, making debugging easier. + """ + outcome = yield + rep = outcome.get_result() + + # Only show output on test failures during the call phase + if rep.when == "call" and rep.failed: + # Try to get the viceroy_server fixture from the test + if hasattr(item, "funcargs") and "viceroy_server" in item.funcargs: + server = item.funcargs["viceroy_server"] + if hasattr(server, "output_lines"): + print( + f"\n=== Viceroy output for failed test: {item.name} ===", + file=sys.stderr, + ) + # Show last 15 lines of output + for line in server.output_lines[-15:]: + print(f" {line}", file=sys.stderr) + print("=== End viceroy output ===", file=sys.stderr) diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py new file mode 100644 index 0000000..2a260d2 --- /dev/null +++ b/fastly_compute/testing.py @@ -0,0 +1,352 @@ +"""Testing utilities for Fastly Compute integration tests. + +This module provides pytest fixtures and base classes for testing +Fastly Compute services with viceroy. + +To enable automatic viceroy output on test failures, add this to your conftest.py: + + pytest_plugins = ["fastly_compute.pytest_plugin"] +""" + +import asyncio +import socket +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +import pytest +import requests + + +@dataclass +class ViceroyServer: + """Represents a running viceroy server instance.""" + + process: subprocess.Popen + base_url: str + output_lines: list[str] # Capture output for debugging + + +class ViceroyTestBase: + """Base class for viceroy integration tests. + + Provides common functionality for testing Fastly Compute services. + Inherit from this class and use the viceroy_server fixture. + + Note: This assumes your WASM file is already built. Use your build system + (e.g., Makefile) to ensure the WASM file is up to date before running tests. + + Example: + ```python + import pytest + from fastly_compute.testing import ViceroyTestBase + + @pytest.mark.integration + class TestMyService(ViceroyTestBase): + def test_my_endpoint(self, viceroy_server): + response = self.get("/my-endpoint", viceroy_server) + assert response.status_code == 200 + ``` + """ + + REQUEST_TIMEOUT = 10 + WASM_FILE = "app.wasm" # Override this in subclasses if needed + + @staticmethod + def _find_free_port() -> int: + """Find an available port on localhost.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + port = s.getsockname()[1] + return port + + @pytest.fixture(scope="class") + def viceroy_server(self) -> ViceroyServer: + """Start viceroy server for the duration of the test class. + + Note: This assumes the WASM file already exists. Use your build system + to ensure it's built before running tests. + + Returns: + ViceroyServer: Server instance with process, base_url, and captured output + """ + print("Starting viceroy server...") + + # Check if WASM file exists + wasm_path = Path(self.WASM_FILE) + if not wasm_path.exists(): + pytest.fail( + f"WASM file '{self.WASM_FILE}' not found. Please build it first." + ) + + # Find an available port + port = self._find_free_port() + base_url = f"http://127.0.0.1:{port}" + output_lines = [] # Capture all output for debugging + + # Start viceroy process + process = subprocess.Popen( + ["viceroy", "serve", self.WASM_FILE, "--addr", f"127.0.0.1:{port}", "-v"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + async def wait_for_ready(): + """Monitor process output for readiness signal.""" + timeout = 15 # Maximum wait time in seconds + + async def read_lines(): + """Async generator to read lines from stdout.""" + loop = asyncio.get_event_loop() + while True: + # Read line in a thread to avoid blocking + line = await loop.run_in_executor(None, process.stdout.readline) + if not line: # EOF + break + line_stripped = line.strip() + output_lines.append(line_stripped) # Capture for debugging + yield line_stripped + + try: + async with asyncio.timeout(timeout): + async for line in read_lines(): + if "Listening on" in line: + print(f"Server ready: {line}") + return + # Check if process died + if process.poll() is not None: + raise RuntimeError( + "Viceroy process ended without starting server" + ) + + # If we get here, process ended without "Listening on" + raise RuntimeError("Viceroy process ended without starting server") + + except TimeoutError as e: + process.terminate() + process.wait() + raise RuntimeError( + f"Viceroy server did not start within {timeout} seconds" + ) from e + + # Wait for server to be ready + try: + asyncio.run(wait_for_ready()) + except RuntimeError as e: + # Print captured output to stderr for debugging + print(f"\nViceroy startup failed: {e}", file=sys.stderr) + print("Viceroy output:", file=sys.stderr) + for line in output_lines: + print(f" {line}", file=sys.stderr) + pytest.fail(str(e)) + + server = ViceroyServer( + process=process, base_url=base_url, output_lines=output_lines + ) + + yield server + + # Cleanup: terminate the process + print("Stopping viceroy server...") + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + def get(self, path: str, server: ViceroyServer, **kwargs) -> requests.Response: + """Make a GET request to the viceroy server. + + Args: + path: URL path to request + server: ViceroyServer instance from fixture + **kwargs: Additional arguments passed to requests.get() + + Returns: + requests.Response: The HTTP response + """ + try: + timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) + return requests.get(f"{server.base_url}{path}", timeout=timeout, **kwargs) + except Exception as e: + # On request failure, print viceroy output for debugging + print(f"\nRequest to {server.base_url}{path} failed: {e}", file=sys.stderr) + print("Recent viceroy output:", file=sys.stderr) + # Show last 20 lines of output + for line in server.output_lines[-20:]: + print(f" {line}", file=sys.stderr) + raise + + def post(self, path: str, server: ViceroyServer, **kwargs) -> requests.Response: + """Make a POST request to the viceroy server. + + Args: + path: URL path to request + server: ViceroyServer instance from fixture + **kwargs: Additional arguments passed to requests.post() + + Returns: + requests.Response: The HTTP response + """ + try: + timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) + return requests.post(f"{server.base_url}{path}", timeout=timeout, **kwargs) + except Exception as e: + # On request failure, print viceroy output for debugging + print( + f"\nPOST request to {server.base_url}{path} failed: {e}", + file=sys.stderr, + ) + print("Recent viceroy output:", file=sys.stderr) + # Show last 20 lines of output + for line in server.output_lines[-20:]: + print(f" {line}", file=sys.stderr) + raise + + def request( + self, method: str, path: str, server: ViceroyServer, **kwargs + ) -> requests.Response: + """Make an HTTP request to the viceroy server. + + Args: + method: HTTP method (GET, POST, PUT, DELETE, etc.) + path: URL path to request + server: ViceroyServer instance from fixture + **kwargs: Additional arguments passed to requests.request() + + Returns: + requests.Response: The HTTP response + """ + try: + timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) + return requests.request( + method, f"{server.base_url}{path}", timeout=timeout, **kwargs + ) + except Exception as e: + # On request failure, print viceroy output for debugging + print( + f"\n{method} request to {server.base_url}{path} failed: {e}", + file=sys.stderr, + ) + print("Recent viceroy output:", file=sys.stderr) + # Show last 20 lines of output + for line in server.output_lines[-20:]: + print(f" {line}", file=sys.stderr) + raise + + +def create_viceroy_server_fixture( + wasm_file: str = "app.wasm", scope: str = "class", timeout: int = 15 +): + """Factory function to create a viceroy server fixture with custom settings. + + Args: + wasm_file: Name of the WASM file to serve (default: "app.wasm") + scope: Pytest fixture scope (default: "class") + timeout: Server startup timeout in seconds (default: 15) + + Returns: + pytest fixture function + + Example: + ```python + from fastly_compute.testing import create_viceroy_server_fixture + + # Custom fixture for different WASM file + my_server = create_viceroy_server_fixture("my-service.wasm") + + class TestMyService: + def test_endpoint(self, my_server): + # my_server is a ViceroyServer instance + response = requests.get(f"{my_server.base_url}/test") + assert response.status_code == 200 + ``` + """ + + @pytest.fixture(scope=scope) + def _viceroy_server_fixture(): + """Custom viceroy server fixture.""" + print(f"Starting viceroy server with {wasm_file}...") + + # Check if WASM file exists + wasm_path = Path(wasm_file) + if not wasm_path.exists(): + pytest.fail(f"WASM file '{wasm_file}' not found. Please build it first.") + + # Find an available port + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + port = s.getsockname()[1] + + base_url = f"http://127.0.0.1:{port}" + output_lines = [] + + # Start viceroy process + process = subprocess.Popen( + ["viceroy", "serve", wasm_file, "--addr", f"127.0.0.1:{port}", "-v"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + async def wait_for_ready(): + """Monitor process output for readiness signal.""" + + async def read_lines(): + """Async generator to read lines from stdout.""" + loop = asyncio.get_event_loop() + while True: + line = await loop.run_in_executor(None, process.stdout.readline) + if not line: + break + line_stripped = line.strip() + output_lines.append(line_stripped) + yield line_stripped + + try: + async with asyncio.timeout(timeout): + async for line in read_lines(): + if "Listening on" in line: + print(f"Server ready: {line}") + return + if process.poll() is not None: + raise RuntimeError( + "Viceroy process ended without starting server" + ) + raise RuntimeError("Viceroy process ended without starting server") + except TimeoutError as e: + process.terminate() + process.wait() + raise RuntimeError( + f"Viceroy server did not start within {timeout} seconds" + ) from e + + # Wait for server to be ready + try: + asyncio.run(wait_for_ready()) + except RuntimeError as e: + print(f"\nViceroy startup failed: {e}", file=sys.stderr) + print("Viceroy output:", file=sys.stderr) + for line in output_lines: + print(f" {line}", file=sys.stderr) + pytest.fail(str(e)) + + server = ViceroyServer( + process=process, base_url=base_url, output_lines=output_lines + ) + + yield server + + # Cleanup + print("Stopping viceroy server...") + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + return _viceroy_server_fixture diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..8635d96 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,91 @@ +# Fastly Compute Integration Tests + +Integration tests for Fastly Compute services using viceroy with automatic server management, dynamic port allocation, and comprehensive error handling. + +## Quick Start + +```python +import pytest +from fastly_compute.testing import ViceroyTestBase + +@pytest.mark.integration +class TestMyService(ViceroyTestBase): + def test_endpoint(self, viceroy_server): + response = self.get("/test", viceroy_server) + assert response.status_code == 200 +``` + +**Prerequisites**: WASM file must exist (handled by your build system). + +## Available Methods + +- `self.get(path, viceroy_server, **kwargs)` - GET request +- `self.post(path, viceroy_server, **kwargs)` - POST request +- `self.request(method, path, viceroy_server, **kwargs)` - Any HTTP method + +## Configuration + +```python +class TestMyService(ViceroyTestBase): + REQUEST_TIMEOUT = 30 # Custom timeout (default: 10s) + WASM_FILE = "my-service.wasm" # Custom WASM file (default: "app.wasm") +``` + +## Advanced Usage + +For custom fixtures: + +```python +from fastly_compute.testing import create_viceroy_server_fixture + +my_server = create_viceroy_server_fixture("custom.wasm") + +class TestCustomService: + def test_endpoint(self, my_server): + response = requests.get(f"{my_server.base_url}/test") + assert response.status_code == 200 +``` + +## Running Tests + +```bash +make test # Build and run tests +pytest -m integration # Run integration tests only +pytest -m integration -v -s # Verbose output with viceroy logs +``` + +## Error Handling + +The framework provides comprehensive error handling: + +- **Missing WASM**: Clear error message if WASM file doesn't exist +- **Startup failures**: Viceroy output displayed automatically +- **Request failures**: Recent viceroy logs shown on HTTP errors +- **Test failures**: Automatic viceroy output display (requires plugin setup) + +### Enabling Automatic Viceroy Output + +To get viceroy logs automatically displayed on test failures, add this to your `conftest.py`: + +```python +pytest_plugins = ["fastly_compute.pytest_plugin"] +``` + +This enables automatic display of recent viceroy server output whenever a test fails, making debugging much easier. + +## Build Integration + +Let your build system handle WASM building: + +```makefile +test: app.wasm + pytest -m integration + +app.wasm: app.py + componentize-py componentize app -o app.wasm +``` + +## Examples + +- `test_app.py` - Tests for the Fastly Compute service functionality (app.wasm) +- `test_testing.py` - Tests for the viceroy testing framework itself \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..57a0bbb --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for Fastly Compute integration tests.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5f52b47 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,4 @@ +"""Pytest configuration for Fastly Compute integration tests.""" + +# Enable the fastly_compute pytest plugin for automatic viceroy output on failures +pytest_plugins = ["fastly_compute.pytest_plugin"] diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..09aa854 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,74 @@ +"""Tests for the Fastly Compute Python service (app.wasm functionality).""" + +import pytest + +from fastly_compute.testing import ViceroyTestBase + + +@pytest.mark.integration +class TestFastlyComputeApp(ViceroyTestBase): + """Integration tests for the Fastly Compute service functionality.""" + + def test_hello_endpoint(self, viceroy_server): + """Test the hello endpoint returns expected content.""" + response = self.get("/hello/test", viceroy_server) + + assert response.status_code == 200 + assert response.text == "Hello test!" + + def test_hello_endpoint_with_different_name(self, viceroy_server): + """Test the hello endpoint with a different name parameter.""" + response = self.get("/hello/world", viceroy_server) + + assert response.status_code == 200 + assert response.text == "Hello world!" + + def test_info_endpoint(self, viceroy_server): + """Test the info endpoint returns expected JSON with WIT data.""" + response = self.get("/info", viceroy_server) + + assert response.status_code == 200 + assert response.headers.get("content-type", "").startswith("application/json") + + data = response.json() + + # Check basic service info + assert data["service"] == "fastly-compute-python" + assert data["status"] == "ok" + assert "message" in data + + # Check WIT API data + assert "vcpu_time_ms" in data + assert isinstance(data["vcpu_time_ms"], int) + + # Check request data + assert data["request_method"] == "GET" + assert data["path_info"] == "/info" + + def test_nonexistent_endpoint(self, viceroy_server): + """Test that nonexistent endpoints return 404.""" + response = self.get("/nonexistent", viceroy_server) + + assert response.status_code == 404 + + def test_post_request_handling(self, viceroy_server): + """Test that POST requests are handled correctly.""" + # Current app.py doesn't handle POST to /api/data, so expect 404 + response = self.post("/api/data", viceroy_server, json={"key": "value"}) + assert response.status_code == 404 + + def test_custom_headers(self, viceroy_server): + """Test requests with custom headers are processed.""" + headers = {"X-Custom-Header": "test-value"} + response = self.get("/info", viceroy_server, headers=headers) + assert response.status_code == 200 + + def test_error_endpoint_handling(self, viceroy_server): + """Test that the error endpoint returns 500 and triggers viceroy output display.""" + response = self.get("/error", viceroy_server) + + # The endpoint should return a 500 error due to the exception + assert response.status_code == 500 + + # This test also serves to verify that the built-in hook works + # If this test fails, we should see viceroy output in the test results diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100644 index 8adce18..0000000 --- a/tests/test_integration.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Integration tests for the Fastly Compute Python service.""" - -import socket -import subprocess -import threading -import time -from dataclasses import dataclass -from pathlib import Path -from queue import Empty, Queue - -import pytest -import requests - - -@dataclass -class ViceroyServer: - """Represents a running viceroy server instance.""" - - process: subprocess.Popen - base_url: str - - -@pytest.mark.integration -class TestFastlyComputeService: - """Integration tests for the Fastly Compute service running under viceroy.""" - - REQUEST_TIMEOUT = 10 - - @staticmethod - def _find_free_port() -> int: - """Find an available port on localhost.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("", 0)) - s.listen(1) - port = s.getsockname()[1] - return port - - @pytest.fixture(scope="class", autouse=True) - def build_service(self): - """Build the WebAssembly component before running tests.""" - print("Building WebAssembly component...") - result = subprocess.run( - ["make", "app.wasm"], - cwd=Path(__file__).parent.parent, - capture_output=True, - text=True, - ) - if result.returncode != 0: - pytest.fail(f"Failed to build service: {result.stderr}") - - @pytest.fixture(scope="class") - def viceroy_server(self): - """Start viceroy server for the duration of the test class.""" - print("Starting viceroy server...") - - # Find an available port - port = self._find_free_port() - base_url = f"http://127.0.0.1:{port}" - - # Start viceroy in the background with the specific port and verbose output - process = subprocess.Popen( - ["viceroy", "serve", "app.wasm", "--addr", f"127.0.0.1:{port}", "-v"], - cwd=Path(__file__).parent.parent, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, # Redirect stderr to stdout for easier monitoring - text=True, - ) - - # Use a queue to communicate between threads; this extra work is mostly - # so that we have a solution that will work well enough on windows. - output_queue = Queue() - - def read_output(): - """Read process output in a separate thread.""" - try: - for line in iter(process.stdout.readline, ""): - output_queue.put(line.strip()) - if not line: - break - except Exception: - pass - - # Start the output reading thread - output_thread = threading.Thread(target=read_output, daemon=True) - output_thread.start() - - # Wait for server to start by monitoring output - timeout = 15 # Maximum wait time in seconds - start_time = time.time() - server_ready = False - - while time.time() - start_time < timeout: - # Check if process is still running - if process.poll() is not None: - # Collect any remaining output - remaining_lines = [] - try: - while True: - line = output_queue.get_nowait() - remaining_lines.append(line) - except Empty: - pass - all_output = "\n".join(remaining_lines) - pytest.fail(f"Viceroy failed to start. Output:\n{all_output}") - - # Check for output indicating server is ready - try: - line = output_queue.get(timeout=0.1) - if "Listening on" in line: - print(f"Server ready: {line}") - server_ready = True - break - except Empty: - continue - - if not server_ready: - process.terminate() - pytest.fail(f"Viceroy server did not start within {timeout} seconds") - - yield ViceroyServer(process=process, base_url=base_url) - - # Cleanup: terminate the process - print("Stopping viceroy server...") - process.terminate() - try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - process.kill() - - def _get(self, path: str, server: ViceroyServer) -> requests.Response: - """Make a GET request to the service.""" - return requests.get(f"{server.base_url}{path}", timeout=self.REQUEST_TIMEOUT) - - def test_hello_endpoint(self, viceroy_server): - """Test the hello endpoint returns expected content.""" - response = self._get("/hello/test", viceroy_server) - - assert response.status_code == 200 - assert response.text == "Hello test!" - - def test_hello_endpoint_with_different_name(self, viceroy_server): - """Test the hello endpoint with a different name parameter.""" - response = self._get("/hello/world", viceroy_server) - - assert response.status_code == 200 - assert response.text == "Hello world!" - - def test_info_endpoint(self, viceroy_server): - """Test the info endpoint returns expected JSON with WIT data.""" - response = self._get("/info", viceroy_server) - - assert response.status_code == 200 - assert response.headers.get("content-type", "").startswith("application/json") - - data = response.json() - - # Check basic service info - assert data["service"] == "fastly-compute-python" - assert data["status"] == "ok" - assert "message" in data - - # Check WIT API data - assert "vcpu_time_ms" in data - assert isinstance(data["vcpu_time_ms"], int) - - # Check request data - assert data["request_method"] == "GET" - assert data["path_info"] == "/info" - - def test_nonexistent_endpoint(self, viceroy_server): - """Test that nonexistent endpoints return 404.""" - response = self._get("/nonexistent", viceroy_server) - - assert response.status_code == 404 diff --git a/tests/test_testing.py b/tests/test_testing.py new file mode 100644 index 0000000..88cacb4 --- /dev/null +++ b/tests/test_testing.py @@ -0,0 +1,110 @@ +"""Tests for the viceroy testing framework functionality.""" + +import pytest +import requests + +from fastly_compute.testing import ViceroyTestBase, create_viceroy_server_fixture + + +@pytest.mark.integration +class TestViceroyTestingFramework(ViceroyTestBase): + """Tests that verify the testing framework itself works correctly.""" + + def test_viceroy_server_fixture_provides_server_info(self, viceroy_server): + """Test that the viceroy_server fixture provides expected attributes.""" + # Check that the fixture returns a ViceroyServer with expected attributes + assert hasattr(viceroy_server, "process") + assert hasattr(viceroy_server, "base_url") + assert hasattr(viceroy_server, "output_lines") + + # Check that base_url is properly formatted + assert viceroy_server.base_url.startswith("http://127.0.0.1:") + + # Check that output_lines contains viceroy startup output + assert len(viceroy_server.output_lines) > 0 + listening_lines = [ + line for line in viceroy_server.output_lines if "Listening on" in line + ] + assert len(listening_lines) > 0 + + def test_get_method_works(self, viceroy_server): + """Test that the get() helper method works correctly.""" + response = self.get("/info", viceroy_server) + + # Verify it returns a requests.Response object + assert isinstance(response, requests.Response) + assert response.status_code == 200 + + def test_post_method_works(self, viceroy_server): + """Test that the post() helper method works correctly.""" + response = self.post("/nonexistent", viceroy_server, json={"test": "data"}) + + # Verify it returns a requests.Response object + assert isinstance(response, requests.Response) + # POST to nonexistent endpoint should return 404 + assert response.status_code == 404 + + def test_request_method_works(self, viceroy_server): + """Test that the request() helper method works correctly.""" + response = self.request("GET", "/info", viceroy_server) + + # Verify it returns a requests.Response object + assert isinstance(response, requests.Response) + assert response.status_code == 200 + + def test_request_timeout_handling(self, viceroy_server): + """Test that request timeouts work correctly.""" + # Test that normal requests work with reasonable timeout + response = self.get("/info", viceroy_server, timeout=5.0) + assert response.status_code == 200 + + # Test that very short timeouts raise TimeoutError + with pytest.raises(requests.exceptions.ReadTimeout): + self.get("/info", viceroy_server, timeout=0.001) + + def test_custom_request_timeout_setting(self, viceroy_server): + """Test that custom REQUEST_TIMEOUT class attribute works.""" + # Temporarily change the timeout + original_timeout = self.REQUEST_TIMEOUT + self.REQUEST_TIMEOUT = 1 + + try: + # This should work with 1 second timeout + response = self.get("/info", viceroy_server) + assert response.status_code == 200 + finally: + # Restore original timeout + self.REQUEST_TIMEOUT = original_timeout + + +class TestCustomWasmFile(ViceroyTestBase): + """Test custom WASM file configuration.""" + + # Use the same WASM file but test the configuration mechanism + WASM_FILE = "app.wasm" + + def test_custom_wasm_file_attribute(self, viceroy_server): + """Test that custom WASM_FILE attribute is respected.""" + # This test verifies that the WASM_FILE attribute mechanism works + # The server should start successfully with our custom WASM file + response = self.get("/info", viceroy_server) + assert response.status_code == 200 + + +# Test the factory function +custom_server = create_viceroy_server_fixture("app.wasm") + + +class TestFactoryFunction: + """Test the create_viceroy_server_fixture factory function.""" + + def test_factory_function_creates_working_fixture(self, custom_server): + """Test that the factory function creates a working viceroy server.""" + # Check that the fixture returns a ViceroyServer with expected attributes + assert hasattr(custom_server, "process") + assert hasattr(custom_server, "base_url") + assert hasattr(custom_server, "output_lines") + + # Test that we can make requests to it + response = requests.get(f"{custom_server.base_url}/info") + assert response.status_code == 200 From 2bc81e968971a245164aa221a73b1ef0c885d568 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Thu, 4 Sep 2025 18:56:16 +0000 Subject: [PATCH 09/13] testing: fix output capturing & simplify --- fastly_compute/testing.py | 264 +++++++++----------------------------- tests/README.md | 44 +------ tests/test_testing.py | 35 +---- 3 files changed, 64 insertions(+), 279 deletions(-) diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py index 2a260d2..4c7aae6 100644 --- a/fastly_compute/testing.py +++ b/fastly_compute/testing.py @@ -8,10 +8,10 @@ pytest_plugins = ["fastly_compute.pytest_plugin"] """ -import asyncio import socket import subprocess -import sys +import threading +import time from dataclasses import dataclass from pathlib import Path @@ -84,6 +84,8 @@ def viceroy_server(self) -> ViceroyServer: port = self._find_free_port() base_url = f"http://127.0.0.1:{port}" output_lines = [] # Capture all output for debugging + output_lock = threading.Lock() + stop_capture = threading.Event() # Start viceroy process process = subprocess.Popen( @@ -93,54 +95,53 @@ def viceroy_server(self) -> ViceroyServer: text=True, ) - async def wait_for_ready(): - """Monitor process output for readiness signal.""" - timeout = 15 # Maximum wait time in seconds - - async def read_lines(): - """Async generator to read lines from stdout.""" - loop = asyncio.get_event_loop() - while True: - # Read line in a thread to avoid blocking - line = await loop.run_in_executor(None, process.stdout.readline) - if not line: # EOF - break - line_stripped = line.strip() - output_lines.append(line_stripped) # Capture for debugging - yield line_stripped - - try: - async with asyncio.timeout(timeout): - async for line in read_lines(): - if "Listening on" in line: - print(f"Server ready: {line}") - return - # Check if process died - if process.poll() is not None: - raise RuntimeError( - "Viceroy process ended without starting server" - ) - - # If we get here, process ended without "Listening on" - raise RuntimeError("Viceroy process ended without starting server") - - except TimeoutError as e: - process.terminate() - process.wait() - raise RuntimeError( - f"Viceroy server did not start within {timeout} seconds" - ) from e + # Start background thread to continuously capture output + def capture_output_thread(): + """Continuously capture viceroy output throughout test execution.""" + while not stop_capture.is_set(): + line = process.stdout.readline() + if not line: # EOF + break + with output_lock: + output_lines.append(line.strip()) + + output_thread = threading.Thread(target=capture_output_thread, daemon=True) + output_thread.start() # Wait for server to be ready - try: - asyncio.run(wait_for_ready()) - except RuntimeError as e: - # Print captured output to stderr for debugging - print(f"\nViceroy startup failed: {e}", file=sys.stderr) - print("Viceroy output:", file=sys.stderr) - for line in output_lines: - print(f" {line}", file=sys.stderr) - pytest.fail(str(e)) + timeout = 15 + start_time = time.monotonic() + server_ready = False + + while time.monotonic() - start_time < timeout: + if process.poll() is not None: + # Process died, collect output and fail + stop_capture.set() + time.sleep(0.1) # Give thread time to capture final output + with output_lock: + all_output = "\n".join(output_lines) + pytest.fail(f"Viceroy failed to start. Output:\n{all_output}") + + # Check if we've seen the "Listening on" message + with output_lock: + for line in output_lines: + if "Listening on" in line: + print(f"Server ready: {line}") + server_ready = True + break + + if server_ready: + break + + if not server_ready: + stop_capture.set() + process.terminate() + process.wait() + with output_lock: + all_output = "\n".join(output_lines) + pytest.fail( + f"Viceroy server did not start within {timeout} seconds. Output:\n{all_output}" + ) server = ViceroyServer( process=process, base_url=base_url, output_lines=output_lines @@ -148,8 +149,9 @@ async def read_lines(): yield server - # Cleanup: terminate the process + # Cleanup: stop output capture and terminate the process print("Stopping viceroy server...") + stop_capture.set() process.terminate() try: process.wait(timeout=5) @@ -168,17 +170,9 @@ def get(self, path: str, server: ViceroyServer, **kwargs) -> requests.Response: Returns: requests.Response: The HTTP response """ - try: - timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) - return requests.get(f"{server.base_url}{path}", timeout=timeout, **kwargs) - except Exception as e: - # On request failure, print viceroy output for debugging - print(f"\nRequest to {server.base_url}{path} failed: {e}", file=sys.stderr) - print("Recent viceroy output:", file=sys.stderr) - # Show last 20 lines of output - for line in server.output_lines[-20:]: - print(f" {line}", file=sys.stderr) - raise + timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) + response = requests.get(f"{server.base_url}{path}", timeout=timeout, **kwargs) + return response def post(self, path: str, server: ViceroyServer, **kwargs) -> requests.Response: """Make a POST request to the viceroy server. @@ -191,20 +185,9 @@ def post(self, path: str, server: ViceroyServer, **kwargs) -> requests.Response: Returns: requests.Response: The HTTP response """ - try: - timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) - return requests.post(f"{server.base_url}{path}", timeout=timeout, **kwargs) - except Exception as e: - # On request failure, print viceroy output for debugging - print( - f"\nPOST request to {server.base_url}{path} failed: {e}", - file=sys.stderr, - ) - print("Recent viceroy output:", file=sys.stderr) - # Show last 20 lines of output - for line in server.output_lines[-20:]: - print(f" {line}", file=sys.stderr) - raise + timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) + response = requests.post(f"{server.base_url}{path}", timeout=timeout, **kwargs) + return response def request( self, method: str, path: str, server: ViceroyServer, **kwargs @@ -220,133 +203,8 @@ def request( Returns: requests.Response: The HTTP response """ - try: - timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) - return requests.request( - method, f"{server.base_url}{path}", timeout=timeout, **kwargs - ) - except Exception as e: - # On request failure, print viceroy output for debugging - print( - f"\n{method} request to {server.base_url}{path} failed: {e}", - file=sys.stderr, - ) - print("Recent viceroy output:", file=sys.stderr) - # Show last 20 lines of output - for line in server.output_lines[-20:]: - print(f" {line}", file=sys.stderr) - raise - - -def create_viceroy_server_fixture( - wasm_file: str = "app.wasm", scope: str = "class", timeout: int = 15 -): - """Factory function to create a viceroy server fixture with custom settings. - - Args: - wasm_file: Name of the WASM file to serve (default: "app.wasm") - scope: Pytest fixture scope (default: "class") - timeout: Server startup timeout in seconds (default: 15) - - Returns: - pytest fixture function - - Example: - ```python - from fastly_compute.testing import create_viceroy_server_fixture - - # Custom fixture for different WASM file - my_server = create_viceroy_server_fixture("my-service.wasm") - - class TestMyService: - def test_endpoint(self, my_server): - # my_server is a ViceroyServer instance - response = requests.get(f"{my_server.base_url}/test") - assert response.status_code == 200 - ``` - """ - - @pytest.fixture(scope=scope) - def _viceroy_server_fixture(): - """Custom viceroy server fixture.""" - print(f"Starting viceroy server with {wasm_file}...") - - # Check if WASM file exists - wasm_path = Path(wasm_file) - if not wasm_path.exists(): - pytest.fail(f"WASM file '{wasm_file}' not found. Please build it first.") - - # Find an available port - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("", 0)) - port = s.getsockname()[1] - - base_url = f"http://127.0.0.1:{port}" - output_lines = [] - - # Start viceroy process - process = subprocess.Popen( - ["viceroy", "serve", wasm_file, "--addr", f"127.0.0.1:{port}", "-v"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - - async def wait_for_ready(): - """Monitor process output for readiness signal.""" - - async def read_lines(): - """Async generator to read lines from stdout.""" - loop = asyncio.get_event_loop() - while True: - line = await loop.run_in_executor(None, process.stdout.readline) - if not line: - break - line_stripped = line.strip() - output_lines.append(line_stripped) - yield line_stripped - - try: - async with asyncio.timeout(timeout): - async for line in read_lines(): - if "Listening on" in line: - print(f"Server ready: {line}") - return - if process.poll() is not None: - raise RuntimeError( - "Viceroy process ended without starting server" - ) - raise RuntimeError("Viceroy process ended without starting server") - except TimeoutError as e: - process.terminate() - process.wait() - raise RuntimeError( - f"Viceroy server did not start within {timeout} seconds" - ) from e - - # Wait for server to be ready - try: - asyncio.run(wait_for_ready()) - except RuntimeError as e: - print(f"\nViceroy startup failed: {e}", file=sys.stderr) - print("Viceroy output:", file=sys.stderr) - for line in output_lines: - print(f" {line}", file=sys.stderr) - pytest.fail(str(e)) - - server = ViceroyServer( - process=process, base_url=base_url, output_lines=output_lines + timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) + response = requests.request( + method, f"{server.base_url}{path}", timeout=timeout, **kwargs ) - - yield server - - # Cleanup - print("Stopping viceroy server...") - process.terminate() - try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - process.kill() - process.wait() - - return _viceroy_server_fixture + return response diff --git a/tests/README.md b/tests/README.md index 8635d96..519acb2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,6 +1,7 @@ # Fastly Compute Integration Tests -Integration tests for Fastly Compute services using viceroy with automatic server management, dynamic port allocation, and comprehensive error handling. +Integration tests for Fastly Compute services using viceroy with automatic +server management, dynamic port allocation, and comprehensive error handling. ## Quick Start @@ -31,21 +32,6 @@ class TestMyService(ViceroyTestBase): WASM_FILE = "my-service.wasm" # Custom WASM file (default: "app.wasm") ``` -## Advanced Usage - -For custom fixtures: - -```python -from fastly_compute.testing import create_viceroy_server_fixture - -my_server = create_viceroy_server_fixture("custom.wasm") - -class TestCustomService: - def test_endpoint(self, my_server): - response = requests.get(f"{my_server.base_url}/test") - assert response.status_code == 200 -``` - ## Running Tests ```bash @@ -54,15 +40,6 @@ pytest -m integration # Run integration tests only pytest -m integration -v -s # Verbose output with viceroy logs ``` -## Error Handling - -The framework provides comprehensive error handling: - -- **Missing WASM**: Clear error message if WASM file doesn't exist -- **Startup failures**: Viceroy output displayed automatically -- **Request failures**: Recent viceroy logs shown on HTTP errors -- **Test failures**: Automatic viceroy output display (requires plugin setup) - ### Enabling Automatic Viceroy Output To get viceroy logs automatically displayed on test failures, add this to your `conftest.py`: @@ -72,20 +49,3 @@ pytest_plugins = ["fastly_compute.pytest_plugin"] ``` This enables automatic display of recent viceroy server output whenever a test fails, making debugging much easier. - -## Build Integration - -Let your build system handle WASM building: - -```makefile -test: app.wasm - pytest -m integration - -app.wasm: app.py - componentize-py componentize app -o app.wasm -``` - -## Examples - -- `test_app.py` - Tests for the Fastly Compute service functionality (app.wasm) -- `test_testing.py` - Tests for the viceroy testing framework itself \ No newline at end of file diff --git a/tests/test_testing.py b/tests/test_testing.py index 88cacb4..ac757e1 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -3,7 +3,7 @@ import pytest import requests -from fastly_compute.testing import ViceroyTestBase, create_viceroy_server_fixture +from fastly_compute.testing import ViceroyTestBase @pytest.mark.integration @@ -75,36 +75,3 @@ def test_custom_request_timeout_setting(self, viceroy_server): finally: # Restore original timeout self.REQUEST_TIMEOUT = original_timeout - - -class TestCustomWasmFile(ViceroyTestBase): - """Test custom WASM file configuration.""" - - # Use the same WASM file but test the configuration mechanism - WASM_FILE = "app.wasm" - - def test_custom_wasm_file_attribute(self, viceroy_server): - """Test that custom WASM_FILE attribute is respected.""" - # This test verifies that the WASM_FILE attribute mechanism works - # The server should start successfully with our custom WASM file - response = self.get("/info", viceroy_server) - assert response.status_code == 200 - - -# Test the factory function -custom_server = create_viceroy_server_fixture("app.wasm") - - -class TestFactoryFunction: - """Test the create_viceroy_server_fixture factory function.""" - - def test_factory_function_creates_working_fixture(self, custom_server): - """Test that the factory function creates a working viceroy server.""" - # Check that the fixture returns a ViceroyServer with expected attributes - assert hasattr(custom_server, "process") - assert hasattr(custom_server, "base_url") - assert hasattr(custom_server, "output_lines") - - # Test that we can make requests to it - response = requests.get(f"{custom_server.base_url}/info") - assert response.status_code == 200 From 7751eebebad5b3f84deef95a5dbdd10a0c0803d7 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Thu, 4 Sep 2025 19:35:11 +0000 Subject: [PATCH 10/13] testing: remove need to pass viceroy_server around We hide the viceroy_server by making it be an autouse fixture on the class which lets us clean things up a fair bit. It stands to reason that all viceroy based tests will want a server. --- fastly_compute/testing.py | 40 +++++++++++++++++++++------------------ tests/README.md | 10 +++++----- tests/test_app.py | 28 +++++++++++++-------------- tests/test_testing.py | 38 ++++++++++++++++++------------------- 4 files changed, 60 insertions(+), 56 deletions(-) diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py index 4c7aae6..85d3bb1 100644 --- a/fastly_compute/testing.py +++ b/fastly_compute/testing.py @@ -45,13 +45,14 @@ class ViceroyTestBase: @pytest.mark.integration class TestMyService(ViceroyTestBase): def test_my_endpoint(self, viceroy_server): - response = self.get("/my-endpoint", viceroy_server) + response = self.get("/my-endpoint") assert response.status_code == 200 ``` """ REQUEST_TIMEOUT = 10 WASM_FILE = "app.wasm" # Override this in subclasses if needed + server: ViceroyServer = None # Will be set by the fixture @staticmethod def _find_free_port() -> int: @@ -61,8 +62,9 @@ def _find_free_port() -> int: port = s.getsockname()[1] return port - @pytest.fixture(scope="class") - def viceroy_server(self) -> ViceroyServer: + @pytest.fixture(scope="class", autouse=True) + @classmethod + def viceroy_server(cls) -> ViceroyServer: """Start viceroy server for the duration of the test class. Note: This assumes the WASM file already exists. Use your build system @@ -74,14 +76,14 @@ def viceroy_server(self) -> ViceroyServer: print("Starting viceroy server...") # Check if WASM file exists - wasm_path = Path(self.WASM_FILE) + wasm_path = Path(cls.WASM_FILE) if not wasm_path.exists(): pytest.fail( - f"WASM file '{self.WASM_FILE}' not found. Please build it first." + f"WASM file '{cls.WASM_FILE}' not found. Please build it first." ) # Find an available port - port = self._find_free_port() + port = cls._find_free_port() base_url = f"http://127.0.0.1:{port}" output_lines = [] # Capture all output for debugging output_lock = threading.Lock() @@ -89,7 +91,7 @@ def viceroy_server(self) -> ViceroyServer: # Start viceroy process process = subprocess.Popen( - ["viceroy", "serve", self.WASM_FILE, "--addr", f"127.0.0.1:{port}", "-v"], + ["viceroy", "serve", cls.WASM_FILE, "--addr", f"127.0.0.1:{port}", "-v"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, @@ -147,6 +149,9 @@ def capture_output_thread(): process=process, base_url=base_url, output_lines=output_lines ) + # Set the server as a class attribute so methods can access it + cls.server = server + yield server # Cleanup: stop output capture and terminate the process @@ -159,45 +164,44 @@ def capture_output_thread(): process.kill() process.wait() - def get(self, path: str, server: ViceroyServer, **kwargs) -> requests.Response: + def get(self, path: str, **kwargs) -> requests.Response: """Make a GET request to the viceroy server. Args: path: URL path to request - server: ViceroyServer instance from fixture **kwargs: Additional arguments passed to requests.get() Returns: requests.Response: The HTTP response """ timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) - response = requests.get(f"{server.base_url}{path}", timeout=timeout, **kwargs) + response = requests.get( + f"{self.server.base_url}{path}", timeout=timeout, **kwargs + ) return response - def post(self, path: str, server: ViceroyServer, **kwargs) -> requests.Response: + def post(self, path: str, **kwargs) -> requests.Response: """Make a POST request to the viceroy server. Args: path: URL path to request - server: ViceroyServer instance from fixture **kwargs: Additional arguments passed to requests.post() Returns: requests.Response: The HTTP response """ timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) - response = requests.post(f"{server.base_url}{path}", timeout=timeout, **kwargs) + response = requests.post( + f"{self.server.base_url}{path}", timeout=timeout, **kwargs + ) return response - def request( - self, method: str, path: str, server: ViceroyServer, **kwargs - ) -> requests.Response: + def request(self, method: str, path: str, **kwargs) -> requests.Response: """Make an HTTP request to the viceroy server. Args: method: HTTP method (GET, POST, PUT, DELETE, etc.) path: URL path to request - server: ViceroyServer instance from fixture **kwargs: Additional arguments passed to requests.request() Returns: @@ -205,6 +209,6 @@ def request( """ timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT) response = requests.request( - method, f"{server.base_url}{path}", timeout=timeout, **kwargs + method, f"{self.server.base_url}{path}", timeout=timeout, **kwargs ) return response diff --git a/tests/README.md b/tests/README.md index 519acb2..b9999a6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,8 +11,8 @@ from fastly_compute.testing import ViceroyTestBase @pytest.mark.integration class TestMyService(ViceroyTestBase): - def test_endpoint(self, viceroy_server): - response = self.get("/test", viceroy_server) + def test_endpoint(self): + response = self.get("/test") assert response.status_code == 200 ``` @@ -20,9 +20,9 @@ class TestMyService(ViceroyTestBase): ## Available Methods -- `self.get(path, viceroy_server, **kwargs)` - GET request -- `self.post(path, viceroy_server, **kwargs)` - POST request -- `self.request(method, path, viceroy_server, **kwargs)` - Any HTTP method +- `self.get(path, **kwargs)` - GET request +- `self.post(path, **kwargs)` - POST request +- `self.request(method, path, **kwargs)` - Any HTTP method ## Configuration diff --git a/tests/test_app.py b/tests/test_app.py index 09aa854..40d31a3 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -9,23 +9,23 @@ class TestFastlyComputeApp(ViceroyTestBase): """Integration tests for the Fastly Compute service functionality.""" - def test_hello_endpoint(self, viceroy_server): + def test_hello_endpoint(self): """Test the hello endpoint returns expected content.""" - response = self.get("/hello/test", viceroy_server) + response = self.get("/hello/test") assert response.status_code == 200 assert response.text == "Hello test!" - def test_hello_endpoint_with_different_name(self, viceroy_server): + def test_hello_endpoint_with_different_name(self): """Test the hello endpoint with a different name parameter.""" - response = self.get("/hello/world", viceroy_server) + response = self.get("/hello/world") assert response.status_code == 200 assert response.text == "Hello world!" - def test_info_endpoint(self, viceroy_server): + def test_info_endpoint(self): """Test the info endpoint returns expected JSON with WIT data.""" - response = self.get("/info", viceroy_server) + response = self.get("/info") assert response.status_code == 200 assert response.headers.get("content-type", "").startswith("application/json") @@ -45,27 +45,27 @@ def test_info_endpoint(self, viceroy_server): assert data["request_method"] == "GET" assert data["path_info"] == "/info" - def test_nonexistent_endpoint(self, viceroy_server): + def test_nonexistent_endpoint(self): """Test that nonexistent endpoints return 404.""" - response = self.get("/nonexistent", viceroy_server) + response = self.get("/nonexistent") assert response.status_code == 404 - def test_post_request_handling(self, viceroy_server): + def test_post_request_handling(self): """Test that POST requests are handled correctly.""" # Current app.py doesn't handle POST to /api/data, so expect 404 - response = self.post("/api/data", viceroy_server, json={"key": "value"}) + response = self.post("/api/data", json={"key": "value"}) assert response.status_code == 404 - def test_custom_headers(self, viceroy_server): + def test_custom_headers(self): """Test requests with custom headers are processed.""" headers = {"X-Custom-Header": "test-value"} - response = self.get("/info", viceroy_server, headers=headers) + response = self.get("/info", headers=headers) assert response.status_code == 200 - def test_error_endpoint_handling(self, viceroy_server): + def test_error_endpoint_handling(self): """Test that the error endpoint returns 500 and triggers viceroy output display.""" - response = self.get("/error", viceroy_server) + response = self.get("/error") # The endpoint should return a 500 error due to the exception assert response.status_code == 500 diff --git a/tests/test_testing.py b/tests/test_testing.py index ac757e1..2c516d6 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -10,59 +10,59 @@ class TestViceroyTestingFramework(ViceroyTestBase): """Tests that verify the testing framework itself works correctly.""" - def test_viceroy_server_fixture_provides_server_info(self, viceroy_server): + def test_viceroy_server_fixture_provides_server_info(self): """Test that the viceroy_server fixture provides expected attributes.""" - # Check that the fixture returns a ViceroyServer with expected attributes - assert hasattr(viceroy_server, "process") - assert hasattr(viceroy_server, "base_url") - assert hasattr(viceroy_server, "output_lines") + # Check that the fixture sets up a ViceroyServer with expected attributes + assert hasattr(self.server, "process") + assert hasattr(self.server, "base_url") + assert hasattr(self.server, "output_lines") # Check that base_url is properly formatted - assert viceroy_server.base_url.startswith("http://127.0.0.1:") + assert self.server.base_url.startswith("http://127.0.0.1:") # Check that output_lines contains viceroy startup output - assert len(viceroy_server.output_lines) > 0 + assert len(self.server.output_lines) > 0 listening_lines = [ - line for line in viceroy_server.output_lines if "Listening on" in line + line for line in self.server.output_lines if "Listening on" in line ] assert len(listening_lines) > 0 - def test_get_method_works(self, viceroy_server): + def test_get_method_works(self): """Test that the get() helper method works correctly.""" - response = self.get("/info", viceroy_server) + response = self.get("/info") # Verify it returns a requests.Response object assert isinstance(response, requests.Response) assert response.status_code == 200 - def test_post_method_works(self, viceroy_server): + def test_post_method_works(self): """Test that the post() helper method works correctly.""" - response = self.post("/nonexistent", viceroy_server, json={"test": "data"}) + response = self.post("/nonexistent", json={"test": "data"}) # Verify it returns a requests.Response object assert isinstance(response, requests.Response) # POST to nonexistent endpoint should return 404 assert response.status_code == 404 - def test_request_method_works(self, viceroy_server): + def test_request_method_works(self): """Test that the request() helper method works correctly.""" - response = self.request("GET", "/info", viceroy_server) + response = self.request("GET", "/info") # Verify it returns a requests.Response object assert isinstance(response, requests.Response) assert response.status_code == 200 - def test_request_timeout_handling(self, viceroy_server): + def test_request_timeout_handling(self): """Test that request timeouts work correctly.""" # Test that normal requests work with reasonable timeout - response = self.get("/info", viceroy_server, timeout=5.0) + response = self.get("/info", timeout=5.0) assert response.status_code == 200 # Test that very short timeouts raise TimeoutError with pytest.raises(requests.exceptions.ReadTimeout): - self.get("/info", viceroy_server, timeout=0.001) + self.get("/info", timeout=0.001) - def test_custom_request_timeout_setting(self, viceroy_server): + def test_custom_request_timeout_setting(self): """Test that custom REQUEST_TIMEOUT class attribute works.""" # Temporarily change the timeout original_timeout = self.REQUEST_TIMEOUT @@ -70,7 +70,7 @@ def test_custom_request_timeout_setting(self, viceroy_server): try: # This should work with 1 second timeout - response = self.get("/info", viceroy_server) + response = self.get("/info") assert response.status_code == 200 finally: # Restore original timeout From 040e3c1e3d72b1aeb0f07c6d632e139a3b53b1ad Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Fri, 5 Sep 2025 16:26:27 +0000 Subject: [PATCH 11/13] testing: drop integration test marker --- Makefile | 2 +- README.md | 2 +- fastly_compute/testing.py | 1 - pyproject.toml | 3 --- tests/README.md | 1 - tests/test_app.py | 3 --- tests/test_testing.py | 1 - 7 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 406eec7..260849f 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ serve: app.wasm viceroy serve app.wasm test: app.wasm - uv run --extra test pytest -m integration + uv run --extra test pytest lint: uv run --extra dev ruff check . diff --git a/README.md b/README.md index 4f2315e..fa6a2bb 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ uv sync --extra dev --extra test make test ``` -The integration tests automatically build the WebAssembly component, start viceroy, and verify all endpoints work correctly with the WIT APIs. +The tests automatically build the WebAssembly component, start viceroy, and verify all endpoints work correctly with the WIT APIs. # Development diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py index 85d3bb1..498e55a 100644 --- a/fastly_compute/testing.py +++ b/fastly_compute/testing.py @@ -42,7 +42,6 @@ class ViceroyTestBase: import pytest from fastly_compute.testing import ViceroyTestBase - @pytest.mark.integration class TestMyService(ViceroyTestBase): def test_my_endpoint(self, viceroy_server): response = self.get("/my-endpoint") diff --git a/pyproject.toml b/pyproject.toml index 293e8fd..317ac56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,6 @@ addopts = [ "--tb=short", "--strict-markers", ] -markers = [ - "integration: marks tests as integration tests (deselect with '-m \"not integration\"')", -] [tool.ruff] target-version = "py312" diff --git a/tests/README.md b/tests/README.md index b9999a6..968651f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,7 +9,6 @@ server management, dynamic port allocation, and comprehensive error handling. import pytest from fastly_compute.testing import ViceroyTestBase -@pytest.mark.integration class TestMyService(ViceroyTestBase): def test_endpoint(self): response = self.get("/test") diff --git a/tests/test_app.py b/tests/test_app.py index 40d31a3..73bf521 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,11 +1,8 @@ """Tests for the Fastly Compute Python service (app.wasm functionality).""" -import pytest - from fastly_compute.testing import ViceroyTestBase -@pytest.mark.integration class TestFastlyComputeApp(ViceroyTestBase): """Integration tests for the Fastly Compute service functionality.""" diff --git a/tests/test_testing.py b/tests/test_testing.py index 2c516d6..669b1fb 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -6,7 +6,6 @@ from fastly_compute.testing import ViceroyTestBase -@pytest.mark.integration class TestViceroyTestingFramework(ViceroyTestBase): """Tests that verify the testing framework itself works correctly.""" From dbd2cf3b24ee756c0e9dfd84b64d22971ad7f41d Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Fri, 5 Sep 2025 16:35:21 +0000 Subject: [PATCH 12/13] docs: fix typo and small updates --- .github/workflows/python-ci.yml | 2 +- README.md | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index f7c3965..1b1fab8 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Rust Toolchain + - name: Set Up Rust Toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: '1.86.0' diff --git a/README.md b/README.md index fa6a2bb..ca4eaf0 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,6 @@ Currently, this demonstrates… native-code compression stdlibs which haven't been compiled against WASI yet. Moving componentize-py to a new Python may help, as [WASIp1 is now a Tier 2 supported platform](https://peps.python.org/pep-0011/#tier-2). -* It crashes every time something tries to write to stdout or stderr. It may be - that those aren't in the preopens; adding those to the preopens should be - possible with changes to Viceroy. We're also using `--stub-wasi` at the - moment, which means things like `fd_write` are coded to immediately trap; that - probably doesn't help. Finally, it may be possible to monkeypatch in Python - and redirect them to a logging endpoint, but our initial attempts were - unsuccessful. # Install Dependencies @@ -66,4 +59,7 @@ make format-check # Run linting make lint + +# Run linting and apply automatic fixes +make lint-fix ``` From 7ff2a5a515cc171486812c54e847bcdfbdb95229 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Fri, 5 Sep 2025 19:28:16 +0000 Subject: [PATCH 13/13] docs: remove references to "integration" tests in a few more places --- fastly_compute/testing.py | 4 ++-- tests/README.md | 9 ++++----- tests/__init__.py | 2 +- tests/conftest.py | 2 +- tests/test_app.py | 2 -- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py index 498e55a..22164d3 100644 --- a/fastly_compute/testing.py +++ b/fastly_compute/testing.py @@ -1,4 +1,4 @@ -"""Testing utilities for Fastly Compute integration tests. +"""Testing utilities for Fastly Compute tests. This module provides pytest fixtures and base classes for testing Fastly Compute services with viceroy. @@ -29,7 +29,7 @@ class ViceroyServer: class ViceroyTestBase: - """Base class for viceroy integration tests. + """Base class for viceroy tests. Provides common functionality for testing Fastly Compute services. Inherit from this class and use the viceroy_server fixture. diff --git a/tests/README.md b/tests/README.md index 968651f..0df0cc7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,6 +1,6 @@ -# Fastly Compute Integration Tests +# Fastly Compute SDK Tests -Integration tests for Fastly Compute services using viceroy with automatic +Tests for Fastly Compute SDK using viceroy with automatic server management, dynamic port allocation, and comprehensive error handling. ## Quick Start @@ -34,9 +34,8 @@ class TestMyService(ViceroyTestBase): ## Running Tests ```bash -make test # Build and run tests -pytest -m integration # Run integration tests only -pytest -m integration -v -s # Verbose output with viceroy logs +make test # Build and run tests +uv run pytest -v -s # Verbose output with viceroy logs ``` ### Enabling Automatic Viceroy Output diff --git a/tests/__init__.py b/tests/__init__.py index 57a0bbb..6ad7227 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Test package for Fastly Compute integration tests.""" +"""Test package for Fastly Compute tests.""" diff --git a/tests/conftest.py b/tests/conftest.py index 5f52b47..748fac2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -"""Pytest configuration for Fastly Compute integration tests.""" +"""Pytest configuration for Fastly Compute tests.""" # Enable the fastly_compute pytest plugin for automatic viceroy output on failures pytest_plugins = ["fastly_compute.pytest_plugin"] diff --git a/tests/test_app.py b/tests/test_app.py index 73bf521..0bf4d30 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -4,8 +4,6 @@ class TestFastlyComputeApp(ViceroyTestBase): - """Integration tests for the Fastly Compute service functionality.""" - def test_hello_endpoint(self): """Test the hello endpoint returns expected content.""" response = self.get("/hello/test")