|  | 
|  | 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. | 
|  | 2 | +// Licensed under the MIT License. | 
|  | 3 | + | 
|  | 4 | +'use strict'; | 
|  | 5 | + | 
|  | 6 | +// tslint:disable:max-func-body-length | 
|  | 7 | + | 
|  | 8 | +import { assert, expect, use } from 'chai'; | 
|  | 9 | +import * as chaipromise from 'chai-as-promised'; | 
|  | 10 | +import * as TypeMoq from 'typemoq'; | 
|  | 11 | +import { CancellationToken, Position, SignatureHelp, | 
|  | 12 | + TextDocument, TextLine, Uri } from 'vscode'; | 
|  | 13 | +import { JediFactory } from '../../client/languageServices/jediProxyFactory'; | 
|  | 14 | +import { IArgumentsResult, JediProxyHandler } from '../../client/providers/jediProxy'; | 
|  | 15 | +import { isPositionInsideStringOrComment } from '../../client/providers/providerUtilities'; | 
|  | 16 | +import { PythonSignatureProvider } from '../../client/providers/signatureProvider'; | 
|  | 17 | + | 
|  | 18 | +use(chaipromise); | 
|  | 19 | + | 
|  | 20 | +suite('Signature Provider unit tests', () => { | 
|  | 21 | + let pySignatureProvider: PythonSignatureProvider; | 
|  | 22 | + let jediHandler: TypeMoq.IMock<JediProxyHandler<IArgumentsResult>>; | 
|  | 23 | + let argResultItems: IArgumentsResult; | 
|  | 24 | + setup(() => { | 
|  | 25 | + const jediFactory = TypeMoq.Mock.ofType(JediFactory); | 
|  | 26 | + jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<IArgumentsResult>>(); | 
|  | 27 | + jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny())) | 
|  | 28 | + .returns(() => jediHandler.object); | 
|  | 29 | + pySignatureProvider = new PythonSignatureProvider(jediFactory.object); | 
|  | 30 | + argResultItems = { | 
|  | 31 | + definitions: [ | 
|  | 32 | + { | 
|  | 33 | + description: 'The result', | 
|  | 34 | + docstring: 'Some docstring goes here.', | 
|  | 35 | + name: 'print', | 
|  | 36 | + paramindex: 0, | 
|  | 37 | + params: [ | 
|  | 38 | + { | 
|  | 39 | + description: 'Some parameter', | 
|  | 40 | + docstring: 'gimme docs', | 
|  | 41 | + name: 'param', | 
|  | 42 | + value: 'blah' | 
|  | 43 | + } | 
|  | 44 | + ] | 
|  | 45 | + } | 
|  | 46 | + ], | 
|  | 47 | + requestId: 1 | 
|  | 48 | + }; | 
|  | 49 | + }); | 
|  | 50 | + | 
|  | 51 | + function testSignatureReturns(source: string, pos: number): Thenable<SignatureHelp> { | 
|  | 52 | + const doc = TypeMoq.Mock.ofType<TextDocument>(); | 
|  | 53 | + const position = new Position(0, pos); | 
|  | 54 | + const lineText = TypeMoq.Mock.ofType<TextLine>(); | 
|  | 55 | + const argsResult = TypeMoq.Mock.ofType<IArgumentsResult>(); | 
|  | 56 | + const cancelToken = TypeMoq.Mock.ofType<CancellationToken>(); | 
|  | 57 | + cancelToken.setup(ct => ct.isCancellationRequested).returns(() => false); | 
|  | 58 | + | 
|  | 59 | + doc.setup(d => d.fileName).returns(() => ''); | 
|  | 60 | + doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => source); | 
|  | 61 | + doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => lineText.object); | 
|  | 62 | + doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => pos - 1); // pos is 1-based | 
|  | 63 | + const docUri = TypeMoq.Mock.ofType<Uri>(); | 
|  | 64 | + docUri.setup(u => u.scheme).returns(() => 'http'); | 
|  | 65 | + doc.setup(d => d.uri).returns(() => docUri.object); | 
|  | 66 | + lineText.setup(l => l.text).returns(() => source); | 
|  | 67 | + argsResult.setup(c => c.requestId).returns(() => 1); | 
|  | 68 | + argsResult.setup(c => c.definitions).returns(() => argResultItems[0].definitions); | 
|  | 69 | + jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { | 
|  | 70 | + return Promise.resolve(argResultItems); | 
|  | 71 | + }); | 
|  | 72 | + | 
|  | 73 | + return pySignatureProvider.provideSignatureHelp(doc.object, position, cancelToken.object); | 
|  | 74 | + } | 
|  | 75 | + | 
|  | 76 | + function testIsInsideStringOrComment(sourceLine: string, sourcePos: number) : boolean { | 
|  | 77 | + const textLine: TypeMoq.IMock<TextLine> = TypeMoq.Mock.ofType<TextLine>(); | 
|  | 78 | + textLine.setup(t => t.text).returns(() => sourceLine); | 
|  | 79 | + const doc: TypeMoq.IMock<TextDocument> = TypeMoq.Mock.ofType<TextDocument>(); | 
|  | 80 | + const pos: Position = new Position(1, sourcePos); | 
|  | 81 | + | 
|  | 82 | + doc.setup(d => d.fileName).returns(() => ''); | 
|  | 83 | + doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => sourceLine); | 
|  | 84 | + doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); | 
|  | 85 | + doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => sourcePos); | 
|  | 86 | + | 
|  | 87 | + return isPositionInsideStringOrComment(doc.object, pos); | 
|  | 88 | + } | 
|  | 89 | + | 
|  | 90 | + test('Ensure no signature is given within a string.', async () => { | 
|  | 91 | + const source = ' print(\'Python is awesome,\')\n'; | 
|  | 92 | + const sigHelp: SignatureHelp = await testSignatureReturns(source, 27); | 
|  | 93 | + expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); | 
|  | 94 | + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a string?'); | 
|  | 95 | + }); | 
|  | 96 | + test('Ensure no signature is given within a line comment.', async () => { | 
|  | 97 | + const source = '# print(\'Python is awesome,\')\n'; | 
|  | 98 | + const sigHelp: SignatureHelp = await testSignatureReturns(source, 28); | 
|  | 99 | + expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); | 
|  | 100 | + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a full-line comment?'); | 
|  | 101 | + }); | 
|  | 102 | + test('Ensure no signature is given within a comment tailing a command.', async () => { | 
|  | 103 | + const source = ' print(\'Python\') # print(\'is awesome,\')\n'; | 
|  | 104 | + const sigHelp: SignatureHelp = await testSignatureReturns(source, 38); | 
|  | 105 | + expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); | 
|  | 106 | + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a trailing comment?'); | 
|  | 107 | + }); | 
|  | 108 | + test('Ensure signature is given for built-in print command.', async () => { | 
|  | 109 | + const source = ' print(\'Python\',)\n'; | 
|  | 110 | + let sigHelp: SignatureHelp; | 
|  | 111 | + try { | 
|  | 112 | + sigHelp = await testSignatureReturns(source, 18); | 
|  | 113 | + expect(sigHelp).to.not.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); | 
|  | 114 | + expect(sigHelp.signatures.length).to.not.equal(0, 'Expected dummy argresult back from testing our print signature.'); | 
|  | 115 | + expect(sigHelp.activeParameter).to.be.equal(0, 'Parameter for print should be the first member of the test argresult\'s params object.'); | 
|  | 116 | + expect(sigHelp.activeSignature).to.be.equal(0, 'The signature for print should be the first member of the test argresult.'); | 
|  | 117 | + expect(sigHelp.signatures[sigHelp.activeSignature].label).to.be.equal('print(param)', `Expected arg result calls for specific returned signature of \'print(param)\' but we got ${sigHelp.signatures[sigHelp.activeSignature].label}`); | 
|  | 118 | + } catch (error) { | 
|  | 119 | + assert(false, `Caught exception ${error}`); | 
|  | 120 | + } | 
|  | 121 | + }); | 
|  | 122 | + test('Ensure isPositionInsideStringOrComment is behaving as expected.', () => { | 
|  | 123 | + const sourceLine: string = ' print(\'Hello world!\')\n'; | 
|  | 124 | + const sourcePos: number = sourceLine.length - 1; | 
|  | 125 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 126 | + | 
|  | 127 | + expect(isInsideStrComment).to.not.be.equal(true, [ | 
|  | 128 | + `Position set to the end of ${sourceLine} but `, | 
|  | 129 | + 'is reported as being within a string or comment.'].join('')); | 
|  | 130 | + }); | 
|  | 131 | + test('Ensure isPositionInsideStringOrComment is behaving as expected at end of source.', () => { | 
|  | 132 | + const sourceLine: string = ' print(\'Hello world!\')\n'; | 
|  | 133 | + const sourcePos: number = 0; | 
|  | 134 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 135 | + | 
|  | 136 | + expect(isInsideStrComment).to.not.be.equal(true, [ | 
|  | 137 | + `Position set to the end of ${sourceLine} but `, | 
|  | 138 | + 'is reported as being within a string or comment.'].join('')); | 
|  | 139 | + }); | 
|  | 140 | + test('Ensure isPositionInsideStringOrComment is behaving as expected at beginning of source.', () => { | 
|  | 141 | + const sourceLine: string = ' print(\'Hello world!\')\n'; | 
|  | 142 | + const sourcePos: number = 0; | 
|  | 143 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 144 | + | 
|  | 145 | + expect(isInsideStrComment).to.not.be.equal(true, [ | 
|  | 146 | + `Position set to the beginning of ${sourceLine} but `, | 
|  | 147 | + 'is reported as being within a string or comment.'].join('')); | 
|  | 148 | + }); | 
|  | 149 | + test('Ensure isPositionInsideStringOrComment is behaving as expected within a string.', () => { | 
|  | 150 | + const sourceLine: string = ' print(\'Hello world!\')\n'; | 
|  | 151 | + const sourcePos: number = 16; | 
|  | 152 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 153 | + | 
|  | 154 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 155 | + `Position set within the string in ${sourceLine} (position ${sourcePos}) but `, | 
|  | 156 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 157 | + }); | 
|  | 158 | + test('Ensure isPositionInsideStringOrComment is behaving as expected immediately before a string.', () => { | 
|  | 159 | + const sourceLine: string = ' print(\'Hello world!\')\n'; | 
|  | 160 | + const sourcePos: number = 8; | 
|  | 161 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 162 | + | 
|  | 163 | + expect(isInsideStrComment).to.be.equal(false, [ | 
|  | 164 | + `Position set to just before the string in ${sourceLine} (position ${sourcePos}) but `, | 
|  | 165 | + 'is reported as being within a string or comment.'].join('')); | 
|  | 166 | + }); | 
|  | 167 | + test('Ensure isPositionInsideStringOrComment is behaving as expected immediately in a string.', () => { | 
|  | 168 | + const sourceLine: string = ' print(\'Hello world!\')\n'; | 
|  | 169 | + const sourcePos: number = 9; | 
|  | 170 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 171 | + | 
|  | 172 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 173 | + `Position set to the start of the string in ${sourceLine} (position ${sourcePos}) but `, | 
|  | 174 | + 'is reported as being within a string or comment.'].join('')); | 
|  | 175 | + }); | 
|  | 176 | + test('Ensure isPositionInsideStringOrComment is behaving as expected within a comment.', () => { | 
|  | 177 | + const sourceLine: string = '# print(\'Hello world!\')\n'; | 
|  | 178 | + const sourcePos: number = 16; | 
|  | 179 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 180 | + | 
|  | 181 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 182 | + `Position set within a full line comment ${sourceLine} (position ${sourcePos}) but `, | 
|  | 183 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 184 | + }); | 
|  | 185 | + test('Ensure isPositionInsideStringOrComment is behaving as expected within a trailing comment.', () => { | 
|  | 186 | + const sourceLine: string = ' print(\'Hello world!\') # some comment...\n'; | 
|  | 187 | + const sourcePos: number = 34; | 
|  | 188 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 189 | + | 
|  | 190 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 191 | + `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, | 
|  | 192 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 193 | + }); | 
|  | 194 | + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very end of a trailing comment.', () => { | 
|  | 195 | + const sourceLine: string = ' print(\'Hello world!\') # some comment...\n'; | 
|  | 196 | + const sourcePos: number = sourceLine.length - 1; | 
|  | 197 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 198 | + | 
|  | 199 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 200 | + `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, | 
|  | 201 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 202 | + }); | 
|  | 203 | + test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string.', () => { | 
|  | 204 | + const sourceLine: string = ' stringVal = \'\'\'This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!\'\'\'\n'; | 
|  | 205 | + const sourcePos: number = 48; | 
|  | 206 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 207 | + | 
|  | 208 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 209 | + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, | 
|  | 210 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 211 | + }); | 
|  | 212 | + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string.', () => { | 
|  | 213 | + const sourceLine: string = ' stringVal = \'\'\'This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!\'\'\'\n'; | 
|  | 214 | + const sourcePos: number = sourceLine.length - 2; // just at the last ' | 
|  | 215 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 216 | + | 
|  | 217 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 218 | + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, | 
|  | 219 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 220 | + }); | 
|  | 221 | + test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string (double-quoted).', () => { | 
|  | 222 | + const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; | 
|  | 223 | + const sourcePos: number = 48; | 
|  | 224 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 225 | + | 
|  | 226 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 227 | + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, | 
|  | 228 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 229 | + }); | 
|  | 230 | + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string (double-quoted).', () => { | 
|  | 231 | + const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; | 
|  | 232 | + const sourcePos: number = sourceLine.length - 2; // just at the last ' | 
|  | 233 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 234 | + | 
|  | 235 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 236 | + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, | 
|  | 237 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 238 | + }); | 
|  | 239 | + test('Ensure isPositionInsideStringOrComment is behaving as expected during construction of a multiline string (double-quoted).', () => { | 
|  | 240 | + const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff'; | 
|  | 241 | + const sourcePos: number = sourceLine.length - 1; // just at the last position in the string before it's termination | 
|  | 242 | + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); | 
|  | 243 | + | 
|  | 244 | + expect(isInsideStrComment).to.be.equal(true, [ | 
|  | 245 | + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, | 
|  | 246 | + 'is reported as NOT being within a string or comment.'].join('')); | 
|  | 247 | + }); | 
|  | 248 | +}); | 
0 commit comments