Compare commits

...
Author SHA1 Message Date
Ludovic MarieandPx228-Da-Da a3ac43ff5d Add worldmap 0.5.0
Submitted by @Px228-Da-Da through the form.

Submission: jeIDcPjSgm8DjiP8ikvZ8ItoBJa-EzlI3_0nDu_ql5E
Co-authored-by: Px228-Da-Da <[email protected]>
2026-11-06 19:17:20 +01:00
Ludovic MarieandPx228-Da-Da 57e676ad49 Add worldmap 0.5.0
Submitted by @Px228-Da-Da through the form.

Submission: jeIDcPjSgm8DjiP8ikvZ8ItoBJa-EzlI3_0nDu_ql5E
Co-authored-by: Px228-Da-Da <[email protected]>
2026-11-06 19:17:18 +01:00
Ludovic MarieandPx228-Da-Da 8ba9a1d9ea Add worldmap 0.5.0
Submitted by @Px228-Da-Da through the form.

Submission: jeIDcPjSgm8DjiP8ikvZ8ItoBJa-EzlI3_0nDu_ql5E
Co-authored-by: Px228-Da-Da <[email protected]>
2026-11-06 19:17:16 +01:00
Ludovic MarieandPx228-Da-Da 1b18062368 Add worldmap 0.5.0
Submitted by @Px228-Da-Da through the form.

Submission: jeIDcPjSgm8DjiP8ikvZ8ItoBJa-EzlI3_0nDu_ql5E
Co-authored-by: Px228-Da-Da <[email protected]>
2026-11-06 19:17:14 +01:00
Ludovic MarieandPx228-Da-Da 93c34ad534 Add worldmap 0.5.0
Submitted by @Px228-Da-Da through the form.

Submission: jeIDcPjSgm8DjiP8ikvZ8ItoBJa-EzlI3_0nDu_ql5E
Co-authored-by: Px228-Da-Da <[email protected]>
2026-11-06 19:17:12 +01:00
d2de4c18de native health: cargo check the mods that apply, and mark those whose own code doesn't compile (#313)
* native health: also cargo check, and mark the mods whose own code doesn't compile

The daily check only applied the mods, so versions that apply but don't
compile on the newest Nightly (eatinganimation's ItemRenderState, hitbox_plus's
module path, carryblock's removed Dim::min_y...) weren't marked broken_on,
while every player's build leaves them out.

After the apply pass, the mods that apply are checked together (cargo check,
with Horizon's `mods` feature) in rounds: each round's errors are traced to the
mod whose helper or support module holds them (scripts/native_trace.py, the
launcher's rule), those mods and the ones needing them leave, and the rest
compile again. An error of the kind two mods cause together (a name defined
twice...) is checked again with that mod alone, so a pair is never marked.
Mods that use an API the launcher prepares ([api], trait_method) aren't
compiled here. Dependencies and horizon-mixins come from the Actions cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* native health: trace helpers whose signature holds an array type, mask each file once

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* native health: an error in the call horizon-mixins generates for a helper is the helper's mod's

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Horizon agent <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-06 00:53:49 +01:00
037298b5a7 native health: mark only the mods that don't apply with just what they need (#309)
The first daily run (42099210086) marked horizon_physics broken for an
E_OVERWRITE_CONFLICT with another mod: a pair conflict, not a mod that
fails on the Nightly. Each mod refused in the full set is now applied
again with only the mods it needs, and is broken on that Horizon only if
horizon-mixins still refuses it.

Co-authored-by: Horizon agent <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-05 21:19:25 +01:00
10 changed files with 488 additions and 19 deletions

No files matched your search

+21 -3
View File
@@ -1,9 +1,10 @@
# Native mods on the newest Nightly, as players' launchers build them (scripts/native_compat.py):
# - on a pull request, each new native-mod version, applied together with the most downloaded native mods
# and compiled (`cargo check`); its support modules' names against every registry mod's;
# - every day after the Nightly, every native mod's newest version, applied: the ones horizon-mixins
# refuses go to the `native-health` release, which publish adds to the index (`broken_on`) so the
# launcher warns before an install.
# - every day after the Nightly, every native mod's newest version, applied, then the rest compiled
# (`cargo check`): the ones horizon-mixins refuses alone and the ones whose own code doesn't compile go
# to the `native-health` release, which publish adds to the index (`broken_on`) so the launcher warns
# before an install.
# The check compiles the submission, whose crates may have build scripts: it runs with a token that can
# only read, and main's script, never the pull request's.
name: native-compat
@@ -18,6 +19,9 @@ on:
horizon:
description: "Horizon commit, or `latest` (the newest Nightly)"
default: latest
upload:
description: "Publish the result (native-health release); `false` for a trial run"
default: "true"
permissions:
contents: read
concurrency:
@@ -69,7 +73,20 @@ jobs:
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
- run: python3 -m unittest discover -s scripts -p 'test_*.py'
- run: rustup toolchain install $RUST --profile minimal && rustup default $RUST
- run: sudo apt-get update && sudo apt-get install -y libasound2-dev libudev-dev pkg-config
# Yesterday's dependencies, checked, and horizon-mixins: only Horizon's own crates and the mods compile again.
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
native/target/compat/cargo
native/target/horizon-mixins
key: native-health-${{ env.RUST }}-${{ github.run_id }}
restore-keys: native-health-${{ env.RUST }}-
- name: Every native mod's newest version on the newest Nightly
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -78,6 +95,7 @@ jobs:
set -o pipefail
python3 scripts/native_compat.py health --horizon "$HORIZON" --out native-health.json | tee -a "$GITHUB_STEP_SUMMARY"
- name: Keep it for publish
if: inputs.upload != 'false'
run: |
gh release view native-health >/dev/null 2>&1 \
|| gh release create native-health --prerelease --title "Native mods' health" \
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+43
View File
@@ -0,0 +1,43 @@
id = "worldmap"
kind = "native-mod"
name = "World Map"
summary = "A full screen, self-writing world map: cave layers, waypoints, dimensions, teleport and PNG export."
description = """
A full screen map that writes itself as you explore.
- **Self-writing map.** The terrain you have seen stays on the map after you leave, kept per world, dimension and cave layer, in the vanilla map colours.
- **Smooth.** Drag to move, scroll to zoom towards the pointer; far zoom blurs instead of flickering.
- **Cave layers.** Under a roof, in a cave or in the Nether the map shows the floor round you; layered (a map for each 16 blocks of height) or full, with a Top Y slider from -64 to 319.
- **Dimensions.** One button shows the Overworld, the Nether or the End from any of them, with the other world's coordinates of the place under the pointer.
- **Waypoints.** Right-click for a menu: add, rename, 16 colours, three shapes, teleport (/tp), delete. Waypoints show in the world with their name and distance, a waypoint is made where you die, and a list with search (L) finds them.
- **Also:** the biome under the pointer, other players on the map, go to coordinates (J), a chunk grid, your trail, and an export of the whole map as a PNG.
- **Keys.** A World Map section in Options > Controls > Key Binds (with Reset), ten keys, all rebindable. Open the map with M.
- **Russian** interface when the game is in Russian.
**What it does and doesn't do:** it reads the chunks the game has loaded around you and the game's own state, draws on its own screen, and keeps files in `config/worldmap/` (the explored chunks and a waypoints file for each world). It sends `/tp` in chat when you choose Teleport. It makes no network connection and needs no other mod.
**Native mod, pinned** to Horizon Nightly `66f3981` (Minecraft 26.3): it adds rows to the game's Key Binds screen and reuses the minimap's colour scan, so it builds on that revision."""
authors = ["Px228-Da-Da"]
owners = ["Px228-Da-Da"]
owner_ids = [160890712]
license = "MIT"
source = "https://github.com/Px228-Da-Da/horizon-worldmap"
icon = "icons/worldmap.png"
categories = ["utility", "adventure"]
tags = ["map", "worldmap", "waypoints", "navigation", "caves", "minimap"]
screenshots = ["screenshots/worldmap/1.png", "screenshots/worldmap/2.png", "screenshots/worldmap/3.png"]
[[versions]]
version = "0.5.0"
published = "2026-11-06"
game = "26.3"
file = "worldmap-0.5.0.hnmod"
sha256 = "970b1186fdcd9f7b479e4a838747489a2f286b7a7dcf5cfff76bc912a2c5c7dd"
changelog = """
First public release.
- Self-writing full screen map, kept per world, dimension and cave layer
- Smooth drag and zoom, cave layers with a Top Y slider, dimension button
- Waypoints (colours, shapes, teleport, death waypoint) with a searchable list
- Biome, other players, go to coordinates, chunk grid, PNG export
- Ten rebindable keys in Key Binds, a settings page, Russian interface"""
Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.
+123 -16
View File
@@ -13,8 +13,10 @@
Popular mods that don't apply on that Nightly are left out, as the launcher leaves them out, and listed.
`health` applies every native mod's newest version on the newest Nightly and writes the ones horizon-mixins
refuses, with its diagnostic: the publish job adds them to the index (`broken_on`), and the launcher warns
before an install.
refuses with only the mods they need; then `cargo check`s the rest together, rounds leaving out the mods the errors
are in (scripts/native_trace.py, as the launcher traces them), and writes those too, each with its first error. An
error of the kind two mods cause together (a name defined twice...) is checked again with that mod alone. The publish
job adds them all to the index (`broken_on`), and the launcher warns before an install.
horizon-mixins only reads the bundles; `cargo check` runs a mod crate's build script, if it has one, so
`check` runs in a job whose token can only read (native-compat.yml). Horizon's source comes from GitHub's archive.
@@ -34,6 +36,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "native" / "tools"))
import native # noqa: E402 (native/tools/native.py: source, latest_nightly, get, log)
import native_trace # noqa: E402
INDEX = "https://horizon-rust.github.io/horizon-mods/index.json"
REGISTRY = "horizon-rust/horizon-mods"
@@ -60,6 +63,10 @@ class Bundle:
self.modules = {(m["file"], m["name"]) for m in manifest.get("module", [])}
self.needs = [d for d, v in manifest.get("dependencies", {}).items()
if not (isinstance(v, dict) and v.get("optional"))]
# The launcher rewrites these before horizon-mixins and stages API packages for them (native_mods/inject.rs,
# api.rs), which this script doesn't: they are applied here, not compiled.
self.api = "api" in manifest or any(isinstance(i, dict) and "trait_method" in i
for i in manifest.get("injection", []))
def unpack(self, into, revision):
out = into / self.id
@@ -172,15 +179,85 @@ def apply(tool, tree, bundles, revision, name):
return None, refused
def compiles(stage):
"""`cargo check` of the game in `stage`, as the launcher compiles it."""
# Errors two mods' code can cause together, while each alone compiles.
PAIR_CODES = {"E0119", "E0201", "E0252", "E0254", "E0255", "E0260", "E0428", "E0592"}
COMPILE_ROUNDS = 6
def check_errors(stage):
"""`cargo check` of the game in `stage` as the launcher compiles it (with Horizon's `mods` feature when it has
one): None when it compiles, else its errors (native_trace.errors). The workspace is synced into one folder
first, so cargo's incremental state survives between rounds."""
work = WORK / "check"
work.mkdir(parents=True, exist_ok=True)
subprocess.run(["rsync", "-a", "--checksum", "--delete", f"{stage}/", f"{work}/"], check=True)
command = ["cargo", "check", "--locked", "--message-format=json", "-p", "horizon-client", "--bin", "horizon-client"]
client = tomllib.loads((work / "crates/horizon-client/Cargo.toml").read_text())
if "mods" in client.get("features", {}):
command += ["--features", "horizon-client/mods"]
env = dict(os.environ, CARGO_TARGET_DIR=str(WORK / "cargo"))
run = subprocess.run(["cargo", "check", "--locked", "-p", "horizon-client", "--bin", "horizon-client"],
cwd=stage, env=env, capture_output=True, text=True)
if run.returncode != 0:
errors = [line for line in run.stderr.splitlines() if line.startswith("error")]
log("\n".join(errors[:10]) or run.stderr[-3000:])
return run.returncode == 0
run = subprocess.run(command, cwd=work, env=env, capture_output=True, text=True)
if run.returncode == 0:
return None
found = native_trace.errors(run.stdout)
for e in found[:10]:
log(e["rendered"].rstrip())
return found or [{"code": "E_CARGO", "message": run.stderr[-2000:], "rendered": run.stderr[-2000:], "spans": []}]
def compiles(stage):
return stage is not None and check_errors(stage) is None
def traced(errors, tree, mods):
"""{id: its first error} for the errors in `mods`' own code (native_trace)."""
tracer = native_trace.Tracer(WORK / "check", tree, {(b.id, b.version) for b in mods})
found = {}
for e in errors:
for span in e["spans"]:
owner = tracer.owner(span)
if owner and owner not in found:
where = f" ({span['file_name']}:{span.get('line_start')})"
found[owner] = {"code": e["code"], "message": e["message"] + where}
return found
def broken_compiling(tool, tree, mods, revision, needed):
"""The mods of `mods` (all applying) whose own code doesn't compile on `revision`: {id: error}."""
by_id = {b.id: b for b in mods}
left, broken = list(mods), {}
for _ in range(COMPILE_ROUNDS):
stage, _ = apply(tool, tree, left, revision, "health-compile")
if stage is None:
break
errors = check_errors(stage)
if errors is None:
log(f" {len(left)} native mods compile together")
return broken
found = traced(errors, tree, left)
if not found:
log(" the errors are in no mod's code: stopping the compile check here")
break
for i, error in sorted(found.items()):
if error["code"] in PAIR_CODES:
alone = [by_id[n] for n in sorted(needed(by_id[i], set())) if n in by_id] + [by_id[i]]
stage, _ = apply(tool, tree, alone, revision, f"health-alone-{i}")
if stage is None or i not in traced(check_errors(stage) or [], tree, alone):
log(f" {i}: {error['code']} only next to other mods: not broken")
continue
log(f" {i} {by_id[i].version}: {error['code']}: {error['message']}")
broken[i] = error
gone = set(found)
while True:
more = {b.id for b in left if b.id not in gone and any(n in gone for n in b.needs)}
if not more:
break
for i in more:
if any(n in broken for n in by_id[i].needs):
broken[i] = {"code": "E_DEPENDENCY", "message": f"needs {', '.join(by_id[i].needs)}"}
gone |= more
left = [b for b in left if b.id not in gone]
return broken
def new_entries(base, head):
@@ -265,17 +342,46 @@ def health(args):
revision = native.latest_nightly() if args.horizon == "latest" else args.horizon
tree = native.source(revision)
tool = mixins_tool([])
_, refused = apply(tool, tree, registry, revision, "health")
versions = {b.id: b.version for b in registry}
_, together = apply(tool, tree, registry, revision, "health")
# A mod refused only next to another (both overwrite one function, both name one module) builds for a
# player without that other one: broken on this Horizon is what horizon-mixins refuses with only the mods
# it needs.
by_id = {b.id: b for b in registry}
def needed(bundle, seen):
for n in bundle.needs:
if n in by_id and n not in seen:
seen.add(n)
needed(by_id[n], seen)
return seen
refused = {}
for i in sorted(together):
mods = [by_id[n] for n in sorted(needed(by_id[i], set()))] + [by_id[i]]
_, alone = apply(tool, tree, mods, revision, f"health-{i}")
if i in alone:
refused[i] = alone[i]
else:
log(f" {i} applies without the others: not broken")
# The rest, compiled; without the mods the launcher prepares differently (Bundle.api) and those needing them.
skipped = {b.id for b in registry if b.api}
while more := {b.id for b in registry if b.id not in skipped and any(n in skipped for n in b.needs)}:
skipped |= more
if skipped:
log(f" not compiled here (an API the launcher prepares): {', '.join(sorted(skipped))}")
compiling = [b for b in registry if b.id not in refused and b.id not in skipped]
failed = broken_compiling(tool, tree, compiling, revision, needed) if args.compile else {}
broken = {i: dict(d, stage="apply") for i, d in refused.items()}
broken |= {i: dict(d, stage="compile") for i, d in failed.items()}
report = {
"horizon": revision,
"mixins": tool.parent.parent.name,
"broken": {i: {"version": versions[i], "code": d.get("code"), "message": d.get("message")}
for i, d in sorted(refused.items())},
"broken": {i: {"version": by_id[i].version, "code": d.get("code"), "message": d.get("message"),
"stage": d["stage"]}
for i, d in sorted(broken.items())},
}
Path(args.out).write_text(json.dumps(report, indent=2) + "\n")
print(f"{len(refused)} of {len(registry)} native mods don't apply on Horizon {revision[:7]}: "
+ ", ".join(sorted(refused)))
print(f"{len(broken)} of {len(registry)} native mods don't build on Horizon {revision[:7]}: "
f"{len(refused)} don't apply ({', '.join(sorted(refused))}), {len(failed)} don't compile "
f"({', '.join(sorted(failed))}).")
return 0
@@ -292,6 +398,7 @@ def main():
n.add_argument("head")
h = sub.add_parser("health")
h.add_argument("--out", required=True)
h.add_argument("--no-compile", dest="compile", action="store_false")
for p in (c, h):
p.add_argument("--horizon", default="latest", help="a Horizon commit, or `latest` (the newest Nightly)")
args = parser.parse_args()
+191
View File
@@ -0,0 +1,191 @@
"""Which native mod a compiler error is in, as the launcher traces it (launcher/src/native_mods/diagnostics.rs).
An error is a mod's when its primary span lies inside what that mod brought into the staged game: one of its
helpers (the function of the helper's name, absent from the pristine source) or one of its support modules (the
inline module of its name, absent from the pristine source). An error that more than one mod's code holds, or none,
names no mod.
"""
import json
import re
from pathlib import Path
PLAN = ".horizon-mixins-stage.json"
def masked(src):
"""`src` (bytes) with comments, strings and char literals blanked, so braces and keywords are code's."""
out = bytearray(src)
i, n = 0, len(src)
def blank(a, b):
for k in range(a, min(b, n)):
if out[k] != ord("\n"):
out[k] = ord(" ")
while i < n:
c = src[i]
if src.startswith(b"//", i):
end = src.find(b"\n", i)
end = n if end < 0 else end
blank(i, end)
i = end
elif src.startswith(b"/*", i):
depth, k = 1, i + 2
while k < n and depth:
if src.startswith(b"/*", k):
depth, k = depth + 1, k + 2
elif src.startswith(b"*/", k):
depth, k = depth - 1, k + 2
else:
k += 1
blank(i, k)
i = k
elif c in b"rb" and (i == 0 or not (chr(src[i - 1]).isalnum() or src[i - 1] == ord("_"))) and \
re.match(rb"(?:br|r)(#*)\"", src[i:i + 260]):
hashes = re.match(rb"(?:br|r)(#*)\"", src[i:i + 260])
close = b'"' + hashes.group(1)
end = src.find(close, i + hashes.end())
end = n if end < 0 else end + len(close)
blank(i, end)
i = end
elif c == ord('"'):
k = i + 1
while k < n and src[k] != ord('"'):
k += 2 if src[k] == ord("\\") else 1
blank(i, k + 1)
i = k + 1
elif c == ord("'"):
# A char literal ('x', '\n', '\u{1F600}'), else a lifetime ('a).
if i + 1 < n and src[i + 1] == ord("\\"):
end = src.find(b"'", i + 2)
end = n if end < 0 else end + 1
blank(i, end)
i = end
elif i + 2 < n and src[i + 2] == ord("'"):
blank(i, i + 3)
i += 3
else:
m = re.match(rb"'([^\x00-\x7f]+)'", src[i:i + 8])
if m:
blank(i, i + m.end())
i += m.end()
else:
i += 1
else:
i += 1
return bytes(out)
def item_ranges(code, keyword, name):
"""Byte ranges of the `fn NAME` or `mod NAME { … }` items in masked source `code`, through their closing brace."""
pattern = rb"\b%s\s+%s\b" % (keyword.encode(), re.escape(name.encode()))
ranges = []
for m in re.finditer(pattern, code):
# The body's `{`, unless a `;` ends the item first; either outside the signature's () and [] (`[f64; 3]`).
open_at, nested, k = -1, 0, m.end()
while k < len(code):
c = code[k]
if c in b"([":
nested += 1
elif c in b")]":
nested -= 1
elif nested <= 0 and c in b"{;":
open_at = k if c == ord("{") else -1
break
k += 1
if open_at < 0:
continue # a declaration without a body
if keyword == "mod" and code[m.end():open_at].strip():
continue
depth, k = 0, open_at
while k < len(code):
if code[k] == ord("{"):
depth += 1
elif code[k] == ord("}"):
depth -= 1
if depth == 0:
ranges.append(_with_call(code, name, m.start(), k + 1) if keyword == "fn" else (m.start(), k + 1))
break
k += 1
return ranges
def _with_call(code, name, start, end):
"""The block horizon-mixins wraps a helper in, `{ fn NAME(…) { … } NAME(…) }`, when that is all the block holds:
an error in the generated call (an argument the helper's signature no longer takes) is the helper's too."""
before = re.search(rb"\{\s*(?:#\[[^\]]*\]\s*)*\Z", code[max(0, start - 400):start])
after = re.match(rb"\s*%s\s*\([^{}]*\)\s*;?\s*\}" % re.escape(name.encode()), code[end:])
if before and after:
return start - (len(before.group(0))), end + after.end()
return start, end
def helper_name(source):
m = re.search(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)", source or "")
return m.group(1) if m else None
class Tracer:
"""Traces errors in the staged workspace `stage` (horizon-mixins' output, holding its plan) whose pristine source
is `pristine`, to the mods in `mods` ({(id, version)})."""
def __init__(self, stage, pristine, mods):
self.stage, self.pristine = Path(stage).resolve(), Path(pristine)
plan = json.loads((self.stage / PLAN).read_text())
self.entries = [] # (id, version, file, keyword, name)
for e in plan.get("injections", []):
name = helper_name(e.get("helper_source"))
if name:
self.entries.append((e.get("mod_id"), e.get("mod_version"), e.get("file"), "fn", name))
for e in plan.get("modules", []):
self.entries.append((e.get("mod_id"), e.get("mod_version"), e.get("file"), "mod", e.get("name")))
self.entries = [e for e in self.entries if (e[0], e[1]) in mods and e[2] and e[4]]
self.cache, self.code = {}, {}
def _masked(self, path):
if path not in self.code:
self.code[path] = masked(path.read_bytes()) if path.is_file() else b""
return self.code[path]
def _ranges(self, rel, keyword, name):
key = (rel, keyword, name)
if key not in self.cache:
found = []
if (self.stage / rel).is_file() and not item_ranges(self._masked(self.pristine / rel), keyword, name):
found = item_ranges(self._masked(self.stage / rel), keyword, name)
self.cache[key] = found
return self.cache[key]
def owner(self, span):
"""The mod (id) whose code holds a cargo JSON span, or None."""
try:
path = (self.stage / span["file_name"]).resolve()
rel = path.relative_to(self.stage).as_posix()
except (KeyError, ValueError):
return None
start, end = span.get("byte_start", 0), span.get("byte_end", 0)
owners = {i for i, _, file, keyword, name in self.entries
if file == rel and any(a <= start and end <= b for a, b in self._ranges(rel, keyword, name))}
return owners.pop() if len(owners) == 1 else None
def errors(cargo_stdout):
"""The errors in `cargo --message-format=json` output: {code, message, rendered, spans (primary)}."""
found = []
for line in cargo_stdout.splitlines():
try:
value = json.loads(line)
except ValueError:
continue
if value.get("reason") != "compiler-message":
continue
message = value.get("message") or {}
if message.get("level") != "error":
continue
found.append({
"code": (message.get("code") or {}).get("code") or "E_COMPILE",
"message": message.get("message", ""),
"rendered": message.get("rendered") or "",
"spans": [s for s in message.get("spans", []) if s.get("is_primary")],
})
return found
+110
View File
@@ -0,0 +1,110 @@
"""python3 -m unittest discover -s scripts -p 'test_*.py'"""
import json
import tempfile
import unittest
from pathlib import Path
from native_trace import Tracer, errors, item_ranges, masked
PRISTINE = """struct MeshTables { base: u8, layers: u8 }
pub fn resolve(x: u8) -> u8 {
x
}
"""
# One mod's helper inside a target (formatted with a trailing comma, an array type's `;` in its signature), another's
# support module; braces in strings, chars and comments must not end them early.
STAGED = """struct MeshTables { base: u8, layers: u8 }
pub fn resolve(x: u8) -> u8 {
if let Some(v) = {
fn fast_mesh_resolve(x: u8, _at: [f64; 3],) -> Option<u8> {
let _ = "}"; let _ = '}'; // }
let _ = r#"}"#;
Some(MeshTables { base: x }.base)
}
fast_mesh_resolve(x, [0.0; 3])
} {
return v;
}
x
}
mod carry_tables {
pub fn lift<'a>(t: &'a super::MeshTables) -> u8 { t.missing }
}
fn untouched() -> u8 { MeshTables { base: 1 }.base }
"""
def span(text, needle, file="src/main.rs"):
start = STAGED.encode().index(needle.encode())
return {"file_name": file, "byte_start": start, "byte_end": start + len(needle), "is_primary": True}
class Trace(unittest.TestCase):
def setUp(self):
self.dir = Path(tempfile.mkdtemp())
(self.dir / "stage/src").mkdir(parents=True)
(self.dir / "pristine/src").mkdir(parents=True)
(self.dir / "stage/src/main.rs").write_text(STAGED)
(self.dir / "pristine/src/main.rs").write_text(PRISTINE)
self.plan([
{"mod_id": "fast_mesh", "mod_version": "0.1.0", "file": "src/main.rs",
"helper_source": "fn fast_mesh_resolve(x: u8) -> Option<u8> { Some(MeshTables { base: x }.base) }"},
], [{"mod_id": "carryblock", "mod_version": "0.1.2", "file": "src/main.rs", "name": "carry_tables"}])
self.mods = {("fast_mesh", "0.1.0"), ("carryblock", "0.1.2")}
def plan(self, injections, modules):
(self.dir / "stage/.horizon-mixins-stage.json").write_text(
json.dumps({"schema": 6, "injections": injections, "modules": modules}))
def tracer(self, mods=None):
return Tracer(self.dir / "stage", self.dir / "pristine", mods or self.mods)
def test_an_error_in_a_helper_names_its_mod_though_formatting_changed_it(self):
self.assertEqual(self.tracer().owner(span(STAGED, "MeshTables { base: x }")), "fast_mesh")
def test_an_error_in_a_support_module_names_its_mod(self):
self.assertEqual(self.tracer().owner(span(STAGED, "t.missing")), "carryblock")
def test_an_error_in_the_generated_call_to_a_helper_names_its_mod(self):
self.assertEqual(self.tracer().owner(span(STAGED, "[0.0; 3]")), "fast_mesh")
def test_an_error_in_horizons_own_code_names_no_mod(self):
self.assertIsNone(self.tracer().owner(span(STAGED, "MeshTables { base: 1 }")))
self.assertIsNone(self.tracer().owner(span(STAGED, "return v")))
def test_a_path_through_another_crate_folder_resolves(self):
(self.dir / "stage/lib").mkdir()
s = span(STAGED, "t.missing", file="lib/../src/main.rs")
self.assertEqual(self.tracer().owner(s), "carryblock")
def test_a_function_the_pristine_source_has_is_not_a_mods(self):
(self.dir / "pristine/src/main.rs").write_text(PRISTINE + "fn fast_mesh_resolve() {}\n")
self.assertIsNone(self.tracer().owner(span(STAGED, "MeshTables { base: x }")))
def test_a_mod_not_built_or_two_owners_name_no_mod(self):
self.assertIsNone(self.tracer({("carryblock", "0.1.2")}).owner(span(STAGED, "MeshTables { base: x }")))
self.plan([{"mod_id": m, "mod_version": "1", "file": "src/main.rs",
"helper_source": "fn fast_mesh_resolve() {}"} for m in ("a", "b")], [])
self.assertIsNone(self.tracer({("a", "1"), ("b", "1")}).owner(span(STAGED, "MeshTables { base: x }")))
def test_braces_in_literals_and_comments_are_not_code(self):
code = masked(STAGED.encode())
(start, end), = item_ranges(code, "fn", "fast_mesh_resolve")
self.assertTrue(STAGED.encode()[start:end].endswith(b"}.base)\n }\n fast_mesh_resolve(x, [0.0; 3])\n }"))
self.assertEqual(len(code), len(STAGED.encode()))
def test_cargo_json_errors(self):
out = "\n".join([
"not json",
json.dumps({"reason": "compiler-artifact"}),
json.dumps({"reason": "compiler-message", "message": {"level": "warning", "message": "w", "spans": []}}),
json.dumps({"reason": "compiler-message", "message": {
"level": "error", "message": "missing fields", "code": {"code": "E0063"}, "rendered": "error[E0063]",
"spans": [{"file_name": "a.rs", "is_primary": True}, {"file_name": "b.rs", "is_primary": False}]}}),
])
(only,) = errors(out)
self.assertEqual((only["code"], [s["file_name"] for s in only["spans"]]), ("E0063", ["a.rs"]))
if __name__ == "__main__":
unittest.main()