This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new e50646a23b fix(wasm): harden fixture verification and document
cleanup. (#7581)
e50646a23b is described below
commit e50646a23bac8029653ddcd4e2f96a3f6a40849c
Author: redyyang <[email protected]>
AuthorDate: Sat Oct 17 20:48:00 2026 +0800
fix(wasm): harden fixture verification and document cleanup. (#7581)
Signed-off-by: redyyang <[email protected]>
Co-authored-by: redyyang <[email protected]>
---
.github/scripts/test-wasm-fixtures.py | 197 +++++++++++++++++++++
.github/scripts/verify-wasm-fixtures.py | 20 +--
.github/workflows/wasm-fixtures.yml | 4 +
.../shenyu/plugin/wasm/api/loader/WasmLoader.java | 7 +
shenyu-plugin/shenyu-plugin-wasm-base/README.md | 17 +-
5 files changed, 230 insertions(+), 15 deletions(-)
diff --git a/.github/scripts/test-wasm-fixtures.py
b/.github/scripts/test-wasm-fixtures.py
new file mode 100644
index 0000000000..3bfe8d82d5
--- /dev/null
+++ b/.github/scripts/test-wasm-fixtures.py
@@ -0,0 +1,197 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Offline regression checks for fixture validation and failure cleanup."""
+
+import importlib.util
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+sys.dont_write_bytecode = True
+SPEC = importlib.util.spec_from_file_location("wasm_fixtures",
Path(__file__).with_name("verify-wasm-fixtures.py"))
+CHECK = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(CHECK)
+
+
+class WatFixtureTest(unittest.TestCase):
+ def setUp(self):
+ temporary = tempfile.TemporaryDirectory()
+ self.addCleanup(temporary.cleanup)
+ self.base = Path(temporary.name)
+ (self.base / "resources").mkdir()
+ self.api = self.base / "api"
+ (self.api / "resources").mkdir(parents=True)
+ self.outputs = {}
+ self.addCleanup(patch.stopall)
+ patch.object(CHECK, "BASE", self.base).start()
+ patch.object(CHECK, "API", self.api).start()
+ patch.object(CHECK.subprocess, "check_output",
return_value="1.0.42\n").start()
+ patch.object(CHECK, "run", side_effect=self.build).start()
+ self.family("wasm-concurrent", self.api)
+
+ def family(self, name, root=None):
+ root = self.base if root is None else root
+ (root / name).mkdir()
+ binary = name + ".wasm"
+ self.outputs[name] = {binary: b"committed fixture"}
+ (root / "resources" / binary).write_bytes(b"committed fixture")
+
+ def build(self, command):
+ family = Path(command[2]).name
+ destination = Path(command[4].split("=", 1)[1])
+ for name, content in self.outputs[family].items():
+ (destination / name).write_bytes(content)
+
+ def test_missing_gateway_is_rejected_even_when_bridge_exists(self):
+ self.family("wasm-bridge")
+ with self.assertRaisesRegex(RuntimeError, "Required WAT fixture family
missing: wasm-gateway"):
+ CHECK.verify_wat()
+
+ def test_additional_fixture_families_are_verified(self):
+ for family in ("wasm-gateway", "wasm-bridge", "wasm-future"):
+ self.family(family)
+ self.outputs["wasm-future"]["wasm-future.wasm"] = b"different binary"
+ with self.assertRaisesRegex(RuntimeError, "WAT fixture
differs.*wasm-future.wasm"):
+ CHECK.verify_wat()
+ self.assertEqual(b"committed fixture", (self.base /
"resources/wasm-future.wasm").read_bytes())
+
+ def test_empty_family_is_rejected(self):
+ self.family("wasm-gateway")
+ self.outputs["wasm-gateway"] = {}
+ with self.assertRaisesRegex(RuntimeError, "No fixtures generated for
wasm-gateway"):
+ CHECK.verify_wat()
+
+ def test_missing_concurrent_family_is_rejected(self):
+ self.family("wasm-gateway")
+ (self.api / "wasm-concurrent").rmdir()
+ with self.assertRaisesRegex(RuntimeError, "Required WAT fixture family
missing: wasm-concurrent"):
+ CHECK.verify_wat()
+
+ def test_concurrent_binary_uses_api_resources(self):
+ self.family("wasm-gateway")
+ self.outputs["wasm-concurrent"]["wasm-concurrent.wasm"] = b"different
binary"
+ with self.assertRaisesRegex(RuntimeError, "WAT fixture
differs.*wasm-concurrent.wasm"):
+ CHECK.verify_wat()
+ self.assertEqual(b"committed fixture", (self.api /
"resources/wasm-concurrent.wasm").read_bytes())
+
+ def test_additional_api_families_are_verified(self):
+ self.family("wasm-gateway")
+ self.family("wasm-future-api", self.api)
+ self.outputs["wasm-future-api"]["wasm-future-api.wasm"] = b"different
binary"
+ with self.assertRaisesRegex(RuntimeError, "WAT fixture
differs.*wasm-future-api.wasm"):
+ CHECK.verify_wat()
+
+ def test_empty_api_family_is_rejected(self):
+ self.family("wasm-gateway")
+ self.outputs["wasm-concurrent"] = {}
+ with self.assertRaisesRegex(RuntimeError, "No fixtures generated for
wasm-concurrent"):
+ CHECK.verify_wat()
+
+ def test_api_family_path_must_be_a_directory(self):
+ self.family("wasm-gateway")
+ (self.api / "wasm-invalid").write_text("not a directory",
encoding="utf-8")
+ with self.assertRaisesRegex(RuntimeError, "WAT fixture family is not a
directory: wasm-invalid"):
+ CHECK.verify_wat()
+
+ def test_matching_binaries_do_not_modify_resources(self):
+ self.family("wasm-gateway")
+ self.family("wasm-bridge")
+ CHECK.verify_wat()
+ for resource in (self.base / "resources").iterdir():
+ self.assertEqual(b"committed fixture", resource.read_bytes())
+
+ def test_family_path_must_be_a_directory(self):
+ self.family("wasm-gateway")
+ (self.base / "wasm-invalid").write_text("not a directory",
encoding="utf-8")
+ with self.assertRaisesRegex(RuntimeError, "WAT fixture family is not a
directory: wasm-invalid"):
+ CHECK.verify_wat()
+
+
+class RustFixtureTest(unittest.TestCase):
+ def setUp(self):
+ temporary = tempfile.TemporaryDirectory()
+ self.addCleanup(temporary.cleanup)
+ self.root = Path(temporary.name)
+ (self.root / "resources").mkdir()
+ self.guests = []
+ self.originals = {}
+ for index in range(2):
+ crate = "rust-guest-" + str(index)
+ source = self.root / crate
+ (source / "src").mkdir(parents=True)
+ (source / "Cargo.toml").write_text("[package]\nname = '" + crate +
"'\nversion = '0.1.0'\n", encoding="utf-8")
+ (source / "src/lib.rs").write_text("", encoding="utf-8")
+ name = crate + ".wasm"
+ resource = self.root / "resources" / name
+ original = ("original " + crate).encode()
+ resource.write_bytes(original)
+ self.originals[resource] = original
+ self.guests.append((self.root, crate, name))
+ self.builds = 0
+ self.failure = None
+ self.fail_build = False
+ self.addCleanup(patch.stopall)
+ patch.object(CHECK, "RUST_GUESTS", self.guests).start()
+ patch.object(CHECK, "run", side_effect=self.run_command).start()
+
+ def run_command(self, command, **kwargs):
+ if command[0] == "cargo":
+ self.builds += 1
+ if self.fail_build and self.builds == 2:
+ raise self.failure
+ manifest = Path(command[command.index("--manifest-path") + 1])
+ binary = manifest.parent / "target/wasm32-wasip1/release" /
(manifest.parent.name.replace("-", "_") + ".wasm")
+ binary.parent.mkdir(parents=True)
+ binary.write_bytes(b"freshly compiled guest")
+ else:
+ for resource in self.originals:
+ self.assertEqual(b"freshly compiled guest",
resource.read_bytes())
+ if self.failure:
+ raise self.failure
+
+ def assert_restored(self):
+ for resource, original in self.originals.items():
+ self.assertEqual(original, resource.read_bytes())
+
+ def test_success_restores_every_resource(self):
+ CHECK.verify_rust()
+ self.assert_restored()
+
+ def test_partial_build_failure_restores_already_replaced_resources(self):
+ self.fail_build = True
+ self.failure = subprocess.CalledProcessError(1, "cargo")
+ with self.assertRaises(subprocess.CalledProcessError):
+ CHECK.verify_rust()
+ self.assert_restored()
+
+ def test_maven_failure_restores_every_resource(self):
+ self.failure = subprocess.CalledProcessError(1, "./mvnw")
+ with self.assertRaises(subprocess.CalledProcessError):
+ CHECK.verify_rust()
+ self.assert_restored()
+
+ def test_keyboard_interrupt_restores_every_resource(self):
+ self.failure = KeyboardInterrupt()
+ with self.assertRaises(KeyboardInterrupt):
+ CHECK.verify_rust()
+ self.assert_restored()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/scripts/verify-wasm-fixtures.py
b/.github/scripts/verify-wasm-fixtures.py
index 7238e0decb..401250b6f0 100644
--- a/.github/scripts/verify-wasm-fixtures.py
+++ b/.github/scripts/verify-wasm-fixtures.py
@@ -40,24 +40,22 @@ def run(command, **kwargs):
def verify_wat():
+ for source_root, required in ((BASE, "wasm-gateway"), (API,
"wasm-concurrent")):
+ if not (source_root / required).is_dir():
+ raise RuntimeError("Required WAT fixture family missing: " +
required)
version = subprocess.check_output(["wat2wasm", "--version"],
text=True).strip()
if version != "1.0.42":
raise RuntimeError("WAT verification requires WABT 1.0.42, found " +
version)
- families = [
- (BASE, ["wasm-bridge", "wasm-gateway"]),
- (API, ["wasm-concurrent"]),
- ]
count = 0
- for source_root, names in families:
- for family in names:
- directory = source_root / family
- if not directory.exists():
- continue
+ for source_root in (BASE, API):
+ for directory in sorted(source_root.glob("wasm-*")):
+ if not directory.is_dir():
+ raise RuntimeError("WAT fixture family is not a directory: " +
directory.name)
with tempfile.TemporaryDirectory(prefix="shenyu-wat-") as
temporary:
run(["make", "-C", str(directory), "build", "RESOURCES=" +
temporary])
binaries = list(Path(temporary).glob("*.wasm"))
if not binaries:
- raise RuntimeError("No fixtures generated for " + family)
+ raise RuntimeError("No fixtures generated for " +
directory.name)
for rebuilt in binaries:
committed = source_root / "resources" / rebuilt.name
if rebuilt.read_bytes() != committed.read_bytes():
@@ -79,7 +77,7 @@ def verify_rust():
shutil.copyfile(source / "Cargo.toml", copied / "Cargo.toml")
shutil.copytree(source / "src", copied / "src")
run(["cargo", "+" + RUST_TOOLCHAIN, "build",
"--manifest-path", str(copied / "Cargo.toml"),
- "--target", "wasm32-wasip1", "--release", "--offline"])
+ "--target", "wasm32-wasip1", "--release"])
binary = copied / "target/wasm32-wasip1/release" /
(crate.replace("-", "_") + ".wasm")
resource = source_root / "resources" / resource_name
originals[resource] = resource.read_bytes()
diff --git a/.github/workflows/wasm-fixtures.yml
b/.github/workflows/wasm-fixtures.yml
index 0923070470..424c8ff067 100644
--- a/.github/workflows/wasm-fixtures.yml
+++ b/.github/workflows/wasm-fixtures.yml
@@ -21,6 +21,7 @@ on:
- 'shenyu-plugin/shenyu-plugin-wasm-api/**'
- 'shenyu-plugin/shenyu-plugin-wasm-base/**'
- '.github/scripts/verify-wasm-fixtures.py'
+ - '.github/scripts/test-wasm-fixtures.py'
- '.github/workflows/wasm-fixtures.yml'
- 'pom.xml'
push:
@@ -29,6 +30,7 @@ on:
- 'shenyu-plugin/shenyu-plugin-wasm-api/**'
- 'shenyu-plugin/shenyu-plugin-wasm-base/**'
- '.github/scripts/verify-wasm-fixtures.py'
+ - '.github/scripts/test-wasm-fixtures.py'
- '.github/workflows/wasm-fixtures.yml'
- 'pom.xml'
@@ -45,6 +47,8 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
+ - name: Test fixture verification failures and resource restoration
+ run: python3 .github/scripts/test-wasm-fixtures.py
- uses: ./actions/setup-java-with-retry
with:
java-version: "17"
diff --git
a/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
b/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
index 2871ba67f8..014c6ae167 100644
---
a/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
+++
b/shenyu-plugin/shenyu-plugin-wasm-api/src/main/java/org/apache/shenyu/plugin/wasm/api/loader/WasmLoader.java
@@ -375,6 +375,13 @@ public class WasmLoader implements AutoCloseable {
return findWasmExtern(funcName);
}
+ /**
+ * Check export names before resolving a function so absent hooks remain
optional
+ * while a matching export of the wrong kind still fails validation.
+ *
+ * @param funcName the exported function name
+ * @return the exported function, or empty only when the name is absent
+ */
private Optional<ExportFunction> findWasmExtern(final String funcName) {
if (findExport(primary, funcName).isPresent()) {
return Optional.of(args -> executeOnPool(funcName, args));
diff --git a/shenyu-plugin/shenyu-plugin-wasm-base/README.md
b/shenyu-plugin/shenyu-plugin-wasm-base/README.md
index 7ed43b47b5..d51a8e877e 100644
--- a/shenyu-plugin/shenyu-plugin-wasm-base/README.md
+++ b/shenyu-plugin/shenyu-plugin-wasm-base/README.md
@@ -130,14 +130,23 @@ compatibility with existing guests.
The `wasm-fixtures` workflow pins WABT 1.0.42 and Rust 1.90.0. It runs
`python3 .github/scripts/verify-wasm-fixtures.py` when either WASM module
changes.
-The script checks the gateway WAT binaries byte-for-byte without modifying the
-committed files, then compiles the five Rust guests for `wasm32-wasip1` and
runs
-the WASM Java suites against them. Original resource bytes are restored even if
-a build or test fails. Legacy Rust compiler versions were not recorded, so
+The script requires the base module's `wasm-gateway` WAT family and the API
+module's `wasm-concurrent` family. It checks every `wasm-*` fixture directory
in
+both modules byte-for-byte, including `wasm-bridge` when present, without
+modifying the committed files. It then compiles the five Rust guests for
+`wasm32-wasip1` and runs the WASM Java suites against them. Original resource
+bytes are restored even if a build or test fails. Legacy Rust compiler
versions were not recorded, so
Rust reproduction is checked by behavior rather than exact binary identity.
The existing TinyGo guests run in the same suites. Use `--wat-only` or
`--rust-only` for a scoped local run. Maven-only builds still use committed
guests.
+Run Rust verification from a clean checkout. It temporarily replaces tracked
+`.wasm` resources while Maven tests execute, then restores them on success,
+ordinary failure or a Python `KeyboardInterrupt` (Ctrl+C). Forced termination
+such as `SIGKILL` bypasses that cleanup; inspect `git diff` and restore only
the
+affected generated fixtures before committing. Cargo may fetch dependencies;
+the verifier does not require a pre-populated offline dependency cache.
+
The optional `removeDiscoveryUpstreamData` callback documented below is now on
the merged base through #7568. The binary request profile in these gateway
tests
belongs to the explicit Java reference adapter; it is not the JSON default