@finos/legend-application-studio
Version:
Legend Studio application core
459 lines (423 loc) • 14 kB
text/typescript
/**
* 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);
}
}