mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Add basic notebook model tests (#3396)
- Ported from the extension - Only adding tests that related to the internally implemented functionality, not to anything provider-specific.
This commit is contained in:
@@ -90,6 +90,7 @@
|
|||||||
"@types/mocha": "2.2.39",
|
"@types/mocha": "2.2.39",
|
||||||
"@types/sanitize-html": "^1.18.2",
|
"@types/sanitize-html": "^1.18.2",
|
||||||
"@types/semver": "5.3.30",
|
"@types/semver": "5.3.30",
|
||||||
|
"@types/should": "^13.0.0",
|
||||||
"@types/sinon": "1.16.34",
|
"@types/sinon": "1.16.34",
|
||||||
"@types/winreg": "^1.2.30",
|
"@types/winreg": "^1.2.30",
|
||||||
"asar": "^0.14.0",
|
"asar": "^0.14.0",
|
||||||
@@ -148,8 +149,10 @@
|
|||||||
"queue": "3.0.6",
|
"queue": "3.0.6",
|
||||||
"remap-istanbul": "^0.6.4",
|
"remap-istanbul": "^0.6.4",
|
||||||
"rimraf": "^2.2.8",
|
"rimraf": "^2.2.8",
|
||||||
|
"should": "^13.2.3",
|
||||||
"sinon": "^1.17.2",
|
"sinon": "^1.17.2",
|
||||||
"source-map": "^0.4.4",
|
"source-map": "^0.4.4",
|
||||||
|
"temp-write": "^3.4.0",
|
||||||
"tslint": "^5.9.1",
|
"tslint": "^5.9.1",
|
||||||
"typemoq": "^0.3.2",
|
"typemoq": "^0.3.2",
|
||||||
"typescript": "2.9.2",
|
"typescript": "2.9.2",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class SessionManager implements nb.SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmptySession implements nb.ISession {
|
export class EmptySession implements nb.ISession {
|
||||||
private _kernel: EmptyKernel;
|
private _kernel: EmptyKernel;
|
||||||
private _defaultKernelLoaded = false;
|
private _defaultKernelLoaded = false;
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ class EmptyKernel implements nb.IKernel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmptyFuture implements FutureInternal {
|
export class EmptyFuture implements FutureInternal {
|
||||||
|
|
||||||
|
|
||||||
get inProgress(): boolean {
|
get inProgress(): boolean {
|
||||||
|
|||||||
95
src/sqltest/parts/notebook/common.ts
Normal file
95
src/sqltest/parts/notebook/common.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { nb, IConnectionProfile } from 'sqlops';
|
||||||
|
|
||||||
|
import { Event, Emitter } from 'vs/base/common/event';
|
||||||
|
import { INotebookModel, ICellModel, IClientSession, IDefaultConnection } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
|
import { NotebookChangeType, CellType } from 'sql/parts/notebook/models/contracts';
|
||||||
|
import { INotebookManager } from 'sql/services/notebook/notebookService';
|
||||||
|
|
||||||
|
export class NotebookModelStub implements INotebookModel {
|
||||||
|
constructor(private _languageInfo?: nb.ILanguageInfo) {
|
||||||
|
}
|
||||||
|
public trustedMode: boolean;
|
||||||
|
|
||||||
|
public get languageInfo(): nb.ILanguageInfo {
|
||||||
|
return this._languageInfo;
|
||||||
|
}
|
||||||
|
onCellChange(cell: ICellModel, change: NotebookChangeType): void {
|
||||||
|
// Default: do nothing
|
||||||
|
}
|
||||||
|
get cells(): ReadonlyArray<ICellModel> {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
get clientSession(): IClientSession {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
get notebookManager(): INotebookManager {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
get kernelChanged(): Event<nb.IKernelChangedArgs> {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
get kernelsChanged(): Event<nb.IKernelSpec> {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
} get defaultKernel(): nb.IKernelSpec {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
get contextsChanged(): Event<void> {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
get specs(): nb.IAllKernels {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
get contexts(): IDefaultConnection {
|
||||||
|
throw new Error('method not implemented.');
|
||||||
|
}
|
||||||
|
changeKernel(displayName: string): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
changeContext(host: string, connection?: IConnectionProfile): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
findCellIndex(cellModel: ICellModel): number {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
addCell(cellType: CellType, index?: number): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
deleteCell(cellModel: ICellModel): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
saveModel(): Promise<boolean> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotebookManagerStub implements INotebookManager {
|
||||||
|
providerId: string;
|
||||||
|
contentManager: nb.ContentManager;
|
||||||
|
sessionManager: nb.SessionManager;
|
||||||
|
serverManager: nb.ServerManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerManagerStub implements nb.ServerManager {
|
||||||
|
public onServerStartedEmitter = new Emitter<void>();
|
||||||
|
onServerStarted: Event<void> = this.onServerStartedEmitter.event;
|
||||||
|
isStarted: boolean = false;
|
||||||
|
calledStart: boolean = false;
|
||||||
|
calledEnd: boolean = false;
|
||||||
|
public result: Promise<void> = undefined;
|
||||||
|
|
||||||
|
startServer(): Promise<void> {
|
||||||
|
this.calledStart = true;
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
stopServer(): Promise<void> {
|
||||||
|
this.calledEnd = true;
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
262
src/sqltest/parts/notebook/model/cell.test.ts
Normal file
262
src/sqltest/parts/notebook/model/cell.test.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as should from 'should';
|
||||||
|
import * as TypeMoq from 'typemoq';
|
||||||
|
import { nb } from 'sqlops';
|
||||||
|
|
||||||
|
import * as objects from 'vs/base/common/objects';
|
||||||
|
|
||||||
|
import { CellTypes } from 'sql/parts/notebook/models/contracts';
|
||||||
|
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||||
|
import { NotebookModelStub } from '../common';
|
||||||
|
import { EmptyFuture } from 'sql/services/notebook/sessionManager';
|
||||||
|
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
|
|
||||||
|
describe('Cell Model', function (): void {
|
||||||
|
let factory = new ModelFactory();
|
||||||
|
it('Should set default values if none defined', async function (): Promise<void> {
|
||||||
|
let cell = factory.createCell(undefined, undefined);
|
||||||
|
should(cell.cellType).equal(CellTypes.Code);
|
||||||
|
should(cell.source).equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should update values', async function (): Promise<void> {
|
||||||
|
let cell = factory.createCell(undefined, undefined);
|
||||||
|
cell.language = 'sql';
|
||||||
|
should(cell.language).equal('sql');
|
||||||
|
cell.source = 'abcd';
|
||||||
|
should(cell.source).equal('abcd');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should match ICell values if defined', async function (): Promise<void> {
|
||||||
|
let output: nb.IStreamResult = {
|
||||||
|
output_type: 'stream',
|
||||||
|
text: 'Some output',
|
||||||
|
name: 'stdout'
|
||||||
|
};
|
||||||
|
let cellData: nb.ICell = {
|
||||||
|
cell_type: CellTypes.Markdown,
|
||||||
|
source: 'some *markdown*',
|
||||||
|
outputs: [output],
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
};
|
||||||
|
let cell = factory.createCell(cellData, undefined);
|
||||||
|
should(cell.cellType).equal(cellData.cell_type);
|
||||||
|
should(cell.source).equal(cellData.source);
|
||||||
|
should(cell.outputs).have.length(1);
|
||||||
|
should(cell.outputs[0].output_type).equal('stream');
|
||||||
|
should((<nb.IStreamResult>cell.outputs[0]).text).equal('Some output');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Should set cell language to python if defined as python in languageInfo', async function (): Promise<void> {
|
||||||
|
let cellData: nb.ICell = {
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'print(\'1\')',
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let notebookModel = new NotebookModelStub({
|
||||||
|
name: 'python',
|
||||||
|
version: '',
|
||||||
|
mimetype: ''
|
||||||
|
});
|
||||||
|
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||||
|
should(cell.language).equal('python');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set cell language to python if defined as pyspark in languageInfo', async function (): Promise<void> {
|
||||||
|
let cellData: nb.ICell = {
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'print(\'1\')',
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let notebookModel = new NotebookModelStub({
|
||||||
|
name: 'pyspark',
|
||||||
|
version: '',
|
||||||
|
mimetype: ''
|
||||||
|
});
|
||||||
|
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||||
|
should(cell.language).equal('python');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set cell language to scala if defined as scala in languageInfo', async function (): Promise<void> {
|
||||||
|
let cellData: nb.ICell = {
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'print(\'1\')',
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let notebookModel = new NotebookModelStub({
|
||||||
|
name: 'scala',
|
||||||
|
version: '',
|
||||||
|
mimetype: ''
|
||||||
|
});
|
||||||
|
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||||
|
should(cell.language).equal('scala');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set cell language to python if no language defined', async function (): Promise<void> {
|
||||||
|
let cellData: nb.ICell = {
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'print(\'1\')',
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let notebookModel = new NotebookModelStub({
|
||||||
|
name: '',
|
||||||
|
version: '',
|
||||||
|
mimetype: ''
|
||||||
|
});
|
||||||
|
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||||
|
should(cell.language).equal('python');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should match cell language to language specified if unknown language defined in languageInfo', async function (): Promise<void> {
|
||||||
|
let cellData: nb.ICell = {
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'std::cout << "hello world";',
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let notebookModel = new NotebookModelStub({
|
||||||
|
name: 'cplusplus',
|
||||||
|
version: '',
|
||||||
|
mimetype: ''
|
||||||
|
});
|
||||||
|
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||||
|
should(cell.language).equal('cplusplus');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should match cell language to mimetype name is not supplied in languageInfo', async function (): Promise<void> {
|
||||||
|
let cellData: nb.ICell = {
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'print(\'1\')',
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let notebookModel = new NotebookModelStub({
|
||||||
|
name: '',
|
||||||
|
version: '',
|
||||||
|
mimetype: 'x-scala'
|
||||||
|
});
|
||||||
|
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||||
|
should(cell.language).equal('scala');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Model Future handling', function(): void {
|
||||||
|
let future: TypeMoq.Mock<EmptyFuture>;
|
||||||
|
let cell: ICellModel;
|
||||||
|
beforeEach(() => {
|
||||||
|
future = TypeMoq.Mock.ofType(EmptyFuture);
|
||||||
|
cell = factory.createCell({
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'print "Hello"',
|
||||||
|
metadata: { language: 'python'},
|
||||||
|
execution_count: 1
|
||||||
|
}, {
|
||||||
|
notebook: new NotebookModelStub({
|
||||||
|
name: '',
|
||||||
|
version: '',
|
||||||
|
mimetype: 'x-scala'
|
||||||
|
}),
|
||||||
|
isTrusted: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send and handle incoming messages', async () => {
|
||||||
|
// Given a future
|
||||||
|
let onReply: nb.MessageHandler<nb.IShellMessage>;
|
||||||
|
let onIopub: nb.MessageHandler<nb.IIOPubMessage>;
|
||||||
|
future.setup(f => f.setReplyHandler(TypeMoq.It.isAny())).callback((handler) => onReply = handler);
|
||||||
|
future.setup(f => f.setIOPubHandler(TypeMoq.It.isAny())).callback((handler) => onIopub = handler);
|
||||||
|
let outputs: ReadonlyArray<nb.ICellOutput> = undefined;
|
||||||
|
cell.onOutputsChanged((o => outputs = o));
|
||||||
|
|
||||||
|
// When I set it on the cell
|
||||||
|
cell.setFuture(future.object);
|
||||||
|
|
||||||
|
// Then I expect outputs to have been cleared
|
||||||
|
should(outputs).have.length(0);
|
||||||
|
should(onReply).not.be.undefined();
|
||||||
|
// ... And when I send an IoPub message
|
||||||
|
let message: nb.IIOPubMessage = {
|
||||||
|
channel: 'iopub',
|
||||||
|
type: 'iopub',
|
||||||
|
parent_header: undefined,
|
||||||
|
metadata: undefined,
|
||||||
|
header: <nb.IHeader> {
|
||||||
|
msg_type: 'stream'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
text: 'Printed hello world'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onIopub.handle(message);
|
||||||
|
// Then I expect an output to be added
|
||||||
|
should(outputs).have.length(1);
|
||||||
|
should(outputs[0].output_type).equal('stream');
|
||||||
|
|
||||||
|
message = objects.deepClone(message);
|
||||||
|
message.header.msg_type = 'display_data';
|
||||||
|
onIopub.handle(message);
|
||||||
|
should(outputs[1].output_type).equal('display_data');
|
||||||
|
|
||||||
|
// ... TODO: And when I sent a reply I expect it to be processed.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete transient tag while handling incoming messages', async () => {
|
||||||
|
// Given a future
|
||||||
|
let onIopub: nb.MessageHandler<nb.IIOPubMessage>;
|
||||||
|
future.setup(f => f.setIOPubHandler(TypeMoq.It.isAny())).callback((handler) => onIopub = handler);
|
||||||
|
let outputs: ReadonlyArray<nb.ICellOutput> = undefined;
|
||||||
|
cell.onOutputsChanged((o => outputs = o));
|
||||||
|
|
||||||
|
//Set the future
|
||||||
|
cell.setFuture(future.object);
|
||||||
|
|
||||||
|
// ... And when I send an IoPub message
|
||||||
|
let message: nb.IIOPubMessage = {
|
||||||
|
channel: 'iopub',
|
||||||
|
type: 'iopub',
|
||||||
|
parent_header: undefined,
|
||||||
|
metadata: undefined,
|
||||||
|
header: <nb.IHeader> {
|
||||||
|
msg_type: 'display_data'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
text: 'Printed hello world',
|
||||||
|
transient: 'transient data'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onIopub.handle(message);
|
||||||
|
//Output array's length should be 1
|
||||||
|
//'transient' tag should no longer exist in the output
|
||||||
|
should(outputs).have.length(1);
|
||||||
|
should(outputs[0]['transient']).be.undefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispose old future', async () => {
|
||||||
|
let oldFuture = TypeMoq.Mock.ofType(EmptyFuture);
|
||||||
|
cell.setFuture(oldFuture.object);
|
||||||
|
|
||||||
|
cell.setFuture(future.object);
|
||||||
|
|
||||||
|
oldFuture.verify(f => f.dispose(), TypeMoq.Times.once());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
202
src/sqltest/parts/notebook/model/clientSession.test.ts
Normal file
202
src/sqltest/parts/notebook/model/clientSession.test.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as should from 'should';
|
||||||
|
import * as TypeMoq from 'typemoq';
|
||||||
|
import { nb } from 'sqlops';
|
||||||
|
|
||||||
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
|
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||||
|
import URI from 'vs/base/common/uri';
|
||||||
|
|
||||||
|
import { ClientSession } from 'sql/parts/notebook/models/clientSession';
|
||||||
|
import { SessionManager, EmptySession } from 'sql/services/notebook/sessionManager';
|
||||||
|
import { NotebookManagerStub, ServerManagerStub } from 'sqltest/parts/notebook/common';
|
||||||
|
|
||||||
|
describe('Client Session', function(): void {
|
||||||
|
let path = URI.file('my/notebook.ipynb');
|
||||||
|
let notebookManager: NotebookManagerStub;
|
||||||
|
let serverManager: ServerManagerStub;
|
||||||
|
let mockSessionManager: TypeMoq.Mock<nb.SessionManager>;
|
||||||
|
let notificationService: TypeMoq.Mock<INotificationService>;
|
||||||
|
let session: ClientSession;
|
||||||
|
let remoteSession: ClientSession;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
serverManager = new ServerManagerStub();
|
||||||
|
mockSessionManager = TypeMoq.Mock.ofType(SessionManager);
|
||||||
|
notebookManager = new NotebookManagerStub();
|
||||||
|
notebookManager.serverManager = serverManager;
|
||||||
|
notebookManager.sessionManager = mockSessionManager.object;
|
||||||
|
notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose);
|
||||||
|
|
||||||
|
session = new ClientSession({
|
||||||
|
notebookManager: notebookManager,
|
||||||
|
notebookUri: path,
|
||||||
|
notificationService: notificationService.object
|
||||||
|
});
|
||||||
|
|
||||||
|
let serverlessNotebookManager = new NotebookManagerStub();
|
||||||
|
serverlessNotebookManager.sessionManager = mockSessionManager.object;
|
||||||
|
remoteSession = new ClientSession({
|
||||||
|
notebookManager: serverlessNotebookManager,
|
||||||
|
notebookUri: path,
|
||||||
|
notificationService: notificationService.object
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set path, isReady and ready on construction', function(): void {
|
||||||
|
should(session.notebookUri).equal(path);
|
||||||
|
should(session.ready).not.be.undefined();
|
||||||
|
should(session.isReady).be.false();
|
||||||
|
should(session.status).equal('starting');
|
||||||
|
should(session.isInErrorState).be.false();
|
||||||
|
should(session.errorMessage).be.undefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should call on serverManager startup if set', async function(): Promise<void> {
|
||||||
|
// Given I have a serverManager that starts successfully
|
||||||
|
serverManager.result = Promise.resolve();
|
||||||
|
should(session.isReady).be.false();
|
||||||
|
|
||||||
|
// When I kick off initialization
|
||||||
|
await session.initialize();
|
||||||
|
|
||||||
|
// Then I expect ready to be completed too
|
||||||
|
await session.ready;
|
||||||
|
should(serverManager.calledStart).be.true();
|
||||||
|
should(session.isReady).be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should go to error state if serverManager startup fails', async function(): Promise<void> {
|
||||||
|
// Given I have a serverManager that fails to start
|
||||||
|
serverManager.result = Promise.reject('error');
|
||||||
|
should(session.isInErrorState).be.false();
|
||||||
|
|
||||||
|
// When I initialize
|
||||||
|
await session.initialize();
|
||||||
|
|
||||||
|
// Then I expect ready to complete, but isInErrorState to be true
|
||||||
|
await session.ready;
|
||||||
|
should(session.isReady).be.true();
|
||||||
|
should(serverManager.calledStart).be.true();
|
||||||
|
should(session.isInErrorState).be.true();
|
||||||
|
should(session.errorMessage).equal('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be ready when session manager is ready', async function(): Promise<void> {
|
||||||
|
serverManager.result = new Promise((resolve) => {
|
||||||
|
serverManager.isStarted = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve());
|
||||||
|
|
||||||
|
// When I call initialize
|
||||||
|
await session.initialize();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
should(session.isReady).be.true();
|
||||||
|
should(session.isInErrorState).be.false();
|
||||||
|
await session.ready;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be in error state if server fails to start', async function(): Promise<void> {
|
||||||
|
serverManager.result = new Promise((resolve) => {
|
||||||
|
serverManager.isStarted = false;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve());
|
||||||
|
|
||||||
|
// When I call initialize
|
||||||
|
await session.initialize();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
await session.ready;
|
||||||
|
should(session.isReady).be.true();
|
||||||
|
should(session.isInErrorState).be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should go to error state if sessionManager fails', async function(): Promise<void> {
|
||||||
|
serverManager.isStarted = true;
|
||||||
|
mockSessionManager.setup(s => s.isReady).returns(() => false);
|
||||||
|
mockSessionManager.setup(s => s.ready).returns(() => Promise.reject('error'));
|
||||||
|
|
||||||
|
// When I call initialize
|
||||||
|
await session.initialize();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
should(session.isReady).be.true();
|
||||||
|
should(session.isInErrorState).be.true();
|
||||||
|
should(session.errorMessage).equal('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should start session automatically if kernel preference requests it', async function(): Promise<void> {
|
||||||
|
serverManager.isStarted = true;
|
||||||
|
mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve());
|
||||||
|
let sessionMock = TypeMoq.Mock.ofType(EmptySession);
|
||||||
|
let startOptions: nb.ISessionOptions = undefined;
|
||||||
|
mockSessionManager.setup(s => s.startNew(TypeMoq.It.isAny())).returns((options) => {
|
||||||
|
startOptions = options;
|
||||||
|
return Promise.resolve(sessionMock.object);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When I call initialize after defining kernel preferences
|
||||||
|
session.kernelPreference = {
|
||||||
|
shouldStart: true,
|
||||||
|
name: 'python'
|
||||||
|
};
|
||||||
|
await session.initialize();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
should(session.isReady).be.true();
|
||||||
|
should(session.isInErrorState).be.false();
|
||||||
|
should(startOptions.kernelName).equal('python');
|
||||||
|
should(startOptions.path).equal(path.fsPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should shutdown session even if no serverManager is set', async function(): Promise<void> {
|
||||||
|
// Given a session against a remote server
|
||||||
|
let expectedId = 'abc';
|
||||||
|
mockSessionManager.setup(s => s.isReady).returns(() => true);
|
||||||
|
mockSessionManager.setup(s => s.shutdown(TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||||
|
let sessionMock = TypeMoq.Mock.ofType(EmptySession);
|
||||||
|
sessionMock.setup(s => s.id).returns(() => expectedId);
|
||||||
|
mockSessionManager.setup(s => s.startNew(TypeMoq.It.isAny())).returns(() => Promise.resolve(sessionMock.object));
|
||||||
|
|
||||||
|
remoteSession.kernelPreference = {
|
||||||
|
shouldStart: true,
|
||||||
|
name: 'python'
|
||||||
|
};
|
||||||
|
await remoteSession.initialize();
|
||||||
|
|
||||||
|
// When I call shutdown
|
||||||
|
await remoteSession.shutdown();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
mockSessionManager.verify(s => s.shutdown(TypeMoq.It.isValue(expectedId)), TypeMoq.Times.once());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Should stop server if server is set', async function(): Promise<void> {
|
||||||
|
// Given a kernel has been started
|
||||||
|
serverManager.isStarted = true;
|
||||||
|
serverManager.result = Promise.resolve();
|
||||||
|
mockSessionManager.setup(s => s.isReady).returns(() => true);
|
||||||
|
mockSessionManager.setup(s => s.shutdown(TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||||
|
|
||||||
|
await session.initialize();
|
||||||
|
|
||||||
|
// When I call shutdown
|
||||||
|
await session.shutdown();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
should(serverManager.calledEnd).be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
77
src/sqltest/parts/notebook/model/contentManagers.test.ts
Normal file
77
src/sqltest/parts/notebook/model/contentManagers.test.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as should from 'should';
|
||||||
|
import * as TypeMoq from 'typemoq';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { nb } from 'sqlops';
|
||||||
|
|
||||||
|
import URI from 'vs/base/common/uri';
|
||||||
|
import * as tempWrite from 'temp-write';
|
||||||
|
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
|
||||||
|
import * as testUtils from '../../../utils/testUtils';
|
||||||
|
import { CellTypes } from 'sql/parts/notebook/models/contracts';
|
||||||
|
|
||||||
|
let expectedNotebookContent: nb.INotebook = {
|
||||||
|
cells: [{
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'insert into t1 values (c1, c2)',
|
||||||
|
metadata: { language: 'python' },
|
||||||
|
execution_count: 1
|
||||||
|
}],
|
||||||
|
metadata: {
|
||||||
|
kernelspec: {
|
||||||
|
name: 'mssql',
|
||||||
|
language: 'sql'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nbformat: 5,
|
||||||
|
nbformat_minor: 0
|
||||||
|
};
|
||||||
|
let notebookContentString = JSON.stringify(expectedNotebookContent);
|
||||||
|
|
||||||
|
function verifyMatchesExpectedNotebook(notebook: nb.INotebook): void {
|
||||||
|
should(notebook.cells).have.length(1, 'Expected 1 cell');
|
||||||
|
should(notebook.cells[0].cell_type).equal(CellTypes.Code);
|
||||||
|
should(notebook.cells[0].source).equal(expectedNotebookContent.cells[0].source);
|
||||||
|
should(notebook.metadata.kernelspec.name).equal(expectedNotebookContent.metadata.kernelspec.name);
|
||||||
|
should(notebook.nbformat).equal(expectedNotebookContent.nbformat);
|
||||||
|
should(notebook.nbformat_minor).equal(expectedNotebookContent.nbformat_minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Local Content Manager', function(): void {
|
||||||
|
let contentManager = new LocalContentManager();
|
||||||
|
|
||||||
|
it('Should return undefined if path is undefined', async function(): Promise<void> {
|
||||||
|
let content = await contentManager.getNotebookContents(undefined);
|
||||||
|
should(content).be.undefined();
|
||||||
|
// tslint:disable-next-line:no-null-keyword
|
||||||
|
content = await contentManager.getNotebookContents(null);
|
||||||
|
should(content).be.undefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should throw if file does not exist', async function(): Promise<void> {
|
||||||
|
await testUtils.assertThrowsAsync(async () => await contentManager.getNotebookContents(URI.file('/path/doesnot/exist.ipynb')), undefined);
|
||||||
|
});
|
||||||
|
it('Should return notebook contents parsed as INotebook when valid notebook file parsed', async function(): Promise<void> {
|
||||||
|
// Given a file containing a valid notebook
|
||||||
|
let localFile = tempWrite.sync(notebookContentString, 'notebook.ipynb');
|
||||||
|
// when I read the content
|
||||||
|
let notebook = await contentManager.getNotebookContents(URI.file(localFile));
|
||||||
|
// then I expect notebook format to match
|
||||||
|
verifyMatchesExpectedNotebook(notebook);
|
||||||
|
});
|
||||||
|
it('Should ignore invalid content in the notebook file', async function(): Promise<void> {
|
||||||
|
// Given a file containing a notebook with some garbage properties
|
||||||
|
let invalidContent = notebookContentString + '\\nasddfdsafasdf';
|
||||||
|
let localFile = tempWrite.sync(invalidContent, 'notebook.ipynb');
|
||||||
|
// when I read the content
|
||||||
|
let notebook = await contentManager.getNotebookContents(URI.file(localFile));
|
||||||
|
// then I expect notebook format to still be valid
|
||||||
|
verifyMatchesExpectedNotebook(notebook);
|
||||||
|
});
|
||||||
|
});
|
||||||
251
src/sqltest/parts/notebook/model/notebookModel.test.ts
Normal file
251
src/sqltest/parts/notebook/model/notebookModel.test.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as should from 'should';
|
||||||
|
import * as TypeMoq from 'typemoq';
|
||||||
|
import { nb } from 'sqlops';
|
||||||
|
|
||||||
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
|
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||||
|
import URI from 'vs/base/common/uri';
|
||||||
|
|
||||||
|
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
|
||||||
|
import * as testUtils from '../../../utils/testUtils';
|
||||||
|
import { NotebookManagerStub } from '../common';
|
||||||
|
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||||
|
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||||
|
import { IClientSession, ICellModel, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
|
import { ClientSession } from 'sql/parts/notebook/models/clientSession';
|
||||||
|
import { CellTypes } from 'sql/parts/notebook/models/contracts';
|
||||||
|
import { Deferred } from 'sql/base/common/promise';
|
||||||
|
import { ConnectionManagementService } from 'sql/parts/connection/common/connectionManagementService';
|
||||||
|
import { Memento } from 'vs/workbench/common/memento';
|
||||||
|
import { Emitter } from 'vs/base/common/event';
|
||||||
|
|
||||||
|
let expectedNotebookContent: nb.INotebook = {
|
||||||
|
cells: [{
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'insert into t1 values (c1, c2)',
|
||||||
|
metadata: { language: 'python' },
|
||||||
|
execution_count: 1
|
||||||
|
}, {
|
||||||
|
cell_type: CellTypes.Markdown,
|
||||||
|
source: 'I am *markdown*',
|
||||||
|
metadata: { language: 'python' },
|
||||||
|
execution_count: 1
|
||||||
|
}],
|
||||||
|
metadata: {
|
||||||
|
kernelspec: {
|
||||||
|
name: 'mssql',
|
||||||
|
language: 'sql'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nbformat: 5,
|
||||||
|
nbformat_minor: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let expectedNotebookContentOneCell: nb.INotebook = {
|
||||||
|
cells: [{
|
||||||
|
cell_type: CellTypes.Code,
|
||||||
|
source: 'insert into t1 values (c1, c2)',
|
||||||
|
metadata: { language: 'python' },
|
||||||
|
execution_count: 1
|
||||||
|
}],
|
||||||
|
metadata: {
|
||||||
|
kernelspec: {
|
||||||
|
name: 'mssql',
|
||||||
|
language: 'sql'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nbformat: 5,
|
||||||
|
nbformat_minor: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let defaultUri = URI.file('/some/path.ipynb');
|
||||||
|
|
||||||
|
let mockClientSession: TypeMoq.Mock<IClientSession>;
|
||||||
|
let sessionReady: Deferred<void>;
|
||||||
|
let mockModelFactory: TypeMoq.Mock<ModelFactory>;
|
||||||
|
let notificationService: TypeMoq.Mock<INotificationService>;
|
||||||
|
|
||||||
|
describe('notebook model', function(): void {
|
||||||
|
let notebookManager = new NotebookManagerStub();
|
||||||
|
let memento: TypeMoq.Mock<Memento>;
|
||||||
|
let queryConnectionService: TypeMoq.Mock<ConnectionManagementService>;
|
||||||
|
let defaultModelOptions: INotebookModelOptions;
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionReady = new Deferred<void>();
|
||||||
|
notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose);
|
||||||
|
memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, '');
|
||||||
|
memento.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => void 0);
|
||||||
|
queryConnectionService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined);
|
||||||
|
queryConnectionService.callBase = true;
|
||||||
|
defaultModelOptions = {
|
||||||
|
notebookUri: defaultUri,
|
||||||
|
factory: new ModelFactory(),
|
||||||
|
notebookManager,
|
||||||
|
notificationService: notificationService.object,
|
||||||
|
connectionService: queryConnectionService.object };
|
||||||
|
mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions);
|
||||||
|
mockClientSession.setup(c => c.initialize(TypeMoq.It.isAny())).returns(() => {
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
mockClientSession.setup(c => c.ready).returns(() => sessionReady.promise);
|
||||||
|
mockModelFactory = TypeMoq.Mock.ofType(ModelFactory);
|
||||||
|
mockModelFactory.callBase = true;
|
||||||
|
mockModelFactory.setup(f => f.createClientSession(TypeMoq.It.isAny())).returns(() => {
|
||||||
|
return mockClientSession.object;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should create single cell if model has no contents', async function(): Promise<void> {
|
||||||
|
// Given an empty notebook
|
||||||
|
let emptyNotebook: nb.INotebook = {
|
||||||
|
cells: [],
|
||||||
|
metadata: {
|
||||||
|
kernelspec: {
|
||||||
|
name: 'mssql',
|
||||||
|
language: 'sql'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nbformat: 5,
|
||||||
|
nbformat_minor: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
|
||||||
|
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(emptyNotebook));
|
||||||
|
notebookManager.contentManager = mockContentManager.object;
|
||||||
|
|
||||||
|
// When I initialize the model
|
||||||
|
let model = new NotebookModel(defaultModelOptions);
|
||||||
|
await model.requestModelLoad();
|
||||||
|
|
||||||
|
// Then I expect to have 1 code cell as the contents
|
||||||
|
should(model.cells).have.length(1);
|
||||||
|
should(model.cells[0].source).be.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should throw if model load fails', async function(): Promise<void> {
|
||||||
|
// Given a call to get Contents fails
|
||||||
|
let error = new Error('File not found');
|
||||||
|
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
|
||||||
|
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).throws(error);
|
||||||
|
notebookManager.contentManager = mockContentManager.object;
|
||||||
|
|
||||||
|
// When I initalize the model
|
||||||
|
// Then it should throw
|
||||||
|
let model = new NotebookModel(defaultModelOptions);
|
||||||
|
should(model.inErrorState).be.false();
|
||||||
|
await testUtils.assertThrowsAsync(() => model.requestModelLoad(), error.message);
|
||||||
|
should(model.inErrorState).be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should convert cell info to CellModels', async function(): Promise<void> {
|
||||||
|
// Given a notebook with 2 cells
|
||||||
|
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
|
||||||
|
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContent));
|
||||||
|
notebookManager.contentManager = mockContentManager.object;
|
||||||
|
|
||||||
|
// When I initalize the model
|
||||||
|
let model = new NotebookModel(defaultModelOptions);
|
||||||
|
await model.requestModelLoad();
|
||||||
|
|
||||||
|
// Then I expect all cells to be in the model
|
||||||
|
should(model.cells).have.length(2);
|
||||||
|
should(model.cells[0].source).be.equal(expectedNotebookContent.cells[0].source);
|
||||||
|
should(model.cells[1].source).be.equal(expectedNotebookContent.cells[1].source);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should load contents but then go to error state if client session startup fails', async function(): Promise<void> {
|
||||||
|
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
|
||||||
|
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell));
|
||||||
|
notebookManager.contentManager = mockContentManager.object;
|
||||||
|
|
||||||
|
// Given I have a session that fails to start
|
||||||
|
mockClientSession.setup(c => c.isInErrorState).returns(() => true);
|
||||||
|
mockClientSession.setup(c => c.errorMessage).returns(() => 'Error');
|
||||||
|
sessionReady.resolve();
|
||||||
|
let sessionFired = false;
|
||||||
|
|
||||||
|
let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, <Partial<INotebookModelOptions>> {
|
||||||
|
factory: mockModelFactory.object
|
||||||
|
});
|
||||||
|
let model = new NotebookModel(options);
|
||||||
|
model.onClientSessionReady((session) => sessionFired = true);
|
||||||
|
await model.requestModelLoad();
|
||||||
|
model.backgroundStartSession();
|
||||||
|
|
||||||
|
// Then I expect load to succeed
|
||||||
|
shouldHaveOneCell(model);
|
||||||
|
should(model.clientSession).not.be.undefined();
|
||||||
|
// but on server load completion I expect error state to be set
|
||||||
|
// Note: do not expect serverLoad event to throw even if failed
|
||||||
|
await model.sessionLoadFinished;
|
||||||
|
should(model.inErrorState).be.true();
|
||||||
|
should(sessionFired).be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not be in error state if client session initialization succeeds', async function(): Promise<void> {
|
||||||
|
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
|
||||||
|
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell));
|
||||||
|
notebookManager.contentManager = mockContentManager.object;
|
||||||
|
let kernelChangedEmitter: Emitter<nb.IKernelChangedArgs> = new Emitter<nb.IKernelChangedArgs>();
|
||||||
|
|
||||||
|
mockClientSession.setup(c => c.isInErrorState).returns(() => false);
|
||||||
|
mockClientSession.setup(c => c.isReady).returns(() => true);
|
||||||
|
mockClientSession.setup(c => c.kernelChanged).returns(() => kernelChangedEmitter.event);
|
||||||
|
|
||||||
|
queryConnectionService.setup(c => c.getActiveConnections(TypeMoq.It.isAny())).returns(() => null);
|
||||||
|
|
||||||
|
sessionReady.resolve();
|
||||||
|
let actualSession: IClientSession = undefined;
|
||||||
|
|
||||||
|
let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, <Partial<INotebookModelOptions>> {
|
||||||
|
factory: mockModelFactory.object
|
||||||
|
});
|
||||||
|
let model = new NotebookModel(options, false);
|
||||||
|
model.onClientSessionReady((session) => actualSession = session);
|
||||||
|
await model.requestModelLoad();
|
||||||
|
model.backgroundStartSession();
|
||||||
|
|
||||||
|
// Then I expect load to succeed
|
||||||
|
should(model.clientSession).not.be.undefined();
|
||||||
|
// but on server load completion I expect error state to be set
|
||||||
|
// Note: do not expect serverLoad event to throw even if failed
|
||||||
|
let kernelChangedArg: nb.IKernelChangedArgs = undefined;
|
||||||
|
model.kernelChanged((kernel) => kernelChangedArg = kernel);
|
||||||
|
await model.sessionLoadFinished;
|
||||||
|
should(model.inErrorState).be.false();
|
||||||
|
should(actualSession).equal(mockClientSession.object);
|
||||||
|
should(model.clientSession).equal(mockClientSession.object);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should sanitize kernel display name when IP is included', async function(): Promise<void> {
|
||||||
|
let model = new NotebookModel(defaultModelOptions);
|
||||||
|
let displayName = 'PySpark (1.1.1.1)';
|
||||||
|
let sanitizedDisplayName = model.sanitizeDisplayName(displayName);
|
||||||
|
should(sanitizedDisplayName).equal('PySpark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should sanitize kernel display name properly when IP is not included', async function(): Promise<void> {
|
||||||
|
let model = new NotebookModel(defaultModelOptions);
|
||||||
|
let displayName = 'PySpark';
|
||||||
|
let sanitizedDisplayName = model.sanitizeDisplayName(displayName);
|
||||||
|
should(sanitizedDisplayName).equal('PySpark');
|
||||||
|
});
|
||||||
|
|
||||||
|
function shouldHaveOneCell(model: NotebookModel): void {
|
||||||
|
should(model.cells).have.length(1);
|
||||||
|
verifyCellModel(model.cells[0], { cell_type: CellTypes.Code, source: 'insert into t1 values (c1, c2)', metadata: { language: 'python' }, execution_count: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyCellModel(cellModel: ICellModel, expected: nb.ICell): void {
|
||||||
|
should(cellModel.cellType).equal(expected.cell_type);
|
||||||
|
should(cellModel.source).equal(expected.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
238
src/typings/should.d.ts
vendored
Executable file
238
src/typings/should.d.ts
vendored
Executable file
@@ -0,0 +1,238 @@
|
|||||||
|
// Type definitions for should.js
|
||||||
|
|
||||||
|
declare function should(obj: any): should.Assertion;
|
||||||
|
|
||||||
|
// node assert methods
|
||||||
|
/*interface NodeAssert {
|
||||||
|
fail(actual: any, expected: any, message?: string, operator?: string): void;
|
||||||
|
ok(value: any, message?: string): void;
|
||||||
|
equal(actual: any, expected: any, message?: string): void;
|
||||||
|
notEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
deepEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
notDeepEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
strictEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
notStrictEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
|
||||||
|
throws(block: Function, message?: string): void;
|
||||||
|
throws(block: Function, error: Function, message?: string): void;
|
||||||
|
throws(block: Function, error: RegExp, message?: string): void;
|
||||||
|
throws(block: Function, error: (err: any) => boolean, message?: string): void;
|
||||||
|
|
||||||
|
doesNotThrow(block: Function, message?: string): void;
|
||||||
|
doesNotThrow(block: Function, error: Function, message?: string): void;
|
||||||
|
doesNotThrow(block: Function, error: RegExp, message?: string): void;
|
||||||
|
doesNotThrow(block: Function, error: (err: any) => boolean, message?: string): void;
|
||||||
|
|
||||||
|
ifError(value: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface should extends NodeAssert, ShouldAssertExt {
|
||||||
|
not: ShouldAssertExt;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
declare module should {
|
||||||
|
interface ShouldAssertExt {
|
||||||
|
exist(obj: any, msg?: string): void;
|
||||||
|
exists(obj: any, msg?: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(actual: any, expected: any, message?: string, operator?: string): void;
|
||||||
|
function ok(value: any, message?: string): void;
|
||||||
|
function equal(actual: any, expected: any, message?: string): void;
|
||||||
|
function notEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
function deepEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
function notDeepEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
function strictEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
function notStrictEqual(actual: any, expected: any, message?: string): void;
|
||||||
|
|
||||||
|
function throws(block: Function, message?: string): void;
|
||||||
|
function throws(block: Function, error: Function, message?: string): void;
|
||||||
|
function throws(block: Function, error: RegExp, message?: string): void;
|
||||||
|
function throws(block: Function, error: (err: any) => boolean, message?: string): void;
|
||||||
|
|
||||||
|
function doesNotThrow(block: Function, message?: string): void;
|
||||||
|
function doesNotThrow(block: Function, error: Function, message?: string): void;
|
||||||
|
function doesNotThrow(block: Function, error: RegExp, message?: string): void;
|
||||||
|
function doesNotThrow(block: Function, error: (err: any) => boolean, message?: string): void;
|
||||||
|
|
||||||
|
function ifError(value: any): void;
|
||||||
|
|
||||||
|
function exist(obj: any, msg?: string): void;
|
||||||
|
function exists(obj: any, msg?: string): void;
|
||||||
|
|
||||||
|
const not: ShouldAssertExt;
|
||||||
|
|
||||||
|
interface Assertion {
|
||||||
|
assert(expr: boolean): this;
|
||||||
|
fail(): this;
|
||||||
|
|
||||||
|
not: this;
|
||||||
|
any: this;
|
||||||
|
only: this;
|
||||||
|
|
||||||
|
// bool
|
||||||
|
true(message?: string): this;
|
||||||
|
True(message?: string): this;
|
||||||
|
|
||||||
|
false(message?: string): this;
|
||||||
|
False(message?: string): this;
|
||||||
|
|
||||||
|
ok(): this;
|
||||||
|
|
||||||
|
//chain
|
||||||
|
an: this;
|
||||||
|
of: this;
|
||||||
|
a: this;
|
||||||
|
and: this;
|
||||||
|
be: this;
|
||||||
|
been: this;
|
||||||
|
has: this;
|
||||||
|
have: this;
|
||||||
|
with: this;
|
||||||
|
is: this;
|
||||||
|
which: this;
|
||||||
|
the: this;
|
||||||
|
it: this;
|
||||||
|
|
||||||
|
//contain
|
||||||
|
containEql(obj: any): this;
|
||||||
|
containDeepOrdered(obj: any): this;
|
||||||
|
containDeep(obj: any): this;
|
||||||
|
|
||||||
|
// eql
|
||||||
|
eql(obj: any, description?: string): this;
|
||||||
|
eqls(obj: any, description?: string): this;
|
||||||
|
deepEqual(obj: any, description?: string): this;
|
||||||
|
|
||||||
|
equal(obj: any, description?: string): this;
|
||||||
|
equals(obj: any, description?: string): this;
|
||||||
|
exactly(obj: any, description?: string): this;
|
||||||
|
|
||||||
|
equalOneOf(...objs: any[]): this;
|
||||||
|
equalOneOf(obj: any[]): this;
|
||||||
|
oneOf(...objs: any[]): this;
|
||||||
|
oneOf(obj: any[]): this;
|
||||||
|
|
||||||
|
//error
|
||||||
|
throw(): this;
|
||||||
|
throw(msg: RegExp | string | Function, properties?: {}): this;
|
||||||
|
throw(properties: {}): this;
|
||||||
|
//TODO how to express generators???
|
||||||
|
throwError(): this;
|
||||||
|
throwError(msg: RegExp | string | Function, properties?: {}): this;
|
||||||
|
throwError(properties: {}): this;
|
||||||
|
|
||||||
|
// match
|
||||||
|
match(
|
||||||
|
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
|
||||||
|
description?: string
|
||||||
|
): this;
|
||||||
|
matchEach(
|
||||||
|
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
|
||||||
|
description?: string
|
||||||
|
): this;
|
||||||
|
matchEvery(
|
||||||
|
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
|
||||||
|
description?: string
|
||||||
|
): this;
|
||||||
|
matchAny(
|
||||||
|
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
|
||||||
|
description?: string
|
||||||
|
): this;
|
||||||
|
matchSome(
|
||||||
|
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
|
||||||
|
description?: string
|
||||||
|
): this;
|
||||||
|
|
||||||
|
//number
|
||||||
|
NaN(): this;
|
||||||
|
Infinity(): this;
|
||||||
|
within(start: number, finish: number, description?: string): this;
|
||||||
|
approximately(value: number, delta: number, description?: string): this;
|
||||||
|
above(value: number, description?: string): this;
|
||||||
|
greaterThan(value: number, description?: string): this;
|
||||||
|
below(value: number, description?: string): this;
|
||||||
|
lessThan(value: number, description?: string): this;
|
||||||
|
aboveOrEqual(value: number, description?: string): this;
|
||||||
|
greaterThanOrEqual(value: number, description?: string): this;
|
||||||
|
belowOrEqual(value: number, description?: string): this;
|
||||||
|
lessThanOrEqual(value: number, description?: string): this;
|
||||||
|
|
||||||
|
//promise
|
||||||
|
Promise(): this;
|
||||||
|
|
||||||
|
fulfilled(): Promise<any>;
|
||||||
|
resolved(): Promise<any>;
|
||||||
|
rejected(): Promise<any>;
|
||||||
|
|
||||||
|
fulfilledWith(obj: any): Promise<any>;
|
||||||
|
resolvedWith(obj: any): Promise<any>;
|
||||||
|
rejectedWith(msg: RegExp | string | Error, properties?: {}): Promise<any>;
|
||||||
|
rejectedWith(properties: {}): Promise<any>;
|
||||||
|
finally: PromisedAssertion;
|
||||||
|
eventually: PromisedAssertion;
|
||||||
|
|
||||||
|
// property
|
||||||
|
propertyWithDescriptor(name: string, descriptor: {}): this;
|
||||||
|
|
||||||
|
property(name: string, value?: any): this;
|
||||||
|
properties(...names: string[]): this;
|
||||||
|
properties(names: string[]): this;
|
||||||
|
properties(props: {}): this;
|
||||||
|
|
||||||
|
length(value: number, description?: string): this;
|
||||||
|
lengthOf(value: number, description?: string): this;
|
||||||
|
|
||||||
|
ownProperty(name: string, description?: string): this;
|
||||||
|
hasOwnProperty(name: string, description?: string): this;
|
||||||
|
|
||||||
|
empty(): this;
|
||||||
|
|
||||||
|
keys(...keys: any[]): this;
|
||||||
|
key(key: any): this;
|
||||||
|
|
||||||
|
value(key: any, value: any): this;
|
||||||
|
|
||||||
|
size(value: number): this;
|
||||||
|
|
||||||
|
propertyByPath(...path: string[]): this;
|
||||||
|
propertyByPath(path: string[]): this;
|
||||||
|
|
||||||
|
//string
|
||||||
|
startWith(prefix: string, description?: string): this;
|
||||||
|
endWith(postfix: string, description?: string): this;
|
||||||
|
|
||||||
|
//type
|
||||||
|
Number(): this;
|
||||||
|
arguments(): this;
|
||||||
|
Arguments(): this;
|
||||||
|
type(typeName: string, description?: string): this;
|
||||||
|
instanceof(constructor: Function, description?: string): this;
|
||||||
|
instanceOf(constructor: Function, description?: string): this;
|
||||||
|
Function(): this;
|
||||||
|
Object(): this;
|
||||||
|
String(): this;
|
||||||
|
Array(): this;
|
||||||
|
Boolean(): this;
|
||||||
|
Error(): this;
|
||||||
|
Date(): this;
|
||||||
|
null(): this;
|
||||||
|
Null(): this;
|
||||||
|
class(className: string): this;
|
||||||
|
Class(className: string): this;
|
||||||
|
undefined(): this;
|
||||||
|
Undefined(): this;
|
||||||
|
iterable(): this;
|
||||||
|
iterator(): this;
|
||||||
|
generator(): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromisedAssertion extends Assertion, PromiseLike<any> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare module 'should' {
|
||||||
|
export = should;
|
||||||
|
}
|
||||||
3
src/typings/temp-write.d.ts
vendored
Normal file
3
src/typings/temp-write.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
declare module 'temp-write' {
|
||||||
|
function sync(input: string, filePath?: string): string;
|
||||||
|
}
|
||||||
82
yarn.lock
82
yarn.lock
@@ -99,6 +99,13 @@
|
|||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
|
||||||
|
|
||||||
|
"@types/should@^13.0.0":
|
||||||
|
version "13.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/should/-/should-13.0.0.tgz#96c00117f1896177848fdecfa336313c230c879e"
|
||||||
|
integrity sha512-Mi6YZ2ABnnGGFMuiBDP0a8s1ZDCDNHqP97UH8TyDmCWuGGavpsFMfJnAMYaaqmDlSCOCNbVLHBrSDEOpx/oLhw==
|
||||||
|
dependencies:
|
||||||
|
should "*"
|
||||||
|
|
||||||
"@types/sinon@1.16.34":
|
"@types/sinon@1.16.34":
|
||||||
version "1.16.34"
|
version "1.16.34"
|
||||||
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-1.16.34.tgz#a9761fff33d0f7b3fe61875b577778a2576a9a03"
|
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-1.16.34.tgz#a9761fff33d0f7b3fe61875b577778a2576a9a03"
|
||||||
@@ -4373,6 +4380,13 @@ macaddress@^0.2.8:
|
|||||||
version "0.2.8"
|
version "0.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
||||||
|
|
||||||
|
make-dir@^1.0.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
||||||
|
integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
|
||||||
|
dependencies:
|
||||||
|
pify "^3.0.0"
|
||||||
|
|
||||||
make-error-cause@^1.1.1:
|
make-error-cause@^1.1.1:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d"
|
resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d"
|
||||||
@@ -5265,6 +5279,11 @@ pify@^2.0.0:
|
|||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
|
|
||||||
|
pify@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
|
||||||
|
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
|
||||||
|
|
||||||
pinkie-promise@^2.0.0:
|
pinkie-promise@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
||||||
@@ -6350,6 +6369,50 @@ shelljs@^0.7.5:
|
|||||||
interpret "^1.0.0"
|
interpret "^1.0.0"
|
||||||
rechoir "^0.6.2"
|
rechoir "^0.6.2"
|
||||||
|
|
||||||
|
should-equal@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
|
||||||
|
integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==
|
||||||
|
dependencies:
|
||||||
|
should-type "^1.4.0"
|
||||||
|
|
||||||
|
should-format@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1"
|
||||||
|
integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE=
|
||||||
|
dependencies:
|
||||||
|
should-type "^1.3.0"
|
||||||
|
should-type-adaptors "^1.0.1"
|
||||||
|
|
||||||
|
should-type-adaptors@^1.0.1:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a"
|
||||||
|
integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==
|
||||||
|
dependencies:
|
||||||
|
should-type "^1.3.0"
|
||||||
|
should-util "^1.0.0"
|
||||||
|
|
||||||
|
should-type@^1.3.0, should-type@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3"
|
||||||
|
integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=
|
||||||
|
|
||||||
|
should-util@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063"
|
||||||
|
integrity sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=
|
||||||
|
|
||||||
|
should@*, should@^13.2.3:
|
||||||
|
version "13.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10"
|
||||||
|
integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==
|
||||||
|
dependencies:
|
||||||
|
should-equal "^2.0.0"
|
||||||
|
should-format "^3.0.3"
|
||||||
|
should-type "^1.4.0"
|
||||||
|
should-type-adaptors "^1.0.1"
|
||||||
|
should-util "^1.0.0"
|
||||||
|
|
||||||
sigmund@^1.0.1, sigmund@~1.0.0:
|
sigmund@^1.0.1, sigmund@~1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
||||||
@@ -6804,6 +6867,23 @@ tar-stream@^1.1.2:
|
|||||||
to-buffer "^1.1.0"
|
to-buffer "^1.1.0"
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
|
temp-dir@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d"
|
||||||
|
integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=
|
||||||
|
|
||||||
|
temp-write@^3.4.0:
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492"
|
||||||
|
integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
is-stream "^1.1.0"
|
||||||
|
make-dir "^1.0.0"
|
||||||
|
pify "^3.0.0"
|
||||||
|
temp-dir "^1.0.0"
|
||||||
|
uuid "^3.0.1"
|
||||||
|
|
||||||
temp@^0.8.3:
|
temp@^0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
|
resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
|
||||||
@@ -7228,7 +7308,7 @@ uuid@^3.0.0, uuid@^3.1.0:
|
|||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||||
|
|
||||||
uuid@^3.3.2:
|
uuid@^3.0.1, uuid@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user