diff --git a/apps/interpreter/src/index.ts b/apps/interpreter/src/index.ts index 5a7487b91..b5520eca5 100644 --- a/apps/interpreter/src/index.ts +++ b/apps/interpreter/src/index.ts @@ -57,6 +57,11 @@ program `Sets the target blocks of the of block debug logging, separated by comma. If not given, all blocks are targeted.`, undefined, ) + .option( + '-po, --parse-only', + 'Only parses the model without running it. Exits with 0 if the model is valid, with 1 otherwise.', + false, + ) .description('Run a Jayvee file') .action(runAction); diff --git a/apps/interpreter/src/parse-only.spec.ts b/apps/interpreter/src/parse-only.spec.ts new file mode 100644 index 000000000..eb5abe1b1 --- /dev/null +++ b/apps/interpreter/src/parse-only.spec.ts @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import * as fs from 'node:fs'; +import * as path from 'path'; +import * as process from 'process'; + +import { + clearBlockExecutorRegistry, + clearConstraintExecutorRegistry, +} from '@jvalue/jayvee-execution/test'; +import { + RunOptions, + interpretModel, + interpretString, +} from '@jvalue/jayvee-interpreter-lib'; + +import { runAction } from './run-action'; + +jest.mock('@jvalue/jayvee-interpreter-lib', () => { + const original: object = jest.requireActual('@jvalue/jayvee-interpreter-lib'); + return { + ...original, + interpretModel: jest.fn(), + interpretString: jest.fn(), + }; +}); + +describe('Parse Only', () => { + const pathToValidModel = path.resolve(__dirname, '../../../example/cars.jv'); + const pathToInvalidModel = path.resolve( + __dirname, + '../test/assets/broken-model.jv', + ); + + const defaultOptions: RunOptions = { + env: new Map(), + debug: false, + debugGranularity: 'minimal', + debugTarget: undefined, + }; + + afterEach(() => { + // Assert that model is not executed + expect(interpretString).not.toBeCalled(); + expect(interpretModel).not.toBeCalled(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(process, 'exit').mockImplementation(() => { + throw new Error(); + }); + + // Reset jayvee specific stuff + clearBlockExecutorRegistry(); + clearConstraintExecutorRegistry(); + }); + + it('should exit with 0 on a valid option', async () => { + await expect( + runAction(pathToValidModel, { + ...defaultOptions, + parseOnly: true, + }), + ).rejects.toBeDefined(); + + expect(process.exit).toBeCalledTimes(1); + expect(process.exit).toHaveBeenCalledWith(0); + }); + + it('should exit with 1 on error', async () => { + expect(fs.existsSync(pathToInvalidModel)).toBe(true); + + await expect( + runAction(pathToInvalidModel, { + ...defaultOptions, + parseOnly: true, + }), + ).rejects.toBeDefined(); + + expect(process.exit).toBeCalledTimes(1); + expect(process.exit).toHaveBeenCalledWith(1); + }); +}); diff --git a/apps/interpreter/src/run-action.ts b/apps/interpreter/src/run-action.ts index a0bfa6faf..2d9274f49 100644 --- a/apps/interpreter/src/run-action.ts +++ b/apps/interpreter/src/run-action.ts @@ -2,11 +2,14 @@ // // SPDX-License-Identifier: AGPL-3.0-only +import * as process from 'process'; + import { LoggerFactory, RunOptions, extractAstNodeFromFile, interpretModel, + parseModel, } from '@jvalue/jayvee-interpreter-lib'; import { JayveeModel, JayveeServices } from '@jvalue/jayvee-language-server'; @@ -23,6 +26,11 @@ export async function runAction( services, loggerFactory.createLogger(), ); + if (options.parseOnly === true) { + const { model, services } = await parseModel(extractAstNodeFn, options); + const exitCode = model != null && services != null ? 0 : 1; + process.exit(exitCode); + } const exitCode = await interpretModel(extractAstNodeFn, options); process.exit(exitCode); } diff --git a/apps/interpreter/test/assets/broken-model.jv b/apps/interpreter/test/assets/broken-model.jv new file mode 100644 index 000000000..447eab7c2 --- /dev/null +++ b/apps/interpreter/test/assets/broken-model.jv @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +pipeline CarsPipeline { + // Try using a CoolCarsExtractor although we only have normal cars. + // This fill result in an error during parsing. + CoolCarsExtractor -> CarsTextFileInterpreter; + + CarsTextFileInterpreter + -> CarsCSVInterpreter + -> NameHeaderWriter + -> CarsTableInterpreter + -> CarsLoader; + + block CarsExtractor oftype HttpExtractor { + url: "https://gist.githubusercontent.com/noamross/e5d3e859aa0c794be10b/raw/b999fb4425b54c63cab088c0ce2c0d6ce961a563/cars.csv"; + } + + block CarsTextFileInterpreter oftype TextFileInterpreter { } + + block CarsCSVInterpreter oftype CSVInterpreter { + enclosing: '"'; + } + + block NameHeaderWriter oftype CellWriter { + at: cell A1; + write: ["name"]; + } + + block CarsTableInterpreter oftype TableInterpreter { + header: true; + columns: [ + "name" oftype text, + "mpg" oftype decimal, + "cyl" oftype integer, + "disp" oftype decimal, + "hp" oftype integer, + "drat" oftype decimal, + "wt" oftype decimal, + "qsec" oftype decimal, + "vs" oftype integer, + "am" oftype integer, + "gear" oftype integer, + "carb" oftype integer + ]; + } + + block CarsLoader oftype SQLiteLoader { + table: "Cars"; + file: "./cars.sqlite"; + } + +} \ No newline at end of file diff --git a/libs/interpreter-lib/src/interpreter.ts b/libs/interpreter-lib/src/interpreter.ts index e63bac142..7fe7ac508 100644 --- a/libs/interpreter-lib/src/interpreter.ts +++ b/libs/interpreter-lib/src/interpreter.ts @@ -4,7 +4,9 @@ import { strict as assert } from 'assert'; +import * as R from '@jvalue/jayvee-execution'; import { + DebugGranularity, ExecutionContext, Logger, NONE, @@ -15,7 +17,6 @@ import { registerDefaultConstraintExecutors, useExtension as useExecutionExtension, } from '@jvalue/jayvee-execution'; -import * as R from '@jvalue/jayvee-execution'; import { StdExecExtension } from '@jvalue/jayvee-extensions/std/exec'; import { BlockDefinition, @@ -33,9 +34,9 @@ import { import * as chalk from 'chalk'; import { NodeFileSystem } from 'langium/node'; -import { LoggerFactory } from './logging/logger-factory'; +import { LoggerFactory } from './logging'; import { ExitCode, extractAstNodeFromString } from './parsing-util'; -import { validateRuntimeParameterLiteral } from './validation-checks/runtime-parameter-literal'; +import { validateRuntimeParameterLiteral } from './validation-checks'; interface InterpreterOptions { debugGranularity: R.DebugGranularity; @@ -48,6 +49,7 @@ export interface RunOptions { debug: boolean; debugGranularity: string; debugTarget: string | undefined; + parseOnly?: boolean; } export async function interpretString( @@ -66,13 +68,25 @@ export async function interpretString( return await interpretModel(extractAstNodeFn, options); } -export async function interpretModel( +/** + * Parses a model without executing it. + * Also sets up the environment so that the model can be properly executed. + * + * @returns non-null model, services and loggerFactory on success. + */ +export async function parseModel( extractAstNodeFn: ( services: JayveeServices, loggerFactory: LoggerFactory, ) => Promise, options: RunOptions, -): Promise { +): Promise<{ + model: JayveeModel | null; + loggerFactory: LoggerFactory; + services: JayveeServices | null; +}> { + let services: JayveeServices | null = null; + let model: JayveeModel | null = null; const loggerFactory = new LoggerFactory(options.debug); if (!isDebugGranularity(options.debugGranularity)) { loggerFactory @@ -83,23 +97,40 @@ export async function interpretModel( ', ', )}.`, ); - return ExitCode.FAILURE; + return { model, services, loggerFactory }; } useStdExtension(); registerDefaultConstraintExecutors(); - const services = createJayveeServices(NodeFileSystem).Jayvee; + services = createJayveeServices(NodeFileSystem).Jayvee; await initializeWorkspace(services); setupJayveeServices(services, options.env); - let model: JayveeModel; try { model = await extractAstNodeFn(services, loggerFactory); + return { model, services, loggerFactory }; } catch (e) { loggerFactory .createLogger() .logErr('Could not extract the AST node of the given model.'); + return { model, services, loggerFactory }; + } +} + +export async function interpretModel( + extractAstNodeFn: ( + services: JayveeServices, + loggerFactory: LoggerFactory, + ) => Promise, + options: RunOptions, +): Promise { + const { model, services, loggerFactory } = await parseModel( + extractAstNodeFn, + options, + ); + + if (model == null || services == null) { return ExitCode.FAILURE; } @@ -111,7 +142,8 @@ export async function interpretModel( loggerFactory, { debug: options.debug, - debugGranularity: options.debugGranularity, + // type of options.debugGranularity is asserted in parseModel + debugGranularity: options.debugGranularity as DebugGranularity, debugTargets: debugTargets, }, );