UNPKG

@finos/legend-studio

Version:
461 lines 22.8 kB
/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { LogEvent, hashObject, UnsupportedOperationError, guaranteeNonNullable, uuid, assertTrue, assertErrorThrown, tryToFormatJSONString, fromGrammarString, toGrammarString, createUrlStringFromData, losslessParse, losslessStringify, tryToMinifyLosslessJSONString, tryToFormatLosslessJSONString, tryToMinifyJSONString, ContentType, } from '@finos/legend-shared'; import { observable, flow, action, makeObservable, makeAutoObservable, flowResult, } from 'mobx'; import { createMockDataForMappingElementSource } from '../../../shared/MockDataUtil.js'; import { extractExecutionResultValues, GRAPH_MANAGER_EVENT, LAMBDA_PIPE, Class, ExpectedOutputMappingTestAssert, ObjectInputData, ObjectInputType, IdentifiedConnection, EngineRuntime, FlatDataInputData, JsonModelConnection, FlatDataConnection, RootFlatDataRecordType, PackageableElementExplicitReference, RelationalInputData, RelationalInputType, DatabaseType, RelationalDatabaseConnection, LocalH2DatasourceSpecification, DefaultH2AuthenticationStrategy, buildSourceInformationSourceId, TableAlias, isStubbed_RawLambda, stub_Class, generateIdentifiedConnectionId, DEPRECATED__validate_MappingTest, } from '@finos/legend-graph'; import { ExecutionPlanState, LambdaEditorState, TAB_SIZE, } from '@finos/legend-application'; import { flatData_setData } from '../../../graphModifier/StoreFlatData_GraphModifierHelper.js'; import { expectedOutputMappingTestAssert_setExpectedOutput, mappingTest_setAssert, mappingTest_setQuery, objectInputData_setData, runtime_addIdentifiedConnection, runtime_addMapping, } from '../../../graphModifier/DSLMapping_GraphModifierHelper.js'; import { localH2DatasourceSpecification_setTestDataSetupCsv, localH2DatasourceSpecification_setTestDataSetupSqls, relationalInputData_setData, } from '../../../graphModifier/StoreRelational_GraphModifierHelper.js'; export var TEST_RESULT; (function (TEST_RESULT) { TEST_RESULT["NONE"] = "NONE"; TEST_RESULT["ERROR"] = "ERROR"; TEST_RESULT["FAILED"] = "FAILED"; TEST_RESULT["PASSED"] = "PASSED"; })(TEST_RESULT = TEST_RESULT || (TEST_RESULT = {})); export class MappingTestQueryState extends LambdaEditorState { editorStore; test; isInitializingLambda = false; query; constructor(editorStore, test, query) { super('', LAMBDA_PIPE); makeObservable(this, { query: observable, isInitializingLambda: observable, setIsInitializingLambda: action, updateLamba: flow, }); this.test = test; this.editorStore = editorStore; this.query = query; } get lambdaId() { return buildSourceInformationSourceId([this.uuid]); } setIsInitializingLambda(val) { this.isInitializingLambda = val; } *updateLamba(val) { this.query = val; mappingTest_setQuery(this.test, val); yield flowResult(this.convertLambdaObjectToGrammarString(true)); } *convertLambdaObjectToGrammarString(pretty) { if (!isStubbed_RawLambda(this.query)) { try { const lambdas = new Map(); lambdas.set(this.lambdaId, this.query); const isolatedLambdas = (yield this.editorStore.graphManagerState.graphManager.lambdasToPureCode(lambdas, pretty)); const grammarText = isolatedLambdas.get(this.lambdaId); this.setLambdaString(grammarText !== undefined ? this.extractLambdaString(grammarText) : ''); this.clearErrors(); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), error); } } else { this.clearErrors(); this.setLambdaString(''); } } // NOTE: since we don't allow edition in text mode, we don't need to implement this *convertLambdaGrammarStringToObject() { throw new UnsupportedOperationError(); } } class MappingTestInputDataState { uuid = uuid(); editorStore; mapping; inputData; constructor(editorStore, mapping, inputData) { this.editorStore = editorStore; this.mapping = mapping; this.inputData = inputData; } } export class MappingTestObjectInputDataState extends MappingTestInputDataState { /** * @workaround https://github.com/finos/legend-studio/issues/68 */ data; constructor(editorStore, mapping, inputData) { super(editorStore, mapping, inputData); makeObservable(this, { data: observable, setData: action, }); /** * @workaround https://github.com/finos/legend-studio/issues/68 */ this.data = tryToFormatLosslessJSONString(inputData.data); } setData(val) { this.data = val; /** * @workaround https://github.com/finos/legend-studio/issues/68 */ objectInputData_setData(this.inputData, tryToMinifyLosslessJSONString(val)); } get runtime() { const engineConfig = this.editorStore.graphManagerState.graphManager.TEMPORARY__getEngineConfig(); const runtime = new EngineRuntime(); runtime_addMapping(runtime, PackageableElementExplicitReference.create(this.mapping)); const connection = new JsonModelConnection(PackageableElementExplicitReference.create(this.editorStore.graphManagerState.graph.modelStore), PackageableElementExplicitReference.create(this.inputData.sourceClass.value), createUrlStringFromData(this.inputData.data, ContentType.APPLICATION_JSON, engineConfig.useBase64ForAdhocConnectionDataUrls)); runtime_addIdentifiedConnection(runtime, new IdentifiedConnection(generateIdentifiedConnectionId(runtime), connection), this.editorStore.changeDetectionState.observerContext); return runtime; } } export class MappingTestFlatDataInputDataState extends MappingTestInputDataState { get runtime() { const engineConfig = this.editorStore.graphManagerState.graphManager.TEMPORARY__getEngineConfig(); const runtime = new EngineRuntime(); runtime_addMapping(runtime, PackageableElementExplicitReference.create(this.mapping)); const connection = new FlatDataConnection(PackageableElementExplicitReference.create(this.inputData.sourceFlatData.value), createUrlStringFromData(this.inputData.data, ContentType.TEXT_PLAIN, engineConfig.useBase64ForAdhocConnectionDataUrls)); runtime_addIdentifiedConnection(runtime, new IdentifiedConnection(generateIdentifiedConnectionId(runtime), connection), this.editorStore.changeDetectionState.observerContext); return runtime; } } export class MappingTestRelationalInputDataState extends MappingTestInputDataState { get runtime() { const datasourceSpecification = new LocalH2DatasourceSpecification(); switch (this.inputData.inputType) { case RelationalInputType.SQL: localH2DatasourceSpecification_setTestDataSetupSqls(datasourceSpecification, // NOTE: this is a gross simplification of handling the input for relational input data [this.inputData.data]); break; case RelationalInputType.CSV: localH2DatasourceSpecification_setTestDataSetupCsv(datasourceSpecification, this.inputData.data); break; default: throw new UnsupportedOperationError(`Invalid input data type`); } const runtime = new EngineRuntime(); runtime_addMapping(runtime, PackageableElementExplicitReference.create(this.mapping)); const connection = new RelationalDatabaseConnection(PackageableElementExplicitReference.create(this.inputData.database.value), DatabaseType.H2, datasourceSpecification, new DefaultH2AuthenticationStrategy()); runtime_addIdentifiedConnection(runtime, new IdentifiedConnection(generateIdentifiedConnectionId(runtime), connection), this.editorStore.changeDetectionState.observerContext); return runtime; } } class MappingTestAssertionState { uuid = uuid(); assert; constructor(assert) { this.assert = assert; } } export class MappingTestExpectedOutputAssertionState extends MappingTestAssertionState { /** * @workaround https://github.com/finos/legend-studio/issues/68 */ expectedResult; constructor(assert) { super(assert); makeObservable(this, { expectedResult: observable, setExpectedResult: action, }); this.expectedResult = fromGrammarString( /** * @workaround https://github.com/finos/legend-studio/issues/68 */ tryToFormatLosslessJSONString(assert.expectedOutput)); } setExpectedResult(val) { this.expectedResult = val; expectedOutputMappingTestAssert_setExpectedOutput(this.assert, /** * @workaround https://github.com/finos/legend-studio/issues/68 */ toGrammarString(tryToMinifyLosslessJSONString(this.expectedResult))); } } export var MAPPING_TEST_EDITOR_TAB_TYPE; (function (MAPPING_TEST_EDITOR_TAB_TYPE) { MAPPING_TEST_EDITOR_TAB_TYPE["SETUP"] = "Test Setup"; MAPPING_TEST_EDITOR_TAB_TYPE["RESULT"] = "Test Result"; })(MAPPING_TEST_EDITOR_TAB_TYPE = MAPPING_TEST_EDITOR_TAB_TYPE || (MAPPING_TEST_EDITOR_TAB_TYPE = {})); export class MappingTestState { uuid = uuid(); selectedTab = MAPPING_TEST_EDITOR_TAB_TYPE.SETUP; editorStore; mappingEditorState; result = TEST_RESULT.NONE; test; runTime = 0; isSkipped = false; errorRunningTest; testExecutionResultText; // NOTE: stored as lossless JSON object text isRunningTest = false; isExecutingTest = false; queryState; inputDataState; assertionState; isGeneratingPlan = false; executionPlanState; constructor(editorStore, test, mappingEditorState) { makeAutoObservable(this, { uuid: false, editorStore: false, mappingEditorState: false, executionPlanState: false, setSelectedTab: action, resetTestRunStatus: action, setResult: action, toggleSkipTest: action, setQueryState: action, setInputDataState: action, setAssertionState: action, setInputDataStateBasedOnSource: action, updateAssertion: action, generatePlan: flow, }); this.editorStore = editorStore; this.mappingEditorState = mappingEditorState; this.test = test; this.queryState = this.buildQueryState(); this.inputDataState = this.buildInputDataState(); this.assertionState = this.buildAssertionState(); this.executionPlanState = new ExecutionPlanState(this.editorStore.applicationStore, this.editorStore.graphManagerState); } setSelectedTab(val) { this.selectedTab = val; } buildQueryState() { const queryState = new MappingTestQueryState(this.editorStore, this.test, this.test.query); flowResult(queryState.updateLamba(this.test.query)).catch(this.editorStore.applicationStore.alertUnhandledError); return queryState; } buildInputDataState() { // NOTE: right now we only support one input data per test assertTrue(this.test.inputData.length > 0, 'Mapping test input data must contain at least one item'); const inputData = this.test.inputData[0]; if (inputData instanceof ObjectInputData) { return new MappingTestObjectInputDataState(this.editorStore, this.mappingEditorState.mapping, inputData); } else if (inputData instanceof FlatDataInputData) { return new MappingTestFlatDataInputDataState(this.editorStore, this.mappingEditorState.mapping, inputData); } else if (inputData instanceof RelationalInputData) { return new MappingTestRelationalInputDataState(this.editorStore, this.mappingEditorState.mapping, inputData); } throw new UnsupportedOperationError(`Can't build state for mapping test input data`, inputData); } buildAssertionState() { const testAssertion = this.test.assert; if (testAssertion instanceof ExpectedOutputMappingTestAssert) { return new MappingTestExpectedOutputAssertionState(testAssertion); } throw new UnsupportedOperationError(`Can't build state of mapping test assertion`, testAssertion); } resetTestRunStatus() { this.testExecutionResultText = undefined; this.runTime = 0; this.setResult(TEST_RESULT.NONE); } setResult(result) { this.result = result; } toggleSkipTest() { this.isSkipped = !this.isSkipped; } setQueryState = (queryState) => { this.queryState = queryState; }; setInputDataState = (inputDataState) => { this.inputDataState = inputDataState; }; setAssertionState = (assertionState) => { this.assertionState = assertionState; }; setInputDataStateBasedOnSource(source, populateWithMockData) { if (source === undefined || source instanceof Class) { // NOTE: By default use object input data if no source is provided const newInputDataState = new MappingTestObjectInputDataState(this.editorStore, this.mappingEditorState.mapping, new ObjectInputData(PackageableElementExplicitReference.create(source ?? stub_Class()), ObjectInputType.JSON, tryToMinifyJSONString('{}'))); if (populateWithMockData) { if (source) { objectInputData_setData(newInputDataState.inputData, createMockDataForMappingElementSource(source, this.editorStore)); } } this.setInputDataState(newInputDataState); } else if (source instanceof RootFlatDataRecordType) { const newInputDataState = new MappingTestFlatDataInputDataState(this.editorStore, this.mappingEditorState.mapping, new FlatDataInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(source._OWNER._OWNER)), '')); if (populateWithMockData) { flatData_setData(newInputDataState.inputData, createMockDataForMappingElementSource(source, this.editorStore)); } this.setInputDataState(newInputDataState); } else if (source instanceof TableAlias) { const newInputDataState = new MappingTestRelationalInputDataState(this.editorStore, this.mappingEditorState.mapping, new RelationalInputData(PackageableElementExplicitReference.create(source.relation.ownerReference.value), '', RelationalInputType.SQL)); if (populateWithMockData) { relationalInputData_setData(newInputDataState.inputData, createMockDataForMappingElementSource(source, this.editorStore)); } this.setInputDataState(newInputDataState); } else { this.editorStore.applicationStore.notifyWarning(new UnsupportedOperationError(`Can't build input data for source`, source)); } } /** * Execute mapping using current info in the test detail panel then set the execution result value as test expected result */ *regenerateExpectedResult() { if (DEPRECATED__validate_MappingTest(this.test)) { this.editorStore.applicationStore.notifyError(`Can't execute test '${this.test.name}'. Please make sure that the test query and input data are valid`); return; } else if (this.isExecutingTest) { this.editorStore.applicationStore.notifyWarning(`Can't execute test '${this.test.name}' while it is running`); return; } try { const query = this.queryState.query; const runtime = this.inputDataState.runtime; this.isExecutingTest = true; const result = (yield this.editorStore.graphManagerState.graphManager.executeMapping(query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, { useLosslessParse: true, })); if (this.assertionState instanceof MappingTestExpectedOutputAssertionState) { this.assertionState.setExpectedResult(losslessStringify(extractExecutionResultValues(result), undefined, TAB_SIZE)); this.updateAssertion(); } else { throw new UnsupportedOperationError(); } } catch (error) { assertErrorThrown(error); if (this.assertionState instanceof MappingTestExpectedOutputAssertionState) { this.assertionState.setExpectedResult(tryToFormatJSONString('{}')); this.updateAssertion(); } else { throw new UnsupportedOperationError(); } this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } finally { this.isExecutingTest = false; } } *runTest() { if (DEPRECATED__validate_MappingTest(this.test)) { this.editorStore.applicationStore.notifyError(`Can't run test '${this.test.name}'. Please make sure that the test is valid`); return; } else if (this.isExecutingTest) { this.editorStore.applicationStore.notifyWarning(`Test '${this.test.name}' is already running`); return; } const startTime = Date.now(); try { const runtime = this.inputDataState.runtime; this.isRunningTest = true; const result = (yield this.editorStore.graphManagerState.graphManager.executeMapping(this.test.query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, { useLosslessParse: true, })); this.testExecutionResultText = losslessStringify(extractExecutionResultValues(result), undefined, TAB_SIZE); let assertionMatched = false; if (this.assertionState instanceof MappingTestExpectedOutputAssertionState) { // TODO: this logic should probably be better handled in by engine mapping test runner assertionMatched = hashObject(extractExecutionResultValues(result)) === hashObject(losslessParse(this.assertionState.expectedResult)); } else { throw new UnsupportedOperationError(); } this.setResult(assertionMatched ? TEST_RESULT.PASSED : TEST_RESULT.FAILED); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); this.errorRunningTest = error; this.setResult(TEST_RESULT.ERROR); } finally { this.isRunningTest = false; this.runTime = Date.now() - startTime; // if the test is currently opened and ran but did not pass, switch to the result tab if ([TEST_RESULT.FAILED, TEST_RESULT.ERROR].includes(this.result) && this.mappingEditorState.currentTabState === this) { this.setSelectedTab(MAPPING_TEST_EDITOR_TAB_TYPE.RESULT); } } } *onTestStateOpen(openTab) { try { // extract test basic info out into state this.queryState = this.buildQueryState(); this.inputDataState = this.buildInputDataState(); this.assertionState = this.buildAssertionState(); // if the test has result, open the test result tab if (openTab) { this.setSelectedTab(openTab); } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error.message); yield flowResult(this.editorStore.graphState.globalCompileInFormMode()); // recompile graph if there is problem with the deep fetch tree of a test } } updateAssertion() { mappingTest_setAssert(this.test, this.assertionState.assert, this.editorStore.changeDetectionState.observerContext); } *generatePlan(debug) { try { this.isGeneratingPlan = true; let rawPlan; if (debug) { const debugResult = (yield this.editorStore.graphManagerState.graphManager.debugExecutionPlanGeneration(this.queryState.query, this.mappingEditorState.mapping, this.inputDataState.runtime, this.editorStore.graphManagerState.graph)); rawPlan = debugResult.plan; this.executionPlanState.setDebugText(debugResult.debug); } else { rawPlan = (yield this.editorStore.graphManagerState.graphManager.generateExecutionPlan(this.queryState.query, this.mappingEditorState.mapping, this.inputDataState.runtime, this.editorStore.graphManagerState.graph)); } try { this.executionPlanState.setRawPlan(rawPlan); const plan = this.editorStore.graphManagerState.graphManager.buildExecutionPlan(rawPlan, this.editorStore.graphManagerState.graph); this.executionPlanState.setPlan(plan); } catch { // do nothing } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } finally { this.isGeneratingPlan = false; } } } //# sourceMappingURL=MappingTestState.js.map