Establish controlled red-suite baseline
This commit is contained in:
14
README.md
14
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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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);
|
||||
impl Wake for ThreadWake {
|
||||
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::Pending if !invoked => {
|
||||
invoked = true;
|
||||
after_pending();
|
||||
after_first_poll();
|
||||
}
|
||||
Poll::Pending => std::thread::park_timeout(Duration::from_millis(10)),
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
18
tests/red-suite-baseline.json
Normal file
18
tests/red-suite-baseline.json
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
207
tools/audit_red_suite.py
Normal file
207
tools/audit_red_suite.py
Normal 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()
|
||||
39
tools/test_audit_red_suite.py
Normal file
39
tools/test_audit_red_suite.py
Normal 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()
|
||||
Reference in New Issue
Block a user