Narrow exported session API, Unit tests for session API

This commit is contained in:
Wesley Wigham 2015-07-21 16:05:03 -07:00
parent 7664f3410c
commit 8e93a49c7b
4 changed files with 505 additions and 45 deletions

View file

@ -136,6 +136,7 @@ var harnessSources = [
"services/documentRegistry.ts",
"services/preProcessFile.ts",
"services/patternMatcher.ts",
"session.ts",
"versionCache.ts",
"convertToBase64.ts",
"transpile.ts"

View file

@ -29,6 +29,7 @@ var Buffer: BufferConstructor = require('buffer').Buffer;
// this will work in the browser via browserify
var _chai: typeof chai = require('chai');
var assert: typeof _chai.assert = _chai.assert;
var expect: typeof _chai.expect = _chai.expect;
declare var __dirname: string; // Node-specific
var global = <any>Function("return this").call(null);

View file

@ -109,13 +109,13 @@ namespace ts.server {
}
export class Session {
projectService: ProjectService;
pendingOperation = false;
fileHash: ts.Map<number> = {};
nextFileId = 1;
errorTimer: any; /*NodeJS.Timer | number*/
immediateId: any;
changeSeq = 0;
protected projectService: ProjectService;
private pendingOperation = false;
private fileHash: ts.Map<number> = {};
private nextFileId = 1;
private errorTimer: any; /*NodeJS.Timer | number*/
private immediateId: any;
private changeSeq = 0;
constructor(
private host: ServerHost,
@ -129,7 +129,7 @@ namespace ts.server {
});
}
handleEvent(eventName: string, project: Project, fileName: string) {
private handleEvent(eventName: string, project: Project, fileName: string) {
if (eventName == "context") {
this.projectService.log("got context event, updating diagnostics for" + fileName, "Info");
this.updateErrorCheck([{ fileName, project }], this.changeSeq,
@ -137,7 +137,7 @@ namespace ts.server {
}
}
logError(err: Error, cmd: string) {
public logError(err: Error, cmd: string) {
var typedErr = <StackTraceError>err;
var msg = "Exception on executing command " + cmd;
if (typedErr.message) {
@ -149,11 +149,11 @@ namespace ts.server {
this.projectService.log(msg);
}
sendLineToClient(line: string) {
private sendLineToClient(line: string) {
this.host.write(line + this.host.newLine);
}
send(msg: protocol.Message) {
public send(msg: protocol.Message) {
var json = JSON.stringify(msg);
if (this.logger.isVerbose()) {
this.logger.info(msg.type + ": " + json);
@ -162,7 +162,7 @@ namespace ts.server {
'\r\n\r\n' + json);
}
event(info: any, eventName: string) {
public event(info: any, eventName: string) {
var ev: protocol.Event = {
seq: 0,
type: "event",
@ -172,7 +172,7 @@ namespace ts.server {
this.send(ev);
}
response(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) {
private response(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) {
var res: protocol.Response = {
seq: 0,
type: "response",
@ -189,11 +189,11 @@ namespace ts.server {
this.send(res);
}
output(body: any, commandName: string, requestSequence = 0, errorMessage?: string) {
public output(body: any, commandName: string, requestSequence = 0, errorMessage?: string) {
this.response(body, commandName, requestSequence, errorMessage);
}
semanticCheck(file: string, project: Project) {
private semanticCheck(file: string, project: Project) {
try {
var diags = project.compilerService.languageService.getSemanticDiagnostics(file);
@ -207,7 +207,7 @@ namespace ts.server {
}
}
syntacticCheck(file: string, project: Project) {
private syntacticCheck(file: string, project: Project) {
try {
var diags = project.compilerService.languageService.getSyntacticDiagnostics(file);
if (diags) {
@ -220,12 +220,12 @@ namespace ts.server {
}
}
errorCheck(file: string, project: Project) {
private errorCheck(file: string, project: Project) {
this.syntacticCheck(file, project);
this.semanticCheck(file, project);
}
updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) {
private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) {
setTimeout(() => {
if (matchSeq(seq)) {
this.projectService.updateProjectStructure();
@ -233,7 +233,7 @@ namespace ts.server {
}, ms);
}
updateErrorCheck(checkList: PendingErrorCheck[], seq: number,
private updateErrorCheck(checkList: PendingErrorCheck[], seq: number,
matchSeq: (seq: number) => boolean, ms = 1500, followMs = 200) {
if (followMs > ms) {
followMs = ms;
@ -269,7 +269,7 @@ namespace ts.server {
}
}
getDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] {
private getDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -291,7 +291,7 @@ namespace ts.server {
}));
}
getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] {
private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -313,7 +313,7 @@ namespace ts.server {
}));
}
getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[]{
private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[]{
fileName = ts.normalizePath(fileName);
let project = this.projectService.getProjectForFile(fileName);
@ -343,7 +343,7 @@ namespace ts.server {
});
}
getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo {
private getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo {
fileName = ts.normalizePath(fileName)
let project = this.projectService.getProjectForFile(fileName)
@ -358,7 +358,7 @@ namespace ts.server {
return projectInfo;
}
getRenameLocations(line: number, offset: number, fileName: string,findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody {
private getRenameLocations(line: number, offset: number, fileName: string,findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -426,7 +426,7 @@ namespace ts.server {
return { info: renameInfo, locs: bakedRenameLocs };
}
getReferences(line: number, offset: number, fileName: string): protocol.ReferencesResponseBody {
private getReferences(line: number, offset: number, fileName: string): protocol.ReferencesResponseBody {
// TODO: get all projects for this file; report refs for all projects deleting duplicates
// can avoid duplicates by eliminating same ref file from subsequent projects
var file = ts.normalizePath(fileName);
@ -473,12 +473,12 @@ namespace ts.server {
};
}
openClientFile(fileName: string) {
private openClientFile(fileName: string) {
var file = ts.normalizePath(fileName);
this.projectService.openClientFile(file);
}
getQuickInfo(line: number, offset: number, fileName: string): protocol.QuickInfoResponseBody {
private getQuickInfo(line: number, offset: number, fileName: string): protocol.QuickInfoResponseBody {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -504,7 +504,7 @@ namespace ts.server {
};
}
getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] {
private getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -531,7 +531,7 @@ namespace ts.server {
});
}
getFormattingEditsAfterKeystroke(line: number, offset: number, key: string, fileName: string): protocol.CodeEdit[] {
private getFormattingEditsAfterKeystroke(line: number, offset: number, key: string, fileName: string): protocol.CodeEdit[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
@ -607,7 +607,7 @@ namespace ts.server {
});
}
getCompletions(line: number, offset: number, prefix: string, fileName: string): protocol.CompletionEntry[] {
private getCompletions(line: number, offset: number, prefix: string, fileName: string): protocol.CompletionEntry[] {
if (!prefix) {
prefix = "";
}
@ -633,7 +633,7 @@ namespace ts.server {
}, []).sort((a, b) => a.name.localeCompare(b.name));
}
getCompletionEntryDetails(line: number, offset: number,
private getCompletionEntryDetails(line: number, offset: number,
entryNames: string[], fileName: string): protocol.CompletionEntryDetails[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
@ -653,7 +653,7 @@ namespace ts.server {
}, []);
}
getSignatureHelpItems(line: number, offset: number, fileName: string): protocol.SignatureHelpItems {
private getSignatureHelpItems(line: number, offset: number, fileName: string): protocol.SignatureHelpItems {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -682,7 +682,7 @@ namespace ts.server {
return result;
}
getDiagnostics(delay: number, fileNames: string[]) {
private getDiagnostics(delay: number, fileNames: string[]) {
var checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => {
fileName = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(fileName);
@ -697,7 +697,7 @@ namespace ts.server {
}
}
change(line: number, offset: number, endLine: number, endOffset: number, insertString: string, fileName: string) {
private change(line: number, offset: number, endLine: number, endOffset: number, insertString: string, fileName: string) {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (project) {
@ -712,7 +712,7 @@ namespace ts.server {
}
}
reload(fileName: string, tempFileName: string, reqSeq = 0) {
private reload(fileName: string, tempFileName: string, reqSeq = 0) {
var file = ts.normalizePath(fileName);
var tmpfile = ts.normalizePath(tempFileName);
var project = this.projectService.getProjectForFile(file);
@ -725,7 +725,7 @@ namespace ts.server {
}
}
saveToTmp(fileName: string, tempFileName: string) {
private saveToTmp(fileName: string, tempFileName: string) {
var file = ts.normalizePath(fileName);
var tmpfile = ts.normalizePath(tempFileName);
@ -735,12 +735,12 @@ namespace ts.server {
}
}
closeClientFile(fileName: string) {
private closeClientFile(fileName: string) {
var file = ts.normalizePath(fileName);
this.projectService.closeClientFile(file);
}
decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] {
private decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] {
if (!items) {
return undefined;
}
@ -759,7 +759,7 @@ namespace ts.server {
}));
}
getNavigationBarItems(fileName: string): protocol.NavigationBarItem[] {
private getNavigationBarItems(fileName: string): protocol.NavigationBarItem[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -775,7 +775,7 @@ namespace ts.server {
return this.decorateNavigationBarItem(project, fileName, items);
}
getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] {
private getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
if (!project) {
@ -814,7 +814,7 @@ namespace ts.server {
});
}
getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] {
private getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] {
var file = ts.normalizePath(fileName);
var project = this.projectService.getProjectForFile(file);
@ -836,7 +836,7 @@ namespace ts.server {
}));
}
exit() {
public exit() {
}
private handlers : Map<(request: protocol.Request) => {response?: any, responseRequired?: boolean}> = {
@ -942,14 +942,14 @@ namespace ts.server {
return {response: this.getProjectInfo(file, needFileNameList)};
},
};
addProtocolHandler(command: string, handler: (request: protocol.Request) => {response?: any, responseRequired: boolean}) {
public addProtocolHandler(command: string, handler: (request: protocol.Request) => {response?: any, responseRequired: boolean}) {
if (this.handlers[command]) {
throw new Error(`Protocol handler already exists for command "${command}"`);
}
this.handlers[command] = handler;
}
executeCommand(request: protocol.Request) : {response?: any, responseRequired?: boolean} {
public executeCommand(request: protocol.Request) : {response?: any, responseRequired?: boolean} {
var handler = this.handlers[request.command];
if (handler) {
return handler(request);
@ -960,7 +960,7 @@ namespace ts.server {
}
}
onMessage(message: string) {
public onMessage(message: string) {
if (this.logger.isVerbose()) {
this.logger.info("request: " + message);
var start = this.hrtime();

View file

@ -0,0 +1,458 @@
/// <reference path="..\..\..\src\harness\harness.ts" />
module ts.server {
let lastWrittenToHost: string,
mockHost: ServerHost = {
args: [],
newLine: '\n',
useCaseSensitiveFileNames: true,
write: (s) => lastWrittenToHost = s,
readFile: () => void 0,
writeFile: () => void 0,
resolvePath: () => void 0,
fileExists: () => false,
directoryExists: () => false,
createDirectory: () => void 0,
getExecutingFilePath: () => void 0,
getCurrentDirectory: () => void 0,
readDirectory: () => void 0,
exit: () => void 0
},
mockLogger: Logger = {
close(): void {},
isVerbose(): boolean { return false; },
loggingEnabled(): boolean { return false; },
perftrc(s: string): void {},
info(s: string): void {},
startGroup(): void {},
endGroup(): void {},
msg(s: string, type?: string): void {},
};
describe('the Session class', () => {
let session:Session,
lastSent:protocol.Message;
beforeEach(() => {
session = new Session(mockHost, Buffer.byteLength, process.hrtime, mockLogger);
session.send = (msg: protocol.Message) => {
lastSent = msg;
};
});
describe('executeCommand', () => {
it('should throw when commands are executed with invalid arguments', () => {
let req : protocol.FileRequest = {
command: CommandNames.Open,
seq: 0,
type: 'command',
arguments: {
file: undefined
}
};
expect(() => session.executeCommand(req)).to.throw();
});
it('should output an error response when a command does not exist', () => {
let req : protocol.Request = {
command: 'foobar',
seq: 0,
type: 'command'
};
session.executeCommand(req);
expect(lastSent).to.deep.equal(<protocol.Response>{
command: CommandNames.Unknown,
type: 'response',
seq: 0,
message: 'Unrecognized JSON command: foobar',
request_seq: 0,
success: false
});
});
it('should return a tuple containing the response and if a response is required on success', () => {
let req : protocol.ConfigureRequest = {
command: CommandNames.Configure,
seq: 0,
type: 'command',
arguments: {
hostInfo: 'unit test',
formatOptions: {
newLineCharacter: '`n'
}
}
};
expect(session.executeCommand(req)).to.deep.equal({
responseRequired: false
});
expect(lastSent).to.deep.equal({
command: CommandNames.Configure,
type: 'response',
success: true,
request_seq: 0,
seq: 0,
body: undefined
});
});
});
describe('onMessage', () => {
it('should not throw when commands are executed with invalid arguments', () => {
let i = 0;
for (name in CommandNames) {
if (!Object.prototype.hasOwnProperty.call(CommandNames, name)) {
continue;
}
let req : protocol.Request = {
command: name,
seq: i++,
type: 'command'
};
session.onMessage(JSON.stringify(req));
req.seq+=2;
req.arguments = {};
session.onMessage(JSON.stringify(req));
req.seq+=2;
req.arguments = null;
session.onMessage(JSON.stringify(req));
}
});
it('should output the response for a correctly handled message', () => {
let req : protocol.ConfigureRequest = {
command: CommandNames.Configure,
seq: 0,
type: 'command',
arguments: {
hostInfo: 'unit test',
formatOptions: {
newLineCharacter: '`n'
}
}
};
session.onMessage(JSON.stringify(req));
expect(lastSent).to.deep.equal(<protocol.ConfigureResponse>{
command: CommandNames.Configure,
type: 'response',
success: true,
request_seq: 0,
seq: 0,
body: undefined
});
});
});
describe('exit', () => {
it('is a noop which can be handled by subclasses', () => {
session.exit(); //does nothing, should keep running tests
expect(session).to.exist;
});
});
describe('send', () => {
it('is an overrideable handle which sends protocol messages over the wire', () => {
let msg = {seq: 0, type: 'none'},
strmsg = JSON.stringify(msg),
len = 1+Buffer.byteLength(strmsg, 'utf8'),
resultMsg = `Content-Length: ${len}\r\n\r\n${strmsg}\n`;
session.send = Session.prototype.send;
assert(session.send);
expect(session.send(msg)).to.not.exist;
expect(lastWrittenToHost).to.equal(resultMsg);
});
});
describe('addProtocolHandler', () => {
it('can add protocol handlers', () => {
let respBody = {
item: false
},
command = 'newhandle',
result = {
response: respBody,
responseRequired: true
};
session.addProtocolHandler(command, (req) => result);
expect(session.executeCommand({
command,
seq: 0,
type: 'command'
})).to.deep.equal(result);
});
it('throws when a duplicate handler is passed', () => {
let respBody = {
item: false
},
resp = {
response: respBody,
responseRequired: true
},
command = 'newhandle';
session.addProtocolHandler(command, (req) => resp);
expect(() => session.addProtocolHandler(command, (req) => resp))
.to.throw(`Protocol handler already exists for command "${command}"`);
});
});
describe('event', () => {
it('can format event responses and send them', () => {
let evt = 'notify-test',
info = {
test: true
};
session.event(info, evt);
expect(lastSent).to.deep.equal({
type: 'event',
seq: 0,
event: evt,
body: info
});
});
});
describe('output', () => {
it('can format command responses and send them', () => {
let body = {
block: {
key: 'value'
}
},
command = 'test';
session.output(body, command);
expect(lastSent).to.deep.equal({
seq: 0,
request_seq: 0,
type: 'response',
command,
body: body,
success: true
});
});
});
});
describe('how Session is extendable via subclassing', () => {
let TestSession = class extends Session {
lastSent: protocol.Message;
customHandler:string = 'testhandler';
constructor(){
super(mockHost, Buffer.byteLength, process.hrtime, mockLogger);
this.addProtocolHandler(this.customHandler, () => {
return {response: undefined, responseRequired: true};
});
}
send(msg: protocol.Message) {
this.lastSent = msg;
}
};
it('can override methods such as send', () => {
let session = new TestSession(),
body = {
block: {
key: 'value'
}
},
command = 'test';
session.output(body, command);
expect(session.lastSent).to.deep.equal({
seq: 0,
request_seq: 0,
type: 'response',
command,
body: body,
success: true
});
});
it('can add and respond to new protocol handlers', () => {
let session = new TestSession();
expect(session.executeCommand({
seq: 0,
type: 'command',
command: session.customHandler
})).to.deep.equal({
response: undefined,
responseRequired: true
});
});
it('has access to the project service', () => {
let ServiceSession = class extends TestSession {
constructor() {
super();
assert(this.projectService);
expect(this.projectService).to.be.instanceOf(ProjectService);
}
};
new ServiceSession();
});
});
describe('an example of using the Session API to create an in-process server', () => {
let inProcHost: ServerHost = {
args: [],
newLine: '\n',
useCaseSensitiveFileNames: true,
write: (s) => lastWrittenToHost = s,
readFile: () => void 0,
writeFile: () => void 0,
resolvePath: () => void 0,
fileExists: () => false,
directoryExists: () => false,
createDirectory: () => void 0,
getExecutingFilePath: () => void 0,
getCurrentDirectory: () => void 0,
readDirectory: () => void 0,
exit: () => void 0
},
InProcSession = class extends Session {
private queue: protocol.Request[] = [];
constructor(private client: {handle: (msg: protocol.Message) => void}) {
super(inProcHost, Buffer.byteLength, process.hrtime, mockLogger);
this.addProtocolHandler('echo', (req: protocol.Request) => ({
response: req.arguments,
responseRequired: true
}));
}
send(msg: protocol.Message) {
this.client.handle(msg);
}
enqueue(msg: protocol.Request) {
this.queue = [msg].concat(this.queue);
}
handleRequest(msg: protocol.Request) {
try {
var {response} = this.executeCommand(msg);
} catch (e) {
this.output(undefined, msg.command, msg.seq, e.toString());
return;
}
if (response) {
this.output(response, msg.command, msg.seq);
}
}
consumeQueue() {
while (this.queue.length > 0) {
let elem = this.queue[this.queue.length-1];
this.queue = this.queue.slice(0,this.queue.length-1);
this.handleRequest(elem);
}
}
},
InProcClient = class {
private server: Session&{enqueue: (msg: protocol.Request) => void};
private seq: number = 0;
private callbacks: ts.Map<(resp: protocol.Response) => void> = {};
private eventHandlers: ts.Map<(args: any) => void> = {};
handle(msg: protocol.Message): void {
if (msg.type === 'response') {
var response = <protocol.Response>msg;
if (this.callbacks[response.request_seq]) {
this.callbacks[response.request_seq](response);
delete this.callbacks[response.request_seq];
}
} else if (msg.type === 'event') {
var event = <protocol.Event>msg;
this.emit(event.event, event.body);
}
}
emit(name: string, args: any): void {
if (!this.eventHandlers[name]) {
return;
}
this.eventHandlers[name](args);
}
on(name: string, handler: (args: any) => void): void {
this.eventHandlers[name] = handler;
}
connect(session: Session&{enqueue: (msg: protocol.Request) => void}): void {
this.server = session;
}
execute(command: string, args: any, callback: (resp: protocol.Response) => void): void {
if (!this.server) {
return;
}
this.seq++;
this.server.enqueue({
seq: this.seq,
type: 'command',
command,
arguments: args
});
this.callbacks[this.seq] = callback;
}
};
it('can be constructed and respond to commands', (done) => {
let cli = new InProcClient(),
session = new InProcSession(cli),
toEcho = {
data: true
},
toEvent = {
data: false
},
responses = 0;
//Connect the client
cli.connect(session);
//add an event handler
cli.on('testevent', (eventinfo) => {
expect(eventinfo).to.equal(toEvent);
responses++;
expect(responses).to.equal(1);
});
//trigger said event from the server
session.event(toEvent,'testevent');
//Queue an echo command
cli.execute('echo', toEcho, (resp) => {
assert(resp.success, resp.message);
responses++;
expect(responses).to.equal(2);
expect(resp.body).to.deep.equal(toEcho);
});
//Queue a configure command
cli.execute('configure', {
hostInfo: 'unit test',
formatOptions: {
newLineCharacter: '`n'
}
}, (resp) => {
assert(resp.success, resp.message);
responses++;
expect(responses).to.equal(3);
done();
});
//Consume the queue and trigger the callbacks
session.consumeQueue();
});
});
}