Skip to content

Commit 672e894

Browse files
committed
Added unit tests for semantic tokens
1 parent 1cd0736 commit 672e894

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import assert from 'assert'
2+
import sinon from 'sinon'
3+
import quibble from 'quibble'
4+
5+
import { TextDocuments, Range } from 'vscode-languageserver'
6+
import { TextDocument } from 'vscode-languageserver-textdocument'
7+
8+
import MatlabLifecycleManager from '../../../src/lifecycle/MatlabLifecycleManager'
9+
import DocumentIndexer from '../../../src/indexing/DocumentIndexer'
10+
import Indexer from '../../../src/indexing/Indexer'
11+
import FileInfoIndex, { MatlabFunctionScopeInfo, MatlabGlobalScopeInfo } from '../../../src/indexing/FileInfoIndex'
12+
import ClientConnection from '../../../src/ClientConnection'
13+
import getMockConnection from '../../mocks/Connection.mock'
14+
import getMockMvm from '../../mocks/Mvm.mock'
15+
import { dynamicImport } from '../../TestUtils'
16+
17+
describe('SemanticTokenProvider', () => {
18+
const URI = 'file://test.m'
19+
20+
let matlabLifecycleManager: MatlabLifecycleManager
21+
let fileInfoIndex: FileInfoIndex
22+
let indexer: Indexer
23+
let documentIndexer: DocumentIndexer
24+
25+
let semanticTokensProvider: any
26+
let documentManager: TextDocuments<TextDocument>
27+
28+
const setup = () => {
29+
const mockMvm = getMockMvm()
30+
31+
matlabLifecycleManager = new MatlabLifecycleManager()
32+
fileInfoIndex = new FileInfoIndex()
33+
indexer = new Indexer(matlabLifecycleManager, mockMvm, fileInfoIndex)
34+
documentIndexer = new DocumentIndexer(indexer, fileInfoIndex)
35+
36+
documentManager = new TextDocuments(TextDocument)
37+
38+
sinon.stub(matlabLifecycleManager, 'getMatlabConnection').resolves({} as any)
39+
40+
type SemanticTokensProviderExports = typeof import('../../../src/providers/semanticTokens/SemanticTokensProvider')
41+
const { default: SemanticTokensProvider } = dynamicImport<SemanticTokensProviderExports>(
42+
module, '../../../src/providers/semanticTokens/SemanticTokensProvider')
43+
44+
semanticTokensProvider = new SemanticTokensProvider(
45+
matlabLifecycleManager,
46+
documentIndexer,
47+
fileInfoIndex
48+
)
49+
50+
const doc = TextDocument.create(
51+
URI, 'matlab', 1, 'abc'
52+
)
53+
54+
sinon.stub(documentManager, 'get').returns(doc)
55+
}
56+
57+
const teardown = () => {
58+
quibble.reset()
59+
sinon.restore()
60+
}
61+
62+
before(() => {
63+
ClientConnection._setConnection(getMockConnection())
64+
})
65+
66+
after(() => {
67+
ClientConnection._clearConnection()
68+
})
69+
70+
describe('#handleSemanticTokensRequest', () => {
71+
beforeEach(() => setup())
72+
afterEach(() => teardown())
73+
74+
it('should return null if there is no MATLAB connection', async () => {
75+
(matlabLifecycleManager.getMatlabConnection as sinon.SinonStub).resolves(null)
76+
77+
const res = await semanticTokensProvider.handleSemanticTokensRequest(
78+
{ textDocument: { uri: URI } }, documentManager
79+
)
80+
81+
assert.strictEqual(res, null, 'Result should be null when there is no MATLAB connection')
82+
})
83+
84+
it('should return null if there is no document at the given URI', async () => {
85+
(documentManager.get as sinon.SinonStub).returns(undefined)
86+
87+
const res = await semanticTokensProvider.handleSemanticTokensRequest(
88+
{ textDocument: { uri: URI } }, documentManager
89+
)
90+
91+
assert.strictEqual(res, null, 'Result should be null when there is no document at the given URI')
92+
})
93+
94+
it('should return null if codeinfo is null', async () => {
95+
fileInfoIndex.codeInfoCache.set(URI, null as any)
96+
97+
const res = await semanticTokensProvider.handleSemanticTokensRequest(
98+
{ textDocument: { uri: URI } }, documentManager
99+
)
100+
101+
assert.strictEqual(res, null, 'Result should be null when codeinfo is null')
102+
})
103+
104+
it('should mark foo as a semantic function', async () => {
105+
const uri = URI
106+
107+
const range = {
108+
start: { line: 0, character: 0 },
109+
end: { line: 0, character: 3 } // "foo"
110+
}
111+
112+
fileInfoIndex.codeInfoCache.set(
113+
URI,
114+
createCodeInfo({
115+
functions: [
116+
{ name: 'foo', range: range }
117+
]
118+
})
119+
)
120+
121+
const result = await semanticTokensProvider.handleSemanticTokensRequest(
122+
{ textDocument: { uri } },
123+
documentManager
124+
)
125+
126+
assert.ok(result, 'Expected semantic tokens result')
127+
128+
const [deltaLine, deltaStart, length, typeIndex] = result!.data
129+
130+
assert.strictEqual(typeIndex, 0, 'Expected function token type')
131+
assert.strictEqual(length, 3, 'Expected token length for "foo"')
132+
})
133+
134+
it('should mark x as a semantic variable', async () => {
135+
const uri = URI
136+
137+
const range = {
138+
start: { line: 0, character: 0 },
139+
end: { line: 0, character: 1 }
140+
}
141+
142+
fileInfoIndex.codeInfoCache.set(
143+
URI,
144+
createCodeInfo({
145+
variables: [
146+
{ name: 'x', range: range }
147+
]
148+
})
149+
)
150+
151+
const result = await semanticTokensProvider.handleSemanticTokensRequest(
152+
{ textDocument: { uri } },
153+
documentManager
154+
)
155+
156+
assert.ok(result)
157+
158+
const [deltaLine, deltaStart, length, typeIndex] = result!.data
159+
160+
assert.strictEqual(typeIndex, 1) // variable
161+
assert.strictEqual(length, 1)
162+
})
163+
164+
it('should encode delta correctly for semantic tokens on same line', async () => {
165+
const uri = URI
166+
167+
const range1 = {
168+
start: { line: 0, character: 1 },
169+
end: { line: 0, character: 2 } // "x"
170+
} as unknown as Range
171+
172+
const range2 = {
173+
start: { line: 0, character: 5 },
174+
end: { line: 0, character: 6 } // "y"
175+
} as unknown as Range
176+
177+
fileInfoIndex.codeInfoCache.set(
178+
URI,
179+
createCodeInfo({
180+
variables: [
181+
{ name: 'x', range: range1 },
182+
{ name: 'y', range: range2 }
183+
]
184+
})
185+
)
186+
187+
const result = await semanticTokensProvider.handleSemanticTokensRequest(
188+
{ textDocument: { uri } } as any,
189+
documentManager
190+
)
191+
192+
assert.ok(result, 'Expected semantic tokens result')
193+
194+
// Token 1
195+
const [dl1, ds1, len1, type1] = result!.data.slice(0, 4)
196+
197+
// Token 2
198+
const [dl2, ds2, len2, type2] = result!.data.slice(5, 9)
199+
200+
// First token (absolute)
201+
assert.strictEqual(dl1, 0)
202+
assert.strictEqual(ds1, 1)
203+
204+
// Second token:
205+
// same line → deltaLine = 0
206+
assert.strictEqual(dl2, 0, 'Expected same line')
207+
208+
// relative start: 5 - 1 = 4
209+
assert.strictEqual(ds2, 4, 'Expected relative deltaStart')
210+
})
211+
212+
it('should encode delta correctly for semantic tokens on different lines', async () => {
213+
const uri = URI
214+
215+
const range1 = {
216+
start: { line: 0, character: 0 },
217+
end: { line: 0, character: 1 } // "x"
218+
}
219+
220+
const range2 = {
221+
start: { line: 2, character: 7 },
222+
end: { line: 2, character: 8 } // "y"
223+
}
224+
225+
fileInfoIndex.codeInfoCache.set(
226+
URI,
227+
createCodeInfo({
228+
variables: [ // Note: Variables are intentionally added in reverse order to test sorting of tokens
229+
{ name: 'y', range: range2 },
230+
{ name: 'x', range: range1 }
231+
]
232+
})
233+
)
234+
235+
const result = await semanticTokensProvider.handleSemanticTokensRequest(
236+
{ textDocument: { uri } } as any,
237+
documentManager
238+
)
239+
240+
assert.ok(result, 'Expected semantic tokens result')
241+
242+
// Token 1
243+
const [dl1, ds1, len1, type1] = result!.data.slice(0, 4)
244+
245+
// Token 2
246+
const [dl2, ds2, len2, type2] = result!.data.slice(5, 9)
247+
248+
// First token: absolute position
249+
assert.strictEqual(dl1, 0, 'Expected first token to have dl1 = 0, sorting should ensure this is the case')
250+
assert.strictEqual(ds1, 0, 'Expected first token to have ds1 = 0, sorting should ensure this is the case')
251+
252+
// Second token:
253+
// line 2 - line 0 = 2
254+
assert.strictEqual(dl2, 2, 'Expected deltaLine = 2')
255+
256+
// since new line → start should NOT be relative
257+
assert.strictEqual(ds2, 7, 'Expected absolute start on new line')
258+
})
259+
})
260+
})
261+
262+
function createCodeInfo({
263+
variables = [],
264+
functions = []
265+
}: {
266+
variables?: Array<{ name: string, range: Range }>
267+
functions?: Array<{ name: string, range: Range }>
268+
}) {
269+
return {
270+
globalScopeInfo: {
271+
variables: new Map(
272+
variables.map(v => [
273+
v.name,
274+
{
275+
references: [
276+
{ components: [{ range: v.range }] }
277+
]
278+
}
279+
])
280+
),
281+
282+
functionOrUnboundReferences: new Map(
283+
functions.map(f => [
284+
f.name,
285+
{
286+
references: [
287+
{ components: [{ range: f.range }] }
288+
]
289+
}
290+
])
291+
),
292+
293+
functionScopes: new Map()
294+
}
295+
} as any
296+
}

0 commit comments

Comments
 (0)