-
+
diff --git a/packages/legend-application-studio/src/components/editor/editor-group/service-editor/__tests__/ServiceEditorTest.test.tsx b/packages/legend-application-studio/src/components/editor/editor-group/service-editor/__tests__/ServiceEditorTest.test.tsx
index c899b3cc75..8e792caaac 100644
--- a/packages/legend-application-studio/src/components/editor/editor-group/service-editor/__tests__/ServiceEditorTest.test.tsx
+++ b/packages/legend-application-studio/src/components/editor/editor-group/service-editor/__tests__/ServiceEditorTest.test.tsx
@@ -41,7 +41,11 @@ import { ServiceEditorState } from '../../../../../stores/editor/editor-state/el
import { LegendStudioPluginManager } from '../../../../../application/LegendStudioPluginManager.js';
import { QueryBuilder_GraphManagerPreset } from '@finos/legend-query-builder';
import { guaranteeNonNullable } from '@finos/legend-shared';
-import { Core_GraphManagerPreset } from '@finos/legend-graph';
+import {
+ Core_GraphManagerPreset,
+ resolveServiceQueryRawLambda,
+} from '@finos/legend-graph';
+import { getContentTypeWithParamFromQuery } from '../../../../../stores/editor/utils/TestableUtils.js';
test(integrationTest('Service Multi Execution Editor'), async () => {
const MOCK__editorStore = TEST__provideMockedEditorStore();
@@ -148,9 +152,15 @@ test(
),
);
- const contentTypeParmPairs = MOCK__editorStore.tabManagerState
- .getCurrentEditorState(ServiceEditorState)
- .testableState.selectedSuiteState?.testStates[0]?.setupState.getContentTypeWithParamFromQuery();
+ const contentTypeParmPairs = getContentTypeWithParamFromQuery(
+ resolveServiceQueryRawLambda(
+ MOCK__editorStore.tabManagerState.getCurrentEditorState(
+ ServiceEditorState,
+ ).service,
+ ),
+ MOCK__editorStore,
+ );
+
expect(contentTypeParmPairs).toHaveLength(2);
const firstPair = guaranteeNonNullable(
guaranteeNonNullable(contentTypeParmPairs)[0],
@@ -223,9 +233,15 @@ test(
),
);
fireEvent.click(getByText(serviceTestEditor, 'Setup'));
- const contentTypeParmPairsForByte = MOCK__editorStore.tabManagerState
- .getCurrentEditorState(ServiceEditorState)
- .testableState.selectedSuiteState?.testStates[0]?.setupState.getContentTypeWithParamFromQuery();
+ const contentTypeParmPairsForByte = getContentTypeWithParamFromQuery(
+ resolveServiceQueryRawLambda(
+ MOCK__editorStore.tabManagerState.getCurrentEditorState(
+ ServiceEditorState,
+ ).service,
+ ),
+ MOCK__editorStore,
+ );
+
expect(contentTypeParmPairsForByte).toHaveLength(1);
expect(
guaranteeNonNullable(guaranteeNonNullable(contentTypeParmPairsForByte)[0])
@@ -284,9 +300,15 @@ test(
),
);
fireEvent.click(getByText(serviceTestEditor, 'Setup'));
- const contentTypeParmPairsForByte = MOCK__editorStore.tabManagerState
- .getCurrentEditorState(ServiceEditorState)
- .testableState.selectedSuiteState?.testStates[0]?.setupState.getContentTypeWithParamFromQuery();
+ const contentTypeParmPairsForByte = getContentTypeWithParamFromQuery(
+ resolveServiceQueryRawLambda(
+ MOCK__editorStore.tabManagerState.getCurrentEditorState(
+ ServiceEditorState,
+ ).service,
+ ),
+ MOCK__editorStore,
+ );
+
expect(contentTypeParmPairsForByte).toHaveLength(1);
expect(
guaranteeNonNullable(guaranteeNonNullable(contentTypeParmPairsForByte)[0])
diff --git a/packages/legend-application-studio/src/components/editor/editor-group/service-editor/testable/ServiceTestsEditor.tsx b/packages/legend-application-studio/src/components/editor/editor-group/service-editor/testable/ServiceTestsEditor.tsx
index 8cb5ac7111..1bd7852c30 100644
--- a/packages/legend-application-studio/src/components/editor/editor-group/service-editor/testable/ServiceTestsEditor.tsx
+++ b/packages/legend-application-studio/src/components/editor/editor-group/service-editor/testable/ServiceTestsEditor.tsx
@@ -35,11 +35,6 @@ import {
RefreshIcon,
TimesIcon,
FilledWindowMaximizeIcon,
- Modal,
- ModalHeader,
- ModalFooter,
- ModalBody,
- ModalTitle,
PlayIcon,
} from '@finos/legend-art';
import {
@@ -47,13 +42,13 @@ import {
PrimitiveInstanceValue,
PrimitiveType,
PureMultiExecution,
+ resolveServiceQueryRawLambda,
} from '@finos/legend-graph';
import {
BasicValueSpecificationEditor,
instanceValue_setValue,
} from '@finos/legend-query-builder';
import {
- ContentType,
filterByType,
guaranteeNonNullable,
prettyCONSTName,
@@ -79,15 +74,16 @@ import {
} from '../../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js';
import { getTestableResultIcon } from '../../../side-bar/testable/GlobalTestRunner.js';
import {
+ ExternalFormatParameterEditorModal,
RenameModal,
TestAssertionEditor,
TestAssertionItem,
} from '../../testable/TestableSharedComponents.js';
-import {
- CODE_EDITOR_LANGUAGE,
- CodeEditor,
-} from '@finos/legend-lego/code-editor';
import { LEGEND_STUDIO_TEST_ID } from '../../../../../__lib__/LegendStudioTesting.js';
+import {
+ getContentTypeWithParamFromQuery,
+ type TestParamContentType,
+} from '../../../../../stores/editor/utils/TestableUtils.js';
export const NewParameterModal = observer(
(props: { setupState: ServiceTestSetupState; isReadOnly: boolean }) => {
@@ -140,86 +136,12 @@ export const NewParameterModal = observer(
},
);
-export const ExternalFormatParameterEditorModal = observer(
- (props: {
- paramState: ServiceValueSpecificationTestParameterState;
- isReadOnly: boolean;
- onClose: () => void;
- updateParamValue: (val: string) => void;
- contentTypeParamPair: {
- contentType: string;
- param: string;
- };
- }) => {
- const {
- paramState,
- isReadOnly,
- onClose,
- updateParamValue,
- contentTypeParamPair,
- } = props;
- const paramValue =
- paramState.varExpression.genericType?.value.rawType === PrimitiveType.BYTE
- ? atob(
- (paramState.valueSpec as PrimitiveInstanceValue)
- .values[0] as string,
- )
- : ((paramState.valueSpec as PrimitiveInstanceValue)
- .values[0] as string);
- return (
-
- );
- },
-);
-
const ServiceTestParameterEditor = observer(
(props: {
isReadOnly: boolean;
paramState: ServiceValueSpecificationTestParameterState;
serviceTestState: ServiceTestState;
- contentTypeParamPair:
- | {
- contentType: string;
- param: string;
- }
- | undefined;
+ contentTypeParamPair: TestParamContentType | undefined;
}) => {
const { serviceTestState, paramState, isReadOnly, contentTypeParamPair } =
props;
@@ -290,7 +212,8 @@ const ServiceTestParameterEditor = observer(
/>
{showPopUp && (
- pair.param === paramState.parameterValue.name,
- )}
+ contentTypeParamPair={getContentTypeWithParamFromQuery(
+ resolveServiceQueryRawLambda(
+ serviceTestState.service,
+ ),
+ serviceTestState.editorStore,
+ ).find(
+ (pair) =>
+ pair.param === paramState.parameterValue.name,
+ )}
/>
))}
diff --git a/packages/legend-application-studio/src/components/editor/editor-group/testable/TestableSharedComponents.tsx b/packages/legend-application-studio/src/components/editor/editor-group/testable/TestableSharedComponents.tsx
index 2db0f9c30c..45864fa14e 100644
--- a/packages/legend-application-studio/src/components/editor/editor-group/testable/TestableSharedComponents.tsx
+++ b/packages/legend-application-studio/src/components/editor/editor-group/testable/TestableSharedComponents.tsx
@@ -27,6 +27,7 @@ import {
ModalFooter,
ModalFooterButton,
ModalHeader,
+ ModalTitle,
PanelContent,
PanelFormTextField,
PanelHeader,
@@ -35,8 +36,16 @@ import {
RefreshIcon,
WrenchIcon,
} from '@finos/legend-art';
-import { type DataElement, TestError } from '@finos/legend-graph';
import {
+ type DataElement,
+ type ValueSpecification,
+ type VariableExpression,
+ type PrimitiveInstanceValue,
+ TestError,
+ PrimitiveType,
+} from '@finos/legend-graph';
+import {
+ ContentType,
prettyCONSTName,
tryToFormatLosslessJSONString,
} from '@finos/legend-shared';
@@ -67,6 +76,7 @@ import {
buildElementOption,
getPackageableElementOptionFormatter,
} from '@finos/legend-lego/graph-editor';
+import type { TestParamContentType } from '../../../../stores/editor/utils/TestableUtils.js';
export const SharedDataElementModal = observer(
(props: {
@@ -597,3 +607,66 @@ export const TestAssertionEditor = observer(
);
},
);
+
+export const ExternalFormatParameterEditorModal = observer(
+ (props: {
+ valueSpec: ValueSpecification;
+ varExpression: VariableExpression;
+ isReadOnly: boolean;
+ onClose: () => void;
+ updateParamValue: (val: string) => void;
+ contentTypeParamPair: TestParamContentType;
+ }) => {
+ const {
+ valueSpec,
+ varExpression,
+ isReadOnly,
+ onClose,
+ updateParamValue,
+ contentTypeParamPair,
+ } = props;
+ const paramValue =
+ varExpression.genericType?.value.rawType === PrimitiveType.BYTE
+ ? atob((valueSpec as PrimitiveInstanceValue).values[0] as string)
+ : ((valueSpec as PrimitiveInstanceValue).values[0] as string);
+ return (
+
+ );
+ },
+);
diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts
index d38c85da0a..74bbd57e01 100644
--- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts
+++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts
@@ -21,27 +21,29 @@ import {
assertErrorThrown,
isNonNullable,
uuid,
- type GeneratorFn,
LogEvent,
guaranteeNonNullable,
addUniqueEntry,
uniq,
assertTrue,
guaranteeType,
+ returnUndefOnError,
+ type PlainObject,
+ filterByType,
+ deleteEntry,
} from '@finos/legend-shared';
import {
type ConcreteFunctionDefinition,
type EmbeddedData,
- type TestResult,
type AtomicTest,
type EngineRuntime,
type ObserverContext,
type ValueSpecification,
+ FunctionParameterValue,
+ VariableExpression,
FunctionTest,
- UniqueTestId,
FunctionStoreTestData,
FunctionTestSuite,
- RunTestsTestableInput,
RawLambda,
PackageableRuntime,
SimpleFunctionExpression,
@@ -53,6 +55,8 @@ import {
Database,
RelationalCSVData,
PackageableElementExplicitReference,
+ observe_ValueSpecification,
+ buildLambdaVariableExpressions,
} from '@finos/legend-graph';
import {
TestablePackageableElementEditorState,
@@ -63,15 +67,20 @@ import { EmbeddedDataEditorState } from '../../data/DataEditorState.js';
import {
functionTestable_deleteDataStore,
functionTestable_setEmbeddedData,
+ function_addParameterValue,
function_addTestSuite,
+ function_deleteParameterValue,
+ function_setParameterName,
+ function_setParameterValueSpec,
+ function_setParameterValues,
} from '../../../../../graph-modifier/DomainGraphModifierHelper.js';
import {
DEFAULT_TEST_ASSERTION_ID,
createDefaultEqualToJSONTestAssertion,
- isTestPassing,
} from '../../../../utils/TestableUtils.js';
import { LEGEND_STUDIO_APP_EVENT } from '../../../../../../__lib__/LegendStudioEvent.js';
import { testSuite_addTest } from '../../../../../graph-modifier/Testable_GraphModifierHelper.js';
+import { generateVariableExpressionMockValue } from '@finos/legend-query-builder';
const addToFunctionMap = (
val: SimpleFunctionExpression,
@@ -202,12 +211,86 @@ export class FunctionStoreTestDataState {
}
}
+export class FunctionTestParameterState {
+ readonly uuid = uuid();
+ readonly editorStore: EditorStore;
+ readonly testState: FunctionTestState;
+ parameterValue: FunctionParameterValue;
+ constructor(
+ parameterValue: FunctionParameterValue,
+ editorStore: EditorStore,
+ testState: FunctionTestState,
+ ) {
+ this.editorStore = editorStore;
+ this.testState = testState;
+ this.parameterValue = parameterValue;
+ }
+}
+
+export class FunctionValueSpecificationTestParameterState extends FunctionTestParameterState {
+ valueSpec: ValueSpecification;
+ varExpression: VariableExpression;
+
+ constructor(
+ parameterValue: FunctionParameterValue,
+ editorStore: EditorStore,
+ testState: FunctionTestState,
+ valueSpec: ValueSpecification,
+ varExpression: VariableExpression,
+ ) {
+ super(parameterValue, editorStore, testState);
+ makeObservable(this, {
+ setName: observable,
+ valueSpec: observable,
+ parameterValue: observable,
+ resetValueSpec: action,
+ updateValueSpecification: action,
+ updateParameterValue: action,
+ });
+ this.valueSpec = valueSpec;
+ this.varExpression = varExpression;
+ }
+
+ updateValueSpecification(val: ValueSpecification): void {
+ this.valueSpec = observe_ValueSpecification(
+ val,
+ this.editorStore.changeDetectionState.observerContext,
+ );
+ this.updateParameterValue();
+ }
+
+ updateParameterValue(): void {
+ const updatedValueSpec =
+ this.editorStore.graphManagerState.graphManager.serializeValueSpecification(
+ this.valueSpec,
+ );
+ function_setParameterValueSpec(this.parameterValue, updatedValueSpec);
+ }
+
+ setName(val: string): void {
+ function_setParameterName(this.parameterValue, val);
+ }
+
+ resetValueSpec(): void {
+ const mockValue = generateVariableExpressionMockValue(
+ this.varExpression,
+ this.editorStore.graphManagerState.graph,
+ this.editorStore.changeDetectionState.observerContext,
+ );
+ if (mockValue) {
+ this.updateValueSpecification(mockValue);
+ }
+ }
+}
+
export class FunctionTestState extends TestableTestEditorState {
readonly parentState: FunctionTestSuiteState;
readonly functionTestableState: FunctionTestableState;
readonly uuid = uuid();
override test: FunctionTest;
- // TODO: param
+ parameterValueStates: FunctionTestParameterState[] = [];
+ newParameterValueName = '';
+ showNewParameterModal = false;
constructor(
editorStore: EditorStore,
@@ -227,7 +310,13 @@ export class FunctionTestState extends TestableTestEditorState {
assertionEditorStates: observable,
testResultState: observable,
runningTestAction: observable,
+ parameterValueStates: observable,
+ setNewParameterValueName: action,
+ setShowNewParameterModal: action,
+ addExpressionParameterValue: action,
+ openNewParamModal: action,
addAssertion: action,
+ addParameterValue: action,
setAssertionToRename: action,
handleTestResult: action,
setSelectedTab: action,
@@ -236,6 +325,188 @@ export class FunctionTestState extends TestableTestEditorState {
this.parentState = parentSuiteState;
this.functionTestableState = parentSuiteState.functionTestableState;
this.test = test;
+ this.parameterValueStates = this.buildParameterStates();
+ }
+ get queryVariableExpressions(): VariableExpression[] {
+ const query =
+ this.functionTestableState.functionEditorState.bodyExpressionSequence;
+ return buildLambdaVariableExpressions(
+ query,
+ this.editorStore.graphManagerState,
+ ).filter(filterByType(VariableExpression));
+ }
+
+ get newParamOptions(): { value: string; label: string }[] {
+ const queryVarExpressions = this.queryVariableExpressions;
+ const currentParams = this.test.parameters ?? [];
+ return queryVarExpressions
+ .filter((v) => !currentParams.find((i) => i.name === v.name))
+ .map((e) => ({ value: e.name, label: e.name }));
+ }
+
+ setNewParameterValueName(val: string): void {
+ this.newParameterValueName = val;
+ }
+
+ setShowNewParameterModal(val: boolean): void {
+ this.showNewParameterModal = val;
+ }
+
+ openNewParamModal(): void {
+ this.setShowNewParameterModal(true);
+ const option = this.newParamOptions[0];
+ if (option) {
+ this.newParameterValueName = option.value;
+ }
+ }
+
+ addParameterValue(): void {
+ try {
+ const expressions = this.queryVariableExpressions;
+ const expression = guaranteeNonNullable(
+ expressions.find((v) => v.name === this.newParameterValueName),
+ );
+ this.addExpressionParameterValue(expression);
+ } catch (error) {
+ assertErrorThrown(error);
+ this.editorStore.applicationStore.notificationService.notifyError(error);
+ } finally {
+ this.setShowNewParameterModal(false);
+ }
+ }
+
+ syncWithQuery(): void {
+ // remove non existing params
+ this.parameterValueStates.forEach((paramState) => {
+ const expression = this.queryVariableExpressions.find(
+ (v) => v.name === paramState.parameterValue.name,
+ );
+ if (!expression) {
+ deleteEntry(this.parameterValueStates, paramState);
+ function_deleteParameterValue(this.test, paramState.parameterValue);
+ }
+ });
+ // add new required params
+ this.queryVariableExpressions.forEach((v) => {
+ const multiplicity = v.multiplicity;
+ const isRequired = multiplicity.lowerBound > 0;
+ const paramState = this.parameterValueStates.find(
+ (p) => p.parameterValue.name === v.name,
+ );
+ if (!paramState && isRequired) {
+ this.addExpressionParameterValue(v);
+ }
+ });
+ }
+
+ addExpressionParameterValue(expression: VariableExpression): void {
+ try {
+ const mockValue = guaranteeNonNullable(
+ generateVariableExpressionMockValue(
+ expression,
+ this.editorStore.graphManagerState.graph,
+ this.editorStore.changeDetectionState.observerContext,
+ ),
+ );
+ const paramValue = new FunctionParameterValue();
+ paramValue.name = expression.name;
+ paramValue.value =
+ this.editorStore.graphManagerState.graphManager.serializeValueSpecification(
+ mockValue,
+ );
+ function_addParameterValue(this.test, paramValue);
+ const paramValueState = new FunctionValueSpecificationTestParameterState(
+ paramValue,
+ this.editorStore,
+ this,
+ observe_ValueSpecification(
+ mockValue,
+ this.editorStore.changeDetectionState.observerContext,
+ ),
+ expression,
+ );
+ this.parameterValueStates.push(paramValueState);
+ } catch (error) {
+ assertErrorThrown(error);
+ this.editorStore.applicationStore.notificationService.notifyError(error);
+ }
+ }
+
+ generateTestParameterValues(): void {
+ try {
+ const varExpressions = this.queryVariableExpressions;
+ const parameterValueStates = varExpressions
+ .map((varExpression) => {
+ const mockValue = generateVariableExpressionMockValue(
+ varExpression,
+ this.editorStore.graphManagerState.graph,
+ this.editorStore.changeDetectionState.observerContext,
+ );
+ if (mockValue) {
+ const paramValue = new FunctionParameterValue();
+ paramValue.name = varExpression.name;
+ paramValue.value =
+ this.editorStore.graphManagerState.graphManager.serializeValueSpecification(
+ mockValue,
+ );
+ return new FunctionValueSpecificationTestParameterState(
+ paramValue,
+ this.editorStore,
+ this,
+ mockValue,
+ varExpression,
+ );
+ }
+ return undefined;
+ })
+ .filter(isNonNullable);
+ function_setParameterValues(
+ this.test,
+ parameterValueStates.map((s) => s.parameterValue),
+ );
+ this.parameterValueStates = parameterValueStates;
+ } catch (error) {
+ assertErrorThrown(error);
+ this.editorStore.applicationStore.notificationService.notifyError(
+ `Unable to generate param values: ${error.message}`,
+ );
+ }
+ }
+
+ buildParameterStates(): FunctionTestParameterState[] {
+ const query =
+ this.functionTestableState.functionEditorState.bodyExpressionSequence;
+ const varExpressions = buildLambdaVariableExpressions(
+ query,
+ this.editorStore.graphManagerState,
+ ).filter(filterByType(VariableExpression));
+ const paramValues = this.test.parameters ?? [];
+ return paramValues.map((pValue) => {
+ const spec = returnUndefOnError(() =>
+ this.editorStore.graphManagerState.graphManager.buildValueSpecification(
+ pValue.value as PlainObject,
+ this.editorStore.graphManagerState.graph,
+ ),
+ );
+ const expression = varExpressions.find((e) => e.name === pValue.name);
+ return spec && expression
+ ? new FunctionValueSpecificationTestParameterState(
+ pValue,
+ this.editorStore,
+ this,
+ observe_ValueSpecification(
+ spec,
+ this.editorStore.changeDetectionState.observerContext,
+ ),
+ expression,
+ )
+ : new FunctionTestParameterState(pValue, this.editorStore, this);
+ });
+ }
+
+ removeParamValueState(paramState: FunctionTestParameterState): void {
+ deleteEntry(this.parameterValueStates, paramState);
+ function_deleteParameterValue(this.test, paramState.parameterValue);
}
}
@@ -395,8 +666,8 @@ export class FunctionTestSuiteState extends TestableTestSuiteEditorState {
export class FunctionTestableState extends TestablePackageableElementEditorState {
readonly functionEditorState: FunctionEditorState;
declare selectedTestSuite: FunctionTestSuiteState | undefined;
+ declare runningSuite: FunctionTestSuite | undefined;
- runningSuite: FunctionTestSuite | undefined;
createSuiteModal = false;
constructor(functionEditorState: FunctionEditorState) {
@@ -407,6 +678,7 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
selectedTestSuite: observable,
testableResults: observable,
runningSuite: observable,
+ testableComponentToRename: observable,
createSuiteModal: observable,
init: action,
buildTestSuiteState: action,
@@ -428,41 +700,7 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
return this.functionEditorState.functionElement;
}
- get passingSuites(): FunctionTestSuite[] {
- const results = this.testableResults;
- if (results?.length) {
- return this.function.tests.filter((suite) =>
- results
- .filter((res) => res.parentSuite?.id === suite.id)
- .every((e) => isTestPassing(e)),
- );
- }
- return [];
- }
-
- get failingSuites(): FunctionTestSuite[] {
- const results = this.testableResults;
- if (results?.length) {
- return this.function.tests.filter((suite) =>
- results
- .filter((res) => res.parentSuite?.id === suite.id)
- .some((e) => !isTestPassing(e)),
- );
- }
- return [];
- }
-
- get staticSuites(): FunctionTestSuite[] {
- const results = this.testableResults;
- if (results?.length) {
- return this.function.tests.filter((suite) =>
- results.every((res) => res.parentSuite?.id !== suite.id),
- );
- }
- return this.function.tests;
- }
-
- init(): void {
+ override init(): void {
if (!this.selectedTestSuite) {
const suite = this.function.tests[0];
this.selectedTestSuite = suite
@@ -545,95 +783,6 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
this.setCreateSuite(false);
}
- *runSuite(suite: FunctionTestSuite): GeneratorFn
{
- try {
- this.runningSuite = suite;
- this.clearTestResultsForSuite(suite);
- this.selectedTestSuite?.testStates.forEach((t) => t.resetResult());
- this.selectedTestSuite?.testStates.forEach((t) =>
- t.runningTestAction.inProgress(),
- );
-
- const input = new RunTestsTestableInput(this.function);
- suite.tests.forEach((t) =>
- input.unitTestIds.push(new UniqueTestId(suite, t)),
- );
- const testResults =
- (yield this.editorStore.graphManagerState.graphManager.runTests(
- [input],
- this.editorStore.graphManagerState.graph,
- )) as TestResult[];
-
- this.handleNewResults(testResults);
- } catch (error) {
- assertErrorThrown(error);
- this.editorStore.applicationStore.notificationService.notifyError(error);
- this.isRunningTestableSuitesState.fail();
- } finally {
- this.selectedTestSuite?.testStates.forEach((t) =>
- t.runningTestAction.complete(),
- );
- this.runningSuite = undefined;
- }
- }
-
- *runAllFailingSuites(): GeneratorFn {
- try {
- this.isRunningFailingSuitesState.inProgress();
- const input = new RunTestsTestableInput(this.testable);
- this.failingSuites.forEach((s) => {
- s.tests.forEach((t) => input.unitTestIds.push(new UniqueTestId(s, t)));
- });
- const testResults =
- (yield this.editorStore.graphManagerState.graphManager.runTests(
- [input],
- this.editorStore.graphManagerState.graph,
- )) as TestResult[];
- this.handleNewResults(testResults);
- this.isRunningFailingSuitesState.complete();
- } catch (error) {
- assertErrorThrown(error);
- this.editorStore.applicationStore.notificationService.notifyError(error);
- this.isRunningFailingSuitesState.fail();
- } finally {
- this.selectedTestSuite?.testStates.forEach((t) =>
- t.runningTestAction.complete(),
- );
- }
- }
-
- handleNewResults(results: TestResult[]): void {
- if (this.testableResults?.length) {
- const newSuitesResults = results
- .map((e) => e.parentSuite?.id)
- .filter(isNonNullable);
- const reducedFilters = this.testableResults.filter(
- (res) => !newSuitesResults.includes(res.parentSuite?.id ?? ''),
- );
- this.setTestableResults([...reducedFilters, ...results]);
- } else {
- this.setTestableResults(results);
- }
- this.testableResults?.forEach((result) => {
- const state = this.selectedTestSuite?.testStates.find(
- (t) =>
- t.test.id === result.atomicTest.id &&
- t.parentState.suite.id === result.parentSuite?.id,
- );
- state?.handleTestResult(result);
- });
- }
-
- clearTestResultsForSuite(suite: FunctionTestSuite): void {
- this.testableResults = this.testableResults?.filter(
- (t) => !(this.resolveSuiteResults(suite) ?? []).includes(t),
- );
- }
-
- resolveSuiteResults(suite: FunctionTestSuite): TestResult[] | undefined {
- return this.testableResults?.filter((t) => t.parentSuite?.id === suite.id);
- }
-
buildTestSuiteState(val: FunctionTestSuite): FunctionTestSuiteState {
return new FunctionTestSuiteState(this.editorStore, this, val);
}
diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestableState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestableState.ts
index eb8a99cd61..9823761e5e 100644
--- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestableState.ts
+++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestableState.ts
@@ -68,7 +68,6 @@ import {
import {
EmbeddedDataCreatorFromEmbeddedData,
createBareExternalFormat,
- isTestPassing,
} from '../../../../utils/TestableUtils.js';
import {
TESTABLE_TEST_TAB,
@@ -542,7 +541,7 @@ export class MappingTestableState extends TestablePackageableElementEditorState
createSuiteState: CreateSuiteState | undefined;
declare selectedTestSuite: MappingTestSuiteState | undefined;
- runningSuite: MappingTestSuite | undefined;
+ declare runningSuite: MappingTestSuite | undefined;
constructor(mappingEditorState: MappingEditorState) {
super(mappingEditorState, mappingEditorState.mapping);
@@ -575,50 +574,6 @@ export class MappingTestableState extends TestablePackageableElementEditorState
return this.mappingEditorState.mapping;
}
- get passingSuites(): MappingTestSuite[] {
- const results = this.testableResults;
- if (results?.length) {
- return this.mapping.tests.filter((suite) =>
- results
- .filter((res) => res.parentSuite?.id === suite.id)
- .every((e) => isTestPassing(e)),
- );
- }
- return [];
- }
-
- get failingSuites(): MappingTestSuite[] {
- const results = this.testableResults;
- if (results?.length) {
- return this.mapping.tests.filter((suite) =>
- results
- .filter((res) => res.parentSuite?.id === suite.id)
- .some((e) => !isTestPassing(e)),
- );
- }
- return [];
- }
-
- get staticSuites(): MappingTestSuite[] {
- const results = this.testableResults;
- if (results?.length) {
- return this.mapping.tests.filter((suite) =>
- results.every((res) => res.parentSuite?.id !== suite.id),
- );
- }
- return this.mapping.tests;
- }
-
- resolveSuiteResults(suite: MappingTestSuite): TestResult[] | undefined {
- return this.testableResults?.filter((t) => t.parentSuite?.id === suite.id);
- }
-
- clearTestResultsForSuite(suite: MappingTestSuite): void {
- this.testableResults = this.testableResults?.filter(
- (t) => !(this.resolveSuiteResults(suite) ?? []).includes(t),
- );
- }
-
override init(): void {
if (!this.selectedTestSuite) {
const suite = this.mapping.tests[0];
@@ -668,7 +623,7 @@ export class MappingTestableState extends TestablePackageableElementEditorState
}
}
- *runSuite(suite: MappingTestSuite): GeneratorFn {
+ override *runSuite(suite: MappingTestSuite): GeneratorFn {
try {
this.runningSuite = suite;
this.clearTestResultsForSuite(suite);
@@ -718,51 +673,4 @@ export class MappingTestableState extends TestablePackageableElementEditorState
this.runningSuite = undefined;
}
}
-
- *runAllFailingSuites(): GeneratorFn {
- try {
- this.isRunningFailingSuitesState.inProgress();
- const input = new RunTestsTestableInput(this.mapping);
- this.failingSuites.forEach((s) => {
- s.tests.forEach((t) => input.unitTestIds.push(new UniqueTestId(s, t)));
- });
- const testResults =
- (yield this.editorStore.graphManagerState.graphManager.runTests(
- [input],
- this.editorStore.graphManagerState.graph,
- )) as TestResult[];
- this.handleNewResults(testResults);
- this.isRunningFailingSuitesState.complete();
- } catch (error) {
- assertErrorThrown(error);
- this.editorStore.applicationStore.notificationService.notifyError(error);
- this.isRunningFailingSuitesState.fail();
- } finally {
- this.selectedTestSuite?.testStates.forEach((t) =>
- t.runningTestAction.complete(),
- );
- }
- }
-
- handleNewResults(results: TestResult[]): void {
- if (this.testableResults?.length) {
- const newSuitesResults = results
- .map((e) => e.parentSuite?.id)
- .filter(isNonNullable);
- const reducedFilters = this.testableResults.filter(
- (res) => !newSuitesResults.includes(res.parentSuite?.id ?? ''),
- );
- this.setTestableResults([...reducedFilters, ...results]);
- } else {
- this.setTestableResults(results);
- }
- this.testableResults?.forEach((result) => {
- const state = this.selectedTestSuite?.testStates.find(
- (t) =>
- t.test.id === result.atomicTest.id &&
- t.parentState.suite.id === result.parentSuite?.id,
- );
- state?.handleTestResult(result);
- });
- }
}
diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestingHelper.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestingHelper.ts
index 46ca0df4d7..baba162642 100644
--- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestingHelper.ts
+++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestingHelper.ts
@@ -184,7 +184,7 @@ export const createBareMappingTest = (
id: string,
storeTestData: StoreTestData | undefined,
observerContext: ObserverContext,
- suite?: MappingTestSuite | undefined,
+ suite: MappingTestSuite,
): MappingTest => {
const mappingTest = new MappingTest();
mappingTest.id = id;
@@ -192,10 +192,10 @@ export const createBareMappingTest = (
mappingTest.assertions = [
createDefaultEqualToJSONTestAssertion(DEFAULT_TEST_ASSERTION_ID),
];
- if (suite) {
- mappingTest.__parent = suite;
- testSuite_addTest(suite, mappingTest, observerContext);
- }
+
+ mappingTest.__parent = suite;
+ testSuite_addTest(suite, mappingTest, observerContext);
+
const assertion = createDefaultEqualToJSONTestAssertion(`expectedAssertion`);
mappingTest.assertions = [assertion];
assertion.parentTest = mappingTest;
diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/service/testable/ServiceTestEditorState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/service/testable/ServiceTestEditorState.ts
index 9c6a47fbb2..ad2553421f 100644
--- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/service/testable/ServiceTestEditorState.ts
+++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/service/testable/ServiceTestEditorState.ts
@@ -15,7 +15,6 @@
*/
import {
- type Binding,
type ServiceTest,
type Service,
type ValueSpecification,
@@ -24,16 +23,7 @@ import {
buildLambdaVariableExpressions,
VariableExpression,
PureMultiExecution,
- PackageableElementImplicitReference,
- matchFunctionName,
- isStubbed_RawLambda,
- InstanceValue,
- LambdaFunctionInstanceValue,
- SimpleFunctionExpression,
- CollectionInstanceValue,
resolveServiceQueryRawLambda,
- PrimitiveInstanceValue,
- PrimitiveType,
} from '@finos/legend-graph';
import { action, flow, makeObservable, observable } from 'mobx';
import {
@@ -59,15 +49,9 @@ import {
isNonNullable,
returnUndefOnError,
uuid,
- getNullableFirstEntry,
- LogEvent,
} from '@finos/legend-shared';
import type { EditorStore } from '../../../../EditorStore.js';
-import {
- QUERY_BUILDER_SUPPORTED_FUNCTIONS,
- generateVariableExpressionMockValue,
-} from '@finos/legend-query-builder';
-import { LEGEND_STUDIO_APP_EVENT } from '../../../../../../__lib__/LegendStudioEvent.js';
+import { generateVariableExpressionMockValue } from '@finos/legend-query-builder';
export enum SERIALIZATION_FORMAT {
PURE = 'PURE',
@@ -199,7 +183,6 @@ export class ServiceTestSetupState {
addServiceTestAssertKeys: action,
syncWithQuery: action,
removeParamValueState: action,
- getContentTypeWithParamFromQuery: action,
});
this.testState = testState;
@@ -243,131 +226,6 @@ export class ServiceTestSetupState {
}));
}
- getContentTypeWithParamFromQuery(): {
- contentType: string;
- param: string;
- }[] {
- const query = resolveServiceQueryRawLambda(this.testState.service);
- if (query && !isStubbed_RawLambda(query)) {
- // safely pass unsupported funtions when building ValueSpecification from Rawlambda
- try {
- const valueSpec =
- this.editorStore.graphManagerState.graphManager.buildValueSpecification(
- this.editorStore.graphManagerState.graphManager.serializeRawValueSpecification(
- query,
- ),
- this.editorStore.graphManagerState.graph,
- );
- if (valueSpec instanceof LambdaFunctionInstanceValue) {
- return this.getContentTypeWithParamRecursively(
- valueSpec.values[0]?.expressionSequence.find(
- (exp) =>
- exp instanceof SimpleFunctionExpression &&
- (matchFunctionName(
- exp.functionName,
- QUERY_BUILDER_SUPPORTED_FUNCTIONS.SERIALIZE,
- ) ||
- matchFunctionName(
- exp.functionName,
- QUERY_BUILDER_SUPPORTED_FUNCTIONS.EXTERNALIZE,
- )),
- ),
- );
- }
- } catch (error) {
- this.editorStore.applicationStore.logService.error(
- LogEvent.create(LEGEND_STUDIO_APP_EVENT.SERVICE_TEST_SETUP_FAILURE),
- error,
- );
- }
- }
- return [];
- }
-
- getContentTypeWithParamRecursively(
- expression: ValueSpecification | undefined,
- ): {
- contentType: string;
- param: string;
- }[] {
- let currentExpression = expression;
- const res: {
- contentType: string;
- param: string;
- }[] = [];
- // use if statement to safely scan service query without breaking the app
- while (currentExpression instanceof SimpleFunctionExpression) {
- if (
- matchFunctionName(
- currentExpression.functionName,
- QUERY_BUILDER_SUPPORTED_FUNCTIONS.INTERNALIZE,
- ) ||
- matchFunctionName(
- currentExpression.functionName,
- QUERY_BUILDER_SUPPORTED_FUNCTIONS.GET_RUNTIME_WITH_MODEL_QUERY_CONNECTION,
- )
- ) {
- if (currentExpression.parametersValues[1] instanceof InstanceValue) {
- if (
- currentExpression.parametersValues[1].values[0] instanceof
- PackageableElementImplicitReference &&
- currentExpression.parametersValues[2] instanceof VariableExpression
- ) {
- res.push({
- contentType: (
- currentExpression.parametersValues[1].values[0].value as Binding
- ).contentType,
- param: currentExpression.parametersValues[2].name,
- });
- } else if (
- matchFunctionName(
- currentExpression.functionName,
- QUERY_BUILDER_SUPPORTED_FUNCTIONS.GET_RUNTIME_WITH_MODEL_QUERY_CONNECTION,
- ) &&
- currentExpression.parametersValues[1] instanceof
- PrimitiveInstanceValue &&
- currentExpression.parametersValues[1].genericType.value.rawType ===
- PrimitiveType.STRING &&
- currentExpression.parametersValues[2] instanceof VariableExpression
- ) {
- res.push({
- contentType: currentExpression.parametersValues[1]
- .values[0] as string,
- param: currentExpression.parametersValues[2].name,
- });
- }
- }
- currentExpression = currentExpression.parametersValues[1];
- } else if (
- matchFunctionName(
- currentExpression.functionName,
- QUERY_BUILDER_SUPPORTED_FUNCTIONS.FROM,
- )
- ) {
- currentExpression = currentExpression.parametersValues[2];
- } else if (
- matchFunctionName(
- currentExpression.functionName,
- QUERY_BUILDER_SUPPORTED_FUNCTIONS.MERGERUNTIMES,
- )
- ) {
- const collection = currentExpression.parametersValues[0];
- if (collection instanceof CollectionInstanceValue) {
- collection.values
- .map((v) => this.getContentTypeWithParamRecursively(v))
- .flat()
- .map((p) => res.push(p));
- }
- currentExpression = collection;
- } else {
- currentExpression = getNullableFirstEntry(
- currentExpression.parametersValues,
- );
- }
- }
- return res;
- }
-
addServiceTestAssertKeys(val: string[]): void {
service_addAssertKeyForTest(this.testState.test, val);
}
diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/testable/TestableEditorState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/testable/TestableEditorState.ts
index cb57ece6cc..6fb510c864 100644
--- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/testable/TestableEditorState.ts
+++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/testable/TestableEditorState.ts
@@ -36,6 +36,7 @@ import {
addUniqueEntry,
deleteEntry,
isNonNullable,
+ filterByType,
} from '@finos/legend-shared';
import { action, flowResult, makeObservable, observable } from 'mobx';
import type { EditorStore } from '../../../EditorStore.js';
@@ -45,7 +46,10 @@ import {
testable_deleteTest,
testable_setId,
} from '../../../../graph-modifier/Testable_GraphModifierHelper.js';
-import { createEmptyEqualToJsonAssertion } from '../../../utils/TestableUtils.js';
+import {
+ createEmptyEqualToJsonAssertion,
+ isTestPassing,
+} from '../../../utils/TestableUtils.js';
import { TESTABLE_RESULT } from '../../../sidebar-state/testable/GlobalTestRunnerState.js';
import {
TestAssertionEditorState,
@@ -144,22 +148,6 @@ export abstract class TestableTestEditorState {
}
}
- *runTest(): GeneratorFn {
- try {
- this.resetResult();
- this.runningTestAction.inProgress();
- const result = (yield flowResult(this.fetchTestResult())) as TestResult;
- this.handleTestResult(result);
- this.runningTestAction.complete();
- } catch (error) {
- assertErrorThrown(error);
- this.editorStore.applicationStore.notificationService.notifyError(
- `Error running test: ${error.message}`,
- );
- this.runningTestAction.fail();
- }
- }
-
// Fetches test results. Caller of test should catch the error
async fetchTestResult(): Promise {
const input = new RunTestsTestableInput(this.testable);
@@ -195,6 +183,18 @@ export abstract class TestableTestEditorState {
});
}
+ correspondsToTestResult(val: TestResult): boolean {
+ const atomicTest = this.test;
+ if (atomicTest.id === val.atomicTest.id) {
+ const parent = atomicTest.__parent;
+ if (parent instanceof TestSuite) {
+ return parent.id === val.parentSuite?.id;
+ }
+ return val.parentSuite === undefined && val.testable === this.testable;
+ }
+ return false;
+ }
+
get assertionCount(): number {
return this.assertionEditorStates.length;
}
@@ -224,6 +224,21 @@ export abstract class TestableTestEditorState {
(state) => state.assertionResultState.result !== TESTABLE_RESULT.PASSED,
).length;
}
+ *runTest(): GeneratorFn {
+ try {
+ this.resetResult();
+ this.runningTestAction.inProgress();
+ const result = (yield flowResult(this.fetchTestResult())) as TestResult;
+ this.handleTestResult(result);
+ this.runningTestAction.complete();
+ } catch (error) {
+ assertErrorThrown(error);
+ this.editorStore.applicationStore.notificationService.notifyError(
+ `Error running test: ${error.message}`,
+ );
+ this.runningTestAction.fail();
+ }
+ }
}
export abstract class TestableTestSuiteEditorState {
@@ -339,6 +354,7 @@ export abstract class TestablePackageableElementEditorState {
readonly testable: Testable;
testableResults: TestResult[] | undefined;
selectedTestSuite: TestableTestSuiteEditorState | undefined;
+ runningSuite: TestSuite | undefined;
testableComponentToRename: Test | undefined;
@@ -353,12 +369,58 @@ export abstract class TestablePackageableElementEditorState {
abstract init(): void;
- abstract handleNewResults(results: TestResult[]): void;
-
get suiteCount(): number {
return this.testable.tests.length;
}
+ get suites(): TestSuite[] {
+ return this.testable.tests.filter(filterByType(TestSuite));
+ }
+
+ get passingSuites(): TestSuite[] {
+ const results = this.testableResults;
+ if (results?.length) {
+ return this.suites.filter((suite) =>
+ results
+ .filter((res) => res.parentSuite?.id === suite.id)
+ .every((e) => isTestPassing(e)),
+ );
+ }
+ return [];
+ }
+
+ get failingSuites(): TestSuite[] {
+ const results = this.testableResults;
+ if (results?.length) {
+ return this.suites.filter((suite) =>
+ results
+ .filter((res) => res.parentSuite?.id === suite.id)
+ .some((e) => !isTestPassing(e)),
+ );
+ }
+ return [];
+ }
+
+ resolveSuiteResults(suite: TestSuite): TestResult[] | undefined {
+ return this.testableResults?.filter((t) => t.parentSuite?.id === suite.id);
+ }
+
+ clearTestResultsForSuite(suite: TestSuite): void {
+ this.testableResults = this.testableResults?.filter(
+ (t) => !(this.resolveSuiteResults(suite) ?? []).includes(t),
+ );
+ }
+
+ get staticSuites(): TestSuite[] {
+ const results = this.testableResults;
+ if (results?.length) {
+ return this.suites.filter((suite) =>
+ results.every((res) => res.parentSuite?.id !== suite.id),
+ );
+ }
+ return this.suites;
+ }
+
setTestableResults(val: TestResult[] | undefined): void {
this.testableResults = val;
}
@@ -377,9 +439,36 @@ export abstract class TestablePackageableElementEditorState {
deleteTestSuite(testSuite: TestSuite): void {
testable_deleteTest(this.testable, testSuite);
if (this.selectedTestSuite?.suite === testSuite) {
+ this.selectedTestSuite = undefined;
this.init();
}
}
+
+ *runAllFailingSuites(): GeneratorFn {
+ try {
+ this.isRunningFailingSuitesState.inProgress();
+ const input = new RunTestsTestableInput(this.testable);
+ this.failingSuites.forEach((s) => {
+ s.tests.forEach((t) => input.unitTestIds.push(new UniqueTestId(s, t)));
+ });
+ const testResults =
+ (yield this.editorStore.graphManagerState.graphManager.runTests(
+ [input],
+ this.editorStore.graphManagerState.graph,
+ )) as TestResult[];
+ this.handleNewResults(testResults);
+ this.isRunningFailingSuitesState.complete();
+ } catch (error) {
+ assertErrorThrown(error);
+ this.editorStore.applicationStore.notificationService.notifyError(error);
+ this.isRunningFailingSuitesState.fail();
+ } finally {
+ this.selectedTestSuite?.testStates.forEach((t) =>
+ t.runningTestAction.complete(),
+ );
+ }
+ }
+
*runTestable(): GeneratorFn {
try {
this.setTestableResults(undefined);
@@ -406,4 +495,55 @@ export abstract class TestablePackageableElementEditorState {
);
}
}
+
+ *runSuite(suite: TestSuite): GeneratorFn {
+ try {
+ this.runningSuite = suite;
+ this.clearTestResultsForSuite(suite);
+ this.selectedTestSuite?.testStates.forEach((t) => t.resetResult());
+ this.selectedTestSuite?.testStates.forEach((t) =>
+ t.runningTestAction.inProgress(),
+ );
+ const input = new RunTestsTestableInput(this.testable);
+ suite.tests.forEach((t) =>
+ input.unitTestIds.push(new UniqueTestId(suite, t)),
+ );
+ const testResults =
+ (yield this.editorStore.graphManagerState.graphManager.runTests(
+ [input],
+ this.editorStore.graphManagerState.graph,
+ )) as TestResult[];
+
+ this.handleNewResults(testResults);
+ } catch (error) {
+ assertErrorThrown(error);
+ this.editorStore.applicationStore.notificationService.notifyError(error);
+ this.isRunningTestableSuitesState.fail();
+ } finally {
+ this.selectedTestSuite?.testStates.forEach((t) =>
+ t.runningTestAction.complete(),
+ );
+ this.runningSuite = undefined;
+ }
+ }
+
+ handleNewResults(results: TestResult[]): void {
+ if (this.testableResults?.length) {
+ const newSuitesResults = results
+ .map((e) => e.parentSuite?.id)
+ .filter(isNonNullable);
+ const reducedFilters = this.testableResults.filter(
+ (res) => !newSuitesResults.includes(res.parentSuite?.id ?? ''),
+ );
+ this.setTestableResults([...reducedFilters, ...results]);
+ } else {
+ this.setTestableResults(results);
+ }
+ this.testableResults?.forEach((result) => {
+ const state = this.selectedTestSuite?.testStates.find((testState) =>
+ testState.correspondsToTestResult(result),
+ );
+ state?.handleTestResult(result);
+ });
+ }
}
diff --git a/packages/legend-application-studio/src/stores/editor/utils/TestableUtils.ts b/packages/legend-application-studio/src/stores/editor/utils/TestableUtils.ts
index 140247473c..463896da4e 100644
--- a/packages/legend-application-studio/src/stores/editor/utils/TestableUtils.ts
+++ b/packages/legend-application-studio/src/stores/editor/utils/TestableUtils.ts
@@ -29,29 +29,44 @@ import {
type AtomicTest,
type Class,
type EmbeddedDataVisitor,
+ type INTERNAL__UnknownConnection,
+ type DataElementReference,
+ type INTERNAL__UnknownEmbeddedData,
+ type TestResult,
+ type ValueSpecification,
+ type Binding,
+ type RawLambda,
ExternalFormatData,
RelationalCSVData,
ConnectionTestData,
EqualToJson,
DEFAULT_TEST_ASSERTION_PREFIX,
RelationalCSVDataTable,
- type INTERNAL__UnknownConnection,
getAllIdentifiedConnectionsFromRuntime,
ModelStoreData,
ModelEmbeddedData,
PackageableElementExplicitReference,
- type DataElementReference,
- type INTERNAL__UnknownEmbeddedData,
- type TestResult,
TestExecuted,
TestExecutionStatus,
+ isStubbed_RawLambda,
+ SimpleFunctionExpression,
+ InstanceValue,
+ CollectionInstanceValue,
+ matchFunctionName,
+ PackageableElementImplicitReference,
+ VariableExpression,
+ PrimitiveInstanceValue,
+ PrimitiveType,
+ LambdaFunctionInstanceValue,
} from '@finos/legend-graph';
import {
assertTrue,
ContentType,
generateEnumerableNameFromToken,
+ getNullableFirstEntry,
guaranteeNonEmptyString,
isNonNullable,
+ LogEvent,
returnUndefOnError,
UnsupportedOperationError,
uuid,
@@ -60,6 +75,8 @@ import { EmbeddedDataType } from '../editor-state/ExternalFormatState.js';
import type { EditorStore } from '../EditorStore.js';
import { createMockDataForMappingElementSource } from './MockDataUtils.js';
import type { DSL_Data_LegendStudioApplicationPlugin_Extension } from '../../extensions/DSL_Data_LegendStudioApplicationPlugin_Extension.js';
+import { QUERY_BUILDER_SUPPORTED_FUNCTIONS } from '@finos/legend-query-builder';
+import { LEGEND_STUDIO_APP_EVENT } from '../../../__lib__/LegendStudioEvent.js';
export const DEFAULT_TEST_ASSERTION_ID = 'assertion_1';
export const DEFAULT_TEST_ID = 'test_1';
@@ -359,3 +376,126 @@ export const TEMPORARY__createRelationalDataFromCSV = (
export const isTestPassing = (testResult: TestResult): boolean =>
testResult instanceof TestExecuted &&
testResult.testExecutionStatus === TestExecutionStatus.PASS;
+
+// external format param type
+export interface TestParamContentType {
+ contentType: string;
+ param: string;
+}
+export const getContentTypeWithParamRecursively = (
+ expression: ValueSpecification | undefined,
+): TestParamContentType[] => {
+ let currentExpression = expression;
+ const res: TestParamContentType[] = [];
+ // use if statement to safely scan service query without breaking the app
+ while (currentExpression instanceof SimpleFunctionExpression) {
+ if (
+ matchFunctionName(
+ currentExpression.functionName,
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.INTERNALIZE,
+ ) ||
+ matchFunctionName(
+ currentExpression.functionName,
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.GET_RUNTIME_WITH_MODEL_QUERY_CONNECTION,
+ )
+ ) {
+ if (currentExpression.parametersValues[1] instanceof InstanceValue) {
+ if (
+ currentExpression.parametersValues[1].values[0] instanceof
+ PackageableElementImplicitReference &&
+ currentExpression.parametersValues[2] instanceof VariableExpression
+ ) {
+ res.push({
+ contentType: (
+ currentExpression.parametersValues[1].values[0].value as Binding
+ ).contentType,
+ param: currentExpression.parametersValues[2].name,
+ });
+ } else if (
+ matchFunctionName(
+ currentExpression.functionName,
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.GET_RUNTIME_WITH_MODEL_QUERY_CONNECTION,
+ ) &&
+ currentExpression.parametersValues[1] instanceof
+ PrimitiveInstanceValue &&
+ currentExpression.parametersValues[1].genericType.value.rawType ===
+ PrimitiveType.STRING &&
+ currentExpression.parametersValues[2] instanceof VariableExpression
+ ) {
+ res.push({
+ contentType: currentExpression.parametersValues[1]
+ .values[0] as string,
+ param: currentExpression.parametersValues[2].name,
+ });
+ }
+ }
+ currentExpression = currentExpression.parametersValues[1];
+ } else if (
+ matchFunctionName(
+ currentExpression.functionName,
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.FROM,
+ )
+ ) {
+ currentExpression = currentExpression.parametersValues[2];
+ } else if (
+ matchFunctionName(
+ currentExpression.functionName,
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.MERGERUNTIMES,
+ )
+ ) {
+ const collection = currentExpression.parametersValues[0];
+ if (collection instanceof CollectionInstanceValue) {
+ collection.values
+ .map((v) => getContentTypeWithParamRecursively(v))
+ .flat()
+ .map((p) => res.push(p));
+ }
+ currentExpression = collection;
+ } else {
+ currentExpression = getNullableFirstEntry(
+ currentExpression.parametersValues,
+ );
+ }
+ }
+ return res;
+};
+
+export const getContentTypeWithParamFromQuery = (
+ query: RawLambda | undefined,
+ editorStore: EditorStore,
+): TestParamContentType[] => {
+ if (query && !isStubbed_RawLambda(query)) {
+ // safely pass unsupported funtions when building ValueSpecification from Rawlambda
+ try {
+ const valueSpec =
+ editorStore.graphManagerState.graphManager.buildValueSpecification(
+ editorStore.graphManagerState.graphManager.serializeRawValueSpecification(
+ query,
+ ),
+ editorStore.graphManagerState.graph,
+ );
+ if (valueSpec instanceof LambdaFunctionInstanceValue) {
+ return getContentTypeWithParamRecursively(
+ valueSpec.values[0]?.expressionSequence.find(
+ (exp) =>
+ exp instanceof SimpleFunctionExpression &&
+ (matchFunctionName(
+ exp.functionName,
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.SERIALIZE,
+ ) ||
+ matchFunctionName(
+ exp.functionName,
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.EXTERNALIZE,
+ )),
+ ),
+ );
+ }
+ } catch (error) {
+ editorStore.applicationStore.logService.error(
+ LogEvent.create(LEGEND_STUDIO_APP_EVENT.SERVICE_TEST_SETUP_FAILURE),
+ error,
+ );
+ }
+ }
+ return [];
+};
diff --git a/packages/legend-application-studio/src/stores/graph-modifier/DomainGraphModifierHelper.ts b/packages/legend-application-studio/src/stores/graph-modifier/DomainGraphModifierHelper.ts
index ad2d4b01a0..06cf85d9c1 100644
--- a/packages/legend-application-studio/src/stores/graph-modifier/DomainGraphModifierHelper.ts
+++ b/packages/legend-application-studio/src/stores/graph-modifier/DomainGraphModifierHelper.ts
@@ -50,6 +50,8 @@ import {
type INTERNAL__UnknownFunctionActivator,
type FunctionStoreTestData,
type ObserverContext,
+ type FunctionParameterValue,
+ type FunctionTest,
type FunctionTestSuite,
type AggregationKind,
type EmbeddedData,
@@ -73,6 +75,7 @@ import {
getOtherAssociatedProperty,
observe_EmbeddedData,
observe_FunctionTestSuite,
+ observe_FunctionParameterValue,
} from '@finos/legend-graph';
// --------------------------------------------- Packageable Element -------------------------------------
@@ -415,6 +418,36 @@ export const function_addTestSuite = action(
},
);
+export const function_setParameterValueSpec = action(
+ (parameterValue: FunctionParameterValue, val: object) => {
+ parameterValue.value = val;
+ },
+);
+
+export const function_setParameterValues = action(
+ (test: FunctionTest, values: FunctionParameterValue[]) => {
+ test.parameters = values.map(observe_FunctionParameterValue);
+ },
+);
+
+export const function_deleteParameterValue = action(
+ (test: FunctionTest, value: FunctionParameterValue) => {
+ deleteEntry(test.parameters ?? [], value);
+ },
+);
+
+export const function_addParameterValue = action(
+ (test: FunctionTest, value: FunctionParameterValue) => {
+ test.parameters?.push(observe_FunctionParameterValue(value));
+ },
+);
+
+export const function_setParameterName = action(
+ (parameterValue: FunctionParameterValue, val: string) => {
+ parameterValue.name = val;
+ },
+);
+
// --------------------------------------------- Enumeration -------------------------------------
export const enum_setName = action((val: Enum, value: string): void => {
diff --git a/packages/legend-application-studio/style/components/editor/_function-editor.scss b/packages/legend-application-studio/style/components/editor/_function-editor.scss
index 46d2414668..8a47759f6d 100644
--- a/packages/legend-application-studio/style/components/editor/_function-editor.scss
+++ b/packages/legend-application-studio/style/components/editor/_function-editor.scss
@@ -266,3 +266,139 @@
color: white;
font-size: 1rem;
}
+
+.function-testable-editor {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ &__header {
+ @include flexVCenter;
+ @include flexHSpaceBetween;
+
+ cursor: default;
+ background: var(--color-dark-grey-50);
+ color: var(--color-light-grey-300);
+ height: 2.8rem;
+ min-height: 2.8rem;
+ position: relative;
+ z-index: 1;
+ border: 0.1rem solid var(--color-dark-grey-200);
+ box-shadow: var(--color-light-shade-280) 0 0.1rem 0.5rem 0;
+ }
+
+ &__header__title {
+ @include flexVCenter;
+
+ padding-left: 0.5rem;
+ user-select: none;
+ }
+
+ &__header__title__label {
+ height: 1.8rem;
+ line-height: 1.8rem;
+ border-radius: 0.1rem;
+ padding: 0 0.5rem;
+ color: var(--color-light-grey-200);
+ background: var(--color-dark-grey-250);
+ font-size: 1.1rem;
+ cursor: default;
+ margin-right: 0.5rem;
+ white-space: nowrap;
+
+ &--data {
+ background: var(--color-blue-200) !important;
+ }
+
+ &--tests {
+ background: var(--color-purple-200) !important;
+ }
+
+ &--tests-suites {
+ background: var(--color-pink-500) !important;
+ }
+
+ &--assertions {
+ background: var(--color-yellow-500) !important;
+ }
+ }
+
+ &__header__title__content {
+ font-weight: bold;
+
+ @include ellipsisTextOverflow;
+ }
+
+ .panel {
+ height: 100%;
+ width: 100%;
+
+ &__header {
+ padding-left: 0.5rem;
+ color: var(--color-light-grey-300);
+ font-weight: bold;
+ background: var(--color-dark-grey-50);
+ border-bottom: 0.1rem solid var(--color-dark-grey-200);
+ }
+
+ &__header__title__label {
+ background: var(--color-dark-grey-250);
+ }
+
+ &__header__action {
+ color: var(--color-light-grey-400);
+ }
+
+ &__header__action[disabled] svg {
+ color: var(--color-dark-grey-300) !important;
+ }
+
+ &__content {
+ height: calc(100% - 2.8rem);
+ background: var(--color-dark-grey-50);
+ }
+
+ &__content > div:first-child {
+ margin-top: 0;
+ }
+
+ &__content__lists {
+ height: 100%;
+ width: 100%;
+ padding: 1rem;
+ overflow: overlay;
+ }
+ }
+}
+
+.function-test-suite-editor {
+ height: 100%;
+ width: 100%;
+
+ &__content {
+ height: 100%;
+ background: var(--color-dark-grey-50);
+ }
+
+ &__doc {
+ &__textarea {
+ max-width: 100%;
+ height: 100% !important;
+ }
+ }
+}
+
+.function-test-editor {
+ height: 100%;
+ width: 100%;
+
+ &__doc {
+ height: 15rem;
+ margin-top: 1rem;
+ }
+
+ &__content {
+ height: 100%;
+ background: var(--color-dark-grey-50);
+ }
+}
diff --git a/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts b/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts
index 54cf7ccabd..d2be37ccf5 100644
--- a/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts
+++ b/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts
@@ -68,7 +68,10 @@ import {
import type { INTERNAL__UnknownFunctionActivator } from '../../../graph/metamodel/pure/packageableElements/function/INTERNAL__UnknownFunctionActivator.js';
import type { SnowflakeApp } from '../../../graph/metamodel/pure/packageableElements/function/SnowflakeApp.js';
import { observe_SnowflakeAppDeploymentConfiguration } from './DSL_FunctionActivatorObserverHelper.js';
-import type { FunctionTest } from '../../../graph/metamodel/pure/packageableElements/function/test/FunctionTest.js';
+import type {
+ FunctionParameterValue,
+ FunctionTest,
+} from '../../../graph/metamodel/pure/packageableElements/function/test/FunctionTest.js';
import type { FunctionTestSuite } from '../../../graph/metamodel/pure/packageableElements/function/test/FunctionTestSuite.js';
import {
observe_AtomicTest,
@@ -460,6 +463,16 @@ export const observe_Association = skipObserved(
);
// ------------------------------------- Function -------------------------------------
+export const observe_FunctionParameterValue = skipObserved(
+ (metamodel: FunctionParameterValue): FunctionParameterValue => {
+ makeObservable(metamodel, {
+ name: observable,
+ value: observable.ref,
+ hashCode: computed,
+ });
+ return metamodel;
+ },
+);
export const observe_FunctionTest = skipObserved(
(metamodel: FunctionTest): FunctionTest => {
@@ -469,6 +482,7 @@ export const observe_FunctionTest = skipObserved(
assertions: observable,
hashCode: computed,
});
+ metamodel.parameters?.forEach(observe_FunctionParameterValue);
metamodel.assertions.forEach(observe_TestAssertion);
return metamodel;
},
@@ -516,6 +530,7 @@ export const observe_ConcreteFunctionDefinition = skipObservedWithContext(
expressionSequence: observable.ref, // only observe the reference, the object itself is not observed
stereotypes: observable,
taggedValues: observable,
+ tests: observable,
_elementHashCode: override,
});
diff --git a/packages/legend-graph/src/graph-manager/helpers/DSL_Data_GraphManagerHelper.ts b/packages/legend-graph/src/graph-manager/helpers/DSL_Data_GraphManagerHelper.ts
index d2e864b2f1..9c77034018 100644
--- a/packages/legend-graph/src/graph-manager/helpers/DSL_Data_GraphManagerHelper.ts
+++ b/packages/legend-graph/src/graph-manager/helpers/DSL_Data_GraphManagerHelper.ts
@@ -30,7 +30,9 @@ export const getNullableTestable = (
graph: PureModel,
plugins: PureGraphManagerPlugin[],
): Testable | undefined =>
- graph.ownTestables.find(
+ // TODO: REMOVE functions once function test runner has been completed in backend
+ // ...this.ownFunctions,
+ [...graph.ownTestables, ...graph.ownFunctions].find(
(e) => e instanceof PackageableElement && e.path === id,
) ??
plugins
diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts
index 4b45cb7f21..e99fa007a9 100644
--- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts
+++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts
@@ -2211,6 +2211,7 @@ export class V1_PureGraphManager extends AbstractPureGraphManager {
graph,
this.pluginManager.getPureGraphManagerPlugins(),
),
+ `Unable to find testable from id`,
);
if (!testable) {
return undefined;