mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-11 11:27:34 +00:00
638 lines
21 KiB
Python
638 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""Test suite for pa_editor.py — validates all operation types and error handling.
|
|
|
|
Run: python tools/migration/test_pa_editor.py
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PA_EDITOR = os.path.join(SCRIPT_DIR, "pa_editor.py")
|
|
PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))
|
|
|
|
PASS = 0
|
|
FAIL = 0
|
|
|
|
|
|
def test(name: str, passed: bool, detail: str = ""):
|
|
global PASS, FAIL
|
|
if passed:
|
|
PASS += 1
|
|
print(f" PASS {name}")
|
|
else:
|
|
FAIL += 1
|
|
print(f" FAIL {name}" + (f" — {detail}" if detail else ""))
|
|
|
|
|
|
def run_editor(manifest_path: str, *args: str) -> tuple[int, str, str]:
|
|
cmd = [
|
|
sys.executable,
|
|
PA_EDITOR,
|
|
manifest_path,
|
|
*args,
|
|
"--project-root",
|
|
PROJECT_ROOT,
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd=PROJECT_ROOT)
|
|
return result.returncode, result.stdout, result.stderr
|
|
|
|
|
|
def write_file(path: str, content: str):
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write(content)
|
|
|
|
|
|
def read_file(path: str) -> str:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def write_manifest(tmpdir: str, manifest: dict) -> str:
|
|
path = os.path.join(tmpdir, "manifest.json")
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(manifest, f, indent=2)
|
|
return path
|
|
|
|
|
|
SAMPLE_JAVA = """\
|
|
package test;
|
|
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
import net.minecraft.entity.player.EntityPlayer;
|
|
import java.util.List;
|
|
|
|
public class Sample {
|
|
private int id;
|
|
|
|
public void load(NBTTagCompound nbt) {
|
|
id = nbt.getInteger("id");
|
|
}
|
|
|
|
public void interact(EntityPlayer player) {
|
|
player.addChatMessage(null);
|
|
}
|
|
}
|
|
"""
|
|
|
|
|
|
def run_tests():
|
|
tmpdir = tempfile.mkdtemp(
|
|
prefix="pa_editor_test_", dir=os.path.join(PROJECT_ROOT, "tools", "migration")
|
|
)
|
|
try:
|
|
_run_all(tmpdir)
|
|
finally:
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
|
|
print(f"\n{'=' * 50}")
|
|
print(f" RESULTS: {PASS} passed, {FAIL} failed")
|
|
print(f"{'=' * 50}")
|
|
return FAIL == 0
|
|
|
|
|
|
def _run_all(tmpdir: str):
|
|
print("\n=== PA Editor Test Suite ===\n")
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 1: Replace by pattern
|
|
# ----------------------------------------------------------------
|
|
f1 = os.path.join(tmpdir, "Replace.java")
|
|
write_file(f1, SAMPLE_JAVA)
|
|
rel1 = os.path.relpath(f1, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel1,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {
|
|
"pattern": "^import net\\.minecraft\\.nbt\\.NBTTagCompound;"
|
|
},
|
|
"content": "import api.INbt;",
|
|
},
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("replace-by-pattern: exit 0", code == 0)
|
|
content = read_file(f1)
|
|
test("replace-by-pattern: import changed", "import api.INbt;" in content)
|
|
test(
|
|
"replace-by-pattern: old import gone",
|
|
"import net.minecraft.nbt.NBTTagCompound;" not in content,
|
|
)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 2: Insert before/after
|
|
# ----------------------------------------------------------------
|
|
f2 = os.path.join(tmpdir, "Insert.java")
|
|
write_file(f2, SAMPLE_JAVA)
|
|
rel2 = os.path.relpath(f2, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel2,
|
|
"ops": [
|
|
{
|
|
"op": "insert",
|
|
"target": {"line": 1},
|
|
"position": "before",
|
|
"content": "// Header comment",
|
|
},
|
|
{
|
|
"op": "insert",
|
|
"target": {"pattern": "^public class"},
|
|
"position": "after",
|
|
"content": " // class body start",
|
|
},
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("insert: exit 0", code == 0)
|
|
content = read_file(f2)
|
|
lines = content.split("\n")
|
|
test("insert-before: first line is comment", lines[0] == "// Header comment")
|
|
test(
|
|
"insert-after: class body comment exists", " // class body start" in content
|
|
)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 3: Delete lines
|
|
# ----------------------------------------------------------------
|
|
f3 = os.path.join(tmpdir, "Delete.java")
|
|
write_file(f3, SAMPLE_JAVA)
|
|
rel3 = os.path.relpath(f3, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel3,
|
|
"ops": [
|
|
{
|
|
"op": "delete",
|
|
"target": {"pattern": "^import net\\.minecraft\\.nbt"},
|
|
"count": 1,
|
|
},
|
|
{
|
|
"op": "delete",
|
|
"target": {"pattern": "^import net\\.minecraft\\.entity"},
|
|
"count": 1,
|
|
},
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("delete: exit 0", code == 0)
|
|
content = read_file(f3)
|
|
test("delete: MC imports removed", "net.minecraft" not in content)
|
|
test("delete: java import preserved", "java.util.List" in content)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 4: Multi-line insert
|
|
# ----------------------------------------------------------------
|
|
f4 = os.path.join(tmpdir, "MultiInsert.java")
|
|
write_file(f4, SAMPLE_JAVA)
|
|
rel4 = os.path.relpath(f4, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel4,
|
|
"ops": [
|
|
{
|
|
"op": "insert",
|
|
"target": {"pattern": "^public class"},
|
|
"position": "before",
|
|
"content": "/**\n * Migrated class.\n * PA types only.\n */",
|
|
},
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, _, _ = run_editor(manifest, "--force")
|
|
test("multi-line-insert: exit 0", code == 0)
|
|
content = read_file(f4)
|
|
test("multi-line-insert: javadoc present", "* Migrated class." in content)
|
|
test("multi-line-insert: javadoc 3 lines", "* PA types only." in content)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 5: Multi-line replace (count > 1)
|
|
# ----------------------------------------------------------------
|
|
f5 = os.path.join(tmpdir, "MultiReplace.java")
|
|
write_file(f5, SAMPLE_JAVA)
|
|
rel5 = os.path.relpath(f5, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel5,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"pattern": "^import net\\.minecraft\\.nbt"},
|
|
"count": 2,
|
|
"content": "import api.INbt;\nimport api.IPlayer;",
|
|
},
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, _, _ = run_editor(manifest, "--force")
|
|
test("multi-replace: exit 0", code == 0)
|
|
content = read_file(f5)
|
|
test(
|
|
"multi-replace: new imports present",
|
|
"import api.INbt;" in content and "import api.IPlayer;" in content,
|
|
)
|
|
test("multi-replace: old imports gone", "net.minecraft" not in content)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 6: Dry-run does not write
|
|
# ----------------------------------------------------------------
|
|
f6 = os.path.join(tmpdir, "DryRun.java")
|
|
write_file(f6, SAMPLE_JAVA)
|
|
rel6 = os.path.relpath(f6, PROJECT_ROOT)
|
|
original = read_file(f6)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel6,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"line": 1},
|
|
"content": "// MODIFIED",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--dry-run")
|
|
test("dry-run: exit 0", code == 0)
|
|
test("dry-run: file unchanged", read_file(f6) == original)
|
|
test("dry-run: output shows DRY RUN", "DRY RUN" in out)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 7: Dry-run JSON output
|
|
# ----------------------------------------------------------------
|
|
code, out, _ = run_editor(manifest, "--dry-run", "--json")
|
|
test("dry-run-json: exit 0", code == 0)
|
|
data = json.loads(out)
|
|
test("dry-run-json: has diffs", "diffs" in data)
|
|
test("dry-run-json: dry_run flag true", data.get("dry_run") is True)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 8: Backup creates .bak files
|
|
# ----------------------------------------------------------------
|
|
f8 = os.path.join(tmpdir, "Backup.java")
|
|
write_file(f8, SAMPLE_JAVA)
|
|
rel8 = os.path.relpath(f8, PROJECT_ROOT)
|
|
original = read_file(f8)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel8,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"line": 1},
|
|
"content": "// MODIFIED",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, _, _ = run_editor(manifest, "--force", "--backup")
|
|
test("backup: exit 0", code == 0)
|
|
test("backup: .bak exists", os.path.isfile(f8 + ".bak"))
|
|
test("backup: .bak has original content", read_file(f8 + ".bak") == original)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 9: Error — file not found
|
|
# ----------------------------------------------------------------
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": os.path.join(tmpdir, "NONEXISTENT.java"),
|
|
"ops": [{"op": "replace", "target": {"line": 1}, "content": "x"}],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("error-file-not-found: exit 1", code == 1)
|
|
test("error-file-not-found: error in output", "file not found" in out)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 10: Error — line out of bounds
|
|
# ----------------------------------------------------------------
|
|
f10 = os.path.join(tmpdir, "OOB.java")
|
|
write_file(f10, SAMPLE_JAVA)
|
|
rel10 = os.path.relpath(f10, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel10,
|
|
"ops": [{"op": "delete", "target": {"line": 9999}, "count": 1}],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("error-oob: exit 1", code == 1)
|
|
test("error-oob: error mentions bounds", "out of bounds" in out)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 11: Error — pattern not found
|
|
# ----------------------------------------------------------------
|
|
f11 = os.path.join(tmpdir, "PatternFail.java")
|
|
write_file(f11, SAMPLE_JAVA)
|
|
rel11 = os.path.relpath(f11, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel11,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"pattern": "WILL_NEVER_MATCH"},
|
|
"content": "x",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("error-pattern-not-found: exit 1", code == 1)
|
|
test("error-pattern-not-found: error mentions pattern", "not found" in out)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 12: Error — invalid regex
|
|
# ----------------------------------------------------------------
|
|
f12 = os.path.join(tmpdir, "BadRegex.java")
|
|
write_file(f12, SAMPLE_JAVA)
|
|
rel12 = os.path.relpath(f12, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel12,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"pattern": "[invalid"},
|
|
"content": "x",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("error-bad-regex: exit 1", code == 1)
|
|
test("error-bad-regex: error mentions regex", "invalid regex" in out)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 13: Atomicity — partial failure prevents ALL writes
|
|
# ----------------------------------------------------------------
|
|
f13a = os.path.join(tmpdir, "Atomic_A.java")
|
|
f13b = os.path.join(tmpdir, "Atomic_B.java")
|
|
write_file(f13a, SAMPLE_JAVA)
|
|
write_file(f13b, SAMPLE_JAVA)
|
|
orig_a = read_file(f13a)
|
|
rel13a = os.path.relpath(f13a, PROJECT_ROOT)
|
|
rel13b = os.path.relpath(f13b, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel13a,
|
|
"ops": [
|
|
{
|
|
"op": "insert",
|
|
"target": {"line": 1},
|
|
"position": "before",
|
|
"content": "// SHOULD NOT APPEAR",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"path": rel13b,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"pattern": "WILL_NEVER_MATCH"},
|
|
"content": "x",
|
|
}
|
|
],
|
|
},
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("atomicity: exit 1", code == 1)
|
|
test("atomicity: file A unchanged", read_file(f13a) == orig_a, read_file(f13a)[:50])
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 14: Multi-file success
|
|
# ----------------------------------------------------------------
|
|
f14a = os.path.join(tmpdir, "Multi_A.java")
|
|
f14b = os.path.join(tmpdir, "Multi_B.java")
|
|
f14c = os.path.join(tmpdir, "Multi_C.java")
|
|
write_file(f14a, SAMPLE_JAVA)
|
|
write_file(f14b, SAMPLE_JAVA)
|
|
write_file(f14c, SAMPLE_JAVA)
|
|
rel14a = os.path.relpath(f14a, PROJECT_ROOT)
|
|
rel14b = os.path.relpath(f14b, PROJECT_ROOT)
|
|
rel14c = os.path.relpath(f14c, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel14a,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"pattern": "^import net\\.minecraft\\.nbt"},
|
|
"content": "import api.INbt;",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"path": rel14b,
|
|
"ops": [
|
|
{
|
|
"op": "delete",
|
|
"target": {"pattern": "^import net\\.minecraft\\.entity"},
|
|
"count": 1,
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"path": rel14c,
|
|
"ops": [
|
|
{
|
|
"op": "insert",
|
|
"target": {"line": 1},
|
|
"position": "before",
|
|
"content": "// File C header",
|
|
}
|
|
],
|
|
},
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force", "--json")
|
|
test("multi-file: exit 0", code == 0)
|
|
data = json.loads(out)
|
|
test("multi-file: 3 files modified", data.get("files_modified") == 3)
|
|
test("multi-file A: import replaced", "import api.INbt;" in read_file(f14a))
|
|
test(
|
|
"multi-file B: import deleted",
|
|
"import net.minecraft.entity.player.EntityPlayer;" not in read_file(f14b),
|
|
)
|
|
test("multi-file C: header added", read_file(f14c).startswith("// File C header"))
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 15: Pattern occurrence targeting
|
|
# ----------------------------------------------------------------
|
|
f15 = os.path.join(tmpdir, "Occurrence.java")
|
|
write_file(f15, "line A\nline B\nline A\nline C\nline A\n")
|
|
rel15 = os.path.relpath(f15, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel15,
|
|
"ops": [
|
|
{
|
|
"op": "replace",
|
|
"target": {"pattern": "^line A", "occurrence": 2},
|
|
"content": "line A_REPLACED",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, _, _ = run_editor(manifest, "--force")
|
|
test("occurrence: exit 0", code == 0)
|
|
lines = read_file(f15).split("\n")
|
|
test("occurrence: line 1 unchanged", lines[0] == "line A")
|
|
test("occurrence: line 3 replaced", lines[2] == "line A_REPLACED")
|
|
test("occurrence: line 5 unchanged", lines[4] == "line A")
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 16: Content as list of strings
|
|
# ----------------------------------------------------------------
|
|
f16 = os.path.join(tmpdir, "ListContent.java")
|
|
write_file(f16, SAMPLE_JAVA)
|
|
rel16 = os.path.relpath(f16, PROJECT_ROOT)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": rel16,
|
|
"ops": [
|
|
{
|
|
"op": "insert",
|
|
"target": {"line": 1},
|
|
"position": "before",
|
|
"content": ["// Line 1", "// Line 2", "// Line 3"],
|
|
}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, _, _ = run_editor(manifest, "--force")
|
|
test("list-content: exit 0", code == 0)
|
|
lines = read_file(f16).split("\n")
|
|
test(
|
|
"list-content: 3 lines inserted",
|
|
lines[0] == "// Line 1" and lines[1] == "// Line 2" and lines[2] == "// Line 3",
|
|
)
|
|
|
|
# ----------------------------------------------------------------
|
|
# TEST 17: Structural validation (missing fields)
|
|
# ----------------------------------------------------------------
|
|
manifest = write_manifest(
|
|
tmpdir, {"edits": [{"path": "x.java", "ops": [{"op": "insert"}]}]}
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("validation: missing content/target", code == 1)
|
|
|
|
manifest = write_manifest(
|
|
tmpdir,
|
|
{
|
|
"edits": [
|
|
{
|
|
"path": "x.java",
|
|
"ops": [{"op": "bogus", "target": {"line": 1}, "content": "x"}],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
code, out, _ = run_editor(manifest, "--force")
|
|
test("validation: invalid op name", code == 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ok = run_tests()
|
|
sys.exit(0 if ok else 1)
|