mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-11 12:37:35 +00:00
1164 lines
38 KiB
Python
1164 lines
38 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
test_analyze.py — Comprehensive test suite for analyze.py subcommands.
|
|
Tests all 6 subcommands with mock index data and real tree-sitter parsing.
|
|
|
|
Run:
|
|
python tools/migration/test_analyze.py
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import tempfile
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
PROJECT_ROOT = SCRIPT_DIR.parent.parent
|
|
ANALYZE_PY = SCRIPT_DIR / "analyze.py"
|
|
|
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
import analyze
|
|
|
|
|
|
MOCK_INDEX = {
|
|
"meta": {
|
|
"scanDir": "src/main/java",
|
|
"projectRoot": str(PROJECT_ROOT),
|
|
"generatedBy": "test_analyze.py",
|
|
"timestamp": "2025-01-01T00:00:00",
|
|
},
|
|
"summary": {
|
|
"totalFiles": 500,
|
|
"filesWithMcDeps": 300,
|
|
"totalMcImports": 1200,
|
|
"uniqueMcTypes": 45,
|
|
"parseErrors": 0,
|
|
"abstractionCoverage": {
|
|
"abstracted": 5,
|
|
"notAbstracted": 40,
|
|
"total": 45,
|
|
"percentage": "11.1%",
|
|
},
|
|
"topTypes": [],
|
|
"topFiles": [],
|
|
"byPackage": {},
|
|
},
|
|
"types": {
|
|
"net.minecraft.entity.player.EntityPlayer": {
|
|
"shortName": "EntityPlayer",
|
|
"package": "net.minecraft.entity.player",
|
|
"importCount": 120,
|
|
"importedBy": [
|
|
"noppes/npcs/controllers/QuestController.java",
|
|
"noppes/npcs/controllers/DialogController.java",
|
|
],
|
|
"paAbstraction": "IPlayer",
|
|
"usage": {
|
|
"extends": [
|
|
{
|
|
"file": "noppes/npcs/entity/FakePlayer.java",
|
|
"class": "FakePlayer",
|
|
}
|
|
],
|
|
"implements": [],
|
|
"constructors": [],
|
|
"casts": [
|
|
{"file": "noppes/npcs/controllers/QuestController.java", "line": 50}
|
|
],
|
|
"instanceof": [
|
|
{"file": "noppes/npcs/controllers/QuestController.java", "line": 42}
|
|
],
|
|
},
|
|
"methodCalls": {
|
|
"getCommandSenderName": {
|
|
"count": 15,
|
|
"files": [
|
|
"noppes/npcs/controllers/QuestController.java",
|
|
"noppes/npcs/controllers/DialogController.java",
|
|
],
|
|
},
|
|
"getEntityId": {
|
|
"count": 8,
|
|
"files": ["noppes/npcs/controllers/QuestController.java"],
|
|
},
|
|
},
|
|
"staticCalls": {},
|
|
"fieldAccesses": {
|
|
"worldObj": {
|
|
"count": 22,
|
|
"files": [
|
|
"noppes/npcs/controllers/QuestController.java",
|
|
"noppes/npcs/controllers/DialogController.java",
|
|
"noppes/npcs/roles/RoleTrader.java",
|
|
],
|
|
}
|
|
},
|
|
},
|
|
"net.minecraft.nbt.NBTTagCompound": {
|
|
"shortName": "NBTTagCompound",
|
|
"package": "net.minecraft.nbt",
|
|
"importCount": 250,
|
|
"importedBy": [
|
|
"noppes/npcs/controllers/QuestController.java",
|
|
"noppes/npcs/controllers/data/Quest.java",
|
|
"core/src/main/java/noppes/npcs/controllers/FactionController.java",
|
|
],
|
|
"paAbstraction": "INbt",
|
|
"usage": {
|
|
"extends": [],
|
|
"implements": [],
|
|
"constructors": [
|
|
{"file": "noppes/npcs/controllers/data/Quest.java", "line": 30}
|
|
],
|
|
"casts": [],
|
|
"instanceof": [],
|
|
},
|
|
"methodCalls": {
|
|
"setString": {
|
|
"count": 80,
|
|
"files": [
|
|
"noppes/npcs/controllers/data/Quest.java",
|
|
"noppes/npcs/controllers/QuestController.java",
|
|
"core/src/main/java/noppes/npcs/controllers/FactionController.java",
|
|
],
|
|
},
|
|
"getString": {
|
|
"count": 60,
|
|
"files": [
|
|
"noppes/npcs/controllers/data/Quest.java",
|
|
"core/src/main/java/noppes/npcs/controllers/FactionController.java",
|
|
],
|
|
},
|
|
"setInteger": {
|
|
"count": 45,
|
|
"files": ["noppes/npcs/controllers/data/Quest.java"],
|
|
},
|
|
},
|
|
"staticCalls": {},
|
|
"fieldAccesses": {},
|
|
},
|
|
"net.minecraft.server.MinecraftServer": {
|
|
"shortName": "MinecraftServer",
|
|
"package": "net.minecraft.server",
|
|
"importCount": 30,
|
|
"importedBy": ["noppes/npcs/CustomNpcs.java"],
|
|
"paAbstraction": None,
|
|
"usage": {
|
|
"extends": [],
|
|
"implements": [],
|
|
"constructors": [],
|
|
"casts": [],
|
|
"instanceof": [],
|
|
},
|
|
"methodCalls": {},
|
|
"staticCalls": {
|
|
"getServer": {
|
|
"count": 10,
|
|
"files": ["noppes/npcs/CustomNpcs.java"],
|
|
}
|
|
},
|
|
"fieldAccesses": {},
|
|
},
|
|
},
|
|
"files": {
|
|
"noppes/npcs/controllers/QuestController.java": {
|
|
"mcImportCount": 5,
|
|
"mcTypes": [
|
|
"EntityPlayer",
|
|
"NBTTagCompound",
|
|
"NBTTagList",
|
|
"ItemStack",
|
|
"MinecraftServer",
|
|
],
|
|
"mcFqns": [
|
|
"net.minecraft.entity.player.EntityPlayer",
|
|
"net.minecraft.nbt.NBTTagCompound",
|
|
"net.minecraft.nbt.NBTTagList",
|
|
"net.minecraft.item.ItemStack",
|
|
"net.minecraft.server.MinecraftServer",
|
|
],
|
|
},
|
|
"noppes/npcs/controllers/DialogController.java": {
|
|
"mcImportCount": 2,
|
|
"mcTypes": ["EntityPlayer", "NBTTagCompound"],
|
|
"mcFqns": [
|
|
"net.minecraft.entity.player.EntityPlayer",
|
|
"net.minecraft.nbt.NBTTagCompound",
|
|
],
|
|
},
|
|
"noppes/npcs/CustomNpcs.java": {
|
|
"mcImportCount": 3,
|
|
"mcTypes": ["MinecraftServer", "NBTTagCompound", "FMLServerStartingEvent"],
|
|
"mcFqns": [
|
|
"net.minecraft.server.MinecraftServer",
|
|
"net.minecraft.nbt.NBTTagCompound",
|
|
"cpw.mods.fml.common.event.FMLServerStartingEvent",
|
|
],
|
|
},
|
|
},
|
|
"hierarchy": {
|
|
"extends": {
|
|
"net.minecraft.entity.player.EntityPlayer": [
|
|
{"file": "noppes/npcs/entity/FakePlayer.java", "class": "FakePlayer"}
|
|
]
|
|
},
|
|
"implements": {},
|
|
},
|
|
}
|
|
|
|
|
|
def _write_mock_index(tmpdir):
|
|
"""Write mock index to a temp directory matching the expected path."""
|
|
idx_dir = Path(tmpdir) / "tools" / "migration"
|
|
idx_dir.mkdir(parents=True, exist_ok=True)
|
|
idx_path = idx_dir / "mc_usage_index.json"
|
|
with open(idx_path, "w", encoding="utf-8") as f:
|
|
json.dump(MOCK_INDEX, f, indent=2)
|
|
return str(tmpdir)
|
|
|
|
|
|
def _make_namespace(overrides):
|
|
"""Build an argparse.Namespace with common defaults."""
|
|
defaults = {
|
|
"project_root": None,
|
|
"index": "tools/migration/mc_usage_index.json",
|
|
"json_output": False,
|
|
"top": 0,
|
|
"methods_only": False,
|
|
"field_accesses_only": False,
|
|
"show_files": False,
|
|
}
|
|
defaults.update(overrides)
|
|
return argparse.Namespace(**defaults)
|
|
|
|
|
|
import argparse
|
|
import io
|
|
from contextlib import redirect_stdout, redirect_stderr
|
|
|
|
|
|
def _capture_cmd(func, args):
|
|
"""Run a command function, capturing stdout/stderr. Returns (returncode, stdout, stderr)."""
|
|
out_buf = io.StringIO()
|
|
err_buf = io.StringIO()
|
|
with redirect_stdout(out_buf), redirect_stderr(err_buf):
|
|
try:
|
|
rc = func(args)
|
|
except SystemExit as e:
|
|
rc = e.code if e.code is not None else 0
|
|
except Exception as e:
|
|
err_buf.write(str(e))
|
|
rc = 1
|
|
return rc, out_buf.getvalue(), err_buf.getvalue()
|
|
|
|
|
|
# ============================================================
|
|
# Tests
|
|
# ============================================================
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
|
|
def check(condition, desc):
|
|
global passed, failed
|
|
if condition:
|
|
passed += 1
|
|
print(f" PASS {desc}")
|
|
else:
|
|
failed += 1
|
|
print(f" FAIL {desc}")
|
|
return condition
|
|
|
|
|
|
def test_query_type_basic():
|
|
"""query-type: finds EntityPlayer, shows PA abstraction and method calls."""
|
|
print("TEST: query-type basic")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "EntityPlayer",
|
|
"fqn": None,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, stderr = _capture_cmd(analyze.cmd_query_type, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("EntityPlayer" in stdout, "Type name in output")
|
|
check("IPlayer" in stdout, "PA abstraction shown")
|
|
check("METHOD CALLS" in stdout, "Method calls section")
|
|
check("getCommandSenderName" in stdout, "Method name present")
|
|
check("FIELD ACCESSES" in stdout, "Field accesses section")
|
|
check("worldObj" in stdout, "Field name present")
|
|
check("EXTENDED BY" in stdout, "Hierarchy section")
|
|
check("Constructors:" in stdout, "Usage summary")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_query_type_by_fqn():
|
|
"""query-type: find by explicit FQN."""
|
|
print("TEST: query-type by FQN")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "EntityPlayer",
|
|
"fqn": "net.minecraft.entity.player.EntityPlayer",
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_query_type, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("net.minecraft.entity.player.EntityPlayer" in stdout, "FQN in output")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_query_type_not_found():
|
|
"""query-type: missing type returns error."""
|
|
print("TEST: query-type not found")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "NonexistentType",
|
|
"fqn": None,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, stderr = _capture_cmd(analyze.cmd_query_type, args)
|
|
check(rc == 1, "Return code 1 for missing type")
|
|
check("not found" in stderr.lower(), "Error message mentions not found")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_query_type_json():
|
|
"""query-type: JSON output mode."""
|
|
print("TEST: query-type JSON output")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "NBTTagCompound",
|
|
"fqn": None,
|
|
"project_root": mock_root,
|
|
"json_output": True,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_query_type, args)
|
|
check(rc == 0, "Return code 0")
|
|
data = json.loads(stdout)
|
|
check(data["fqn"] == "net.minecraft.nbt.NBTTagCompound", "Correct FQN in JSON")
|
|
check(data["paAbstraction"] == "INbt", "PA abstraction in JSON")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_query_file_basic():
|
|
"""query-file: finds QuestController, shows MC types."""
|
|
print("TEST: query-file basic")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"file": "noppes/npcs/controllers/QuestController.java",
|
|
"readiness": False,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_query_file, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("QuestController" in stdout, "File name in output")
|
|
check("MC Import Count: 5" in stdout, "Import count shown")
|
|
check("MC TYPES USED" in stdout, "Types section present")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_query_file_readiness():
|
|
"""query-file: readiness score calculation."""
|
|
print("TEST: query-file readiness")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"file": "noppes/npcs/controllers/QuestController.java",
|
|
"readiness": True,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_query_file, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("MIGRATION READINESS" in stdout, "Readiness header present")
|
|
check("Blocking" in stdout, "Blocking types shown")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_query_file_not_found():
|
|
"""query-file: missing file returns error."""
|
|
print("TEST: query-file not found")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"file": "nonexistent/File.java",
|
|
"readiness": False,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, stderr = _capture_cmd(analyze.cmd_query_file, args)
|
|
check(rc == 1, "Return code 1 for missing file")
|
|
check("not found" in stderr.lower(), "Error message about missing file")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_query_file_json():
|
|
"""query-file: JSON output with readiness."""
|
|
print("TEST: query-file JSON output")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"file": "noppes/npcs/controllers/DialogController.java",
|
|
"readiness": True,
|
|
"project_root": mock_root,
|
|
"json_output": True,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_query_file, args)
|
|
check(rc == 0, "Return code 0")
|
|
data = json.loads(stdout)
|
|
check(data["mcImportCount"] == 2, "Import count correct")
|
|
check("readiness" in data, "Readiness in JSON")
|
|
check(data["readiness"]["score"] == 100.0, "All types abstracted = 100%")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_extract_signatures_real_file():
|
|
"""extract-signatures: parse real PA interface file with tree-sitter."""
|
|
print("TEST: extract-signatures real file")
|
|
parser, lang = analyze.setup_treesitter_parser()
|
|
if parser is None:
|
|
print(" SKIP tree-sitter not available")
|
|
return
|
|
|
|
inbt_path = (
|
|
PROJECT_ROOT
|
|
/ "platform-api"
|
|
/ "src"
|
|
/ "main"
|
|
/ "java"
|
|
/ "noppes"
|
|
/ "npcs"
|
|
/ "api"
|
|
/ "INbt.java"
|
|
)
|
|
if not inbt_path.exists():
|
|
print(f" SKIP {inbt_path} not found")
|
|
return
|
|
|
|
args = _make_namespace(
|
|
{
|
|
"file": str(inbt_path),
|
|
"public_only": False,
|
|
"project_root": str(PROJECT_ROOT),
|
|
}
|
|
)
|
|
rc, stdout, stderr = _capture_cmd(analyze.cmd_extract_signatures, args)
|
|
check(rc == 0, f"Return code 0 (stderr: {stderr[:100]})")
|
|
check("SIGNATURES:" in stdout, "Header present")
|
|
check("Total methods:" in stdout, "Method count shown")
|
|
check("setString" in stdout, "Known method found")
|
|
check("getString" in stdout, "Known getter found")
|
|
check("hasKey" in stdout, "Query method found")
|
|
|
|
|
|
def test_extract_signatures_temp_file():
|
|
"""extract-signatures: parse a temp Java file."""
|
|
print("TEST: extract-signatures temp file")
|
|
parser, lang = analyze.setup_treesitter_parser()
|
|
if parser is None:
|
|
print(" SKIP tree-sitter not available")
|
|
return
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f:
|
|
f.write("""
|
|
package test;
|
|
|
|
public class TestSig {
|
|
/** Constructor doc */
|
|
public TestSig(int x) {}
|
|
|
|
public String getName() { return "test"; }
|
|
|
|
private void helper(int a, String b) {}
|
|
|
|
public static void doStuff(Object... args) {}
|
|
|
|
@Override
|
|
public String toString() { return ""; }
|
|
}
|
|
""")
|
|
tmp = f.name
|
|
|
|
try:
|
|
args = _make_namespace(
|
|
{
|
|
"file": tmp,
|
|
"public_only": False,
|
|
"project_root": str(PROJECT_ROOT),
|
|
}
|
|
)
|
|
rc, stdout, stderr = _capture_cmd(analyze.cmd_extract_signatures, args)
|
|
check(rc == 0, f"Return code 0 (stderr: {stderr[:100]})")
|
|
check("getName" in stdout, "getName method found")
|
|
check("helper" in stdout, "private helper found")
|
|
check("doStuff" in stdout, "static varargs method found")
|
|
check("[constructor]" in stdout, "Constructor detected")
|
|
finally:
|
|
os.unlink(tmp)
|
|
|
|
|
|
def test_extract_signatures_public_only():
|
|
"""extract-signatures: --public-only filters private methods."""
|
|
print("TEST: extract-signatures public-only")
|
|
parser, lang = analyze.setup_treesitter_parser()
|
|
if parser is None:
|
|
print(" SKIP tree-sitter not available")
|
|
return
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f:
|
|
f.write("""
|
|
public class Filtered {
|
|
public void pubMethod() {}
|
|
private void privMethod() {}
|
|
protected void protMethod() {}
|
|
void packageMethod() {}
|
|
}
|
|
""")
|
|
tmp = f.name
|
|
|
|
try:
|
|
args = _make_namespace(
|
|
{
|
|
"file": tmp,
|
|
"public_only": True,
|
|
"project_root": str(PROJECT_ROOT),
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_extract_signatures, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("pubMethod" in stdout, "Public method included")
|
|
check("privMethod" not in stdout, "Private method excluded")
|
|
check("protMethod" not in stdout, "Protected method excluded")
|
|
finally:
|
|
os.unlink(tmp)
|
|
|
|
|
|
def test_extract_signatures_json():
|
|
"""extract-signatures: JSON output includes all fields."""
|
|
print("TEST: extract-signatures JSON output")
|
|
parser, lang = analyze.setup_treesitter_parser()
|
|
if parser is None:
|
|
print(" SKIP tree-sitter not available")
|
|
return
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f:
|
|
f.write("""
|
|
public interface IFoo {
|
|
String doThing(int x, String y);
|
|
void noArgs();
|
|
}
|
|
""")
|
|
tmp = f.name
|
|
|
|
try:
|
|
args = _make_namespace(
|
|
{
|
|
"file": tmp,
|
|
"public_only": False,
|
|
"json_output": True,
|
|
"project_root": str(PROJECT_ROOT),
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_extract_signatures, args)
|
|
check(rc == 0, "Return code 0")
|
|
data = json.loads(stdout)
|
|
check(data["total_methods"] == 2, "Two methods found")
|
|
sig0 = data["signatures"][0]
|
|
check(sig0["name"] == "doThing", "First method name")
|
|
check(sig0["return_type"] == "String", "Return type captured")
|
|
check(len(sig0["params"]) == 2, "Two parameters captured")
|
|
check(sig0["params"][0]["type"] == "int", "First param type")
|
|
check(sig0["params"][1]["name"] == "y", "Second param name")
|
|
finally:
|
|
os.unlink(tmp)
|
|
|
|
|
|
def test_surface_miner_basic():
|
|
"""surface-miner: find methods/fields for NBTTagCompound."""
|
|
print("TEST: surface-miner basic")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "NBTTagCompound",
|
|
"fqn": None,
|
|
"scope": None,
|
|
"compare_existing": None,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_surface_miner, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("MINIMAL SURFACE" in stdout, "Header present")
|
|
check("METHODS" in stdout, "Methods section")
|
|
check("setString" in stdout, "setString method found")
|
|
check("getString" in stdout, "getString method found")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_surface_miner_scoped():
|
|
"""surface-miner: --scope filters to files under path."""
|
|
print("TEST: surface-miner scoped")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "NBTTagCompound",
|
|
"fqn": None,
|
|
"scope": "core/src/main/java",
|
|
"compare_existing": None,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_surface_miner, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("scoped to core/src/main/java" in stdout, "Scope shown in output")
|
|
check("setString" in stdout, "setString in core scope")
|
|
check("getString" in stdout, "getString in core scope")
|
|
check("setInteger" not in stdout, "setInteger not in core scope")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_surface_miner_not_found():
|
|
"""surface-miner: missing type returns error."""
|
|
print("TEST: surface-miner not found")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "FakeType",
|
|
"fqn": None,
|
|
"scope": None,
|
|
"compare_existing": None,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, _, stderr = _capture_cmd(analyze.cmd_surface_miner, args)
|
|
check(rc == 1, "Return code 1 for missing type")
|
|
check("not found" in stderr.lower(), "Error message")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_diff_signatures_basic():
|
|
"""diff-signatures: detect added/removed methods between two temp files."""
|
|
print("TEST: diff-signatures basic")
|
|
parser, lang = analyze.setup_treesitter_parser()
|
|
if parser is None:
|
|
print(" SKIP tree-sitter not available")
|
|
return
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f1:
|
|
f1.write("""
|
|
public class OldClass {
|
|
public void keepMethod() {}
|
|
public void removeMe() {}
|
|
public int changeable() { return 0; }
|
|
}
|
|
""")
|
|
old_path = f1.name
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f2:
|
|
f2.write("""
|
|
public class NewClass {
|
|
public void keepMethod() {}
|
|
public void addedMethod() {}
|
|
public String changeable() { return ""; }
|
|
}
|
|
""")
|
|
new_path = f2.name
|
|
|
|
try:
|
|
args = _make_namespace(
|
|
{
|
|
"old": old_path,
|
|
"new": new_path,
|
|
"project_root": str(PROJECT_ROOT),
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_diff_signatures, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("SIGNATURE DIFF" in stdout, "Header present")
|
|
check("ADDED" in stdout, "Added section")
|
|
check("addedMethod" in stdout, "Added method detected")
|
|
check("REMOVED" in stdout, "Removed section")
|
|
check("removeMe" in stdout, "Removed method detected")
|
|
check("CHANGED" in stdout, "Changed section")
|
|
check("changeable" in stdout, "Changed method detected")
|
|
finally:
|
|
os.unlink(old_path)
|
|
os.unlink(new_path)
|
|
|
|
|
|
def test_diff_signatures_identical():
|
|
"""diff-signatures: identical files produce no diffs."""
|
|
print("TEST: diff-signatures identical")
|
|
parser, lang = analyze.setup_treesitter_parser()
|
|
if parser is None:
|
|
print(" SKIP tree-sitter not available")
|
|
return
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f:
|
|
f.write("""
|
|
public class Same {
|
|
public void alpha() {}
|
|
public void beta(int x) {}
|
|
}
|
|
""")
|
|
path = f.name
|
|
|
|
try:
|
|
args = _make_namespace(
|
|
{
|
|
"old": path,
|
|
"new": path,
|
|
"project_root": str(PROJECT_ROOT),
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_diff_signatures, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("No differences found" in stdout, "No diffs for identical files")
|
|
check("=2 unchanged" in stdout, "2 unchanged methods")
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
def test_diff_signatures_json():
|
|
"""diff-signatures: JSON output has added/removed/changed."""
|
|
print("TEST: diff-signatures JSON output")
|
|
parser, lang = analyze.setup_treesitter_parser()
|
|
if parser is None:
|
|
print(" SKIP tree-sitter not available")
|
|
return
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f1:
|
|
f1.write("public class A { public void m1() {} }")
|
|
p1 = f1.name
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".java", delete=False) as f2:
|
|
f2.write("public class A { public void m1() {} public void m2() {} }")
|
|
p2 = f2.name
|
|
|
|
try:
|
|
args = _make_namespace(
|
|
{
|
|
"old": p1,
|
|
"new": p2,
|
|
"json_output": True,
|
|
"project_root": str(PROJECT_ROOT),
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_diff_signatures, args)
|
|
check(rc == 0, "Return code 0")
|
|
data = json.loads(stdout)
|
|
check(len(data["added"]) == 1, "One method added")
|
|
check(data["added"][0]["name"] == "m2", "Added method is m2")
|
|
check(len(data["removed"]) == 0, "No methods removed")
|
|
check(data["unchanged"] == 1, "One unchanged")
|
|
finally:
|
|
os.unlink(p1)
|
|
os.unlink(p2)
|
|
|
|
|
|
def test_type_usage_basic():
|
|
"""type-usage: basic invocation shows all sections."""
|
|
print("TEST: type-usage basic")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "EntityPlayer",
|
|
"fqn": None,
|
|
"scope": None,
|
|
"top": 0,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, stderr = _capture_cmd(analyze.cmd_type_usage, args)
|
|
check(rc == 0, f"Return code 0 (stderr: {stderr[:100]})")
|
|
check("TYPE USAGE:" in stdout, "Header present")
|
|
check("EntityPlayer" in stdout, "Type name in output")
|
|
check("IPlayer" in stdout, "PA abstraction shown")
|
|
check("METHOD CALLS" in stdout, "Method calls section")
|
|
check("getCommandSenderName" in stdout, "Method name present")
|
|
check("FIELD ACCESSES" in stdout, "Field accesses section")
|
|
check("worldObj" in stdout, "Field name present")
|
|
check("CONSTRUCTORS:" in stdout, "Constructors section")
|
|
check("CASTS:" in stdout, "Casts section")
|
|
check("INSTANCEOF:" in stdout, "Instanceof section")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_type_usage_scoped():
|
|
"""type-usage: --scope filters to files under path."""
|
|
print("TEST: type-usage scoped")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "EntityPlayer",
|
|
"fqn": None,
|
|
"scope": "noppes/npcs/controllers/",
|
|
"top": 0,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_type_usage, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("noppes/npcs/controllers/" in stdout, "Scope shown in output")
|
|
check("getCommandSenderName" in stdout, "Method in scope present")
|
|
check("QuestController" in stdout, "Scoped file present")
|
|
# RoleTrader is outside controllers/ scope for method calls
|
|
# but worldObj has it — check that scoped files appear
|
|
check("worldObj" in stdout, "Field in scope present")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_type_usage_json():
|
|
"""type-usage: JSON output has all expected keys."""
|
|
print("TEST: type-usage JSON output")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "EntityPlayer",
|
|
"fqn": None,
|
|
"scope": None,
|
|
"top": 0,
|
|
"json_output": True,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_type_usage, args)
|
|
check(rc == 0, "Return code 0")
|
|
data = json.loads(stdout)
|
|
check(data["type"] == "net.minecraft.entity.player.EntityPlayer", "Correct FQN")
|
|
check(data["shortName"] == "EntityPlayer", "Short name present")
|
|
check(data["paAbstraction"] == "IPlayer", "PA abstraction present")
|
|
check("methodCalls" in data, "methodCalls key present")
|
|
check("staticCalls" in data, "staticCalls key present")
|
|
check("fieldAccesses" in data, "fieldAccesses key present")
|
|
check("constructors" in data, "constructors key present")
|
|
check("casts" in data, "casts key present")
|
|
check("instanceof" in data, "instanceof key present")
|
|
check("count" in data["casts"], "casts has count")
|
|
check("files" in data["casts"], "casts has files")
|
|
check(data["totalFilesInScope"] > 0, "totalFilesInScope > 0")
|
|
check(data["totalFilesGlobal"] > 0, "totalFilesGlobal > 0")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_type_usage_top_limit():
|
|
"""type-usage: --top limits methods/fields shown."""
|
|
print("TEST: type-usage --top limit")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "NBTTagCompound",
|
|
"fqn": None,
|
|
"scope": None,
|
|
"top": 1,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_type_usage, args)
|
|
check(rc == 0, "Return code 0")
|
|
# NBTTagCompound has 3 methods (setString, getString, setInteger)
|
|
# With --top 1, only the highest should show
|
|
check("setString" in stdout, "Top method (setString) present")
|
|
# setInteger has lowest count (1 file), should be excluded
|
|
check("setInteger" not in stdout, "Lower method excluded by --top 1")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_type_usage_not_found():
|
|
"""type-usage: missing type returns error."""
|
|
print("TEST: type-usage not found")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"type": "NonexistentType",
|
|
"fqn": None,
|
|
"scope": None,
|
|
"top": 0,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, _, stderr = _capture_cmd(analyze.cmd_type_usage, args)
|
|
check(rc == 1, "Return code 1 for missing type")
|
|
check("not found" in stderr.lower(), "Error message mentions not found")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_coverage_report_basic():
|
|
"""coverage-report: basic stats from mock index."""
|
|
print("TEST: coverage-report basic")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"detailed": False,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_coverage_report, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("MIGRATION COVERAGE REPORT" in stdout, "Header present")
|
|
check("OVERALL STATISTICS:" in stdout, "Stats section")
|
|
check("Total files scanned" in stdout, "File count label")
|
|
check("ABSTRACTION COVERAGE:" in stdout, "Coverage section")
|
|
check("TOP BLOCKING TYPES" in stdout, "Blocking types section")
|
|
check("MinecraftServer" in stdout, "Blocking type shown")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_coverage_report_detailed():
|
|
"""coverage-report: detailed mode includes package and file breakdown."""
|
|
print("TEST: coverage-report detailed")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"detailed": True,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_coverage_report, args)
|
|
check(rc == 0, "Return code 0")
|
|
check("PACKAGE BREAKDOWN" in stdout, "Package breakdown in detailed mode")
|
|
check("TOP FILES BY MC DEPENDENCY" in stdout, "File breakdown in detailed mode")
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_coverage_report_json():
|
|
"""coverage-report: JSON output."""
|
|
print("TEST: coverage-report JSON output")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
mock_root = _write_mock_index(tmpdir)
|
|
args = _make_namespace(
|
|
{
|
|
"detailed": False,
|
|
"json_output": True,
|
|
"project_root": mock_root,
|
|
}
|
|
)
|
|
rc, stdout, _ = _capture_cmd(analyze.cmd_coverage_report, args)
|
|
check(rc == 0, "Return code 0")
|
|
data = json.loads(stdout)
|
|
check("abstractionCoverage" in data, "Coverage in JSON")
|
|
check(data["abstractionCoverage"]["abstracted"] == 2, "2 abstracted types")
|
|
check(len(data["topBlockingTypes"]) == 1, "1 blocking type")
|
|
check(
|
|
data["topBlockingTypes"][0]["shortName"] == "MinecraftServer",
|
|
"MinecraftServer is blocking",
|
|
)
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_missing_index():
|
|
"""All index-dependent commands fail gracefully when index is missing."""
|
|
print("TEST: missing index")
|
|
tmpdir = tempfile.mkdtemp()
|
|
try:
|
|
args = _make_namespace(
|
|
{
|
|
"type": "EntityPlayer",
|
|
"fqn": None,
|
|
"project_root": tmpdir,
|
|
}
|
|
)
|
|
rc, _, stderr = _capture_cmd(analyze.cmd_query_type, args)
|
|
check(rc == 1, "Return code 1 when index missing")
|
|
check(
|
|
"not found" in stderr.lower() or "index" in stderr.lower(),
|
|
"Error mentions missing index",
|
|
)
|
|
finally:
|
|
shutil.rmtree(tmpdir)
|
|
|
|
|
|
def test_utility_functions():
|
|
"""Test utility functions directly."""
|
|
print("TEST: utility functions")
|
|
|
|
root = analyze.auto_detect_project_root()
|
|
check(root.exists(), "auto_detect_project_root returns existing path")
|
|
|
|
check(
|
|
analyze._find_type_key(MOCK_INDEX, "EntityPlayer")
|
|
== "net.minecraft.entity.player.EntityPlayer",
|
|
"_find_type_key by short name",
|
|
)
|
|
|
|
check(
|
|
analyze._find_type_key(MOCK_INDEX, "X", fqn="net.minecraft.nbt.NBTTagCompound")
|
|
== "net.minecraft.nbt.NBTTagCompound",
|
|
"_find_type_key by FQN",
|
|
)
|
|
|
|
check(
|
|
analyze._find_type_key(MOCK_INDEX, "Nonexistent") is None,
|
|
"_find_type_key returns None for missing",
|
|
)
|
|
|
|
check(
|
|
analyze._find_file_key(
|
|
MOCK_INDEX, "noppes/npcs/controllers/QuestController.java"
|
|
)
|
|
== "noppes/npcs/controllers/QuestController.java",
|
|
"_find_file_key exact match",
|
|
)
|
|
|
|
check(
|
|
analyze._find_file_key(MOCK_INDEX, "controllers/QuestController.java")
|
|
== "noppes/npcs/controllers/QuestController.java",
|
|
"_find_file_key suffix match",
|
|
)
|
|
|
|
readiness = analyze._calc_readiness(
|
|
MOCK_INDEX["files"]["noppes/npcs/CustomNpcs.java"]
|
|
)
|
|
check(0 < readiness["score"] < 100, "Partial readiness for CustomNpcs")
|
|
check(len(readiness["blocking"]) > 0, "Has blocking types")
|
|
|
|
|
|
def test_signature_key():
|
|
"""Test signature_key uniqueness."""
|
|
print("TEST: signature_key")
|
|
sig1 = {"name": "foo", "params": [{"type": "int", "name": "x"}]}
|
|
sig2 = {"name": "foo", "params": [{"type": "int", "name": "y"}]}
|
|
sig3 = {"name": "foo", "params": [{"type": "String", "name": "x"}]}
|
|
|
|
check(
|
|
analyze.signature_key(sig1) == analyze.signature_key(sig2),
|
|
"Same name+types, different param names = same key",
|
|
)
|
|
check(
|
|
analyze.signature_key(sig1) != analyze.signature_key(sig3),
|
|
"Same name, different types = different key",
|
|
)
|
|
|
|
|
|
def main():
|
|
global passed, failed
|
|
print("=" * 80)
|
|
print("ANALYZE.PY COMPREHENSIVE TEST SUITE")
|
|
print("=" * 80)
|
|
print()
|
|
|
|
tests = [
|
|
test_utility_functions,
|
|
test_signature_key,
|
|
test_query_type_basic,
|
|
test_query_type_by_fqn,
|
|
test_query_type_not_found,
|
|
test_query_type_json,
|
|
test_query_file_basic,
|
|
test_query_file_readiness,
|
|
test_query_file_not_found,
|
|
test_query_file_json,
|
|
test_extract_signatures_real_file,
|
|
test_extract_signatures_temp_file,
|
|
test_extract_signatures_public_only,
|
|
test_extract_signatures_json,
|
|
test_surface_miner_basic,
|
|
test_surface_miner_scoped,
|
|
test_surface_miner_not_found,
|
|
test_type_usage_basic,
|
|
test_type_usage_scoped,
|
|
test_type_usage_json,
|
|
test_type_usage_top_limit,
|
|
test_type_usage_not_found,
|
|
test_diff_signatures_basic,
|
|
test_diff_signatures_identical,
|
|
test_diff_signatures_json,
|
|
test_coverage_report_basic,
|
|
test_coverage_report_detailed,
|
|
test_coverage_report_json,
|
|
test_missing_index,
|
|
]
|
|
|
|
for test_fn in tests:
|
|
try:
|
|
test_fn()
|
|
except Exception as e:
|
|
failed += 1
|
|
print(f" FAIL EXCEPTION: {e}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
print()
|
|
|
|
print("=" * 80)
|
|
print("SUMMARY")
|
|
print("=" * 80)
|
|
total = passed + failed
|
|
print(f" {passed}/{total} checks passed, {failed} failed")
|
|
print()
|
|
|
|
return 0 if failed == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|