/// const expect: typeof _chai.expect = _chai.expect; namespace ts.server { let lastWrittenToHost: string; const mockHost: ServerHost = { args: [], newLine: "\n", useCaseSensitiveFileNames: true, write(s): void { lastWrittenToHost = s; }, readFile(): string { return void 0; }, writeFile(): void {}, resolvePath(): string { return void 0; }, fileExists: () => false, directoryExists: () => false, getDirectories: () => [], createDirectory(): void {}, getExecutingFilePath(): string { return void 0; }, getCurrentDirectory(): string { return void 0; }, getEnvironmentVariable(name: string): string { return ""; }, readDirectory(): string[] { return []; }, exit(): void { }, setTimeout(callback, ms, ...args) { return 0; }, clearTimeout(timeoutId) { } }; const 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; let lastSent: protocol.Message; beforeEach(() => { session = new Session(mockHost, Utils.byteLength, process.hrtime, mockLogger); session.send = (msg: protocol.Message) => { lastSent = msg; }; }); describe("executeCommand", () => { it("should throw when commands are executed with invalid arguments", () => { const 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", () => { const req: protocol.Request = { command: "foobar", seq: 0, type: "command" }; session.executeCommand(req); expect(lastSent).to.deep.equal({ 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", () => { const 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; } const req: protocol.Request = { command: name, seq: i, type: "command" }; i++; session.onMessage(JSON.stringify(req)); req.seq = i; i++; req.arguments = {}; session.onMessage(JSON.stringify(req)); req.seq = i; i++; /* tslint:disable no-null-keyword */ req.arguments = null; /* tslint:enable no-null-keyword */ session.onMessage(JSON.stringify(req)); req.seq = i; i++; req.arguments = ""; session.onMessage(JSON.stringify(req)); req.seq = i; i++; req.arguments = 0; session.onMessage(JSON.stringify(req)); req.seq = i; i++; req.arguments = []; session.onMessage(JSON.stringify(req)); } session.onMessage("GARBAGE NON_JSON DATA"); }); it("should output the response for a correctly handled message", () => { const 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({ command: CommandNames.Configure, type: "response", success: true, request_seq: 0, seq: 0, body: undefined }); }); }); describe("send", () => { it("is an overrideable handle which sends protocol messages over the wire", () => { const msg = {seq: 0, type: "none"}; const strmsg = JSON.stringify(msg); const len = 1 + Utils.byteLength(strmsg, "utf8"); const 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", () => { const respBody = { item: false }; const command = "newhandle"; const 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", () => { const respBody = { item: false }; const resp = { response: respBody, responseRequired: true }; const 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", () => { const evt = "notify-test"; const 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", () => { const body = { block: { key: "value" } }; const 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", () => { class TestSession extends Session { lastSent: protocol.Message; customHandler = "testhandler"; constructor() { super(mockHost, Utils.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", () => { const session = new TestSession(); const body = { block: { key: "value" } }; const 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", () => { const 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", () => { class ServiceSession 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", () => { class InProcSession extends Session { private queue: protocol.Request[] = []; constructor(private client: InProcClient) { super(mockHost, Utils.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.unshift(msg); } handleRequest(msg: protocol.Request) { let response: protocol.Response; try { ({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) { const elem = this.queue.pop(); this.handleRequest(elem); } } } class InProcClient { private server: InProcSession; private seq = 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") { const 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") { const event = msg; this.emit(event.event, event.body); } } emit(name: string, args: any): void { if (this.eventHandlers[name]) { this.eventHandlers[name](args); } } on(name: string, handler: (args: any) => void): void { this.eventHandlers[name] = handler; } connect(session: InProcSession): 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) => { const cli = new InProcClient(); const session = new InProcSession(cli); const toEcho = { data: true }; const toEvent = { data: false }; let 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(); }); }); }