"""Isolated integration checks for tools/prepare_debug_world.py; no real saves are accessed."""
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
import xml.etree.ElementTree as ET
SCRIPT = Path(__file__).resolve().parents[1] / "tools" / "prepare_debug_world.py"
XSI_TYPE = "{http://www.w3.org/2001/XMLSchema-instance}type"
class DebugWorldCopyTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="xfe-world-copy-tests-")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name).resolve()
self.roaming = self.root / "Roaming"
self.local = self.root / "Local"
self.account = self.roaming / "SpaceEngineers" / "Saves" / "76561198000000001"
self.account.mkdir(parents=True)
self.environment = os.environ.copy()
self.environment.update(
APPDATA=str(self.roaming),
LOCALAPPDATA=str(self.local),
PYTHONIOENCODING="utf-8",
)
self.report = self.root / "artifacts" / "preparation.json"
self.config = self.local / "XFE" / "SpaceEngineersAgent" / "config.json"
def world(self, name, modified=1000, ams=True, program_data=False, name_tag=False):
world = self.account / name
world.mkdir()
for filename in ("Sandbox.sbc", "Sandbox_config.sbc"):
root = ET.Element("MyObjectBuilder_Checkpoint")
ET.SubElement(root, "SessionName").text = name
settings = ET.SubElement(root, "Settings")
ET.SubElement(settings, "OnlineMode").text = "PUBLIC"
ET.SubElement(settings, "AutoSaveInMinutes").text = "5"
ET.SubElement(settings, "InventorySizeMultiplier").text = "10"
ET.SubElement(root, "Unrelated").text = "保留其他世界设置"
ET.ElementTree(root).write(world / filename, encoding="utf-8", xml_declaration=True)
sector = ET.Element("MyObjectBuilder_Sector")
entities = ET.SubElement(sector, "SectorObjects")
grid = ET.SubElement(entities, "MyObjectBuilder_EntityBase", {XSI_TYPE: "MyObjectBuilder_CubeGrid"})
ET.SubElement(grid, "EntityId").text = "900000000000000001"
ET.SubElement(grid, "DisplayName").text = "临时矿机测试网格"
blocks = ET.SubElement(grid, "CubeBlocks")
block = ET.SubElement(blocks, "MyObjectBuilder_CubeBlock", {XSI_TYPE: "MyObjectBuilder_MyProgrammableBlock"})
ET.SubElement(block, "EntityId").text = "900000000000000002"
ET.SubElement(block, "CustomName").text = "XFEAMS 测试矿机" if name_tag else "临时可编程方块"
code = "// XFE AMS 2.0\npublic void Main() { Echo(\"测试\"); }" if ams else "public void Main() { }"
ET.SubElement(block, "ProgramData" if program_data else "Program").text = code
ET.SubElement(block, "CustomData").text = "目标矿种=\n优先矿种=Iron,Cobalt,Nickel,Silicon"
ET.ElementTree(sector).write(world / "SANDBOX_0_0_0_.sbs", encoding="utf-8", xml_declaration=True)
(world / "Asteroid.vx2").write_bytes(b"\x00\x01fixture-voxel\xff\x80")
(world / "Extra").mkdir()
(world / "Extra" / "settings.json").write_text('{"unchanged":"metadata"}', encoding="utf-8")
os.utime(world / "Sandbox.sbc", (modified, modified))
return world
@staticmethod
def snapshot(world):
return {
str(path.relative_to(world)): (
hashlib.sha256(path.read_bytes()).hexdigest(),
path.stat().st_size,
path.stat().st_mtime_ns,
)
for path in world.rglob("*")
if path.is_file()
}
def run_copy(self, source=None, output=None, succeeds=True):
command = [sys.executable, str(SCRIPT), "--out", str(output or self.report)]
if source is not None:
command += ["--source", str(source)]
completed = subprocess.run(
command,
cwd=self.root,
env=self.environment,
text=True,
encoding="utf-8",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=20,
check=False,
)
if succeeds:
self.assertEqual(completed.returncode, 0, completed.stderr)
response = json.loads(completed.stdout)
self.assertTrue(Path(response["debugWorld"]).is_relative_to(self.root))
return response, json.loads((output or self.report).read_text(encoding="utf-8"))
self.assertNotEqual(completed.returncode, 0, "Copy command unexpectedly succeeded")
return completed
def test_automatic_selection_uses_newest_ams_and_skips_debug_copies(self):
old = self.world("旧 AMS", modified=1000)
newest_ams = self.world("新 AMS", modified=2000)
newer_plain = self.world("更晚但没有 AMS", modified=3000, ams=False)
previous_debug = self.world("XFE Agent Debug AMS previous", modified=4000)
snapshots = {path: self.snapshot(path) for path in (old, newest_ams, newer_plain, previous_debug)}
response, report = self.run_copy()
self.assertEqual(Path(response["original"]), newest_ams)
self.assertEqual(Path(report["original"]), newest_ams)
self.assertEqual(response["programCount"], 1)
for path, snapshot in snapshots.items():
self.assertEqual(self.snapshot(path), snapshot)
def test_copy_changes_session_settings_only_in_new_world(self):
source = self.world("Original AMS")
original = self.snapshot(source)
response, report = self.run_copy(source)
target = Path(response["debugWorld"])
self.assertNotEqual(target, source)
self.assertEqual(target.parent, source.parent)
self.assertEqual(self.snapshot(source), original)
for filename in ("Sandbox.sbc", "Sandbox_config.sbc"):
with self.subTest(filename=filename):
original_xml = ET.parse(source / filename).getroot()
copy_xml = ET.parse(target / filename).getroot()
self.assertEqual(original_xml.findtext("Settings/OnlineMode"), "PUBLIC")
self.assertEqual(original_xml.findtext("Settings/AutoSaveInMinutes"), "5")
self.assertEqual(original_xml.findtext("SessionName"), source.name)
self.assertEqual(copy_xml.findtext("Settings/OnlineMode"), "OFFLINE")
self.assertEqual(copy_xml.findtext("Settings/AutoSaveInMinutes"), "0")
self.assertEqual(copy_xml.findtext("SessionName"), target.name)
self.assertEqual(copy_xml.findtext("Unrelated"), original_xml.findtext("Unrelated"))
self.assertEqual(copy_xml.findtext("Settings/InventorySizeMultiplier"), "10")
for filename in ("SANDBOX_0_0_0_.sbs", "Asteroid.vx2", "Extra/settings.json"):
self.assertEqual((target / filename).read_bytes(), (source / filename).read_bytes())
self.assertEqual(report["originalFiles"], {name: state[0] for name, state in original.items()})
block = report["programmableBlocks"][0]
self.assertEqual(block["entityId"], "900000000000000002")
self.assertEqual(block["gridId"], "900000000000000001")
self.assertIn("优先矿种=", block["customData"])
self.assertEqual(len(block["sourceSha256"]), 64)
def test_authorization_appends_copy_and_retains_previous_settings(self):
source = self.world("Authorization AMS")
previous = [str(self.root / "previous authorized test world")]
self.config.parent.mkdir(parents=True)
self.config.write_text(json.dumps({"allowedWorldPaths": previous, "extraSetting": {"retain": True}}), encoding="utf-8-sig")
response, _ = self.run_copy(source)
config = json.loads(self.config.read_text(encoding="utf-8"))
self.assertEqual(config["allowedWorldPaths"], previous + [response["debugWorld"]])
self.assertNotIn(str(source), config["allowedWorldPaths"])
self.assertEqual(config["extraSetting"], {"retain": True})
self.assertFalse(self.config.with_suffix(".tmp").exists())
def test_backups_are_excluded_from_copy_and_manifest_but_preserved_in_source(self):
source = self.world("Backups AMS")
for relative in ("Backup/older-world/old.sbs", "Extra/backup/old.txt"):
path = source / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"preserve original backup")
original = self.snapshot(source)
response, report = self.run_copy(source)
target = Path(response["debugWorld"])
self.assertFalse((target / "Backup").exists())
self.assertFalse((target / "Extra/backup").exists())
self.assertTrue(all("backup" not in [part.lower() for part in Path(name).parts] for name in report["originalFiles"]))
self.assertEqual(self.snapshot(source), original)
def test_explicit_source_takes_precedence_over_newer_world(self):
selected = self.world("Explicit AMS", modified=1000)
self.world("Newer AMS", modified=2000)
response, _ = self.run_copy(selected)
self.assertEqual(Path(response["original"]), selected)
def test_program_data_and_named_ams_block_are_recognized(self):
source = self.world("ProgramData AMS", ams=False, program_data=True, name_tag=True)
response, report = self.run_copy(source)
self.assertEqual(response["programCount"], 1)
self.assertEqual(report["programmableBlocks"][0]["name"], "XFEAMS 测试矿机")
def test_explicit_source_without_ams_fails_without_writes(self):
source = self.world("No AMS", ams=False)
original = self.snapshot(source)
self.config.parent.mkdir(parents=True)
self.config.write_text('{"allowedWorldPaths":[],"retain":true}', encoding="utf-8")
old_config = self.config.read_bytes()
failed = self.run_copy(source, succeeds=False)
self.assertIn("No saved world containing AMS", failed.stderr)
self.assertEqual(self.snapshot(source), original)
self.assertEqual(self.config.read_bytes(), old_config)
self.assertEqual([path.name for path in self.account.iterdir()], [source.name])
self.assertFalse(self.report.exists())
def test_no_matching_automatic_source_fails_without_authorization(self):
source = self.world("Plain world", ams=False)
original = self.snapshot(source)
self.run_copy(succeeds=False)
self.assertFalse(self.config.exists())
self.assertFalse(self.report.exists())
self.assertEqual(self.snapshot(source), original)
def test_report_cannot_overwrite_original_world_file(self):
source = self.world("Protected original AMS")
original = self.snapshot(source)
self.run_copy(source, output=source / "Sandbox.sbc", succeeds=False)
self.assertEqual(self.snapshot(source), original)
self.assertFalse(self.config.exists())
self.assertEqual([path.name for path in self.account.iterdir()], [source.name])
if __name__ == "__main__":
unittest.main()
"""Isolated integration checks for tools/prepare_debug_world.py; no real saves are accessed."""
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
import xml.etree.ElementTree as ET
SCRIPT = Path(__file__).resolve().parents[1] / "tools" / "prepare_debug_world.py"
XSI_TYPE = "{http://www.w3.org/2001/XMLSchema-instance}type"
class DebugWorldCopyTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="xfe-world-copy-tests-")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name).resolve()
self.roaming = self.root / "Roaming"
self.local = self.root / "Local"
self.account = self.roaming / "SpaceEngineers" / "Saves" / "76561198000000001"
self.account.mkdir(parents=True)
self.environment = os.environ.copy()
self.environment.update(
APPDATA=str(self.roaming),
LOCALAPPDATA=str(self.local),
PYTHONIOENCODING="utf-8",
)
self.report = self.root / "artifacts" / "preparation.json"
self.config = self.local / "XFE" / "SpaceEngineersAgent" / "config.json"
def world(self, name, modified=1000, ams=True, program_data=False, name_tag=False):
world = self.account / name
world.mkdir()
for filename in ("Sandbox.sbc", "Sandbox_config.sbc"):
root = ET.Element("MyObjectBuilder_Checkpoint")
ET.SubElement(root, "SessionName").text = name
settings = ET.SubElement(root, "Settings")
ET.SubElement(settings, "OnlineMode").text = "PUBLIC"
ET.SubElement(settings, "AutoSaveInMinutes").text = "5"
ET.SubElement(settings, "InventorySizeMultiplier").text = "10"
ET.SubElement(root, "Unrelated").text = "保留其他世界设置"
ET.ElementTree(root).write(world / filename, encoding="utf-8", xml_declaration=True)
sector = ET.Element("MyObjectBuilder_Sector")
entities = ET.SubElement(sector, "SectorObjects")
grid = ET.SubElement(entities, "MyObjectBuilder_EntityBase", {XSI_TYPE: "MyObjectBuilder_CubeGrid"})
ET.SubElement(grid, "EntityId").text = "900000000000000001"
ET.SubElement(grid, "DisplayName").text = "临时矿机测试网格"
blocks = ET.SubElement(grid, "CubeBlocks")
block = ET.SubElement(blocks, "MyObjectBuilder_CubeBlock", {XSI_TYPE: "MyObjectBuilder_MyProgrammableBlock"})
ET.SubElement(block, "EntityId").text = "900000000000000002"
ET.SubElement(block, "CustomName").text = "XFEAMS 测试矿机" if name_tag else "临时可编程方块"
code = "// XFE AMS 2.0\npublic void Main() { Echo(\"测试\"); }" if ams else "public void Main() { }"
ET.SubElement(block, "ProgramData" if program_data else "Program").text = code
ET.SubElement(block, "CustomData").text = "目标矿种=\n优先矿种=Iron,Cobalt,Nickel,Silicon"
ET.ElementTree(sector).write(world / "SANDBOX_0_0_0_.sbs", encoding="utf-8", xml_declaration=True)
(world / "Asteroid.vx2").write_bytes(b"\x00\x01fixture-voxel\xff\x80")
(world / "Extra").mkdir()
(world / "Extra" / "settings.json").write_text('{"unchanged":"metadata"}', encoding="utf-8")
os.utime(world / "Sandbox.sbc", (modified, modified))
return world
@staticmethod
def snapshot(world):
return {
str(path.relative_to(world)): (
hashlib.sha256(path.read_bytes()).hexdigest(),
path.stat().st_size,
path.stat().st_mtime_ns,
)
for path in world.rglob("*")
if path.is_file()
}
def run_copy(self, source=None, output=None, succeeds=True):
command = [sys.executable, str(SCRIPT), "--out", str(output or self.report)]
if source is not None:
command += ["--source", str(source)]
completed = subprocess.run(
command,
cwd=self.root,
env=self.environment,
text=True,
encoding="utf-8",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=20,
check=False,
)
if succeeds:
self.assertEqual(completed.returncode, 0, completed.stderr)
response = json.loads(completed.stdout)
self.assertTrue(Path(response["debugWorld"]).is_relative_to(self.root))
return response, json.loads((output or self.report).read_text(encoding="utf-8"))
self.assertNotEqual(completed.returncode, 0, "Copy command unexpectedly succeeded")
return completed
def test_automatic_selection_uses_newest_ams_and_skips_debug_copies(self):
old = self.world("旧 AMS", modified=1000)
newest_ams = self.world("新 AMS", modified=2000)
newer_plain = self.world("更晚但没有 AMS", modified=3000, ams=False)
previous_debug = self.world("XFE Agent Debug AMS previous", modified=4000)
snapshots = {path: self.snapshot(path) for path in (old, newest_ams, newer_plain, previous_debug)}
response, report = self.run_copy()
self.assertEqual(Path(response["original"]), newest_ams)
self.assertEqual(Path(report["original"]), newest_ams)
self.assertEqual(response["programCount"], 1)
for path, snapshot in snapshots.items():
self.assertEqual(self.snapshot(path), snapshot)
def test_copy_changes_session_settings_only_in_new_world(self):
source = self.world("Original AMS")
original = self.snapshot(source)
response, report = self.run_copy(source)
target = Path(response["debugWorld"])
self.assertNotEqual(target, source)
self.assertEqual(target.parent, source.parent)
self.assertEqual(self.snapshot(source), original)
for filename in ("Sandbox.sbc", "Sandbox_config.sbc"):
with self.subTest(filename=filename):
original_xml = ET.parse(source / filename).getroot()
copy_xml = ET.parse(target / filename).getroot()
self.assertEqual(original_xml.findtext("Settings/OnlineMode"), "PUBLIC")
self.assertEqual(original_xml.findtext("Settings/AutoSaveInMinutes"), "5")
self.assertEqual(original_xml.findtext("SessionName"), source.name)
self.assertEqual(copy_xml.findtext("Settings/OnlineMode"), "OFFLINE")
self.assertEqual(copy_xml.findtext("Settings/AutoSaveInMinutes"), "0")
self.assertEqual(copy_xml.findtext("SessionName"), target.name)
self.assertEqual(copy_xml.findtext("Unrelated"), original_xml.findtext("Unrelated"))
self.assertEqual(copy_xml.findtext("Settings/InventorySizeMultiplier"), "10")
for filename in ("SANDBOX_0_0_0_.sbs", "Asteroid.vx2", "Extra/settings.json"):
self.assertEqual((target / filename).read_bytes(), (source / filename).read_bytes())
self.assertEqual(report["originalFiles"], {name: state[0] for name, state in original.items()})
block = report["programmableBlocks"][0]
self.assertEqual(block["entityId"], "900000000000000002")
self.assertEqual(block["gridId"], "900000000000000001")
self.assertIn("优先矿种=", block["customData"])
self.assertEqual(len(block["sourceSha256"]), 64)
def test_authorization_appends_copy_and_retains_previous_settings(self):
source = self.world("Authorization AMS")
previous = [str(self.root / "previous authorized test world")]
self.config.parent.mkdir(parents=True)
self.config.write_text(json.dumps({"allowedWorldPaths": previous, "extraSetting": {"retain": True}}), encoding="utf-8-sig")
response, _ = self.run_copy(source)
config = json.loads(self.config.read_text(encoding="utf-8"))
self.assertEqual(config["allowedWorldPaths"], previous + [response["debugWorld"]])
self.assertNotIn(str(source), config["allowedWorldPaths"])
self.assertEqual(config["extraSetting"], {"retain": True})
self.assertFalse(self.config.with_suffix(".tmp").exists())
def test_backups_are_excluded_from_copy_and_manifest_but_preserved_in_source(self):
source = self.world("Backups AMS")
for relative in ("Backup/older-world/old.sbs", "Extra/backup/old.txt"):
path = source / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"preserve original backup")
original = self.snapshot(source)
response, report = self.run_copy(source)
target = Path(response["debugWorld"])
self.assertFalse((target / "Backup").exists())
self.assertFalse((target / "Extra/backup").exists())
self.assertTrue(all("backup" not in [part.lower() for part in Path(name).parts] for name in report["originalFiles"]))
self.assertEqual(self.snapshot(source), original)
def test_explicit_source_takes_precedence_over_newer_world(self):
selected = self.world("Explicit AMS", modified=1000)
self.world("Newer AMS", modified=2000)
response, _ = self.run_copy(selected)
self.assertEqual(Path(response["original"]), selected)
def test_program_data_and_named_ams_block_are_recognized(self):
source = self.world("ProgramData AMS", ams=False, program_data=True, name_tag=True)
response, report = self.run_copy(source)
self.assertEqual(response["programCount"], 1)
self.assertEqual(report["programmableBlocks"][0]["name"], "XFEAMS 测试矿机")
def test_explicit_source_without_ams_fails_without_writes(self):
source = self.world("No AMS", ams=False)
original = self.snapshot(source)
self.config.parent.mkdir(parents=True)
self.config.write_text('{"allowedWorldPaths":[],"retain":true}', encoding="utf-8")
old_config = self.config.read_bytes()
failed = self.run_copy(source, succeeds=False)
self.assertIn("No saved world containing AMS", failed.stderr)
self.assertEqual(self.snapshot(source), original)
self.assertEqual(self.config.read_bytes(), old_config)
self.assertEqual([path.name for path in self.account.iterdir()], [source.name])
self.assertFalse(self.report.exists())
def test_no_matching_automatic_source_fails_without_authorization(self):
source = self.world("Plain world", ams=False)
original = self.snapshot(source)
self.run_copy(succeeds=False)
self.assertFalse(self.config.exists())
self.assertFalse(self.report.exists())
self.assertEqual(self.snapshot(source), original)
def test_report_cannot_overwrite_original_world_file(self):
source = self.world("Protected original AMS")
original = self.snapshot(source)
self.run_copy(source, output=source / "Sandbox.sbc", succeeds=False)
self.assertEqual(self.snapshot(source), original)
self.assertFalse(self.config.exists())
self.assertEqual([path.name for path in self.account.iterdir()], [source.name])
if __name__ == "__main__":
unittest.main()