From baf7e1ea73b39cea14ad828c0bb83b410ade5f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20M=C3=BCller?= Date: Mon, 13 Nov 2023 11:18:21 +0100 Subject: [PATCH 1/5] - Replaced @typescript-eslint/unbound-method with jest/unbound-method for test files - Fixed an jest ts type issue preventing jest.mock returns to use mockFn.mockRejectedValueOnce etc --- .eslintrc.json | 7 ++++++- .../rdbms/exec/test/mocks/postgres-loader-executor-mock.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index ca3801f25..65f42ce94 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,10 +43,15 @@ }, { "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], + "plugins": ["jest"], "env": { "jest": true }, - "rules": {} + "rules": { + // you should turn the original rule off *only* for test files + "@typescript-eslint/unbound-method": "off", + "jest/unbound-method": "error" + } } ] } diff --git a/libs/extensions/rdbms/exec/test/mocks/postgres-loader-executor-mock.ts b/libs/extensions/rdbms/exec/test/mocks/postgres-loader-executor-mock.ts index 6aba1c095..3c7d0cd77 100644 --- a/libs/extensions/rdbms/exec/test/mocks/postgres-loader-executor-mock.ts +++ b/libs/extensions/rdbms/exec/test/mocks/postgres-loader-executor-mock.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { BlockExecutorMock } from '@jvalue/jayvee-execution/test'; import { Client } from 'pg'; -type MockedPgClient = jest.Mocked>; +type MockedPgClient = jest.Mocked; export class PostgresLoaderExecutorMock implements BlockExecutorMock { private _pgClient: MockedPgClient | undefined; @@ -26,7 +26,7 @@ export class PostgresLoaderExecutorMock implements BlockExecutorMock { ) => void = defaultPostgresMockRegistration, ) { // setup pg mock - this._pgClient = new Client(); + this._pgClient = new Client() as MockedPgClient; registerMocks(this._pgClient); } restore() { From 8d53fad9a0633fb42ddb546c9ea34449e09d5786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20M=C3=BCller?= Date: Mon, 13 Nov 2023 11:26:07 +0100 Subject: [PATCH 2/5] Added tests for PostgresLoaderExecutor --- .../src/lib/postgres-loader-executor.spec.ts | 167 ++++++++++++++++++ .../valid-postgres-loader.jv | 20 +++ .../test/test-extension/TestBlockTypes.jv | 8 + 3 files changed, 195 insertions(+) create mode 100644 libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts create mode 100644 libs/extensions/rdbms/exec/test/assets/postgres-loader-executor/valid-postgres-loader.jv create mode 100644 libs/extensions/rdbms/exec/test/test-extension/TestBlockTypes.jv diff --git a/libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts b/libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts new file mode 100644 index 000000000..56785beb2 --- /dev/null +++ b/libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import * as path from 'path'; + +import * as R from '@jvalue/jayvee-execution'; +import { + constructTable, + getTestExecutionContext, +} from '@jvalue/jayvee-execution/test'; +import { + BlockDefinition, + IOType, + PrimitiveValuetypes, + createJayveeServices, +} from '@jvalue/jayvee-language-server'; +import { + ParseHelperOptions, + expectNoParserAndLexerErrors, + loadTestExtensions, + parseHelper, + readJvTestAssetHelper, +} from '@jvalue/jayvee-language-server/test'; +import { AstNode, AstNodeLocator, LangiumDocument } from 'langium'; +import { NodeFileSystem } from 'langium/node'; +import { Client } from 'pg'; + +import { PostgresLoaderExecutor } from './postgres-loader-executor'; + +jest.mock('pg', () => { + const mClient = { + connect: jest.fn(), + query: jest.fn(), + end: jest.fn(), + }; + return { Client: jest.fn(() => mClient) }; +}); + +describe('Validation of PostgresLoaderExecutor', () => { + let parse: ( + input: string, + options?: ParseHelperOptions, + ) => Promise>; + + let locator: AstNodeLocator; + let pgClient: jest.Mocked; + + const readJvTestAsset = readJvTestAssetHelper( + __dirname, + '../../test/assets/postgres-loader-executor/', + ); + + async function parseAndExecuteExecutor( + input: string, + IOInput: R.Table, + ): Promise> { + const document = await parse(input, { validationChecks: 'all' }); + expectNoParserAndLexerErrors(document); + + const block = locator.getAstNode( + document.parseResult.value, + 'pipelines@0/blocks@1', + ) as BlockDefinition; + + return new PostgresLoaderExecutor().doExecute( + IOInput, + getTestExecutionContext(locator, document, [block]), + ); + } + + beforeAll(async () => { + // Create language services + const services = createJayveeServices(NodeFileSystem).Jayvee; + await loadTestExtensions(services, [ + path.resolve(__dirname, '../../test/test-extension/TestBlockTypes.jv'), + ]); + locator = services.workspace.AstNodeLocator; + // Parse function for Jayvee (without validation) + parse = parseHelper(services); + }); + beforeEach(() => { + pgClient = new Client() as jest.Mocked; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should diagnose no error on valid loader config', async () => { + const text = readJvTestAsset('valid-postgres-loader.jv'); + + const inputTable = constructTable( + [ + { + columnName: 'Column1', + column: { + values: ['value 1'], + valuetype: PrimitiveValuetypes.Text, + }, + }, + { + columnName: 'Column2', + column: { + values: [20.2], + valuetype: PrimitiveValuetypes.Decimal, + }, + }, + ], + 1, + ); + const result = await parseAndExecuteExecutor(text, inputTable); + + expect(R.isErr(result)).toEqual(false); + if (R.isOk(result)) { + expect(result.right.ioType).toEqual(IOType.NONE); + expect(pgClient.connect).toBeCalledTimes(1); + expect(pgClient.query).nthCalledWith(1, 'DROP TABLE IF EXISTS "Test";'); + expect(pgClient.query).nthCalledWith( + 2, + `CREATE TABLE IF NOT EXISTS "Test" ("Column1" text,"Column2" real);`, + ); + expect(pgClient.query).nthCalledWith( + 3, + `INSERT INTO "Test" ("Column1","Column2") VALUES ('value 1',20.2)`, + ); + expect(pgClient.end).toBeCalledTimes(1); + } + }); + + it('should diagnose error on pg client connect error', async () => { + const text = readJvTestAsset('valid-postgres-loader.jv'); + + const inputTable = constructTable( + [ + { + columnName: 'Column1', + column: { + values: ['value 1'], + valuetype: PrimitiveValuetypes.Text, + }, + }, + { + columnName: 'Column2', + column: { + values: [20.2], + valuetype: PrimitiveValuetypes.Decimal, + }, + }, + ], + 1, + ); + pgClient.connect.mockImplementation(() => { + throw new Error('Connection error'); + }); + const result = await parseAndExecuteExecutor(text, inputTable); + + expect(R.isOk(result)).toEqual(false); + if (R.isErr(result)) { + expect(result.left.message).toEqual( + 'Could not write to postgres database: Connection error', + ); + expect(pgClient.connect).toBeCalledTimes(1); + expect(pgClient.query).toBeCalledTimes(0); + expect(pgClient.end).toBeCalledTimes(1); + } + }); +}); diff --git a/libs/extensions/rdbms/exec/test/assets/postgres-loader-executor/valid-postgres-loader.jv b/libs/extensions/rdbms/exec/test/assets/postgres-loader-executor/valid-postgres-loader.jv new file mode 100644 index 000000000..93b769bc9 --- /dev/null +++ b/libs/extensions/rdbms/exec/test/assets/postgres-loader-executor/valid-postgres-loader.jv @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +pipeline TestPipeline { + + block TestExtractor oftype TestFileExtractor { + } + + block TestBlock oftype PostgresLoader { + host: "localhost"; + port: 5432; + username: "postgres"; + password: "postgres"; + database: "TestDB"; + table: "Test"; + } + + TestExtractor -> TestBlock; +} diff --git a/libs/extensions/rdbms/exec/test/test-extension/TestBlockTypes.jv b/libs/extensions/rdbms/exec/test/test-extension/TestBlockTypes.jv new file mode 100644 index 000000000..e050777aa --- /dev/null +++ b/libs/extensions/rdbms/exec/test/test-extension/TestBlockTypes.jv @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +builtin blocktype TestTableExtractor { + input inPort oftype None; + output outPort oftype Table; +} From e6b224d3a84b4e06d21f2fbe868ed56ac88c3dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20M=C3=BCller?= Date: Mon, 20 Nov 2023 09:31:12 +0100 Subject: [PATCH 3/5] Fixed an jest ts type issue preventing jest.mock returns to use mockFn.mockRejectedValueOnce etc --- .../rdbms/exec/test/mocks/sqlite-loader-executor-mock.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/extensions/rdbms/exec/test/mocks/sqlite-loader-executor-mock.ts b/libs/extensions/rdbms/exec/test/mocks/sqlite-loader-executor-mock.ts index 888cce28b..566d379a7 100644 --- a/libs/extensions/rdbms/exec/test/mocks/sqlite-loader-executor-mock.ts +++ b/libs/extensions/rdbms/exec/test/mocks/sqlite-loader-executor-mock.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { BlockExecutorMock } from '@jvalue/jayvee-execution/test'; import * as sqlite3 from 'sqlite3'; -type MockedSqlite3Database = jest.Mocked>; +type MockedSqlite3Database = jest.Mocked; export class SQLiteLoaderExecutorMock implements BlockExecutorMock { private _sqliteClient: MockedSqlite3Database | undefined; @@ -26,7 +26,7 @@ export class SQLiteLoaderExecutorMock implements BlockExecutorMock { ) => void = defaultSQLiteMockRegistration, ) { // setup sqlite3 mock - this._sqliteClient = new sqlite3.Database('test'); + this._sqliteClient = new sqlite3.Database('test') as MockedSqlite3Database; registerMocks(this._sqliteClient); } restore() { From 0161f4a7a8c641bdebd2eea96f5ffb51aec78612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20M=C3=BCller?= Date: Mon, 20 Nov 2023 09:35:10 +0100 Subject: [PATCH 4/5] Added tests for SQLiteLoaderExecutor --- .../src/lib/sqlite-loader-executor.spec.ts | 200 ++++++++++++++++++ .../valid-sqlite-loader.jv | 16 ++ 2 files changed, 216 insertions(+) create mode 100644 libs/extensions/rdbms/exec/src/lib/sqlite-loader-executor.spec.ts create mode 100644 libs/extensions/rdbms/exec/test/assets/sqlite-loader-executor/valid-sqlite-loader.jv diff --git a/libs/extensions/rdbms/exec/src/lib/sqlite-loader-executor.spec.ts b/libs/extensions/rdbms/exec/src/lib/sqlite-loader-executor.spec.ts new file mode 100644 index 000000000..26613a35c --- /dev/null +++ b/libs/extensions/rdbms/exec/src/lib/sqlite-loader-executor.spec.ts @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import * as path from 'path'; + +import * as R from '@jvalue/jayvee-execution'; +import { + constructTable, + getTestExecutionContext, +} from '@jvalue/jayvee-execution/test'; +import { + BlockDefinition, + IOType, + PrimitiveValuetypes, + createJayveeServices, +} from '@jvalue/jayvee-language-server'; +import { + ParseHelperOptions, + expectNoParserAndLexerErrors, + loadTestExtensions, + parseHelper, + readJvTestAssetHelper, +} from '@jvalue/jayvee-language-server/test'; +import { AstNode, AstNodeLocator, LangiumDocument } from 'langium'; +import { NodeFileSystem } from 'langium/node'; +import * as sqlite3 from 'sqlite3'; + +import { SQLiteLoaderExecutor } from './sqlite-loader-executor'; + +type SqliteRunCallbackType = ( + result: sqlite3.RunResult, + err: Error | null, +) => void; +// eslint-disable-next-line no-var +var databaseMock: jest.Mock; +// eslint-disable-next-line no-var +var databaseRunMock: jest.Mock; +// eslint-disable-next-line no-var +var databaseCloseMock: jest.Mock; +jest.mock('sqlite3', () => { + databaseMock = jest.fn(); + databaseRunMock = jest.fn(); + databaseCloseMock = jest.fn(); + return { + Database: databaseMock, + }; +}); +function mockDatabaseDefault() { + const mockDB = { + close: databaseCloseMock, + run: databaseRunMock, + }; + databaseMock.mockImplementation(() => { + return mockDB; + }); +} + +describe('Validation of SQLiteLoaderExecutor', () => { + let parse: ( + input: string, + options?: ParseHelperOptions, + ) => Promise>; + + let locator: AstNodeLocator; + + const readJvTestAsset = readJvTestAssetHelper( + __dirname, + '../../test/assets/sqlite-loader-executor/', + ); + + async function parseAndExecuteExecutor( + input: string, + IOInput: R.Table, + ): Promise> { + const document = await parse(input, { validationChecks: 'all' }); + expectNoParserAndLexerErrors(document); + + const block = locator.getAstNode( + document.parseResult.value, + 'pipelines@0/blocks@1', + ) as BlockDefinition; + + return new SQLiteLoaderExecutor().doExecute( + IOInput, + getTestExecutionContext(locator, document, [block]), + ); + } + + beforeAll(async () => { + // Create language services + const services = createJayveeServices(NodeFileSystem).Jayvee; + await loadTestExtensions(services, [ + path.resolve(__dirname, '../../test/test-extension/TestBlockTypes.jv'), + ]); + locator = services.workspace.AstNodeLocator; + // Parse function for Jayvee (without validation) + parse = parseHelper(services); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should diagnose no error on valid loader config', async () => { + mockDatabaseDefault(); + databaseRunMock.mockImplementation( + (sql: string, callback: SqliteRunCallbackType) => { + callback( + { + lastID: 0, + changes: 0, + } as sqlite3.RunResult, + null, + ); + return this; + }, + ); + const text = readJvTestAsset('valid-sqlite-loader.jv'); + + const inputTable = constructTable( + [ + { + columnName: 'Column1', + column: { + values: ['value 1'], + valuetype: PrimitiveValuetypes.Text, + }, + }, + { + columnName: 'Column2', + column: { + values: [20.2], + valuetype: PrimitiveValuetypes.Decimal, + }, + }, + ], + 1, + ); + const result = await parseAndExecuteExecutor(text, inputTable); + + expect(R.isErr(result)).toEqual(false); + if (R.isOk(result)) { + expect(result.right.ioType).toEqual(IOType.NONE); + expect(databaseRunMock).toBeCalledTimes(3); + expect(databaseRunMock).nthCalledWith( + 1, + 'DROP TABLE IF EXISTS "Test";', + expect.any(Function), + ); + expect(databaseRunMock).nthCalledWith( + 2, + `CREATE TABLE IF NOT EXISTS "Test" ("Column1" text,"Column2" real);`, + expect.any(Function), + ); + expect(databaseRunMock).nthCalledWith( + 3, + `INSERT INTO "Test" ("Column1","Column2") VALUES ('value 1',20.2)`, + expect.any(Function), + ); + expect(databaseCloseMock).toBeCalledTimes(1); + } + }); + + it('should diagnose error on sqlite database open error', async () => { + databaseMock.mockImplementation(() => { + throw new Error('File not found'); + }); + const text = readJvTestAsset('valid-sqlite-loader.jv'); + + const inputTable = constructTable( + [ + { + columnName: 'Column1', + column: { + values: ['value 1'], + valuetype: PrimitiveValuetypes.Text, + }, + }, + { + columnName: 'Column2', + column: { + values: [20.2], + valuetype: PrimitiveValuetypes.Decimal, + }, + }, + ], + 1, + ); + const result = await parseAndExecuteExecutor(text, inputTable); + + expect(R.isOk(result)).toEqual(false); + if (R.isErr(result)) { + expect(result.left.message).toEqual( + 'Could not write to sqlite database: File not found', + ); + expect(databaseRunMock).toBeCalledTimes(0); + expect(databaseCloseMock).toBeCalledTimes(0); + } + }); +}); diff --git a/libs/extensions/rdbms/exec/test/assets/sqlite-loader-executor/valid-sqlite-loader.jv b/libs/extensions/rdbms/exec/test/assets/sqlite-loader-executor/valid-sqlite-loader.jv new file mode 100644 index 000000000..3b9354b53 --- /dev/null +++ b/libs/extensions/rdbms/exec/test/assets/sqlite-loader-executor/valid-sqlite-loader.jv @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +pipeline TestPipeline { + + block TestExtractor oftype TestFileExtractor { + } + + block TestBlock oftype SQLiteLoader { + table: "Test"; + file: "./test.db"; + } + + TestExtractor -> TestBlock; +} From 8206d8ff8d07f053f7fd3b68c3be04fea6f37b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20M=C3=BCller?= Date: Mon, 20 Nov 2023 09:36:25 +0100 Subject: [PATCH 5/5] Refactored to match SQLiteLoaderExecutor tests --- .../src/lib/postgres-loader-executor.spec.ts | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts b/libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts index 56785beb2..2b7c9a0c6 100644 --- a/libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts +++ b/libs/extensions/rdbms/exec/src/lib/postgres-loader-executor.spec.ts @@ -24,15 +24,23 @@ import { } from '@jvalue/jayvee-language-server/test'; import { AstNode, AstNodeLocator, LangiumDocument } from 'langium'; import { NodeFileSystem } from 'langium/node'; -import { Client } from 'pg'; import { PostgresLoaderExecutor } from './postgres-loader-executor'; +// eslint-disable-next-line no-var +var databaseConnectMock: jest.Mock; +// eslint-disable-next-line no-var +var databaseQueryMock: jest.Mock; +// eslint-disable-next-line no-var +var databaseEndMock: jest.Mock; jest.mock('pg', () => { + databaseConnectMock = jest.fn(); + databaseQueryMock = jest.fn(); + databaseEndMock = jest.fn(); const mClient = { - connect: jest.fn(), - query: jest.fn(), - end: jest.fn(), + connect: databaseConnectMock, + query: databaseQueryMock, + end: databaseEndMock, }; return { Client: jest.fn(() => mClient) }; }); @@ -44,7 +52,6 @@ describe('Validation of PostgresLoaderExecutor', () => { ) => Promise>; let locator: AstNodeLocator; - let pgClient: jest.Mocked; const readJvTestAsset = readJvTestAssetHelper( __dirname, @@ -79,9 +86,6 @@ describe('Validation of PostgresLoaderExecutor', () => { // Parse function for Jayvee (without validation) parse = parseHelper(services); }); - beforeEach(() => { - pgClient = new Client() as jest.Mocked; - }); afterEach(() => { jest.clearAllMocks(); }); @@ -113,17 +117,20 @@ describe('Validation of PostgresLoaderExecutor', () => { expect(R.isErr(result)).toEqual(false); if (R.isOk(result)) { expect(result.right.ioType).toEqual(IOType.NONE); - expect(pgClient.connect).toBeCalledTimes(1); - expect(pgClient.query).nthCalledWith(1, 'DROP TABLE IF EXISTS "Test";'); - expect(pgClient.query).nthCalledWith( + expect(databaseConnectMock).toBeCalledTimes(1); + expect(databaseQueryMock).nthCalledWith( + 1, + 'DROP TABLE IF EXISTS "Test";', + ); + expect(databaseQueryMock).nthCalledWith( 2, `CREATE TABLE IF NOT EXISTS "Test" ("Column1" text,"Column2" real);`, ); - expect(pgClient.query).nthCalledWith( + expect(databaseQueryMock).nthCalledWith( 3, `INSERT INTO "Test" ("Column1","Column2") VALUES ('value 1',20.2)`, ); - expect(pgClient.end).toBeCalledTimes(1); + expect(databaseEndMock).toBeCalledTimes(1); } }); @@ -149,7 +156,7 @@ describe('Validation of PostgresLoaderExecutor', () => { ], 1, ); - pgClient.connect.mockImplementation(() => { + databaseConnectMock.mockImplementation(() => { throw new Error('Connection error'); }); const result = await parseAndExecuteExecutor(text, inputTable); @@ -159,9 +166,9 @@ describe('Validation of PostgresLoaderExecutor', () => { expect(result.left.message).toEqual( 'Could not write to postgres database: Connection error', ); - expect(pgClient.connect).toBeCalledTimes(1); - expect(pgClient.query).toBeCalledTimes(0); - expect(pgClient.end).toBeCalledTimes(1); + expect(databaseConnectMock).toBeCalledTimes(1); + expect(databaseQueryMock).toBeCalledTimes(0); + expect(databaseEndMock).toBeCalledTimes(1); } }); });