UNPKG

@finos/legend-application-studio

Version:
459 lines (423 loc) 14 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 { type TestAssertion, type AssertionStatus, type ValueSpecification, AssertFail, type TestResult, TestExecuted, TestError, EqualToJson, ExternalFormatData, EqualToJsonAssertFail, MultiExecutionServiceTestResult, AssertPass, TestExecutionStatus, EqualTo, observe_ValueSpecification, } from '@finos/legend-graph'; import { type GeneratorFn, ActionState, assertErrorThrown, ContentType, UnsupportedOperationError, assertTrue, isNonNullable, IllegalStateError, guaranteeNonNullable, guaranteeType, returnUndefOnError, type PlainObject, } from '@finos/legend-shared'; import { action, flow, flowResult, makeObservable, observable } from 'mobx'; import type { EditorStore } from '../../../EditorStore.js'; import { externalFormatData_setData } from '../../../../graph-modifier/DSL_Data_GraphModifierHelper.js'; import { getTestableResultFromAssertionStatus, TESTABLE_RESULT, } from '../../../sidebar-state/testable/GlobalTestRunnerState.js'; import type { TestableTestEditorState } from './TestableEditorState.js'; import { isTestPassing } from '../../../utils/TestableUtils.js'; import { equalTo_setExpected } from '../../../../graph-modifier/Testable_GraphModifierHelper.js'; export enum TEST_ASSERTION_TAB { EXPECTED = 'EXPECTED', RESULT = 'RESULT', } export abstract class TestAssertionStatusState { resultState: TestAssertionResultState; status: AssertionStatus; constructor(resultState: TestAssertionResultState, status: AssertionStatus) { this.resultState = resultState; this.status = status; } } export class AssertFailState extends TestAssertionStatusState { declare status: AssertFail; constructor(resultState: TestAssertionResultState, status: AssertFail) { super(resultState, status); this.status = status; makeObservable(this, { status: observable, }); } } export class EqualToJsonAssertFailState extends AssertFailState { declare status: EqualToJsonAssertFail; diffModal = false; constructor( resultState: TestAssertionResultState, status: EqualToJsonAssertFail, ) { super(resultState, status); this.status = status; makeObservable(this, { diffModal: observable, setDiffModal: action, }); } setDiffModal(val: boolean): void { this.diffModal = val; } } export class UnsupportedAssertionStatusState extends TestAssertionStatusState {} export class TestAssertionResultState { testResult: TestResult | undefined; statusState: | TestAssertionStatusState | Map<string, TestAssertionResultState> | undefined; readonly editorStore: EditorStore; readonly assertionState: TestAssertionEditorState; constructor( editorStore: EditorStore, assertionState: TestAssertionEditorState, ) { makeObservable(this, { testResult: observable, setTestResult: action, statusState: observable, }); this.editorStore = editorStore; this.assertionState = assertionState; } setTestResult(val: TestResult | undefined): void { this.testResult = val; this.statusState = undefined; if (val instanceof TestExecuted) { const status = val.assertStatuses.find( (_status) => _status.assertion === this.assertionState.assertion, ); this.statusState = this.buildStatus(status); } else if (val instanceof MultiExecutionServiceTestResult) { const statusMap = new Map<string, TestAssertionResultState>(); Array.from(val.keyIndexedTestResults.entries()).forEach((keyedResult) => { const resultState = new TestAssertionResultState( this.editorStore, this.assertionState, ); resultState.setTestResult(keyedResult[1]); statusMap.set(keyedResult[0], resultState); }); this.statusState = statusMap; } } buildStatus( val: AssertionStatus | undefined, ): TestAssertionStatusState | undefined { if (val) { if (val instanceof EqualToJsonAssertFail) { return new EqualToJsonAssertFailState(this, val); } if (val instanceof AssertFail) { return new AssertFailState(this, val); } return new UnsupportedAssertionStatusState(this, val); } return undefined; } get result(): TESTABLE_RESULT { if (this.assertionState.testState.runningTestAction.isInProgress) { return TESTABLE_RESULT.IN_PROGRESS; } if (this.testResult instanceof TestError) { return TESTABLE_RESULT.ERROR; } else if ( this.testResult instanceof TestExecuted && this.testResult.testExecutionStatus === TestExecutionStatus.PASS ) { return TESTABLE_RESULT.PASSED; } else if ( this.testResult instanceof TestExecuted && this.testResult.testExecutionStatus === TestExecutionStatus.FAIL && this.statusState instanceof TestAssertionStatusState ) { return getTestableResultFromAssertionStatus(this.statusState.status); } else if (this.testResult instanceof MultiExecutionServiceTestResult) { const passed = Array.from( this.testResult.keyIndexedTestResults.entries(), ).every((keyResult) => { const result = keyResult[1]; if ( result instanceof TestExecuted && result.testExecutionStatus === TestExecutionStatus.PASS ) { return true; } if ( result instanceof TestExecuted && result.testExecutionStatus === TestExecutionStatus.FAIL ) { const status = result.assertStatuses.find( (_status) => _status.assertion === this.assertionState.assertion, ); if (status instanceof AssertPass) { return true; } } return false; }); if (passed) { return TESTABLE_RESULT.PASSED; } const assertionErrors = Array.from( this.testResult.keyIndexedTestResults.values(), ).find((t) => t instanceof TestError); if (assertionErrors) { return TESTABLE_RESULT.ERROR; } return TESTABLE_RESULT.FAILED; } return TESTABLE_RESULT.DID_NOT_RUN; } } export abstract class TestAssertionState { readonly editorStore: EditorStore; assertion: TestAssertion; result: TestAssertionResultState; constructor( editorStore: EditorStore, assertionState: TestAssertionEditorState, ) { this.editorStore = editorStore; this.assertion = assertionState.assertion; this.result = new TestAssertionResultState(editorStore, assertionState); } abstract generateExpected(status: AssertFail): boolean; abstract generateBare(): TestAssertion; abstract label(): string; abstract get supportsGeneratingAssertion(): boolean; } export class EqualToJsonAssertionState extends TestAssertionState { declare assertion: EqualToJson; setExpectedValue(val: string): void { externalFormatData_setData(this.assertion.expected, val); } override get supportsGeneratingAssertion(): boolean { return true; } generateExpected(status: AssertFail): boolean { if (status instanceof EqualToJsonAssertFail) { const expected = status.actual; this.setExpectedValue(expected); return true; } return false; } generateBare(): TestAssertion { const bareAssertion = new EqualToJson(); bareAssertion.expected = new ExternalFormatData(); bareAssertion.expected.contentType = ContentType.APPLICATION_JSON; bareAssertion.expected.data = ''; return bareAssertion; } label(): string { return 'EqualToJSON'; } } export class EqualToAssertionState extends TestAssertionState { declare assertion: EqualTo; valueSpec: ValueSpecification; constructor( editorStore: EditorStore, assertionState: TestAssertionEditorState, valueSpec: ValueSpecification, ) { super(editorStore, assertionState); makeObservable(this, { valueSpec: observable, assertion: observable, updateValueSpec: action, }); this.valueSpec = observe_ValueSpecification( valueSpec, this.editorStore.changeDetectionState.observerContext, ); } updateValueSpec(val: ValueSpecification): void { this.valueSpec = observe_ValueSpecification( val, this.editorStore.changeDetectionState.observerContext, ); const object = this.editorStore.graphManagerState.graphManager.serializeValueSpecification( val, ); equalTo_setExpected(this.assertion, object); } override generateExpected(status: AssertFail): boolean { throw new Error('Method not implemented.'); } override generateBare(): TestAssertion { const equal = new EqualTo(); equal.expected = {}; return equal; } override label(): string { return 'Equal To'; } override get supportsGeneratingAssertion(): boolean { return false; } } export class UnsupportedAssertionState extends TestAssertionState { override get supportsGeneratingAssertion(): boolean { return false; } generateBare(): TestAssertion { throw new UnsupportedOperationError(); } generateExpected(status: AssertFail): boolean { return false; } label(): string { return 'Unsupported'; } } export class TestAssertionEditorState { readonly editorStore: EditorStore; readonly testState: TestableTestEditorState; assertionState: TestAssertionState; assertionResultState: TestAssertionResultState; assertion: TestAssertion; selectedTab = TEST_ASSERTION_TAB.EXPECTED; generatingExpectedAction = ActionState.create(); constructor( editorStore: EditorStore, assertion: TestAssertion, testState: TestableTestEditorState, ) { makeObservable(this, { selectedTab: observable, assertionResultState: observable, setSelectedTab: action, generateExpected: flow, }); this.editorStore = editorStore; this.assertion = assertion; this.testState = testState; this.assertionState = this.buildAssertionState(assertion); this.assertionResultState = new TestAssertionResultState(editorStore, this); } setSelectedTab(val: TEST_ASSERTION_TAB): void { this.selectedTab = val; } *generateExpected(): GeneratorFn<void> { try { assertTrue( this.assertionState.supportsGeneratingAssertion, 'Assertion does not support generation', ); this.generatingExpectedAction.inProgress(); const result = (yield flowResult( this.testState.fetchTestResult(), )) as TestResult; let testExecuted: TestExecuted; if (result instanceof TestExecuted) { testExecuted = result; } else if (result instanceof MultiExecutionServiceTestResult) { testExecuted = guaranteeNonNullable( Array.from(result.keyIndexedTestResults.values()) .map((testResult) => { if (testResult instanceof TestExecuted) { return testResult; } else if (testResult instanceof TestError) { throw new IllegalStateError(testResult.error); } return undefined; }) .filter(isNonNullable)[0], 'Unable to derive expected result from test result', ); } else { throw new UnsupportedOperationError( 'Unable to derive expected result from test result', ); } // if test is passing, update UI and return // if test errors report error if (isTestPassing(testExecuted)) { this.testState.handleTestResult(testExecuted); return; } else if (testExecuted instanceof TestError) { throw new IllegalStateError(testExecuted.error); } const assertionStatus = testExecuted.assertStatuses.find( (aStatus) => aStatus.assertion.id === this.assertion.id && aStatus instanceof AssertFail, ); const assertFail = guaranteeType( assertionStatus, AssertFail, 'Unable to derive expected result from test result', ); const generated = this.assertionState.generateExpected(assertFail); if (generated) { this.setSelectedTab(TEST_ASSERTION_TAB.EXPECTED); } this.editorStore.applicationStore.notificationService.notifySuccess( `Expected results generated!`, ); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.notificationService.notifyError( `Error generating expected result, please check data input: ${error.message}.`, ); this.setSelectedTab(TEST_ASSERTION_TAB.EXPECTED); this.generatingExpectedAction.fail(); } finally { this.generatingExpectedAction.complete(); } } buildAssertionState(assertion: TestAssertion): TestAssertionState { if (assertion instanceof EqualToJson) { return new EqualToJsonAssertionState(this.editorStore, this); } else if (assertion instanceof EqualTo) { const val = returnUndefOnError(() => this.editorStore.graphManagerState.graphManager.buildValueSpecification( assertion.expected as PlainObject<ValueSpecification>, this.editorStore.graphManagerState.graph, ), ); if (val) { return new EqualToAssertionState(this.editorStore, this, val); } } return new UnsupportedAssertionState(this.editorStore, this); } }