Establish controlled red-suite baseline

This commit is contained in:
2026-08-08 23:35:04 +02:00
parent 3140e65dc7
commit e63f2c615a
9 changed files with 314 additions and 20 deletions

View File

@@ -12,12 +12,13 @@ the main assembly's packet/message/asset/primitive wire-data, core
runtime/networking, avatar-facing manager, world/social/service manager, RLV, runtime/networking, avatar-facing manager, world/social/service manager, RLV,
LSL tools, Utilities, Vivox, and WebRTC slices now have complete callable, LSL tools, Utilities, Vivox, and WebRTC slices now have complete callable,
failure-only signatures. The independent downstream fixture compiles every failure-only signatures. The independent downstream fixture compiles every
cataloged type and member with zero exclusions; semantic Rust test translations cataloged type and member with zero exclusions, and all 1,289 NUnit invocations
are the next stage. now have reviewed Rust parity cases. Production behavior implementation is the
next stage.
Pending NUnit cases are inventory only, not generated fake tests. A case counts A case counts as translated only when an explicit Rust test body calls the
as translated only when an explicit Rust test body calls the mapped API and mapped API and retains the upstream identity and body hash. The parity ledger
retains the upstream identity and body hash. now contains zero pending or unreviewed cases.
The source snapshot, compatibility rules, dependency research, and ordered The source snapshot, compatibility rules, dependency research, and ordered
implementation plan are in [RUSTREWRITE.md](RUSTREWRITE.md). implementation plan are in [RUSTREWRITE.md](RUSTREWRITE.md).
@@ -37,6 +38,7 @@ cargo test --workspace --no-run
python3 tools/generate_rust_mapping.py --check python3 tools/generate_rust_mapping.py --check
python3 tools/generate_api_shims.py --check python3 tools/generate_api_shims.py --check
python3 tools/check_test_parity.py python3 tools/check_test_parity.py
python3 tools/audit_red_suite.py
``` ```
`tests/upstream-tests.json` is the machine-readable NUnit parity catalog. `tests/upstream-tests.json` is the machine-readable NUnit parity catalog.
@@ -46,3 +48,5 @@ verifies the catalog against the pinned adjacent LibreMetaverse checkout without
overwriting those files. overwriting those files.
Running `cargo test --workspace` is intentionally red during the shim stage. Running `cargo test --workspace` is intentionally red during the shim stage.
The controlled audit aggregates every expected failure by standardized C#
member ID and rejects unrelated fixture, assertion, compile, or symbol errors.

View File

@@ -364,6 +364,32 @@ The test-suite gate is complete only when:
- no production method contains real behavior beyond what is required to make - no production method contains real behavior beyond what is required to make
signatures and constants compile. signatures and constants compile.
### 3.3 Fixed controlled-red baseline
Milestone 03 closes against the pinned 1,289-invocation catalog, not the older
1,295 source-text estimate. The six-case difference is intentional: five
attributes were commented out and one parameterless `[Test]` marker duplicated
a parameterized `[TestCase]` method. Reintroducing those entries would create
tests that NUnit never runs.
`tests/red-suite-baseline.json` fixes the machine-checked ledger at 1,266
ordinary translations, 19 live-grid translations, four benchmarks, and zero
pending, drifted, duplicate, missing, or unreviewed cases. Run the controlled
audit with:
```sh
python3 tools/audit_red_suite.py
```
With all three live-grid credentials present, the baseline executes every case:
22 tests pass, 1,287 fail at 131 standardized C# member IDs, and none are
ignored. Twenty passes are gate/support tests. The only two passing parity cases
verify the required `BAKED_TEXTURE_COUNT` constant and composable validation
flags; they do not represent implemented production behavior. Without complete
credentials, the same audit conditionally ignores exactly the 19 live-grid
cases and requires the remaining 1,268 parity failures to retain standardized
member IDs. Production crates remain the failure-only milestone-02 shims.
## 4. Validated Rust dependency map ## 4. Validated Rust dependency map
Versions below were queried from crates.io on 2026-08-08 with `cargo search` Versions below were queried from crates.io on 2026-08-08 with `cargo search`

View File

@@ -77,7 +77,7 @@ fn estate_packet(blocked: &[UUID], trusted: &[UUID], allowed: &[UUID]) -> Estate
} }
} }
fn block_on_with<F: Future>(future: F, mut after_pending: impl FnMut()) -> F::Output { fn block_on_with<F: Future>(future: F, mut after_first_poll: impl FnMut()) -> F::Output {
struct ThreadWake(std::thread::Thread); struct ThreadWake(std::thread::Thread);
impl Wake for ThreadWake { impl Wake for ThreadWake {
fn wake(self: Arc<Self>) { fn wake(self: Arc<Self>) {
@@ -93,7 +93,7 @@ fn block_on_with<F: Future>(future: F, mut after_pending: impl FnMut()) -> F::Ou
Poll::Ready(value) => return value, Poll::Ready(value) => return value,
Poll::Pending if !invoked => { Poll::Pending if !invoked => {
invoked = true; invoked = true;
after_pending(); after_first_poll();
} }
Poll::Pending => std::thread::park_timeout(Duration::from_millis(10)), Poll::Pending => std::thread::park_timeout(Duration::from_millis(10)),
} }

View File

@@ -7,14 +7,14 @@ use libremetaverse::{AppearanceManager, AvatarManager, Error};
// parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AppearanceManager_Null_Throws::test a3c872805bc924b5a1a9441d5c9eb61ed6d6e9477e4bb411131359290b144ab4 translated // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AppearanceManager_Null_Throws::test a3c872805bc924b5a1a9441d5c9eb61ed6d6e9477e4bb411131359290b144ab4 translated
#[test] #[test]
fn appearance_manager_null_throws() { fn appearance_manager_null_throws() {
assert!(matches!( assert_eq!(
AppearanceManager::new(None), AppearanceManager::new(None).err(),
Err(Error::ArgumentNull) Some(Error::ArgumentNull)
)); );
} }
// parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AvatarManager_Null_Throws::test 54f23cbf0ba87b0525c2914e78d75808e93fd1c460fc0bec4d15f09575b1431c translated // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AvatarManager_Null_Throws::test 54f23cbf0ba87b0525c2914e78d75808e93fd1c460fc0bec4d15f09575b1431c translated
#[test] #[test]
fn avatar_manager_null_throws() { fn avatar_manager_null_throws() {
assert!(matches!(AvatarManager::new(None), Err(Error::ArgumentNull))); assert_eq!(AvatarManager::new(None).err(), Some(Error::ArgumentNull));
} }

View File

@@ -7,17 +7,17 @@ use libremetaverse::{AgentManager, Error, GridManager, ObjectManager};
// parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.GridManager_Null_Throws::test e972a95f334b0f46948cda34c27124c28c44faf4372d0e598d7dc60050b49be4 translated // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.GridManager_Null_Throws::test e972a95f334b0f46948cda34c27124c28c44faf4372d0e598d7dc60050b49be4 translated
#[test] #[test]
fn grid_manager_null_throws() { fn grid_manager_null_throws() {
assert!(matches!(GridManager::new(None), Err(Error::ArgumentNull))); assert_eq!(GridManager::new(None).err(), Some(Error::ArgumentNull));
} }
// parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.ObjectManager_Null_Throws::test 8a11feba0edfb35e5dd99f681182e8f5a5cf8a3f738211b45a683a8a2ad45ddb translated // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.ObjectManager_Null_Throws::test 8a11feba0edfb35e5dd99f681182e8f5a5cf8a3f738211b45a683a8a2ad45ddb translated
#[test] #[test]
fn object_manager_null_throws() { fn object_manager_null_throws() {
assert!(matches!(ObjectManager::new(None), Err(Error::ArgumentNull))); assert_eq!(ObjectManager::new(None).err(), Some(Error::ArgumentNull));
} }
// parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AgentManager_Null_Throws::test d86e41b6cdd03063685aa330c31b9a274c1d400b9a461abf3065fe185fe0bbf0 translated // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AgentManager_Null_Throws::test d86e41b6cdd03063685aa330c31b9a274c1d400b9a461abf3065fe185fe0bbf0 translated
#[test] #[test]
fn agent_manager_null_throws() { fn agent_manager_null_throws() {
assert!(matches!(AgentManager::new(None), Err(Error::ArgumentNull))); assert_eq!(AgentManager::new(None).err(), Some(Error::ArgumentNull));
} }

View File

@@ -0,0 +1,18 @@
{
"schema_version": 1,
"upstream_commit": "2aa70bb68513b39795da5d13c88f31b86e85a3ba",
"reviewed_cases": 1289,
"parity_report": {
"pending": 0,
"translated": 1266,
"ignored_live": 19,
"benchmark": 4,
"drifted": 0,
"unreviewed": 0
},
"allowed_parity_passes": [
"LibreMetaverse.Tests/AppearanceManagerTests.cs::AppearanceManagerTests.BAKED_TEXTURE_COUNT_Is11::test",
"LibreMetaverse.Tests/MarketplaceFolderClassifierTests.cs::MarketplaceFolderClassifierTests.ValidateListing_ValidFlags_AreBitmaskComposable::test"
],
"support_passes": 20
}

View File

@@ -3054,7 +3054,7 @@
"body_sha256": "e972a95f334b0f46948cda34c27124c28c44faf4372d0e598d7dc60050b49be4", "body_sha256": "e972a95f334b0f46948cda34c27124c28c44faf4372d0e598d7dc60050b49be4",
"categories": [], "categories": [],
"fixture_dependencies": [], "fixture_dependencies": [],
"rust_body_sha256": "7ced2dbdadce6cc8654767c7f17f491b428f21c3d397f3236fd46e686ca3f219", "rust_body_sha256": "440233e304c7561b384d23798d815d10806cb624558cc847475b2f517b5275c4",
"status": "translated", "status": "translated",
"rust_file": "tests/compat/tests/world_constructor_semantics.rs", "rust_file": "tests/compat/tests/world_constructor_semantics.rs",
"rust_line": 7, "rust_line": 7,
@@ -3092,7 +3092,7 @@
"body_sha256": "a3c872805bc924b5a1a9441d5c9eb61ed6d6e9477e4bb411131359290b144ab4", "body_sha256": "a3c872805bc924b5a1a9441d5c9eb61ed6d6e9477e4bb411131359290b144ab4",
"categories": [], "categories": [],
"fixture_dependencies": [], "fixture_dependencies": [],
"rust_body_sha256": "5d9e748f126a3ff03db848d71258287441dd520c3f2a5bfadb5921f3e7fba7d0", "rust_body_sha256": "65ab61a0cc83ccef52575370de46c61fa91a93246938d2e7a5df41b2547c6e41",
"status": "translated", "status": "translated",
"rust_file": "tests/compat/tests/social_constructor_semantics.rs", "rust_file": "tests/compat/tests/social_constructor_semantics.rs",
"rust_line": 7, "rust_line": 7,
@@ -3111,7 +3111,7 @@
"body_sha256": "8a11feba0edfb35e5dd99f681182e8f5a5cf8a3f738211b45a683a8a2ad45ddb", "body_sha256": "8a11feba0edfb35e5dd99f681182e8f5a5cf8a3f738211b45a683a8a2ad45ddb",
"categories": [], "categories": [],
"fixture_dependencies": [], "fixture_dependencies": [],
"rust_body_sha256": "c6849d0a524859f5436a8e0a78a9baf75a761a8edaa801a3eed09c4057c7eb96", "rust_body_sha256": "551175a56c31bfee564b3f408c6a9868d56e0b6763e36cf71c0cf372760ee575",
"status": "translated", "status": "translated",
"rust_file": "tests/compat/tests/world_constructor_semantics.rs", "rust_file": "tests/compat/tests/world_constructor_semantics.rs",
"rust_line": 13, "rust_line": 13,
@@ -3130,7 +3130,7 @@
"body_sha256": "d86e41b6cdd03063685aa330c31b9a274c1d400b9a461abf3065fe185fe0bbf0", "body_sha256": "d86e41b6cdd03063685aa330c31b9a274c1d400b9a461abf3065fe185fe0bbf0",
"categories": [], "categories": [],
"fixture_dependencies": [], "fixture_dependencies": [],
"rust_body_sha256": "4c6198a60bbdee2fe94096d579ebff31b8b38048982d3aea25e0c0bb041ba70c", "rust_body_sha256": "0ccd306db6d19296ce8793dc7b6712601f79c5c7cd6f013171102f581589b065",
"status": "translated", "status": "translated",
"rust_file": "tests/compat/tests/world_constructor_semantics.rs", "rust_file": "tests/compat/tests/world_constructor_semantics.rs",
"rust_line": 19, "rust_line": 19,
@@ -3149,7 +3149,7 @@
"body_sha256": "54f23cbf0ba87b0525c2914e78d75808e93fd1c460fc0bec4d15f09575b1431c", "body_sha256": "54f23cbf0ba87b0525c2914e78d75808e93fd1c460fc0bec4d15f09575b1431c",
"categories": [], "categories": [],
"fixture_dependencies": [], "fixture_dependencies": [],
"rust_body_sha256": "fed746879e584f0c75b19f08009234588905204e5954d7a5f61d14c40664bef1", "rust_body_sha256": "1307f3ee0990260c80fa9ec1b693231d5c3e1f94c293fab7bcca4d5c5140ce55",
"status": "translated", "status": "translated",
"rust_file": "tests/compat/tests/social_constructor_semantics.rs", "rust_file": "tests/compat/tests/social_constructor_semantics.rs",
"rust_line": 16, "rust_line": 16,

207
tools/audit_red_suite.py Normal file
View File

@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Run and verify MetaCrate's controlled red test-suite baseline."""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict, deque
from dataclasses import dataclass
import json
import os
from pathlib import Path
import re
import subprocess
ROOT = Path(__file__).resolve().parents[1]
CATALOG = ROOT / "tests" / "upstream-tests.json"
BASELINE = ROOT / "tests" / "red-suite-baseline.json"
LIVE_KEYS = ("GRID_USER", "GRID_PASSWORD", "GRID_LOGIN_URL")
SUMMARY_RE = re.compile(
r"test result: (?:ok|FAILED)\. (\d+) passed; (\d+) failed; "
r"(\d+) ignored; (\d+) measured; (\d+) filtered out"
)
RESULT_RE = re.compile(
r"^test (.+) \.\.\. (ok|FAILED|ignored)(?:, .*)?$", re.MULTILINE
)
PANIC_RE = re.compile(r"(?:^|\n)thread '([^']+)'(?: \(\d+\))? panicked at ")
MEMBER_RE = re.compile(
r'unimplemented C# API member: ([^\n]+)|NotImplemented \{ csharp_member: "([^"]+)" \}'
)
@dataclass(frozen=True)
class LogAudit:
passed: int
failed: int
ignored: int
measured: int
filtered: int
passed_names: tuple[str, ...]
failed_names: tuple[str, ...]
ignored_names: tuple[str, ...]
members: Counter[str]
nonstandard_failures: tuple[str, ...]
def parse_log(text: str) -> LogAudit:
"""Parse libtest output and associate each failed test with its member ID."""
summaries = [tuple(map(int, match)) for match in SUMMARY_RE.findall(text)]
if not summaries:
raise ValueError("controlled suite log contains no libtest summaries")
totals = tuple(sum(row[index] for row in summaries) for index in range(5))
results = RESULT_RE.findall(text)
names = {
state: tuple(name for name, result in results if result == state)
for state in ("ok", "FAILED", "ignored")
}
panic_members: dict[str, deque[tuple[str, ...]]] = defaultdict(deque)
starts = list(PANIC_RE.finditer(text))
for index, start in enumerate(starts):
end = starts[index + 1].start() if index + 1 < len(starts) else len(text)
chunk = text[start.end() : end]
members = tuple(first or second for first, second in MEMBER_RE.findall(chunk))
panic_members[start.group(1)].append(members)
member_counts: Counter[str] = Counter()
nonstandard: list[str] = []
for name in names["FAILED"]:
members = panic_members[name].popleft() if panic_members[name] else ()
if members:
member_counts[members[0]] += 1
else:
nonstandard.append(name)
if totals[0] != len(names["ok"]) or totals[1] != len(names["FAILED"]):
raise ValueError("libtest summaries do not match parsed pass/fail result lines")
return LogAudit(
*totals,
names["ok"],
names["FAILED"],
names["ignored"],
member_counts,
tuple(nonstandard),
)
def dotenv_values() -> dict[str, str]:
values: dict[str, str] = {}
path = ROOT / ".env"
if not path.exists():
return values
for raw_line in path.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
line = line.removeprefix("export ")
if "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip("'\"")
return values
def has_live_credentials() -> bool:
dotenv = dotenv_values()
return all((os.environ.get(key) or dotenv.get(key, "")).strip() for key in LIVE_KEYS)
def pending_calls() -> list[str]:
pending: list[str] = []
roots = (ROOT / "tests" / "compat" / "tests", ROOT / "crates")
for path in sorted(file for root in roots for file in root.rglob("*.rs")):
if re.search(r"\bpending\s*\(", path.read_text()):
pending.append(path.relative_to(ROOT).as_posix())
return pending
def verify(log: LogAudit, live: bool) -> None:
catalog = json.loads(CATALOG.read_text())
baseline = json.loads(BASELINE.read_text())
report = catalog["report"]
expected_cases = baseline["reviewed_cases"]
if catalog["upstream_commit"] != baseline["upstream_commit"]:
raise ValueError("red-suite baseline targets a different upstream commit")
if catalog["expected_cases"] != expected_cases or sum(report.values()) != expected_cases:
raise ValueError("parity catalog does not contain the fixed reviewed case count")
expected_report = baseline["parity_report"]
if report != expected_report:
raise ValueError(f"parity report changed: expected {expected_report}, found {report}")
pending = pending_calls()
if pending:
raise ValueError("pending( remains in Rust test sources: " + ", ".join(pending))
by_test = defaultdict(list)
for case in catalog["tests"]:
by_test[case["rust_test"]].append(case["id"])
passed_cases = sorted(
case_id for name in log.passed_names for case_id in by_test.get(name, [])
)
allowed_passes = sorted(baseline["allowed_parity_passes"])
if passed_cases != allowed_passes:
raise ValueError(
f"parity pass baseline changed: expected {allowed_passes}, found {passed_cases}"
)
support_passes = log.passed - len(passed_cases)
if support_passes != baseline["support_passes"]:
raise ValueError(
f"support pass baseline changed: expected {baseline['support_passes']}, "
f"found {support_passes}"
)
expected_ignored = 0 if live else report["ignored_live"]
expected_failed = expected_cases - len(allowed_passes) - expected_ignored
if (log.failed, log.ignored) != (expected_failed, expected_ignored):
raise ValueError(
"controlled totals changed: "
f"expected failed={expected_failed}, ignored={expected_ignored}; "
f"found failed={log.failed}, ignored={log.ignored}"
)
if log.nonstandard_failures:
raise ValueError(
"failures without standardized member IDs: "
+ ", ".join(log.nonstandard_failures)
)
if sum(log.members.values()) != log.failed:
raise ValueError("standardized member aggregation does not cover every failure")
def run_suite() -> tuple[int, str]:
command = [
"cargo",
"test",
"--workspace",
"--no-fail-fast",
"--",
"--nocapture",
"--test-threads=1",
]
completed = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
return completed.returncode, completed.stdout + completed.stderr
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--log", type=Path, help="audit an existing captured cargo-test log")
args = parser.parse_args()
if args.log:
return_code, text = 101, args.log.read_text()
else:
return_code, text = run_suite()
if return_code != 101:
raise SystemExit(f"controlled suite returned {return_code}, expected Cargo test failure 101")
live = has_live_credentials()
audit = parse_log(text)
verify(audit, live)
print(
f"controlled red suite is current: passed={audit.passed}, failed={audit.failed}, "
f"ignored={audit.ignored}, live_credentials={str(live).lower()}, "
f"standardized_members={len(audit.members)}"
)
for member, count in audit.members.most_common():
print(f"{count:4} {member}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,39 @@
import unittest
from audit_red_suite import parse_log
class AuditRedSuiteTests(unittest.TestCase):
def test_associates_standardized_boundaries_with_failed_tests(self) -> None:
audit = parse_log(
"""
thread 'direct' panicked at generated.rs:1:1:
unimplemented C# API member: M:Example.Direct
test direct ... FAILED
thread 'typed' panicked at test.rs:1:1:
assertion failed: left == right
left: Some(NotImplemented { csharp_member: "M:Example.Typed" })
right: Some(ArgumentNull)
test typed ... FAILED
test support ... ok
test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out
"""
)
self.assertEqual(audit.members["M:Example.Direct"], 1)
self.assertEqual(audit.members["M:Example.Typed"], 1)
self.assertEqual(audit.nonstandard_failures, ())
def test_reports_nonstandard_failure(self) -> None:
audit = parse_log(
"""
thread 'broken' panicked at test.rs:1:1:
assertion failed
test broken ... FAILED
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
"""
)
self.assertEqual(audit.nonstandard_failures, ("broken",))
if __name__ == "__main__":
unittest.main()