Skip to content

Commit 41d556f

Browse files
committed
Add MATLAB unit tests for navigation and indexing
1 parent 2f83a86 commit 41d556f

File tree

10 files changed

+486
-0
lines changed

10 files changed

+486
-0
lines changed
24 Bytes
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
% Copyright 2025 The MathWorks, Inc.
2+
classdef tParseInfoFromDocument < matlab.unittest.TestCase
3+
methods (TestClassSetup)
4+
function setup (~)
5+
% Add function under test to path
6+
addpath("../../../../../matlab");
7+
8+
% No need to add testData directory to path
9+
end
10+
end
11+
12+
methods (Test)
13+
% Test parsing info from a script file
14+
function testParsingScriptFile (testCase)
15+
filePath = fullfile(pwd, 'testData', 'sampleScript.m');
16+
code = fileread(filePath);
17+
18+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 0);
19+
20+
testCase.assertEmpty(result.packageName);
21+
testCase.assertFalse(result.classInfo.hasClassInfo);
22+
testCase.assertEmpty(result.functionInfo);
23+
24+
% Note: Function references appear first in the list
25+
expectedReferences = {
26+
{'disp', toRange(1, 1, 1, 5)},...
27+
{'linspace', toRange(4, 5, 4, 13)},...
28+
{'sin', toRange(5, 5, 5, 8)},...
29+
{'plot', toRange(8, 1, 8, 5)},...
30+
{'x', toRange(4, 1, 4, 2)},...
31+
{'y', toRange(5, 1, 5, 2)},...
32+
{'x', toRange(5, 9, 5, 10)},...
33+
{'x', toRange(8, 6, 8, 7)},...
34+
{'y', toRange(8, 9, 8, 10)}
35+
};
36+
testCase.assertEqual(result.references, expectedReferences);
37+
38+
expectedSections = {
39+
struct(title = "Create Data", range = toRange(3, 1, 6, 2)),...
40+
struct(title = "Plot", range = toRange(7, 1, 8, 11))
41+
};
42+
43+
testCase.assertEqual(result.sections, expectedSections);
44+
end
45+
46+
% Test parsing info from a function file
47+
function testParsingFunctionFile (testCase)
48+
filePath = fullfile(pwd, 'testData', '+package', 'sampleFunction.m');
49+
code = fileread(filePath);
50+
51+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 0);
52+
53+
testCase.assertEqual(result.packageName, "package");
54+
testCase.assertFalse(result.classInfo.hasClassInfo);
55+
testCase.assertEqual(numel(result.functionInfo), 3);
56+
57+
% ---------- Local Function ---------- %
58+
fcnInfo = result.functionInfo{1};
59+
testCase.assertEqual(fcnInfo.name, 'localFunction');
60+
testCase.assertEqual(fcnInfo.range, toRange(11, 1, 14, 4));
61+
testCase.assertEmpty(fcnInfo.parentClass);
62+
testCase.assertFalse(fcnInfo.isPublic);
63+
64+
expectedVarDefs = {
65+
... Input variables
66+
{'in1Local', toRange(12, 24, 12, 32)},...
67+
{'in2Local', toRange(12, 34, 12, 42)},...
68+
... Output variables
69+
{'outLocal', toRange(11, 10, 11, 18)},...
70+
... Definitions within function body
71+
{'outLocal', toRange(13, 5, 13, 13)}
72+
};
73+
testCase.assertEqual(fcnInfo.variableInfo.definitions, expectedVarDefs);
74+
75+
expectedVarRefs = {
76+
... In order of appearance
77+
{'outLocal', toRange(11, 10, 11, 18)},...
78+
{'in1Local', toRange(12, 24, 12, 32)},...
79+
{'in2Local', toRange(12, 34, 12, 42)},...
80+
{'outLocal', toRange(13, 5, 13, 13)},...
81+
{'in1Local', toRange(13, 20, 13, 28)},...
82+
{'in2Local', toRange(13, 30, 13, 38)},
83+
};
84+
testCase.assertEqual(fcnInfo.variableInfo.references, expectedVarRefs);
85+
86+
testCase.assertEmpty(fcnInfo.globals);
87+
testCase.assertFalse(fcnInfo.isPrototype);
88+
testCase.assertEqual(fcnInfo.declaration, toRange(11, 1, 12, 43));
89+
90+
% ---------- Nested Function ---------- %
91+
fcnInfo = result.functionInfo{2};
92+
93+
testCase.assertEqual(fcnInfo.name, 'nestedFunction');
94+
testCase.assertEqual(fcnInfo.range, toRange(5, 5, 8, 8));
95+
testCase.assertEmpty(fcnInfo.parentClass);
96+
testCase.assertFalse(fcnInfo.isPublic);
97+
98+
expectedVarDefs = {
99+
... Global variables
100+
{'globalVar', toRange(6, 16, 6, 25)},...
101+
... Input variables
102+
{'inNested', toRange(5, 42, 5, 50)},...
103+
... Output variables
104+
{'outNested', toRange(5, 14, 5, 23)},...
105+
... Definitions within function body
106+
{'outNested', toRange(7, 9, 7, 18)}
107+
};
108+
testCase.assertEqual(fcnInfo.variableInfo.definitions, expectedVarDefs);
109+
110+
expectedVarRefs = {
111+
... In order of appearance
112+
{'outNested', toRange(5, 14, 5, 23)},...
113+
{'inNested', toRange(5, 42, 5, 50)},...
114+
{'globalVar', toRange(6, 16, 6, 25)},...
115+
{'outNested', toRange(7, 9, 7, 18)},...
116+
{'inNested', toRange(7, 25, 7, 33)}
117+
};
118+
testCase.assertEqual(fcnInfo.variableInfo.references, expectedVarRefs);
119+
120+
testCase.assertEqual(fcnInfo.globals, {'globalVar'});
121+
testCase.assertFalse(fcnInfo.isPrototype);
122+
testCase.assertEqual(fcnInfo.declaration, toRange(5, 5, 5, 51));
123+
124+
% ---------- Main Function ---------- %
125+
fcnInfo = result.functionInfo{3};
126+
127+
testCase.assertEqual(fcnInfo.name, 'sampleFunction');
128+
testCase.assertEqual(fcnInfo.range, toRange(1, 1, 9, 4));
129+
testCase.assertEmpty(fcnInfo.parentClass);
130+
testCase.assertTrue(fcnInfo.isPublic);
131+
132+
expectedVarDefs = {
133+
... Global variables
134+
{'globalVar', toRange(6, 16, 6, 25)},...
135+
... Input variables
136+
{'in1', toRange(1, 41, 1, 44)},...
137+
{'in2', toRange(1, 46, 1, 49)},...
138+
{'in3', toRange(1, 51, 1, 54)},...
139+
... Output variables
140+
{'out1', toRange(1, 11, 1, 15)},...
141+
{'out2', toRange(1, 17, 1, 21)},...
142+
... Definitions within function body
143+
{'out1', toRange(2, 5, 2, 9)},...
144+
{'out2', toRange(3, 5, 3, 9)},...
145+
{'outNested', toRange(7, 9, 7, 18)}
146+
};
147+
testCase.assertEqual(fcnInfo.variableInfo.definitions, expectedVarDefs);
148+
149+
expectedVarRefs = {
150+
... In order of appearance
151+
{'out1', toRange(1, 11, 1, 15)},...
152+
{'out2', toRange(1, 17, 1, 21)},...
153+
{'in1', toRange(1, 41, 1, 44)},...
154+
{'in2', toRange(1, 46, 1, 49)},...
155+
{'in3', toRange(1, 51, 1, 54)},...
156+
{'out1', toRange(2, 5, 2, 9)},...
157+
{'in1', toRange(2, 26, 2, 29)},...
158+
{'in2', toRange(2, 31, 2, 34)},...
159+
{'out2', toRange(3, 5, 3, 9)},...
160+
{'in3', toRange(3, 27, 3, 30)},...
161+
{'outNested', toRange(5, 14, 5, 23)},...
162+
{'inNested', toRange(5, 42, 5, 50)},...
163+
{'globalVar', toRange(6, 16, 6, 25)},...
164+
{'outNested', toRange(7, 9, 7, 18)},...
165+
{'inNested', toRange(7, 25, 7, 33)}
166+
};
167+
testCase.assertEqual(fcnInfo.variableInfo.references, expectedVarRefs);
168+
169+
testCase.assertEqual(fcnInfo.globals, {'globalVar'});
170+
testCase.assertFalse(fcnInfo.isPrototype);
171+
testCase.assertEqual(fcnInfo.declaration, toRange(1, 1, 1, 55));
172+
end
173+
174+
% Test parsing info from a class file
175+
function testParsingClassFile (testCase)
176+
filePath = fullfile(pwd, 'testData', 'SampleClass.m');
177+
code = fileread(filePath);
178+
179+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 0);
180+
181+
testCase.assertEmpty(result.packageName);
182+
183+
% ---------- Class Info ---------- %
184+
classInfo = result.classInfo;
185+
testCase.assertTrue(classInfo.isClassDef);
186+
testCase.assertTrue(classInfo.hasClassInfo);
187+
testCase.assertEqual(classInfo.name, 'SampleClass');
188+
testCase.assertEqual(classInfo.range, toRange(1, 1, 34, 4));
189+
testCase.assertEqual(classInfo.declaration, toRange(1, 1, 2, 24))
190+
191+
expectedProperties = {
192+
struct(name = 'PropA', range = toRange(10, 9, 10, 14), parentClass = 'SampleClass', isPublic = false),...
193+
struct(name = 'PropB', range = toRange(11, 9, 11, 14), parentClass = 'SampleClass', isPublic = false)
194+
};
195+
testCase.assertEqual(classInfo.properties, expectedProperties);
196+
197+
expectedEnumerations = {
198+
struct(name = 'A', range = toRange(4, 9, 4, 10), parentClass = 'SampleClass', isPublic = false),...
199+
struct(name = 'B', range = toRange(5, 9, 5, 10), parentClass = 'SampleClass', isPublic = false),...
200+
struct(name = 'C', range = toRange(6, 9, 6, 10), parentClass = 'SampleClass', isPublic = false)
201+
};
202+
testCase.assertEqual(classInfo.enumerations, expectedEnumerations);
203+
204+
testCase.assertEmpty(classInfo.classDefFolder);
205+
testCase.assertEqual(classInfo.baseClasses, {'SuperA', 'SuperB'});
206+
207+
testCase.assertEqual(numel(result.functionInfo), 4);
208+
209+
% Note: Only checking certain attributes of functions, as the
210+
% majority of other cases should be covered by the above test.
211+
212+
% ---------- Constructor ---------- %
213+
fcnInfo = result.functionInfo{1};
214+
testCase.assertEqual(fcnInfo.name, 'SampleClass');
215+
testCase.assertEqual(fcnInfo.parentClass, 'SampleClass');
216+
testCase.assertTrue(fcnInfo.isPublic);
217+
testCase.assertFalse(fcnInfo.isPrototype);
218+
219+
% ---------- Abstract Function ---------- %
220+
fcnInfo = result.functionInfo{2};
221+
testCase.assertEqual(fcnInfo.name, 'abstractFcn');
222+
testCase.assertEqual(fcnInfo.parentClass, 'SampleClass');
223+
testCase.assertTrue(fcnInfo.isPublic);
224+
testCase.assertTrue(fcnInfo.isPrototype);
225+
226+
227+
% ---------- Private Function ---------- %
228+
fcnInfo = result.functionInfo{3};
229+
testCase.assertEqual(fcnInfo.name, 'privateFcn');
230+
testCase.assertEqual(fcnInfo.parentClass, 'SampleClass');
231+
testCase.assertTrue(fcnInfo.isPublic); % This is currently marked as public, despite being hidden
232+
testCase.assertFalse(fcnInfo.isPrototype);
233+
234+
235+
% ---------- Public Function ---------- %
236+
fcnInfo = result.functionInfo{4};
237+
testCase.assertEqual(fcnInfo.name, 'publicFcn');
238+
testCase.assertEqual(fcnInfo.parentClass, 'SampleClass');
239+
testCase.assertTrue(fcnInfo.isPublic);
240+
testCase.assertFalse(fcnInfo.isPrototype);
241+
242+
% ---------- References ---------- %
243+
% Note: Property references appear first, then function references
244+
expectedReferences = {
245+
{'obj.PropA', toRange(16, 13, 16, 22)},...
246+
{'obj.PropB', toRange(17, 13, 17, 22)},...
247+
{'obj.PropA', toRange(30, 38, 30, 47)},...
248+
{'obj.PropB', toRange(31, 30, 31, 39)},...
249+
{'SampleClass', toRange(15, 24, 15, 35)},...
250+
{'publicFcn', toRange(20, 18, 20, 27)},...
251+
{'abstractFcn', toRange(25, 15, 25, 26)},...
252+
{'privateFcn', toRange(29, 18, 29, 28)},...
253+
{'disp', toRange(30, 13, 30, 17)},...
254+
{'num2str', toRange(30, 30, 30, 37)},...
255+
{'disp', toRange(31, 13, 31, 17)}
256+
};
257+
testCase.assertEqual(result.references, expectedReferences);
258+
end
259+
260+
% Test parsing info from a @folder test
261+
function testParsingFolderClass (testCase)
262+
% Note: Only checking certain attributes of functions and
263+
% classes, as the majority of other cases should be covered
264+
% by the above tests.
265+
266+
classFolder = fullfile(pwd, 'testData', '@FolderClass');
267+
268+
% ---------- Classdef File ---------- %
269+
filePath = fullfile(classFolder, 'FolderClass.m');
270+
code = fileread(filePath);
271+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 0);
272+
273+
testCase.assertEmpty(result.packageName);
274+
testCase.assertEqual(numel(result.functionInfo), 1);
275+
276+
classInfo = result.classInfo;
277+
testCase.assertTrue(classInfo.isClassDef);
278+
testCase.assertTrue(classInfo.hasClassInfo);
279+
testCase.assertEqual(classInfo.name, 'FolderClass');
280+
testCase.assertEqual(classInfo.classDefFolder, classFolder);
281+
282+
% ---------- Function File ---------- %
283+
filePath = fullfile(classFolder, 'folderFunction.m');
284+
code = fileread(filePath);
285+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 0);
286+
287+
testCase.assertEmpty(result.packageName);
288+
testCase.assertEqual(numel(result.functionInfo), 1);
289+
290+
classInfo = result.classInfo;
291+
testCase.assertFalse(classInfo.isClassDef);
292+
testCase.assertTrue(classInfo.hasClassInfo);
293+
testCase.assertEqual(classInfo.name, 'FolderClass');
294+
testCase.assertEqual(classInfo.classDefFolder, classFolder);
295+
end
296+
297+
% Test parsing info with different analysis limits
298+
function testFileAnalysisLimit (testCase)
299+
code = "function foo (), end";
300+
filePath = "some/path/foo.m";
301+
302+
% Case 1: Ensure data is returned with unlimited analysis limit
303+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 0);
304+
testCase.assertNotEmpty(result.functionInfo);
305+
306+
% Case 2: Ensure no data is returned with small limit
307+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 10);
308+
testCase.assertEmpty(result.functionInfo);
309+
310+
% Case 3: Ensure data is returned with large limit
311+
result = matlabls.handlers.indexing.parseInfoFromDocument(code, filePath, 100);
312+
testCase.assertNotEmpty(result.functionInfo);
313+
end
314+
end
315+
end
316+
317+
% Helper functions:
318+
function range = toRange(lineStart, charStart, lineEnd, charEnd)
319+
range = struct(lineStart = lineStart, charStart = charStart, lineEnd = lineEnd, charEnd = charEnd);
320+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function [out1, out2] = sampleFunction (in1, in2, in3)
2+
out1 = localFunction(in1, in2);
3+
out2 = nestedFunction(in3);
4+
5+
function outNested = nestedFunction (inNested)
6+
global globalVar
7+
outNested = abs(inNested);
8+
end
9+
end
10+
11+
function outLocal =...
12+
localFunction (in1Local, in2Local)
13+
outLocal = sum(in1Local, in2Local);
14+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
classdef FolderClass < handle
2+
properties
3+
Prop1
4+
Prop2
5+
end
6+
7+
methods
8+
function obj = FolderClass (in1, in2)
9+
obj.Prop1 = in1;
10+
obj.Prop2 = in2;
11+
end
12+
end
13+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
function out = folderFunction (obj, in)
2+
disp(obj.Prop1)
3+
out = in;
4+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
classdef SampleClass <...
2+
SuperA & SuperB
3+
enumeration
4+
A (1, 'a')
5+
B (2, 'b')
6+
C (3, 'c')
7+
end
8+
9+
properties
10+
PropA
11+
PropB
12+
end
13+
14+
methods
15+
function obj = SampleClass (a, b)
16+
obj.PropA = a;
17+
obj.PropB = b;
18+
end
19+
20+
function publicFcn (obj)
21+
end
22+
end
23+
24+
methods (Abstract)
25+
out = abstractFcn (in)
26+
end
27+
28+
methods (Hidden)
29+
function privateFcn (obj)
30+
disp(['PropA: ', num2str(obj.PropA)]);
31+
disp(['PropB: ', obj.PropB]);
32+
end
33+
end
34+
end

0 commit comments

Comments
 (0)