diff --git a/legend-engine-config/legend-engine-server/pom.xml b/legend-engine-config/legend-engine-server/pom.xml index 39f78ba9a37..92cde0224ef 100644 --- a/legend-engine-config/legend-engine-server/pom.xml +++ b/legend-engine-config/legend-engine-server/pom.xml @@ -994,6 +994,11 @@ legend-engine-test-runner-mapping runtime + + org.finos.legend.engine + legend-engine-test-runner-function + runtime + diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/HelperFunctionBuilder.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/HelperFunctionBuilder.java new file mode 100644 index 00000000000..1546c73110a --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/HelperFunctionBuilder.java @@ -0,0 +1,150 @@ +// Copyright 2023 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. + +package org.finos.legend.engine.language.pure.compiler.toPureGraph; + +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.impl.factory.Lists; +import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.data.EmbeddedDataFirstPassBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.TestBuilderHelper; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.assertion.TestAssertionFirstPassBuilder; +import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Function; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.ParameterValue; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.StoreTestData; +import org.finos.legend.engine.shared.core.operational.Assert; +import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; +import org.finos.legend.pure.generated.Root_meta_external_store_model_ModelStore_Impl; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTest; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTestSuite; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTestSuite_Impl; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTest_Impl; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_ParameterValue; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_ParameterValue_Impl; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_StoreTestData; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_StoreTestData_Impl; +import org.finos.legend.pure.generated.Root_meta_pure_test_AtomicTest; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition; + +public class HelperFunctionBuilder +{ + public static void processFunctionSuites(Function func, ConcreteFunctionDefinition metamodelFunction, CompileContext compileContext, ProcessingContext ctx) + { + if (func.tests != null && !func.tests.isEmpty()) + { + TestBuilderHelper.validateTestSuiteIdsList(func.tests, func.sourceInformation); + metamodelFunction._tests(ListIterate.collect(func.tests, suite -> buildFunctionTestSuites(metamodelFunction, suite, compileContext, ctx))); + } + } + + static org.finos.legend.pure.m3.coreinstance.meta.pure.test.Test buildFunctionTestSuites(ConcreteFunctionDefinition metamodelFunction, org.finos.legend.engine.protocol.pure.v1.model.test.Test test, CompileContext compileContext, ProcessingContext processingContext + ) + { + if (test instanceof FunctionTestSuite) + { + // validate tests and test suite ids + FunctionTestSuite testSuite = (FunctionTestSuite) test; + TestBuilderHelper.validateNonEmptySuite(testSuite); + TestBuilderHelper.validateTestIds(testSuite.tests, testSuite.sourceInformation); + Root_meta_legend_function_metamodel_FunctionTestSuite metamodelSuite = new Root_meta_legend_function_metamodel_FunctionTestSuite_Impl("",null, compileContext.pureModel.getClass("meta::legend::function::metamodel::FunctionTestSuite")); + if (testSuite.testData != null && !testSuite.testData.isEmpty()) + { + TestBuilderHelper.validateIds(ListIterate.collect(testSuite.testData, testData -> testData.store), testSuite.sourceInformation, "Multiple test data found for stores"); + RichIterable runtimes; + // TODO: we can remove some of these checks once we support these use cases + try + { + runtimes = org.finos.legend.pure.generated.core_pure_corefunctions_metaExtension.Root_meta_pure_functions_meta_extractRuntimesFromFunctionDefinition_FunctionDefinition_1__Runtime_MANY_(metamodelFunction, compileContext.pureModel.getExecutionSupport()); + } + catch (Exception error) + { + throw new EngineException("Unable to extract runtime from function which test data is provided for. Test Data is only supported to be provided for runtimes", testSuite.sourceInformation, EngineErrorType.COMPILATION, error); + } + if (runtimes.isEmpty()) + { + throw new EngineException("Function test data requires a function to have one runtime: No runtimes found in function." + metamodelFunction.getName(), testSuite.sourceInformation, EngineErrorType.COMPILATION); + } + if (runtimes.size() > 1) + { + throw new EngineException("Function test data requires a function to have one runtime. Found " + runtimes.size() + " runtimes in function " + metamodelFunction.getName(), testSuite.sourceInformation, EngineErrorType.COMPILATION); + } + org.finos.legend.pure.generated.Root_meta_core_runtime_Runtime runtime = runtimes.getOnly(); + metamodelSuite._testData(ListIterate.collect(testSuite.testData, storeData -> buildFunctionTestData(runtime, storeData, compileContext, processingContext))); + } + metamodelSuite + ._id(testSuite.id) + ._tests(ListIterate.collect(testSuite.tests, unitTest -> (Root_meta_pure_test_AtomicTest) buildFunctionTestSuites(metamodelFunction, unitTest, compileContext, processingContext))) + ._testable(metamodelFunction); + return metamodelSuite; + } + else if (test instanceof FunctionTest) + { + FunctionTest functionTest = (FunctionTest) test; + Root_meta_legend_function_metamodel_FunctionTest metamodelTest = new Root_meta_legend_function_metamodel_FunctionTest_Impl("",null, compileContext.pureModel.getClass("meta::legend::function::metamodel::FunctionTest")) + ._id(functionTest.id); + if (functionTest.parameters != null && !functionTest.parameters.isEmpty()) + { + metamodelTest._parameters(ListIterate.collect(functionTest.parameters, param -> processFunctionTestParameterValue(param, compileContext))); + } + TestBuilderHelper.validateNonEmptyTest(functionTest); + if (functionTest.assertions.size() > 1) + { + throw new EngineException("Function test only support one assertion", test.sourceInformation, EngineErrorType.COMPILATION); + } + metamodelTest._assertions(ListIterate.collect(functionTest.assertions, assertion -> assertion.accept(new TestAssertionFirstPassBuilder(compileContext, processingContext)))); + return metamodelTest; + } + return null; + } + + private static Root_meta_legend_function_metamodel_StoreTestData buildFunctionTestData(org.finos.legend.pure.generated.Root_meta_core_runtime_Runtime runtime, StoreTestData storeTestData, CompileContext compileContext, ProcessingContext ctx) + { + Root_meta_legend_function_metamodel_StoreTestData_Impl metamodelStoreTestData = new Root_meta_legend_function_metamodel_StoreTestData_Impl("", null, compileContext.pureModel.getClass("meta::legend::function::metamodel::StoreTestData")); + org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store resolvedStore = null; + if (storeTestData.store.equals("ModelStore")) + { + resolvedStore = new Root_meta_external_store_model_ModelStore_Impl(""); + } + else + { + resolvedStore = compileContext.resolveStore(storeTestData.store, storeTestData.sourceInformation); + } + try + { + org.finos.legend.pure.generated.Root_meta_core_runtime_Connection connection = runtime.connectionByElement(resolvedStore, compileContext.pureModel.getExecutionSupport()); + Assert.assertTrue(connection != null, () -> "connection not found"); + } + catch (Exception exception) + { + // throw new EngineException("Store '" + storeTestData.store + "' not specified in the runtime in the function", storeTestData.sourceInformation, EngineErrorType.COMPILATION, exception); + } + metamodelStoreTestData._data(storeTestData.data.accept(new EmbeddedDataFirstPassBuilder(compileContext, ctx))); + metamodelStoreTestData._store(resolvedStore); + metamodelStoreTestData._doc(storeTestData.doc); + return metamodelStoreTestData; + } + + private static Root_meta_legend_function_metamodel_ParameterValue processFunctionTestParameterValue(ParameterValue parameterValue, CompileContext context) + { + Root_meta_legend_function_metamodel_ParameterValue pureParameterValue = new Root_meta_legend_function_metamodel_ParameterValue_Impl("", null, context.pureModel.getClass("meta::legend::function::metamodel::ParameterValue")); + pureParameterValue._name(parameterValue.name); + pureParameterValue._value(Lists.immutable.with(parameterValue.value.accept(new ValueSpecificationBuilder(context, Lists.mutable.empty(), new ProcessingContext(""))))); + return pureParameterValue; + } + + +} diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/HelperMappingBuilder.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/HelperMappingBuilder.java index 0b7db947fb2..9f81ca7100d 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/HelperMappingBuilder.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/HelperMappingBuilder.java @@ -25,6 +25,7 @@ import org.eclipse.collections.impl.list.mutable.FastList; import org.eclipse.collections.impl.utility.ListIterate; import org.finos.legend.engine.language.pure.compiler.toPureGraph.handlers.Handlers; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.TestBuilderHelper; import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Multiplicity; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.AssociationMapping; @@ -51,6 +52,9 @@ import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.Variable; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.Lambda; import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; +import org.finos.legend.pure.generated.Root_meta_external_store_model_ModelStore_Impl; +import org.finos.legend.pure.generated.Root_meta_pure_data_StoreTestData; +import org.finos.legend.pure.generated.Root_meta_pure_data_StoreTestData_Impl; import org.finos.legend.pure.generated.Root_meta_pure_mapping_EnumValueMapping_Impl; import org.finos.legend.pure.generated.Root_meta_pure_mapping_EnumerationMapping_Impl; import org.finos.legend.pure.generated.Root_meta_pure_mapping_MappingClass_Impl; @@ -60,7 +64,6 @@ import org.finos.legend.pure.generated.Root_meta_pure_mapping_aggregationAware_GroupByFunctionSpecification_Impl; import org.finos.legend.pure.generated.Root_meta_pure_mapping_metamodel_MappingTestSuite; import org.finos.legend.pure.generated.Root_meta_pure_mapping_metamodel_MappingTestSuite_Impl; -import org.finos.legend.pure.generated.Root_meta_external_store_model_ModelStore_Impl; import org.finos.legend.pure.generated.Root_meta_pure_mapping_xStore_XStoreAssociationImplementation_Impl; import org.finos.legend.pure.generated.Root_meta_pure_metamodel_function_LambdaFunction_Impl; import org.finos.legend.pure.generated.Root_meta_pure_metamodel_function_property_Property_Impl; @@ -68,8 +71,6 @@ import org.finos.legend.pure.generated.Root_meta_pure_metamodel_type_generics_GenericType_Impl; import org.finos.legend.pure.generated.Root_meta_pure_metamodel_valuespecification_VariableExpression_Impl; import org.finos.legend.pure.generated.Root_meta_pure_test_AtomicTest; -import org.finos.legend.pure.generated.Root_meta_pure_data_StoreTestData; -import org.finos.legend.pure.generated.Root_meta_pure_data_StoreTestData_Impl; import org.finos.legend.pure.m3.coreinstance.meta.pure.mapping.AssociationImplementation; import org.finos.legend.pure.m3.coreinstance.meta.pure.mapping.InstanceSetImplementation; import org.finos.legend.pure.m3.coreinstance.meta.pure.mapping.Mapping; @@ -94,7 +95,6 @@ import org.finos.legend.pure.m3.navigation.PackageableElement.PackageableElement; import org.finos.legend.pure.m4.coreinstance.SourceInformation; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -510,50 +510,21 @@ public static Test processMappingTestAndTestSuite(org.finos.legend.engine.protoc if (test instanceof MappingTestSuite) { // validate tests and test suite ids - MappingTestSuite testSuite = (MappingTestSuite) test; - if (testSuite.tests == null || testSuite.tests.isEmpty()) - { - throw new EngineException("Mapping TestSuites should have at least 1 test", testSuite.sourceInformation, EngineErrorType.COMPILATION); - } - List testIds = ListIterate.collect(testSuite.tests, t -> t.id); - List duplicateTestIds = testIds.stream().filter(e -> Collections.frequency(testIds, e) > 1).distinct().collect(Collectors.toList()); - if (!duplicateTestIds.isEmpty()) - { - throw new EngineException("Multiple tests found with ids : '" + String.join(",", duplicateTestIds) + "'", testSuite.sourceInformation, EngineErrorType.COMPILATION); - } - if (test instanceof MappingTestSuite) - { - - MappingTestSuite queryTestSuite = (MappingTestSuite) test; - Root_meta_pure_mapping_metamodel_MappingTestSuite compiledMappingSuite = new Root_meta_pure_mapping_metamodel_MappingTestSuite_Impl("", null, context.pureModel.getClass("meta::pure::mapping::metamodel::MappingTestSuite")); - - return compiledMappingSuite._id(queryTestSuite.id) - ._query(HelperValueSpecificationBuilder.buildLambda(queryTestSuite.func, context)) - ._tests(ListIterate.collect(queryTestSuite.tests, unitTest -> (Root_meta_pure_test_AtomicTest) HelperMappingBuilder.processMappingTestAndTestSuite(unitTest, pureMapping, context))) - ._testable(pureMapping); - } - else - { - throw new EngineException("Unsupported Mapping Test Suite", testSuite.sourceInformation, EngineErrorType.COMPILATION); - } + MappingTestSuite queryTestSuite = (MappingTestSuite) test; + TestBuilderHelper.validateNonEmptySuite(queryTestSuite); + TestBuilderHelper.validateTestIds(queryTestSuite.tests, queryTestSuite.sourceInformation); + Root_meta_pure_mapping_metamodel_MappingTestSuite compiledMappingSuite = new Root_meta_pure_mapping_metamodel_MappingTestSuite_Impl("", null, context.pureModel.getClass("meta::pure::mapping::metamodel::MappingTestSuite")); + return compiledMappingSuite._id(queryTestSuite.id) + ._query(HelperValueSpecificationBuilder.buildLambda(queryTestSuite.func, context)) + ._tests(ListIterate.collect(queryTestSuite.tests, unitTest -> (Root_meta_pure_test_AtomicTest) HelperMappingBuilder.processMappingTestAndTestSuite(unitTest, pureMapping, context))) + ._testable(pureMapping); } else if (test instanceof MappingTest) { MappingTest mappingTest = (MappingTest) test; Root_meta_pure_test_AtomicTest pureMappingTest = (Root_meta_pure_test_AtomicTest) TestCompilerHelper.compilePureMappingTests(mappingTest, context, new ProcessingContext("Mapping Test '" + mappingTest.id + "' Second Pass")); - if (mappingTest.assertions == null || mappingTest.assertions.isEmpty()) - { - throw new EngineException("Mapping Tests should have at least 1 assert", mappingTest.sourceInformation, EngineErrorType.COMPILATION); - } - - List assertionIds = ListIterate.collect(mappingTest.assertions, a -> a.id); - List duplicateAssertionIds = assertionIds.stream().filter(e -> Collections.frequency(assertionIds, e) > 1).distinct().collect(Collectors.toList()); - - if (!duplicateAssertionIds.isEmpty()) - { - throw new EngineException("Multiple assertions found with ids : '" + String.join(",", duplicateAssertionIds) + "'", mappingTest.sourceInformation, EngineErrorType.COMPILATION); - } - + TestBuilderHelper.validateNonEmptyTest(mappingTest); + TestBuilderHelper.validateAssertionIds(mappingTest.assertions, mappingTest.sourceInformation); pureMappingTest._assertions(ListIterate.collect(mappingTest.assertions, assertion -> context.getCompilerExtensions().getExtraTestAssertionProcessors().stream() .map(processor -> processor.value(assertion, context, new ProcessingContext("Test Assertion '" + assertion.id + "'"))) .filter(Objects::nonNull) diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementFourthPassBuilder.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementFourthPassBuilder.java index 40eafa5e92b..1fbd53c8dc0 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementFourthPassBuilder.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementFourthPassBuilder.java @@ -18,6 +18,7 @@ import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.impl.factory.Lists; import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.test.TestBuilderHelper; import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.PackageableElementVisitor; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.PackageableConnection; @@ -169,13 +170,7 @@ public PackageableElement visit(Mapping mapping) } if (mapping.testSuites != null) { - List testSuiteIds = ListIterate.collect(mapping.testSuites, suite -> suite.id); - List duplicateTestSuiteIds = testSuiteIds.stream().filter(e -> Collections.frequency(testSuiteIds, e) > 1).distinct().collect(Collectors.toList()); - - if (!duplicateTestSuiteIds.isEmpty()) - { - throw new EngineException("Multiple testSuites found with ids : '" + String.join(",", duplicateTestSuiteIds) + "'", mapping.sourceInformation, EngineErrorType.COMPILATION); - } + TestBuilderHelper.validateTestSuiteIdsList(mapping.testSuites, mapping.sourceInformation); pureMapping._tests(ListIterate.collect(mapping.testSuites, suite -> HelperMappingBuilder.processMappingTestAndTestSuite(suite, pureMapping, this.context))); } return pureMapping; diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementSecondPassBuilder.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementSecondPassBuilder.java index 9c71c619561..358dd672e3b 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementSecondPassBuilder.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PackageableElementSecondPassBuilder.java @@ -180,7 +180,9 @@ public PackageableElement visit(Function function) FunctionType fType = ((FunctionType) targetFunc._classifierGenericType()._typeArguments().getFirst()._rawType()); HelperModelBuilder.checkCompatibility(this.context, body.getLast()._genericType()._rawType(), body.getLast()._multiplicity(), fType._returnType()._rawType(), fType._returnMultiplicity(), "Error in function '" + packageString + "'", function.body.get(function.body.size() - 1).sourceInformation); ctx.pop(); - return targetFunc._expressionSequence(body); + targetFunc._expressionSequence(body); + HelperFunctionBuilder.processFunctionSuites(function, targetFunc, this.context, ctx); + return targetFunc; } @Override diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PureModel.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PureModel.java index 3cd223becc8..ca2511ac833 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PureModel.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/PureModel.java @@ -42,6 +42,7 @@ import org.finos.legend.engine.language.pure.compiler.toPureGraph.validator.ClassValidator; import org.finos.legend.engine.language.pure.compiler.toPureGraph.validator.EnumerationValidator; import org.finos.legend.engine.language.pure.compiler.toPureGraph.validator.ProfileValidator; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.validator.FunctionValidator; import org.finos.legend.engine.language.pure.compiler.toPureGraph.validator.PureModelContextDataValidator; import org.finos.legend.engine.protocol.pure.v1.model.SourceInformation; import org.finos.legend.engine.protocol.pure.v1.model.context.AlloySDLC; @@ -287,6 +288,7 @@ public PureModel(PureModelContextData pureModelContextData, CompilerExtensions e new EnumerationValidator().validate(this, pureModelContextData); new ClassValidator().validate(this, pureModelContextData); new AssociationValidator().validate(this, pureModelContextData); + new FunctionValidator().validate(getContext(), pureModelContextData); new org.finos.legend.engine.language.pure.compiler.toPureGraph.validator.MappingValidator().validate(this, pureModelContextData, extensions); extraPostValidators.forEach(validator -> validator.value(this, pureModelContextData)); long postValidationFinished = System.currentTimeMillis(); diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/test/TestBuilderHelper.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/test/TestBuilderHelper.java new file mode 100644 index 00000000000..80a2c785545 --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/test/TestBuilderHelper.java @@ -0,0 +1,70 @@ +// Copyright 2020 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. + +package org.finos.legend.engine.language.pure.compiler.toPureGraph.test; + +import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.protocol.pure.v1.model.SourceInformation; +import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; +import org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest; +import org.finos.legend.engine.protocol.pure.v1.model.test.TestSuite; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.TestAssertion; +import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class TestBuilderHelper +{ + public static void validateNonEmptySuite(T suite) + { + if (suite.tests == null || suite.tests.isEmpty()) + { + throw new EngineException("TestSuites should have at least 1 test", suite.sourceInformation, EngineErrorType.COMPILATION); + } + } + + public static void validateNonEmptyTest(T test) + { + if (test.assertions == null || test.assertions.isEmpty()) + { + throw new EngineException("Tests should have at least 1 assert", test.sourceInformation, EngineErrorType.COMPILATION); + } + } + + public static void validateTestSuiteIdsList(List suites, SourceInformation sourceInformation) + { + validateIds(ListIterate.collect(suites, suite -> suite.id), sourceInformation, "Multiple testSuites found with ids"); + } + + public static void validateTestIds(List tests, SourceInformation sourceInformation) + { + validateIds(ListIterate.collect(tests, test -> test.id), sourceInformation, "Multiple tests found with ids"); + } + + public static void validateAssertionIds(List assertions, SourceInformation sourceInformation) + { + validateIds(ListIterate.collect(assertions, a -> a.id), sourceInformation, "Multiple assertions found with ids"); + } + + public static void validateIds(List ids, SourceInformation sourceInformation, String message) + { + List duplicateIds = ids.stream().filter(e -> Collections.frequency(ids, e) > 1).distinct().collect(Collectors.toList()); + if (!duplicateIds.isEmpty()) + { + throw new EngineException(message + " : '" + String.join(",", duplicateIds) + "'", sourceInformation, EngineErrorType.COMPILATION); + } + } +} diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/validator/FunctionValidator.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/validator/FunctionValidator.java new file mode 100644 index 00000000000..4f08d82d5bc --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/main/java/org/finos/legend/engine/language/pure/compiler/toPureGraph/validator/FunctionValidator.java @@ -0,0 +1,103 @@ +// Copyright 2023 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. + +package org.finos.legend.engine.language.pure.compiler.toPureGraph.validator; + +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.CompileContext; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.HelperModelBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.protocol.pure.v1.model.SourceInformation; +import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Function; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite; +import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTest; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTestSuite; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_ParameterValue; +import org.finos.legend.pure.generated.Root_meta_pure_test_AtomicTest; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.type.FunctionType; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.valuespecification.InstanceValue; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.valuespecification.VariableExpression; + +import java.util.*; +import java.util.stream.Collectors; + +public class FunctionValidator +{ + + public void validate(CompileContext compileContext, PureModelContextData pureModelContextData) + { + ListIterate.selectInstancesOf(pureModelContextData.getElements(), Function.class) + .forEach(_func -> validateFunction(_func, compileContext)); + } + + public void validateFunction(Function func, CompileContext compileContext) + { + if (func.tests != null && !func.tests.isEmpty()) + { + PureModel pureModel = compileContext.pureModel; + String packageString = pureModel.buildPackageString(func._package, HelperModelBuilder.getSignature(func)); + org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition targetFunc = pureModel.getConcreteFunctionDefinition(packageString, func.sourceInformation); + for (org.finos.legend.pure.m3.coreinstance.meta.pure.test.Test test: targetFunc._tests()) + { + Root_meta_legend_function_metamodel_FunctionTestSuite metamodelSuite = (Root_meta_legend_function_metamodel_FunctionTestSuite) test; + FunctionTestSuite protocolSuite = ListIterate.detect(func.tests, t -> t.id.equals(metamodelSuite._id())); + for (Root_meta_pure_test_AtomicTest atomicTest: metamodelSuite._tests()) + { + Root_meta_legend_function_metamodel_FunctionTest functionTest = (Root_meta_legend_function_metamodel_FunctionTest) atomicTest; + RichIterable parameters = ((FunctionType)targetFunc._classifierGenericType()._typeArguments().getOnly()._rawType())._parameters(); + FunctionTest protocolTest = (FunctionTest) ListIterate.detect(protocolSuite.tests, t -> t.id.equals(functionTest._id())); + validateFunctionTestParameterValues(compileContext,(List) functionTest._parameters().toList(), parameters, protocolTest.sourceInformation); + } + } + } + } + + public static void validateFunctionTestParameterValues(CompileContext context, List parameterValues, RichIterable parameters, SourceInformation sourceInformation) + { + Set processedParams = parameterValues.stream().map(e -> e._name()).collect(Collectors.toSet()); + for (VariableExpression param : parameters) + { + Optional parameterValue = ListIterate.detectOptional(parameterValues, p -> p._name().equals(param._name())); + + if (parameterValue.isPresent()) + { + InstanceValue paramValue = (InstanceValue) parameterValue.get()._value().getOnly(); + org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.multiplicity.Multiplicity paramMultiplicity = param._multiplicity(); + org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.multiplicity.Multiplicity paramValueMultiplicity = paramValue._multiplicity(); + if (!"Nil".equals(paramValue._genericType()._rawType())) + { + HelperModelBuilder.checkCompatibility(context, paramValue._genericType()._rawType(), paramValueMultiplicity, param._genericType()._rawType(), paramMultiplicity, "Parameter value type does not match with parameter type for parameter: '" + param._name() + "'", sourceInformation); + } + } + else + { + if (param._multiplicity()._lowerBound() != null && param._multiplicity()._lowerBound()._value() != null && param._multiplicity()._lowerBound()._value() != 0) + { + throw new EngineException("Parameter value required for parameter: '" + param._name() + "'", sourceInformation, EngineErrorType.COMPILATION); + } + } + processedParams.remove(param._name()); + } + if (!processedParams.isEmpty()) + { + throw new EngineException("Parameter values not found in function parameter: " + String.join(",", processedParams), sourceInformation, EngineErrorType.COMPILATION); + } + } + +} diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/test/java/org/finos/legend/engine/language/pure/compiler/test/fromGrammar/TestFunctionCompilationFromGrammar.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/test/java/org/finos/legend/engine/language/pure/compiler/test/fromGrammar/TestFunctionCompilationFromGrammar.java new file mode 100644 index 00000000000..8e62dbcbfa6 --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/test/java/org/finos/legend/engine/language/pure/compiler/test/fromGrammar/TestFunctionCompilationFromGrammar.java @@ -0,0 +1,342 @@ +// Copyright 2020 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. + +package org.finos.legend.engine.language.pure.compiler.test.fromGrammar; + +import org.junit.Test; + +import static org.finos.legend.engine.language.pure.compiler.test.TestCompilationFromGrammar.TestCompilationFromGrammarTestSuite.test; + +public class TestFunctionCompilationFromGrammar +{ + + @Test + public void testFunctionTest() + { + + test("function model::MyFunc(): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " testSuite_1:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testFail:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " },\n" + + " testPass:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n"); + + test("function model::MyFunc(): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " testSuite_1:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " },\n" + + " testDuplicate:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n", "COMPILATION error at [6:3-43:3]: Multiple tests found with ids : 'testDuplicate'"); + + test("function model::MyFunc(): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " duplicateSuite:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " duplicateSuite:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n", "COMPILATION error at [1:1-50:1]: Multiple testSuites found with ids : 'duplicateSuite'"); + + + test("function model::MyFunc(firstName: String[1]): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " duplicateSuite:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " parameters:\n" + + " [\n" + + " firstName = 'Nicole'\n" + + " ]\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n"); + + test("function model::MyFunc(firstName: String[1]): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " duplicateSuite:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n", "COMPILATION error at [10:7-25:7]: Parameter value required for parameter: 'firstName'"); + + test("function model::MyFunc(firstName: String[1]): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " duplicateSuite:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n", "COMPILATION error at [10:7-25:7]: Parameter value required for parameter: 'firstName'"); + + test("function model::MyFunc(): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " duplicateSuite:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " parameters: [" + + " notFound = 'xx' " + + " ]" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n", "COMPILATION error at [10:7-25:7]: Parameter values not found in function parameter: notFound"); + + test("function model::MyFunc(): String[1]\n" + + "{\n" + + " ''\n" + + "}\n" + + "[\n" + + " duplicateSuite:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testDuplicate:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n", "COMPILATION error at [10:7-15:7]: Tests should have at least 1 assert"); + + + test("function model::Hello(name: String[1]): String[1]\n" + + "{\n" + + " 'Hello!. My name is ' + $name + '.';\n" + + "}\n" + + "[\n" + + " testSuite_1:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testFail:\n" + + " {\n" + + " parameters: \n" + + " [\n" + + " name = 'John'\n" + + " ]\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualTo\n" + + " #{\n" + + " expected: 'Hello!. My name is John.';\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n"); + } + +} diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/test/java/org/finos/legend/engine/language/pure/compiler/test/fromGrammar/TestMappingCompilationFromGrammar.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/test/java/org/finos/legend/engine/language/pure/compiler/test/fromGrammar/TestMappingCompilationFromGrammar.java index 020145baa8c..be839c06558 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/test/java/org/finos/legend/engine/language/pure/compiler/test/fromGrammar/TestMappingCompilationFromGrammar.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-compiler/src/test/java/org/finos/legend/engine/language/pure/compiler/test/fromGrammar/TestMappingCompilationFromGrammar.java @@ -2250,7 +2250,7 @@ public void testMappingTestSuite() " ]\n" + ")\n" + "\n", - "COMPILATION error at [30:5-35:5]: Mapping TestSuites should have at least 1 test"); + "COMPILATION error at [30:5-35:5]: TestSuites should have at least 1 test"); test("###Pure\n" + "Class test::model\n" + "{\n" + diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar-api/src/test/java/org/finos/legend/engine/language/pure/grammar/api/test/TestGrammarToJsonApi.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar-api/src/test/java/org/finos/legend/engine/language/pure/grammar/api/test/TestGrammarToJsonApi.java index a059a7ad820..70399d6423d 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar-api/src/test/java/org/finos/legend/engine/language/pure/grammar/api/test/TestGrammarToJsonApi.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar-api/src/test/java/org/finos/legend/engine/language/pure/grammar/api/test/TestGrammarToJsonApi.java @@ -83,7 +83,7 @@ public void testLambdaParsingError() @Test public void testMixedParsingErrors() { - test("{\"code\": \"Class A {,\", \"isolatedLambdas\": {\"good\": \"|'good'\", \"bad\": \"|,\"}}", "{\"codeError\":{\"message\":\"Unexpected token ','. Valid alternatives: ['import', 'Class', 'Association', 'Profile', 'Enum', 'Measure', 'function', 'extends', 'stereotypes', 'tags', 'Error', 'Warn', 'native', 'projects', 'as', 'composite', 'shared', 'none', 'all', 'let', 'allVersions', 'allVersionsInRange', 'toBytes', '(', '<']\",\"sourceInformation\":{\"endColumn\":10,\"endLine\":1,\"sourceId\":\"\",\"startColumn\":10,\"startLine\":1}},\"isolatedLambdas\":{\"lambdaErrors\":{\"bad\":{\"message\":\"Unexpected token ','. Valid alternatives: ['import', 'Class', 'Association', 'Profile', 'Enum', 'Measure', 'function', 'extends', 'stereotypes', 'tags', 'Error', 'Warn', 'native', 'projects', 'as', 'composite', 'shared', 'none', 'all', 'let', 'allVersions', 'allVersionsInRange', 'toBytes', '!', '[', '(', '$', '^', '|', '@', '+', '-']\",\"sourceInformation\":{\"endColumn\":2,\"endLine\":1,\"sourceId\":\"bad\",\"startColumn\":2,\"startLine\":1}}},\"lambdas\":{\"good\":{\"_type\":\"lambda\",\"body\":[{\"_type\":\"string\",\"sourceInformation\":{\"endColumn\":7,\"endLine\":1,\"sourceId\":\"good\",\"startColumn\":2,\"startLine\":1},\"value\":\"good\"}],\"parameters\":[],\"sourceInformation\":{\"endColumn\":7,\"endLine\":1,\"sourceId\":\"good\",\"startColumn\":1,\"startLine\":1}}}},\"renderStyle\":\"STANDARD\"}"); + test("{\"code\": \"Class A {},\", \"isolatedLambdas\": {\"good\": \"|'good'\", \"bad\": \"|,\"}}", "{\"codeError\":{\"message\":\"Unexpected token\",\"sourceInformation\":{\"endColumn\":11,\"endLine\":1,\"sourceId\":\"\",\"startColumn\":11,\"startLine\":1}},\"isolatedLambdas\":{\"lambdaErrors\":{\"bad\":{\"message\":\"Unexpected token ','. Valid alternatives: ['import', 'Class', 'Association', 'Profile', 'Enum', 'Measure', 'function', 'extends', 'stereotypes', 'tags', 'Error', 'Warn', 'native', 'projects', 'as', 'composite', 'shared', 'none', 'data', 'tests', 'parameters', 'asserts', 'store', 'all', 'let', 'allVersions', 'allVersionsInRange', 'toBytes', '!', '[', '(', '$', '^', '|', '@', '+', '-']\",\"sourceInformation\":{\"endColumn\":2,\"endLine\":1,\"sourceId\":\"bad\",\"startColumn\":2,\"startLine\":1}}},\"lambdas\":{\"good\":{\"_type\":\"lambda\",\"body\":[{\"_type\":\"string\",\"sourceInformation\":{\"endColumn\":7,\"endLine\":1,\"sourceId\":\"good\",\"startColumn\":2,\"startLine\":1},\"value\":\"good\"}],\"parameters\":[],\"sourceInformation\":{\"endColumn\":7,\"endLine\":1,\"sourceId\":\"good\",\"startColumn\":1,\"startLine\":1}}}},\"renderStyle\":\"STANDARD\"}"); } @Test diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainLexerGrammar.g4 b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainLexerGrammar.g4 index 890e9ffd021..bf66e24ad28 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainLexerGrammar.g4 +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainLexerGrammar.g4 @@ -35,3 +35,9 @@ AS: 'as'; AGGREGATION_TYPE_COMPOSITE: 'composite'; AGGREGATION_TYPE_SHARED: 'shared'; AGGREGATION_TYPE_NONE: 'none'; + +FUNCTION_TEST_DATA: 'data'; +FUNCTION_SUITE_TESTS: 'tests'; +FUNCTION_TEST_PARAMETERS: 'parameters'; +FUNCTION_TEST_ASSERTS: 'asserts'; +FUNCTION_TEST_DATA_STORE: 'store'; diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainParserGrammar.g4 b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainParserGrammar.g4 index 9532405f5ec..28d696903dd 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainParserGrammar.g4 +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/antlr4/org/finos/legend/engine/language/pure/grammar/from/antlr4/domain/DomainParserGrammar.g4 @@ -20,6 +20,8 @@ identifier: VALID_STRING | STRING | NATIVE | PROJECTS | AS | CONSTRAINT_ENFORCEMENT_LEVEL_ERROR | CONSTRAINT_ENFORCEMENT_LEVEL_WARN | AGGREGATION_TYPE_COMPOSITE | AGGREGATION_TYPE_SHARED | AGGREGATION_TYPE_NONE + | FUNCTION_TEST_DATA | FUNCTION_SUITE_TESTS | FUNCTION_TEST_PARAMETERS + | FUNCTION_TEST_ASSERTS | FUNCTION_TEST_DATA_STORE ; @@ -170,9 +172,47 @@ functionDefinition: FUNCTION stereotypes? taggedValu BRACE_OPEN codeBlock BRACE_CLOSE + functionTestSuites? +; +functionTestSuites: BRACKET_OPEN + functionTestSuite (COMMA functionTestSuite)* + BRACKET_CLOSE +; +functionTestSuite: identifier COLON BRACE_OPEN (testData | functionTestSuiteTests )* BRACE_CLOSE +; +testData: FUNCTION_TEST_DATA COLON BRACKET_OPEN (storeTestData ( COMMA storeTestData )*)? BRACKET_CLOSE SEMI_COLON +; +storeTestData: BRACE_OPEN + ( + storePointer | + storeData + )* + BRACE_CLOSE +; +storePointer: FUNCTION_TEST_DATA_STORE COLON qualifiedName SEMI_COLON +; +storeData: FUNCTION_TEST_DATA COLON embeddedData SEMI_COLON +; +embeddedData: identifier ISLAND_OPEN (embeddedDataContent)* +; +embeddedDataContent: ISLAND_START | ISLAND_BRACE_OPEN | ISLAND_CONTENT | ISLAND_HASH | ISLAND_BRACE_CLOSE | ISLAND_END +; +functionTestSuiteTests: FUNCTION_SUITE_TESTS COLON BRACKET_OPEN (functionTestBlock ( COMMA functionTestBlock )*)? BRACKET_CLOSE +; +functionTestBlock: identifier COLON BRACE_OPEN (functionTestParameters | functionTestAsserts )* BRACE_CLOSE +; +functionTestParameters: FUNCTION_TEST_PARAMETERS COLON BRACKET_OPEN ( functionTestParameter ( COMMA functionTestParameter )* )? BRACKET_CLOSE +; +functionTestParameter: identifier EQUAL primitiveValue +; +functionTestAsserts: FUNCTION_TEST_ASSERTS COLON BRACKET_OPEN ( functionTestAssert ( COMMA functionTestAssert )* )? BRACKET_CLOSE +; +functionTestAssert: identifier COLON testAssertion +; +testAssertion: identifier ISLAND_OPEN (testAssertionContent)* +; +testAssertionContent: ISLAND_START | ISLAND_BRACE_OPEN | ISLAND_CONTENT | ISLAND_HASH | ISLAND_BRACE_CLOSE | ISLAND_END ; - - // -------------------------------------- CONSTRAINT -------------------------------------- constraints: BRACKET_OPEN diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/from/domain/DomainParseTreeWalker.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/from/domain/DomainParseTreeWalker.java index 576e88891c1..ec401d0d4bb 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/from/domain/DomainParseTreeWalker.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/from/domain/DomainParseTreeWalker.java @@ -19,7 +19,9 @@ import org.antlr.v4.runtime.RuleContext; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.atn.PredictionMode; +import org.antlr.v4.runtime.misc.Interval; import org.antlr.v4.runtime.tree.TerminalNode; +import org.checkerframework.dataflow.qual.Pure; import org.eclipse.collections.api.list.ListIterable; import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.impl.factory.Lists; @@ -35,7 +37,9 @@ import org.finos.legend.engine.language.pure.grammar.from.antlr4.graphFetchTree.GraphFetchTreeParserGrammar; import org.finos.legend.engine.language.pure.grammar.from.antlr4.navigation.NavigationLexerGrammar; import org.finos.legend.engine.language.pure.grammar.from.antlr4.navigation.NavigationParserGrammar; +import org.finos.legend.engine.language.pure.grammar.from.data.embedded.HelperEmbeddedDataGrammarParser; import org.finos.legend.engine.language.pure.grammar.from.extension.EmbeddedPureParser; +import org.finos.legend.engine.language.pure.grammar.from.test.assertion.HelperTestAssertionGrammarParser; import org.finos.legend.engine.language.pure.grammar.to.HelperValueSpecificationGrammarComposer; import org.finos.legend.engine.protocol.pure.v1.model.SourceInformation; import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; @@ -45,10 +49,12 @@ import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Class; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Constraint; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.DefaultValue; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Domain; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.EnumValue; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Enumeration; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Measure; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Multiplicity; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.ParameterValue; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Profile; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Property; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.QualifiedProperty; @@ -56,7 +62,9 @@ import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.TagPtr; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.TaggedValue; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Unit; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.StoreTestData; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.section.ImportAwareCodeSection; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.TestAssertion; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.ValueSpecification; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.Variable; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.application.AppliedFunction; @@ -416,6 +424,10 @@ private org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain variable.sourceInformation = this.walkerSourceInformation.getSourceInformation(functionVariableExpressionContext); return variable; }); + if (ctx.functionTestSuites() != null) + { + func.tests = ListIterate.collect(ctx.functionTestSuites().functionTestSuite(), this::visitFunctionSuite); + } func.returnType = ctx.functionTypeSignature().type().getText(); func.returnMultiplicity = this.buildMultiplicity(ctx.functionTypeSignature().multiplicity().multiplicityArgument()); func.sourceInformation = this.walkerSourceInformation.getSourceInformation(ctx); @@ -423,6 +435,76 @@ private org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain return func; } + private org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite visitFunctionSuite(DomainParserGrammar.FunctionTestSuiteContext functionTestSuiteContext) + { + org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite suite = new org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite(); + suite.sourceInformation = this.walkerSourceInformation.getSourceInformation(functionTestSuiteContext); + suite.id = PureGrammarParserUtility.fromIdentifier(functionTestSuiteContext.identifier()); + // data + DomainParserGrammar.TestDataContext testDataContext = PureGrammarParserUtility.validateAndExtractOptionalField(functionTestSuiteContext.testData(), "data", suite.sourceInformation); + if (testDataContext != null) + { + suite.testData = ListIterate.collect(testDataContext.storeTestData(), this::visitStoreTestData); + } + // Tests + DomainParserGrammar.FunctionTestSuiteTestsContext testSuiteTestsContext = PureGrammarParserUtility.validateAndExtractRequiredField(functionTestSuiteContext.functionTestSuiteTests(), "tests", suite.sourceInformation); + suite.tests = ListIterate.collect(testSuiteTestsContext.functionTestBlock(), this::visitFunctionTest); + return suite; + } + + private org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.StoreTestData visitStoreTestData(DomainParserGrammar.StoreTestDataContext storeDataContext) + { + StoreTestData storeTestData = new StoreTestData(); + DomainParserGrammar.StorePointerContext storePointerContext = PureGrammarParserUtility.validateAndExtractRequiredField(storeDataContext.storePointer(), "store", storeTestData.sourceInformation); + storeTestData.store = PureGrammarParserUtility.fromQualifiedName(storePointerContext.qualifiedName().packagePath() == null ? Collections.emptyList() : storePointerContext.qualifiedName().packagePath().identifier(), storePointerContext.qualifiedName().identifier()); + DomainParserGrammar.StoreDataContext dataContext = PureGrammarParserUtility.validateAndExtractRequiredField(storeDataContext.storeData(), "data", storeTestData.sourceInformation); + storeTestData.data = HelperEmbeddedDataGrammarParser.parseEmbeddedData(dataContext.embeddedData(), this.walkerSourceInformation, this.parserContext.getPureGrammarParserExtensions()); + return storeTestData; + } + + private org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest visitFunctionTest(DomainParserGrammar.FunctionTestBlockContext ctx) + { + org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest functionTest = new org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest(); + functionTest.sourceInformation = this.walkerSourceInformation.getSourceInformation(ctx); + functionTest.id = PureGrammarParserUtility.fromIdentifier(ctx.identifier()); + + // parameters value + DomainParserGrammar.FunctionTestParametersContext testParameterContext = PureGrammarParserUtility.validateAndExtractOptionalField(ctx.functionTestParameters(), "parameters", functionTest.sourceInformation); + if (testParameterContext != null) + { + functionTest.parameters = ListIterate.collect(testParameterContext.functionTestParameter(), this::visitFunctionTestParameter); + } + DomainParserGrammar.FunctionTestAssertsContext testAssertsContext = PureGrammarParserUtility.validateAndExtractRequiredField(ctx.functionTestAsserts(), "asserts", functionTest.sourceInformation); + functionTest.assertions = ListIterate.collect(testAssertsContext.functionTestAssert(), this::visitFunctionTestAsserts); + return functionTest; + } + + private TestAssertion visitFunctionTestAsserts(DomainParserGrammar.FunctionTestAssertContext ctx) + { + TestAssertion testAssertion = HelperTestAssertionGrammarParser.parseTestAssertion(ctx.testAssertion(), this.walkerSourceInformation, this.parserContext.getPureGrammarParserExtensions()); + testAssertion.id = PureGrammarParserUtility.fromIdentifier(ctx.identifier()); + return testAssertion; + } + + private ParameterValue visitFunctionTestParameter(DomainParserGrammar.FunctionTestParameterContext ctx) + { + ParameterValue parameterValue = new ParameterValue(); + parameterValue.name = PureGrammarParserUtility.fromIdentifier(ctx.identifier()); + parameterValue.value = this.visitTestParameter(ctx.primitiveValue()); + return parameterValue; + } + + private ValueSpecification visitTestParameter(DomainParserGrammar.PrimitiveValueContext primitiveValueContext) + { + DomainParser parser = new DomainParser(); + int startLine = primitiveValueContext.getStart().getLine(); + int lineOffset = walkerSourceInformation.getLineOffset() + startLine - 1; + int columnOffset = (startLine == 1 ? walkerSourceInformation.getColumnOffset() : 0) + primitiveValueContext.getStart().getCharPositionInLine(); + ParseTreeWalkerSourceInformation serviceParamSourceInformation = new ParseTreeWalkerSourceInformation.Builder(walkerSourceInformation.getSourceId(), lineOffset, columnOffset).build(); + String expectedValue = primitiveValueContext.start.getInputStream().getText(Interval.of(primitiveValueContext.start.getStartIndex(), primitiveValueContext.stop.getStopIndex())); + ValueSpecification valueSpecification = parser.parsePrimitiveValue(expectedValue, serviceParamSourceInformation, null); + return valueSpecification; + } // ----------------------------------------------- MEASURE ----------------------------------------------- diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/DEPRECATED_PureGrammarComposerCore.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/DEPRECATED_PureGrammarComposerCore.java index 29619dc633c..66e4faf98ef 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/DEPRECATED_PureGrammarComposerCore.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/DEPRECATED_PureGrammarComposerCore.java @@ -106,6 +106,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import static org.finos.legend.engine.language.pure.grammar.to.HelperDomainGrammarComposer.renderFunctionTestSuites; import static org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposerUtility.convertString; import static org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposerUtility.getTabSize; import static org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposerUtility.getTabString; @@ -395,7 +396,8 @@ public String visit(Function function) + ": " + function.returnType + "[" + HelperDomainGrammarComposer.renderMultiplicity(function.returnMultiplicity) + "]\n" + "{\n" + LazyIterate.collect(function.body, b -> " " + b.accept(Builder.newInstance(this).withIndentation(getTabSize(1)).build())).makeString(";\n") + (function.body.size() > 1 ? ";" : "") + - "\n}"; + "\n}" + + renderFunctionTestSuites(function, toContext()); } diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/HelperDomainGrammarComposer.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/HelperDomainGrammarComposer.java index cfbe4a9757c..d8b2310a789 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/HelperDomainGrammarComposer.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/main/java/org/finos/legend/engine/language/pure/grammar/to/HelperDomainGrammarComposer.java @@ -15,15 +15,23 @@ package org.finos.legend.engine.language.pure.grammar.to; import org.eclipse.collections.impl.utility.LazyIterate; +import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.language.pure.grammar.to.data.HelperEmbeddedDataGrammarComposer; +import org.finos.legend.engine.language.pure.grammar.to.test.assertion.HelperTestAssertionGrammarComposer; import org.finos.legend.engine.protocol.pure.v1.model.context.EngineErrorType; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.AggregationKind; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Constraint; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Function; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Multiplicity; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.ParameterValue; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Property; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.QualifiedProperty; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.StereotypePtr; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.TaggedValue; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Unit; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.StoreTestData; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.Variable; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.Lambda; import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException; @@ -116,10 +124,10 @@ public static String renderDerivedProperty(QualifiedProperty qualifiedProperty, + LazyIterate.collect(functionParameters, p -> p.accept(DEPRECATED_PureGrammarComposerCore.Builder.newInstance(transformer).withVariableInFunctionSignature().build())).makeString(",") + ") {" + (qualifiedProperty.body.size() <= 1 - ? LazyIterate.collect(qualifiedProperty.body, b -> b.accept(transformer)).makeString("\n") - : LazyIterate - .collect(qualifiedProperty.body, b -> b.accept(transformer)) - .makeString("\n" + getTabString(2),";\n" + getTabString(2),";\n" + getTabString())) + ? LazyIterate.collect(qualifiedProperty.body, b -> b.accept(transformer)).makeString("\n") + : LazyIterate + .collect(qualifiedProperty.body, b -> b.accept(transformer)) + .makeString("\n" + getTabString(2), ";\n" + getTabString(2), ";\n" + getTabString())) + "}: " + qualifiedProperty.returnType + "[" + renderMultiplicity(qualifiedProperty.returnMultiplicity) + "]"; } @@ -162,4 +170,99 @@ public static String renderConstraint(Constraint constraint, List al return builder.toString(); } } + + + public static String renderFunctionTestSuites(Function function, PureGrammarComposerContext context) + { + StringBuilder stringBuilder = new StringBuilder(); + if (function.tests == null) + { + return stringBuilder.toString(); + } + stringBuilder.append("\n[\n"); + stringBuilder.append(String.join(",\n", ListIterate.collect(function.tests, suite -> renderFunctionTestSuite(suite, context)))).append("\n"); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + public static String renderFunctionTestSuite(FunctionTestSuite functionTestSuite, PureGrammarComposerContext context) + { + int baseIndentation = 1; + StringBuilder str = new StringBuilder(); + str.append(getTabString(baseIndentation)).append(functionTestSuite.id).append(":\n"); + str.append(getTabString(baseIndentation)).append("{\n"); + if (functionTestSuite.testData != null) + { + str.append(getTabString(baseIndentation + 1)).append("data").append(":\n"); + str.append(getTabString(baseIndentation + 1)).append("[\n"); + str.append(String.join(",\n", ListIterate.collect(functionTestSuite.testData, storeData -> renderFunctionTestData(storeData, baseIndentation + 2, context)))).append("\n"); + str.append(getTabString(baseIndentation + 1)).append("];\n"); + } + // tests + if (functionTestSuite.tests != null) + { + str.append(getTabString(baseIndentation + 1)).append("tests").append(":\n"); + str.append(getTabString(baseIndentation + 1)).append("[\n"); + str.append(String.join(",\n", ListIterate.collect(functionTestSuite.tests, test -> renderFunctionTest((FunctionTest) test, baseIndentation + 2, context)))).append("\n"); + str.append(getTabString(baseIndentation + 1)).append("]\n"); + } + str.append(getTabString(baseIndentation)).append("}"); + return str.toString(); + } + + public static String renderFunctionTestData(StoreTestData storeTestData, int currentInt, PureGrammarComposerContext context) + { + StringBuilder dataStrBuilder = new StringBuilder(); + dataStrBuilder.append(getTabString(currentInt)).append("{\n"); + dataStrBuilder.append(getTabString(currentInt + 1)).append("store").append(": ").append(storeTestData.store).append(";\n"); + dataStrBuilder.append(getTabString(currentInt + 1)).append("data").append(":\n"); + dataStrBuilder.append(HelperEmbeddedDataGrammarComposer.composeEmbeddedData(storeTestData.data, PureGrammarComposerContext.Builder.newInstance(context).withIndentationString(getTabString(currentInt + 2)).build())); + dataStrBuilder.append(";\n"); + dataStrBuilder.append(getTabString(currentInt)).append("}"); + return dataStrBuilder.toString(); + } + + + public static String renderFunctionTest(FunctionTest functionTest, int baseInd, PureGrammarComposerContext context) + { + StringBuilder str = new StringBuilder(); + str.append(getTabString(baseInd)).append(functionTest.id).append(":\n"); + str.append(getTabString(baseInd)).append("{\n"); + + if (functionTest.doc != null) + { + str.append(getTabString(baseInd + 1)).append("doc: ").append(convertString(functionTest.doc, true) + ";\n"); + } + // Parameters + if (functionTest.parameters != null && !functionTest.parameters.isEmpty()) + { + str.append(getTabString(baseInd + 1)).append("parameters:\n"); + str.append(getTabString(baseInd + 1)).append("[\n"); + str.append(String.join(",\n", ListIterate.collect(functionTest.parameters, param -> renderFunctionTestParam(param, baseInd + 2, context)))).append("\n"); + str.append(getTabString(baseInd + 1)).append("]\n"); + } + // Assertions + if (functionTest.assertions != null && !functionTest.assertions.isEmpty()) + { + str.append(getTabString(baseInd + 1)).append("asserts:\n"); + str.append(getTabString(baseInd + 1)).append("[\n"); + str.append(String.join(",\n", ListIterate.collect(functionTest.assertions, testAssertion -> HelperTestAssertionGrammarComposer.composeTestAssertion(testAssertion, PureGrammarComposerContext.Builder.newInstance(context).withIndentationString(getTabString(baseInd + 2)).build())))).append("\n"); + str.append(getTabString(baseInd + 1)).append("]\n"); + } + str.append(getTabString(baseInd)).append("}"); + return str.toString(); + } + + + private static String renderFunctionTestParam(ParameterValue parameterValue, int baseIndentation, PureGrammarComposerContext context) + { + StringBuilder str = new StringBuilder(); + + str.append(getTabString(baseIndentation)).append(parameterValue.name); + str.append(" = "); + str.append(parameterValue.value.accept(DEPRECATED_PureGrammarComposerCore.Builder.newInstance(context).build())); + + return str.toString(); + } + } diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/roundtrip/TestDomainGrammarRoundtrip.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/roundtrip/TestDomainGrammarRoundtrip.java index 77a39ee2778..0b779006f67 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/roundtrip/TestDomainGrammarRoundtrip.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-language-pure-grammar/src/test/java/org/finos/legend/engine/language/pure/grammar/test/roundtrip/TestDomainGrammarRoundtrip.java @@ -487,6 +487,136 @@ public void testFunction() "}\n"); } + @Test + public void testFunctionTest() + { + test("function model::Simple(): String[1]\n" + + "{\n" + + " 'Hello ' + ' World!'\n" + + "}\n" + + "[\n" + + " testSuite_1:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " test_1:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '{}';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n\n" + + "function model::Simple2(): String[1]\n" + + "{\n" + + " 'Hello ' + ' World!'\n" + + "}\n" + + "[\n" + + " testSuite_1:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " test_1:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '{}';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n"); + test("function model::P(): String[1]\n" + + "{\n" + + " 'x'\n" + + "}\n" + + "[\n" + + " testSuite_1:\n" + + " {\n" + + " data:\n" + + " [\n" + + " {\n" + + " store: store::TestDB;\n" + + " data:\n" + + " Reference\n" + + " #{\n" + + " testServiceStoreTestSuites::TestData\n" + + " }#;\n" + + " }\n" + + " ];\n" + + " tests:\n" + + " [\n" + + " test_1:\n" + + " {\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualToJson\n" + + " #{\n" + + " expected:\n" + + " ExternalFormat\n" + + " #{\n" + + " contentType: 'application/json';\n" + + " data: '[]';\n" + + " }#;\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n" + ); + + test("function model::Hello(name: String[1]): String[1]\n" + + "{\n" + + " 'Hello! My name is ' + $name + '.'\n" + + "}\n" + + "[\n" + + " testSuite_1:\n" + + " {\n" + + " tests:\n" + + " [\n" + + " testFail:\n" + + " {\n" + + " parameters:\n" + + " [\n" + + " name = 'John'\n" + + " ]\n" + + " asserts:\n" + + " [\n" + + " assertion_1:\n" + + " EqualTo\n" + + " #{\n" + + " expected:\n" + + " 'Hello! My name is John.';\n" + + " }#\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "]\n"); + } + @Test public void testDecimalWithScale() { diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/CorePureProtocolExtension.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/CorePureProtocolExtension.java index c2c8f984e6f..1a7015ca98d 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/CorePureProtocolExtension.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/CorePureProtocolExtension.java @@ -45,6 +45,8 @@ import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Unit; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.externalFormat.Binding; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.externalFormat.ExternalFormatSchemaSet; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.Mapping; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.mappingTest.MappingTest; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.mapping.mappingTest.MappingTestSuite; @@ -73,6 +75,7 @@ public class CorePureProtocolExtension implements PureProtocolExtension { public static final String MAPPING_CLASSIFIER_PATH = "meta::pure::mapping::Mapping"; + public static final String FUNCTION_CLASSIFIER_PATH = "meta::pure::metamodel::function::ConcreteFunctionDefinition"; @Override public List>>> getExtraProtocolSubTypeInfoCollectors() @@ -136,9 +139,11 @@ public List>>> getExtraProtocolSubTypeInfo .build(), ProtocolSubTypeInfo.newBuilder(TestSuite.class) .withSubtype(MappingTestSuite.class, "mappingTestSuite") + .withSubtype(FunctionTestSuite.class, "functionTestSuite") .build(), ProtocolSubTypeInfo.newBuilder(Test.class) .withSubtype(MappingTest.class, "mappingTest") + .withSubtype(FunctionTest.class, "functionTest") .build() )); } @@ -151,7 +156,7 @@ public Map, String> getExtraProtoc .withKeyValue(Class.class, "meta::pure::metamodel::type::Class") .withKeyValue(Enumeration.class, "meta::pure::metamodel::type::Enumeration") .withKeyValue(Mapping.class, MAPPING_CLASSIFIER_PATH) - .withKeyValue(Function.class, "meta::pure::metamodel::function::ConcreteFunctionDefinition") + .withKeyValue(Function.class, FUNCTION_CLASSIFIER_PATH) .withKeyValue(Measure.class, "meta::pure::metamodel::type::Measure") .withKeyValue(PackageableConnection.class, "meta::pure::runtime::PackageableConnection") .withKeyValue(PackageableRuntime.class, "meta::pure::runtime::PackageableRuntime") diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/domain/Function.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/domain/Function.java index c1999d7f512..c209080b29b 100644 --- a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/domain/Function.java +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/domain/Function.java @@ -16,6 +16,7 @@ import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.PackageableElement; import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.PackageableElementVisitor; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.ValueSpecification; import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.Variable; @@ -32,6 +33,7 @@ public class Function extends PackageableElement public List stereotypes = Collections.emptyList(); public List taggedValues = Collections.emptyList(); public List body = Collections.emptyList(); + public List tests; @Override public T accept(PackageableElementVisitor visitor) diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/ConnectionTestData.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/ConnectionTestData.java new file mode 100644 index 00000000000..a0f5d4bc4e6 --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/ConnectionTestData.java @@ -0,0 +1,25 @@ +// Copyright 2023 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. + +package org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function; + +import org.finos.legend.engine.protocol.pure.v1.model.SourceInformation; +import org.finos.legend.engine.protocol.pure.v1.model.data.EmbeddedData; + +public class ConnectionTestData +{ + public String id; + public EmbeddedData data; + public SourceInformation sourceInformation; +} diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/FunctionTest.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/FunctionTest.java new file mode 100644 index 00000000000..f5e61d8ba54 --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/FunctionTest.java @@ -0,0 +1,26 @@ + +// Copyright 2023 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. + +package org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function; + +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.ParameterValue; +import org.finos.legend.engine.protocol.pure.v1.model.test.AtomicTest; + +import java.util.List; + +public class FunctionTest extends AtomicTest +{ + public List parameters; +} diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/FunctionTestSuite.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/FunctionTestSuite.java new file mode 100644 index 00000000000..a86eb0b0e80 --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/FunctionTestSuite.java @@ -0,0 +1,29 @@ +// Copyright 2023 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. + +package org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function; + +import org.finos.legend.engine.protocol.pure.v1.model.SourceInformation; +import org.finos.legend.engine.protocol.pure.v1.model.test.TestSuite; + +import java.util.List; + +public class FunctionTestSuite extends TestSuite +{ + public List connectionsTestData; + + public List testData; + + public SourceInformation sourceInformation; +} diff --git a/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/StoreTestData.java b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/StoreTestData.java new file mode 100644 index 00000000000..73f7119c59b --- /dev/null +++ b/legend-engine-core/legend-engine-core-language-pure/legend-engine-protocol-pure/src/main/java/org/finos/legend/engine/protocol/pure/v1/model/packageableElement/function/StoreTestData.java @@ -0,0 +1,34 @@ +// Copyright 2023 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. + +package org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function; + +import org.finos.legend.engine.protocol.pure.v1.model.SourceInformation; +import org.finos.legend.engine.protocol.pure.v1.model.data.EmbeddedData; + +/** Use to mock data in function using a runtime for execution + * store represents the store you want to mock data for + * This assume the function uses 1 (or none) runtime + * We will resolve the connection used for the store + * In the future, this could be extended to add runtime pointer if more than one runtime + * and/or one connection but for now the expectation is one store can be mocked + */ + +public class StoreTestData +{ + public String doc; + public String store; + public EmbeddedData data; + public SourceInformation sourceInformation; +} diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/pom.xml b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/pom.xml new file mode 100644 index 00000000000..d3c0ea726fb --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/pom.xml @@ -0,0 +1,175 @@ + + + + + + org.finos.legend.engine + legend-engine-core-test + 4.36.1-SNAPSHOT + + 4.0.0 + + legend-engine-test-runner-function + Legend Engine - Test Runner - Function + + + + + org.finos.legend.pure + legend-pure-m3-core + + + org.finos.legend.pure + legend-pure-m2-dsl-mapping-pure + + + + + org.finos.legend.engine + legend-engine-pure-code-compiled-core + + + org.finos.legend.engine + legend-engine-testable + + + org.finos.legend.engine + legend-engine-pure-platform-dsl-mapping-java + + + org.finos.legend.engine + legend-engine-pure-code-core-extension + + + org.finos.legend.engine + legend-engine-shared-core + + + org.finos.legend.engine + legend-engine-language-pure-compiler + + + org.finos.legend.engine + legend-engine-protocol-pure + + + org.finos.legend.engine + legend-engine-language-pure-grammar + + + org.finos.legend.engine + legend-engine-executionPlan-generation + + + org.bouncycastle + * + + + + + org.finos.legend.engine + legend-engine-executionPlan-generation + + + org.finos.legend.engine + legend-engine-executionPlan-generation + + + org.finos.legend.engine + legend-engine-executionPlan-execution + + + + + org.finos.legend.engine + legend-engine-xt-relationalStore-protocol + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-grammar + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-executionPlan + test + + + org.finos.legend.engine + legend-engine-xt-relationalStore-javaPlatformBinding-pure + test + + + org.finos.legend.engine + legend-engine-xt-json-pure + test + + + org.finos.legend.engine + legend-engine-xt-json-javaPlatformBinding-pure + test + + + org.finos.legend.engine + legend-engine-xt-serviceStore-pure + test + + + org.finos.legend.engine + legend-engine-xt-serviceStore-javaPlatformBinding-pure + test + + + org.finos.legend.engine + legend-engine-configuration + test + + + net.javacrumbs.json-unit + json-unit + test + + + junit + junit + test + + + + + + org.slf4j + slf4j-api + + + + + + org.eclipse.collections + eclipse-collections-api + + + org.eclipse.collections + eclipse-collections + + + + + \ No newline at end of file diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestRunner.java b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestRunner.java new file mode 100644 index 00000000000..a7b66ae85af --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestRunner.java @@ -0,0 +1,289 @@ +// Copyright 2023 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. + + +package org.finos.legend.engine.testable.function.extension; + +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.Maps; +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.tuple.Pair; +import org.eclipse.collections.impl.tuple.Tuples; +import org.eclipse.collections.impl.utility.ListIterate; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.ConnectionFirstPassBuilder; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.plan.execution.PlanExecutor; +import org.finos.legend.engine.plan.execution.planHelper.PrimitiveValueSpecificationToObjectVisitor; +import org.finos.legend.engine.plan.execution.result.Result; +import org.finos.legend.engine.plan.execution.result.serialization.SerializationFormat; +import org.finos.legend.engine.plan.generation.PlanGenerator; +import org.finos.legend.engine.plan.generation.extension.PlanGeneratorExtension; +import org.finos.legend.engine.plan.generation.transformers.PlanTransformer; +import org.finos.legend.engine.plan.platform.PlanPlatform; +import org.finos.legend.engine.protocol.pure.v1.extension.ConnectionFactoryExtension; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.protocol.pure.v1.model.data.EmbeddedData; +import org.finos.legend.engine.protocol.pure.v1.model.data.EmbeddedDataHelper; +import org.finos.legend.engine.protocol.pure.v1.model.executionPlan.SingleExecutionPlan; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.Connection; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Function; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.ParameterValue; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTest; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.StoreTestData; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.Store; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.modelToModel.ModelStore; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.TestAssertion; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.status.AssertionStatus; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestError; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestExecuted; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestResult; +import org.finos.legend.engine.pure.code.core.PureCoreExtensionLoader; +import org.finos.legend.engine.shared.core.operational.Assert; +import org.finos.legend.engine.testable.assertion.TestAssertionEvaluator; +import org.finos.legend.engine.testable.extension.TestRunner; +import org.finos.legend.pure.generated.Root_meta_core_runtime_Connection; +import org.finos.legend.pure.generated.Root_meta_core_runtime_ConnectionStore; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTestSuite; +import org.finos.legend.pure.generated.Root_meta_pure_extension_Extension; +import org.finos.legend.pure.generated.Root_meta_pure_test_AtomicTest; +import org.finos.legend.pure.generated.Root_meta_pure_test_TestSuite; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +import static org.finos.legend.engine.language.pure.compiler.toPureGraph.HelperModelBuilder.getElementFullPath; + +public class FunctionTestRunner implements TestRunner +{ + private static final Logger LOGGER = LoggerFactory.getLogger(FunctionTestRunner.class); + private final ConcreteFunctionDefinition functionDefinition; + private final MutableList extensions; + private final PlanExecutor executor; + private final String pureVersion; + private final MutableList factories = org.eclipse.collections.api.factory.Lists.mutable.withAll(ServiceLoader.load(ConnectionFactoryExtension.class)); + private List closeables = Lists.mutable.empty(); + private List> storeConnectionsPairs = Lists.mutable.empty(); + + public FunctionTestRunner(ConcreteFunctionDefinition functionDefinition, String pureVersion) + { + this.pureVersion = pureVersion; + this.functionDefinition = functionDefinition; + this.executor = PlanExecutor.newPlanExecutorBuilder().withAvailableStoreExecutors().build(); + this.extensions = Lists.mutable.withAll(ServiceLoader.load(PlanGeneratorExtension.class)); + } + + @Override + public TestResult executeAtomicTest(Root_meta_pure_test_AtomicTest atomicTest, PureModel pureModel, PureModelContextData data) + { + throw new UnsupportedOperationException("Function Test should be executed in context of Mapping Test Suite only"); + } + + @Override + public List executeTestSuite(Root_meta_pure_test_TestSuite testSuite, List atomicTestIds, PureModel pureModel, PureModelContextData pureModelContextData) + { + List results = Lists.mutable.empty(); + RichIterable routerExtensions = PureCoreExtensionLoader.extensions().flatCollect(e -> e.extraPureCoreExtensions(pureModel.getExecutionSupport())); + MutableList planTransformers = extensions.flatCollect(PlanGeneratorExtension::getExtraPlanTransformers); + Assert.assertTrue(testSuite instanceof Root_meta_legend_function_metamodel_FunctionTestSuite, () -> "Function test suite expected in functions"); + Root_meta_legend_function_metamodel_FunctionTestSuite functionTestSuite = (Root_meta_legend_function_metamodel_FunctionTestSuite) testSuite; + String testablePath = getElementFullPath(this.functionDefinition, pureModel.getExecutionSupport()); + try + { + // handle data + Function protocolFunc = ListIterate.detect(pureModelContextData.getElementsOfType(Function.class), el -> el.getPath().equals(testablePath)); + FunctionTestSuite protocolSuite = ListIterate.detect(protocolFunc.tests, t -> t.id.equals(testSuite._id())); + FunctionTestRunnerContext runnerContext = new FunctionTestRunnerContext(Tuples.pair(pureModelContextData, pureModel), Tuples.pair(protocolSuite, functionTestSuite), extensions.flatCollect(PlanGeneratorExtension::getExtraPlanTransformers), + new ConnectionFirstPassBuilder(pureModel.getContext()), + PureCoreExtensionLoader.extensions().flatCollect(e -> e.extraPureCoreExtensions(pureModel.getExecutionSupport()))); + List functionTests = protocolSuite.tests.stream().filter(t -> t instanceof FunctionTest) + .map(t -> (FunctionTest) t).filter(t -> atomicTestIds.contains(t.id)).collect(Collectors.toList()); + // setup + this.setup(runnerContext); + // run tests + for (FunctionTest functionTest: functionTests) + { + TestResult functionTestResult = executeFunctionTest(functionTest, runnerContext); + functionTestResult.testable = testablePath; + functionTestResult.testSuiteId = functionTestSuite._id(); + results.add(functionTestResult); + } + // + this.tearDown(); + } + catch (Exception e) + { + // this is to catch any error for the setup of the test suite. we return test error for each test run + for (Root_meta_pure_test_AtomicTest testedError: functionTestSuite._tests()) + { + if (atomicTestIds.contains(testedError._id()) && results.stream().noneMatch(t -> t.atomicTestId.equals(testedError._id()))) + { + TestError testError = new TestError(); + testError.atomicTestId = testedError._id(); + testError.error = e.toString(); + results.add(testError); + } + } + } + return results; + } + + private TestResult executeFunctionTest(FunctionTest functionTest, FunctionTestRunnerContext context) + { + try + { + // build plan + SingleExecutionPlan executionPlan = PlanGenerator.generateExecutionPlan(this.functionDefinition, null, null, null, context.getPureModel(), this.pureVersion, PlanPlatform.JAVA, null, context.getRouterExtensions(), context.getExecutionPlanTransformers()); + // execute assertion + TestAssertion assertion = functionTest.assertions.get(0); + // add execute + PlanExecutor.ExecuteArgsBuilder executeArgs = context.getExecuteBuilder().withPlan(executionPlan); + Map parameters = Maps.mutable.empty(); + if (functionTest.parameters != null) + { + for (ParameterValue parameterValue : functionTest.parameters) + { + parameters.put(parameterValue.name, parameterValue.value.accept(new PrimitiveValueSpecificationToObjectVisitor())); + } + } + executeArgs.withParams(parameters); + Result result = this.executor.executeWithArgs(executeArgs.build()); + AssertionStatus assertionResult = assertion.accept(new TestAssertionEvaluator(result, SerializationFormat.RAW)); + TestExecuted testResult = new TestExecuted(Collections.singletonList(assertionResult)); + testResult.atomicTestId = functionTest.id; + return testResult; + } + catch (Exception error) + { + TestError testError = new TestError(); + testError.atomicTestId = functionTest.id; + testError.error = error.toString(); + return testError; + } + } + + private void setup(FunctionTestRunnerContext context) + { + Root_meta_legend_function_metamodel_FunctionTestSuite functionTestSuite = context.getTestSuite(); + FunctionTestSuite protocolFunctionSuite = context.getProtocolSuite(); + if (functionTestSuite._testData() == null || functionTestSuite._testData().isEmpty()) + { + return; + } + if (protocolFunctionSuite.testData == null || protocolFunctionSuite.testData.isEmpty()) + { + return; + } + org.finos.legend.pure.generated.Root_meta_core_runtime_Runtime runtime = getRuntimesInFunction(context.getPureModel()); + if (runtime == null) + { + return; + } + runtime._connectionStores().forEach(connectionStore -> + { + // find connections that have been mocked and replace + Object element = connectionStore._element(); + if (element instanceof org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store) + { + org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store store = (org.finos.legend.pure.m3.coreinstance.meta.pure.store.Store) element; + String storePath = getElementFullPath(store, context.getPureModel().getExecutionSupport()); + Optional optionalStoreTestData = protocolFunctionSuite.testData.stream().filter(pTestData -> pTestData.store.equals(storePath)).findFirst(); + if (optionalStoreTestData.isPresent()) + { + StoreTestData storeTestData = optionalStoreTestData.get(); + EmbeddedData testData = EmbeddedDataHelper.resolveEmbeddedDataInPMCD(context.getPureModelContextData(), storeTestData.data); + Pair> closeableMockedConnections = this.factories.collect(f -> f.tryBuildTestConnectionsForStore(context.getDataElementIndex(), resolveStore(context.getPureModelContextData(), storePath), testData)).select(Objects::nonNull).select(Optional::isPresent) + .collect(Optional::get).getFirstOptional().orElseThrow(() -> new UnsupportedOperationException("Unsupported store type for: '" + storePath + "' mentioned while running the function tests")); + Connection mockedConnection = closeableMockedConnections.getOne(); + Root_meta_core_runtime_Connection mockedCompileConnection = mockedConnection.accept(context.getConnectionVisitor()); + // we replace with mocked connection. We set back to original at cleanup + Root_meta_core_runtime_Connection realConnection = connectionStore._connection(); + this.storeConnectionsPairs.add(Tuples.pair(connectionStore, realConnection)); + connectionStore._connection(mockedCompileConnection); + this.closeables.addAll(closeableMockedConnections.getTwo()); + } + else + { + LOGGER.warn("No test data found store + " + storePath + ". When building test data for runtime"); + } + } + }); + } + + private void tearDown() + { + if (this.closeables != null) + { + this.closeables.forEach(closeable -> + { + try + { + closeable.close(); + } + catch (IOException e) + { + LOGGER.warn("Exception occurred closing closeable resource" + e); + } + }); + } + // restore original connection value + if (this.storeConnectionsPairs != null) + { + this.storeConnectionsPairs.forEach(storeConnectionsPairs -> + { + storeConnectionsPairs.getOne()._connection(storeConnectionsPairs.getTwo()); + }); + } + } + + private Store resolveStore(PureModelContextData pureModelContextData, String store) + { + if (store.equals("ModelStore")) + { + return new ModelStore(); + } + else + { + return ListIterate.detect(pureModelContextData.getElementsOfType(Store.class), x -> x.getPath().equals(store)); + } + } + + public org.finos.legend.pure.generated.Root_meta_core_runtime_Runtime getRuntimesInFunction(PureModel pureModel) + { + RichIterable runtimes = org.finos.legend.pure.generated.core_pure_corefunctions_metaExtension.Root_meta_pure_functions_meta_extractRuntimesFromFunctionDefinition_FunctionDefinition_1__Runtime_MANY_(this.functionDefinition, pureModel.getExecutionSupport()); + if (runtimes.isEmpty()) + { + return null; + } + else if (runtimes.size() == 1) + { + return runtimes.getOnly(); + } + else + { + throw new UnsupportedOperationException("Currently cannot test functions with more than one runtime present"); + } + } +} diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestRunnerContext.java b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestRunnerContext.java new file mode 100644 index 00000000000..2cf3f875c1d --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestRunnerContext.java @@ -0,0 +1,115 @@ +// Copyright 2022 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. + +package org.finos.legend.engine.testable.function.extension; + +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.api.factory.Maps; +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.tuple.Pair; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.plan.execution.PlanExecutor; +import org.finos.legend.engine.plan.generation.transformers.PlanTransformer; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.ConnectionVisitor; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.data.DataElement; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.function.FunctionTestSuite; +import org.finos.legend.pure.generated.Root_meta_core_runtime_Connection; +import org.finos.legend.pure.generated.Root_meta_legend_function_metamodel_FunctionTestSuite; +import org.finos.legend.pure.generated.Root_meta_pure_extension_Extension; + +import java.util.Map; + +public class FunctionTestRunnerContext +{ + private final Pair models; + private final Pair suites; + private final Map dataElementIndex; + + // execution + private final MutableList executionPlanTransformers; + private final ConnectionVisitor connectionVisitor; + private final RichIterable routerExtensions; + private final PlanExecutor.ExecuteArgsBuilder executeBuilder; + + public FunctionTestRunnerContext(Pair models, Pair suites, + MutableList executionPlanTransformers, + ConnectionVisitor connectionVisitor, RichIterable routerExtensions + ) + { + this.models = models; + this.suites = suites; + this.executionPlanTransformers = executionPlanTransformers; + this.connectionVisitor = connectionVisitor; + this.routerExtensions = routerExtensions; + this.executeBuilder = PlanExecutor.withArgs(); + this.dataElementIndex = this.buildDataElementIndex(models.getOne()); + + } + + private Map buildDataElementIndex(PureModelContextData pureModelContextData) + { + Map result = Maps.mutable.empty(); + pureModelContextData.getElementsOfType(DataElement.class).forEach(d -> result.put(d.getPath(), d)); + return result; + } + + public PureModel getPureModel() + { + return models.getTwo(); + } + + public PureModelContextData getPureModelContextData() + { + return models.getOne(); + } + + public Root_meta_legend_function_metamodel_FunctionTestSuite getTestSuite() + { + return this.suites.getTwo(); + } + + public FunctionTestSuite getProtocolSuite() + { + return this.suites.getOne(); + } + + public Map getDataElementIndex() + { + return dataElementIndex; + } + + + public PlanExecutor.ExecuteArgsBuilder getExecuteBuilder() + { + return executeBuilder; + } + + + public ConnectionVisitor getConnectionVisitor() + { + return connectionVisitor; + } + + public RichIterable getRouterExtensions() + { + return routerExtensions; + } + + public MutableList getExecutionPlanTransformers() + { + return executionPlanTransformers; + } + +} diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestableRunnerExtension.java b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestableRunnerExtension.java new file mode 100644 index 00000000000..d1c86b0250d --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/java/org/finos/legend/engine/testable/function/extension/FunctionTestableRunnerExtension.java @@ -0,0 +1,45 @@ +// Copyright 2023 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. + +package org.finos.legend.engine.testable.function.extension; + +import org.finos.legend.engine.protocol.pure.PureClientVersions; +import org.finos.legend.engine.protocol.pure.v1.CorePureProtocolExtension; +import org.finos.legend.engine.testable.extension.TestRunner; +import org.finos.legend.engine.testable.extension.TestableRunnerExtension; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition; +import org.finos.legend.pure.m3.coreinstance.meta.pure.test.Testable; + +public class FunctionTestableRunnerExtension implements TestableRunnerExtension +{ + + private String pureVersion = PureClientVersions.production; + + @Override + public String getSupportedClassifierPath() + { + return CorePureProtocolExtension.FUNCTION_CLASSIFIER_PATH; + } + + @Override + public TestRunner getTestRunner(Testable testable) + { + if (testable instanceof org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition) + { + return new FunctionTestRunner((ConcreteFunctionDefinition) testable, pureVersion); + } + return null; + } + +} diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/resources/META-INF/services/org.finos.legend.engine.testable.extension.TestableRunnerExtension b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/resources/META-INF/services/org.finos.legend.engine.testable.extension.TestableRunnerExtension new file mode 100644 index 00000000000..5f077d2799c --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/main/resources/META-INF/services/org.finos.legend.engine.testable.extension.TestableRunnerExtension @@ -0,0 +1 @@ +org.finos.legend.engine.testable.function.extension.FunctionTestableRunnerExtension \ No newline at end of file diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/java/org/finos/legend/engine/testable/function/TestFunctionTestSuite.java b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/java/org/finos/legend/engine/testable/function/TestFunctionTestSuite.java new file mode 100644 index 00000000000..ab8835cfd7a --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/java/org/finos/legend/engine/testable/function/TestFunctionTestSuite.java @@ -0,0 +1,189 @@ +// Copyright 2023 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. + +package org.finos.legend.engine.testable.function; + +import net.javacrumbs.jsonunit.JsonAssert; +import org.finos.legend.engine.language.pure.compiler.Compiler; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.language.pure.grammar.from.PureGrammarParser; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.status.AssertFail; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.status.AssertionStatus; +import org.finos.legend.engine.protocol.pure.v1.model.test.assertion.status.EqualToJsonAssertFail; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestExecuted; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestExecutionStatus; +import org.finos.legend.engine.protocol.pure.v1.model.test.result.TestResult; +import org.finos.legend.engine.shared.core.deployment.DeploymentMode; +import org.finos.legend.engine.testable.extension.TestRunner; +import org.finos.legend.engine.testable.function.extension.FunctionTestableRunnerExtension; +import org.finos.legend.pure.generated.Root_meta_pure_test_TestSuite; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition; +import org.finos.legend.pure.m3.coreinstance.meta.pure.test.TestAccessor; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.net.URL; +import java.util.List; + +public class TestFunctionTestSuite +{ + + @Test + public void testFunctionTest() + { + List inlineServiceStoreTestResults = executeFunctionTest("legend-testable-function-test-model-relational.pure", "model::PersonQuery__TabularDataSet_1_"); + Assert.assertEquals(1, inlineServiceStoreTestResults.size()); + Assert.assertTrue(inlineServiceStoreTestResults.get(0) instanceof TestExecuted); + TestExecuted testExecuted = (TestExecuted) inlineServiceStoreTestResults.get(0); + Assert.assertEquals(TestExecutionStatus.PASS, testExecuted.testExecutionStatus); + } + + @Test + public void testFunctionPrimitiveValue() + { + List testResults = executeFunctionTest("legend-testable-function-test-model.pure", "model::Hello_String_1__String_1_"); + Assert.assertEquals(2, testResults.size()); + Assert.assertTrue(hasTestPassed(findTestById(testResults, "testPass"))); + // TODO: assert failure for equalTo should return expected value as separate value in class + String message = "expected:Hello World! My name is Johnx., Found : Hello World! My name is John."; + TestResult failedResult = findTestById(testResults, "testFail"); + Assert.assertTrue(failedResult instanceof TestExecuted); + TestExecuted executed = (TestExecuted) failedResult; + Assert.assertEquals(TestExecutionStatus.FAIL, executed.testExecutionStatus); + Assert.assertEquals(1, executed.assertStatuses.size()); + AssertionStatus assertionStatus = executed.assertStatuses.get(0); + Assert.assertTrue(assertionStatus instanceof AssertFail); + AssertFail fail = (AssertFail) assertionStatus; + Assert.assertEquals(message, fail.message); + + } + + @Test + @Ignore + // TODO: fix test + public void testFunctionTestM2M() + { + List inlineServiceStoreTestResults = executeFunctionTest("legend-testable-function-test-model-m2m.pure", "model::PersonQuery__String_1_"); + Assert.assertEquals(1, inlineServiceStoreTestResults.size()); + Assert.assertTrue(inlineServiceStoreTestResults.get(0) instanceof TestExecuted); + TestExecuted testExecuted = (TestExecuted) inlineServiceStoreTestResults.get(0); + Assert.assertEquals(TestExecutionStatus.PASS, testExecuted.testExecutionStatus); + } + + @Test + public void testFunctionTestWithParameters() + { + List testResults = executeFunctionTest("legend-testable-function-test-model-relational.pure", "model::PersonWithParams_String_1__TabularDataSet_1_"); + Assert.assertEquals(2, testResults.size()); + Assert.assertTrue(hasTestPassed(findTestById(testResults, "testPass"))); + String expected = "[]"; + String actual = "[ {\n" + + " \"First Name\" : \"Nicole\",\n" + + " \"Last Name\" : \"Smith\"\n" + + "} ]"; + testFailingTest(findTestById(testResults, "testFail"), expected, actual); + } + + @Test + public void testFunctionTestWithM2M() + { + List inlineServiceStoreTestResults = executeFunctionTest("legend-testable-function-test-model-relational.pure", "model::PersonQuery__TabularDataSet_1_"); + Assert.assertEquals(1, inlineServiceStoreTestResults.size()); + Assert.assertTrue(inlineServiceStoreTestResults.get(0) instanceof TestExecuted); + TestExecuted testExecuted = (TestExecuted) inlineServiceStoreTestResults.get(0); + Assert.assertEquals(TestExecutionStatus.PASS, testExecuted.testExecutionStatus); + } + + private List executeFunctionTest(String grammar, String fullPath) + { + FunctionTestableRunnerExtension functionTestableRunnerExtension = new FunctionTestableRunnerExtension(); + String pureModelString = getResourceAsString("testable/" + grammar); + PureModelContextData pureModelContextData = PureGrammarParser.newInstance().parseModel(pureModelString); + PureModel pureModel = Compiler.compile(pureModelContextData, DeploymentMode.TEST, null); + org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.PackageableElement element = pureModel.getPackageableElement(fullPath); + Assert.assertTrue(element instanceof org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.function.ConcreteFunctionDefinition); + ConcreteFunctionDefinition functionMetamodel = (ConcreteFunctionDefinition) element; + TestRunner testRunner = functionTestableRunnerExtension.getTestRunner(functionMetamodel); + Assert.assertNotNull("Unable to get function test runner from testable extension", testRunner); + return functionMetamodel._tests().flatCollect(testSuite -> + { + List atomicTestIds = ((Root_meta_pure_test_TestSuite) testSuite)._tests().collect(TestAccessor::_id).toList(); + return testRunner.executeTestSuite((Root_meta_pure_test_TestSuite) testSuite, atomicTestIds, pureModel, pureModelContextData); + }).toList(); + } + + private void testFailingTest(TestResult testResult, String expected, String actual) + { + TestExecuted testExecuted = guaranteedTestExecuted(testResult); + Assert.assertEquals(TestExecutionStatus.FAIL, testExecuted.testExecutionStatus); + AssertionStatus status = testExecuted.assertStatuses.get(0); + if (status instanceof EqualToJsonAssertFail) + { + EqualToJsonAssertFail equalToJsonAssertFail = (EqualToJsonAssertFail)status; + JsonAssert.assertJsonEquals(expected, equalToJsonAssertFail.expected); + JsonAssert.assertJsonEquals(actual, equalToJsonAssertFail.actual); + } + else + { + throw new RuntimeException("Test Assertion" + status.id + " expected to fail"); + } + } + + private TestExecuted guaranteedTestExecuted(TestResult result) + { + if (result instanceof TestExecuted) + { + return (TestExecuted) result; + } + throw new RuntimeException("test expected to have been executed"); + } + + + private boolean hasTestPassed(TestResult result) + { + if (result instanceof TestExecuted) + { + return ((TestExecuted) result).testExecutionStatus.equals(TestExecutionStatus.PASS); + } + return false; + } + + private TestResult findTestById(List results, String id) + { + return results.stream().filter(test -> test.atomicTestId.equals(id)).findFirst().orElseThrow(() -> new RuntimeException("Test Id " + id + " not found")); + } + + private String getResourceAsString(String path) + { + try + { + URL infoURL = TestFunctionTestSuite.class.getClassLoader().getResource(path); + if (infoURL != null) + { + java.util.Scanner scanner = new java.util.Scanner(infoURL.openStream()).useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : null; + } + return null; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + + +} diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model-m2m.pure b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model-m2m.pure new file mode 100644 index 00000000000..13fa6d37371 --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model-m2m.pure @@ -0,0 +1,99 @@ +Class model::Person +{ + firstName: String[1]; + lastName: String[1]; +} + +Class model::DifferentPerson +{ + fullName: String[1]; +} + +function model::PersonQuery(): String[1] +{ + model::DifferentPerson.all()->graphFetch( + #{ + model::DifferentPerson{ + fullName + } + }# + )->serialize( + #{ + model::DifferentPerson{ + fullName + } + }# + )->from( + model::M2MMapping, + model::MyRuntime + ) +} +[ + testSuite_1: + { + data: + [ + { + store: ModelStore; + data: ExternalFormat + #{ + contentType: 'application/json'; + data: '{\n "firstName": "John",\n "lastName": "Doe"\n}'; + }#; + } + ]; + tests: + [ + test_1: + { + asserts: + [ + assertion_1: + EqualToJson + #{ + expected: + ExternalFormat + #{ + contentType: 'application/json'; + data: '{\n "fullName" : "John Doe"\n}'; + }#; + }# + ] + } + ] + } + ] + +###Mapping +Mapping model::M2MMapping +( + *model::DifferentPerson: Pure + { + ~src model::Person + fullName: $src.firstName + ' ' + $src.lastName + } +) + + +###Runtime +Runtime model::MyRuntime +{ + mappings: + [ + model::M2MMapping + ]; + connections: + [ + ModelStore: + [ + connection_1: + #{ + JsonModelConnection + { + class: model::Person; + url: 'executor:default'; + } + }# + ] + ]; +} diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model-relational.pure b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model-relational.pure new file mode 100644 index 00000000000..ccef9eb6e85 --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model-relational.pure @@ -0,0 +1,263 @@ +###Data +Data data::RelationalData +{ + Relational + #{ + default.PersonTable: + 'id,firm_id,firstName,lastName,employeeType\n' + + '1,1,John,Doe,FTO\n' + + '2,1,Nicole,Smith,FTC\n' + + '3,2,Time,Smith,FTE\n'; + + default.FirmTable: + 'id,legal_name\n' + + '1,Finos\n' + + '2,Apple'; + + }# +} + +###Relational +Database store::TestDB +( + Table FirmTable + ( + id INTEGER PRIMARY KEY, + legal_name VARCHAR(200) + ) + Table PersonTable + ( + id INTEGER PRIMARY KEY, + firm_id INTEGER, + firstName VARCHAR(200), + lastName VARCHAR(200), + employeeType VARCHAR(200) + ) + + Join FirmPerson(PersonTable.firm_id = FirmTable.id) +) + + +###Pure +Class model::Person +{ + firstName: String[1]; + lastName: String[1]; + employeeType: model::EmployeeType[1]; +} + +Enum model::EmployeeType +{ + CONTRACT, + FULL_TIME +} + +Class model::Firm +{ + legalName: String[1]; + employees: model::Person[*]; +} + + +###Mapping +Mapping execution::RelationalMapping +( + *model::Person: Relational + { + ~primaryKey + ( + [store::TestDB]PersonTable.id + ) + ~mainTable [store::TestDB]PersonTable + firstName: [store::TestDB]PersonTable.firstName, + lastName: [store::TestDB]PersonTable.lastName, + employeeType: EnumerationMapping EmployeeTypeMapping: [store::TestDB]PersonTable.employeeType + } + + model::EmployeeType: EnumerationMapping EmployeeTypeMapping + { + CONTRACT: ['FTC', 'FTO'], + FULL_TIME: 'FTE' + } + + *model::Firm: Relational + { + ~primaryKey + ( + [store::TestDB]FirmTable.id + ) + ~mainTable [store::TestDB]FirmTable + legalName: [store::TestDB]FirmTable.legal_name, + employees[model_Person]: [store::TestDB]@FirmPerson + } +) + + +###Connection +RelationalDatabaseConnection model::MyConnection +{ + store: store::TestDB; + type: H2; + specification: LocalH2 + { + testDataSetupSqls: [ + 'Drop table if exists FirmTable;\nDrop table if exists PersonTable;\nCreate Table FirmTable(id INT, Legal_Name VARCHAR(200));\nCreate Table PersonTable(id INT, firm_id INT, lastName VARCHAR(200), firstName VARCHAR(200), employeeType VARCHAR(200));\nInsert into FirmTable (id, Legal_Name) values (1, \'FirmA\');\nInsert into FirmTable (id, Legal_Name) values (2, \'Apple\');\nInsert into PersonTable (id, firm_id, lastName, firstName, employeeType) values (1, 1, \'John\', \'Doe\', \'FTC\');\nInsert into PersonTable (id, firm_id, lastName, firstName, employeeType) values (2, 2, \'Tim\', \'Smith\', \'FTE\');\nInsert into PersonTable (id, firm_id, lastName, firstName, employeeType) values (3, 3, \'Nicole\', \'Doe\', \'FTO\');\n\n' + ]; + }; + auth: DefaultH2; +} + +###Runtime +Runtime execution::Runtime +{ + mappings: + [ + execution::RelationalMapping + ]; + connections: + [ + store::TestDB: + [ + connection_1: model::MyConnection + ] + ]; +} + +###Pure +function model::PersonQuery(): meta::pure::tds::TabularDataSet[1] +{ + model::Person.all()->project( + [ + x|$x.firstName, + x|$x.lastName + ], + [ + 'First Name', + 'Last Name' + ] + )->from( + execution::RelationalMapping, + execution::Runtime + ) +} +[ + testSuite_1: + { + data: + [ + { + store: store::TestDB; + data: + Relational + #{ + default.PersonTable: + 'id,firm_id,firstName,lastName,employeeType\n'+ + '1,1,I\'m John,"Doe, Jr",FTO\n'+ + '2,1,Nicole,Smith,FTC\n'+ + '3,2,Time,Smith,FTE\n'; + }#; + } + ]; + tests: + [ + test_1: + { + asserts: + [ + assertion_1: + EqualToJson + #{ + expected: + ExternalFormat + #{ + contentType: 'application/json'; + data: '[ {\n "First Name" : "I\'m John",\n "Last Name" : "Doe, Jr"\n}, {\n "First Name" : "Nicole",\n "Last Name" : "Smith"\n}, {\n "First Name" : "Time",\n "Last Name" : "Smith"\n} ]'; + }#; + }# + ] + } + ] + } +] +function model::PersonWithParams(firstName: String[1]): meta::pure::tds::TabularDataSet[1] +{ + model::Person.all()->filter( + x|$x.firstName == + $firstName + )->project( + [ + x|$x.firstName, + x|$x.lastName + ], + [ + 'First Name', + 'Last Name' + ] + )->from( + execution::RelationalMapping, + execution::Runtime + ) +} +[ + testSuite_1: + { + data: + [ + { + store: store::TestDB; + data: + Relational + #{ + default.PersonTable: + 'id,firm_id,firstName,lastName,employeeType\n'+ + '1,1,I\'m John,"Doe, Jr",FTO\n'+ + '2,1,Nicole,Smith,FTC\n'+ + '3,2,Time,Smith,FTE\n'; + }#; + } + ]; + tests: + [ + testFail: + { + parameters: + [ + firstName = 'Nicole' + ] + asserts: + [ + assertion_1: + EqualToJson + #{ + expected: + ExternalFormat + #{ + contentType: 'application/json'; + data: '[]'; + }#; + }# + ] + }, + testPass: + { + parameters: + [ + firstName = 'Nicole' + ] + asserts: + [ + assertion_1: + EqualToJson + #{ + expected: + ExternalFormat + #{ + contentType: 'application/json'; + data: '[ {\n "First Name" : "Nicole",\n "Last Name" : "Smith"\n} ]'; + }#; + }# + ] + } + ] + } +] \ No newline at end of file diff --git a/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model.pure b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model.pure new file mode 100644 index 00000000000..0a4c0c639ac --- /dev/null +++ b/legend-engine-core/legend-engine-core-test/legend-engine-test-runner-function/src/test/resources/testable/legend-testable-function-test-model.pure @@ -0,0 +1,43 @@ +function model::Hello(name: String[1]): String[1] +{ + 'Hello World! My name is ' + $name + '.'; +} +[ + testSuite_1: + { + tests: + [ + testPass: + { + parameters: + [ + name = 'John' + ] + asserts: + [ + assertion_1: + EqualTo + #{ + expected: 'Hello World! My name is John.'; + }# + ] + }, + testFail: + { + parameters: + [ + name = 'John' + ] + asserts: + [ + assertion_1: + EqualTo + #{ + expected: 'Hello World! My name is Johnx.'; + }# + ] + } + + ] + } +] diff --git a/legend-engine-core/legend-engine-core-test/pom.xml b/legend-engine-core/legend-engine-core-test/pom.xml index 08521304baf..8a9811feadd 100644 --- a/legend-engine-core/legend-engine-core-test/pom.xml +++ b/legend-engine-core/legend-engine-core-test/pom.xml @@ -28,6 +28,7 @@ legend-engine-test-runner-mapping + legend-engine-test-runner-function legend-engine-test-runner-shared legend-engine-test-server-shared legend-engine-test-data-generation diff --git a/legend-engine-pure/legend-engine-pure-code/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/function/metamodel.pure b/legend-engine-pure/legend-engine-pure-code/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/function/metamodel.pure new file mode 100644 index 00000000000..87cc1afff74 --- /dev/null +++ b/legend-engine-pure/legend-engine-pure-code/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/function/metamodel.pure @@ -0,0 +1,127 @@ +// Copyright 2021 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 meta::legend::function::metamodel::*; +import meta::pure::data::*; + +Class meta::legend::function::metamodel::FunctionTestSuite extends meta::pure::test::TestSuite +{ + connectionsTestData : ConnectionTestData[*]; + testData: meta::legend::function::metamodel::StoreTestData[*]; +} + +Class meta::legend::function::metamodel::StoreTestData +{ + doc: String[0..1]; + store: meta::pure::store::Store[1]; + data: EmbeddedData[1]; +} + +Class meta::legend::function::metamodel::FunctionTest extends meta::pure::test::AtomicTest +{ + parameters : ParameterValue[*]; +} + +Class meta::legend::function::metamodel::ParameterValue +{ + name : String[1]; + value : Any[*]; +} + +Class {doc.doc = 'Do not use for now'} meta::legend::function::metamodel::ConnectionTestData +{ + doc: String[0..1]; + connectionId: String[1]; + testData: EmbeddedData[1]; +} + +###Diagram +Diagram meta::legend::function::metamodel::FunctionTestDiagram(width=0.0, height=0.0) +{ + TypeView cview_2( + type=meta::legend::function::metamodel::FunctionTestSuite, + position=(440.00000, 157.00000), + width=256.73633, + height=58.00000, + stereotypesVisible=true, + attributesVisible=true, + attributeStereotypesVisible=true, + attributeTypesVisible=true, + color=#FFFFCC, + lineWidth=1.0) + + TypeView cview_5( + type=meta::legend::function::metamodel::ParameterValue, + position=(1071.05127, 282.00000), + width=110.05859, + height=58.00000, + stereotypesVisible=true, + attributesVisible=true, + attributeStereotypesVisible=true, + attributeTypesVisible=true, + color=#FFFFCC, + lineWidth=1.0) + + TypeView cview_1( + type=meta::legend::function::metamodel::FunctionTest, + position=(1035.00000, 190.00000), + width=182.03516, + height=58.00000, + stereotypesVisible=true, + attributesVisible=true, + attributeStereotypesVisible=true, + attributeTypesVisible=true, + color=#FFFFCC, + lineWidth=1.0) + + TypeView cview_6( + type=meta::legend::function::metamodel::StoreTestData, + position=(792.01758, 174.00000), + width=168.42969, + height=86.00000, + stereotypesVisible=true, + attributesVisible=true, + attributeStereotypesVisible=true, + attributeTypesVisible=true, + color=#FFFFCC, + lineWidth=1.0) + + PropertyView pview_0( + property=meta::legend::function::metamodel::FunctionTest.parameters, + source=cview_1, + target=cview_5, + points=[(1126.01758,219.00000),(1126.08057,311.00000)], + label='', + propertyPosition=(0.0,0.0), + multiplicityPosition=(0.0,0.0), + color=#000000, + lineWidth=-1.0, + stereotypesVisible=true, + nameVisible=true, + lineStyle=SIMPLE) + + PropertyView pview_1( + property=meta::legend::function::metamodel::FunctionTestSuite.testData, + source=cview_2, + target=cview_6, + points=[(568.36816,186.00000),(876.23242,217.00000)], + label='', + propertyPosition=(0.0,0.0), + multiplicityPosition=(0.0,0.0), + color=#000000, + lineWidth=-1.0, + stereotypesVisible=true, + nameVisible=true, + lineStyle=SIMPLE) +} \ No newline at end of file diff --git a/legend-engine-pure/legend-engine-pure-code/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/test/diagram.pure b/legend-engine-pure/legend-engine-pure-code/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/test/diagram.pure index cb8a45c34a1..f0f48f0b1c0 100644 --- a/legend-engine-pure/legend-engine-pure-code/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/test/diagram.pure +++ b/legend-engine-pure/legend-engine-pure-code/legend-engine-pure-code-compiled-core/src/main/resources/core/pure/test/diagram.pure @@ -1,18 +1,6 @@ ###Diagram Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) { - TypeView cview_5( - type=meta::pure::test::assertion::TestAssertion, - position=(1506.48096, 346.00000), - width=99.13086, - height=58.00000, - stereotypesVisible=true, - attributesVisible=true, - attributeStereotypesVisible=true, - attributeTypesVisible=true, - color=#FFFFCC, - lineWidth=1.0) - TypeView cview_7( type=meta::pure::test::assertion::EqualToJson, position=(1577.48096, 486.00000), @@ -52,8 +40,8 @@ Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) TypeView cview_21( type=meta::pure::test::AtomicTest, position=(1243.17143, 353.09795), - width=97.38477, - height=44.00000, + width=105.56689, + height=58.00000, stereotypesVisible=true, attributesVisible=true, attributeStereotypesVisible=true, @@ -64,8 +52,8 @@ Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) TypeView cview_20( type=meta::pure::test::TestSuite, position=(1012.17143, 356.20356), - width=97.38477, - height=44.00000, + width=105.56689, + height=58.00000, stereotypesVisible=true, attributesVisible=true, attributeStereotypesVisible=true, @@ -181,10 +169,22 @@ Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) color=#FFFFCC, lineWidth=1.0) + TypeView cview_5( + type=meta::pure::test::assertion::TestAssertion, + position=(1505.23958, 355.93103), + width=99.13086, + height=58.00000, + stereotypesVisible=true, + attributesVisible=true, + attributeStereotypesVisible=true, + attributeTypesVisible=true, + color=#FFFFCC, + lineWidth=1.0) + GeneralizationView gview_0( source=cview_6, target=cview_5, - points=[(1460.71899,509.00000),(1556.04639,375.00000)], + points=[(1460.71899,509.00000),(1554.80501,384.93103)], label='', color=#000000, lineWidth=-1.0, @@ -193,7 +193,7 @@ Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) GeneralizationView gview_1( source=cview_7, target=cview_5, - points=[(1662.36475,508.00000),(1556.04639,375.00000)], + points=[(1662.36475,508.00000),(1554.80501,384.93103)], label='', color=#000000, lineWidth=-1.0, @@ -229,7 +229,7 @@ Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) GeneralizationView gview_5( source=cview_20, target=cview_2, - points=[(1060.86381,378.20356),(1187.69483,238.00000)], + points=[(1064.95487,385.20356),(1187.69483,238.00000)], label='', color=#000000, lineWidth=-1.0, @@ -238,7 +238,7 @@ Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) GeneralizationView gview_6( source=cview_21, target=cview_2, - points=[(1291.86381,375.09795),(1187.69483,238.00000)], + points=[(1295.95487,382.09795),(1187.69483,238.00000)], label='', color=#000000, lineWidth=-1.0, @@ -294,7 +294,7 @@ Diagram meta::pure::test::MetaModelDiagram(width=0.0, height=0.0) property=meta::pure::test::AtomicTest.assertions, source=cview_21, target=cview_5, - points=[(1291.86381,375.09795),(1556.04639,375.00000)], + points=[(1295.95487,382.09795),(1554.80501,384.93103)], label='', propertyPosition=(0.0,0.0), multiplicityPosition=(0.0,0.0), diff --git a/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/main/java/org/finos/legend/engine/language/pure/dsl/service/compiler/toPureGraph/ServiceCompilerExtensionImpl.java b/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/main/java/org/finos/legend/engine/language/pure/dsl/service/compiler/toPureGraph/ServiceCompilerExtensionImpl.java index b8489548c9b..8050a6fb75f 100644 --- a/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/main/java/org/finos/legend/engine/language/pure/dsl/service/compiler/toPureGraph/ServiceCompilerExtensionImpl.java +++ b/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/main/java/org/finos/legend/engine/language/pure/dsl/service/compiler/toPureGraph/ServiceCompilerExtensionImpl.java @@ -231,7 +231,7 @@ public List testIds = ListIterate.collect(serviceTestSuite.tests, t -> t.id); @@ -270,7 +270,7 @@ else if (test instanceof ServiceTest) pureServiceTest._keysAddAll(Lists.mutable.withAll(serviceTest.keys)); if (serviceTest.assertions == null || serviceTest.assertions.isEmpty()) { - throw new EngineException("Service Tests should have atleast 1 assert", serviceTest.sourceInformation, EngineErrorType.COMPILATION); + throw new EngineException("Service Tests should have at least 1 assert", serviceTest.sourceInformation, EngineErrorType.COMPILATION); } List assertionIds = ListIterate.collect(serviceTest.assertions, a -> a.id); diff --git a/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/test/java/org/finos/legend/engine/language/pure/compiler/test/TestServiceCompilationFromGrammar.java b/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/test/java/org/finos/legend/engine/language/pure/compiler/test/TestServiceCompilationFromGrammar.java index 8888eacbd7d..a363310e76e 100644 --- a/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/test/java/org/finos/legend/engine/language/pure/compiler/test/TestServiceCompilationFromGrammar.java +++ b/legend-engine-xts-service/legend-engine-language-pure-dsl-service/src/test/java/org/finos/legend/engine/language/pure/compiler/test/TestServiceCompilationFromGrammar.java @@ -2226,7 +2226,7 @@ public void testServiceTestSuiteCompilationErrorMessages() " }\n" + " ]\n" + "}\n", - "COMPILATION error at [106:5-123:5]: Service TestSuites should have atleast 1 test" + "COMPILATION error at [106:5-123:5]: Service TestSuites should have at least 1 test" ); //Test Single TestSuite without asserts @@ -2275,7 +2275,7 @@ public void testServiceTestSuiteCompilationErrorMessages() " }\n" + " ]\n" + "}\n", - "COMPILATION error at [122:9-127:9]: Service Tests should have atleast 1 assert" + "COMPILATION error at [122:9-127:9]: Service Tests should have at least 1 assert" ); //Multiple TestSuites with same ids diff --git a/pom.xml b/pom.xml index b6e40ef94f7..5a8514bda04 100644 --- a/pom.xml +++ b/pom.xml @@ -1810,6 +1810,11 @@ legend-engine-test-runner-mapping ${project.version} + + org.finos.legend.engine + legend-engine-test-runner-function + ${project.version} + org.finos.legend.engine legend-engine-service-post-validation-runner