Initial Commit

This commit is contained in:
2026-09-15 15:24:03 -04:00
commit 4dc7086e7e
605 changed files with 101380 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Unit tests for the script-local function lift (matlab_lint.split_local_functions).
Pure Python, no container and no network:
python3 tests/test_local_functions.py
This one is a unit test rather than a smoke case because it is a source
transform on the user's own code. The smoke suite can only tell us that a
script ran; these cases are about the files it must NOT touch, and about
the line numbers staying where the model put them, which is what an error
message points at.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))
from owuitools.matlab_lint import split_local_functions # noqa: E402
FAILS: list[str] = []
def check(label: str, condition: bool, detail: str = "") -> None:
if condition:
print(f"ok {label}")
else:
FAILS.append(f"{label}: {detail}")
print(f"FAIL {label}\n {detail}")
def lifted(label: str, code: str, names: list[str], script_must_not: str | None = "function") -> None:
r = split_local_functions(code)
check(f"{label}: lifts {names}",
[f.name for f in r.functions] == names,
f"got {[f.name for f in r.functions]}, bailed={r.bailed!r}")
check(f"{label}: line count preserved",
len(r.script.split("\n")) == len(code.split("\n")),
f"{len(r.script.split(chr(10)))} vs {len(code.split(chr(10)))}")
if names and script_must_not:
check(f"{label}: definitions gone from the script",
script_must_not not in r.script,
r.script)
def bails(label: str, code: str) -> None:
r = split_local_functions(code)
check(f"{label}: left alone",
r.functions == () and r.script == code,
f"lifted {[f.name for f in r.functions]}, bailed={r.bailed!r}")
# --- the case this exists for -------------------------------------------
lifted("plain script", """x = 3;
disp(double_it(x));
function y = double_it(v)
y = 2 * v;
end
""", ["double_it"])
lifted("two functions", """disp(a(1) + b(2));
function y = a(v)
y = v;
end
function y = b(v)
y = v * 10;
end
""", ["a", "b"])
lifted("output list and no args", """disp(pair());
function [p, q] = pair()
p = 1; q = 2;
end
""", ["pair"])
lifted("no parentheses in signature", """disp(answer);
function answer = answer
answer = 42;
end
""", ["answer"])
# --- things that must not confuse the block scan ------------------------
lifted("one-line if/end above", """v = [1 2 3];
if numel(v) > 2, disp(tail(v)); end
function y = tail(x)
y = x(end);
end
""", ["tail"])
lifted("end used as an index", """v = [1 2 3];
disp(v(end));
disp(last(v));
function y = last(x)
s = struct('f', {1, 2});
y = x(end) + numel(s);
end
""", ["last"])
lifted("switch and try inside the function", """disp(pick(2));
function y = pick(n)
switch n
case 1
y = 'one';
otherwise
try
y = 'other';
catch
y = '';
end
end
end
""", ["pick"])
# The script keeps its own text verbatim, so the word survives inside the
# string and the comment - what must not survive is a real definition.
lifted("the word function inside a string", """s = 'function y = fake(x)';
disp(real_one(s));
function y = real_one(x)
y = numel(x);
end
""", ["real_one"], script_must_not=None)
lifted("a comment that looks like a definition", """% function y = commented(x)
disp(genuine(1));
function y = genuine(x)
y = x;
end
""", ["genuine"], script_must_not=None)
# --- files it must refuse to touch --------------------------------------
bails("function file", """function y = only(x)
y = x;
end
""")
bails("code after the definitions", """disp(f(1));
function y = f(x)
y = x;
end
disp('trailing statement');
""")
bails("classdef", """classdef Thing
properties
v = 1;
end
end
""")
bails("nested function", """disp(outer(1));
function y = outer(x)
function z = inner(w)
z = w + 1;
end
y = inner(x);
end
""")
bails("duplicate names", """disp(f(1));
function y = f(x)
y = x;
end
function y = f(x)
y = x + 1;
end
""")
bails("unbalanced end", """disp(1);
end
""")
bails("no functions at all", """x = 1;
disp(x);
""")
# A signature split across lines still names its function on the first
# line, and the whole block - continuation and all - is lifted verbatim.
lifted("continued signature", """disp(f(1));
function y = f(x, ...
z)
y = x;
end
""", ["f"])
# One that continues BEFORE the name has nothing to read, and is refused.
bails("name on the continued line", """disp(f(1));
function y = ...
f(x)
y = x;
end
""")
# --- the blanking must be exact -----------------------------------------
_code = "a = 1;\ndisp(f(a));\nfunction y = f(x)\n y = x;\nend\n"
_r = split_local_functions(_code)
check("script keeps its first lines verbatim",
_r.script.split("\n")[:2] == ["a = 1;", "disp(f(a));"],
_r.script.split("\n")[:2])
check("lifted text is the original source",
_r.functions[0].text == "function y = f(x)\n y = x;\nend\n",
repr(_r.functions[0].text))
check("lifted function remembers the line it came from",
_r.functions[0].line == 3, _r.functions[0].line)
check("tail is blank, not deleted",
_r.script.split("\n")[2:] == ["", "", "", ""],
repr(_r.script.split("\n")[2:]))
print()
print("=" * 60)
if FAILS:
print(f"FAILED {len(FAILS)}")
for f in FAILS:
print(" -", f)
sys.exit(1)
print("all local-function cases pass")