Compare commits

...
Author SHA1 Message Date
rdbt 36eb98b6f6 Merge pull request #15815 from rdbtCVS/t3/fix-nightly-parity-regressions
Fix seven parity regressions blocking nightly builds
2026-11-12 23:53:35 +00:00
rdbtCVSandCodex GPT-6.1 a884525213 Align the head cache test with whole-payload texture rejection
Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 23:23:18 +00:00
rdbtCVSandCodex GPT-6.1 c35f0ba323 Clarify the authlib reference outside the game port ledger
Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 23:04:01 +00:00
rdbtCVSandCodex GPT-6.1 7e6b2db114 Merge main with the manual-proof parity fix
Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 22:55:36 +00:00
rdbtCVSandCodex GPT-6.1 768f0ddb83 Fix seven parity regressions blocking nightly builds
Restore vanilla void thresholds, HUD backdrops and profile texture validation; repair the block-event, AI-label, display-name and save-timer harness contracts.

Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 22:55:15 +00:00
rdbt 7c1a43138c Merge pull request #15570 from ilguerro/fix/parity-manual-proof-scripts
Leave the six hand-run proof scripts out of parity runs
2026-11-12 22:43:08 +00:00
Riccardo GuerrieroandClaude Opus 5.5 cc1fea2714 Leave the six hand-run proof scripts out of parity runs
tools/parity.py runs every tools/test_*_parity.py with no arguments. Six of
them are two-step proofs that need arguments (a Java side, then a native
side against its reference): builtin_component_delivery,
entity_movement_predicate, guest_close_event, machine_recipe_placement,
predicate_reference_startup and self_pose_metadata. Run bare, each exits
in 0 s with a usage error, so every parity-main run since they landed
(11-07 to 11-09) has failed on them, and the Nightly waits for a green
parity-main.

A script can now declare PARITY_MANUAL = "<how to run it>": parity.py
leaves it out of every run (--changed and parity-main included) and,
asked for it by name, says how to run it. The six declare it; nothing
else about them changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd42tRMKJk8JighAmETj
2026-11-12 11:08:41 +01:00
17 changed files with 117 additions and 77 deletions

No files matched your search

@@ -39,14 +39,13 @@ pub fn text_with_backdrop(
background: u32,
) {
if background != 0 {
// Font.PreparedTextBuilder.markBackground: one pixel above and left of
// the glyphs, the glyph advance to the right and nine high: not a
// fixed two-pixel frame.
// GuiGraphicsExtractor.textWithBackdrop adds two pixels around the
// text's advance and the font's nine-pixel line height.
out.push(fill(
x - 1,
y - 1,
x + w,
y + 9,
x - 2,
y - 2,
x + w + 2,
y + 11,
multiply(background, color),
));
}
@@ -1491,10 +1490,10 @@ mod boss_bar_room_tests {
mod backdrop_tests {
use super::*;
/// `markBackground`: the backdrop reaches one pixel above and left of the
/// glyphs, their advance to the right and nine down, not 2 on every side.
/// `GuiGraphicsExtractor.textWithBackdrop`: two pixels around the advance
/// and the nine-pixel font line, independently of glyph-background drawing.
#[test]
fn the_backdrop_frames_the_glyphs_by_one_pixel() {
fn the_backdrop_frames_the_text_by_two_pixels() {
let mut out = Vec::new();
text_with_backdrop(
&mut out,
@@ -1508,6 +1507,6 @@ mod backdrop_tests {
let Some(GuiDraw::Fill { x0, y0, x1, y1, .. }) = out.first() else {
panic!("a backdrop is a fill first");
};
assert_eq!((*x0, *y0, *x1, *y1), (9, 19, 16, 29));
assert_eq!((*x0, *y0, *x1, *y1), (8, 18, 18, 31));
}
}
+11 -17
View File
@@ -93,18 +93,17 @@ pub fn unpack(value: &str, signature: Signature, valid_uris: &[impl AsRef<str>])
signed: signature == Signature::Signed,
..empty.clone()
};
// Each texture is loaded independently: an invalid or disallowed URL for one
// texture (cape, elytra) must not drop another (the skin). Vanilla's
// SkinManager works the same way.
// Authlib's MinecraftServicesSessionService validates every URL in
// unpackTextures before returning any textures. A bad cape rejects the skin too.
for (kind, texture) in textures {
let Some(url) = texture.get("url").and_then(|u| u.as_str()) else {
continue;
return empty;
};
let allowed = valid_uris
.iter()
.any(|u| url.starts_with(&u.as_ref().replace("{textureId}", "")));
if !allowed {
continue;
return empty;
}
if kind == "SKIN" {
out.slim = texture
@@ -1388,10 +1387,9 @@ mod tests {
&super::TEXTURE_URIS,
);
assert_eq!(textures.slim, slim && !url.contains("evil"));
// Textures are judged per URL (vanilla's SkinManager): the
// cape's URL is always allowed here, so it is kept even when
// the skin's URL is not.
assert!(textures.cape.is_some());
// Authlib rejects the whole payload if any URL is invalid,
// including the allowed cape when the skin is disallowed.
assert_eq!(textures.cape.is_some(), !url.contains("evil"));
assert_eq!(parsed.skin.is_some(), !url.contains("evil"));
assert!(std::sync::Arc::ptr_eq(&parsed, &cache.get(&profile)));
}
@@ -1544,18 +1542,14 @@ mod tests {
assert!(t.slim);
assert!(!unpack(wide, Signature::Signed, &TEXTURE_URIS).slim);
}
/// `unpack` keeps each texture independently: a cape from another origin
/// must not drop the skin.
/// Authlib's `MinecraftServicesSessionService` rejects the whole profile in
/// `unpackTextures` when any texture URL is outside the allowed origins.
#[test]
fn unpack_keeps_the_skin_when_another_texture_is_rejected() {
fn unpack_rejects_the_skin_when_another_texture_is_rejected() {
use super::{Signature, TEXTURE_URIS, unpack};
let mixed = "eyJ0ZXh0dXJlcyI6IHsiU0tJTiI6IHsidXJsIjogImh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL3NraW4xIn0sICJDQVBFIjogeyJ1cmwiOiAiaHR0cHM6Ly9ldmlsLmV4YW1wbGUvdGV4dHVyZS9jYXBlMSJ9fX0=";
let t = unpack(mixed, Signature::Unsigned, &TEXTURE_URIS);
assert_eq!(
t.skin.as_deref(),
Some("https://textures.minecraft.net/texture/skin1")
);
assert_eq!(t.cape, None);
assert_eq!(t, super::Textures::default());
}
#[test]
fn profiles_parse_like_resolvable_profiles() {
@@ -237,7 +237,7 @@ fn goal_name(m: &Mob, name: &str) -> String {
"RestrictSun" => "RestrictSunGoal",
"FleeSun" => "FleeSunGoal",
// `Cat.CatAvoidEntityGoal`, `Fox.FoxAvoidEntityGoal`... name their subclass.
"AvoidEntity" if m.species() == Species::Cat => "CatAvoidEntityGoal",
"AvoidEntity" | "CatAvoid" if m.species() == Species::Cat => "CatAvoidEntityGoal",
"AvoidEntity" => "AvoidEntityGoal",
"BowAttack" => "RangedBowAttackGoal",
"Swell" => "SwellGoal",
@@ -18,7 +18,7 @@ impl Game<'_> {
/// `Entity.checkBelowWorld` for the boat at `i`: true when it fell out of
/// the world and is gone.
pub(in crate::game) fn boat_below_world(&mut self, i: usize) -> bool {
if !below_world(self.boats[i].position.y, self.world.bottom_y()) {
if !below_world(self.boats[i].position.y, self.world.shape.min_y) {
return false;
}
self.eject_boat_mobs(i, true);
@@ -41,7 +41,7 @@ impl Game<'_> {
kind: &str,
) -> Option<usize> {
let i = cart_at(&self.carts, id, slot)?;
if !below_world(self.carts[i].position.y, self.world.bottom_y()) {
if !below_world(self.carts[i].position.y, self.world.shape.min_y) {
return Some(i);
}
self.eject_cart_mobs(i);
@@ -68,44 +68,51 @@ mod tests {
assert!(below_world(-64.5, 0));
}
/// A tick discards the boat and the chest minecart under -128 in the
/// overworld, before they move, and spills the cart's slots; the two on
/// the line or above it stay, whatever their fall this tick.
/// A tick discards vehicles below each dimension's own floor, even when
/// its chunk storage extends lower, and spills the container's contents.
/// Vehicles on the line or above it survive this tick.
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn a_tick_discards_the_vehicles_under_the_world() {
use horizon_client::chunk::Chunk;
use horizon_core::position::ChunkPos;
let mut g = crate::game::native_machines::tests::server_game();
g.build_registries();
g.set_allow_commands(true);
g.world
.chunks
.insert((0, 0), std::sync::Arc::new(Chunk::empty(0, 0)));
g.block_ticks.add_empty_chunk(ChunkPos::new(0, 0)).unwrap();
assert!(g.run_command("forceload add 0 0"));
for summon in [
"summon oak_boat 0.5 -128 0.5",
"summon oak_boat 4.5 -128.5 4.5",
"summon minecart 6.5 -127 6.5",
"summon chest_minecart 2.5 -129 2.5 {Items:[{Slot:0b,id:\"minecraft:diamond\",count:3}]}",
] {
assert!(g.run_command(summon), "{summon}");
for min_y in [-64, 0] {
let mut g = crate::game::native_machines::tests::server_game();
g.world.shape.min_y = min_y;
g.build_registries();
g.set_allow_commands(true);
g.world
.chunks
.insert((0, 0), std::sync::Arc::new(Chunk::empty(0, 0)));
g.block_ticks.add_empty_chunk(ChunkPos::new(0, 0)).unwrap();
assert!(g.run_command("forceload add 0 0"));
let line = min_y - 64;
for summon in [
format!("summon oak_boat 0.5 {line} 0.5"),
format!("summon oak_boat 4.5 {} 4.5", f64::from(line) - 0.5),
format!("summon minecart 6.5 {} 6.5", line + 1),
format!(
"summon chest_minecart 2.5 {} 2.5 {{Items:[{{Slot:0b,id:\"minecraft:diamond\",count:3}}]}}",
line - 1
),
] {
assert!(g.run_command(&summon), "{summon}");
}
g.sync_tick_chunks();
g.tick_boats();
g.tick_carts();
let boats: Vec<f64> = g.boats.iter().map(|b| b.position.x).collect();
assert_eq!(boats, [0.5]);
let carts: Vec<f64> = g.carts.iter().map(|c| c.position.x).collect();
assert_eq!(carts, [6.5]);
let spilled: u32 = g
.dropped
.iter()
.filter(|i| &*i.stack.item == "minecraft:diamond")
.map(|i| i.stack.count)
.sum();
assert_eq!(spilled, 3);
}
g.sync_tick_chunks();
g.tick_boats();
g.tick_carts();
let boats: Vec<f64> = g.boats.iter().map(|b| b.position.x).collect();
assert_eq!(boats, [0.5]);
let carts: Vec<f64> = g.carts.iter().map(|c| c.position.x).collect();
assert_eq!(carts, [6.5]);
let spilled: u32 = g
.dropped
.iter()
.filter(|i| &*i.stack.item == "minecraft:diamond")
.map(|i| i.stack.count)
.sum();
assert_eq!(spilled, 3);
}
#[test]
+1
View File
@@ -72,6 +72,7 @@ A harness can declare, at the top level of its script:
- `PARITY_SHARDS = N`: it runs as `NAME#1` to `NAME#N`, in parallel, each told its share in `PARITY_SHARD=i/N` (`test_datafix_parity.py` upgrades every sixth world). `NAME` on the command line or in `--known-failures` means all of them.
- `PARITY_RUST_SCOPE = [...]`: the Rust sources its probes can reach, as path prefixes. Cargo's dep-info names every file of every crate a probe links, so without it a probe of `horizon-world` depends on all 350 of its files. The runner keeps only the scope's files if the scope is closed: nothing in it names a module, crate-root re-export or macro outside it (`crate::`, `super::`, other workspace crates; comments and strings ignored), declares a module it doesn't cover, or includes source, and nothing outside it implements anything for a type it defines. Crate roots then count without their declarations of other modules, so adding a module elsewhere doesn't rerun the harness. If the scope isn't closed the runner says why and keeps every file. `datafix` depends on 41 files this way, not 354.
- `PARITY_MANUAL = "<how to run it>"`: the script is a proof someone runs by hand with arguments (a Java side, then a native side against its reference), not a harness. `parity.py` leaves it out of every run, `--changed` and parity-main included, and names that text when it is asked for it by name.
### Which harnesses a change affects
+9
View File
@@ -413,6 +413,15 @@ class ShardTests(unittest.TestCase):
self.assertEqual(parity.SHARDS["x#2"], "2/3")
self.assertEqual(parity.base_name("x#2"), "x")
def test_a_manual_proof_script_is_left_out(self):
with tempfile.TemporaryDirectory() as temp, mock.patch.object(parity, "ROOT", Path(temp)), \
mock.patch.dict(parity.MANUAL, clear=True):
(Path(temp) / "tools").mkdir()
(Path(temp) / "tools/test_x_parity.py").write_text('import sys\nPARITY_MANUAL = "--side java, then --side native"\n')
(Path(temp) / "tools/test_y_parity.py").write_text("")
self.assertEqual(sorted(parity.harnesses()), ["y"])
self.assertEqual(parity.MANUAL, {"x": "--side java, then --side native"})
class OracleArgumentTests(unittest.TestCase):
def test_an_agent_jar_is_an_input(self):
+11
View File
@@ -72,5 +72,16 @@ class SaveRoundtripPathsTests(unittest.TestCase):
self.assertEqual(roundtrip.summarize(report), 2)
class SaveRoundtripTimersTests(unittest.TestCase):
def test_elapsed_despawn_timers_do_not_hide_other_saved_state_changes(self):
report = [
('entity.DespawnDelay @minecraft:trader_llama#57', 47384, 47375),
('entity.Passengers[].DespawnDelay @minecraft:trader_llama#58', 100, 91),
('entity.PersistenceRequired @minecraft:trader_llama#57', 0, 1),
]
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(roundtrip.summarize(report), 1)
if __name__ == '__main__':
unittest.main()
+13 -4
View File
@@ -90,27 +90,33 @@ FAILURE_TAILS, FAILURE_TAIL_LINES = 10, 30 # the summary prints the end of the
def harness_settings(path):
"""PARITY_SHARDS and PARITY_RUST_SCOPE, read from a harness's source without running it."""
"""PARITY_SHARDS, PARITY_RUST_SCOPE and PARITY_MANUAL, read from a harness's source without running it."""
import ast
text = Path(path).read_text(encoding="utf-8")
settings = {}
if "PARITY_" in text:
for node in ast.parse(text).body:
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) \
and node.targets[0].id in ("PARITY_SHARDS", "PARITY_RUST_SCOPE"):
and node.targets[0].id in ("PARITY_SHARDS", "PARITY_RUST_SCOPE", "PARITY_MANUAL"):
settings[node.targets[0].id] = ast.literal_eval(node.value)
return settings
SHARDS = {} # "datafix#2" -> "2/8": the share of a sharded harness each entry runs (PARITY_SHARD)
MANUAL = {} # "guest_close_event" -> how to run it: a proof script that needs arguments, which no run starts
def harnesses():
"""Harness names and scripts. A harness declaring PARITY_SHARDS = N runs as N entries, NAME#1 to NAME#N."""
"""Harness names and scripts. A harness declaring PARITY_SHARDS = N runs as N entries, NAME#1 to NAME#N.
A script declaring PARITY_MANUAL = "<how to run it>" is a proof run by hand with arguments: left out (MANUAL)."""
found = {}
for path in sorted((ROOT / "tools").glob("test_*_parity.py")):
name = path.name[len("test_"):-len("_parity.py")]
count = harness_settings(path).get("PARITY_SHARDS", 1)
settings = harness_settings(path)
if settings.get("PARITY_MANUAL"):
MANUAL[name] = settings["PARITY_MANUAL"]
continue
count = settings.get("PARITY_SHARDS", 1)
if count > 1:
for index in range(1, count + 1):
found[f"{name}#{index}"] = path
@@ -1652,6 +1658,9 @@ def main():
# A sharded harness's name stands for all its shards.
args.names = [entry for name in args.names for entry in ([name] if name in found else
[shard for shard in found if base_name(shard) == name] or [name])]
by_hand = [name for name in args.names if name in MANUAL]
if by_hand:
parser.error("; ".join(f"{name} is run by hand, not by parity.py: {MANUAL[name]}" for name in by_hand))
unknown = [name for name in args.names if name not in found]
if unknown:
parser.error("unknown harnesses: " + ", ".join(unknown))
+4 -2
View File
@@ -16,7 +16,8 @@ except ImportError:
import lab
import test_rust_parity as base
TEST = 'level::tests::fresh_chunk_'
# The sculk regression lives in level::tests::trigger_tests after the test split.
TEST = 'fresh_chunk_'
def main():
@@ -41,7 +42,8 @@ def main():
lab.ROOT / 'crates/horizon-world/src/level/mod.rs',
lab.ROOT / 'crates/horizon-world/src/level/pistons.rs',
lab.ROOT / 'crates/horizon-world/src/level/sculk.rs',
lab.ROOT / 'crates/horizon-world/src/level/tests.rs']
lab.ROOT / 'crates/horizon-world/src/level/tests.rs',
lab.ROOT / 'crates/horizon-world/src/level/tests/trigger_tests.rs']
hashes = base.source_hashes(sources)
def run(command, label, **kwargs):
@@ -17,6 +17,7 @@ import struct
import subprocess
import time
import uuid
PARITY_MANUAL = "a proof run by hand, once per side: --side java, then --side native, each with --binary BIN" # not a harness: tools/parity.py leaves it out
try:
from . import anvil_dump as nbt, net_capture, test_locator_bar_parity as net
except ImportError:
@@ -1,6 +1,7 @@
"""Actual named movement predicates over a frozen, UUID-bound unmounted pig."""
import argparse,hashlib,json,sys
from pathlib import Path
PARITY_MANUAL = "a proof run by hand: --work-dir DIR --reference-only for the Java side, then --work-dir DIR2 --reference-dir DIR for the native one" # not a harness: tools/parity.py leaves it out
try:
from . import test_command_exec_parity as driver
except ImportError:
+1
View File
@@ -11,6 +11,7 @@ import json
from pathlib import Path
import subprocess
import sys
PARITY_MANUAL = "a proof run by hand: --side java --work-dir DIR, then --side native --work-dir DIR2 --binary BIN --reference DIR/vanilla.jsonl" # not a harness: tools/parity.py leaves it out
ROOT = Path(__file__).resolve().parents[1]
@@ -14,6 +14,7 @@ import hashlib
import struct
import uuid
import re
PARITY_MANUAL = "a proof run by hand, once per side: --side java, then --side native, each with --binary BIN" # not a harness: tools/parity.py leaves it out
try:
from . import anvil_dump as nbt, net_capture, test_locator_bar_parity as net
+9 -7
View File
@@ -24,18 +24,20 @@ except ImportError:
NAME = 'mob-display-name-parity'
SCRIPT = [
('setup', ['chat on']),
('summon', ['cmd Alice summon minecraft:wolf 2.5 -60 4.5 {NoAI:1b}']),
('named', ['cmd Alice summon minecraft:pig 4.5 -60 4.5 {NoAI:1b,CustomName:"Bob"}']),
# Only the script's own mobs may match its selectors. Both servers load a
# real generated world and may otherwise spawn different numbers of pigs.
('setup', ['chat on', 'cmd Alice gamerule spawn_mobs false']),
('summon', ['cmd Alice summon minecraft:wolf 2.5 -60 4.5 {NoAI:1b,Tags:["display_name_parity"]}']),
('named', ['cmd Alice summon minecraft:pig 4.5 -60 4.5 {NoAI:1b,CustomName:"Bob",Tags:["display_name_parity"]}']),
# /team names the team with PlayerTeam.getFormattedDisplayName and its member with
# ScoreHolder.getFeedbackDisplayName (the display name, its scoreboard name shown on hover).
('team', ['cmd Alice team add reds', 'cmd Alice team modify reds color red',
'cmd Alice team join reds @e[type=minecraft:pig]']),
('data', ['cmd Alice data get entity @e[type=minecraft:pig,limit=1] Health']),
('kill', ['cmd Alice kill @e[type=minecraft:wolf]']),
'cmd Alice team join reds @e[type=minecraft:pig,tag=display_name_parity]']),
('data', ['cmd Alice data get entity @e[type=minecraft:pig,tag=display_name_parity,limit=1] Health']),
('kill', ['cmd Alice kill @e[type=minecraft:wolf,tag=display_name_parity]']),
# /scoreboard names score holders with getFeedbackDisplayName too: an entity's UUID, a player's name on hover.
('scores', ['cmd Alice scoreboard objectives add points dummy',
'cmd Alice scoreboard players set @e[type=minecraft:pig] points 3',
'cmd Alice scoreboard players set @e[type=minecraft:pig,tag=display_name_parity] points 3',
'cmd Alice scoreboard players set Alice points 1']),
]
INTS = re.compile(r'"uuid":\[(-?\d+),(-?\d+),(-?\d+),(-?\d+)\]')
@@ -31,6 +31,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import lab # noqa: E402
PARITY_MANUAL = "a proof run by hand: --kind java --output DIR, then --kind native --output DIR2 --binary BIN" # not a harness: tools/parity.py leaves it out
META = '{"pack":{"description":"predicate reference graph","min_format":[121,0],"max_format":[121,0]}}\n'
# 26.3 dispatches a predicate on `type`; a named reference is a holder: an id where a predicate goes.
+1 -1
View File
@@ -86,7 +86,7 @@ IGNORED = {
r'server_data\.state_updating_resumes_at)$':
'block entity timers advance',
r'^entity\.(Passengers\[\]\.)*(Age|Motion\[\]|Rotation\[\]|Pos\[\]|OnGround|HurtTime|Health|TimeInOverworld|fall_distance|'
r'wasOnGround|Fire|Air|DeathTime|Fuse|life|inGround|ticks_in_ground|Time|EggLayTime)$': 'entity simulation advances',
r'wasOnGround|Fire|Air|DeathTime|Fuse|life|inGround|ticks_in_ground|Time|EggLayTime|DespawnDelay)$': 'entity simulation advances',
r'^level\.dat\.Data\.(Time|LastPlayed)$': 'clock and wall time of the save',
r'^level\.dat\.Data\.(ServerBrands.*|singleplayer_uuid)$': 'a client world names its brand and its player',
r'^entity\.listener\.selector\.(event|tick)$': 'the warden listener holds the last vibration it heard; timing',
+1
View File
@@ -4,6 +4,7 @@ Initial default delivery is observational; flags never stand in for a typed Pose
"""
import argparse,hashlib,json,os,re,time
from pathlib import Path
PARITY_MANUAL = "a proof run by hand: --work-dir DIR --reference-only for the Java side, then --work-dir DIR2 --reference-dir DIR --binary BIN" # not a harness: tools/parity.py leaves it out
try:
from . import net_capture,test_locator_bar_parity as net
from .test_builtin_component_delivery_parity import Commands,generate,offline,uuid_words