Skip to content

Commit

Permalink
Add unit tests for the upgrade function
Browse files Browse the repository at this point in the history
  • Loading branch information
kaklakariada committed Aug 10, 2023
1 parent ac4c5bc commit bb6b0f4
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 24 deletions.
76 changes: 76 additions & 0 deletions extension/src/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ExaScriptsRow } from '@exasol/extension-manager-interface';
import { describe, expect, it } from '@jest/globals';
import { AdapterScript } from './adapterScript';
import { InstalledScripts, failureResult, successResult, validateInstalledScripts, validateVersions } from './common';

function script({ schema = "schema", name = "name", inputType, resultType = "EMITS", type = "UDF", text = "", comment }: Partial<ExaScriptsRow>): ExaScriptsRow {
return { schema, name, inputType, resultType, type, text, comment }
}

describe("common", () => {
describe("validateInstalledScripts()", () => {
const importPath = script({ name: "IMPORT_PATH" })
const importMetadata = script({ name: "IMPORT_METADATA" })
const importFiles = script({ name: "IMPORT_FILES" })
const exportPath = script({ name: "EXPORT_PATH" })
const exportTable = script({ name: "EXPORT_TABLE" })
it("all scripts available", () => {
const result = validateInstalledScripts([importPath, importMetadata, importFiles, exportPath, exportTable]);
expect(result).toStrictEqual(successResult({
importPath: new AdapterScript(importPath),
importMetadata: new AdapterScript(importMetadata),
importFiles: new AdapterScript(importFiles),
exportPath: new AdapterScript(exportPath),
exportTable: new AdapterScript(exportTable)
}))
})

describe("scripts missing", () => {
const tests: { name: string; scripts: ExaScriptsRow[], expectedMessage: string }[] = [
{ name: "all scripts missing", scripts: [], expectedMessage: "Validation failed: Script 'IMPORT_PATH' is missing, Script 'IMPORT_METADATA' is missing, Script 'IMPORT_FILES' is missing, Script 'EXPORT_PATH' is missing, Script 'EXPORT_TABLE' is missing" },
{ name: "importPath missing", scripts: [importMetadata, importFiles, exportPath, exportTable], expectedMessage: "Validation failed: Script 'IMPORT_PATH' is missing" },
{ name: "importMetadata missing", scripts: [importPath, importFiles, exportPath, exportTable], expectedMessage: "Validation failed: Script 'IMPORT_METADATA' is missing" },
{ name: "importFiles missing", scripts: [importPath, importMetadata, exportPath, exportTable], expectedMessage: "Validation failed: Script 'IMPORT_FILES' is missing" },
{ name: "exportPath missing", scripts: [importPath, importMetadata, importFiles, exportTable], expectedMessage: "Validation failed: Script 'EXPORT_PATH' is missing" },
{ name: "exportTable missing", scripts: [importPath, importMetadata, importFiles, exportPath], expectedMessage: "Validation failed: Script 'EXPORT_TABLE' is missing" },
]
tests.forEach(test => it(test.name, () => {
expect(validateInstalledScripts(test.scripts)).toStrictEqual(failureResult(test.expectedMessage))
}));
})

function adapterScript(version: string | undefined): AdapterScript {
return new AdapterScript(script({ text: `%jar /buckets/bfsdefault/default/exasol-cloud-storage-extension-${version ?? 'unknown-version'}.jar;` }))
}

function scriptVersions(importPathVersion: string | undefined,
importMetadataVersion: string | undefined, importFilesVersion: string | undefined,
exportPathVersion: string | undefined, exportTableVersion: string | undefined): InstalledScripts {
return {
importPath: adapterScript(importPathVersion),
importMetadata: adapterScript(importMetadataVersion),
importFiles: adapterScript(importFilesVersion),
exportPath: adapterScript(exportPathVersion),
exportTable: adapterScript(exportTableVersion)
}
}

describe("validateVersions()", () => {
const version = "1.2.3"
it("all scripts have same version", () => {
expect(validateVersions(scriptVersions(version, version, version, version, version))).toStrictEqual(successResult(version))
})
describe("not all scripts have same version", () => {
const tests: { name: string; scripts: InstalledScripts, expectedMessage: string }[] = [
{ name: "unknown version", scripts: scriptVersions(undefined, undefined, undefined, undefined, undefined), expectedMessage: "Failed to get version from script 'importPath' with text '%jar /buckets/bfsdefault/default/exasol-cloud-storage-extension-unknown-version.jar;'" },
{ name: "one missing version", scripts: scriptVersions(version, version, version, version, undefined), expectedMessage: "Failed to get version from script 'exportTable' with text '%jar /buckets/bfsdefault/default/exasol-cloud-storage-extension-unknown-version.jar;'" },
{ name: "two different versions", scripts: scriptVersions(version, version, version, version, "1.2.4"), expectedMessage: "Not all scripts use the same version. Found versions: '1.2.3, 1.2.4'" },
{ name: "five different versions", scripts: scriptVersions("1.0.1", "1.0.2", "1.0.3", "1.0.4", "1.0.5"), expectedMessage: "Not all scripts use the same version. Found versions: '1.0.1, 1.0.2, 1.0.3, 1.0.4, 1.0.5'" },
]
tests.forEach(test => it(test.name, () => {
expect(validateVersions(test.scripts)).toStrictEqual(failureResult(test.expectedMessage))
}))
})
})
})
})
90 changes: 73 additions & 17 deletions extension/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { Context, ExaScriptsRow, InternalServerError } from "@exasol/extension-manager-interface";
import { AdapterScript } from "./adapterScript";


/** Definition of an Exasol `SCRIPT` with all information required for creating it in the database. */
export interface ScriptDefinition {
name: string
type: "SET" | "SCALAR"
args: string
scriptClass: string
}

export interface InstalledScripts {
importPath: AdapterScript
importMetadata: AdapterScript
importFiles: AdapterScript
exportPath: AdapterScript
exportTable: AdapterScript
}

/** Script definitions for the required scripts. */
export const SCRIPTS: { [key: string]: ScriptDefinition } = {
importPath: {
name: "IMPORT_PATH",
Expand Down Expand Up @@ -50,8 +43,7 @@ export const SCRIPTS: { [key: string]: ScriptDefinition } = {
}
}



/** The same information from {@link SCRIPTS} but in form of a list. */
export function getAllScripts(): ScriptDefinition[] {
return [
SCRIPTS.importPath,
Expand All @@ -62,6 +54,43 @@ export function getAllScripts(): ScriptDefinition[] {
];
}

/** All installed scripts required for this extension. */
export interface InstalledScripts {
importPath: AdapterScript
importMetadata: AdapterScript
importFiles: AdapterScript
exportPath: AdapterScript
exportTable: AdapterScript
}

/** Successful result of a function. */
export type SuccessResult<T> = { type: "success", result: T }
/** Failure result of a function. */
export type FailureResult = { type: "failure", message: string }

/**
* Represents a result of an operation that can be successful or a failure.
* In case of success it contains the result, in case of error it contains an error message.
*/
export type Result<T> = SuccessResult<T> | FailureResult

/**
* Create a new {@link SuccessResult}.
* @param result the result value
* @returns a new {@link SuccessResult}
*/
export function successResult<T>(result: T): SuccessResult<T> {
return { type: "success", result }
}

/**
* Create a new {@link FailureResult}.
* @param message error message
* @returns a new {@link FailureResult}
*/
export function failureResult(message: string): FailureResult {
return { type: "failure", message }
}

function createMap(scripts: ExaScriptsRow[]): Map<string, AdapterScript> {
const map = new Map<string, AdapterScript>();
Expand All @@ -78,15 +107,19 @@ function validateScript(expectedScript: ScriptDefinition, actualScript: AdapterS
return []
}

export function validateInstalledScripts(scriptRows: ExaScriptsRow[]): InstalledScripts | undefined {
/**
* Validate that all required scripts are installed.
* @param scriptRows list of all installed scripts
* @returns a successful or a failed {@link Result} with all installed scripts
*/
export function validateInstalledScripts(scriptRows: ExaScriptsRow[]): Result<InstalledScripts> {
const scripts = createMap(scriptRows)
const allScripts = getAllScripts();
const validationErrors = allScripts.map(script => validateScript(script, scripts.get(script.name)))
.flatMap(finding => finding);
const metadataScript = scripts.get(SCRIPTS.importMetadata.name);
if (metadataScript == undefined || validationErrors.length > 0) {
console.log("Validation failed:", validationErrors)
return undefined
return failureResult(`Validation failed: ${validationErrors.join(', ')}`)
}
function getScript(scriptDefinition: ScriptDefinition): AdapterScript {
const script = scripts.get(scriptDefinition.name)
Expand All @@ -100,16 +133,40 @@ export function validateInstalledScripts(scriptRows: ExaScriptsRow[]): Installed
const importFiles: AdapterScript = getScript(SCRIPTS.importPath)
const exportPath: AdapterScript = getScript(SCRIPTS.exportPath)
const exportTable: AdapterScript = getScript(SCRIPTS.exportTable)
return {
return successResult({
importPath,
importMetadata,
importFiles,
exportPath,
exportTable
}
})
}

/**
* Verify that all scripts have the same version.
* @param scripts installed scripts
* @returns a failure {@link Result} if not all scripts have the same version, else a successful {@link Result} with the common version.
*/
export function validateVersions(scripts: InstalledScripts): Result<string> {
let key: keyof InstalledScripts;
const versionSet = new Set<string>()
for (key in scripts) {
const version = scripts[key].getVersion()
if (version) {
versionSet.add(version)
} else {
return failureResult(`Failed to get version from script '${key}' with text '${scripts[key].text}'`)
}
}
const versions: string[] = []
versionSet.forEach(value => versions.push(value))
if (versions.length === 1) {
return successResult(versions[0])
}
return failureResult(`Not all scripts use the same version. Found versions: '${versions.join(', ')}'`)
}

/** Information about the current extension version. */
export interface ExtensionInfo {
version: string;
fileName: string;
Expand All @@ -119,7 +176,6 @@ export type ExtendedContext = Context & {
qualifiedName(name: string): string
}


export function extendContext(context: Context): ExtendedContext {
return {
...context,
Expand Down
7 changes: 4 additions & 3 deletions extension/src/findInstallations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { ExaScriptsRow, Installation } from "@exasol/extension-manager-interface
import { validateInstalledScripts } from "./common";

export function findInstallations(scriptRows: ExaScriptsRow[]): Installation[] {
const scripts = validateInstalledScripts(scriptRows)
if (scripts) {
const result = validateInstalledScripts(scriptRows)
if (result.type === "success") {
return [{
name: "Cloud Storage Extension",
version: scripts.importMetadata.getVersion() ?? "(unknown)"
version: result.result.importMetadata.getVersion() ?? "(unknown)"
}];
} else {
console.warn(result.message)
return [];
}
}
11 changes: 10 additions & 1 deletion extension/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ export type ContextMock = Context & {
sqlExecute: jestMock.Mock<(query: string, ...args: any) => void>,
sqlQuery: jestMock.Mock<(query: string, ...args: any) => QueryResult>
getScriptByName: jestMock.Mock<(scriptName: string) => ExaScriptsRow | null>
simulateScripts: (scripts: ExaScriptsRow[]) => void
}
}

export function createMockContext(): ContextMock {
const mockedScripts: Map<string, ExaScriptsRow> = new Map()
const execute = jestMock.fn<(query: string, ...args: any) => void>().mockName("sqlClient.execute()")
const query = jestMock.fn<(query: string, ...args: any) => QueryResult>().mockName("sqlClient.query()")
const getScriptByName = jestMock.fn<(scriptName: string) => ExaScriptsRow | null>().mockName("metadata.getScriptByName()")

getScriptByName.mockImplementation((scriptName) => mockedScripts.get(scriptName) || null)
const sqlClient: SqlClient = {
execute: execute,
query: query
Expand All @@ -44,6 +46,10 @@ export function createMockContext(): ContextMock {
sqlExecute: execute,
sqlQuery: query,
getScriptByName: getScriptByName,
simulateScripts(scripts: ExaScriptsRow[]) {
mockedScripts.clear()
scripts.forEach(script => mockedScripts.set(script.name, script))
},
}
}
}
Expand All @@ -57,3 +63,6 @@ export function adapterScript({ name = "S3_FILES_ADAPTER", type = "ADAPTER", tex
export function importScript({ name = "IMPORT_FROM_S3_DOCUMENT_FILES", type = "UDF", inputType = "SET", resultType = "EMITS" }: Partial<ExaScriptsRow>): ExaScriptsRow {
return script({ name, type, inputType, resultType })
}
export function scriptWithVersion(name: string, version: string): ExaScriptsRow {
return script({ name, text: `CREATE ... %jar /path/to/exasol-cloud-storage-extension-${version}.jar; more text` })
}
50 changes: 50 additions & 0 deletions extension/src/upgrade.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ExaScriptsRow, PreconditionFailedError } from '@exasol/extension-manager-interface';
import { describe, expect, it } from '@jest/globals';
import { createExtension } from './extension';
import { EXTENSION_DESCRIPTION } from './extension-description';
import { createMockContext, scriptWithVersion } from './test-utils';


const currentVersion = EXTENSION_DESCRIPTION.version
describe("upgrade()", () => {
const version = "1.2.3"
const importPath = scriptWithVersion("IMPORT_PATH", version)
const importMetadata = scriptWithVersion("IMPORT_METADATA", version)
const importFiles = scriptWithVersion("IMPORT_FILES", version)
const exportPath = scriptWithVersion("EXPORT_PATH", version)
const exportTable = scriptWithVersion("EXPORT_TABLE", version)
const allScripts = [importPath, importMetadata, importFiles, exportPath, exportTable]

describe("validateInstalledScripts()", () => {
it("success", () => {
const context = createMockContext()
context.mocks.simulateScripts(allScripts)
expect(createExtension().upgrade(context)).toStrictEqual({
previousVersion: version, newVersion: currentVersion
})
const executeCalls = context.mocks.sqlExecute.mock.calls
expect(executeCalls.length).toBe(10)
})
describe("failure", () => {
const tests: { name: string; scripts: ExaScriptsRow[], expectedMessage: string }[] = [
{ name: "no script", scripts: [], expectedMessage: "Not all required scripts are installed: Validation failed: Script 'IMPORT_PATH' is missing, Script 'IMPORT_METADATA' is missing, Script 'IMPORT_FILES' is missing, Script 'EXPORT_PATH' is missing, Script 'EXPORT_TABLE' is missing" },
{ name: "one missing script", scripts: [importPath, importMetadata, importFiles, exportPath], expectedMessage: "Not all required scripts are installed: Validation failed: Script 'EXPORT_TABLE' is missing" },
{ name: "inconsistent versions", scripts: [importPath, importMetadata, importFiles, exportPath, scriptWithVersion("EXPORT_TABLE", "1.2.4")], expectedMessage: "Installed script use inconsistent versions: Not all scripts use the same version. Found versions: '1.2.3, 1.2.4'" },
{
name: "version already up-to-date", scripts: [
scriptWithVersion("IMPORT_PATH", currentVersion), scriptWithVersion("IMPORT_METADATA", currentVersion),
scriptWithVersion("IMPORT_FILES", currentVersion), scriptWithVersion("EXPORT_PATH", currentVersion), scriptWithVersion("EXPORT_TABLE", currentVersion)
],
expectedMessage: "Current version 2.7.3 already installed"
},
]
tests.forEach(test => it(test.name, () => {
const context = createMockContext()
context.mocks.simulateScripts(test.scripts)
expect(() => createExtension().upgrade(context)).toThrowError(new PreconditionFailedError(test.expectedMessage))
const executeCalls = context.mocks.sqlExecute.mock.calls
expect(executeCalls.length).toBe(0)
}))
})
})
})
26 changes: 23 additions & 3 deletions extension/src/upgrade.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import {
PreconditionFailedError,
UpgradeResult
} from "@exasol/extension-manager-interface";
import { ExtendedContext, ExtensionInfo } from "./common";
import { ExtendedContext, ExtensionInfo, SCRIPTS, validateInstalledScripts, validateVersions } from "./common";
import { installExtension } from "./install";


function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}

export function upgrade(context: ExtendedContext, extensionInfo: ExtensionInfo): UpgradeResult {
const previousVersion = "getAdapterVersion(extensionInfo, scripts)"

const scriptList = Object.entries(SCRIPTS).map(([_key, value]) => value.name)
.map(scriptName => context.metadata.getScriptByName(scriptName))
.filter(notEmpty);

const installedScripts = validateInstalledScripts(scriptList)
if (installedScripts.type === "failure") {
throw new PreconditionFailedError(`Not all required scripts are installed: ${installedScripts.message}`)
}
const previousVersion = validateVersions(installedScripts.result)
if (previousVersion.type === "failure") {
throw new PreconditionFailedError(`Installed script use inconsistent versions: ${previousVersion.message}`)
}
const newVersion = extensionInfo.version
if (previousVersion.result === newVersion) {
throw new PreconditionFailedError(`Current version ${newVersion} already installed`)
}
installExtension(context, extensionInfo, newVersion)
return { previousVersion, newVersion };
return { previousVersion: previousVersion.result, newVersion };
}

0 comments on commit bb6b0f4

Please sign in to comment.