@finos/legend-studio
Version:
461 lines • 22.8 kB
JavaScript
/**
* 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