diff --git a/README.md b/README.md index 47ed8f9..f8d7006 100644 --- a/README.md +++ b/README.md @@ -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, LSL tools, Utilities, Vivox, and WebRTC slices now have complete callable, failure-only signatures. The independent downstream fixture compiles every -cataloged type and member with zero exclusions; semantic Rust test translations -are the next stage. +cataloged type and member with zero exclusions, and all 1,289 NUnit invocations +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 -as translated only when an explicit Rust test body calls the mapped API and -retains the upstream identity and body hash. +A case counts as translated only when an explicit Rust test body calls the +mapped API and retains the upstream identity and body hash. The parity ledger +now contains zero pending or unreviewed cases. The source snapshot, compatibility rules, dependency research, and ordered 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_api_shims.py --check python3 tools/check_test_parity.py +python3 tools/audit_red_suite.py ``` `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. 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. diff --git a/RUSTREWRITE.md b/RUSTREWRITE.md index 305e7ba..c087e0a 100644 --- a/RUSTREWRITE.md +++ b/RUSTREWRITE.md @@ -364,6 +364,32 @@ The test-suite gate is complete only when: - no production method contains real behavior beyond what is required to make 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 Versions below were queried from crates.io on 2026-08-08 with `cargo search` diff --git a/crates/libremetaverse/src/world_internal_semantics.rs b/crates/libremetaverse/src/world_internal_semantics.rs index 137a614..1db44af 100644 --- a/crates/libremetaverse/src/world_internal_semantics.rs +++ b/crates/libremetaverse/src/world_internal_semantics.rs @@ -77,7 +77,7 @@ fn estate_packet(blocked: &[UUID], trusted: &[UUID], allowed: &[UUID]) -> Estate } } -fn block_on_with(future: F, mut after_pending: impl FnMut()) -> F::Output { +fn block_on_with(future: F, mut after_first_poll: impl FnMut()) -> F::Output { struct ThreadWake(std::thread::Thread); impl Wake for ThreadWake { fn wake(self: Arc) { @@ -93,7 +93,7 @@ fn block_on_with(future: F, mut after_pending: impl FnMut()) -> F::Ou Poll::Ready(value) => return value, Poll::Pending if !invoked => { invoked = true; - after_pending(); + after_first_poll(); } Poll::Pending => std::thread::park_timeout(Duration::from_millis(10)), } diff --git a/tests/compat/tests/social_constructor_semantics.rs b/tests/compat/tests/social_constructor_semantics.rs index 2aba2c3..a2f50e2 100644 --- a/tests/compat/tests/social_constructor_semantics.rs +++ b/tests/compat/tests/social_constructor_semantics.rs @@ -7,14 +7,14 @@ use libremetaverse::{AppearanceManager, AvatarManager, Error}; // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AppearanceManager_Null_Throws::test a3c872805bc924b5a1a9441d5c9eb61ed6d6e9477e4bb411131359290b144ab4 translated #[test] fn appearance_manager_null_throws() { - assert!(matches!( - AppearanceManager::new(None), - Err(Error::ArgumentNull) - )); + assert_eq!( + AppearanceManager::new(None).err(), + Some(Error::ArgumentNull) + ); } // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AvatarManager_Null_Throws::test 54f23cbf0ba87b0525c2914e78d75808e93fd1c460fc0bec4d15f09575b1431c translated #[test] fn avatar_manager_null_throws() { - assert!(matches!(AvatarManager::new(None), Err(Error::ArgumentNull))); + assert_eq!(AvatarManager::new(None).err(), Some(Error::ArgumentNull)); } diff --git a/tests/compat/tests/world_constructor_semantics.rs b/tests/compat/tests/world_constructor_semantics.rs index 4264ee9..6c48f22 100644 --- a/tests/compat/tests/world_constructor_semantics.rs +++ b/tests/compat/tests/world_constructor_semantics.rs @@ -7,17 +7,17 @@ use libremetaverse::{AgentManager, Error, GridManager, ObjectManager}; // parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.GridManager_Null_Throws::test e972a95f334b0f46948cda34c27124c28c44faf4372d0e598d7dc60050b49be4 translated #[test] 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 #[test] 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 #[test] fn agent_manager_null_throws() { - assert!(matches!(AgentManager::new(None), Err(Error::ArgumentNull))); + assert_eq!(AgentManager::new(None).err(), Some(Error::ArgumentNull)); } diff --git a/tests/red-suite-baseline.json b/tests/red-suite-baseline.json new file mode 100644 index 0000000..13e2e47 --- /dev/null +++ b/tests/red-suite-baseline.json @@ -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 +} diff --git a/tests/upstream-tests.json b/tests/upstream-tests.json index 690315f..fee2244 100644 --- a/tests/upstream-tests.json +++ b/tests/upstream-tests.json @@ -3054,7 +3054,7 @@ "body_sha256": "e972a95f334b0f46948cda34c27124c28c44faf4372d0e598d7dc60050b49be4", "categories": [], "fixture_dependencies": [], - "rust_body_sha256": "7ced2dbdadce6cc8654767c7f17f491b428f21c3d397f3236fd46e686ca3f219", + "rust_body_sha256": "440233e304c7561b384d23798d815d10806cb624558cc847475b2f517b5275c4", "status": "translated", "rust_file": "tests/compat/tests/world_constructor_semantics.rs", "rust_line": 7, @@ -3092,7 +3092,7 @@ "body_sha256": "a3c872805bc924b5a1a9441d5c9eb61ed6d6e9477e4bb411131359290b144ab4", "categories": [], "fixture_dependencies": [], - "rust_body_sha256": "5d9e748f126a3ff03db848d71258287441dd520c3f2a5bfadb5921f3e7fba7d0", + "rust_body_sha256": "65ab61a0cc83ccef52575370de46c61fa91a93246938d2e7a5df41b2547c6e41", "status": "translated", "rust_file": "tests/compat/tests/social_constructor_semantics.rs", "rust_line": 7, @@ -3111,7 +3111,7 @@ "body_sha256": "8a11feba0edfb35e5dd99f681182e8f5a5cf8a3f738211b45a683a8a2ad45ddb", "categories": [], "fixture_dependencies": [], - "rust_body_sha256": "c6849d0a524859f5436a8e0a78a9baf75a761a8edaa801a3eed09c4057c7eb96", + "rust_body_sha256": "551175a56c31bfee564b3f408c6a9868d56e0b6763e36cf71c0cf372760ee575", "status": "translated", "rust_file": "tests/compat/tests/world_constructor_semantics.rs", "rust_line": 13, @@ -3130,7 +3130,7 @@ "body_sha256": "d86e41b6cdd03063685aa330c31b9a274c1d400b9a461abf3065fe185fe0bbf0", "categories": [], "fixture_dependencies": [], - "rust_body_sha256": "4c6198a60bbdee2fe94096d579ebff31b8b38048982d3aea25e0c0bb041ba70c", + "rust_body_sha256": "0ccd306db6d19296ce8793dc7b6712601f79c5c7cd6f013171102f581589b065", "status": "translated", "rust_file": "tests/compat/tests/world_constructor_semantics.rs", "rust_line": 19, @@ -3149,7 +3149,7 @@ "body_sha256": "54f23cbf0ba87b0525c2914e78d75808e93fd1c460fc0bec4d15f09575b1431c", "categories": [], "fixture_dependencies": [], - "rust_body_sha256": "fed746879e584f0c75b19f08009234588905204e5954d7a5f61d14c40664bef1", + "rust_body_sha256": "1307f3ee0990260c80fa9ec1b693231d5c3e1f94c293fab7bcca4d5c5140ce55", "status": "translated", "rust_file": "tests/compat/tests/social_constructor_semantics.rs", "rust_line": 16, diff --git a/tools/audit_red_suite.py b/tools/audit_red_suite.py new file mode 100644 index 0000000..3501c12 --- /dev/null +++ b/tools/audit_red_suite.py @@ -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() diff --git a/tools/test_audit_red_suite.py b/tools/test_audit_red_suite.py new file mode 100644 index 0000000..1d1b39d --- /dev/null +++ b/tools/test_audit_red_suite.py @@ -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()