diff --git a/apps/docs/docs/dev/12-jayvee-testing.md b/apps/docs/docs/dev/12-jayvee-testing.md index 1121c1ef9..1a5bada75 100644 --- a/apps/docs/docs/dev/12-jayvee-testing.md +++ b/apps/docs/docs/dev/12-jayvee-testing.md @@ -20,7 +20,7 @@ These kind of tests are mainly located inside the [language-server](https://gith The testing utils are located inside the `language-server` in a dedicated [test folder](https://github.com/jvalue/jayvee/tree/main/libs/language-server/src/test). These utils can be imported using `@jvalue/jayvee-language-server/test` and contain the following parts: -[**langium-utils.ts**](https://github.com/jvalue/jayvee/blob/main/libs/language-server/src/test/langium-utils.ts): +[**langium-utils.ts**](https://github.com/jvalue/jayvee/blob/main/libs/language-server/src/test/langium-utils.ts): This utils file contains two functions: - `parseHelper` to simplify parsing the input (content of a *.jv file) and returning the corresponding `LangiumDocument`, and - `validationHelper` parse and validate the created document. @@ -100,34 +100,23 @@ pipeline Pipeline { ### Existing tests Currently there are already tests for the following parts: - Language-server validation checks (located [here](https://github.com/jvalue/jayvee/tree/main/libs/language-server/src/lib/validation)) -- Language-server constraint validation (located [here](https://github.com/jvalue/jayvee/tree/dev/libs/language-server/src/lib/constraint)) -- Custom block (property) validation of the three existing extensions (std extension located [here](https://github.com/jvalue/jayvee/blob/dev/libs/extensions/std/lang/src)) -- Grammar validation tests for all official full examples from the [/example](https://github.com/jvalue/jayvee/tree/main/example) folder (located [here](https://github.com/jvalue/jayvee/blob/dev/libs/extensions/std/lang/src/example-validation.spec.ts)) -- Grammar validation tests for all block examples of the std extension (located [here](https://github.com/jvalue/jayvee/blob/dev/libs/extensions/std/lang/src/meta-inf-example-validation.spec.ts)) +- Language-server constraint validation (located [here](https://github.com/jvalue/jayvee/tree/main/libs/language-server/src/lib/constraint)) +- Custom block (property) validation of the three existing extensions (std extension located [here](https://github.com/jvalue/jayvee/blob/main/libs/extensions/std/lang/src)) +- Grammar validation tests for all official full examples from the [/example](https://github.com/jvalue/jayvee/tree/main/example) folder (located [here](https://github.com/jvalue/jayvee/blob/main/libs/extensions/std/lang/src/example-validation.spec.ts)) +- Grammar validation tests for all block examples of the std extension (located [here](https://github.com/jvalue/jayvee/blob/main/libs/extensions/std/lang/src/meta-inf-example-validation.spec.ts)) ## Execution tests -These kind of tests are mainly located inside the [interpreter](https://github.com/jvalue/jayvee/tree/main/libs/language-server), the [interpreter-lib](https://github.com/jvalue/jayvee/tree/dev/libs/interpreter-lib), the [execution lib](https://github.com/jvalue/jayvee/tree/dev/libs/execution) as well as the execution parts of each extension (for example [std/exec](https://github.com/jvalue/jayvee/tree/main/libs/extensions/std/exec)). +These kind of tests are mainly located inside the [interpreter](https://github.com/jvalue/jayvee/tree/main/libs/language-server), the [interpreter-lib](https://github.com/jvalue/jayvee/tree/main/libs/interpreter-lib), the [execution lib](https://github.com/jvalue/jayvee/tree/main/libs/execution) as well as the execution parts of each extension (for example [std/exec](https://github.com/jvalue/jayvee/tree/main/libs/extensions/std/exec)). ### Testing utils -The testing utils for execution tests are spread between the extensions, with the interfaces and base utils located inside the [execution lib](https://github.com/jvalue/jayvee/tree/dev/libs/execution). +The testing utils for execution tests are spread between the extensions, with the interfaces and base utils located inside the [execution lib](https://github.com/jvalue/jayvee/tree/main/libs/execution). They can be imported using `@jvalue/jayvee-extensions/rdbms/test`, `@jvalue/jayvee-extensions/std/test` and `@jvalue/jayvee-execution/test`. -[**utils.ts**](https://github.com/jvalue/jayvee/blob/dev/libs/execution/test/utils.ts): -At the moment this only contains two functions: -- `clearBlockExecutorRegistry` for clearing the registry containing all `BlockExecutor`s, and -- `clearConstraintExecutorRegistry` clearing the corresponding `ConstraintExecutor`s registry. -They are required in case the tested method initializes Jayvee itself (see [smoke test](#existing-tests-1)). - -[**test-logger.ts**](https://github.com/jvalue/jayvee/blob/dev/libs/execution/test/test-logger.ts): -This contains a subclass of the [`DefaultLogger`](https://github.com/jvalue/jayvee/blob/dev/libs/execution/src/lib/logging/default-logger.ts) used for tests which require a `Logger` implementation. The `TestLogger` contains the following tests functionality: -- `getLogs`: retrieve the cached logs that the logger received. -- `clearLogs`: clear the cached logs. - -[**block-executor-mocks.ts**](https://github.com/jvalue/jayvee/blob/dev/libs/execution/test/block-executor-mock.ts): +[**block-executor-mocks.ts**](https://github.com/jvalue/jayvee/blob/main/libs/execution/test/block-executor-mock.ts): `BlockExecutorMock` interface for defining mocks for `AbstractBlockExecutor`. Generally only loader and executor blocks require mocks, because they interact with "the outside world" (i.e. `HttpExtractor` making http calls). Due to how vastly different each `BlockExecutor` can be, this interface is very simple, containing only a `setup(...args: unknown[])` and a `restore()` method. See below for existing implementations. -[**rdbms/exec/test**](https://github.com/jvalue/jayvee/tree/dev/libs/extensions/rdbms/exec/test): +[**rdbms/exec/test**](https://github.com/jvalue/jayvee/tree/main/libs/extensions/rdbms/exec/test): Contains the implementation of `BlockExecutorMock` for `PostgresLoaderExecutor` and `SQLiteLoaderExecutor`. Both of these executors are mocked using `jest.mock` to mock the corresponding libraries (`pg` and `sqlite3`) **Usage:** @@ -187,7 +176,7 @@ describe('Dummy describe', () => { }); ``` -[**std/exec/test/mocks**](https://github.com/jvalue/jayvee/tree/dev/libs/extensions/std/exec/test): +[**std/exec/test/mocks**](https://github.com/jvalue/jayvee/tree/main/libs/extensions/std/exec/test): Contains the implementation of `BlockExecutorMock` for `HttpExtractorExecutorMock`. This implementation uses [nock](https://www.npmjs.com/package/nock) for mocking HTTP(S) responses. The `setup` method is further specified requiring one parameter `registerMocks: () => Array`, which returns all used `nock.Scope` (i.e. the return value of `nock('')`), see usage below: @@ -251,4 +240,4 @@ describe('Dummy describe', () => { ### Existing tests Currently there are already tests for the following parts: -- Smoke test for official examples (located [here](https://github.com/jvalue/jayvee/blob/dev/apps/interpreter/src/examples-smoke-test.spec.ts)) +- Smoke test for official examples (located [here](https://github.com/jvalue/jayvee/blob/main/apps/interpreter/src/examples-smoke-test.spec.ts)) diff --git a/apps/interpreter/src/examples-smoke-test.spec.ts b/apps/interpreter/src/examples-smoke-test.spec.ts index 0deeadc63..43532847a 100644 --- a/apps/interpreter/src/examples-smoke-test.spec.ts +++ b/apps/interpreter/src/examples-smoke-test.spec.ts @@ -4,10 +4,7 @@ import * as path from 'path'; -import { - clearConstraintExecutorRegistry, - processExitMockImplementation, -} from '@jvalue/jayvee-execution/test'; +import { processExitMockImplementation } from '@jvalue/jayvee-execution/test'; import { PostgresLoaderExecutorMock, SQLiteLoaderExecutorMock, @@ -71,9 +68,6 @@ describe('jv example smoke tests', () => { httpExtractorMock.restore(); postgresLoaderMock.restore(); sqliteLoaderMock.restore(); - - // Clear registries - clearConstraintExecutorRegistry(); }); it('should have no errors when executing cars.jv example', async () => { diff --git a/apps/interpreter/src/parse-only.spec.ts b/apps/interpreter/src/parse-only.spec.ts index 6293736b6..57a6dfbb3 100644 --- a/apps/interpreter/src/parse-only.spec.ts +++ b/apps/interpreter/src/parse-only.spec.ts @@ -6,7 +6,6 @@ import * as fs from 'node:fs'; import * as path from 'path'; import * as process from 'process'; -import { clearConstraintExecutorRegistry } from '@jvalue/jayvee-execution/test'; import { RunOptions, interpretModel, @@ -49,9 +48,6 @@ describe('Parse Only', () => { jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error(); }); - - // Reset jayvee specific stuff - clearConstraintExecutorRegistry(); }); it('should exit with 0 on a valid option', async () => { diff --git a/libs/execution/src/lib/constraints/constraint-executor-extension.ts b/libs/execution/src/lib/constraints/constraint-executor-extension.ts new file mode 100644 index 000000000..aab4d44cc --- /dev/null +++ b/libs/execution/src/lib/constraints/constraint-executor-extension.ts @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { strict as assert } from 'assert'; + +import { + ConstraintDefinition, + Registry, + isExpressionConstraintDefinition, + isTypedConstraintDefinition, +} from '@jvalue/jayvee-language-server'; +import { assertUnreachable } from 'langium'; + +import { ConstraintExecutor } from './constraint-executor'; +import { AllowlistConstraintExecutor } from './executors/allowlist-constraint-executor'; +import { DenylistConstraintExecutor } from './executors/denylist-constraint-executor'; +import { ExpressionConstraintExecutor } from './executors/expression-constraint-executor'; +import { LengthConstraintExecutor } from './executors/length-constraint-executor'; +import { RangeConstraintExecutor } from './executors/range-constraint-executor'; +import { RegexConstraintExecutor } from './executors/regex-constraint-executor'; +import { TypedConstraintExecutorClass } from './typed-constraint-executor-class'; + +export interface JayveeConstraintExtension { + registerConstraintExecutor(executorClass: TypedConstraintExecutorClass): void; + + getConstraintExecutors(): TypedConstraintExecutorClass[]; + + createConstraintExecutor( + constraint: ConstraintDefinition, + ): ConstraintExecutor; +} + +export class DefaultConstraintExtension + extends Registry + implements JayveeConstraintExtension +{ + constructor() { + super(); + + this.registerConstraintExecutor(AllowlistConstraintExecutor); + this.registerConstraintExecutor(DenylistConstraintExecutor); + this.registerConstraintExecutor(RegexConstraintExecutor); + this.registerConstraintExecutor(LengthConstraintExecutor); + this.registerConstraintExecutor(RangeConstraintExecutor); + } + + registerConstraintExecutor(executorClass: TypedConstraintExecutorClass) { + this.register(executorClass.type, executorClass); + } + + getConstraintExecutors() { + return this.getAll(); + } + + createConstraintExecutor( + constraint: ConstraintDefinition, + ): ConstraintExecutor { + if (isTypedConstraintDefinition(constraint)) { + const constraintType = constraint.type.ref?.name; + assert( + constraintType !== undefined, + `Could not resolve reference to constraint type of ${constraint.name}`, + ); + const constraintExecutor = this.get(constraintType); + assert( + constraintExecutor !== undefined, + `No executor was registered for constraint type ${constraintType}`, + ); + + return new constraintExecutor(); + } else if (isExpressionConstraintDefinition(constraint)) { + return new ExpressionConstraintExecutor(constraint); + } + assertUnreachable(constraint); + } +} diff --git a/libs/execution/src/lib/constraints/constraint-executor-registry.ts b/libs/execution/src/lib/constraints/constraint-executor-registry.ts deleted file mode 100644 index b8c9a1ea7..000000000 --- a/libs/execution/src/lib/constraints/constraint-executor-registry.ts +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg -// -// SPDX-License-Identifier: AGPL-3.0-only - -import { strict as assert } from 'assert'; - -import { - ConstraintDefinition, - Registry, - isExpressionConstraintDefinition, - isTypedConstraintDefinition, -} from '@jvalue/jayvee-language-server'; -import { assertUnreachable } from 'langium'; - -import { ConstraintExecutor } from './constraint-executor'; -import { AllowlistConstraintExecutor } from './executors/allowlist-constraint-executor'; -import { DenylistConstraintExecutor } from './executors/denylist-constraint-executor'; -import { ExpressionConstraintExecutor } from './executors/expression-constraint-executor'; -import { LengthConstraintExecutor } from './executors/length-constraint-executor'; -import { RangeConstraintExecutor } from './executors/range-constraint-executor'; -import { RegexConstraintExecutor } from './executors/regex-constraint-executor'; -import { TypedConstraintExecutorClass } from './typed-constraint-executor-class'; - -export const constraintExecutorRegistry = - new Registry(); - -export function registerDefaultConstraintExecutors() { - registerConstraintExecutor(AllowlistConstraintExecutor); - registerConstraintExecutor(DenylistConstraintExecutor); - registerConstraintExecutor(RegexConstraintExecutor); - registerConstraintExecutor(LengthConstraintExecutor); - registerConstraintExecutor(RangeConstraintExecutor); -} - -export function registerConstraintExecutor( - executorClass: TypedConstraintExecutorClass, -) { - constraintExecutorRegistry.register(executorClass.type, executorClass); -} - -export function getRegisteredConstraintExecutors(): TypedConstraintExecutorClass[] { - return constraintExecutorRegistry.getAll(); -} - -export function createConstraintExecutor( - constraint: ConstraintDefinition, -): ConstraintExecutor { - if (isTypedConstraintDefinition(constraint)) { - const constraintType = constraint.type.ref?.name; - assert( - constraintType !== undefined, - `Could not resolve reference to constraint type of ${constraint.name}`, - ); - const constraintExecutor = constraintExecutorRegistry.get(constraintType); - assert( - constraintExecutor !== undefined, - `No executor was registered for constraint type ${constraintType}`, - ); - - return new constraintExecutor(); - } else if (isExpressionConstraintDefinition(constraint)) { - return new ExpressionConstraintExecutor(constraint); - } - assertUnreachable(constraint); -} diff --git a/libs/execution/src/lib/constraints/default-constraint-executors.spec.ts b/libs/execution/src/lib/constraints/default-constraint-executors-extension.spec.ts similarity index 66% rename from libs/execution/src/lib/constraints/default-constraint-executors.spec.ts rename to libs/execution/src/lib/constraints/default-constraint-executors-extension.spec.ts index fd3e00494..78fff87b9 100644 --- a/libs/execution/src/lib/constraints/default-constraint-executors.spec.ts +++ b/libs/execution/src/lib/constraints/default-constraint-executors-extension.spec.ts @@ -9,26 +9,22 @@ import { } from '@jvalue/jayvee-language-server'; import { NodeFileSystem } from 'langium/node'; -import { - getRegisteredConstraintExecutors, - registerDefaultConstraintExecutors, -} from './constraint-executor-registry'; +import { DefaultConstraintExtension } from './constraint-executor-extension'; -describe('default constraint executors', () => { +describe('default constraint extension', () => { it('should include executors for all constraint types', async () => { // Create language services const services = createJayveeServices(NodeFileSystem).Jayvee; await initializeWorkspace(services); - registerDefaultConstraintExecutors(); + const defaultConstraintExtension = new DefaultConstraintExtension(); getAllBuiltinConstraintTypes( services.shared.workspace.LangiumDocuments, ).forEach((constraintType) => { - const matchingConstraintExecutorClass = - getRegisteredConstraintExecutors().find( - (c) => c.type === constraintType.type, - ); + const matchingConstraintExecutorClass = defaultConstraintExtension + .getConstraintExecutors() + .find((c) => c.type === constraintType.type); expect(matchingConstraintExecutorClass).toBeDefined(); }); diff --git a/libs/execution/src/lib/constraints/index.ts b/libs/execution/src/lib/constraints/index.ts index 4a4cf0d53..35f9ce9a3 100644 --- a/libs/execution/src/lib/constraints/index.ts +++ b/libs/execution/src/lib/constraints/index.ts @@ -2,4 +2,4 @@ // // SPDX-License-Identifier: AGPL-3.0-only -export * from './constraint-executor-registry'; +export * from './constraint-executor-extension'; diff --git a/libs/execution/src/lib/execution-context.ts b/libs/execution/src/lib/execution-context.ts index fe5412992..7c072ea36 100644 --- a/libs/execution/src/lib/execution-context.ts +++ b/libs/execution/src/lib/execution-context.ts @@ -25,6 +25,7 @@ import { } from '@jvalue/jayvee-language-server'; import { assertUnreachable, isReference } from 'langium'; +import { JayveeConstraintExtension } from './constraints'; import { DebugGranularity, DebugTargets, @@ -43,6 +44,7 @@ export class ExecutionContext { constructor( public readonly pipeline: PipelineDefinition, public readonly executionExtension: JayveeExecExtension, + public readonly constraintExtension: JayveeConstraintExtension, public readonly logger: Logger, public readonly runOptions: { isDebugMode: boolean; diff --git a/libs/execution/src/lib/types/valuetypes/value-representation-validity.ts b/libs/execution/src/lib/types/valuetypes/value-representation-validity.ts index 3835308a4..4642d7e83 100644 --- a/libs/execution/src/lib/types/valuetypes/value-representation-validity.ts +++ b/libs/execution/src/lib/types/valuetypes/value-representation-validity.ts @@ -22,7 +22,6 @@ import { ValuetypeVisitor, } from '@jvalue/jayvee-language-server'; -import { createConstraintExecutor } from '../../constraints/constraint-executor-registry'; import { type ExecutionContext } from '../../execution-context'; export function isValidValueRepresentation( @@ -53,7 +52,8 @@ class ValueRepresentationValidityVisitor extends ValuetypeVisitor { this.context.evaluationContext, ); for (const constraint of constraints) { - const constraintExecutor = createConstraintExecutor(constraint); + const constraintExecutor = + this.context.constraintExtension.createConstraintExecutor(constraint); this.context.enterNode(constraint); const valueFulfilledConstraint = constraintExecutor.isValid( diff --git a/libs/execution/test/utils/test-infrastructure-util.ts b/libs/execution/test/utils/test-infrastructure-util.ts index d0fa2e62c..3896db24a 100644 --- a/libs/execution/test/utils/test-infrastructure-util.ts +++ b/libs/execution/test/utils/test-infrastructure-util.ts @@ -14,12 +14,12 @@ import { CachedLogger, DebugGranularity, DebugTargets, + DefaultConstraintExtension, ExecutionContext, JayveeExecExtension, StackNode, Table, TableColumn, - constraintExecutorRegistry, } from '../../src'; export class TestExecExtension extends JayveeExecExtension { @@ -28,10 +28,6 @@ export class TestExecExtension extends JayveeExecExtension { } } -export function clearConstraintExecutorRegistry() { - constraintExecutorRegistry.clear(); -} - export function processExitMockImplementation(code?: number) { if (code === undefined || code === 0) { return undefined as never; @@ -62,6 +58,7 @@ export function getTestExecutionContext( const executionContext = new ExecutionContext( pipeline, new TestExecExtension(), + new DefaultConstraintExtension(), new CachedLogger(runOptions.isDebugMode, undefined, loggerPrintLogs), runOptions, new EvaluationContext(new RuntimeParameterProvider()), diff --git a/libs/interpreter-lib/src/interpreter.ts b/libs/interpreter-lib/src/interpreter.ts index f47b1556e..b2ee632bf 100644 --- a/libs/interpreter-lib/src/interpreter.ts +++ b/libs/interpreter-lib/src/interpreter.ts @@ -7,14 +7,15 @@ import { strict as assert } from 'assert'; import * as R from '@jvalue/jayvee-execution'; import { DebugGranularity, + DefaultConstraintExtension, ExecutionContext, + JayveeConstraintExtension, JayveeExecExtension, Logger, executeBlocks, isDebugGranularity, logExecutionDuration, parseValueToInternalRepresentation, - registerDefaultConstraintExecutors, } from '@jvalue/jayvee-execution'; import { StdExecExtension } from '@jvalue/jayvee-extensions/std/exec'; import { @@ -98,8 +99,6 @@ export async function parseModel( return { model, services, loggerFactory }; } - registerDefaultConstraintExecutors(); - services = createJayveeServices(NodeFileSystem).Jayvee; await initializeWorkspace(services); setupJayveeServices(services, options.env); @@ -136,6 +135,7 @@ export async function interpretModel( const interpretationExitCode = await interpretJayveeModel( model, new StdExecExtension(), + new DefaultConstraintExtension(), services.RuntimeParameterProvider, loggerFactory, { @@ -176,6 +176,7 @@ function setupRuntimeParameterProvider( async function interpretJayveeModel( model: JayveeModel, executionExtension: JayveeExecExtension, + constraintExtension: JayveeConstraintExtension, runtimeParameterProvider: RuntimeParameterProvider, loggerFactory: LoggerFactory, runOptions: InterpreterOptions, @@ -184,6 +185,7 @@ async function interpretJayveeModel( return runPipeline( pipeline, executionExtension, + constraintExtension, runtimeParameterProvider, loggerFactory, runOptions, @@ -200,6 +202,7 @@ async function interpretJayveeModel( async function runPipeline( pipeline: PipelineDefinition, executionExtension: JayveeExecExtension, + constraintExtension: JayveeConstraintExtension, runtimeParameterProvider: RuntimeParameterProvider, loggerFactory: LoggerFactory, runOptions: InterpreterOptions, @@ -207,6 +210,7 @@ async function runPipeline( const executionContext = new ExecutionContext( pipeline, executionExtension, + constraintExtension, loggerFactory.createLogger(), { isDebugMode: runOptions.debug, diff --git a/libs/language-server/src/lib/util/registry.ts b/libs/language-server/src/lib/util/registry.ts index d06571860..7ba731af6 100644 --- a/libs/language-server/src/lib/util/registry.ts +++ b/libs/language-server/src/lib/util/registry.ts @@ -5,9 +5,9 @@ import { strict as assert } from 'assert'; export class Registry { - private readonly registry = new Map(); + protected readonly registry = new Map(); - register(key: string, classToRegister: C) { + protected register(key: string, classToRegister: C) { assert( !this.registry.has(key), `Multiple keys "${key}" were registered, expected at most one register call per key`, @@ -15,21 +15,21 @@ export class Registry { this.registry.set(key, classToRegister); } - getAll(): C[] { + protected getAll(): C[] { return [...this.registry.values()]; } - getAllEntries(): { key: string; value: C }[] { + protected getAllEntries(): { key: string; value: C }[] { return [...this.registry.entries()].map(([k, v]) => { return { key: k, value: v }; }); } - get(key: string): C | undefined { + protected get(key: string): C | undefined { return this.registry.get(key); } - clear() { + protected clear() { this.registry.clear(); } }