Initial Commit
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user